翻譯:muhlenXi
校對:Yousanflics,numbbbbb,Cee
定稿:CMBhtml
在 Swift 的世界中,有一個熱議好久的主題,什麼時候使用 class 和什麼時候使用 struct ,今天,我想發表一下我本身的觀點。編程
事實上,這個問題的答案很簡單:當你須要值語義的時候用 struct,當你須要引用語義的時候就用 class。swift
好了,下週同一時間請再次訪問個人博客……數組
等等網絡
怎麼了?app
這沒有回答上述中的問題socket
你什麼意思?答案就在那兒。函數
是的,可是……性能
可是什麼?ui
那什麼是值語義,什麼是引用語義呢?
昂,你提醒了我。我確實應該講解一下。
還有它們和 struct、class 的關係
好吧。
這些問題的核心就是數據和數據的存儲位置。咱們用局部變量、參數、屬性和全局變量來存儲數據。存儲數據有兩種最基本的方式。
對於值語義,數據是直接保存在變量中。對於引用語義,數據保存在其餘地方,變量存儲的是該數據的引用地址。當咱們訪問數據時,這種差別不必定很明顯。可是拷貝數據時就徹底不同了。對於值語義,你獲得的是該數據的拷貝。對於引用語義,你獲得的是該數據的引用地址拷貝。
這有些抽象,咱們經過一個示例來了解一下。先暫時跳過 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);
複製代碼
打印的結果以下:
The number in reference2 is 43
The number in value2 is 42
複製代碼
爲何打印結果會不同?
代碼 SomeClass *reference = [[SomeClass alloc] init]
在內存中建立了 SomeClass 類的一個新實例,而後將該實例的引用放到 reference 變量中。代碼 reference2 = reference
將 reference 變量的值(實例的引用)賦值給新的 reference2 變量。而後 reference.number = 43
將兩個變量指向的對象(同一個對象)的 number 屬性修改成 43。 這就致使打印的 reference2 的值也是 43。
代碼 struct SomeStruct value = {}
建立 SomeStruct 結構體的一個新實例並賦值給變量 value。代碼 value2 = value
拷貝 value 的值到 變量 value2 中。每一個變量包含各自的數據塊。而代碼 value.number = 43
僅僅修改 value 變量的值。因此,value2 變量的值仍然是 42。
用 Swift 實現這個例子:
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)")
複製代碼
和以前同樣,打印以下:
The number in reference2 is 43
The number in value2 is 42
複製代碼
值類型不是新出的類型。可是對於不少人來講,他們感受上很新。這是怎麼回事?
大部分 Objective-C 代碼不會用到 struct。咱們一般操做的是 CGRect 、 CGPoint ,不多本身定義結構體。一方面,結構體不實用,沒法作函數式的引用賦值。在 Objective-C 中,正確保存對象的引用到 struct 中是很困難的,尤爲是使用 ARC 的時候。
大部分語言沒有相似 struct 結構體的東西。像 Python 和 JavaScript 這樣「一切皆對象」的語言都只有引用類型。若是你是從這樣的語言轉到 Swift,值類型這個概念可能對你來講更加陌生。
不過等一下!有一個地方几乎全部的語言都會使用值類型:數值(number)!只要你寫過一段時間代碼,不管是什麼語言,確定能理解下面這段代碼的行爲:
var x = 42
var x2 = x
x++
print("x=\(x) x2=\(x2)")
// prints: x=43 x2=42
複製代碼
這對咱們來講是很是明顯和天然的,咱們甚至沒有意識到它的行爲不同凡響。可是它確確實實是值類型。從你編程的第一天開始就一直在使用值類型,即便你沒有意識到這一點。
因爲許多語言的核心是「一切皆對象」,number 實際上是用引用類型來實現的。然而,它們是不可變引用類型,不可變引用類型和值類型的差別是很難察覺的。它們的行爲和值類型同樣,即便它們不是以這種方式實現。
這是理解值類型和引用類型的重要部分。就語言語義方面,區別是很重要的。當修改數據時,若是你的數據是不可變的,那麼值類型/引用類型之間的區別就消失了,或者至少變成純粹的性能問題而不是語義問題。
Objective-C 中也有相似的東西,就是標記指針(tagged pointers)。標記指針把對象直接存儲在指針值中,所以它其實是值類型,拷貝指針至關於拷貝對象。Objective-C 的庫只會把不可變類型存儲到標記指針中,因此使用的時候感覺不到區別。有些 NSNumber 是引用類型,有些是值類型,可是使用上沒有區別。
既然咱們已經知道值類型是如何工做的,那麼你本身的數據類型該用什麼呢?
這二者之間的根本區別在於,當你使用 =
時會發生什麼。值類型會獲得該對象的副本,引用類型僅僅獲得該對象的引用。
所以,決定使用哪個的基本問題是:是否須要拷貝?是否須要常常拷貝?
首先來看一些毫無爭議的例子。Integer 顯然是可拷貝的,它應該是值類型。網絡套接字(Network sockets)明顯是不可拷貝的,它應該是引用類型。再好比使用 (x, y) 實數對錶示的座標(Points)是可拷貝的,它應該是值類型。表明磁盤的控制器是明顯不可拷貝的,它應該是引用類型。
有些類型理論上能夠拷貝,可是這種拷貝可能不是你想要的。這種狀況下,它們應該是引用類型。舉個例子,屏幕上的按鈕在代碼層面能夠拷貝,可是拷貝的按鈕和原始按鈕並不同。點擊拷貝的按鈕並不會觸發原始按鈕,拷貝的按鈕在屏幕上的位置也和原始按鈕不同。若是你須要把按鈕當成參數傳遞,或者將它賦值給一個新變量,那你須要的是原始按鈕的引用,只有明確聲明的時候才進行拷貝。所以,按鈕應該是引用類型。
視圖和窗口控制器也相似。它們能夠支持拷貝,但通常來講這不是你指望的行爲,它們應該是引用類型。
接着談談模型(model)類型。假設你有一個 User 類型,用來表示系統中的用戶,而後用 Crime 類型來表示 User 的操做。這兩個類型看起來均可以拷貝,能夠設置成值類型。可是,若是你的程序須要更新 User 的 Crime 而且能把改動同步到其餘代碼,那最好用一個用戶控制器(User Controller)來管理 User,顯然這個用戶控制器應該是引用類型。
集合是個有趣的例子。集合包括數組、字典、字符串等類型。它們是可拷貝的嗎?顯然是。是否須要常常拷貝?這就很差說了
大部分語言的回答是「No」,它們的集合是引用類型。好比 Objective-C、Java、Python、JavaScript 以及一些我能想到的語言。(一個例外是 C++ 的 STL 集合,可是 C++ 是語言中的瘋子,它作的每件事都很奇怪。)
Swift 是可拷貝的。這意味着 Array、Dictionary 和 String 是結構體而不是類。能夠將他們的拷貝做爲參數來使用。若是拷貝付出的代價很小,這麼作就徹底合理。Swift 爲了實現這個功能花了很大功夫。。
嵌套值類型和引用類型有四種方式。哪怕只用到了其中一種,你的生活都會變得更加有趣。
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
是 outer
的拷貝,它僅僅拷貝了 inner
的引用,所以兩個結構體的 inner
共享一個存儲空間。所以更新 outer.inner.value
的值會影響 outer2.inner.value
的值。神奇!
若是使用得當,上面的這種行爲使編程變得很方便,它容許你建立一個支持寫時複製的 struct,容許你不須要拷貝大量的數據就能夠實現值語義。這就是 Swift 的集合工做機制,你也能夠建立本身的集合。若是想了解更多,能夠閱讀 一塊兒來構建 Swift Array。
這種行爲也至關危險。舉個例子,你有一個可拷貝的 Person 類,因此它能夠是 struct 類型,爲了懷舊,你決定用 NSString 類型來保存姓名:
struct Person {
var name: NSString
}
複製代碼
而後生成一對夫婦的實例,分別給每一個實例的姓名賦值:
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)
複製代碼
結果以下:
Bob Josephsonson, Jr.
Bob Josephsonson, Jr.
複製代碼
喔!
發生了什麼?與 Swift 中的 String 類型不一樣,NSString 是一個引用類型,是不可變的,可是它有一個可變的子類 NSMutableString。構建 bob 時,生成了一個被 name 中字符串所持有的引用。隨後改變 這個字符串時,改動被同步到了 bob 中。雖然 bob 是用 let 聲明值類型,可是此處的賦值操做顯然改變了 bob。事實上,這沒有覆寫 bob,只不過是改變了 bob 持有的引用的數據。由於 name 是 bob 的一部分數據,從語義上看,就好像覆寫了 bob。
這種行爲在 Objective-C 中一直存在。每一個有經驗的 Objective-C 開發者都能避免這種行爲。由於一個 NSString 實際上多是一個 NSMutableString。爲了防止這種行爲,能夠聲明一個 copy 的屬性或者在初始化的時候顯式的調用 copy 方法。在許多 Cocoa 的集合中能夠發現這種作法。
Swift 的解決方法很簡單:用值類型而不是引用類型。在這種狀況下,聲明 name 爲 String 類型便可。這樣就不用擔憂無心中出現存儲共享的問題。
有些狀況下,解決方法可能沒有這麼簡單。舉個例子,你可能會建立一個 包含引用類型變量 view 的 struct,而且它不能改變爲值類型。這也許表示你的類型不該該是 struct,由於你不管如何也不能實現值語義。
移動值語義類型的數據時,新數據是原數據的拷貝。然而,引用語義類型的數據獲得的是原數據的引用拷貝。這意味着你能夠在任何地方經過引用覆寫原數據。而值語義只能經過改變原數據來改變原數據的值。選擇類型時,要考慮該類型是否適合拷貝和傾向於拷貝的固有類型。最後,注意值類型中嵌套的引用類型,若是你不留心將會發生一些糟糕的事情。
今天的內容到此結束,此次是真的結束了,下次再見。大家的建議對 Friday Q&A 是最好的鼓勵,因此若是你關於這個主題有什麼好的想法,請發郵件到這裏。
你喜歡這篇文章麼?個人書裏還有更多有意思的內容!第二卷 和 第三卷正在出售中!包括 ePub,PDF,紙質版,iBooks 和 Kindle,點擊查看更多信息。