在这篇文章中,我们将看看如何用Objective-C语言编写值对象。在编写时,我们将会接触到Objective-C中的重要协议和方法。一个值对象是一个包含一些值的对象,并且可以进行相等比较。通常值对象可以被用作模型对象。例如,考虑一个简单的Person对象:
1 2 3 4 5 6 7 |
@interface Person : NSObject @property (nonatomic,copy) NSString* name; @property (nonatomic,strong) NSDate* birthDate; @property (nonatomic) NSUInteger numberOfKids; @end |
创建这些类型的对象是我们工作的面包和黄油(译者注:基本元素),虽然这些对象看上去很简单,但是仍然包含许多微妙之处。
有一件事,我们很多人硬性的认为这些对象应该是一成不变的。一旦你创建了一个Person对象,它不可能被改变。我们将在稍后涉及到可变性这个问题。
属性
首先要注意的是我们使用属性来定义一个Person的特征。创建属性是想当机械的:对于普通对象的属性,你设置它们为nonatomic
和strong
,而对于标量属性你只需要设置nonatomic
。默认情况下,它们也是assign
。有一个例外,对于具有可变副本的属性,你想将他们定义为copy
。例如,name属性的类型是NSString
,有可能出现的情况是,有人创建了一个Person对象,并指定类型为NSMutableString
的值。然后一段时间后,他或她可能会改变这个可变的字符串。如果我们的属性是strong
而不是copy
,我们的Person对象会随之改变,这不是我们想要的。对于容器类型也是一样的,例如数组或者字典。
请注意,这个拷贝是浅拷贝,容器可能还包含可变对象。例如,如果你有一个NSMutableArray *a
包含有NSMutableDictionary
元素,则[a copy]
将会给你一个不可变数组,但是元素是相同的NSMutableDictionary
对象。正如我们稍后将看到的,不可变对象的拷贝是无成本的,但是它增加了引用计数。
在旧的代码中,你可能看不到属性,因为他们是相对近期才加入到Objective-C语言的。代替现有属性,有可能会看到自定义的getter和setter方法,或纯实例变量。对于现在的代码,似乎似乎大多数人都同意使用属性,这也是我们所推荐的。
更多阅读:NSString:copy or retian
初始化方法
如果我们想要不可变对象,我们应该确保他们被创建后不能进行修改。我们可以通过添加一个初始化方法和在接口里使我们的属性只读来做到这一点。我们的接口将如下所示:
1 2 3 4 5 6 7 8 9 10 11 |
@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 |
然后,在我们的实现中,我们必须使我们的属性readwrite
,从而生成实例变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@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 |
现在我们可以构造新的Person对象,但不能修改它们了。这是非常有帮助的,当编写与Person对象工作的其他类时,我们知道我们正在工作的值不能改变。
相等比较
要比较是否相等,我们必须实现isEqual:
方法。我们希望isEqual:
返回true当且仅当所有的属性都相等。由Mike Ash(实现相等和散列)和NSHipster(相等)写的两篇很好的文章解释了如何做到这点。首先,让我们写isEqual:
:
1 2 3 4 5 6 7 8 9 10 11 |
- (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; } |
现在,我们检查是否我们是相同类型的类。如果不是,我们肯定不相等。然后对每个对象的属性,我们检查是否指针是相等的。||左侧的运算数似乎是多余的,但如果两个属性都为nil
则返回YES
。为了比较标量值相等像NSUInteger
,我们可以只使用==
。
有一件事值得注意:这里我们分成不同的属性到他们自己的布尔值里。在实践中,可能将它们合成一个大的条件更有意义,因为这样你直接得到惰性求值。在上面的例子中,如果名字不相等,我们就不需要检查任何其他的属性。通过把所有组合成一个if语句,我们直接得到优化。
下一步,按照这个文档,我们需要实现一个哈希函数也是如此。Apple说:
如果两个对象相等,他们必须有相同的哈希值。如果你在子类中定义了isEqual:
,并且打算把该子类的实例放入集合中,这最后一点就特别重要了。请确保你在你的子类中也定义了哈希。
首先,我们可以尝试运行下面没有实现哈希函数的代码:
1 2 3 4 |
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); |
我第一次跑了上面的代码,一切都很好,在字典中有两个项目。第二次,只有一个了。事情变得非常不可预测了,所以我们照着文档说的来做了。
正如你可能还记得您的计算机科学课程中,写一个好的哈希函数不是很容易的。一个好的哈希函数必须是确定性的和均匀的。确定性意味着,在相同的输入下需要生成相同的哈希值。均匀表示哈希函数的结果应该均匀地将输入映射在输出范围内。你的输出越均匀,你在集合中使用这些对象的性能越好。
首先,为了弄清楚,让我们来看看当我们没有一个哈希函数发生了什么,我们尝试使用Person对象作为字典的键:
1 2 3 4 5 6 7 8 9 |
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]); |
这在我的机器上运行需要29秒。相比之下,当我们实现一个基本的哈希函数,相同的代码运行只需要0.4秒。这不是合适的基准,但也给出了一个好的迹象,为什么要实现一个适当的哈希函数是很重要的。 对于Person类,我们可以用这样的哈希函数开始:
1 2 3 4 |
- (NSUInteger)hash { return self.name.hash ^ self.birthDate.hash ^ self.numberOfKids; } |
这将从我们的属性中产生三个哈希值并且XOR他们。在这种情况下,对我们来说已经足够了,因为NSString的哈希函数对于短字符串来说很好(过去表现良好的字符串最多96个字符,但是现在已经改变了。见CFString.c,寻找哈希)。对于严重的散列,你的哈希函数取决于你拥有的数据。这被Mike Ash的文章和其他地方所提及。
在哈希的文档里,有如下的段落:
如果一个可变对象被添加到使用哈希值来确定集合中对象位置的集合中,当对象在集合中,对象的哈希方法返回的值必须不能改变。因此,无论是哈希方法必须不依赖于任何对象的内部状态信息,还是当对象在集合中你必须确保该对象的内部状态信息不会改变。因此,例如,一个可变字典可以放入一个哈希表中,但是当它在那里你不能改变它。(请注意,可能很难知道给定的对象是否在一个集合中。)
这是为了确保你的对象是不可变的另一个非常重要的原因。然后,你甚至不必担心这个问题了。
更多阅读
- A hash function for CGRect
- A Hash Function for Hash Table Lookup
- SpookyHash: a 128-bit noncryptographic hash
- Why do hash functions use prime numbers?
NSCopying
为了确保我们的对象是有用的,可以方便的实现NSCopying
协议。让我们举例来说,在容器类中使用它们。对于我们类中的一个可变的变量,NSCopying
可以被这样实现:
1 2 3 4 5 6 7 |
- (id)copyWithZone:(NSZone *)zone { Person* p = [[Person allocWithZone:zone] initWithName:self.name birthDate:self.birthDate numberOfKids:self.numberOfKids]; return p; } |
然而,在协议文档中,他们提到另一种方式来实现NSCopying
:
当类和它的内容是不可变的,通过保留原有的实现NSCopying,而不是创建一个新的副本。
因此,对于我们不可变的版本,我们只要这样做:
1 2 3 4 |
- (id)copyWithZone:(NSZone *)zone { return self; } |
NSCoding
如果我们要序列化我们的对象,我们可以通过实现NSCoding
来做到这一点。该协议存在两个必需的方法: