20、Objective-C 初始化、属性与键值编码详解

Objective-C 初始化、属性与键值编码详解

1. 初始化方法的争论与实现

在初始化对象时,关于是否在 init 方法中使用访问器方法存在争论。一方认为不能在 init 方法中使用访问器方法,因为访问器假定对象已准备好工作,而在 init 方法完成之前对象并未准备好。另一方则认为在实际情况中这几乎不是问题,访问器方法可能会处理其他事务。

在大多数情况下,两种方法都可行。例如,在 initWithProductName: 方法中,我们可以直接设置实例变量:

- (instancetype)initWithProductName:(NSString *)pn
{
    if (self = [super init]) {
        _productName = [pn copy];
        _voltage = 120;
    }
    return self;
}
2. 多个初始化方法

创建 BNRAppliance 的子类 BNROwnedAppliance ,在 BNROwnedAppliance.h 中添加一个可变的所有者名称集合和三个方法:

#import "BNRAppliance.h"
@interface BNROwnedAppliance : BNRAppliance 
@property (readonly) NSSet *ownerNames;
- (instancetype)initWithProductName:(NSString *)pn
                     firstOwnerName:(NSString *)n;
- (void)addOwnerName:(NSString *)n;
- (void)removeOwnerName:(NSString *)n;
@end

BNROwnedAppliance.m 中实现这些方法:

#import "BNROwnedAppliance.h"
@interface BNROwnedAppliance ()
{
    NSMutableSet *_ownerNames;
}
@end
@implementation BNROwnedAppliance
- (instancetype)initWithProductName:(NSString *)pn
                     firstOwnerName:(NSString *)n
{
    if (self = [super initWithProductName:pn]) {
        _ownerNames = [[NSMutableSet alloc] init];
        if (n) {
            [_ownerNames addObject:n];
        } 
    }
    return self;
}
- (void)addOwnerName:(NSString *)n
{
    [_ownerNames addObject:n];
}
- (void)removeOwnerName:(NSString *)n
{
    [_ownerNames removeObject:n];
}
- (NSSet *)ownerNames
{
    return [_ownerNames copy];
}
@end

需要注意的是,这个类不会初始化 voltage productName ,这些由 BNRAppliance initWithProductName: 方法处理。当创建子类时,通常只需要初始化子类引入的实例变量,让父类处理它引入的实例变量。

3. 初始化方法链与指定初始化方法

如果直接使用 [[OwnedAppliance alloc] initWithProductName:@"Toaster"]; 会导致 ownerNames 集合未正确初始化。解决方法是在 BNROwnedAppliance.m 中实现父类的 initWithProductName: 方法,调用 initWithProductName:firstOwnerName: 并传递默认值:

- (instancetype)initWithProductName:(NSString *)pn
{
    return [self initWithProductName:pn firstOwnerName:nil];
}

每个类都有一个指定初始化方法,如 NSObject 的指定初始化方法是 init BNRAppliance 的是 initWithProductName: BNROwnedAppliance 的是 initWithProductName:firstOwnerName: 。其他初始化方法应直接或间接调用指定初始化方法。

当类的指定初始化方法与父类不同时,需要在头文件中进行文档说明:

// BNRAppliance.h
#import <Foundation/Foundation.h>
@interface BNRAppliance : NSObject 
@property (nonatomic, copy) NSString *productName;
@property (nonatomic) int voltage;
// The designated initializer
- (instancetype)initWithProductName:(NSString *)pn;
@end

// BNROwnedAppliance.h
#import "BNRAppliance.h"
@interface BNROwnedAppliance : BNRAppliance 
@property (readonly) NSSet *ownerNames;
// The designated initializer
- (instancetype)initWithProductName:(NSString *)pn
           firstOwnerName:(NSString *)n;
- (void)addOwnerName:(NSString *)n;
- (void)removeOwnerName:(NSString *)n;
@end

指定初始化方法的规则如下:
- 如果一个类有多个初始化方法,只有一个应该完成实际工作,即指定初始化方法。其他初始化方法应直接或间接调用指定初始化方法。
- 指定初始化方法在初始化自身实例变量之前,应调用父类的指定初始化方法。
- 如果类的指定初始化方法与父类不同,必须重写父类的指定初始化方法,使其调用新的指定初始化方法。
- 如果有多个初始化方法,应在头文件中明确文档说明哪个是指定初始化方法。

