MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Objective-C中self与super关键字的本质区别

2022-11-192.0k 阅读

一、内存结构与对象本质

在Objective - C中,理解对象的内存结构是掌握selfsuper关键字本质区别的基础。

每个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类,并且结构体中会有用于存储nameage的空间。

二、消息传递机制

Objective - C是基于消息传递的语言。当我们调用一个对象的方法,比如[person introduce],实际上是向person对象发送了一条introduce消息。

消息传递的过程如下:

  1. 首先,根据对象的isa指针找到对象所属的类。
  2. 在类的方法列表中查找与消息对应的方法实现。如果在本类中没有找到,就沿着继承链向父类查找,直到找到方法实现或者到达根类NSObject
  3. 找到方法实现后,执行该方法。

三、self关键字

  1. self的含义 self代表当前对象,也就是正在执行方法的那个对象。在对象的方法内部,self指向调用该方法的对象实例。

继续以Person类为例,在introduce方法中:

- (void)introduce {
    NSLog(@"I'm %@, %ld years old.", self.name, (long)self.age);
}

这里的self就是调用introduce方法的Person对象实例。如果我们有多个Person对象,每个对象在调用introduce方法时,方法内部的self都指向该对象自身。

  1. self与方法调用 当通过self调用方法时,消息传递机制按照正常的流程进行。它从当前对象的isa指针指向的类开始,在类的方法列表中查找方法实现。

例如,我们在Person类中添加一个新方法updateAge

- (void)updateAge {
    self.age++;
    [self introduce];
}

updateAge方法中,[self introduce]这条语句会向当前self所指向的Person对象发送introduce消息。消息传递首先从Person类的方法列表中查找introduce方法的实现,然后执行该实现。

  1. 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关键字

  1. 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方法的实现并执行。

  1. super与方法调用 使用super调用方法时,虽然是从父类的方法列表中查找方法实现,但方法执行时的self依然是当前对象。也就是说,在父类方法执行过程中,如果访问self,这个self还是指向调用super方法的那个子类对象实例。

在上面的Student类的introduce方法中,[super introduce]执行的是Person类的introduce方法。在Person类的introduce方法内部,self依然指向Student对象实例。所以当Person类的introduce方法访问self.nameself.age时,实际上访问的是Student对象的nameage属性。

  1. super的实现原理 在编译器层面,当遇到super关键字时,编译器会生成特殊的代码来改变方法查找的顺序。它不会从当前类的方法列表开始查找,而是直接从父类的方法列表开始。

从底层实现角度看,每个类在内存中有一个指向其父类的指针。编译器利用这个指针找到父类,然后在父类的方法列表中查找方法实现。这就实现了跳过当前类,从父类查找方法的功能。

五、self与super的本质区别

  1. 方法查找起点不同

    • self:从当前对象的isa指针指向的类开始查找方法实现。如果当前类没有找到,就沿着继承链向上查找,直到找到方法实现或者到达根类NSObject
    • super:直接从当前类的父类开始查找方法实现。它跳过了当前类的方法列表,直接在父类的方法列表中查找。
  2. 对当前类的认知不同

    • self:代表当前对象,完全基于当前类的视角。在方法调用过程中,它以当前类为起点,按照正常的继承体系查找方法。
    • super:是一个编译器指令,它让编译器改变方法查找的策略,从父类的角度去查找方法,但执行方法时依然基于当前对象(self不变)。
  3. 内存相关差异

    • selfself就是当前对象实例,它的内存地址就是对象在内存中的地址。
    • supersuper本身不涉及内存地址,它只是影响方法查找的方向,不代表任何具体的内存实体。
  4. 应用场景不同

    • self:通常用于在当前对象内部调用方法、访问属性等操作,以实现对象自身的功能。比如在Person类的updateAge方法中,[self introduce]使用self来调用自身的introduce方法,以实现更新年龄后展示最新信息。
    • super:主要用于在子类中调用父类的方法实现,以便在子类方法中扩展或修改父类的行为。例如在Student类的introduce方法中,[super introduce]先调用父类Personintroduce方法展示基本信息,然后子类再添加自己特有的信息展示。

六、示例代码分析

  1. 简单继承关系下的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调用了父类AnimalmakeSound方法,然后输出自己特有的声音信息。

这里[super makeSound]Animal类的方法列表中查找makeSound方法并执行,而self在整个过程中始终指向Dog对象实例。如果我们在Animal类的makeSound方法中访问self,实际上访问的是Dog对象,这样就保证了在父类方法执行过程中能够正确访问子类对象的状态。

  1. 多层继承关系下的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保持一致性,使得每个方法都能正确访问当前对象的状态。

  1. 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继承自BaseClassSubClassprintSubProperty方法首先输出自己的subProperty,然后通过super调用BaseClassprintProperty方法输出baseProperty

这里要注意,在BaseClassprintProperty方法中,self指向SubClass对象实例,所以能够正确访问subClass对象设置的baseProperty值。这表明在通过super调用父类方法时,self的指向不会改变,保证了属性访问的一致性。

七、常见误区与陷阱

  1. 认为super是父类对象 这是一个常见的误区。super不是一个对象,它不代表父类对象。它只是一个编译器指令,用于改变方法查找的顺序。比如在Dog类的makeSound方法中,[super makeSound]并不是向一个父类Animal对象发送消息,而是从Animal类的方法列表中查找makeSound方法并执行,执行过程中self依然是Dog对象实例。

  2. 在父类方法中误用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来调用父类的方法,避免递归调用。

  1. 在子类方法中对super的错误使用 在子类重写方法中,如果对super的使用不当,可能会导致逻辑错误。例如,在Student类的introduce方法中,如果写成:
- (void)introduce {
    NSLog(@"I'm a student at %@.", self.school);
    [super introduce];
}

这样会先输出子类特有的信息,然后再输出父类的信息,可能不符合预期的展示逻辑。通常情况下,我们会先调用[super introduce]输出父类的基本信息,然后再添加子类特有的信息。

八、总结与最佳实践

  1. 理解本质是关键 要深入理解selfsuper的本质区别,需要掌握Objective - C的内存结构、消息传递机制等基础知识。self代表当前对象,从当前类开始查找方法;super是编译器指令,从父类开始查找方法,但执行时self不变。

  2. 遵循正确的使用场景

    • 使用self来实现对象自身的功能,调用自身的方法和访问自身的属性。
    • 使用super在子类中扩展或修改父类的行为,调用父类的方法实现。
  3. 代码审查与测试 在编写代码时,要特别注意selfsuper的使用。通过代码审查可以发现一些潜在的错误,比如是否在应该使用super的地方误用了self,或者相反。同时,编写单元测试来验证selfsuper相关的功能,确保代码的正确性和稳定性。

通过深入理解selfsuper的本质区别,并在实际编程中遵循最佳实践,我们能够更好地利用Objective - C的继承和多态特性,编写出更加健壮和易于维护的代码。