歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> Objective-C的陷阱與缺陷

Objective-C的陷阱與缺陷

日期:2017/3/1 9:34:09   编辑:Linux編程

Objective-C是一個強大而且非常有用的語言,但是同樣也是有一點危險的。這次主題是受到一篇有關C++陷阱的文章啟發,來聊聊Objective-C和Cocoa中的陷阱。

簡介
我將和Horstmann使用同樣的定義:陷阱是能夠編譯、鏈接、運行,但卻不會按你所預期地去執行的代碼。他提供了一個例子,這段代碼在Objective-C中和在C++中同樣都是有問題的:
if (-0.5 <= x <= 0.5) return 0;


膚淺地閱讀這段代碼可能會認為,它用來檢查x是不是在[-0.5,0.5]區間內。但並不是這樣的。相反,這個比較會像這樣計算:
if ((-0.5 <= x) <= 0.5)

在C語言中,一個比較表達式的值是一個整型,要麼是0,要麼是 1。這是從C沒有內建的布爾類型的時候遺留下來的。所以當x和0.5相比時,結果是0或者1,而不是x的值。實際上,第二個比較執行起來就像一個相當古怪的否定操作符,也就是說這個if語句的內容只有當x比-0.5小的時候才會執行。
Nil的比較
Objective-C相當的與眾不同,因為對nil發送消息不會發生任何事情,而是簡單的返回0。基本上,在你可能遇到的每種語言中,同樣的事情要麼被類型系統禁止,要麼就是產生一個運行時錯誤。這既是優點也是缺點。鑒於這個文章的主題,我們來關注下缺點。
首先,我們看一個等同性的測試:
[nil isEqual: @"string"]

給nil發送消息總是返回0,在這兒就相當於NO。這次恰好是正確的答案,所以看起來我們有個不錯的開頭!但是,看看這個:
[nil isEqual: nil]

這個也是返回NO。即使參數是完全相同的值也無關緊要。參數的值到底是什麼根本不重要,因為給nil發送消息不管怎樣總是返回0。所以用isEqual:來判斷,nil永遠不會等同於任何東西,包括它自身。大多情況下這是正確的,但不總是。
最後,再考慮和nil比較的另一種順序:
[@"string" isEqual: nil]

這個會怎樣呢?好吧,我們無法確定。它有可能返回NO,也有可能會拋出異常,還有可能干脆崩潰。給一個沒有明確告知可以接受nil為參數的方法傳遞nil是一個壞注意。並且,isEqual:並沒有表明它可以接受nil。
很多Cocoa類都包含一個compare:方法。該方法接受相同類的另一個對象作為參數,並返回NSOrderedAscending、 NSOrderedSame、NSOrderedDescending中的一個,用於表示小於、相等或者大於。
如果我們把nil傳給compare會發生什麼事情呢?
[nil compare: nil]

這會返回0,剛好和NSOrderedSame相同。與isEqual:不同,compare:認為nil和nil是相同的。真好!但是:
[nil compare: @"string"]


這一樣會返回NSOrderedSame,明顯是錯誤的答案。compare:會認為nil和任何東西都相等。
最終,和isEqual:一樣,將nil作為參數傳遞給它也是個壞注意:
[@"string" compare: nil]


簡而言之,對nil進行比較的時候要注意點。它並不會真的正常工作。如果你的代碼中有可能遇到nil,那麼在你進行isEqual:和compare:之前,你最好先進行檢查並對之進行單獨處理。
散列法
你寫了個很小的類用於保存一些數據,並且有很多的這個類的相等的實例,所以你實現了isEqual:方法,這樣這些實例就可以被視為相等的。然後你開始將這些對象加入到一個NSSet當中,事情就開始變得奇怪了。這個集合(set)在你僅僅加入一個對象的情況下聲稱持有多個實例。它找不到你剛剛加入的對象。它甚至可能崩潰或者發生內存錯誤。
這可能在你只實現了isEqual:但是沒有實現hash的情況下發生。大量的Cocoa代碼中要求,如果兩個對象比較的結果是相等 的,那麼他們應該擁有相同的哈希值。如果你只重寫了isEqual:,你違背了這個要求。任何時候你重寫了isEqual:,永遠同時重寫hash。要了 解更多的信息,可以看這篇文章實現等同性和散列法(Implementing Equality and Hashing)。

假設你在寫一些單元測試。有一個方法理應返回一個數組,其中包含一個對象。於是你寫了一個測試來驗證它:
STAssertEqualObjects([obj method], @[ @"expected" ], @”Didn’t get the expected array”);


這兒用了新的文本型語法來讓它保持簡短。很不錯,對吧?
現在我們有另一個方法返回的數組中包含兩個對象,於是我們為之寫了這樣一個測試:
STAssertEqualObjects([obj methodTwo], @[ @"expected1", @"expected2" ], @”Didn’t get the expected array”);