4. 危险的初始化方法

有时不能安全地重写父类的指定初始化方法。例如,创建 NSObject 的子类 BNRWallSafe ,其指定初始化方法是 initWithSecretCode: ,但使用默认值初始化 secretCode 不安全。此时,我们可以重写 init 方法,抛出异常提醒开发者:

- (instancetype)init
{
    [NSException raise:@"BNRWallSafeInitialization" 
                format:@"Use initWithSecretCode:, not init"];
}
5. 属性的更多知识
5.1 属性的可变性

属性可以声明为 readwrite readonly ,默认是 readwrite ,即同时创建 setter 和 getter 方法。如果不想创建 setter 方法,可以将属性标记为 readonly

@property (readonly) int voltage;
5.2 生命周期说明符

属性还可以声明为 unsafe_unretained assign strong weak copy ,这些选项决定了 setter 方法如何处理属性的内存管理。
| 说明符 | 描述 |
| ---- | ---- |
| assign | 非对象类型的默认值,只是将传入的值赋给属性。例如: @property (assign) int averageScore; 生成的 setter 方法相当于: - (void)setAverageScore:(int)d { _averageScore = d; } |
| strong | 确保对传入对象保持强引用,同时释放对旧对象的所有权。对于对象属性, strong 是对象指针的默认值。 |
| weak | 不意味着对指向的对象拥有所有权。如果该对象被释放,属性将被设置为 nil ,避免悬空指针。 |
| unsafe_unretained | 与 weak 类似,不意味着拥有所有权,但当指向的对象被释放时,属性不会自动设置为 nil 。 |
| copy | 对传入对象进行复制,并使指针指向这个副本。例如: @property (copy) NSString *lastName; 生成的 setter 方法相当于: - (void)setLastName:(NSString *)d { _lastName = [d copy]; } |

对于有可变子类的对象类型,使用 copy 属性很常见。例如, NSString 有子类 NSMutableString ,使用 copy 可以防止传入的可变字符串修改属性值。

如果对象有可变和不可变版本, copy 方法返回不可变副本。如果需要可变副本,可以使用 mutableCopy 方法。但没有名为 mutableCopy 的属性生命周期说明符,如果需要设置属性为对象的可变副本,需要自己实现 setter 方法:

- (void)setOwnerNamesInternal:(NSSet *)newNames
{
    _ownerNamesInternal = [newNames mutableCopy];
}
6. 原子性与非原子性

nonatomic 选项会使 setter 方法运行稍快。苹果的 UIKit 中每个属性都标记为 nonatomic ,建议将读写属性都标记为 nonatomic 。由于属性的默认值是 atomic ,需要显式地将每个属性标记为 nonatomic

7. 实现访问器方法

默认情况下,编译器会为声明的属性合成访问器方法。但在某些情况下,需要自己实现访问器方法,例如需要更新应用的用户界面或更新缓存信息时。

例如,声明一个属性并实现自定义的 setter 方法:

@property (nonatomic, copy) NSString* currentState;
- (void)setCurrentState:(NSString *)currentState 
{
    _currentState = [currentState copy];
    // Some code that updates UI
    ...
}

如果声明一个属性并自己实现了两个访问器方法,编译器将不会合成实例变量,需要使用 @synthesize 语句自己创建:

#import "Badger.h"
@interface Badger : NSObject ()
@property (nonatomic) Mushroom *mushroom;
@end
@implementation Badger;
@synthesize mushroom = _mushroom;
- (Mushroom *)mushroom
{
    return _mushroom;
}
- (void)setMushroom:(Mushroom *)mush
{
    _mushroom = mush;
}
...
8. 键值编码

键值编码是使用属性名称读取和设置属性的能力, NSObject 定义了键值编码方法,因此每个对象都有此功能。

例如,将 [a setProductName:@"Washing Machine"]; 重写为键值编码方式:

[a setValue:@"Washing Machine" forKey:@"productName"];

使用 valueForKey: 方法读取属性值:

