In this article, we’ll look at how to write value objects in Objective-C. In doing that, we’ll touch upon important protocols and methods in Objective-C. A value object is an object that holds some values, and can be compared for equality. Often, value objects can be used as model objects. For example, consider a simple Personobject:

在这篇文章中,我们将会讨论一下在 Objective-C 中的值对象。在这个过程中,我们会触及到 Objective-C 中一些重要的协议和方法。一个 值对象(value object) 就是一个拥有多个值的对象,并且可以比较是否相等。通常,值对象是用于数据模型对象。例如,我们考虑这样一个简单的 Person 对象:

@interface Person : NSObject

@property (nonatomic,copy) NSString* name;
@property (nonatomic,strong) NSDate* birthDate;
@property (nonatomic) NSUInteger numberOfKids;

@end

Creating these kinds of objects is the bread and butter of our work, and while these objects look deceivingly simple, there are a lot of subtleties involved.

创建类似这样的对象是相当平常的,尽管看起来很简单,但是其中仍有一些微妙的地方需要考虑。

One thing that a lot of us learned the hard way is that these objects should be immutable. Once you create a Person object, it’s impossible to change it anymore. We’ll touch upon mutability later in this issue.

我们知道,这些对象应该是不可变的(immutable)。也即说,一旦创建了一个Person对象,就不能再修改它了。在本文的后面我们将讨论可变性(mutability )。

Properties

The first thing to notice is that we use properties to define the attributes that aPerson has. Creating the properties is quite mechanical: for normal object properties, you make them nonatomic and strong, and for scalar properties you make them just nonatomic. By default, they’re also assign. There’s one exception: for properties that have a mutable counterpart, you want to define them as copy. For example, the name property is of type NSString. What could happen is that somebody creates a Person object and assigns a value of type NSMutableString. Then, sometime later, he or she might change the mutable string. If our property would have been strong instead of copy, our Person object would have changed too, which is not what we want. It’s the same for containers, such as arrays or dictionaries.

首先,注意到我们使用 property 来定义一个 Person 对象拥有的属性 (attribute)。创建这些 property 是相当机械的,对于普通的 property ,你可以用 nonatomic 和 strong 修改。对于标量属性(scalar property,即数值类型变量)通常是用 nonatomic 修饰。默认情况下,这些属性也是 assign 。但是有一个例外:对于某些 property 有一个可变类型的副本,你应该用 copy 修饰。例如:name 属性的类型是 NSString。如果创建了一个 Person 对象,然后为 name 属性赋值一个 NSMutableString 类型的值。然后,很有可能修改了这个可变字符串的值。如果属性是被 strong 修饰而非 copy ,那么 Person 对象仍然可以被修改,当然这不是我们想要的。对于数组,字典等的集合容器,情况也是如此。

Be aware that the copy is shallow; the containers might still contain mutable objects. For example, if you have an NSMutableArray* a containing NSMutableDictionary elements, then [a copy] will give you an immutable array, but the elements will be the same NSMutableDictionary objects. As we’ll see later, copy for immutable objects is free, but it increases the retain count.

注意到 copy 是浅拷贝;集合容器中仍有可能包含了可变类型的对象。例如:对于一个可变类型的数组 NSMutableArray *a 包含一个 NSMutableDictionary 对象,然后通过   [a copy] 可以得到一个不可变类型的数组。但是这个数组同样含有一个相同的 NSMutableDictionary 对象。所以对于一个不可变类型的对象进行copy 操作,只是增加了 retain 计数。

译者注:

(1)例子1:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[])
{

    @autoreleasepool {
        
        NSString * a = [NSString stringWithFormat:@"%d", 10];
        
		NSLog(@"%p", a);
		
		NSString * b = [a copy];
		
		NSLog(@"%p", b);
        
    }
    return 0;
}

结果输出:

2014-01-27 15:02:44.159 Foun[850:303] 0x7fff72ae20a0
2014-01-27 15:02:44.163 Foun[850:303] 0x7fff72ae20a0