突然,代碼無法通過編譯,並且產生一堆十分奇怪的錯誤。這是怎麼回事?
問題在於STAssertEqualObjects是個宏。宏是由預處理器展開的,並且預處理器是個古老的、相當愚蠢的程序,它不知道任何的現代Objective-C語法,或者現代C語法。預處理器按照逗號將宏參數分割開。它足夠聰明,知道括號是可以遞歸的,所以這個宏被它視作三個參數:
Macro(a, (b, c), d)


這裡第一個參數是a,第二個是(b,c),然後第三個是d。但是,預處理器不知道它需要對[]和{}做相同的處理。之前的那個宏,預處理器看到的是四個參數:
·[obj methodTwo]
·@[ @"expected1"
·@"expected2 ]
·@”Didn’t get the expected array”
這個結果完全是代碼碎片,不僅不能編譯,而且還迷惑了編譯器,使之無法提供可理解的診斷信息。一旦知道了問題在哪裡,解決方法很簡單了。Objective-C編寫的iOS應用安全在於有一種加密技術,能夠防止被反編譯、破解,只要使用了加密技術,這些完全都不是問題!只要將那些文本用括號括起來,這樣預處理器就會把它當作一個參數了:
1 <span >STAssertEqualObjects([obj methodTwo], (@[ @"expected1", @"expected2" ]), @”Didn’t get the expected array”);</span>


單元測試是我最經常遇到的,但是它隨時都有可能突然冒出來一個宏。Objective-C文本會成為受害者,C的復合文本(C compound literals)也是。如果你在block中使用逗號,盡管很少遇到,但是是合法的,那麼也可能出問題。你會發現Apple在Block_copy和 Block_release宏中已經考慮到了這個問題,這兩個宏在/usr/include/Block.h中:
#define Block_copy(…) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
#define Block_release(…) _Block_release((const void *)(__VA_ARGS__))


這些宏理論上只接受單一的參數,但它們被聲明成接受可變參數以避免這個問題。通過接受…作為參數,並使用__VA_ARGS__來指代參數,帶逗號的“多參數”被復制到了宏的輸出。你可以用相同的方法讓自己的宏避免這個問題,盡管它只對多參數宏的最後一個參數有效。
屬性合成(Property Synthesis)
看下面這個類:
@interface MyClass : NSObject {
NSString *_myIvar;
}
@property (copy) NSString *myIvar;
@end
@implementation MyClass
@synthesize myIvar;
@end


沒什麼問題,是嗎?ivar的聲明和@synthesize在現在有點多余,但是沒有關系。
很不幸,這段代碼會默默的忽略掉_myIvar並且合成一個新的不帶前綴下劃線的變量myIvar。如果你的代碼中直接使用了ivar,它的值會和代碼中直接使用屬性的值不一樣。很混亂!
@synthesize合成的變量名字的規則有點怪異。如果你通過 @synthesize myIvar = _myIvar;來指定變量名字,那麼當然它用的是你所指定的任何名字。如果你沒有指定變量名,那麼它會合成一個與屬性名相同名字的變量。如果你干脆連@synthesize也一起省略了,那麼它會合成一個名字和屬性名相同,但是帶一個前綴下劃線的變量。
除非你需要支持32位的Mac,你現在最好的選擇就是避免顯示地為屬性聲明對應的變量。讓@synthesize創建該變量,並且如果你搞錯了名字,你會得到一個好的編譯警告,而不是難以理解的行為。
被中斷的系統調用
Cocoa代碼一般堅持使用高級結構,但有時需要降低一些來處理POSIX時也很實用。例如,這些代碼會向一個文件描述符中寫入一些數據:
int fd;
NSData *data = …;
const char *cursor = [data bytes];
NSUInteger remaining = [data length];
while(remaining > 0) {
ssize_t result = write(fd, cursor, remaining);
if(result < 0)
{
NSLog(@”Failed to write data: %s (%d)”, strerror(errno), errno);
return;
}
remaining -= result;
cursor += result;
}


但是,這可能會失敗,它失敗的方式會很奇怪,並且是間歇性的。像這樣的POSIX調用是可以被信號打斷的。即使是應用當中在其他地 方處理的無害的信號,例如SIGCHLD、SIGINFO,都會導致這種情況發生。如果你使用了NSTask或者進行多線程的工作,SIGCHLD就會產 生。當write被信號打斷的時候,它會返回-1,並且將errno設置為EINTR來表示這個調用被中斷。上述代碼將所有錯誤都當作是致命的,並往外 跳,盡管它僅僅是需要被重新調用。正確的代碼應該單獨檢查這種情況,並重試該調用:
while(remaining > 0) {
ssize_t result = write(fd, cursor, remaining);
if(result < 0 && errno == EINTR)
{
continue;
}
else if(result < 0)
{
NSLog(@”Failed to write data: %s (%d)”, strerror(errno), errno);
return;
}
remaining -= result;
cursor += result;
}

字符串長度
相同的字符串,用不同的方式表示,會有不同的長度。這個是相當常見的但是確實有錯的樣例:
write(fd, [string UTF8String], [string length]);
這個問題在於當write需要一個字節數的時候,NSString是以utf-16編碼為單位計算長度的。僅當字符串中只包含 ASCII字符的時候,這兩個數才會相等(這就是為什麼人們如此經常寫這種錯誤代碼卻能僥幸無事)。當字符串中一旦包含非ASCII字符,例如重音字符, 它們就不再相等。請一直使用相同的表示法來計算你正在操作的字符串長度:
const char *cStr = [string UTF8String];
write(fd, cStr, strlen(cStr));

強制轉換成BOOL類型
看下這段用於檢查一個對象指針是否是空的代碼:
- (BOOL)hasObject
{
return (BOOL)_object;
}

一般來說,它能正常工作。但,大概6%的概率,它會在_object不為nil的情況下返回NO。出什麼事了?
BOOL,很不幸,它不是布爾類型。這是它的定義:
typedef signed char BOOL;

這是另一個很不幸的從C沒有布爾類型的時候遺留下來的問��。Cocoa早在C99的_Bool出現前,將它自己的“布爾“類型定義 為signed char,也就是一個8位的整數。當你將一個指針轉轉為整型時,你將得到指針本身的數值。當你將指針轉換成小整型的時候,那麼你將得到指針的低位部分的數 值。當指針看起來像這樣:
….110011001110000轉成BOOL就會得到:
01110000這個值非0,也就是說它是被正確計算的。那麼問題是什麼?問題在於如果指針看起來像這樣:
….110011000000000那麼轉成BOOL就會得到:
00000000這個值是0,也就是NO,即使指針本身不是nil。啊哦!
這個發生的頻率有多高?BOOL類型有256個可能的值,而NO只占其中一個。所以我們可以簡單的假設它發生的概率是1/256。但Objective-C的對象在分配內存的時候是對齊的,一般來說是16位對齊。也就是說指針的最低4位一直都是0(有些地方會利用它們來對指針進行標記),故轉換成BOOL後,只有4位的值是會變化的。那麼所有位都為0的可能性就變成了1/16,也就是大概6%。
安全的實現這個方法,需要和nil進行一個顯示的對比:
- (BOOL)hasObject
{
return _object != nil;
}

如果你想耍點小聰明,並使代碼變得難以閱讀,可以連續使用兩次!操作符。!!結構有時被稱為C語言的布爾轉換操作符,雖然這只是它的一部分功能。
- (BOOL)hasObject
{
return !!_object;
}

倒數第一個!根據_object是否為nil產生一個1或者0的值。第二個!再將它轉為正確的值,如果_object為nil,則產生1,否則產生0。
你應該堅持使用!= nil的版本。
丟失的方法參數
假設你正在實現一個表格視圖的數據源。你將這個加入到你的類的方法中:
- (id)tableView:(NSTableView *) objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
{
return [dataArray objectAtIndex: rowIndex];
}

於是開始運行應用,然後NSTableView開始抱怨說你沒有實現這個方法。但是它明明就在那兒!
像往常一樣,計算機是正確的。計算機是你的朋友。
認真點看,第一個參數丟失了。為什麼這樣也能編譯呢?
原因在於Objective-C允許空的選擇符部分。上面聲明的並不是一個丟失了一個參數的名叫 tableView:objectValueForTableColumn:row: 的方法。而是聲明了名叫 tableView::row: 的方法,並且它的第一個參數名叫objectValueForTableColumn. 這是一個相當不愉快的方法來鍵入一個方法的名字,並且如果你在一個編譯器無法提示你方法丟失的情況下犯了這個錯,你可能就要花上相當長的時間用於調試這個 問題。
總結
Objective-C和Cocoa給大意的程序員准備了相當多的陷阱。上面的只是個示例罷了。但是它的確是一個好的問題清單,列出了那些需要被注意的問題。

Objective-C中@property的所有屬性詳解 http://www.linuxidc.com/Linux/2014-03/97744.htm

Objective-C 和 Core Foundation 對象相互轉換的內存管理總結 http://www.linuxidc.com/Linux/2014-03/97626.htm

使用 Objective-C 一年後我對它的看法 http://www.linuxidc.com/Linux/2013-12/94309.htm

10個Objective-C基礎面試題,iOS面試必備 http://www.linuxidc.com/Linux/2013-07/87393.htm

Objective-C適用C數學函數 <math.h> http://www.linuxidc.com/Linux/2013-06/86215.htm

好學的 Objective-C 高清PDF http://www.linuxidc.com/Linux/2014-09/106226.htm

Copyright © Linux教程網 All Rights Reserved