Swift主要為我們提供了以下四種”named types“ 分別是:enum
约巷、struct
、class
和protocol
挥唠。相信熟悉iOS開發(fā)的同學們對于枚舉液走、結構體和類的概念一點都不陌生。相比于前輩Objective-C中的這三者钱慢,Swift將enum
和struct
變得更加靈活且強大逮京,并且賦予了他們很多和class相同的屬性實現(xiàn)更加豐富多彩的功能,以至于有時候我們很難分清他們到底有什么區(qū)別以及我該什么時候用哪種類型束莫,接下來本文將重點介紹一下在Swift中enum
和struct
的定義和新特性以及兩者與class
之間的異同懒棉,也是自己學習Swift
以來的階段性總結。
枚舉(enum)
枚舉的定義:
Swift中的枚舉是為一組有限種可能性的相關值提供的通用類型(在C/C++/Objective-C中览绿,枚舉是一個被命名的整型常數(shù)的集合)策严;使用枚舉可以類型安全并且有提示性地操作這些值。與結構體挟裂、類相似享钞,使用關鍵詞enum
來定義枚舉,并在一對大括號內定義具體內容包括使用case
關鍵字列舉成員。就像下面一樣:
//定義一個表示學生類型的全新枚舉類型 StudentType栗竖,他有三個成員分別是pupil(小學生暑脆,玩LOL最怕遇到這種隊友了)、middleSchoolStudent(中學生狐肢,現(xiàn)在的中學生都很拽)添吗、collegeStudents(大學生,據(jù)說大學生活很不錯份名,注意斷句)
enum StudentType {
case pupil
case middleSchoolStudent
case collegeStudent
}
上面的代碼可以讀作:如果存在一個StudentType
的實例碟联,他要么是pupil (小學生)
、要么是middleSchoolStudent(中學生)
僵腺、要么是collegeStudent(大學生)
鲤孵。注意,和C辰如、Objective-C中枚舉的不同普监,Swift 中的枚舉成員在被創(chuàng)建時不會分配一個默認的整數(shù)值。而且不需要給枚舉中的每一個成員都提供值(如果你需要也是可以的)琉兜。如果一個值(所謂“原始值”)要被提供給每一個枚舉成員凯正,那么這個值可以是字符串、字符豌蟋、任意的整數(shù)值廊散,或者是浮點類型(引自文檔翻譯)。簡單說Swift中定義的枚舉只需要幫助我們表明不同的情況就夠了梧疲,他的成員可以沒有值允睹,也可以有其他類型的值(不局限于整數(shù)類型)。
枚舉中有兩個很容易混淆的概念:原始值(raw value)幌氮、關聯(lián)值(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
屬性來訪問成員的原始值隘马,或者是使用原始值初始化器來嘗試創(chuàng)建一個枚舉的新實例
// 常量student1值是 10
let student1 = StudentType.pupil.rawValue
// 變量student2值是 15
var student2 = StudentType.middleSchoolStudent.rawValue
// 使用成員rawValue屬性創(chuàng)建一個`StudentType`枚舉的新實例
let student3 = StudentType.init(rawValue: 15)
// student3的值是 Optional<Senson>.Type
type(of: student3)
// student4的值是nil太防,因為并不能通過整數(shù)30得到一個StudentType實例的值
let student4 = StudentType.init(rawValue: 30)
使用原始值初始化器這種方式初始化創(chuàng)建得到StudentType
的實例student4
是一個StudentType
的可選類型,因為并不是給定一個年齡就能找到對應的學生類型酸员,比如在StudentType
中給定年齡為30就找不到對應的學生類型(很可能30歲的人已經是博士了)蜒车。所以原始值初始化器是一個可失敗初始化器讳嘱。
總結一句:原始值是為枚舉的成員們綁定了一組類型必須相同值不同的固定的值(可能是整型,浮點型酿愧,字符類型等等)沥潭。這樣很好解釋為什么提供原始值的時候用的是等號。
* 枚舉的關聯(lián)值(associated value)
關聯(lián)值和原始值不同嬉挡,關聯(lián)值更像是為枚舉的成員們綁定了一組類型钝鸽,不同的成員可以是不同的類型(提供關聯(lián)值時用的是括號)。例如下面的代碼:
//定義一個表示學生類型的枚舉類型 StudentType庞钢,他有三個成員分別是pupil拔恰、middleSchoolStudent、collegeStudents
enum StudentType {
case pupil(String)
case middleSchoolStudent(Int, String)
case collegeStudents(Int, String)
}
這里我們并沒有為StudentType
的成員提供具體的值基括,而是為他們綁定了不同的類型颜懊,分別是pupil
綁定String
類型、middleSchoolStudent
和collegeStudents
綁定(Int
阱穗, String
)元祖類型饭冬。接下來就可以創(chuàng)建不同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
冰沙,看到這你可能會想侨艾,是否可以為一個枚舉成員提供原始值并且綁定類型呢,答案是不能的拓挥!因為首先給成員提供了固定的原始值唠梨,那他以后就不能改變了;而為成員提供關聯(lián)值(綁定類型)就是為了創(chuàng)建枚舉實例的時候賦值侥啤。這不是互相矛盾嗎当叭。
* 遞歸枚舉
遞歸枚舉是擁有另一個枚舉作為枚舉成員關聯(lián)值的枚舉(引自文檔翻譯)。
關于遞歸枚舉我們可以拆封成兩個概念來看:遞歸 + 枚舉盖灸。遞歸是指在程序運行中函數(shù)(或方法)直接或間接調用自己的這樣一種方式蚁鳖,其特點為重復有限個步驟、格式較為簡單赁炎。下面是一個經典的通過遞歸算法求解n!(階乘)的函數(shù)醉箕。
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)
函數(shù)factorial (n: int)-> Int
在執(zhí)行過程中很明顯的調用了自身。結合枚舉的概念我們這里可以簡單的理解為遞歸枚舉類似上面將枚舉值本身傳入給成員去判斷的情況。因為實在沒找到很好體現(xiàn)遞歸枚舉的例子讥裤,而且本人對遞歸枚舉的使用場景都用在哪些地方還不是很了解放棒,所以呢這里就不獻丑了。
可以看出Swift
中枚舉變得更加靈活和復雜坞琴,有遞歸枚舉的概念哨查,還有很多和Class
類似的特性,比如:計算屬性用來提供關于枚舉當前值的額外信息剧辐;實例方法提供與枚舉表示值相關的功能寒亥;定義初始化器來初始化成員值;而且能夠遵循協(xié)議來提供標準功能等等荧关,由于筆者目前還沒有更加深入的學習這些東西溉奕,所以這些內容有機會將在后面的章節(jié)講到。
結構體(struct)
* 結構體的定義:
結構體是由一系列具有相同類型或不同類型的數(shù)據(jù)構成的數(shù)據(jù)集合忍啤。結構體是一種值類型的數(shù)據(jù)結構加勤,在Swift
中常常使用結構體封裝一些屬性甚至是方法來組成新的復雜類型,目的是簡化運算同波。我們通過使用關鍵詞struct來定義結構體鳄梅。并在一對大括號內定義具體內容包括他的成員和自定義的方法(是的,Swift
中的結構體有方法了)未檩,定義好的結構體存在一個自動生成的成員初始化器戴尸,使用它來初始化結構體實例的成員屬性。廢話不多說直接上代碼:
//定義一個 Student(學生)類型的結構體用于表示一個學生冤狡,Student的成員分別是語孙蒙、數(shù)、外三科`Int`類型的成績
struct Student {
var chinese: Int
var math: Int
var english: Int
}
看到這里熟悉Swift的同學可能已經發(fā)現(xiàn)了一點結構體和類的區(qū)別了:定義結構體類型時其成員可以沒有初始值悲雳。如果使用這種格式定義一個類挎峦,編譯器是會報錯的,他會提醒你這個類沒有被初始化合瓢。
- 結構體實例的創(chuàng)建 :
創(chuàng)建結構體和類的實例的語法非常相似坦胶,結構體和類兩者都能使用初始化器語法來生成新的實例。最簡單的語法是在類或結構體名字后面接一個空的圓括號晴楔,例如let student1 = Student()
迁央。這樣就創(chuàng)建了一個新的類或者結構體的實例,任何成員都被初始化為它們的默認值(前提是成員均有默認值)滥崩。但是結合上面的代碼,由于在定義Student
結構體時我們并沒有為他的成員賦初值讹语,所以let student1 = Student()
在編譯器中報錯了钙皮,此處報錯并不是因為不能這樣創(chuàng)建實例而是因為student1成員沒有默認值,所以我們可以使用下面的方式創(chuàng)建實例:
//使用Student類型的結構體創(chuàng)建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()
總結一句:定義結構體類型時其成員可以沒有初始值,但是創(chuàng)建結構體實例時該實例的成員必須有初值茸时。
-
自定義的初始化器
當我們想要使用自己的方式去初始化創(chuàng)建一個Student
類型的實例時贡定,系統(tǒng)提供的成員初始化器可能就不夠用了。例如可都,我們希望通過形如:let student5 = Student(stringScore: "70,80,90")
的方式創(chuàng)建實例時缓待,就需要自定義初始化方法了:
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")
一旦我們自定義了初始化器,系統(tǒng)自動的初始化器就不起作用了渠牲,如果還需要使用到系統(tǒng)提供的初始化器旋炒,在我們自定義初始化器后就必須顯式的定義出來。
-
定義其他方法
如果此時需要修改某個學生某科的成績签杈,該如何實現(xiàn)呢瘫镇?當然,我們可以定義下面的方法:
//更改某個學生某門學科的成績
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
結構體內部的方法而不是外部的方法行剂,所以我定義了一個修改某個學生數(shù)學成績的內部方法用于和之前修改語文成績的外部方法對比:
struct Student {
var chinese: Int = 50
var math: Int = 50
var english: Int = 50
//修改數(shù)學成績
mutating func changeMath(num: Int) {
self.math += num
}
}
var student7 = Student(chinese: 20, math: 30, english: 40)
//更改分數(shù)中語文學科的成績
func changeChinese(num: Int, student: inout Student){
student.chinese += num
}
changeChinese(num: 20, student: &student7)
student7.changeMath(num: 10)
盡管兩者都能達到同樣的效果,但是把修改結構體成員的方法定義在結構體內部顯得更加合理同時滿足面向對象封裝的特點钳降。以上兩點就是我們?yōu)?code>Student結構體內部添加changeMath(num:)
的原因厚宰,他讓我們把類型相關的計算表現(xiàn)的更加自然和統(tǒng)一,即自己的事情應該用自己的方法實現(xiàn)不應該被別人關心遂填。值得一提的是在結構體內部方法中如果修改了結構體的成員铲觉,那么該方法之前應該加入:mutating
關鍵字。
由于結構體是值類型吓坚,Swift規(guī)定不能直接在結構體的方法(初始化器除外)中修改成員撵幽。原因很簡單,結構體作為值的一種表現(xiàn)類型怎么能提供改變自己值的方法呢礁击,但是使用mutating我們便可以辦到這點盐杂,當然這也是和類的不同點逗载。
-
常見的結構體
Swift中很多的基礎數(shù)據(jù)類型都是結構體類型,下面列舉的是一些常用的結構體類型:
//表示數(shù)值類型的結構體:
Int链烈,F(xiàn)loat厉斟,Double,CGFloat...
//表示字符和字符串類型的結構體
Character强衡,String...
//位置和尺寸的結構體
CGPoint擦秽,CGSize...
//集合類型結構體
Array,Set漩勤,Dictionary...
很多時候你不細心觀察的話可能不會想到自己信手拈來的代碼中居然藏了這么多結構體感挥。另外有時候在使用類和結構體的時候會出現(xiàn)下面的情況
// Person 類
class Person {
var name: String = "jack"
let life: Int = 1
}
var s1 = Person()
var s2 = s1
s2.name = "mike"
s1
// People 結構體數(shù)據(jù)結構
struct People {
var name: String = "jack"
let life: Int = 1
}
var p1 = People()
var p2 = p1
p2.name = "mike"
p1
細心的同學可能已經發(fā)現(xiàn)了其中的詭異。變量s1
锯七、s2
是Person
類的實例链快,修改了s2
的name
屬性,s1
的name
也會改變眉尸;而p1
域蜗、p2
作為People
結構體的實例,修改了p1
的name
屬性噪猾,p2
的name
并不會發(fā)生改變霉祸。這是為什么呢?總結中告訴你袱蜡。
總結
-
枚舉丝蹭、結構體、類的共同點:
1坪蚁,定義屬性和方法奔穿;
2,下標語法訪問值敏晤;
3贱田,初始化器;
4嘴脾,支持擴展增加功能男摧;
5,可以遵循協(xié)議译打;
-
類特有的功能:
1耗拓,繼承;
2奏司,允許類型轉換乔询;
3,析構方法釋放資源韵洋;
4哥谷,引用計數(shù)岸夯;
-
類是引用類型:
引用類型(reference types,通常是類)被復制的時候其實復制的是一份引用们妥,兩份引用指向同一個對象。所以在修改一個實例的數(shù)據(jù)時副本的數(shù)據(jù)也被修改了(s1
勉吻、s2
)监婶。
-
枚舉,結構體是值類型
值類型(value types)的每一個實例都有一份屬于自己的數(shù)據(jù),在復制時修改一個實例的數(shù)據(jù)并不影響副本的數(shù)據(jù)(p1
齿桃、p2
)惑惶。值類型和引用類型是這三兄弟最本質的區(qū)別。
-
我該如何選擇
關于在新建一個類型時如何選擇到底是使用值類型還是引用類型的問題其實在理解了兩者之間的區(qū)別后是非常簡單的短纵,在這蘋果官方已經做出了非常明確的指示(以下內容引自蘋果官方文檔):
當你使用Cocoa框架的時候带污,很多API都要通過NSObject的子類使用,所以這>時候必須要用到引用類型class香到。在其他情況下鱼冀,有下面幾個準則:
- 什么時候該用值類型:
要用==運算符來比較實例的數(shù)據(jù)時
你希望那個實例的拷貝能保持獨立的狀態(tài)時
數(shù)據(jù)會被多個線程使用時
- 什么時候該用引用類型(class):
要用==運算符來比較實例身份的時候
你希望有創(chuàng)建一個共享的、可變對象的時候
相對于 Objective-C 中的結構體悠就,Swift 對結構體的使用比重大了很多千绪,結構體成為了實現(xiàn)面向對象的重要工具。Swift 中的結構體與 C++ 和 Objective-C 中的結構體有很大的差別梗脾,C++ 和 Objective-C 中的結構體只能定義一組相關的成員變量荸型,而 Swift 中的結構體不僅可以定義成員變量(屬性),還可以定義成員方法炸茧。因此瑞妇,我們可以把結構體看做是一種輕量級的類。Swift 中類和結構體非常類似梭冠,都具有定義和使用屬性辕狰、方法、下標和構造器等面向對象特性妈嘹,但是結構體不具有繼承性柳琢,也不具備運行時強制類型轉換、使用析構器和使用引用計等能力润脸。Swift 中 struct 是值類型柬脸,而 class 是引用類型,所以這篇文章 struct 的行為也可以用到所有的值類型上面毙驯,相同地 class 的行為也可以用到引用類型上倒堕。值類型的變量直接包含他們的數(shù)據(jù),而引用類型的變量存儲對他們的數(shù)據(jù)引用爆价,因此后者稱為對象垦巴,因此對一個變量操作可能影響另一個變量所引用的對象媳搪。對于值類型都有他們自己的數(shù)據(jù)副本,因此對一個變量操作不可能影響另一個變量骤宣。
1.類和結構體定義
Swift中的類和結構體定義的語法是非常相似的秦爆。我們可以使用 class 關鍵詞定義類,使用 struct 關鍵詞定義結構體憔披,它們的語法格式如下:
// 定義類
class 類名 {
定義類的成員
}
// 建立一個 class 名稱為 ClassCoder
class ClassCoder {
var name = "IAMCJ"
var age = 0
}
// 定義結構體
struct 結構體名 {
定義結構體的成員
}
// 建立一個 struct 名稱為 StructCoder
struct StructCoder {
var name = "IAMCJ"
var age = 0
}
2.類和結構體實例化
// 類實例化
let classCoder = ClassCoder()
// class 不能直接用 ClassCoder(name:"CJ",age:18) 必需要自己創(chuàng)建構造函數(shù)才可以
classCoder.name = "CJ"
classCoder.age = 18
// 結構體實例化
var structCoder = StructCoder(name:"CJ",age:18)
// 另外一種實例化方法
var structCoder = StructCoder()
structCoder.name = "CJ"
structCoder.age = 18
區(qū)別:class 在實例化的時候不能自動把 property 放在 constructor 的參數(shù)里面去等限,想要和 struct 一樣的效果就需要我們自己去創(chuàng)建構造函數(shù)了。
賦值給另外一個變量
// 類賦值
let classCoder = ClassCoder()
classCoder.name = "CJ"
classCoder.age = 18
// classCoder.name=CJ,classCoder.age=18
let classCoder1 = classCoder
classCoder1.name = "NOTCJ"
classCoder1.age = 100
// classCoder.name=NOTCJ,classCoder.age=100,classCoder1.name=NOTCJ,classCoder1.age=100
// 結構體賦值
var structCoder = StructCoder()
structCoder.name = "CJ"
structCoder.age = 18
// structCoder.name=CJ,structCoder.age=18
var structCoder1 = structCoder
structCoder1.name = "NOTCJ"
structCoder1.age = 100
// structCoder.name=CJ,structCoder.age=18,structCoder1.name=NOTCJ,structCoder1.age=100
區(qū)別:class 是引用類型芬膝,顧名思義在賦值的時候只是給另外一個變量賦予了一個引用的效果望门,而 struct 是值類型,它會復制一份完全相同的內容給另外一個變量锰霜,從上面的測試可以清晰的分辨出他們的不同之處筹误。結合這篇文章可能能讓你更好的理解。
是否可變
let classCoder = ClassCoder()
classCoder.name = "CJ"
classCoder.age = 18
// 此處可以修改
let structCoder = StructCoder()
structCoder.name = "CJ"
區(qū)別:let 在 class 上并不會報錯癣缅。但是 Swift 常用的 String, Array, Dictionary 都是 struct厨剪,所以 let 是會有效果的,這里需要大家注意一下所灸。
5. mutating 關鍵字
//在不修改原 class 和 struct 的情況下添加一個 method:modifyCoderName(newName:)
// 類
class ClassCoder {
var name = "IAMCJ"
var age = 0
}
extension ClassCoder {
func modifyCoderName(newName:String) {
self.name = newName
}
}
// 結構體
struct StructCoder {
var name = "IAMCJ"
var age = 0
}
extension StructCoder {
mutating func modifyCoderName(newName:String) {
self.name = newName
}
}
區(qū)別:struct 在 function 里面需要修改 property 的時候需要加上 mutating 關鍵字丽惶,而 class 就不用了。
6.關于繼承
// 創(chuàng)建一個 繼承與 ClassCoder 類的 ClassSwiftCoder:
class ClassCoder {
var name = "IAMCJ"
var age = 0
}
extension ClassCoder {
func modifyCoderName(newName:String) {
self.name = newName
}
}
class ClassSwiftCoder:ClassCoder {
}
// 實例化一個 Swift 程序員 ClassSwiftCoder:
let swiftCoder = ClassSwiftCoder()
swiftCoder.name = "CJ"
swiftCoder.name = "18"
swiftCoder.modifyCoderName(newName: "帥")