(2)例子2:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[])
{

    @autoreleasepool {
        
        NSString * a = [NSMutableString stringWithFormat:@"%d", 10];
        
		NSLog(@"%p", a);
		
		NSString * b = [a copy];
		
		NSLog(@"%p", b);
        
    }
    return 0;
}

结果输出:

2014-01-27 15:03:58.353 Foun[862:303] 0x100204130
2014-01-27 15:03:58.357 Foun[862:303] 0x7fff72ae20a0

In older code, you might not see properties, as they are a relatively recent addition to Objective-C. Instead of properties, you might see custom getters and setters, or plain instance variables. For modern code, it seems most people agree on using properties, which is also what we recommend.

在旧的带中,你可能没见过 property,因为这个在之后才添加到 Objective-C 中的。除了property,你可能看过自定义的 getter 和 setter 方法,或者普通的实例变量。在现在的代码中,越来越多的使用 property,而这也是我们所推荐的。

More Reading

NSStringcopy or retain

下面给出一个例子,简单的了解一下 copy 和 retain 对于 NSString 的不同:

@interface Person : NSObject

@property (nonatomic,retain) NSString* name;
@property (nonatomic,strong) NSDate* birthDate;
@property (nonatomic) NSUInteger numberOfKids;

@end

Person *person = [[Person alloc] init];
        
        NSMutableString *someName = [NSMutableString stringWithFormat:@"Chris"];
        
        person.name = someName;
        
        [someName setString:@"Debajit"];
        
        NSLog(@"name = %@",person.name);

①如果,Person的name用retain修饰,输出结果为:Debajit

② 如果,Person的name用copy修饰,输出结果为:Chris

小结:有NSString类型的property可能被传入一个NSString或者NSMutableString类型的值,所以为了防止NSString类型的property值在程序运行过程发生变化,推荐是使用copy修饰。

Initializers

If we want immutable objects, we should make sure that they can’t be modified after being created. We can do this by adding an initializer and making our properties readonly in the interface. Our interface then looks like this:

如果我们需要的是不可变的对象,即要求对象在创建之后即不可修改。可以在 interface 中添加一个初始化方法并且用 readonly 修饰属性。例如:

@interface Person : NSObject

@property (nonatomic,copy,readonly) NSString* name;
@property (nonatomic,strong,readonly) NSDate* birthDate;
@property (nonatomic,readonly) NSUInteger numberOfKids;

- (instancetype)initWithName:(NSString*)name
                   birthDate:(NSDate*)birthDate
                numberOfKids:(NSUInteger)numberOfKids;

@end

Then, in our implementation, we have to make our properties readwrite, so that the instance variables get generated:

接着,在实现中,必须使这些属性 可读可写,这样才可以生成实例变量:

@interface Person ()

@property (nonatomic,copy) NSString* name;
@property (nonatomic,strong) NSDate* birthDate;
@property (nonatomic) NSUInteger numberOfKids;


@end

@implementation Person

- (instancetype)initWithName:(NSString*)name
                   birthDate:(NSDate*)birthDate
                numberOfKids:(NSUInteger)numberOfKids
{
    self = [super init];
    if (self) {
        self.name = name;
        self.birthDate = birthDate;
        self.numberOfKids = numberOfKids;
    }
    return self;
}

@end

Now, we can construct new Person objects, but not modify them anymore. This is very helpful; when writing other classes that work with Person objects, we know that these values won’t change as we are working with them.

现在我们可以创建一个新的 Person 的对象,但是不能修改它们。这是很有用的;我们知道,在其他类和 Person打交道的时候,Person 对象的值不会发生变化。

Comparing for Equality

To compare for equality, we have to implement the isEqual: method. We want theisEqual: method to be true if and only if all properties are equal. There are two good articles by Mike Ash (Implement Equality and Hashing) and NSHipster (Equality) that explain how to do this. First, let’s write isEqual::

