Swift中enum、struct、class三者異同

enum:
Swift中的枚舉是為一組有限種可能性的相關值提供的通用類型(在C/C++/C#中,枚舉是一個被命名的整型常數的集合);使用枚舉可以類型安全并且有提示性地操作這些值。與結構體、類相似,使用關鍵詞enum來定義枚舉,并在一對大括號內定義具體內容包括使用case關鍵字列舉成員。就像下面一樣:

//定義一個表示學生類型的全新枚舉類型 StudentType,他有三個成員分別是pupil(小學生,玩LOL最怕遇到這種隊友了)、middleSchoolStudent(中學生,現在的中學生都很拽)、collegeStudents(大學生,據說大學生活很不錯,注意斷句)
enum StudentType {
  case pupil
  case middleSchoolStudent
  case collegeStudent
}

上面的代碼可以讀作:如果存在一個StudentType的實例,他要么是pupil (小學生)、要么是middleSchoolStudent(中學生)、要么是collegeStudent(大學生)。注意,和C、objective-c中枚舉的不同,Swift 中的枚舉成員在被創建時不會分配一個默認的整數值。而且不需要給枚舉中的每一個成員都提供值(如果你需要也是可以的)。如果一個值(所謂“原始值”)要被提供給每一個枚舉成員,那么這個值可以是字符串、字符、任意的整數值,或者是浮點類型(引自文檔翻譯)。簡單說Swift中定義的枚舉只需要幫助我們表明不同的情況就夠了,他的成員可以沒有值,也可以有其他類型的值(不局限于整數類型)。
枚舉中有兩個很容易混淆的概念:原始值(raw value)、關聯值(associated value),兩個詞聽起來比較模糊,下面簡單介紹一下:
枚舉的原始值(raw value)
枚舉成員可以用相同類型的默認值預先填充,這樣的值我們稱為原始值(raw value),下面的StudentType中三個成員分別被Int類型的10 、15、 20填充表示不同階段學生的年齡。注意:Int修飾的是StudentType成員原始值的類型而不是StudentType的類型,StudentType類型從定義開始就是一個全新的枚舉類型

enum StudentType: Int{
    case pupil = 10
    case middleSchoolStudent = 15
    case collegeStudents = 20
}

定義好StudentType成員的原始值之后,我們可以使用枚舉成員的rawValue屬性來訪問成員的原始值,或者是使用原始值初始化器來嘗試創建一個枚舉的新實例

//  常量student1值是 10
let student1 = StudentType.pupil.rawValue
//  變量student2值是 15
var student2 = StudentType.middleSchoolStudent.rawValue
//  使用成員rawValue屬性創建一個`StudentType`枚舉的新實例
let student3 = StudentType.init(rawValue: 15)
//  student3的值是 Optional<senson>.Type
type(of: student3)
//  student4的值是nil,因為并不能通過整數30得到一個StudentType實例的值
let student4 = StudentType.init(rawValue: 30)</senson>

使用原始值初始化器這種方式初始化創建得到StudentType的實例student4是一個StudentType的可選類型,因為并不是給定一個年齡就能找到對應的學生類型,比如在StudentType中給定年齡為30就找不到對應的學生類型(很可能30歲的人已經是博士了)。所以原始值初始化器是一個可失敗初始化器。
總結一句:原始值是為枚舉的成員們綁定了一組類型必須相同值不同的固定的值(可能是整型,浮點型,字符類型等等)。這樣很好解釋為什么提供原始值的時候用的是等號。
枚舉的關聯值(associated value)
關聯值和原始值不同,關聯值更像是為枚舉的成員們綁定了一組類型,不同的成員可以是不同的類型(提供關聯值時用的是括號)。例如下面的代碼:

//定義一個表示學生類型的枚舉類型 StudentType,他有三個成員分別是pupil、middleSchoolStudent、collegeStudents
enum StudentType {
  case pupil(String)
  case middleSchoolStudent(Int, String)
  case collegeStudents(Int, String)
}

這里我們并沒有為StudentType的成員提供具體的值,而是為他們綁定了不同的類型,分別是pupil綁定String類型、middleSchoolStudent和collegeStudents綁定(Int, String)元祖類型。接下來就可以創建不同StudentType枚舉實例并為對應的成員賦值了。

  //student1 是一個StudentType類型的常量,其值為pupil(小學生),特征是"have fun"(總是在玩耍)
let student1 = StudentType.pupil("have fun")
  //student2 是一個StudentType類型的常量,其值為middleSchoolStudent(中學生),特征是 7, "always study"(一周7天總是在學習)
let student2 = StudentType.middleSchoolStudent(7, "always study")
  //student3 是一個StudentType類型的常量,其值為collegeStudent(大學生),特征是 7, "always LOL"(一周7天總是在擼啊擼)
let student3 = StudentType.middleSchoolStudent(7, "always LOL")

這個時候如果需要判斷某個StudentType實例的具體的值就需要這樣做了

switch student3 {
    case .pupil(let things):
        print("is a pupil and \(things)")
    case .middleSchoolStudent(let day, let things):
        print("is a middleSchoolStudent and \(day) days \(things)")
    case .collegeStudent(let day, let things):
        print("is a collegeStudent and \(day) days \(things)")
  }

控制臺輸出:is a collegeStudent and 7 days always LOL,看到這你可能會想,是否可以為一個枚舉成員提供原始值并且綁定類型呢,答案是不能的!因為首先給成員提供了固定的原始值,那他以后就不能改變了;而為成員提供關聯值(綁定類型)就是為了創建枚舉實例的時候賦值。這不是互相矛盾嗎。

結構體(struct)
結構體的定義:
結構體是由一系列具有相同類型或不同類型的數據構成的數據集合。結構體是一種值類型的數據結構,在Swift中常常使用結構體封裝一些屬性甚至是方法來組成新的復雜類型,目的是簡化運算。我們通過使用關鍵詞struct來定義結構體。并在一對大括號內定義具體內容包括他的成員和自定義的方法(是的,Swift中的結構體有方法了),定義好的結構體存在一個自動生成的成員初始化器,使用它來初始化結構體實例的成員屬性。廢話不多說直接上代碼:

//定義一個 Student(學生)類型的結構體用于表示一個學生,Student的成員分別是語、數、外三科`Int`類型的成績
struct Student {
  var chinese: Int
  var math: Int
  var english: Int
}

看到這里熟悉Swift的同學可能已經發現了一點結構體和類的區別了:定義結構體類型時其成員可以沒有初始值。如果使用這種格式定義一個類,編譯器是會報錯的,他會提醒你這個類沒有被初始化。

結構體實例的創建 :
創建結構體和類的實例的語法非常相似,結構體和類兩者都能使用初始化器語法來生成新的實例。最簡單的語法是在類或結構體名字后面接一個空的圓括號,例如 let student1 = Student()。這樣就創建了一個新的類或者結構體的實例,任何成員都被初始化為它們的默認值(前提是成員均有默認值)。但是結合上面的代碼,由于在定義Student結構體時我們并沒有為他的成員賦初值,所以 let student1 = Student()在編譯器中報錯了,此處報錯并不是因為不能這樣創建實例而是因為student1成員沒有默認值,所以我們可以使用下面的方式創建實例:

//使用Student類型的結構體創建Student類型的實例(變量或常量)并初始化三個成員(這個學生的成績會不會太好了點)
let student2 = Student(chinese: 90, math: 80, english: 70)

所有的結構體都有一個自動生成的成員初始化器,你可以使用它來初始化新結構體實例的成員就像上面一樣(前提是沒有自定義的初始化器)。如果我們在定義Student時為他的成員賦上初值,那么下面的代碼是編譯通過的:

struct Student {
  var chinese: Int = 50
  var math: Int = 50
  var english: Int = 50
}
let student2 = Student(chinese: 90, math: 80, english: 70)
let student4 = Student()

總結一句:定義結構體類型時其成員可以沒有初始值,但是創建結構體實例時該實例的成員必須有初值。
自定義的初始化器
當我們想要使用自己的方式去初始化創建一個Student類型的實例時,系統提供的成員初始化器可能就不夠用了。例如,我們希望通過形如:let student5 =Student(stringScore: "70,80,90")的方式創建實例時,就需要自定義初始化方法了:

struct Student {
  var chinese: Int = 50
  var math: Int = 50
  var english: Int = 50
      init() {}
      init(chinese: Int, math: Int, english: Int) {
            self.chinese = chinese
            self.math = math
           self.english = english
      }
      init(stringScore: String) {
           let cme = stringScore.characters.split(separator: ",")
           chinese = Int(atoi(String(cme.first!)))
           math = Int(atoi(String(cme[1])))
           english = Int(atoi(String(cme.last!)))
      }
  }
  let student6 = Student()
  let student7 = Student(chinese: 90, math: 80, english: 70)
  let student8 = Student(stringScore: "70,80,90")

一旦我們自定義了初始化器,系統自動的初始化器就不起作用了,如果還需要使用到系統提供的初始化器,在我們自定義初始化器后就必須顯式的定義出來。
定義其他方法
如果此時需要修改某個學生某科的成績,該如何實現呢?當然,我們可以定義下面的方法:

//更改某個學生某門學科的成績
func changeChinese(num: Int, student: inout Student){
  student.chinese += num
}
changeChinese(num: 20, student: &student7)

此時student7的語文成績就由原來的90被修改到了110,但是此方法有兩個明顯的弊端:1,學生的語文成績chinese是Student結構體的內部成員,一個學生的某科成績無需被Student的使用者了解。即我們只關心學生的語文成績更改了多少,而不是關心學生語文成績本身是多少。2,更改一個學生的語文成績本身就是和Student結構體內部成員計算相關的事情,我們更希望達到形如:student7.changeChinese(num: 10) 的效果,因為只有學生本身清楚自己需要將語文成績更改多少(更像是面向對象封裝的思想)。很明顯此時changeChinese(num:)方法是Student結構體內部的方法而不是外部的方法,所以我定義了一個修改某個學生數學成績的內部方法用于和之前修改語文成績的外部方法對比:

struct Student {
    var chinese: Int = 50
    var math: Int = 50
    var english: Int = 50
   //修改數學成績
    mutating func changeMath(num: Int) {
        self.math += num
    }
  }
  var student7 = Student(chinese: 20, math: 30, english: 40)
  //更改分數中語文學科的成績
  func changeChinese(num: Int, student: inout Student){
      student.chinese += num
    }
  changeChinese(num: 20, student: &student7)
  student7.changeMath(num: 10)

盡管兩者都能達到同樣的效果,但是把修改結構體成員的方法定義在結構體內部顯得更加合理同時滿足面向對象封裝的特點。以上兩點就是我們為Student結構體內部添加changeMath(num:)的原因,他讓我們把類型相關的計算表現的更加自然和統一,即自己的事情應該用自己的方法實現不應該被別人關心。值得一提的是在結構體內部方法中如果修改了結構體的成員,那么該方法之前應該加入:mutating關鍵字。由于結構體是值類型,Swift規定不能直接在結構體的方法(初始化器除外)中修改成員。原因很簡單,結構體作為值的一種表現類型怎么能提供改變自己值的方法呢,但是使用mutating我們便可以辦到這點,當然這也是和類的不同點。

常見的結構體
Swift中很多的基礎數據類型都是結構體類型,下面列舉的是一些常用的結構體類型:

//表示數值類型的結構體:
  Int,Float,Double,CGFloat...
//表示字符和字符串類型的結構體
  Character,String...
//位置和尺寸的結構體
  CGPoint,CGSize...
//集合類型結構體
  Array,Set,Dictionary..

很多時候你不細心觀察的話可能不會想到自己信手拈來的代碼中居然藏了這么多結構體。另外有時候在使用類和結構體的時候會出現下面的情況

// Person 類
class Person {
    var name: String = "jack"
    let life: Int = 1
}
    var s1 = Person()
    var s2 = s1
     s2.name = "mike"
     s1
// People 結構體數據結構
struct People {
    var name: String = "jack"
    let life: Int = 1
}
    var p1 = People()
    var p2 = p1
      p2.name = "mike"
      p1

細心的同學可能已經發現了其中的詭異。變量s1、s2是Person類的實例,修改了s2的name屬性,s1的name也會改變;而p1、p2作為People結構體的實例,修改了p1的name屬性,p2的name并不會發生改變。這是為什么呢?總結中告訴你。(類是應用類型,結構體是枚舉類型)

總結

關于枚舉、結構體的介紹這里僅僅是冰山一角,他們還有更加豐富的功能需要讀者在閱讀完本文后深入學習。了解這些基礎內容,可以幫助我們在Swift開發中更熟練的使用他們。這里根據官方文檔介紹結合自己的理解簡單的做一下總結:

枚舉、結構體、類的共同點:
定義屬性和方法;
下標語法訪問值;
初始化器;
支持擴展增加功能;
可以遵循協議;

類特有的功能:
繼承;
允許類型轉換;
析構方法釋放資源;
引用計數;

類是引用類型
引用類型(reference types,通常是類)被復制的時候其實復制的是一份引用,兩份引用指向同一個對象。所以在修改一個實例的數據時副本的數據也被修改了(s1、s2)。

枚舉,結構體是值類型
值類型(value types)的每一個實例都有一份屬于自己的數據,在復制時修改一個實例的數據并不影響副本的數據(p1、p2)。值類型和引用類型是這三兄弟最本質的區別。

我該如何選擇
關于在新建一個類型時如何選擇到底是使用值類型還是引用類型的問題其實在理解了兩者之間的區別后是非常簡單的,在這蘋果官方已經做出了非常明確的指示(以下內容引自蘋果官方文檔):

當你使用Cocoa框架的時候,很多API都要通過NSObject的子類使用,所以這時候必須要用到引用類型class。在其他情況下,有下面幾個準則:

1.什么時候該用值類型:

要用==運算符來比較實例的數據時
你希望那個實例的拷貝能保持獨立的狀態時
數據會被多個線程使用時
2.什么時候該用引用類型(class):

要用==運算符來比較實例身份的時候
你希望有創建一個共享的、可變對象的時候

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容