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

Objective-C 属性与方法的使用技巧

2023-07-023.8k 阅读

Objective-C 属性使用技巧

1. 属性声明与修饰符

在 Objective-C 中,属性(property)是一种简化实例变量访问和管理的机制。通过属性,我们可以更方便地封装数据,同时也增强了代码的可读性和维护性。属性的声明语法如下:

@property (attributes) type variableName;
  • 修饰符分类
    • 原子性(Atomicity)nonatomicatomic。默认情况下,属性是 atomic,这意味着编译器会自动生成访问器方法,这些方法是线程安全的。例如:
@property (atomic, strong) NSString *atomicString;

然而,atomic 并不保证整个对象的线程安全,只是保证了属性的读写操作是原子性的。在多线程环境下,如果你对对象有复杂的操作,仍然需要额外的同步机制。nonatomic 则不提供原子性保证,访问器方法的实现更简单,性能更高,适合在单线程环境或对性能要求较高的场景下使用:

@property (nonatomic, strong) NSString *nonatomicString;
  • 内存管理strongweakassigncopy 等。
    • strong:强引用,会增加对象的引用计数。当一个对象被一个 strong 属性引用时,只要这个属性还存在,对象就不会被释放。例如:
@property (nonatomic, strong) NSObject *strongObject;
- `weak`:弱引用,不会增加对象的引用计数。当被引用的对象释放时,指向它的 `weak` 属性会自动被设置为 `nil`,从而避免野指针。常用于解决循环引用问题,比如视图控制器之间的父子关系:
@property (nonatomic, weak) UIViewController *weakViewController;
- `assign`:简单赋值,适用于基本数据类型(如 `int`,`float` 等)。对于对象类型,不推荐使用 `assign`,因为当对象释放时,指向它的 `assign` 属性不会自动置为 `nil`,会导致野指针。
@property (nonatomic, assign) int number;
- `copy`:用于对象的不可变副本创建。当设置属性值时,会创建对象的副本。常用于 `NSString`,`NSArray`,`NSDictionary` 等不可变类型,以确保属性值不会被意外修改。例如:
@property (nonatomic, copy) NSString *copyString;
  • 读写权限readwritereadonlyreadwrite 是默认的,意味着属性同时具有读和写权限,编译器会自动生成 gettersetter 方法。readonly 则只生成 getter 方法,属性只能读不能写,常用于一些内部状态的暴露,例如:
@property (nonatomic, readonly, strong) NSArray *dataArray;

2. 自定义访问器方法

虽然编译器会自动为属性生成访问器方法,但在某些情况下,我们可能需要自定义这些方法来实现更复杂的逻辑。例如,在设置属性值时进行一些额外的操作,或者在获取属性值时进行一些计算。

  • 自定义 setter 方法: 假设我们有一个 Person 类,有一个 age 属性,我们希望在设置年龄时确保年龄在合理范围内。
@interface Person : NSObject
@property (nonatomic, assign) NSInteger age;
@end

@implementation Person
- (void)setAge:(NSInteger)age {
    if (age >= 0 && age <= 120) {
        _age = age;
    } else {
        NSLog(@"Invalid age value");
    }
}
@end

在上述代码中,我们重写了 setAge: 方法,对传入的年龄值进行了检查。注意,在自定义 setter 方法中,我们直接访问实例变量 _age,而不是通过 self.age,这是为了避免递归调用。

  • 自定义 getter 方法: 再比如,我们有一个 Circle 类,有一个 radius 属性,我们希望通过一个属性 area 来获取圆的面积,这个面积是根据半径实时计算的。
@interface Circle : NSObject
@property (nonatomic, assign) CGFloat radius;
@property (nonatomic, readonly) CGFloat area;
@end

@implementation Circle
- (CGFloat)area {
    return M_PI * self.radius * self.radius;
}
@end

在这个例子中,我们自定义了 area 属性的 getter 方法,每次获取 area 属性时,都会根据当前的 radius 值计算圆的面积。

3. 属性与 KVO(Key - Value Observing)

KVO 是一种基于观察者模式的机制,允许我们监听对象属性值的变化。属性在 KVO 中起着重要的作用。

  • 注册 KVO: 首先,在观察对象中注册对另一个对象属性的观察。例如,我们有一个 Car 类,有一个 speed 属性,我们在 ViewController 中观察 speed 的变化。
#import "Car.h"

@interface ViewController ()
@property (nonatomic, strong) Car *car;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.car = [[Car alloc] init];
    [self.car addObserver:self forKeyPath:@"speed" options:NSKeyValueObservingOptionNew context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"speed"]) {
        NSNumber *newSpeed = change[NSKeyValueChangeNewKey];
        NSLog(@"The car's new speed is %@", newSpeed);
    }
}

- (void)dealloc {
    [self.car removeObserver:self forKeyPath:@"speed"];
}
@end

在上述代码中,我们在 viewDidLoad 方法中为 carspeed 属性注册了 KVO,在 observeValueForKeyPath:ofObject:change:context: 方法中处理属性值变化的逻辑。注意,在对象销毁时(dealloc 方法中),要移除 KVO 观察。

  • 触发 KVO: 在被观察对象中,当属性值发生变化时,KVO 机制会自动通知观察者。通常,通过属性的 setter 方法来触发 KVO。例如在 Car 类中:
@interface Car : NSObject
@property (nonatomic, assign) NSInteger speed;
@end

@implementation Car
- (void)setSpeed:(NSInteger)speed {
    _speed = speed;
    [self willChangeValueForKey:@"speed"];
    // 这里可以进行其他操作
    [self didChangeValueForKey:@"speed"];
}
@end

setSpeed: 方法中,我们手动调用 willChangeValueForKey:didChangeValueForKey: 方法,这两个方法会触发 KVO 通知,告知观察者属性值即将改变和已经改变。

Objective-C 方法使用技巧

1. 方法的声明与实现

在 Objective-C 中,方法分为实例方法和类方法。实例方法作用于类的实例对象,而类方法作用于类本身。

  • 实例方法声明与实现: 实例方法的声明在类的接口部分(@interface),实现在类的实现部分(@implementation)。例如,我们有一个 Dog 类,有一个实例方法 bark
@interface Dog : NSObject
- (void)bark;
@end

@implementation Dog
- (void)bark {
    NSLog(@"Woof!");
}
@end

在上述代码中,- (void)bark; 是方法声明,- (void)bark { NSLog(@"Woof!"); } 是方法实现。注意,实例方法以 - 开头。

  • 类方法声明与实现: 类方法的声明和实现类似,不过以 + 开头。例如,我们在 Dog 类中添加一个类方法 createDog 来创建 Dog 对象。
@interface Dog : NSObject
+ (Dog *)createDog;
@end

@implementation Dog
+ (Dog *)createDog {
    return [[Dog alloc] init];
}
@end

这里 + (Dog *)createDog; 是类方法声明,+ (Dog *)createDog { return [[Dog alloc] init]; } 是类方法实现。

2. 方法参数与返回值

Objective-C 的方法可以有多个参数和不同类型的返回值。

  • 方法参数: 方法参数的命名规则比较独特,每个参数都有一个外部名称和一个内部名称(通常相同)。例如,我们有一个 Calculator 类,有一个方法 add:and: 用于两个数相加。
@interface Calculator : NSObject
- (NSInteger)add:(NSInteger)num1 and:(NSInteger)num2;
@end

@implementation Calculator
- (NSInteger)add:(NSInteger)num1 and:(NSInteger)num2 {
    return num1 + num2;
}
@end

在调用这个方法时,我们需要使用完整的参数名称:

Calculator *calculator = [[Calculator alloc] init];
NSInteger result = [calculator add:5 and:3];
NSLog(@"The result is %ld", (long)result);

这种命名方式使得方法调用的意图更加清晰。

  • 返回值: 方法的返回值类型可以是基本数据类型、对象类型,甚至是 void(表示无返回值)。对于对象类型的返回值,要注意内存管理。例如,一个方法返回一个新创建的 NSString 对象:
@interface StringGenerator : NSObject
- (NSString *)generateString;
@end

@implementation StringGenerator
- (NSString *)generateString {
    return [NSString stringWithFormat:@"Generated string"];
}
@end

这里使用 stringWithFormat: 方法创建的 NSString 对象是自动释放的,调用者不需要手动释放。

3. 方法的重载与重写

在 Objective-C 中,没有严格意义上的方法重载(像 C++ 或 Java 那样基于参数列表不同的重载),但可以通过方法名称的不同来实现类似功能。而方法重写是一个重要的特性。

  • 方法重载的替代方式: 虽然不能有完全相同名称但参数列表不同的方法,但我们可以通过不同的方法名称来实现类似功能。例如,在 MathUtils 类中,我们有两个方法用于计算不同类型数据的和。