为了比较相等,我们必须实现 isEqual: 方法。当且仅当所有的属性都相等,isEqual: 方法才返回真。

- (BOOL)isEqual:(id)obj
{
    if(![obj isKindOfClass:[Person class]]) return NO;
    
    Person* other = (Person*)obj;

    BOOL nameIsEqual = self.name == other.name || [self.name isEqual:other.name];
    BOOL birthDateIsEqual = self.birthDate == other.birthDate || [self.birthDate isEqual:other.birthDate];
    BOOL numberOfKidsIsEqual = self.numberOfKids == other.numberOfKids;
    return nameIsEqual && birthDateIsEqual && numberOfKidsIsEqual;
}

Now, we check if we’re the same kind of class. If not, we’re definitely not equal. Then, for each object property, we check if the pointer is equal. The left operand of the ||might seem superfluous, but it’s there to return YES if both properties are nil. To compare scalar values like NSUInteger for equality, we can just use ==.

首先,检查是否是来自相同的类,如果不是,那么肯定不相等。然后,依次比较对象中的每一个属性是否相等。 ||  左边的似乎看起来有点多余,但是有可能两个属性值都为nil的时候,返回 YES;或者比较两个标量值,像NSUInteger的时候,也是要用到 ==。

One thing that’s good to note: here, we split up the different properties into their own BOOLs. In practice, it might make more sense to combine them into one big clause, because then you get the lazy evaluation for free. In the example above, if the name is not equal, we don’t need to check any of the other properties. By combining everything into one if statement we get that optimization for free.

还有一点需要注意的是:我们将不同属性值的比较结果都用 BOOL 保存。在实际中,更通常的是将他们整合到一个判断语句中,这样就可以用到短路求值。例如上面例子中,如果 name 属性不相等,我们就不必再比较其他属性是否相等了。通过将所有的属性比较整合到一个 if 语句中更有效率。

Next, as per the documentation, we need to implement a hash function as well.

同样,按官方文档中所说的那样,我们还需要实现一个哈希函数。

 Apple says:

If two objects are equal, they must have the same hash value. This last point is particularly important if you define isEqual: in a subclass and intend to put instances of that subclass into a collection. Make sure you also define hash in your subclass.

如果两个对象相等,它们必须有相同的哈希值。假如你在自定义类中已经实现了isEqual方法,并且你想将该自定义类的实例对象放入到集合中,那么你就必须要确保在自定义类中实现了哈希函数。

First, we can try to run the following code, without having a hash function implemented:

首先,在没有实现哈希函数的情况下,运行下面的代码:

Person* p1 = [[Person alloc] initWithName:name birthDate:start numberOfKids:0];
Person* p2 = [[Person alloc] initWithName:name birthDate:start numberOfKids:0];
NSDictionary* dict = @{p1: @"one", p2: @"two"};
NSLog(@"%@", dict);

The first time I ran the above code, everything was sort of okay, and there were two items in the dictionary. The second time, there was only one. Things just get very unpredictable, so let’s just do as the documentation says.

第一次运行上面的代码,一切正常,在字典中有两项。但是第二次运行的时候,就出现问题了。

译者注:为了确保代码可以运行,在Person类中要实现NSCoding协议。

- (id)copyWithZone:(NSZone *)zone
{
    Person* p = [[Person allocWithZone:zone] initWithName:self.name
                                                birthDate:self.birthDate
                                             numberOfKids:self.numberOfKids];
    return p;
}

下面会有关于NSCoding协议的介绍。

As you might remember from your computer science classes, writing a good hash function is not easy. A good hash function needs to be deterministic and uniform. Deterministic means that the same input needs to generate the same hash value. Uniform means that the result of the hash function should map the inputs uniformly over the output range. The more uniform your output, the better the performance when you use these objects in a collection.

正如我们在计算机科学课上学到的,写出一个好的哈希函数并非易事。一个好的哈希函数要求确定性和均匀性。确定性意味着相同的哈希函数输入,必须生成相同的哈希函数值。均匀性意味着哈希函数的结果必须是有输入内容均匀映射的结果。输出的结果越均匀,在集合中使用这些对象的性能就越好。

