第三章 接口与API设计—第17条:实现description方法

测试程序时,经常需要打印并查看对象信息。一种办法是编写代码把对象的全部属性都输出到日志中。不过最常用的做法还是像下面这样:

NSLog(@"object = %@", object);

在构建需要打印到日志的字符串时,object对象会收到description消息,该方法所返回的描述信息将取代"格式字符串"(format string)里的"%@"。比方说,object是个数组,若用下列代码打印其信息:

NSArray *object = @[@"A string", @(123)];
NSLog(@"object = %@", object);

则会输出:

object = (
     "A string",
     123
)

然而,如果在自定义的类上这么做,那么输出的信息却是下面这样:

object = <EOCPerson: 0x7fd9a1600600>

与object为数组时所输出的信息相比,上面这种内容不太有用。除非在自己的类里覆写description方法,否则打印信息时就会调用NSObject类所实现的默认方法。此方法定义在NSObject协议里,不过NSObject类也实现了它。因为NSObject并不是唯一的"根类",所以许多方法都要定义在NSObject协议里。比方说,NSProxy也是一个遵从了NSObject协议的"根类"。由于description等方法定义在NSObject协议里,因此像NSProxy这种"根类"及其子类也必须实现它们。如前所见,这些实现好的方法并没有打印出较为有用的内容,只不过是输出了类名和对象的内存地址。只有在你想判断两指针是否真的指向同一对象时,这种信息才有用处。除此之外,再也看不出其他有用的内容了。我们想打印出来的对象信息应该比这更多才对。
要想输出更为有用的信息也很简单,只需覆写description方法并将描述此对象的字符串返回即可。例如,有下面这个代表个人信息的类:

#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject

@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;

- (id)initWithFirstName:(NSString *)firstName
                       lastName:(NSString *)lastName;
@end

@implementation EOCPerson
- (id)initWithFirstName:(NSString *)firstName
                       lastName:(NSString *)lastName
{
        if ((self == [super init])) {
              _firstName = [firstName copy];
              _lastName = [lastName copy];
        }
        return self;
}
@end

该类的description方法通常可以这样实现:

- (NSString*)description {
    return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">", 
            [self class], self, _firstName, _lastName];
}

假如按上面的代码来写,那么EOCPerson对象就会输出如下格式的信息:

EOCPerson *person = [[EOCPerson alloc] initWithFirstName: @"Bob" lastName:@"Smith"];
NSLog(@"person = %@", person);
//Output:
//person = <EOCPerson: 0x7fb249c030f0, "Bob Smith">

这样就比覆写之前所输出的信息更加清楚,也更为有用了。笔者建议: 在新实现的description方法中,也应该像默认的实现那样,打印出类的名字和指针地址,因为这些内容有时也许会用到。不过大家刚才也看到了,NSArray类的对象就没有打印这两项内容。显然,在实现description方法时,没有固定规则可循,应根据当前对象来决定在description方法里打印何种信息。
有个简单的方法,可以在description中输出很多互不相同的信息,那就是借助NSDictionary类的description方法。此方法输出的信息的格式如下:

{
      key: value;
      foo: bar;
}

在自定义的description方法中,把待打印的信息放到字典里面,然后将字典对象的description方法所输出的内容包含在字符串里并返回,这样就可以实现精简的信息输出方式了。例如,下面这个类表示某地点的名称和地理坐标(纬度与经度):

#import <Foundation/Foundation.h>

@interface EOCLocation : NSObject
@property (nonatomic, copy, readonly) NSString *title;
@property (nonatomic, assign, readonly) float latitude;
@property (nonatomic, assign, readonly) float longitude;
- (id)initWithTitle:(NSString*)title latitude:(float)latitude longitude:(float)longitude;
@end

@implementation EOCLocation
- (id)initWithTitle:(NSString*)title latitude:(float)latitude longitude:(float)longitude {
    if ((self = [super init])) {
        _title = [title copy];
        _latitude = latitude;
        _longitude = longitude;
    }
    return self;
}
@end

要是这个类的description方法能够打印出地名和经纬度就好了。我们可以像下面这样编写description方法,用NSDictionary来实现此功能:

- (NSString*)description {
    return [NSString stringWithFormat:@"<%@: %p, %@>",
            [self class], 
            self, 
            @{@"title":_title, 
              @"latitude":@(_latitude), 
              @"longitude":@(_longitude)}
           ];
}

输出的信息格式为:

location = <EOCLocation: 0x7f98f2e01d20, {
        latitude = "50.506";
        longitude = 0;
        title = London;
}>

这比仅仅输出指针和类名要有用多了,而且对象中的每条属性都能打印得很好。也可以在格式字符串中直接为每个实例变量留好位置,然后逐个打印出来,不过,用NSDictionary来实现此功能可以令代码更易维护: 如果以后还要向类中新增属性,并且要在description方法中打印,那么只需修改字典内容即可。
NSObject协议中还有个方法要注意,那就是debugDescription,此方法的用意与description非常相似。二者区别在于,debugDescription方法是开发者在调试器(debugger)中以控制台命令打印对象时才调用的。在NSObject类的默认实现中,此方法只是直接调用了description。以EOCPerson类为例,我们在创建实例所用的代码后面插入断点,然后通过调试器(假设使用LLDB)运行程序,使之暂停于此:

EOCPerson *person = [[EOCPerson alloc] initWithFirstName:@"Bob" lastName:@"Smith"];
NSLog(@"person = %@", person);
// Breakpoint here

当程序运行到断点时,开发者就可以向调试控制台里输入命令了。LLDB的"po"命令可以完成对象打印(print-object)工作,其输出如下:

EOCTest[640:c07] person = <EOCPerson: 0x712a4d0, "Bob Smith">(lldb) po person
(EOCPerson *) $1 = 0x0712a4d0 <EOCPerson: 0x712a4d0, "Bob Smith">

请注意,控制台中的"(EOCPerson *) $ 1= 0x712a4d0"是由调试器所添加的,其后的内容才是由debugDescription所返回的信息。
你也许只想把人名放在EOCPerson对象的普通描述信息中,而把更详尽的内容放在调试所用的描述信息里,此时可用下列代码实现这两个方法:

- (NSString*)description {
    return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName];
}

- (NSString*)debugDescription {
    return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">", 
            [self class], self, _firstName, _lastName];
}

写好之后,再把刚才的程序码运行一遍,这次po命令所打印出来的对象信息如下所示:

EOCTest[640:c07] person = Bob Smith
(lldb) po person
(EOCPerson *)$1 = 0x07117fb0<EOCPerson: 0x7117fb0, "Bob Smith">

你可能不想把类名与指针地址这种额外内容放在普通的描述信息里,但是却希望调试的时候能够很方便地看到它们,在此情况下,就可以使用这种输出方式来实现。Foundation框架的NSArray类就是这么做的。例如:

NSArray *array = @[@"Effective Objective-C 2.0", @(123), @(YES)];
NSLog(@"array = %@", array);
// Breakpoint here

运行上述程序码,待其停在断点处,然后用po命令打印数组对象,就可以看到如下信息:

EOCTest[713:c07] array = (
      "Effective Objective-C 2.0",
      123,
       1
)
(lldb) po array
(NSArray *)$1 = 0x071275b0 <_NSArrayI 0x71275b0>(
Effective Objective-C 2.0,
123,
1
)

要点
实现description方法返回一个有意义的字符串,用以描述该实例。
若想在调试时打印出更详尽的对象描述信息,则应实现debugDescription方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容