int main (int argc, const char * argv[])
{
    @autoreleasepool {
        BNRAppliance *a = [[BNRAppliance alloc] init];
        NSLog(@"a is %@", a);
        [a setValue:@"Washing Machine" forKey:@"productName"];
        [a setVoltage:240];
        NSLog(@"a is %@", a);
        NSLog(@"the product name is %@", [a valueForKey:@"productName"]);
    }
    return 0;
}

如果输入属性名称错误,编译器不会给出警告,但会在运行时出错。

键值编码在标准框架中很有用,例如 Core Data 框架使用键值编码来操作自定义数据对象。即使对象没有访问器方法,键值编码也能操作变量。例如,在 BNRAppliance.h 中显式声明实例变量并注释掉属性声明,同时在 BNRAppliance.m 中移除相关方法的使用:

// BNRAppliance.h
#import <Foundation/Foundation.h>
@interface BNRAppliance : NSObject 
{
    NSString *_productName;
}
// @property (nonatomic, copy) NSString *productName;
@property (nonatomic) int voltage;
// The designated initializer
- (instancetype)initWithProductName:(NSString *)pn;
@end

// BNRAppliance.m
@implementation BNRAppliance
- (instancetype)initWithProductName:(NSString *)pn
{
    if (self = [super init]) {
        _productName = [pn copy];
        _voltage = 120;
    }
    return self;
}
- (instancetype)init
{
    return [self initWithProductName:@"Unknown"];
}
- (NSString *)description
{
    return [NSString stringWithFormat:@"<%@: %d volts>", _productName, self.voltage];
}
@end

综上所述,掌握 Objective-C 的初始化方法、属性的使用和键值编码等知识,对于开发高质量的应用程序至关重要。通过合理运用这些技术,可以提高代码的可读性、可维护性和性能。

Objective-C 初始化、属性与键值编码详解

9. 键值编码的操作流程及注意事项

键值编码的操作流程可以总结如下:
1. 设置属性值 :使用 setValue:forKey: 方法,传入要设置的值和属性的名称作为键。例如:

[a setValue:@"Washing Machine" forKey:@"productName"];
  1. 读取属性值 :使用 valueForKey: 方法,传入属性的名称作为键。例如:
NSString *productName = [a valueForKey:@"productName"];

在使用键值编码时,需要注意以下几点:
- 键的正确性 :如果输入的键名称错误,编译器不会给出警告,但在运行时会抛出 NSUnknownKeyException 异常。例如:

NSLog(@"the product name is %@", [a valueForKey:@"productNammmme"]);

运行上述代码会导致程序崩溃,并输出错误信息:

*** Terminating app due to uncaught exception 'NSUnknownKeyException', 
reason: '[<BNRAppliance 0x100108dd0> valueForUndefinedKey:]: 
this class is not key value coding-compliant for the key productNammmme.'
  • 无访问器的情况 :即使对象没有显式的访问器方法,键值编码也能正常工作。它会直接访问实例变量。例如,在 BNRAppliance 类中,即使注释掉 productName 的属性声明,键值编码仍然可以操作该实例变量。
10. 键值编码的应用场景

键值编码在很多场景中都有广泛的应用,下面介绍几个常见的应用场景:
- 数据绑定 :在界面开发中,可以使用键值编码将界面元素与数据模型绑定。例如,将一个 UILabel 的文本属性与一个对象的某个属性绑定,当对象的属性值发生变化时, UILabel 的文本会自动更新。
- 数据传递 :在不同的对象之间传递数据时,可以使用键值编码来简化数据的设置和获取。例如,在一个视图控制器中,可以使用键值编码将数据传递给另一个视图控制器。
- 框架集成 :许多标准框架,如 Core Data ,使用键值编码来操作自定义数据对象。 Core Data 可以通过键值编码将对象的属性保存到数据库中,并在需要时从数据库中恢复。

11. 初始化方法、属性与键值编码的综合应用

在实际开发中,初始化方法、属性和键值编码通常会结合使用。下面通过一个示例来展示它们的综合应用:

// BNRAppliance.h
#import <Foundation/Foundation.h>

@interface BNRAppliance : NSObject 
{
    NSString *_productName;
    int _voltage;
}
@property (nonatomic) int voltage;
// The designated initializer
- (instancetype)initWithProductName:(NSString *)pn;
@end

// BNRAppliance.m
@implementation BNRAppliance
- (instancetype)initWithProductName:(NSString *)pn
{
    if (self = [super init]) {
        _productName = [pn copy];
        _voltage = 120;
    }
    return self;
}

- (instancetype)init
{
    return [self initWithProductName:@"Unknown"];
}

- (NSString *)description
{
    return [NSString stringWithFormat:@"<%@: %d volts>", _productName, self.voltage];
}
@end

// BNROwnedAppliance.h
#import "BNRAppliance.h"

@interface BNROwnedAppliance : BNRAppliance 
@property (readonly) NSSet *ownerNames;
// The designated initializer
- (instancetype)initWithProductName:(NSString *)pn
           firstOwnerName:(NSString *)n;
- (void)addOwnerName:(NSString *)n;
- (void)removeOwnerName:(NSString *)n;
@end

// BNROwnedAppliance.m
#import "BNROwnedAppliance.h"

@interface BNROwnedAppliance ()
{
    NSMutableSet *_ownerNames;
}
@end

@implementation BNROwnedAppliance
- (instancetype)initWithProductName:(NSString *)pn
                     firstOwnerName:(NSString *)n
{
    if (self = [super initWithProductName:pn]) {
        _ownerNames = [[NSMutableSet alloc] init];
        if (n) {
            [_ownerNames addObject:n];
        } 
    }
    return self;
}

- (instancetype)initWithProductName:(NSString *)pn
{
    return [self initWithProductName:pn firstOwnerName:nil];
}

- (void)addOwnerName:(NSString *)n
{
    [_ownerNames addObject:n];
}

- (void)removeOwnerName:(NSString *)n
{
    [_ownerNames removeObject:n];
}

- (NSSet *)ownerNames
{
    return [_ownerNames copy];
}
@end

// main.m
#import <Foundation/Foundation.h>
#import "BNROwnedAppliance.h"

int main (int argc, const char * argv[])
{
    @autoreleasepool {
        BNROwnedAppliance *appliance = [[BNROwnedAppliance alloc] initWithProductName:@"Toaster" firstOwnerName:@"John"];

        // 使用键值编码设置属性值
        [appliance setValue:@"New Toaster" forKey:@"productName"];

        // 使用键值编码读取属性值
        NSString *productName = [appliance valueForKey:@"productName"];
        NSLog(@"Product Name: %@", productName);

        // 调用初始化方法和属性方法
        [appliance addOwnerName:@"Jane"];
        NSSet *ownerNames = appliance.ownerNames;
        NSLog(@"Owner Names: %@", ownerNames);
    }
    return 0;
}

在上述示例中,我们创建了 BNRAppliance BNROwnedAppliance 两个类,使用了初始化方法来初始化对象的属性。同时,使用了键值编码来设置和读取对象的属性值。最后,调用了属性的访问器方法来获取对象的属性值。

12. 总结

通过本文的介绍,我们详细了解了 Objective-C 中的初始化方法、属性和键值编码的相关知识。下面是对这些知识的总结:
- 初始化方法 :每个类都有一个指定初始化方法,其他初始化方法应直接或间接调用指定初始化方法。在创建子类时,通常只需要初始化子类引入的实例变量,让父类处理它引入的实例变量。
- 属性 :属性可以声明为 readwrite readonly ,并可以使用不同的生命周期说明符来控制内存管理。 nonatomic 选项可以提高 setter 方法的运行速度。在某些情况下,需要自己实现访问器方法。
- 键值编码 :键值编码是使用属性名称读取和设置属性的能力,每个对象都有此功能。它在标准框架中广泛应用,即使对象没有访问器方法,也能操作变量。

掌握这些知识对于开发高质量的 Objective-C 应用程序至关重要。在实际开发中,我们应该根据具体的需求合理运用这些技术,提高代码的可读性、可维护性和性能。

下面是一个简单的 mermaid 流程图,展示了键值编码的基本操作流程:

graph TD;
    A[开始] --> B[设置属性值: setValue:forKey:];
    B --> C[读取属性值: valueForKey:];
    C --> D{键是否正确};
    D -- 是 --> E[正常操作];
    D -- 否 --> F[抛出异常];
    E --> G[结束];
    F --> G;

通过这个流程图,我们可以更直观地了解键值编码的操作流程和可能出现的情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值