First, just for kicks, let’s have a look at what happens when we don’t have a hash function, and we try to use Person objects as keys in a dictionary:

首先,我们将 Person 对象作为字典的key值,运行下面代码,我们看看假如不添加哈希函数会发生什么?

NSMutableDictionary* dictionary = [NSMutableDictionary dictionary];

NSDate* start = [NSDate date];
for (int i = 0; i < 50000; i++) {
    NSString* name = randomString();
    Person* p = [[Person alloc] initWithName:name birthDate:[NSDate date] numberOfKids:i++];
    [dictionary setObject:@"value" forKey:p];
}
NSLog(@"%f", [[NSDate date] timeIntervalSinceDate:start]);

This takes 29 seconds on my machine. In comparison, when we implement a basic hash function, the same code runs in 0.4 seconds. These are not proper benchmarks, but do give a very good indication of why it’s important to implement a proper hashfunction. For the Person class, we can start with a hash function like this:

在我的机器上运行花费了29秒。作为比较,当我们实现了一个最基本的哈希函数,相同的代码运行仅需要 0.4秒。这可能不是很精准,但是给我们一个很好的指示:这就是实现一个合适的哈希函数的重要性。对于 Person 类,我们可以使用下面这个哈希函数:

- (NSUInteger)hash
{
    return self.name.hash ^ self.birthDate.hash ^ self.numberOfKids;
}

This will take the three hashes from our properties and XOR them. In this case, it’s good enough for our purposes, because the NSString hashing function is good for short strings (it used to only perform well for strings up to 96 characters, but now that has changed. See CFString.c, look for hash). For serious hashing, your hashing function depends on the data you have. This is covered in Mike Ash’s article andelsewhere.

这个哈希函数将我们的属性进行XOR异或操作。在这个例子中,运行效果还好。但是严格意义上的哈希函数依赖于数据格式。

In the documentation for hash, there’s the following paragraph:

在文档中关于哈希函数,有如下一段解释:

If a mutable object is added to a collection that uses hash values to determine the object’s position in the collection, the value returned by the hash method of the object must not change while the object is in the collection. Therefore, either the hash method must not rely on any of the object’s internal state information or you must make sure the object’s internal state information does not change while the object is in the collection. Thus, for example, a mutable dictionary can be put in a hash table but you must not change it while it is in there. (Note that it can be difficult to know whether or not a given object is in a collection.)

将一个可变对象添加到集合中,是使用哈希值去判断该对象在集合中的位置,所以要求哈希函数返回的值不能发生变化。所以,要么哈希函数不依赖与对象的内部状态信息,要么你必须保证集合中对象的内部状态信息不发生变化。因此,例如说,一个可变类型的字典添加到一个 hash table中,但是你不能修改这个可变字典。(提示:但是很难注意到给定的对象是否在集合中。)

This is another very important reason to make sure your objects are immutable. Then you don’t even have to worry about this problem.

这是另一个很重要的原因要确保你的对象是不可变的。这样你就不必考虑这个问题了。

More Reading

NSCopying

To make sure our objects are useful, it’s convenient to have implemented theNSCopying protocol. This allows us, for example, to use them in container classes. For a mutable variant of our class, NSCopying can be implemented like this:

为了确保我们的对象是有用的,需要实现NSCopying协议中的方法。这样,我们就可以在集合容器类中使用这些自定义对象。对于可变类型的类,实现 NSCopying 协议如下:

译者注:NSCopying协议中只有一个方法:

@protocol NSCopying

- (id)copyWithZone:(NSZone *)zone;

@end

- (id)copyWithZone:(NSZone *)zone
{
    Person* p = [[Person allocWithZone:zone] initWithName:self.name
                                                birthDate:self.birthDate
                                             numberOfKids:self.numberOfKids];
    return p;
}