@interface MathUtils : NSObject
- (NSInteger)sumOfIntegers:(NSInteger)num1 and:(NSInteger)num2;
- (CGFloat)sumOfFloats:(CGFloat)num1 and:(CGFloat)num2;
@end

@implementation MathUtils
- (NSInteger)sumOfIntegers:(NSInteger)num1 and:(NSInteger)num2 {
    return num1 + num2;
}

- (CGFloat)sumOfFloats:(CGFloat)num1 and:(CGFloat)num2 {
    return num1 + num2;
}
@end

这两个方法虽然功能类似,但通过不同的方法名称来区分。

  • 方法重写: 方法重写发生在子类中,子类可以重写父类的方法以提供不同的实现。例如,我们有一个 Animal 类和它的子类 Cat
@interface Animal : NSObject
- (void)makeSound;
@end

@implementation Animal
- (void)makeSound {
    NSLog(@"Some generic animal sound");
}
@end

@interface Cat : Animal
@end

@implementation Cat
- (void)makeSound {
    NSLog(@"Meow");
}
@end

在上述代码中,Cat 类重写了 Animal 类的 makeSound 方法,提供了猫叫的具体实现。当我们创建 Cat 对象并调用 makeSound 方法时,会执行 Cat 类中重写的方法。

4. 方法的动态派发

Objective-C 是一种动态语言,方法的调用在运行时才确定具体执行的代码,这就是方法的动态派发。

  • 动态派发原理: 当向一个对象发送消息(调用方法)时,运行时系统会首先在对象的类的方法列表中查找对应的方法实现。如果没有找到,会沿着继承链向上查找,直到找到方法实现或者到达根类 NSObject。例如:
Animal *animal = [[Cat alloc] init];
[animal makeSound];

这里我们创建了一个 Cat 对象,并将其赋值给 Animal 类型的变量 animal。当调用 makeSound 方法时,运行时系统会根据对象的实际类型(Cat)来查找并执行 Cat 类中重写的 makeSound 方法,而不是 Animal 类中的方法。

  • 动态方法解析: 在方法查找过程中,如果运行时系统没有找到方法实现,它会尝试进行动态方法解析。例如,我们在 MyClass 类中调用一个未实现的方法 unknownMethod
@interface MyClass : NSObject
@end

@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(unknownMethod)) {
        class_addMethod(self, sel, (IMP)myUnknownMethodImplementation, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
void myUnknownMethodImplementation(id self, SEL _cmd) {
    NSLog(@"Dynamic method implementation");
}
@end

在上述代码中,当调用 unknownMethod 方法时,运行时系统会调用 resolveInstanceMethod: 类方法。我们在这个方法中动态添加了 unknownMethod 的实现。class_addMethod 函数用于向类中添加方法,IMP 是方法实现的指针,"v@:" 是方法的类型编码,表示无返回值,第一个参数是对象本身(self),第二个参数是方法选择器(SEL)。

5. 方法与协议(Protocol)

协议是一种定义方法列表的方式,类可以遵循协议来表明它实现了这些方法。

  • 协议声明: 协议的声明使用 @protocol 关键字。例如,我们定义一个 Printable 协议,要求遵循该协议的类实现 print 方法。
@protocol Printable <NSObject>
- (void)print;
@end

这里 <NSObject> 表示该协议继承自 NSObject 协议,包含了 NSObject 协议定义的方法。

  • 类遵循协议: 一个类可以在声明时表明遵循某个协议。例如,Document 类遵循 Printable 协议。
@interface Document : NSObject <Printable>
@end

@implementation Document
- (void)print {
    NSLog(@"Printing document...");
}
@end

Document 类的实现中,我们实现了 Printable 协议中定义的 print 方法。

  • 协议作为参数类型: 协议可以作为方法参数的类型,这样可以接受任何遵循该协议的对象。例如,我们有一个 Printer 类,有一个 printObject: 方法,接受遵循 Printable 协议的对象。
@interface Printer : NSObject
- (void)printObject:(id <Printable>)object;
@end

@implementation Printer
- (void)printObject:(id <Printable>)object {
    [object print];
}
@end

printObject: 方法中,我们可以调用传入对象的 print 方法,因为该对象遵循 Printable 协议,保证了 print 方法的存在。

通过合理运用属性和方法的这些技巧,可以编写出更加健壮、高效和可维护的 Objective-C 代码,充分发挥这门语言的特性和优势。无论是在小型应用还是大型项目中,这些技巧都能帮助开发者更好地组织代码结构,提升代码质量。