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