However, in the protocol documentation, they mention another way to implementNSCopying:

然而,在协议文档中,提到另一种方式实现NSCopying协议:

Implement NSCopying by retaining the original instead of creating a new copy when the class and its contents are immutable.

如果自定义类及其内容是不可变的,那么实现NSCopying可以通过保留原有的对象,而不是创建一个新的副本。

So, for our immutable version, we can just do this:

所以,对于不可变版本,我们只需要:

- (id)copyWithZone:(NSZone *)zone
{
    return self;
}

NSCoding

If we want to serialize our objects, we can do that by implementing NSCoding. This protocol exists of two required methods:

如果我们想序列话对象,我们需要实现 NSCoding 协议。这个协议有两个必须实现的协议方法:

- (id)initWithCoder:(NSCoder *)decoder
- (void)encodeWithCoder:(NSCoder *)encoder

Implementing this is equally straightforward as implementing the equals methods, and also quite mechanical:

实现这两个方法也是相当的简单和机械:

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super init];
    if (self) {
        self.name = [aDecoder decodeObjectForKey:@"name"];
        self.birthDate = [aDecoder decodeObjectForKey:@"birthDate"];
        self.numberOfKids = [aDecoder decodeIntegerForKey:@"numberOfKids"];
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:self.name forKey:@"name"];
    [aCoder encodeObject:self.birthDate forKey:@"birthDate"];
    [aCoder encodeInteger:self.numberOfKids forKey:@"numberOfKids"];
}

Read more about it on NSHipster and Mike Ash’s blog. By the way, don’t useNSCoding when dealing with untrusted sources, such as data coming from the network, because the data may be tampered with. By modifying the archived data, it’s very possible to perform a remote code execution attack. Instead, useNSSecureCoding or a custom format like JSON.

顺便说一句,不要使用 NSCoding 协议处理不受信任的来源,例如网络数据,因为这些数据可能被篡改。通过修改归档数据,很有可能只需一个远程代码攻击。取而代之的是,使用 NSSecureCoding 或者自定义格式如JSON.

Mantle

Now, we’re left with the question: can we automate this? It turns out we can. One way would be code generation, but luckily there’s a better alternative: Mantle. Mantle uses introspection to generate isEqual: and hash. In addition, it provides methods that help you create dictionaries, which can then be used to write and read JSON. Of course, doing this generically and at runtime will not be as efficient as writing your own, but on the other hand, doing it automatically is a process that is much less prone to errors.

现在,我们有一个疑问:能否自动化实现这些呢?事实证明,可以的。一种方法是代码生成,幸运的是,有一个更好的选择: Mantle Mantle 通过内置的方法产生 isEqual 和 hash 方法。除此之外,它提供了一些方法帮助你创建字典,然后用于读写JSON数据。当然,通过这种方法一般运行效率都比不上自己代码实现,但是另一方面,自动实现这个过程更不容易代码出错。

Mutability

In C, and also in Objective-C, mutable values are the default. In a way, they are very convenient, in that you can change anything at anytime. When building smaller systems, this is mostly not a problem. However, as many of us learned the hard way, when building larger systems, it is much easier when things are immutable. In Objective-C, we’ve had immutable objects for a long time, and now other languages are also starting to add it.

在C语言或者 Objective-C中,变量值默认都是可变的。某种程度上,这样是很方便的,因为你随时可以修改。在小系统中应用,这没有什么大问题。但是在大系统应用中,不可变类型的变量是更常用的。在 Objective-C 中,不可变类型的对象已经存在很长时间了,现在其它编程语言也开始加入不可变类型了。

We’ll look at two problems with mutable objects. One is that they might change when you don’t expect it, and the other one is when using mutable objects in a multithreading context.

现在,我们来看看可变类型对象存在的两个问题。其一是:非预期的变化;其二是:多线程环境下使用可变类型对象。

Unexpected Changes

Suppose we have a table view controller, which has a people property:

