Swift:什麼時候使用結構體和類

來源:互聯網
上載者:User

標籤:實值型別   參考型別   結構體   類   swift   

發佈於 2015 年 8 月 14 日

世界上對swift持續不斷的討論話題中有一個就是什麼時候使用結構體什麼時候使用類。我想我今天要貢獻一些自己的想法。

值 VS 引用

答案其實很簡單:當你需要值語義(所謂值語義是一個對象被系統標準的複製方式複製後,與被複製的對象之間毫無關係,可以彼此獨立改變互不影響)的時候使用結構體,當你需要引用語義(所謂值語義是一個對象被系統標準的複製方式複製後,與被複製的對象之間毫無關係,可以彼此獨立改變互不影響)的時候使用類。就是那樣!

歡迎下周再來。。。

等等!
怎麼了?

這沒有回答這個問題
什麼意思?就是這樣的啊!

是的,但是。。。
但是什嗎?

什麼是值語義和引用語義呢?
哦,這個啊。也許我接下來應該講講這個。

並且他們怎麼和結構體和類相關聯的呢?
好的。

所有的問題都歸結於資料和資料被儲存在什麼地方。我們通常將資料存在在局部變數、參數、屬性以及全域變數中。從根本上說有兩種不同的方法將資料存放區在所有這些地方。

值語義中,資料直接存在於被儲存的位置。引用語義中,資料存在於別的地方,而儲存的位置中儲存著一個對資料的引用。當你擷取資料的時候這種差別可能不那麼明顯。而當你拷貝那Block Storage地區時這個不同就會顯現出來。值語義中,你會擷取一份新的資料的拷貝,而引用語義下,你會擷取一份新的對同樣的資料的引用的拷貝。

這真的很抽象。讓我們來看一個例子,暫時把swift的這個問題從你腦海中移除,讓我們來看一個OC的例子:

@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 43The number in value2 is 42

為什麼會有這樣的差異呢?

代碼 SomeClass *reference = [[SomeClass alloc] init]在記憶體中建立了一個新的SomeClass類型的執行個體,然後在變數中賦值對那個執行個體的引用。代碼reference2 = reference在一個新的變數中賦值了對同一個對象的引用。現在兩個變數都指向了同一個對象,而reference.number = 43修改了儲存在那個對象中的number屬性的值。所以當列印對象的number屬性值時,結果是43。

代碼 struct SomeStruct value = {}建立了SomeStruct的一個執行個體並賦值給變數。代碼value2 = value在第二個變數中拷貝了這個執行個體的副本。每個變數包含了一塊獨立的資料。代碼value.number = 43 只修改了value變數中的資料,然後當列印value2的number時結果仍然是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 43The number in value2 is 42
實值型別的體驗

實值型別不是一個新的概念,但是對於很多人來說他們覺得這是新的。為什麼呢?

結構體在大多數OC代碼中不是很常用。我們通常以CGRect或者CGPoint以及其他類似結構的形式接觸他們,但是一般不會建立我們自己的結構體。原因之一是,他們不是那麼的實用.想要用OC語言將一個對象的引用正確地儲存在一個結構體中真的是一件很困難的事情,尤其是在使用APC的情況下。

很多其他的語言根本沒有類似struct這樣的類型。很多認為“一切皆對象”的語言如Python、JavaScript等也都只有參考型別。如果你是從那樣的語言轉而學習Swift的,這個概念對你來說可能會更陌生。

但是別急!有這麼一個地區幾乎所有的語言都使用實值型別:數字!下面的例子連剛開始編程幾周的程式員都不會覺得陌生,忽略掉語言:

    var x = 42    var x2 = x    x++    print("x=\(x) x2=\(x2)")    // prints: x=43 x2=42

這對我們來說是那麼的明顯和自然以至於我們根本沒有覺察到他表現得有些不同,但是它就那樣展現在我們面前。只要你在編程你就在跟實值型別打交道,即使你沒有意識到!

很多語言實際上把數字實現為參考型別,因為他們堅持“一切皆對象”的哲學。不管怎樣,他們是不可變類型,而實值型別和不可變參考型別之間的區別很難察覺。他們表現得跟實值型別很像,即使他們不是像實值型別那樣實現的。

這是關於理解實值型別和參考型別的相當大的一部分內容。就語言的語義來說,只有在資料被改變的時候他們的差異會有影響。但是如果你的資料是不可變的,那麼實值型別和參考型別的差別就不存在了,至少問題就轉向效能而不是文法了。

這甚至出現在了OC中的標記指標(tagged pointers)中。就像標記指標中那樣,一個Object Storage Service在一個指標的值中,這是個實值型別。拷貝儲存地區就拷貝了對象。這個差別不明顯,因為OC庫很小心地只在不可變類型中加入了標記指標。一些NSNumbers對象是參考型別,另外一些則是實值型別,但是這並沒有什麼差別。

做出選擇

現在我們知道實值型別是怎麼工作的了,你怎麼選擇你自己的資料類型呢?

從根本上講這兩者的區別就是當你在他們身上使用等號的時候發生了什麼。實值型別被拷貝,而參考型別有了另外一個引用。

因此當決定使用哪種資料類型時根本上要問的問題就是:拷貝這個類型有意義嗎?你想方便地使用拷貝操作並且會頻繁使用嗎?

