[编写高质量iOS代码的52个有效方法](二)对象
2016-07-22 19:44
645 查看
[编写高质量iOS代码的52个有效方法](二)对象
参考书籍:《Effective Objective-C 2.0》 【英】 Matt Galloway先睹为快
6.理解“属性”这一概念7.在对象内部尽量直接访问实例变量
8.理解“对象等同性”这一概念
9.以“类簇模式”隐藏实现细节
10.在既有类中使用关联对象存放自定义数据
目录
编写高质量iOS代码的52个有效方法二对象先睹为快
目录
第6条理解属性这一概念
第7条在对象内部尽量直接访问实例变量
第8条理解对象等同性这一概念
第9条以类簇模式隐藏实现细节
第10条在既有类中使用关联对象存放自定义数据
第6条:理解“属性”这一概念
属性是Objective-C的一项特性,用于封装对象中的数据,编译器会自动编写访问属性所需的方法(getter方法和setter方法)。除此之外,编译器还会自动向类中添加适当类型的实例变量,并且在属性名前加下划线,以此作为实例变量的名字。也可以在类的实现代码里通过@synthesize语法来指定实例变量的名字:// EOCPerson.h @interface EOCPerson : NSObject // 声明属性 @property NSString *firstName; @property NSString *lastName; @end // EOCPerson.m @implementation EOCPerson // 通过@synthesize语法修改默认的实例变量名 @synthesize firstName = _firstName; @synthesize lastName = _lastName; @end
若不想令编译器自动合成存取方法,则可以自己实现。还有一种方法是使用@dynamic关键字阻止编译器自动合成存取方法和实例变量。这样的话,就需要在运行期动态创建存取方法。(12条中会有具体示例)
// EOCPerson.h @interface EOCPerson : NSObject @property NSString *firstName; @property NSString *lastName; @end // EOCPerson.m @implementation EOCPerson // 通过@dynamic关键字阻止编译器自动合成存取方法和实例变量 @dynamic firstName, lastName; @end
属性特质:
1. 原子性:默认情况下,由编译器合成的方法是atomic(原子的),如果指定了nonatomic(非原子的)特质,则不会在方法中使用同步锁。由于iOS中使用同步锁的开销较大,因此iOS程序一般都会使用nonatomic属性。
2. 读/写权限:默认情况下,属性为readwrite(读写)特质,@synthesize实现属性时会自动生成getter方法和setter方法。如果属性是readonly(只读)特质,则@synthesize实现属性时只会生成getter方法。
3. 内存管理语义
(1)assign 设置方法只会针对纯量类型(如CGFloat,NSInteger等)的简单赋值操作
(2)strong 属性定义为一种拥有关系,为这种属性设置新值时,设置方法会保留新值,并释放旧值,再将新值设置上去。
(4)unsafe_unretained 此特质的语义和assign相同,但它适用于对象类型,该特质也表达一种非拥有关系,当目标对象遭到摧毁时,属性值不会自动清空,这一点与weak有区别。
(5)copy 此特质所表达的所属关系与strong类似,然而设置方法并不保留新值,而是将其拷贝,多用来保护NSString* 类型属性的封装性。
4. 方法名 getter= 和 setter=分别用来指定存取方法的方法名,其中指定setter方法名不常见。
// 声明一个非原子性,只读,用assign方式管理内存,获取方法名为“isON”的布尔类型属性 @property(nonatomic, readonly, assign, getter = isOn) Bool on;
在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义:
// EOCPerson.h @interface EOCPerson : NSObject // 声明属性 @property (copy, readonly) NSString *firstName; @property (copy, readonly) NSString *lastName; -(id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName; @end // EOCPerson.m -(id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName{ if((self = [super init])){ // 实现初始化方法时,遵循属性定义中的copy语义 _firstName = [firstName copy]; _lastName = [lastName copy]; } } @end
第7条:在对象内部尽量直接访问实例变量
在对象之外访问实例变量时,总是应该通过属性来做。在对象内部访问实例变量时,除了几种特殊情况之外,最好在读取实例变量时采用直接访问的形式,而在设置实例变量时通过属性来做:// EOCPerson.h @interface EOCPerson : NSObject @property (nonatomic, copy) NSString *firstName; @property (nonatomic, copy) NSString *lastName; -(NSString *)fullName; -(void)setFullName:(NSString *)fullName; @end // EOCPerson.m @implementation EOCPerson // 直接访问实例变量 -(NSString *)fullName{ return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName]; } // 通过属性访问实例变量(存取方法) -(void)setFullName:(NSString *)fullName{ NSArray *components = [fullName componentsSeparatedByString:@" "]; self.firstName = [components objectAtIndex:0]; self.lastName = [components objectAtIndex:1]; } @end
两种写法的区别:
1. 直接访问实例变量速度比较快,因为不经过Objective-C的方法派发。
2. 直接访问实例变量时,不会调用其setter方法,这就绕过了为相关属性所定义的内存管理语义。
3. 如果直接访问实例变量,那么就不会触发KVO(键值观察)通知。
4. 通过属性来访问有助于排查与之相关的错误,因为可以给getter方法或setter方法中新增断点来进行监控。
综上:进行写操作时通过属性访问,进行读操作时直接访问实例变量。这种折中方案既能提高读取速度,又能控制对属性的写入操作。
特殊场景1:在初始化方法(init)中总是应该直接访问实例变量,因为子类可能会重写setter方法。只有一种特殊情况,待初始化的实例变量声明在超类中,而我们又无法在子类中直接访问此实例变量的话,那么就需要调用setter方法了。
特殊场景2:惰性初始化。这种情况下,必须通过getter方法来访问属性,否则,实例变量永远不会初始化。例如EOCPerson类中用到了一个用于表示人脑中信息的属性,这个属性创建成本较高且不常用,所以在getter方法中对其执行惰性初始化:
// brain属性的getter方法 -(EOCBrain *)brain{ if(!_brain){ _brain = [EOCBrain new]; } return _brain; }
若没有调用getter方法就直接访问实例变量,则会看到尚未设置好的brain,所以采用惰性初始化时,必须通过getter方法来访问实例变量。
第8条:理解“对象等同性”这一概念
根据等同性来比较对象是一个非常有用的功能。==操作符比较的是两个指针本身,并不是其所指的对象。应该使用NSObject协议中声明的isEqual:方法来判断两个对象的等同性:NSString *foo = @"Badger 123"; NSString *bar = [NSString stringWithFormat:@"Badger %i",123]; Bool equalA = (foo == bar); // equalA为NO Bool equalB = [foo isEqual:bar]; // equalB为YES Bool equalC = [foo isEqualToString:bar]; // equalC为YES
在确定比较对象为NSString类时,可以使用isEqualToString:方法,调用速度比isEqual:快,因为后者需要执行额外的步骤来确定受测对象的类型。 NSObject协议中有两个用于判断等同性的关键方法:
-(BOOL)isEqual:(id)object; -(NSUInteger)hash;
NSObject类对这两个方法的默认实现是,当且仅当其指针值完全相等时,这两个对象才相等。如果想正确地重写这两个方法,需要注意的是:如果isEqual方法判定两个对象相等,那么其hash方法也必须返回同一个值。但是如果两个对象的hash方法返回同一个值,那么isEqual方法未必认为两者相等。 下面是一个重写EOCPerson类,isEqual方法和hash方法的示例,EOCPerson类共有3个属性,如果两个EOCPerson对象的这3个属性都相等,则认为两个对象相等:
// EOCPerson.h @interface EOCPerson : NSObject @property (nonatomic, copy) NSString *firstName; @property (nonatomic, copy) NSString *lastName; @property (nonatomic, assign) NSInteger age; @end // EOCPerson.m @implementation EOCPerson -(BOOL)isEqual:(id)object{ // 如果指针相等,则指向同一对象,所以相等 if(self == object) return YES; // 如果不属于同一个类,则对象也必定不相等(不过有时可能认为:可以与子类对象相等) if([self class] != [object class]) return NO; EOCPerson *otherPerson = (EOCPerson*)object; // 只有每个属性都相等,才认为两个对象相等。 if(![_firstName isEqualToString:otherPerson.firstName]) return NO; if(![_lastName isEqualToString:otherPerson.lastName]) return NO; if(_age != otherPerson.age) return NO; return YES } // 哈希码生成方法只要能保证两个相同的对象生成的是两个相同的哈希码就行了(相同哈希码的对象不一定相等),写法有很多种。 // 下面是一种比较好的哈希码生成方式,既能保持高效率,又能使生成的哈希码至少位于一定范围之内,而不会过于频繁地重复。 -(NSUInteger)hash{ NSUInteger firstNameHash = [_firstName hash]; NSUInteger lastNameHash = [_lastName hash]; NSUInteger ageHash = _age; return firstNameHash ^ lastNameHash ^ ageHash; } @end
如果自定义的类经常需要判断等同性,那么最好自己来编写一个与isEqualToString:方法类似的判断方法。既能提升检测速度,又提升代码的可读性。
-(BOOL)isEqualToPerson:(EOCPerson*)otherPerson{ // 如果指针相等,则指向同一对象,所以相等 if(self == object) return YES; // 只有每个属性都相等,才认为两个对象相等。 if(![_firstName isEqualToString:otherPerson.firstName]) return NO; if(![_lastName isEqualToString:otherPerson.lastName]) return NO; if(_age != otherPerson.age) return NO; return YES } -(BOOL)isEqual:(id)object{ // 如果两个对象属于同一个类,则由自己的方法判定,否则交由超类判定 if([self class] != [object class]){ return [self isEqualToPerson:(EOCPerson*)obj]; }else{ return [super isEqual:object]; } }
在容器中放入可变类对象后,就不应该改变其哈希码了。要解决这个问题,需要确保哈希码不是根据对象的可变部分计算出来的,或是保证放入容器后就不再改变对象内容了。 下面是一个错误示例:
NSMutableSet *set = [NSMutableSet new]; NSMutableArray *arrayA = [@[@1, @2]mutableCopy]; [set addObject:arrayA]; NSLog(@"set = %@", set); // set = {(1,2)} NSMutableArray *arrayB = [@[@1, @2]mutableCopy]; [set addObject:arrayB]; NSLog(@"set = %@", set); // set = {(1,2)} NSSet或NSMutableSet中不会保存重复的对象 NSMutableArray *arrayC = [@[@1]mutableCopy]; [set addObject:arrayC]; NSLog(@"set = %@", set); // set = {(1),(1,2)} arrayC与set里已有对象不等,可以添加到set中 [arrayC addObject:@2]; // 改变arrayC中的内容,使其与set中的对象相等 NSLog(@"set = %@", set); // set = {(1,2),(1,2)} 出现问题,set中出现两个相等的对象,与set的语义冲突 NSSet *setB = [set copy]; NSLog(@"setB = %@", setB); // setB = {(1,2)} 拷贝之后重复的对象又消失了
如果将某对象放入容器之后又修改其内容将会造成难以预料的行为! 进行等同性判定的时候还需要注意:不要盲目地逐个检测每条属性,而是应该按照具体需求来制定检测方案。
第9条:以“类簇模式”隐藏实现细节
类簇是一种很有用的模式,可以隐藏抽象基类背后的实现细节。Objective-C的系统框架中普遍使用此模式。比如,iOS的用户界面框架UIKit中的UIButton类,创建按钮调用的是下面的类方法:+(UIButton*)buttonWithType:(UIButtonType)type;
该方法返回的对象取决于传入的按钮类型,所有的返回类型的对象都继承自同一个基类:UIButton。这样做的意义在于,UIButton类的使用者无须关心创建出来的按钮具体属于哪个子类,也不用考虑按钮的绘制方式等实现细节。只需要知道明白如何创建按钮,如何设置title等属性,增加触摸动作的目标对象等问题。 下面是一个创建类簇的示例:
// EOCEmployee.h // 用枚举类型表示雇员的类型 typedef NS_ENUM(NSUInteger, EOCEmployeeType){ EOCEmployeeTypeDeveloper, EOCEmployeeTypeDesigner, EOCEmployeeTypeFinance, }; @interface EOCEmployee : NSObject @property (copy) NSString *name; @property NSUInteger salary; + (EOCEmployee*)employeeWithType:(EOCEmployeeType)type; - (void)doADayWork; @end // EOCEmployee.m @implementation EOCEmployee // 使用工厂模式创建类簇 + (EOCEmployee*)employeeWithType:(EOCEmployeeType)type{ // 根据传入的雇员类型参数返回不同的子类对象 switch (type) { case EOCEmployeeTypeDeveloper: return [EOCEmployeeDeveloper new]; break; case EOCEmployeeTypeDesigner: return [EOCEmployeeDesigner new]; break; case EOCEmployeeTypeFinance: return [EOCEmployeeFinance new]; break; } } -(void)doADayWork{ // 由子类实现 } @end // EOCEmployeeTypeDeveloper枚举类型对应的子类,其它两个子类类似 // EOCEmployeeDeveloper.h @interface EOCEmployeeDeveloper : EOCEmployee @end // EOCEmployeeDeveloper.m @implementation EOCEmployeeDeveloper - (void)doADayWork{ // 具体实现代码 } @end
系统框架中有许多类簇。大部分容器都是类簇,例如NSArray与NSMutableArray,虽然有两个抽象基类,但仍然可以合起来算作一个类簇。两个类共属于同一类簇,这意味着二者在实现各自类型的数组时可以共用实现代码,此外还可以把可变数组复制为不可变数组,反之亦然。
id maybeAnArray = /* . . . */; // b1永远为NO,因为[maybeAnArray class]返回的是隐藏在NSArray类簇公共接口后面的某个内部类型,而不是NSArray类本身 bool b1 = ([maybeAnArray class] == [NSArray class]); // 可以用isKindOfClass:方法来判定maybeAnArray所属的类是否位于NSArray类簇中 bool b2 = [maybeAnArray isKindOfClass:[NSArray class]];
对于Cocoa中NSArray这样的类簇来说,可以新增子类,但是需要遵守下面几条规则:
1. 子类应该继承自类簇中的抽象基类。
2. 子类应该定义自己的数据存储方式。
3. 子类应当重写超类文档中指明需要重写的方法(一般会定义在基类的开发文档中)。
第10条:在既有类中使用关联对象存放自定义数据
有时需要在对象中存放相关信息。这时我们通常会从对象所属的类中继承一个子类,然后改用这个子类对象。有时候类的实例可能是由某种机制所创建的,而开发者无法令这种机制创建出自己所写的子类实例。之中情况就需要用关联对象来解决问题。 可以给某关联对象关联许多其他对象,这些对象通过键来区分。存储对象值的时候可以指明存储策略,用以维护相应的内存管理语义。关联类型 | 等效的属性特质 |
---|---|
OBJC_ASSOCIATION_ASSIGN | assgin |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic,retain |
OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic,copy |
OBJC_ASSOCIATION_RETAIN | retain |
OBJC_ASSOCIATION_COPY | copy |
// 以给定的键和策略为某对象设置关联对象值 void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy) // 根据给定的键从某对象中获取关联对象值 id objc_getAssociatedObject(id object, void *key) // 移除指定对象的全部关联对象 void objc_removeAssociatedObjects(id object)
对关联对象的操作与对NSDictionary的操作类似,但有个重要差别,设置关联对象时用到的键是void*类型的。如果在两个键上调用isEqual方法返回的是YES,那么NSDictionary就认为二者相等,而设置关联对象时,若想要两个键匹配到同一个值,则二者必须是完全相同的指针才行,所以通常使用静态全局变量作为关联对象的键。 下面是一个关联对象用法的示例,示例使用的是UIAlertView类(这个类现在已经被废弃了,推荐使用UIAlertController类,这里仅作演示使用)
// 导入运行时系统框架 #import <objc/runtime.h> // 定义全局静态变量做键 static void *EOCMyAlertViewKey = "EOCMyAlertViewKey"; - (void)askUserAQuestion{ UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil]; // 定义一个处理按钮逻辑的块 void (^block)(NSUInteger) = ^(NSUInteger buttonIndex){ if (buttonIndex == 0) { // 停止 } else{ // 继续 } }; // 将块设置为alert的关联对象 objc_setAssociatedObject(alert, EOCMyAlertViewKey, block, OBJC_ASSOCIATION_COPY); [alert show]; } - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{ // 获取关联对象中的块并执行 void (^block)(NSUInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey); block(buttonIndex); }
这样做的好处是,将创建警告视图与处理操作结果的代码放到了一起,这样能增强代码的可读性。需要注意的是,块可能要捕获某些变量,这也许会造成保留环。因为关联对象之间的关系并没有正式的定义,其内存管理语句是在关联的时候才定义的,而不是接口中预先定好的。使用块作关联对象的时候一定要小心,更好的做法是将块保存为子类中的属性。
相关文章推荐
- C# 6.0的属性(Property)的语法与初始值详解
- C#中Property和Attribute的区别实例详解
- JavaScript中检查对象property的存在性方法介绍
- JavaScript中对象property的删除方法介绍
- JavaScript中遍历对象的property的3种方法介绍
- javascript 对象属性property与元素属性attribute的浏览器支持
- JavaScript Table排序 2.0 (更新)
- JavaScript对象的property属性详解
- Objective-C的内省(Introspection)用法小结
- JavaScript中property和attribute的区别详细介绍
- JavaScript中的property和attribute介绍
- JavaScript从数组的indexOf()深入之Object的Property机制
- javascript中attribute和property的区别详解
- JavaScript中对象property的读取和写入方法介绍
- Objective-C中常用的结构体NSRange,NSPoint,NSSize(CGSize),NSRect实例分析
- Objective-C中使用NSString类操作字符串的方法小结
- Objective-C中NSNumber与NSDictionary的用法简介
- Objective-C实现冒泡排序算法的简单示例
- Objective-C中NSLog输出格式大全
- Objective-C实现自定义的半透明导航