假设我们有一个表视图控制器,其中有一个 people 属性:

@interface ViewController : UITableViewController

@property (nonatomic, strong) NSArray* people;

@end

And in our implementation, we just map each array element to a cell:

在实现中,数组每一个元素对应一个表单元:

 - (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView
 {
     return 1;
 }
 
 - (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section
 {
     return self.people.count;
 }

Now, in the code that sets up the above view controller, we might have code like this:

现在,使用如下代码实现上述的视图控制器:

self.items = [NSMutableArray array];
[self loadItems]; // Add 100 items to the array
tableVC.people = self.items;
[self.navigationController pushViewController:tableVC animated:YES];

The table view will start firing methods like tableView:numberOfRowsInSection:, and at first, everything will be fine. But suppose that, at some point, we do the following:

表视图最先会调用  tableView:numberOfRowsInSection: 方法,正常情况下,运行没有问题。但是假如,某个时候:

[self.items removeObjectAtIndex:1];

This changes our items array, but it also changes the people array in our table view controller. If we do this without any further communication to the table view controller, the table view will still think there are 100 items, whereas our array will only contain 99. Bad things will happen. Instead, what we should have done is declare our property as copy:

这修改了 items 数组,但是这也改变了表视图控制器中的 people 数组。如果我们不通知表视图控制器,那么表视图仍以为有100项,然后数组中只有 99 项。这时,Bad things will happen. 然而,如果我们声明这个数组属性为 copy:

 @interface ViewController : UITableViewController
 
 @property (nonatomic, copy) NSArray* items;
 
 @end

Now, whenever we assign a mutable array to items, an immutable copy will be made. If we assign a regular (immutable) array value, the copy operation is free, and it only increases the retain count.

现在,当我们赋值给 items 数组一个可变数组,就会进行复制;如果赋值给 items 数组一个不可变数组,就不进行复制,只是增加数组的retain计数。

译者小结:为了防止数组NSArray对象在程序运行过程中发生变化,推荐使用 copy 修饰。

Multithreading

Suppose we have a mutable object, Account, representing a bank account, that has a method transfer:to::

假设有一个可变对象 Account,表示一个银行账号,其由一个方法:transfer:to:

- (void)transfer:(double)amount to:(Account*)otherAccount
{
    self.balance = self.balance - amount;
    otherAccount.balance = otherAccount.balance + amount;
}

Multithreaded code can go wrong in many ways. For example, if thread A readsself.balance, thread B might be modifying it before thread A continues. For a good explanation of all the dangers involved, see our second issue.

多线程的代码很容易出错。例如,线程A读self.balance,线程B可能在线程A读之前修改 self.balance。对于更多解释介绍,可以看  second issue

If we have immutable objects instead, things are much easier. We cannot modify them, and this forces us to provide mutability at a completely different level, yielding much simpler code.

如果是不可变对象,一切就简单多了。我们不能对其进行修改,这就迫使我们在不同的层次提供可变现,以产生更简单的代码。

Caching

Another thing where immutability can help is when caching values. For example, suppose you’ve parsed a markdown document into a tree structure with nodes for all the different elements. If you want to generate HTML out of that, you can cache the value, because you know no children will ever change. If you have mutable objects, you would need to either generate the HTML from scratch every time, or build optimizations and observe every single object. With immutability, you don’t have to worry about invalidating caches. Of course, this might come with a performance penalty. In almost all cases, however, the simplicity will outweigh the slight decrease in performance.

Immutability in Other Languages

Immutable objects are one of the concepts inspired by functional programming languages like Haskell. In Haskell, values are immutable by default. Haskell programs typically have a purely functional core, where there are no mutable objects, there is no state, and there are no side-effects like I/O.

We can learn from this in Objective-C programming. By having immutable objects where possible, our programs become much easier to test. There’s a great talk by Gary Bernhardt that shows how having immutable objects helps us write better software. In the talk he uses Ruby, but the concepts apply equally well to Objective-C.


Further Reading