Swift 圈中有一個(gè)被反復(fù)討論的話題是:何時(shí)使用struct,何時(shí)使用class.我覺(jué)得今天我也要給出我的個(gè)人觀點(diǎn).
值 VS 引用
答案真的很簡(jiǎn)單了:當(dāng)你需要用值語(yǔ)義的時(shí)候使用class,需要用引用語(yǔ)義使用struct.就是這樣!
我們下周再見(jiàn)…
等下 干啥?
還沒(méi)回答我的問(wèn)題呢 你啥意思?答案不明擺著么?
哦,但是… 啥?
什么是值/引用語(yǔ)義? 哦我明白了,我可能接下來(lái)會(huì)探討下.
還有他們是如何關(guān)聯(lián)到class和struct上的 嗯
所有都?xì)w根結(jié)底到數(shù)據(jù)以及數(shù)據(jù)存儲(chǔ)的位置.我們把東西存在局部變量,參數(shù),屬性和全局變量中.從根本上又劃分為兩種不同的方式.
對(duì) 于值語(yǔ)義來(lái)說(shuō),數(shù)據(jù)直接存在于存儲(chǔ)單元中.對(duì)于引用語(yǔ)義,數(shù)據(jù)存在于其他地方,存儲(chǔ)單元存儲(chǔ)一個(gè)對(duì)數(shù)據(jù)的引用.當(dāng)你存儲(chǔ)數(shù)據(jù)的時(shí)候這種差異不一定明顯.要 注意的是拷貝存儲(chǔ)的時(shí)候.對(duì)于值語(yǔ)義,你得到的是數(shù)據(jù)一份新拷貝.對(duì)于引用語(yǔ)義,你得到的是一份指向相同數(shù)據(jù)引用的新拷貝.
真是抽象,我們來(lái)看個(gè)例子吧.我們暫時(shí)先把 Swift 這茬放下,我們先看個(gè) Objective-C 的例子:
@interface SomeClass : NSObject
@property int number;
@end
@implementation SomeClass
@end
struct SomeStruct {
int number;
};
SomeClass *reference = [[SomeClass alloc] init];
reference.number = 42;
SomeClass *reference2 = reference; reference.number = 43;
NSLog(@"The number in reference2 is %d", reference2.number);
struct SomeStruct value = {};
value.number = 42;
struct SomeStruct value2 = value;
value.number = 43;
NSLog(@"The number in value2 is %d", value2.number);
打印結(jié)果:
The number in reference2 is 43
The number in value2 is 42
為啥不一樣呢?
SomeClass *reference = [[SomeClass alloc] init] 這行代碼在內(nèi)存中創(chuàng)建了一個(gè) SomeClass 的新實(shí)例,然后在變量中放置了一個(gè)對(duì)那個(gè)實(shí)例的引用. reference2 = reference 這行代碼在新變量中放置了對(duì)相同對(duì)象的引用. reference.number = 43 這行代碼修改的是兩個(gè)變量當(dāng)前一起指向的對(duì)象中存儲(chǔ)的數(shù)字.結(jié)果就是日志打印的是對(duì)象中的值,即43.
struct SomeStruct value = {} 這行代碼在變量中創(chuàng)建了一個(gè) SomeStruct 的新實(shí)例. value2 = value 將實(shí)例拷貝到第二個(gè)變量.每個(gè)變量有各自的數(shù)據(jù)塊.
Swift 對(duì)應(yīng)的例子:
class SomeClass {
var number: Int = 0
}
struct SomeStruct {
var number: Int = 0
}
var reference = SomeClass()
reference.number = 42
var reference2 = reference reference.number = 43
print("The number in reference2 is \(reference2.number)")
var value = SomeStruct()
value.number = 42
var value2 = value
value.number = 43
print("The number in value2 is \(value2.number)")
輸出結(jié)果跟以前一樣: 1 2 The number in reference2 is 43 The number in value2 is 42
值類型的經(jīng)驗(yàn)
值類型并不是新鮮事物,但是對(duì)于很多人來(lái)說(shuō)覺(jué)得它是新的.這是怎么回事呢?
struct 在絕大部分 Objective-C 代碼中并不是很常用.我們偶爾以 CGRect 和 CGPoint 等方式接觸到它們,但很少會(huì)自己去寫.首先,它們不是很實(shí)用.用 Objective-C 在 struct 中正確地存儲(chǔ)對(duì)象的引用的確很難,尤其是使用 ARC 的時(shí)候.
很多其他語(yǔ)言干脆沒(méi)有類似 struct 的東東.許多語(yǔ)言如同 Python 和 JavaScript 一樣”萬(wàn)物皆對(duì)象”,只有引用類型.如果你是從這類語(yǔ)言轉(zhuǎn)型到 Swift 的, 你可能對(duì) struct 的概念就更陌生了.
等一下!有種情況下幾乎所有語(yǔ)言都使用的值類型:數(shù)字!稍微有點(diǎn)編程經(jīng)驗(yàn)的程序員都不會(huì)對(duì)下面的行為感到驚訝,這跟語(yǔ)言無(wú)關(guān):
var x = 42 var x2 = x x++ print("x=\(x) x2=\(x2)") // prints: x=43 x2=42
這對(duì)我們來(lái)說(shuō)如此顯而易見(jiàn)和自然以至于我們甚至沒(méi)意識(shí)到結(jié)果的差異,但它就在我們眼前.你從開(kāi)始編程之日起一直在使用值類型,即使你沒(méi)意識(shí)到.
很多語(yǔ)言實(shí)際上將數(shù)字實(shí)現(xiàn)為引用類型,因?yàn)樗鼈兪?rdquo;萬(wàn)物皆對(duì)象”哲學(xué)的死忠粉.然而,它們是不可變類型,值類型與不可變引用類型之間的差異很難察覺(jué).它們表現(xiàn)得與值類型相同,盡管實(shí)現(xiàn)方式可能不同.
這是理解值和引用類型重要的一環(huán).當(dāng)數(shù)據(jù)變化時(shí),差異主要關(guān)系到語(yǔ)法方面.假如數(shù)據(jù)是不可變的,那么值和引用的差別就消失了,或至少變成了僅是性能問(wèn)題而不是語(yǔ)法差異.
實(shí)際上 Objective-C 的
tagged pointers 對(duì)此提到過(guò).一個(gè)對(duì)象遇上了 tagged pointer 的處理,然后存儲(chǔ)在指針的值中,就成了值類型.拷貝操作這時(shí)拷貝的就是對(duì)象內(nèi)容了.表面上沒(méi)差異,因?yàn)?Objective-C 函數(shù)庫(kù)小心翼翼地僅將不可變類型放到 tagged pointer 中.有些 NSNumber 對(duì)象是引用類型而有些是值類型,但用起來(lái)沒(méi)什么差別.
做出抉擇
既然我們知道了值類型的工作原理,我們改為自己的數(shù)據(jù)類型選擇那種方式呢? 這兩種類型根本的區(qū)別就是當(dāng)對(duì)其使用=時(shí)會(huì)發(fā)生什么.值類型是被拷貝,而引用類型只是得到另一個(gè)新引用.
因此在選擇使用哪種類型時(shí)面對(duì)的根本問(wèn)題是:拷貝它有意義么?拷貝操作是你想要變得簡(jiǎn)單,并經(jīng)常使用的么?
我們先看些極端的,顯而易見(jiàn)的例子.整型數(shù)明顯是可以拷貝的,應(yīng)該是值類型.網(wǎng)絡(luò)套接字感覺(jué)是不能被拷貝,應(yīng)該是引用類型.像是用 x,y 對(duì)兒的點(diǎn)坐標(biāo)是可拷貝的,應(yīng)該是值類型.用來(lái)表示磁盤的控制器感覺(jué)上不太容易被拷貝,應(yīng)該是引用類型.
有些類型可以被拷貝,但它們不總是你希望的那樣.建議把它們?cè)O(shè)為引用類型.比如屏幕上的一個(gè)按鈕從概念上講是可以拷貝的.副本按鈕不會(huì)跟原來(lái)的按鈕完全一 樣.點(diǎn)擊副本按鈕將不會(huì)激活原來(lái)的按鈕.副本不會(huì)占用相同的屏幕位置.如果你將按鈕傳遞到周圍或放到一個(gè)新的變量里,你大概將想引用原本的按鈕,除非明確 被請(qǐng)求要做一份拷貝.這意味著你的按鈕類型應(yīng)該是一個(gè)引用類型.
視圖和窗口控制器是類似的例子.它們可能想象上是能拷貝的,但它幾乎從來(lái)都不是你想要的那樣.它們應(yīng)該是引用類型.
用于 Model 的類型該怎么搞?比方你有個(gè) User 類型來(lái)表示系統(tǒng)中的用戶,或者 Crime 類型來(lái)表示用戶的活動(dòng).這些都是可完美拷貝的,所以它們或許應(yīng)該是值類型.然而,你可能希望你程序中某處對(duì) User 的 Crime 上的更新在程序的其他地方也可見(jiàn).這就建議 User 應(yīng)該被某種用戶控制器來(lái)管理,而且它應(yīng)該是引用類型.
集合是個(gè)有趣的例子.這包括比如數(shù)組和字典之類的東西,以及字符串.它們是可拷貝的么?顯而易見(jiàn).你想要做的拷貝操作是否易發(fā)生且經(jīng)常發(fā)生呢?這不好說(shuō).
大多數(shù)語(yǔ)言對(duì)此說(shuō)”不”,而是實(shí)現(xiàn)為引用類型. Objective-C,Java,Python,JavaScript 和幾乎其他所有我能想到的語(yǔ)言都是這么干的.(一個(gè)主要的例外就是 C++ 的 STL 中的集合類型,但是 C++ 是語(yǔ)言世界中胡言亂語(yǔ)的瘋子,它不走尋常路.)
Swift 說(shuō)”不錯(cuò)”,這意味著如 Array,Dictionary 和 String 都是 struct 而不是 class. 它們?cè)谫x值和作為參數(shù)傳遞時(shí)被拷貝.只要拷貝的開(kāi)銷小,這就是個(gè)徹底明智的選擇,而Swift費(fèi)了很大力氣去實(shí)現(xiàn)這點(diǎn).
嵌套類型
嵌套使用值類型和引用類型會(huì)有四種組合方式.只是其中一個(gè)比較有趣.
如果一個(gè)引用類型包含了另一個(gè)引用類型,沒(méi)有什么有趣的發(fā)生.任何指向其內(nèi)部或外部值的引用通常都能修改它.每個(gè)人都會(huì)看到發(fā)生的變更.
如果一個(gè)值類型包含了另一個(gè)值類型,這實(shí)際上只是讓其占用空間更多.內(nèi)部值是外部值的一部分.如果你將外部值放進(jìn)某個(gè)新的存儲(chǔ)區(qū),所有的值都會(huì)被拷貝,包括內(nèi)部值.如果你將內(nèi)部值放入某個(gè)新的存儲(chǔ),它會(huì)被拷貝.
一個(gè)引用類型包含了一個(gè)值類型實(shí)際上讓被引用的值占用空間更大了.擁有對(duì)外部值的引用就可以操作全部值,包括被嵌入的值.被嵌入值的所有變更對(duì)指向外部值的引用是可見(jiàn)的.如果你將內(nèi)部值放入某個(gè)新的存儲(chǔ)區(qū),它會(huì)被拷貝至那里.
一個(gè)值類型包含著一個(gè)引用類型那就不這么簡(jiǎn)單了.你實(shí)際上暗地里破壞了你想要用的值類型語(yǔ)義.這樣做或好后壞,取決于你怎樣去做.當(dāng)你把一個(gè)引用類型放入到 一個(gè)值類型中,當(dāng)你把它放入新的存儲(chǔ)區(qū)時(shí)外部值會(huì)被拷貝,但是拷貝后的副本有一個(gè)指向相同內(nèi)嵌的原始對(duì)象的引用.這有個(gè)例子: class Inner { var value = 42 }
class Inner {
var value = 42
}
struct Outer {
var value = 42
var inner = Inner()
}
var outer = Outer()
var outer2 = outer
outer.value = 43
outer.inner.value = 43
print("outer2.value=\(outer2.value) outer2.inner.value=\(outer2.inner.value)")
輸出是:
outer2.value=42
outer2.inner.value=43
雖然 outer2 得到一份 value 的拷貝,但它只拷貝了 inner 的*引用,于是這兩個(gè) struct 最終共享同一個(gè) Inner 實(shí)例.因此對(duì) outer.inner.value 的更新會(huì)影響到 outer2.inner.value. 唉呀媽呀!
這種做法真的很方便.用這個(gè)方法可以創(chuàng)建個(gè)能夠執(zhí)行寫時(shí)拷貝的 struct,還能實(shí)現(xiàn)讓值語(yǔ)義實(shí)際上不到處拷貝一坨坨的數(shù)據(jù).這就是 Swift 中的集合的工作原理,你自己也可以實(shí)現(xiàn)自己的集合類型.
這么做也可能變得極其危險(xiǎn).比方說(shuō)你創(chuàng)建了個(gè) Person 類型.它被用作 Model 類當(dāng)然也是可拷貝的,所以可以用 struct 實(shí)現(xiàn)咯.突發(fā)一陣對(duì) OC 的懷舊,你決定用 NSString 作為 Person 的 name:
struct Person {
var name: NSString
}
然后你創(chuàng)建按了一對(duì)兒
Person 實(shí)例,拼接字符串構(gòu)建出
name:
let name = NSMutableString()
name.appendString("Bob")
name.appendString(" ")
name.appendString("Josephsonson")
let bob = Person(name: name)
name.appendString(", Jr.")
let bobjr = Person(name: name)
然后輸出它們:
print(bob.name)
print(bobjr.name)
結(jié)果產(chǎn)生了:
Bob Josephsonson, Jr.
Bob Josephsonson, Jr.
靠! 發(fā)生了什么?區(qū)別于 Swift 的 String 類型, NSString 是一個(gè)引用類型.它是不可變的,但它有個(gè)可變的子類, NSMutableString. 當(dāng) bob 創(chuàng)建時(shí),它創(chuàng)建了一個(gè)對(duì) name 字符串的引用.當(dāng)那個(gè)字符串隨后被修改時(shí),變更會(huì)通過(guò) bob 展現(xiàn)出來(lái).要注意到即使 bob 是被 let 約束的值類型,但實(shí)際上改變了 bob.這算不上真的修改了 bob,只是修改了 bob 中引用的一個(gè)值,但因?yàn)槟莻€(gè)值是 bob 數(shù)據(jù)的一部分,從語(yǔ)義上讓人感到像是對(duì) bob 作了修改.
這種事情在 Objective-C 中一直在發(fā)生.每個(gè)有經(jīng)驗(yàn)的 Objective-C 程序員都有到處寫防御拷貝的習(xí)慣.因?yàn)橐粋€(gè) NSString 實(shí)例可能實(shí)際上卻是 NSMutableString, 為了避免災(zāi)難,你要將屬性定義為 copy,或者在初始化時(shí)顯式調(diào)用 copy 方法.這同樣適用于 Cocoa 中各種各樣的集合類型.
在 Swift 中解決方案更簡(jiǎn)單些:使用值類型而不是引用類型.在這種情況下,讓 name 成為 String.再也不用擔(dān)心無(wú)意中把引用共享咯.
在其他情況下,解決方案可能更簡(jiǎn)單.比如,你創(chuàng)建了一個(gè)包含視圖的 struct,而視圖是引用類型且不能改成值類型.這或許是個(gè)好的跡象表明你不該用 struct, 因?yàn)槟悴还茉鯓佣疾荒芫S持值語(yǔ)義.
結(jié)論
當(dāng)移動(dòng)值類型時(shí)它們會(huì)被拷貝,然而引用類型只是得到了一個(gè)對(duì)相同底層對(duì)象新引用.這意味著對(duì)引用類型的修改在每個(gè)引用上都看的到,然而對(duì)值類型的修改只會(huì)影 響你修改的那塊存儲(chǔ)區(qū).當(dāng)選擇使用哪種類型時(shí),思考下如何拷貝你的類型比較恰當(dāng),如果需要深層拷貝就傾向于選擇值類型.最后,謹(jǐn)防值類型中嵌入的引用類 型,稍有不慎就會(huì)遭殃.
本文版權(quán)歸傳智播客ios培訓(xùn)學(xué)院所有,歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明作者出處。謝謝!
作者:傳智播客ios培訓(xùn)學(xué)院
首發(fā):http://xamj520.com/ios/