在這篇文章中,我們將看看如何用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
來做到這一點。該協議存在兩個必需的方法:
1 2 |
- (id)initWithCoder:(NSCoder *)decoder - (void)encodeWithCoder:(NSCoder *)encoder |
實現這個和實現相等方法同樣簡單,也比較機械:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
- (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"]; } |
關於它可以從NSHipster和Mike Ash的部落格中閱讀更多。順便說一句,當處理不受信任的來源,如資料來自網路,不要使用NSCoding
。因為資料可能被篡改。通過修改存檔的資料,它很可能要執行遠端程式碼進行攻擊。取而代之,使用NSSecureCoding或像JSON的自定義格式。
Mantle
現在我們留下了一個問題:我們可以自動化它嗎?事實證明,我們可以做到。一種方法是程式碼生成,但幸運的是有一個更好的選擇:Mantle。Mantle使用內省(introspection)來產生isEqual:
和雜湊。此外,它提供了一些方法來幫助你建立字典,然後可以用於寫入和讀取JSON。當然,一般執行時這樣做將不會像自己寫的程式碼一樣有效率,但在另一方面,自動執行是一個更不容易出錯的過程。
可變性
在C語言和Objective-C語言中,可變的值是預設值。在某種程度上,它們是非常方便的,因為你可以在任何時候改變任何東西。當建立較小的系統,這應該是沒有問題的。然而,正如我們許多人瞭解的方法,建立規模更大的系統時,事情是不可變時會相當容易。在Objective-C中,我們已經使用不可變物件很長時間了,並且現在其他語言也開始新增。
我們來看看可變物件的兩個問題。一個是當你不希望它改變時它們可能會改變,另一個是在多執行緒環境中使用可變物件。
意想不到的變化
假設我們有一個表檢視控制器,其中有一個People屬性:
1 2 3 4 5 |
@interface ViewController : UITableViewController @property (nonatomic, strong) NSArray* people; @end |
而在我們的實現裡,我們只是對映每個陣列元素到一個單元格:
1 2 3 4 5 6 7 8 9 |
- (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView { return 1; } - (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section { return self.people.count; } |
現在,在設定了以上檢視控制器的程式碼中,我們可能有這樣的程式碼:
1 2 3 4 |
self.items = [NSMutableArray array]; [self loadItems]; // Add 100 items to the array tableVC.people = self.items; [self.navigationController pushViewController:tableVC animated:YES]; |
表檢視將開始呼叫方法,如tableView:numberOfRowsInSection:
,開始一切都很好,但是假設在某些時候,我們執行以下操作:
1 |
[self.items removeObjectAtIndex:1]; |
這改變了我們的items陣列,但是它也改變了我們表檢視控制器裡的People陣列。如果我們這樣做而沒有和表檢視控制器有任何進一步的溝通,表檢視將仍然認為有100個專案,而我們的陣列只包含99個。不好的事情將會發生。取而代之,我們應該做的是以copy
宣告我們的屬性:
1 2 3 4 5 |
@interface ViewController : UITableViewController @property (nonatomic, copy) NSArray* items; @end |
現在,無論什麼時候我們分配一個可變的陣列給items,一個不可變的副本將會建立。如果我們分配一個常規(不可變)的陣列的值,拷貝操作是無害的,它僅僅增加了引用計數。
多執行緒
假設我們有一個可變物件,Account,代表一個銀行賬戶,它有一個方法transfer:to:
:
1 2 3 4 5 |
- (void)transfer:(double)amount to:(Account*)otherAccount { self.balance = self.balance - amount; otherAccount.balance = otherAccount.balance + amount; } |
多執行緒的程式碼可以在許多方面產生錯誤。例如,如果執行緒A讀取self.balance
,執行緒B可能會線上程A繼續之前修改它。對於所有涉及到的危險的一個很好的解釋,請參閱我們的第二個問題。
如果我們將它替換為不可變物件,事情就容易多了。我們不能對其進行修改,這迫使我們在一個完全不同的層次上提供可變性,產生更簡單的程式碼。
快取
另一件事,不可變性可以幫助的是在快取值的時候。例如,假設你已經解析了一個markdown文件為一個包含所有不同元素節點的樹形結構。如果你想生成的另外的HTML,你可以快取這個值,因為你知道沒有任何子節點會改變。如果你有可變物件,你則需要每次從零開始生成HTML,或構建優化並觀察每一個單獨的物件。和不變性相比,你不必擔心無效的快取。當然,這可能會帶來效能損失。在幾乎所有情況下,然而,簡單將超過在效能上的略有下降。
在其他語言裡的不可變性
不可變物件是靈感來自於像Haskell的函數語言程式設計語言的概念之一。在Haskell中,值預設是不可變的。Haskell程式通常有一個單純功能的核心,裡面沒有可變物件,沒有狀態,而且沒有副作用,像I/O。
我們可以在Objective-C程式設計中借鑑這個。在可能的情況下使用不可變物件,我們的專案將變得更容易測試。Gary Bernhardt有一個很棒的討論,顯示瞭如何使用不可變物件來幫助我們寫出更好的軟體。在這個討論中,他使用的是Ruby,但是其概念也同樣適用於Objective-C語言。