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"];
-
读取属性值
:使用
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;
通过这个流程图,我们可以更直观地了解键值编码的操作流程和可能出现的情况。
超级会员免费看
11

被折叠的 条评论
为什么被折叠?



