Objective-C中self与super关键字的本质区别
一、内存结构与对象本质
在Objective - C中,理解对象的内存结构是掌握self
与super
关键字本质区别的基础。
每个Objective - C对象在内存中都由一个结构体表示,这个结构体至少包含两个成员:isa
指针和其他实例变量。isa
指针指向对象所属的类,通过这个指针,对象可以找到其对应的类的元数据,包括类的方法列表、属性列表等。
例如,定义一个简单的类Person
:
#import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (void)introduce;
@end
@implementation Person
- (void)introduce {
NSLog(@"I'm %@, %ld years old.", self.name, (long)self.age);
}
@end
当我们创建Person
类的实例时:
Person *person = [[Person alloc] init];
person.name = @"John";
person.age = 30;
[person introduce];
person
对象在内存中会有一个结构体,isa
指针指向Person
类,并且结构体中会有用于存储name
和age
的空间。
二、消息传递机制
Objective - C是基于消息传递的语言。当我们调用一个对象的方法,比如[person introduce]
,实际上是向person
对象发送了一条introduce
消息。
消息传递的过程如下:
- 首先,根据对象的
isa
指针找到对象所属的类。 - 在类的方法列表中查找与消息对应的方法实现。如果在本类中没有找到,就沿着继承链向父类查找,直到找到方法实现或者到达根类
NSObject
。 - 找到方法实现后,执行该方法。
三、self关键字
- self的含义
self
代表当前对象,也就是正在执行方法的那个对象。在对象的方法内部,self
指向调用该方法的对象实例。
继续以Person
类为例,在introduce
方法中:
- (void)introduce {
NSLog(@"I'm %@, %ld years old.", self.name, (long)self.age);
}
这里的self
就是调用introduce
方法的Person
对象实例。如果我们有多个Person
对象,每个对象在调用introduce
方法时,方法内部的self
都指向该对象自身。
- self与方法调用
当通过
self
调用方法时,消息传递机制按照正常的流程进行。它从当前对象的isa
指针指向的类开始,在类的方法列表中查找方法实现。
例如,我们在Person
类中添加一个新方法updateAge
:
- (void)updateAge {
self.age++;
[self introduce];
}
在updateAge
方法中,[self introduce]
这条语句会向当前self
所指向的Person
对象发送introduce
消息。消息传递首先从Person
类的方法列表中查找introduce
方法的实现,然后执行该实现。
- self的内存地址
self
的内存地址就是对象实例在内存中的地址。我们可以通过以下代码验证:
Person *person = [[Person alloc] init];
person.name = @"John";
person.age = 30;
NSLog(@"Person object address: %p", person);
[person updateAge];
@implementation Person
- (void)updateAge {
NSLog(@"self address in updateAge: %p", self);
self.age++;
[self introduce];
}
- (void)introduce {
NSLog(@"self address in introduce: %p", self);
NSLog(@"I'm %@, %ld years old.", self.name, (long)self.age);
}
@end
运行上述代码,会发现person
对象的地址、updateAge
方法中self
的地址以及introduce
方法中self
的地址是相同的。这进一步证明了self
就是指向当前正在执行方法的对象实例。
四、super关键字
- super的含义
super
并不是一个对象,它是一个编译器指令。super
用于告诉编译器,当查找方法实现时,跳过当前类的方法列表,直接从父类的方法列表开始查找。
假设我们有一个Student
类继承自Person
类:
@interface Student : Person
@property (nonatomic, copy) NSString *school;
- (void)study;
@end
@implementation Student
- (void)study {
NSLog(@"%@ is studying at %@.", self.name, self.school);
}
- (void)introduce {
[super introduce];
NSLog(@"I'm a student at %@.", self.school);
}
@end
在Student
类的introduce
方法中,[super introduce]
表示从Person
类(Student
类的父类)的方法列表中查找introduce
方法的实现并执行。
- super与方法调用
使用
super
调用方法时,虽然是从父类的方法列表中查找方法实现,但方法执行时的self
依然是当前对象。也就是说,在父类方法执行过程中,如果访问self
,这个self
还是指向调用super
方法的那个子类对象实例。
在上面的Student
类的introduce
方法中,[super introduce]
执行的是Person
类的introduce
方法。在Person
类的introduce
方法内部,self
依然指向Student
对象实例。所以当Person
类的introduce
方法访问self.name
和self.age
时,实际上访问的是Student
对象的name
和age
属性。
- super的实现原理
在编译器层面,当遇到
super
关键字时,编译器会生成特殊的代码来改变方法查找的顺序。它不会从当前类的方法列表开始查找,而是直接从父类的方法列表开始。
从底层实现角度看,每个类在内存中有一个指向其父类的指针。编译器利用这个指针找到父类,然后在父类的方法列表中查找方法实现。这就实现了跳过当前类,从父类查找方法的功能。
五、self与super的本质区别
-
方法查找起点不同
- self:从当前对象的
isa
指针指向的类开始查找方法实现。如果当前类没有找到,就沿着继承链向上查找,直到找到方法实现或者到达根类NSObject
。 - super:直接从当前类的父类开始查找方法实现。它跳过了当前类的方法列表,直接在父类的方法列表中查找。
- self:从当前对象的
-
对当前类的认知不同
- self:代表当前对象,完全基于当前类的视角。在方法调用过程中,它以当前类为起点,按照正常的继承体系查找方法。
- super:是一个编译器指令,它让编译器改变方法查找的策略,从父类的角度去查找方法,但执行方法时依然基于当前对象(
self
不变)。
-
内存相关差异
- self:
self
就是当前对象实例,它的内存地址就是对象在内存中的地址。 - super:
super
本身不涉及内存地址,它只是影响方法查找的方向,不代表任何具体的内存实体。
- self:
-
应用场景不同
- self:通常用于在当前对象内部调用方法、访问属性等操作,以实现对象自身的功能。比如在
Person
类的updateAge
方法中,[self introduce]
使用self
来调用自身的introduce
方法,以实现更新年龄后展示最新信息。 - super:主要用于在子类中调用父类的方法实现,以便在子类方法中扩展或修改父类的行为。例如在
Student
类的introduce
方法中,[super introduce]
先调用父类Person
的introduce
方法展示基本信息,然后子类再添加自己特有的信息展示。
- self:通常用于在当前对象内部调用方法、访问属性等操作,以实现对象自身的功能。比如在
六、示例代码分析
- 简单继承关系下的self与super
#import <Foundation/Foundation.h>
@interface Animal : NSObject
- (void)makeSound;
@end
@implementation Animal
- (void)makeSound {
NSLog(@"Animal makes a sound.");
}
@end
@interface Dog : Animal
- (void)makeSound {
[super makeSound];
NSLog(@"Dog barks.");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Dog *dog = [[Dog alloc] init];
[dog makeSound];
}
return 0;
}
在上述代码中,Dog
类继承自Animal
类。Dog
类重写了makeSound
方法。在Dog
类的makeSound
方法中,首先使用super
调用了父类Animal
的makeSound
方法,然后输出自己特有的声音信息。
这里[super makeSound]
从Animal
类的方法列表中查找makeSound
方法并执行,而self
在整个过程中始终指向Dog
对象实例。如果我们在Animal
类的makeSound
方法中访问self
,实际上访问的是Dog
对象,这样就保证了在父类方法执行过程中能够正确访问子类对象的状态。
- 多层继承关系下的self与super
#import <Foundation/Foundation.h>
@interface Shape : NSObject
- (void)draw;
@end
@implementation Shape
- (void)draw {
NSLog(@"Drawing a shape.");
}
@end
@interface Rectangle : Shape
- (void)draw {
[super draw];
NSLog(@"Drawing a rectangle.");
}
@end
@interface FilledRectangle : Rectangle
- (void)draw {
[super draw];
NSLog(@"Filling the rectangle.");
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
FilledRectangle *filledRectangle = [[FilledRectangle alloc] init];
[filledRectangle draw];
}
return 0;
}
在这个多层继承的例子中,FilledRectangle
类继承自Rectangle
类,Rectangle
类又继承自Shape
类。每个子类都重写了draw
方法,并在方法中使用super
调用父类的draw
方法。
在FilledRectangle
类的draw
方法中,[super draw]
会调用Rectangle
类的draw
方法。而Rectangle
类的draw
方法中又有[super draw]
,会继续调用Shape
类的draw
方法。在这个过程中,self
始终指向FilledRectangle
对象实例。这体现了super
在多层继承关系中如何从父类逐步查找和执行方法,同时self
保持一致性,使得每个方法都能正确访问当前对象的状态。
- self与super在属性访问中的差异
#import <Foundation/Foundation.h>
@interface BaseClass : NSObject
@property (nonatomic, copy) NSString *baseProperty;
- (void)printProperty;
@end
@implementation BaseClass
- (void)printProperty {
NSLog(@"Base property: %@", self.baseProperty);
}
@end
@interface SubClass : BaseClass
@property (nonatomic, copy) NSString *subProperty;
- (void)printSubProperty;
@end
@implementation SubClass
- (void)printSubProperty {
NSLog(@"Sub property: %@", self.subProperty);
[super printProperty];
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
SubClass *subClass = [[SubClass alloc] init];
subClass.baseProperty = @"Base value";
subClass.subProperty = @"Sub value";
[subClass printSubProperty];
}
return 0;
}
在这个例子中,SubClass
继承自BaseClass
。SubClass
的printSubProperty
方法首先输出自己的subProperty
,然后通过super
调用BaseClass
的printProperty
方法输出baseProperty
。
这里要注意,在BaseClass
的printProperty
方法中,self
指向SubClass
对象实例,所以能够正确访问subClass
对象设置的baseProperty
值。这表明在通过super
调用父类方法时,self
的指向不会改变,保证了属性访问的一致性。
七、常见误区与陷阱
-
认为super是父类对象 这是一个常见的误区。
super
不是一个对象,它不代表父类对象。它只是一个编译器指令,用于改变方法查找的顺序。比如在Dog
类的makeSound
方法中,[super makeSound]
并不是向一个父类Animal
对象发送消息,而是从Animal
类的方法列表中查找makeSound
方法并执行,执行过程中self
依然是Dog
对象实例。 -
在父类方法中误用self导致递归调用 假设我们在
Animal
类的makeSound
方法中不小心写成这样:
@implementation Animal
- (void)makeSound {
[self makeSound];
NSLog(@"Animal makes a sound.");
}
@end
这样就会导致无限递归调用,因为[self makeSound]
会从当前对象(也就是Dog
对象,因为self
指向调用方法的对象)所属的类(Dog
类)开始查找方法,而Dog
类重写了makeSound
方法,又会调用[self makeSound]
,如此循环下去,最终导致程序崩溃。正确的做法是使用super
来调用父类的方法,避免递归调用。
- 在子类方法中对super的错误使用
在子类重写方法中,如果对
super
的使用不当,可能会导致逻辑错误。例如,在Student
类的introduce
方法中,如果写成:
- (void)introduce {
NSLog(@"I'm a student at %@.", self.school);
[super introduce];
}
这样会先输出子类特有的信息,然后再输出父类的信息,可能不符合预期的展示逻辑。通常情况下,我们会先调用[super introduce]
输出父类的基本信息,然后再添加子类特有的信息。
八、总结与最佳实践
-
理解本质是关键 要深入理解
self
与super
的本质区别,需要掌握Objective - C的内存结构、消息传递机制等基础知识。self
代表当前对象,从当前类开始查找方法;super
是编译器指令,从父类开始查找方法,但执行时self
不变。 -
遵循正确的使用场景
- 使用
self
来实现对象自身的功能,调用自身的方法和访问自身的属性。 - 使用
super
在子类中扩展或修改父类的行为,调用父类的方法实现。
- 使用
-
代码审查与测试 在编写代码时,要特别注意
self
与super
的使用。通过代码审查可以发现一些潜在的错误,比如是否在应该使用super
的地方误用了self
,或者相反。同时,编写单元测试来验证self
与super
相关的功能,确保代码的正确性和稳定性。
通过深入理解self
与super
的本质区别,并在实际编程中遵循最佳实践,我们能够更好地利用Objective - C的继承和多态特性,编写出更加健壮和易于维护的代码。