Swift中enum、struct、class三者異同

wso2701435 7年前發布 | 4K 次閱讀 Swift Apple Swift開發

前言

由于在開發過程中常常需要用到系統提供的基礎類型之外的的類型,所以Swift允許我們根據自己的需要構建屬于自己的類型系統以便于更加靈活和方便的開發程序并將其稱之為named types。Swift主要為我們提供了以下四種named types 分別是:enum、struct、class和protocol,  相信熟悉objective-c開發的同學們對于iOS中枚舉、結構體和類的概念一點都不陌生。相比于前輩objective-c中的這三者,Swift將enum和struct變得更加靈活且強大,并且賦予了他們很多和class相同的屬性實現更加豐富多彩的功能,以至于有時候我們很難分清他們到底有什么區別以及我該什么時候用哪種類型,接下來本文將重點介紹一下在Swift中enum和struct的定義和新特性以及兩者與class之間的異同,也是自己學習Swift以來的階段性總結

枚舉(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





 .Type
type(of: student3)
//  student4的值是nil,因為并不能通過整數30得到一個StudentType實例的值
let student4 = StudentType.init(rawValue: 30)


  

使用原始值初始化器這種方式初始化創建得到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,看到這你可能會想,是否可以為一個枚舉成員提供原始值并且綁定類型呢,答案是不能的!因為首先給成員提供了固定的原始值,那他以后就不能改變了;而為成員提供關聯值(綁定類型)就是為了創建枚舉實例的時候賦值。這不是互相矛盾嗎。

  • 遞歸枚舉

遞歸枚舉是擁有另一個枚舉作為枚舉成員關聯值的枚舉(引自文檔翻譯)。

關于遞歸枚舉我們可以拆封成兩個概念來看:遞歸 + 枚舉。遞歸是指在程序運行中函數(或方法)直接或間接調用自己的這樣一種方式,其特點為重復有限個步驟、格式較為簡單。下面是一個經典的通過遞歸算法求解n!(階乘)的函數。

func factorial(n: Int)->Int {
    if n > 0 {
        return n * factorial(n: n - 1)
    } else {
        return 1
    }
}
//1 * 2 * 3 * 4 * 5 * 6 = 720
let sum = factorial(n: 6)

函數factorial (n: int)-> Int在執行過程中很明顯的調用了自身。結合枚舉的概念我們這里可以簡單的理解為遞歸枚舉類似上面將枚舉值本身傳入給成員去判斷的情況。因為實在沒找到很好體現遞歸枚舉的例子,而且本人對遞歸枚舉的使用場景都用在哪些地方還不是很了解,所以呢這里就不獻丑了。可以看出Swift中枚舉變得更加靈活和復雜,有遞歸枚舉的概念,還有很多和類類似的特性,比如:計算屬性用來提供關于枚舉當前值的額外信息;實例方法提供與枚舉表示值相關的功能;定義初始化器來初始化成員值;而且能夠遵循協議來提供標準功能等等,由于筆者目前還沒有更加深入的學習這些東西,所以這些內容有機會將在后面的章節講到。

結構體(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):

  • 要用==運算符來比較實例身份的時候

  • 你希望有創建一個共享的、可變對象的時候

最后

以上就是本人前段時間學習心得,示例代碼在Swift3.0語法下都是編譯通過的,知識點比較少,部分描述引自官方的文檔。如果文中有任何紕漏或錯誤歡迎在評論區留言指出,本人將在第一時間修改過來。

 

來自:http://www.cocoachina.com/swift/20161221/18377.html

 

 本文由用戶 wso2701435 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!