先讓我們來看一些比較極端的,明顯的例子。Integer明顯是可以被拷貝的,他們應該是實值型別的。網路通訊端明顯不能被拷貝,他們應該是參考型別。point中的x,y是可以拷貝的,他們應該是實值型別。一個代表著磁碟的控制器明顯不能被拷貝,他們應該是參考型別。

有些類型可以被拷貝但是你不想拷貝一直發生。這就表明他們應該是參考型別的。例如,螢幕中的一個按鈕在概念上是應該能被拷貝的。但是拷貝的按鈕跟原始的那個並不完全一樣。你點擊拷貝的按鈕並不會觸發原始的那個。拷貝的按鈕也不會佔據原始按鈕在螢幕中的位置。那就意味著你的button應該是參考型別的。

View 和window controllers是一個類似的例子。他們可能是可拷貝的,這是極為有可能的,但是你幾乎永遠都不想那麼做,所以他們應該是參考型別的。

模型類會怎麼樣呢?你可能有一個User類型來代表你系統的使用者,或者一個Crime類型代表一個User的動作。這肯定是可拷貝的,所以他們應該是實值型別的。然而,你可能想將程式中某個地方使用者的操作更新到到程式的另外一個地方使其可見。這意味著你的使用者應該被某個參考型別的user controller進行管理。

集合是個有趣的例子。他們包含了數組,字典還有字串。他們是可拷貝的嗎?很明顯是。你想讓拷貝成為一項便捷又頻繁的操作嗎?那就不是很明確了。

大多數語言對這個問題說“不”並把他們的集合設定為參考型別。這在OC、Java、Python、JavaScript以及任何我能想到的語言中都是這樣的(一個主要的例外就是C++中的STL集合類型,但是C++是語言界的奇葩,它總是表現得跟大家不一樣)。

Swift對此說“yes”,那也就意味著Array,Dictionary和String都是結構體而不是類。當他們被賦值以及作為參數被傳遞的時候會被拷貝。如果拷貝的代價很小的話這絕對是明智的決定,而這也正是Swift很努力要做到的。

巢狀型別

當嵌套值和參考型別的時候有四種不同的組合。只這其中的一種就會讓你的生活很有趣。

如果你有一個參考型別嵌套了另外一個參考型別,沒有什麼特別的事會發生。像通常那樣,任何一個指向內部或者外部值的指標都能操縱他指向的對象。只要其中一個引用操縱值使其改變,其他引用指向的值也就跟著變了。

如果你有一個實值型別嵌套了另外一個實值型別,這就會有效地使值所佔的記憶體地區變大。內部值是外部值的一部分。如果你把外部值放到一塊新的儲存空間裡,所有的值包括內部值都會被拷貝。如果你把內部值放進一塊新的儲存空間中,只有內部值會被拷貝。

一個參考型別嵌套了一個實值型別會有效擴大這個參考型別所佔記憶體地區。任何指向外部值的指標都可以操縱一切,包括嵌套的內部值。內部值的任何改變對於引用外部值的指標來說都是可見的。如果你把內部值放進一塊新的儲存區,就會在那Block Storage區拷貝一份新的值。

一個實值型別嵌套一個參考型別就沒有那麼簡單了。你可以有效地打破值語義而不被察覺。這可能是好的也可能是壞的,取決於你怎麼做。當你把一個參考型別嵌套進一個實值型別中,外部值被放進一塊新的記憶體地區時就會被拷貝,但是拷貝的對象仍然指向原始的那個嵌套對象。下面是一個舉例:

    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的引用,因此兩個結構體就共用了同一個inner對象。這樣一來當改變outer.inner.value的值也會影響outer2.inner.value的值。哎呀!

這個行為會很有用。當你小心使用,你建立的結構體就具有寫時拷貝功能(只有當你執行outer2.value = 43時才會真正的產生一個副本,否則outer2與outer仍指向共同的資源),這種高效的值語義的實現不會使資料拷貝得到處都是。Swift中的集合就是這麼做的,你也可以自己建立一個這樣的類型。想要瞭解更多請看Let’s Build Swift.Array.

這也可能會很危險。例如,我們正在建立一個Person對象。這是一個模型類所以明顯是可拷貝的,所以它可以是結構體。按照通常的做法,你把Person類的name設定為NSString類型

    struct Person {        var name: NSString    }

然後你建立兩個Person對象,並且分不同的部分建立名字:

    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的資料中的一部分,從語感上講,這看起來像是改變了bob。

這種事在OC中一直都在發生。每一位有些經驗的OC程式員都會有這樣的習慣,在所有地方都採用保護性的copy來修飾屬性。由於NSString實際上可能是NSmutableString類型,你將屬性設定為copy,或者在你自己的初始化方法中寫具體的copy實現,來避免產生一些問題的產生。這同樣適用於可變的集合類型。

在Swift中,在這兒的結論就更簡單了:使用實值型別而不是參考型別。在這個例子中,將name作為String類型。那麼你就不用擔心不經意間共用引用了。

結論

無論在什麼時候你移動一個實值型別他都會被拷貝,而參考型別則是產生了對同樣的底層對象的一個新的引用。那也就意味著參考型別的改變對所有其他的引用都是可見的,而改變實值型別隻影響你改變的那塊記憶體地區。當選擇使用哪種類型時,考慮你的類型是否適合被拷貝,當類型從本質上來說是可拷貝時傾向使用實值型別。最後,記住如果你在實值型別中嵌入參考型別,不小心的話就會出錯!

原文地址

Swift:什麼時候使用結構體和類

相關文章

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.