第17条:实现description方法
本条要点:(作者总结)
- 实现 description 方法返回一个有意义的字符串,用以描述该实例。
- 若想在调试时打印出来更详尽的对象描述信息,则应实现 debugDescription 方法。
调试程序时,经常需要打印并查看对象信息。一种办法是编写代码把对象的全部属性都输出到日志中。不过最常用的做法还是像下面这样:
1 NSLog(@"object = %@", object);
在构建需要打印到日志的字符串时,object 对象会收到 description 消息,该方法所返回的描述信息将取代 “格式字符串” (format string)里的 “%@”。比方说,object 是个数组,若用下列代码打印其信息:
1 NSArray *object = @[@"A string", @(123)]; 2 NSLog(@"object = %@", object);
则会输出:
1 object = ( 2 "A string", 3 123 4 )
然而,如果在自定义的类上这么做,那么输出的信息却是下面这样:
1 object = <EOCPerson: 0x7fd9a1600600>
与 object 为数组时输出的信息相比,上面这种内容不太有用。除非在自己的类里覆写 description 方法,否则打印信息时就会调用 NSObject 类所实现的默认方法。此方法定义在 NSObject 协议里,不过 NSObject 类也实现了它。因为 NSObject 并不是唯一的 “根类”,所以许多方法都要定义在 NSObject 协议里,比方说,NSProxy 也是一个遵从了 NSObject 协议的 “根类”。由于 description 等方法定义在 NSObject 协议里,因此像 NSProxy 这种 “根类”及其子类也必须实现它们。如前所见,这些实现好的方法并没有打印出较为有用的内容,只不过是输出了类名和对象的内存地址。只有在你想判断两指针是否真的指向同一对象时,这种信息才有用处。除此之外,再也看不出其他有用的内容了。我们想打印出来的对象信息应该比这更多才对。
要想输出更为有用的信息也很简单,只需覆写 description 方法并将描述此对象的字符串返回即可。例如,有下面这个代表个人信息的类:
1 #import <Foundation/Foundation.h> 2 3 @interface EOCPerson : NSObject 4 5 @property (nonatomic, copy, readonly) NSString *firstName; 6 @property (nonatomic, copy, readonly) NSString *lastName; 7 8 - (instancetype)initWithFirstName:(NSString *)firstName 9 lastName:(NSString *)lastName; 10 11 @end
1 #import "EOCPerson.h" 2 3 @implementation EOCPerson 4 5 - (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName { 6 if (self = [super init]) { 7 _firstName = [firstName copy]; 8 _lastName = [lastName copy]; 9 } 10 return self; 11 } 12 13 @end
该类的 description 方法通常可以这样实现:
1 - (NSString *)description { 2 return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">", [self class], self, _firstName, _lastName]; 3 }
假如按上面的代码来写,那么 EOCPerson 对象就会输出如下格式的信息:
1 EOCPerson *person = [[EOCPerson alloc] initWithFirstName:@"Bob" lastName:@"sSmith"]; 2 NSLog(@"person = %@", person);
打印信息:
1 person = <EOCPerson: 0x60000002b400, "Bob sSmith">
这样就必覆写之前所输出的信息更加清楚,也更有用了。笔者建议: 在新实现的 description 方法中,也应该像默认的实现那样,打印出类的名字和指针地址,因为这些内容有时也许会用到。不过大家刚才也看到了,NSArray 类的对象就没有打印这两项内容。显然,在实现 description 方法时,没有固定规则可循,应根据当前对象来决定在 description 方法里打印何种信息。
有个简单的办法,可以在 description 中输出很多互不相同的信息,那就是借助 NSDictionary 类的 description 方法。此方法输出的信息的格式如下:
1 { 2 key: value; 3 foo: bar; 4 }
在自定义的 description 方法中,把待打印的信息存放到字典里面,然后将字典对象的 description 方法所输出的内容包含在字符串里并返回,这样就可以实现精简的信息输出方式了。例如,下面这个类表示某地点的名称和地理坐标(纬度和经度):
1 #import <Foundation/Foundation.h> 2 3 @interface EOCLocation : NSObject 4 5 @property (nonatomic, copy, readonly) NSString *title; 6 @property (nonatomic, assign, readonly) float latitude; 7 @property (nonatomic, assign, readonly) float longitude; 8 9 - (instancetype)initWithTitle:(NSString *)title latitude:(float)latitude longitude:(float)longitude; 10 11 @end 12 13 #import "EOCLocation.h" 14 15 @implementation EOCLocation 16 17 - (instancetype)initWithTitle:(NSString *)title latitude:(float)latitude longitude:(float)longitude { 18 if (self = [super init]) { 19 _title = [title copy]; 20 _latitude = latitude; 21 _longitude = longitude; 22 } 23 return self; 24 } 25 26 @end
要是这个类的 description 方法能够打印出地名和经纬度就好了。我们可以像下面这样编写 description 方法,用 NSDictionary 来实现此功能:
1 - (NSString *)description { 2 return [NSString stringWithFormat:@"<%@: %p, %@>", [self class], self, @{@"title": _title, @"latitude": @(_latitude), @"longitude": @(_longitude)}]; 3 }
输出的信息格式是:
1 <EOCLocation: 0x60000002caa0, { 2 latitude = 34; 3 longitude = "163.45"; 4 title = Beijing; 5 }>
这比仅仅输出指针和类名要有用多了,而且对象中的每条属性都能打印的很好。也可以在格式字符串中直接为每个实例变量留好位置,然后逐个打印出来,不过,用 NSDictionary 来实现此功能可以令代码更易维护: 如果以后还要向类中新增属性,并且要在 description 方法中打印,那么只需修改字典内容即可。
NSObject 协议中还又个方法要注意,那就是 debugDescription,此方法的用意与 description 非常相似。二者区别在于,debugDescription 方法是开发者在调试器(debugger)中以控制台命令打印对象时才调用的。在 NSObject 类的默认实现中,此方法只是直接调用了 description。以 EOCPerson 类为例,我们在创建实例所用的代码后面插入断点,然后通过调试器(假设使用 LLDB)运行程序,使之暂停于此:
1 EOCPerson *person = [[EOCPerson alloc] initWithFirstName:@"Bob" lastName:@"sSmith"]; 2 NSLog(@"person = %@", person);
3 Breakpoint here
当程序运行到断点时,开发者就可以向调试控制台里输入命令了。LLDB 的 “po” 命令可以完成对象打印(print-object)工作,其输出如下:
1 po person 2 <EOCPerson: 0x60000002cee0, "Bob sSmith">
请注意,控制台中的内容是 debugDescription 所返回的信息。
你也许只想把人名放在 EOCPerson 对象的普通描述信息中,而把更详尽的内容放在调试所用的描述信息里,此时可用下列代码实现这两个方法:
1 - (NSString *)description { 2 return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">", [self class], self, _firstName, _lastName]; 3 } 4 5 - (NSString *)debugDescription { 6 return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">", [self class], self, _firstName, _lastName]; 7 }
写好之后,再把钢才的程序码运行一遍,这次 po 命令所打印出来的对象信息如下所示:
你可能不想把类名与指针地址这种额外内容放在普通的描述信息里,但是却希望调试的时候能够很方便地看到它们,在此情况下,就可以使用这种输出方式来实现。Foundation 框架的 NSArray 类就是这么做的。例如:
1 NSArray *array = @[@"Effective Objective-C 2.0", @(123), @(YES)]; 2 NSLog(@"array = %@", array); 3 // Breakpoint here
运行上述程序码,待其停在断点处,然后用 po 命令打印数组对象,就可以看到如下信息:
1 array = ( 2 "Effective Objective-C 2.0", 3 123, 4 1 5 ) 6 (lldb) po array 7 <__NSArrayI 0x60000004f270>( 8 Effective Objective-C 2.0, 9 123, 10 1 11 )
END