Objective-c官方文档 封装数据属性
版权声明:原创作品,谢绝转载!否则将追究法律责任。
很多对象需要跟踪信息为了执行他们的任务。一些对象设计模型一个或者多个值。例如NSNumber 类用来保存一个值或者自定义的类有一些属性。有一些对象不在一般的范围内。也许处理界面交互和一些信息展示这些对象用来跟踪界面元素或者相关的模型对象。
声明公有的属性公开数据:
Objective-c属性提供了一个定义类信息的方法目的是数据的封装
@interface XYZPerson : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end
在这个例子中这个XYZPerson 类定义了String类型的属性用来存储人的名字
在面向对象思想中一个只要的原则是对象应该隐藏内部实现在公有接口的后面。我们访问对象的属性而不是直接访问内部的值。
我们用设置器和访问器方法来获取和设置属性的值
NSString *firstName = [somePerson firstName];
[somePerson setFirstName:@"Johnny"];
默认情况下这些设置器和访问器方法是自动合成的由编译器,因此你不需要做任何事情除了声明属性。
合成的方法遵循特定的命名规范:
这个方法用来访问值(设置器方法)和属性有相同的名字。
例如:firstName 的访问器方法名字就叫做firstName
这个方法用来设置值(设置器方法)开始用set然后用上属性的名字第一个字母大写:
例如:setFirstName;
如果你不希望属性改变那么就用readonly只读的:
@property (readonly)NSString *fullName;
关键字显示对象怎么和属性交互,以及告诉编译器怎么合成相关的设置器和访问器方法。
例如这个编译器将要合成fullName访问器方法,但是没有setFullName方法。
和只读readOnly相反的是readwrite属性默认是可读写的。
如果你想给访问器方法设置不同的名字,你可以再给属性添加他的属性的时候指定名字。例如Boolean属性(这个属性有YES 或者NO值)给他自定义访问器方法以“IS“开头
@property (getter = isFinished)BOOL finished;
如果你有多个属性这样设置:
@property (readonly,getter = isFinished)BOOL finished;
在这个例子中编译器合成的是isFinished方法而不是setFinished方法。
点语法是调用的访问器方法
除了明确的访问器方法调用Objective-c还提供了点语法访问对象的属性。
点语法让你像这样访问属性:
NSString *firstName = somePerson.firstName;
点语法纯粹是一个包装器方法用来调用访问器方法的。当用逗号时候,属性仍然可以被访问和修改用getter和setter方法例如:
访问值用somePerson.firstName 和 [somePerson firstName]:是一样的。
设置值用somePerson.firstName = @"qiqi";和
[somePerson setFirstName:@“qiqi”];
这就意味着属性的访问用点语法也可以控制属性。如果属性是readonly编译器将要得到一个警告当你用逗号的时候。
大多数属性都有实例变量。
默认情况下,可读写的属性是有辅助实例变量的,再次由编译器自动合成。
实例变量是一个变量,在整个对象生命周期内都存在。当对象第一次被创建的时候,用于实例变量的内存分配,当对象销毁的时候就被释放了。除非有特别的指示否则合成的实例变量和属性有相同的名字,但是有一个下划线例如属性叫做firstName。合成的实例变量的名字叫做_firstName.
虽然他的最佳实践是一个对象访问自己的属性用访问器或者点语法,他也有可能在实现文件里直接访问实例变量用实例方法。下划线加上实例变量表明你访问的是实例变量而不是局部变量例如:
- (void)someMethod {
NSString *myString = @"An interesting string";
_someString = myString;
}
这个例子很明显一个是局部变量和一个实例变量。
通常我们应该用访问器方法和点语法来进行属性的访问即你访问对象的属性用他们自己的实现,在这个例子中我们可以用self:
-(void)someMethod
{
NSString *myString = @"qiqi";
self.someString = myString;
//or
[self setSomeString:myString];
}
这个特殊的例外是当写初始化方法或者销毁方法或者自定义访问方法我们后来描述的。
你可以自定义合成实例变量的名字
@implementation YourClass
@synthesize propertyName = _instanceVariableName;
@end
例如:
@sythesize firstName = _my_firstName;
在这个例子中,属性将要被叫做firstName,并且可以访问通过firstName并且setFirstName访问器方法或者点语法,但是有辅助实例变量_my_firstName
重要注意:如果你用@synthesize没有指定实例变量的名字,就像:
@sytheSize firstName;
这个实例变量将要和你的属性的名字一样。
在这个例子中实例变量叫做firstName没有下划线。
你可以定义实例变量而不用属性
属性的最佳实践在对象上是任何时候你需要跟踪对象的值或者其他的对象。
如果你需要定义你自己的实例变量没有定义属性,你可以在头文件或者实现文件里用如下:
@interface SomeClass:NSObject
{
NSString *_myInstanceVariable;
}
@implementation SomeClass
{
NSString *_anotherCustomInstanceVarible;
}
@end
你可以添加实例变量在延展里
访问实例变量直接从初始化方法里:
setter方法可以有额外的副作用。他们可以出发KVC通知,或者执行进一步的任务如果是你的自定义方法。
你应该直接访问实例变量从你的初始化方法里应为属性在这时候已经被设置了。其余的对象可能尚未初始化完成。即使你不提供自定义的访问器方法在未来的时候可能被其他的子类给覆盖。
一个典型的init方法。
- (id)init {
self = [super init];
if (self) {
// initialize instance variables here
}
return self;
}
一个init方法应该分派self和父类初始化方法在做自己的初始化之前。由于父类可能初始化失败或者返回nil因此我们应该检查来确认self不是nil在我们本类初始化时候。
在调用父类初始化方法的时候会层层调用父类的初始化方法,当都初始化完毕的时候那么再初始化自己。
前面我们看的对象初始化除了调用init方法外,或者调用方法初始化特殊值。
- (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName;
你可以实现这个方法像这样:
- (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName {
self = [super init];
if (self) {
_firstName = aFirstName;
_lastName = aLastName;
}
return self;
}
指定初始化是主要的初始化方法:
如果一个对象一个或者多个初始化方法,你应该定义哪个方法是设计的初始化者,这个提供了很多可选的初始化(例如可以有很多的参数),并且可以为其他的方法调用提供便利,你应该也典型重载初始化方法来给你的初始化方法设定默认的值。
如果下面这个方法的类有生日的属性,我们设计初始化方法可能这样:
- (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName dateOfBirth:(NSDate *)aDOB;
这个方法将要设置我们的实例变量,如果你将提供便利初始化第一个和第二个名字,你将要这样实现你的初始化方法:
- (id)initWithFirstName:(NSString *) aFrameworkName lastName:(NSString *)aLastName
{
return [self initWithFirstName:aFirstName lastName:aLastName dateOfBirth:nil];
}
你可以实现标准的init方法来提供适当的默认值
- (id)init
{
return [self initWithFirstName:@"qiqi" lastName@"deo" dateOfBirth:nil];
}
你需要写初始化方法当子类用多个初始化方法,你应该重写父类的初始化方法,来实现自己的初始化,或者添加自己的初始化方法,但是都得初始化父类在初始化自己之前。
你可以实现自己的访问器方法:
属性不能一直有自己的辅助实例变量。
举一个例子我们为了跟踪属性的值可以自定义打印出他的值例如:
- (NSString *)fullName {
return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}
如果你需要写自定义的访问器方法为属性并且有一个实例变量。你必须直接定义实例变量。例如:我们经常在属性里面延迟初始化实例变量懒加载设计模式:
- (XYZObject *)someImportantObject {
if (!_someImportantObject) {
_someImportantObject = [[XYZObject alloc] init];
}
return _someImportantObject;
}
再返回值之前需要检测返回值是不是nil,如果为空那么就初始化对象。
编译器会自动合成一个实例变量在所有的情况下,它也是合成只要一个访问器方法。如果你自己实现访问器和设置器方法,那么编译器会认为你自己控制属性的实现,而不会自动的生成实例变量。
如果你仍然需要实例变量的话,你将要需要请求一个合成。
@synthesize property = _property;
属性默认的是原子性的:
默认的Objective-c属性是原子性的:
@interface XYZObject : NSObject
@property NSObject *implicitAtomicObject; // atomic by default
@property (atomic) NSObject *explicitAtomicObject; // explicitly marked atomic
@end
注意:属性的原子性的特性并不是意味着这个对象的线程是安全的。我们想象一下,一个对象有名字和姓在原子性的访问器被访问。如果一个线程同时访问他们的名和姓,这个原子性的访问方法不一定正确返回值。如果名字在访问之前改变,姓在访问之后改变,你会有一个不一致,不对应的结果,那么这不是你想要的结果。
通过对象的所有权和责任管理对象图
就像你已经看到的,Objective-c的对象是动态创建的在堆上,这意味着你需要用指针来跟踪对象地址,不像常量,不是一直能确定对象的生命周期的范围通过指针变量,相反的是一个对象必须长时间保存在内存中以便于其他的对象引用。
不要试图担心手动管理每个对象的生命周期,你应该试图考虑对象之间的关系。
例如这个XYZPerson对象,例如,有两个名字的属性有效的被XYZPerson实例拥有,这意味着他们应该呆在内存足够长只要XYZPerson在内存中。
当一个对象依赖其他的对象用这种方式,有效的采取其他对象的所有权,这个第一个对象强引用其他的对象。在Objective-c一个对象保持存活状态只要他最少有一个强引用指向他从别的对象:
当XYZPerson对象被销毁从内存,这两个对象也将要被销毁,假设没有其他的强引用指向他们。
下面有一个更复杂的例子:
当我点击更新的按钮,这个 badge preview更新相关名字信息。
第一次人的详细信息被输入和更新如下图:
当用户修改这个人的名字变成如下:
这个View管理一个强引用关系到字符串对象尽管XYZPerson有不同的名字,这意味这这个字符串对象将在内存中被View来打印和使用。
当第二次点击的时候这个View被告诉来更新内部的属性匹配人的对象。因此变成如下图:
到现在,这个字符串对象没有强引用指向他,他就会被从内存中移除掉。
默认的Objective-c的属性和实例变量维持着强引用指向其他对象, 这只是在合适的情况下,但是会引起潜在的问题强引用循环。
怎么避免强引用
尽管强应用适合单向对象之间的关系,你需要小心处理组相互关联的对象,如果一个组的对象组有一圈强引用,他们相互存活即使他们没有强引用从组的外部。
一个明显的例子在tableView和他的delegate之前有潜在的强引用存在。为了使通用的tableView这个类在很多环境能用到。他的delegate做出一些决定给外部对象。这意味着他表现什么依赖与另一个对象,或者做什么如果用户和表示图交互。下图:
这样将会出现一个问题如果其他的对象放弃对tableview和delegate如下图:
即使不需要对象们存活在内存中,没有强引用指向tableView或者delegate不同于两个对象之间的关系,剩下的两个强引用保持着他们之间的关系。但是这是个强引用的循环。
有一个方法来解决这个问题就是替代其中一个强引用为弱引用。弱引用并不意味着两个对象之前的所有权,并且不能保持一个对象的存活。
现在变成这样下图:
当没有其他对象给tableView或者tableView的delegate,就没有强引用指向delegate对象如下图:
这意味着delegate对象将要被销毁,从而释放了tableView的强引用。
一旦delegate被销毁,就没有其他的强引用指向tableView,因此他也被销毁。
使用强和弱来管理所有权
默认情况下,对象属性这样声明
@property id delegate;
用强引用来实现实例变量,声明一个弱引用,给他添加属性想这样:
@property (weak)id delegate;
注意:weak的相反是strong。所以没有必要声明,默认是strong
局部变量(不是属性的实例变量)是默认保持强引用的,这意味着下面代码将要像你预想的这样工作:
NSDate *originalDate = self.lastModificationDate;
self.lastModificationDate = [NSDate date];
NSLog(@"Last modification date changed from %@ to %@",
originalDate, self.lastModificationDate);
在这个例子中,局部变量originalDate保持着强引用对lastModification对象。当lastmodification属性改变的时候,这个属性不能保持强引用给原始的date,但是date是继续保持存活由于originalDate强引用变量。
注意:一个变量维持着强引用给另一个对象只要这个变量在这个范围内,或者直到他分配给另一个对象或者nil。
如果你不想要一个变量维持强引用,你可以声明弱引用像这样:
NSObject * __weak weakVariable;
因为弱引用不能保持对象存活,有可能引用对象被销毁然后这个引用仍然被使用。为了避免这个悬空的指针指向之前原始对象占用的内存。弱引用需要自动把他设置为空当对象释放的时候。
这意味着如果你用弱引用在之前的例子中:
这个originalDate的变量可能潜在的被设置为空。当self.lastModification被再次指定,这个属性不能维持强引用关系对originaldate。如果没有强引用指向他,这个原始日期将要被销毁并且originaldate被设置nil。
弱的引用可能是困惑的来源像这样:
NSObject * __weak someObject = [[NSObject alloc] init];
这个例子,刚创建的对象没有强引用对他,因此很快被释放并且指针指向nil。
NSObject *cachedObject = self.someWeakProperty; // 1
if (cachedObject) { // 2
[someObject doSomethingImportantWith:cachedObject]; // 3
} // 4
cachedObject = nil; // 5
在这个例子中,强引用在第一行创建,意味着对象确保在下面两个方法调用的时候仍然存活着。在第5行这个对象被设置为nil。从此放弃了强引用,如果原始对象没有其他的强引用指向他,他就会被销毁并且指针指向空。
用Unsafe Unretained引用对一些类
有一些类在cocoa和cocoa Touch不支持弱引用。这意味着你不能定义弱引用给属性和局部变量来追踪他们,这些类包括NSTextView,NSFont,和NSColorSpace,还有很多在Transitioning to ARC Release Notes.笔记里。
如果你需要弱引用对一些类你必须用不安全的引用。对于一个属性,这意味着用Unsafe Unretained
@property (unsafe_unretained) NSObject *unsafeProperty;
对于变量,你需要这样使用_unsafe_untetained
NSObject *_unsafe_unretained unsafeReference;
这个引用有点像弱引用,他不让他的相关对象活着,但是不能设置nil如果目标对象被销毁了。这意味着你将要遗留一个悬空指针指向原来对象占用的那块内存,因此属于不安全是因为发送一个消息到悬空指针结果是崩溃的。
复制属性维持着他们的拷贝
在一些情况下一个对象也许希望保持他的一份copy。
@interface XYZBadgeView : NSView
@property NSString *firstName;
@property NSString *lastName;
@end
例如这个类的接口声明两个属性默认是强引用的。想想会发生什么如果另一个对象创建一个字符串设置他的属性像这样:
NSMutableString *nameString = [NSMutableString stringWithString:@"John"];
self.badgeView.firstName = nameString;
这个完美有效的,因为NSMutableString是NSString的子类。尽管这个View认为这是处理NSString的一个实例,这意味着这个字符串对象是可以被改变的:[nameString appending:@"qiqi"];
在这个例子当中,尽管这个名字是@“john”在当时创建的时候被设置为View的firstName属性,但是现在因为改变了结果变成“johnqiqi”因为可变字符串是被改变了。
你可能选择这个View应该维持他自己的副本包括他的属性,所以他有效的被扑捉这个字符串在这个属性被设置的时候。添加两个属性他们的属性关键字是copy。
@interface XYZBadgeView : NSView
@property (copy) NSString *firstName;
@property (copy) NSString *lastName;
@end
这个View维持着他们自己的copy里面有两个字符串,即使一个可变的字符串被设置随后又发生了改变,这个View捕捉了在那时候设置的值,例如:
NSMutableString *nameString = [NSMutableString stringWithString:@"John"];
self.badgeView.firstName = nameString;
[nameString appendString:@"ny"];
这时候firstName被View拥有将是一个未受影响的copy对原始的“John”字符串。这个copy关键字属性意味着属性将要用的是强引用,因为他可能被新创建的对象所拥有。
注意:任何属性带有copy关键字的都必须支持NSCopying协议,这意味着他应该符合NSCopying协议。
如果你需要直接设置copy属性的辅助实例变量,例如一个初始化方法,不要忘了设置copy原始的对象。例如:
- (id)initWithSomeOriginalString:(NSString *)aString {
self = [super init];
if (self) {
_instanceVariableForCopyProperty = [aString copy];
}
return self;
}