如何重载description 和 debugDescription 赞
Item 17: Implement the description Method
While debugging, you will often find it useful to print out an object to inspect it. One way is to write logging code to print out all the properties of the object. But the most usual way is to do something like this:
NSLog(@"object = %@", object);
When the string to log is built up, object
will be sent the description message and put in place of the %@
in the format string. So, for instance, if the object were an array, the output might look like this:
NSArray *object = @[@"A string", @(123)];
NSLog(@"object = %@", object);
Which outputs this:
object = (
"A string",
123
)
However, if you try this on a class of your own, you’ll often see something that looks more like this:
object = <EOCPerson: 0x7fd9a1600600>
That is not as helpful as the array! Unless you override description
in your own class, the default implementation from NSObject
will be invoked. The method is defined on the NSObject
protocol, but the NSObject
class implements it. Many methods are part of the NSObject
protocol, and it’s done this way because NSObject
is not the only root class. NSProxy
is an example of another root class, which conforms to the NSObject
protocol. Because methods like description
are defined within the protocol, subclasses of these other root classes also must implement them. That implementation doesn’t do much, as you can see. What it does do is show the class name alongside the memory address of the object. This would be useful to you only if you wanted to see whether two objects were exactly the same object. However, you can’t tell any more about the objects than that. It’s more likely that you are going to want to know some more information about the object.
To make the output more useful, all you need to do is override description
to return the string you want to represent your object. For example, consider a class to describe a person:
#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
A typical description
method implementation for this would be as follows:
- (NSString*)description {
return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">",
[self class], self, _firstName, _lastName];
}
If this were to be used, the output for an object of type EOCPerson
would now look like the following:
EOCPerson *person = [[EOCPerson alloc]
initWithFirstName:@"Bob"
lastName:@"Smith"];
NSLog(@"person = %@", person);
// Output:
// person = <EOCPerson: 0x7fb249c030f0, "Bob Smith">
This is clear and contains much more useful information. I suggest displaying the class name and pointer address just like the default implementation, simply because it’s useful to see sometimes. Although as you saw earlier, NSArray
doesn’t do this, and there certainly is no rule about it. What you choose to put in the description is whatever makes sense for the object in question.
A simple way to write a description
method containing a lot of different bits of information is to piggyback on NSDictionary
’s description
method. It returns something that looks like this:
{
key: value;
foo: bar;
}
This compact description can be used by forming a dictionary within your owndescription
method and returning a string containing this dictionary’s description
method. For example, the following class describes a location with a title and coordinates (latitude and longitude):
#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
It would be nice if the description
method for this showed the title as well as the latitude and longitude. If an NSDictionary were used
, the description
method would look like this:
- (NSString*)description {
return [NSString stringWithFormat:@"<%@: %p, %@>",
[self class],
self,
@{@"title":_title,
@"latitude":@(_latitude),
@"longitude":@(_longitude)}
];
}
The output would look like this:
location = <EOCLocation: 0x7f98f2e01d20, {
latitude = "51.506";
longitude = 0;
title = London;
}>
This is much more useful than just the pointer and class name, and all the properties of the object are nicely presented. You could also have used a string format that was formed of each of the instance variables, but the NSDictionary
approach is easier to maintain when more properties are added to the class and want to form part of thedescription
method.
Another method to be aware of, also part of the NSObject
protocol, isdebugDescription
, whose purpose is very similar to description
. The difference is that debugDescription
is the method called when you invoke the print-object command within the debugger. The default implementation within the NSObject
class simply calls directly through to description
. For example, taking the EOCPerson
class, running an application in the debugger (LLDB in this case), and pausing at a breakpoint just after creating an instance looks like this:
EOCPerson *person = [[EOCPerson alloc]
initWithFirstName:@"Bob"
lastName:@"Smith"];
NSLog(@"person = %@", person);
// Breakpoint here
When the breakpoint has been hit, the debug console will be ready to receive input. The command po
in LLDB will perform the print-object function, yielding the following:
EOCTest[640:c07] person = <EOCPerson: 0x712a4d0, "Bob Smith">
(lldb) po person
(EOCPerson *) $1 = 0x0712a4d0 <EOCPerson: 0x712a4d0, "Bob Smith">
Note that the (EOCPerson *) $1 = 0x712a4d0
is added by the debugger. The portion after that is what is returned from the debug-description method.
You may decide to make the normal description of an EOCPerson
to be just the person’s name and then implement the debug-description method to provide the more thorough description. In that case, the two methods would look like this:
- (NSString*)description {
return [NSString stringWithFormat:@"%@ %@",
_firstName, _lastName];
}
- (NSString*)debugDescription {
return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">",
[self class], self, _firstName, _lastName];
}
This time, running the same code as previously and using the print-object command will yield the following:
EOCTest[640:c07] person = Bob Smith
(lldb) po person
(EOCPerson *) $1 = 0x07117fb0 <EOCPerson: 0x7117fb0, "Bob Smith">
This output can be particularly useful when you don’t want all that extra information about the class name and pointer address to be visible in the normal description but still want the ability to access it easily during debugging. An example of a class that does this from the Foundation framework is NSArray
. For example:
NSArray *array = @[@"Effective Objective-C 2.0", @(123), @(YES)];
NSLog(@"array = %@", array);
// Breakpoint here
In that case, running, stopping at the breakpoint, and printing out the array object looks like this:
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
)
Things to Remember
Implement the description method to provide a meaningful string description of instances.
If the object description could do with more detail for use during debugging, implement debugDescription
.