前言
由于在開發(fā)過程中常常需要用到系統(tǒng)提供的基礎(chǔ)類型之外的的類型,所以Swift允許我們根據(jù)自己的需要構(gòu)建屬于自己的類型系統(tǒng)以便于更加靈活和方便的開發(fā)程序并將其稱之為 “named types“寻咒。
Swift主要為我們提供了以下四種”named types“ 分別是:enum
哮翘、struct
、class
和protocol
毛秘。相信熟悉iOS開發(fā)的同學(xué)們對于枚舉饭寺、結(jié)構(gòu)體和類的概念一點都不陌生。相比于前輩Objective-C中的這三者叫挟,Swift將enum
和struct
變得更加靈活且強大艰匙,并且賦予了他們很多和class
相同的屬性實現(xiàn)更加豐富多彩的功能,以至于有時候我們很難分清他們到底有什么區(qū)別以及我該什么時候用哪種類型抹恳,接下來本文將重點介紹一下在Swift中enum
和struct
的定義和新特性以及兩者與class
之間的異同员凝,也是自己學(xué)習(xí)Swift以來的階段性總結(jié)。
?枚舉(enum)
-
枚舉的定義:
Swift中的枚舉是為一組有限種可能性的相關(guān)值提供的通用類型(在C/C++/Objective-C中奋献,枚舉是一個被命名的整型常數(shù)的集合)健霹;使用枚舉可以類型安全并且有提示性地操作這些值。與結(jié)構(gòu)體瓶蚂、類相似糖埋,使用關(guān)鍵詞enum
來定義枚舉,并在一對大括號內(nèi)定義具體內(nèi)容包括使用case
關(guān)鍵字列舉成員扬跋。就像下面一樣:
//定義一個表示學(xué)生類型的全新枚舉類型 StudentType,他有三個成員分別是pupil(小學(xué)生凌节,玩LOL最怕遇到這種隊友了)钦听、middleSchoolStudent(中學(xué)生,現(xiàn)在的中學(xué)生都很拽)倍奢、collegeStudents(大學(xué)生朴上,據(jù)說大學(xué)生活很不錯,注意斷句)
enum StudentType {
case pupil
case middleSchoolStudent
case collegeStudent
}
上面的代碼可以讀作:如果存在一個StudentType
的實例卒煞,他要么是pupil (小學(xué)生)
痪宰、要么是middleSchoolStudent(中學(xué)生)
、要么是collegeStudent(大學(xué)生)
。注意衣撬,和C乖订、Objective-C中枚舉的不同,Swift 中的枚舉成員在被創(chuàng)建時不會分配一個默認(rèn)的整數(shù)值具练。而且不需要給枚舉中的每一個成員都提供值(如果你需要也是可以的)乍构。如果一個值(所謂“原始值”)要被提供給每一個枚舉成員,那么這個值可以是字符串扛点、字符哥遮、任意的整數(shù)值,或者是浮點類型(引自文檔翻譯)陵究。簡單說Swift中定義的枚舉只需要幫助我們表明不同的情況就夠了眠饮,他的成員可以沒有值,也可以有其他類型的值(不局限于整數(shù)類型)铜邮。
枚舉中有兩個很容易混淆的概念:原始值(raw value)仪召、關(guān)聯(lián)值(associated value),兩個詞聽起來比較模糊牲距,下面簡單介紹一下:
-
枚舉的原始值(raw value)
枚舉成員可以用相同類型的默認(rèn)值預(yù)先填充返咱,這樣的值我們稱為原始值(raw value),下面的StudentType
中三個成員分別被Int
類型的10
牍鞠、15
咖摹、20
填充表示不同階段學(xué)生的年齡。注意: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
的可選類型,因為并不是給定一個年齡就能找到對應(yīng)的學(xué)生類型攀芯,比如在StudentType
中給定年齡為30就找不到對應(yīng)的學(xué)生類型(很可能30歲的人已經(jīng)是博士了)屯断。所以原始值初始化器是一個可失敗初始化器。
總結(jié)一句:原始值是為枚舉的成員們綁定了一組類型必須相同值不同的固定的值(可能是整型侣诺,浮點型殖演,字符類型等等)。這樣很好解釋為什么提供原始值的時候用的是等號年鸳。
-
枚舉的關(guān)聯(lián)值(associated value)
關(guān)聯(lián)值和原始值不同趴久,關(guān)聯(lián)值更像是為枚舉的成員們綁定了一組類型,不同的成員可以是不同的類型(提供關(guān)聯(lián)值時用的是括號)搔确。例如下面的代碼:
//定義一個表示學(xué)生類型的枚舉類型 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
枚舉實例并為對應(yīng)的成員賦值了。
//student1 是一個StudentType類型的常量瘦真,其值為pupil(小學(xué)生)刊头,特征是"have fun"(總是在玩耍)
let student1 = StudentType.pupil("have fun")
//student2 是一個StudentType類型的常量,其值為middleSchoolStudent(中學(xué)生)诸尽,特征是 7, "always study"(一周7天總是在學(xué)習(xí))
let student2 = StudentType.middleSchoolStudent(7, "always study")
//student3 是一個StudentType類型的常量原杂,其值為collegeStudent(大學(xué)生),特征是 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
您机,看到這你可能會想穿肄,是否可以為一個枚舉成員提供原始值并且綁定類型呢,答案是不能的际看!因為首先給成員提供了固定的原始值咸产,那他以后就不能改變了;而為成員提供關(guān)聯(lián)值(綁定類型)就是為了創(chuàng)建枚舉實例的時候賦值仲闽。這不是互相矛盾嗎脑溢。
-
遞歸枚舉
遞歸枚舉是擁有另一個枚舉作為枚舉成員關(guān)聯(lián)值的枚舉(引自文檔翻譯)。
關(guān)于遞歸枚舉我們可以拆封成兩個概念來看:遞歸 + 枚舉赖欣。遞歸是指在程序運行中函數(shù)(或方法)直接或間接調(diào)用自己的這樣一種方式屑彻,其特點為重復(fù)有限個步驟、格式較為簡單顶吮。下面是一個經(jīng)典的通過遞歸算法求解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í)行過程中很明顯的調(diào)用了自身。結(jié)合枚舉的概念我們這里可以簡單的理解為遞歸枚舉類似上面將枚舉值本身傳入給成員去判斷的情況悴了。因為實在沒找到很好體現(xiàn)遞歸枚舉的例子搏恤,而且本人對遞歸枚舉的使用場景都用在哪些地方還不是很了解,所以呢這里就不獻(xiàn)丑了湃交。
可以看出Swift中枚舉變得更加靈活和復(fù)雜熟空,有遞歸枚舉的概念,還有很多和Class
類似的特性巡揍,比如:計算屬性用來提供關(guān)于枚舉當(dāng)前值的額外信息痛阻;實例方法提供與枚舉表示值相關(guān)的功能菌瘪;定義初始化器來初始化成員值腮敌;而且能夠遵循協(xié)議來提供標(biāo)準(zhǔn)功能等等阱当,由于筆者目前還沒有更加深入的學(xué)習(xí)這些東西,所以這些內(nèi)容有機會將在后面的章節(jié)講到糜工。
? 結(jié)構(gòu)體(struct)
-
結(jié)構(gòu)體的定義:
結(jié)構(gòu)體是由一系列具有相同類型或不同類型的數(shù)據(jù)構(gòu)成的數(shù)據(jù)集合弊添。結(jié)構(gòu)體是一種值類型的數(shù)據(jù)結(jié)構(gòu),在Swift中常常使用結(jié)構(gòu)體封裝一些屬性甚至是方法來組成新的復(fù)雜類型捌木,目的是簡化運算油坝。我們通過使用關(guān)鍵詞struct
來定義結(jié)構(gòu)體。并在一對大括號內(nèi)定義具體內(nèi)容包括他的成員和自定義的方法(是的刨裆,Swift中的結(jié)構(gòu)體有方法了)澈圈,定義好的結(jié)構(gòu)體存在一個自動生成的成員初始化器,使用它來初始化結(jié)構(gòu)體實例的成員屬性帆啃。廢話不多說直接上代碼:
//定義一個 Student(學(xué)生)類型的結(jié)構(gòu)體用于表示一個學(xué)生瞬女,Student的成員分別是語、數(shù)努潘、外三科`Int`類型的成績
struct Student {
var chinese: Int
var math: Int
var english: Int
}
看到這里熟悉Swift的同學(xué)可能已經(jīng)發(fā)現(xiàn)了一點結(jié)構(gòu)體和類的區(qū)別了:定義結(jié)構(gòu)體類型時其成員可以沒有初始值诽偷。如果使用這種格式定義一個類,編譯器是會報錯的疯坤,他會提醒你這個類沒有被初始化报慕。
-
結(jié)構(gòu)體實例的創(chuàng)建 :
創(chuàng)建結(jié)構(gòu)體和類的實例的語法非常相似,結(jié)構(gòu)體和類兩者都能使用初始化器語法來生成新的實例压怠。最簡單的語法是在類或結(jié)構(gòu)體名字后面接一個空的圓括號眠冈,例如let student1 = Student()
。這樣就創(chuàng)建了一個新的類或者結(jié)構(gòu)體的實例刑峡,任何成員都被初始化為它們的默認(rèn)值(前提是成員均有默認(rèn)值)洋闽。但是結(jié)合上面的代碼,由于在定義Student
結(jié)構(gòu)體時我們并沒有為他的成員賦初值突梦,所以let student1 = Student()
在編譯器中報錯了诫舅,此處報錯并不是因為不能這樣創(chuàng)建實例而是因為student1
成員沒有默認(rèn)值,所以我們可以使用下面的方式創(chuàng)建實例:
//使用Student類型的結(jié)構(gòu)體創(chuàng)建Student類型的實例(變量或常量)并初始化三個成員(這個學(xué)生的成績會不會太好了點)
let student2 = Student(chinese: 90, math: 80, english: 70)
所有的結(jié)構(gòu)體都有一個自動生成的成員初始化器宫患,你可以使用它來初始化新結(jié)構(gòu)體實例的成員就像上面一樣(前提是沒有自定義的初始化器)刊懈。如果我們在定義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()
總結(jié)一句:定義結(jié)構(gòu)體類型時其成員可以沒有初始值娃闲,但是創(chuàng)建結(jié)構(gòu)體實例時該實例的成員必須有初值虚汛。
-
自定義的初始化器
當(dā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)提供的初始化器冷溶,在我們自定義初始化器后就必須顯式的定義出來。
-
定義其他方法
如果此時需要修改某個學(xué)生某科的成績尊浓,該如何實現(xiàn)呢逞频?當(dāng)然,我們可以定義下面的方法:
//更改某個學(xué)生某門學(xué)科的成績
func changeChinese(num: Int, student: inout Student){
student.chinese += num
}
changeChinese(num: 20, student: &student7)
此時student7
的語文成績就由原來的90被修改到了110栋齿,但是此方法有兩個明顯的弊端:1苗胀,學(xué)生的語文成績chinese
是Student
結(jié)構(gòu)體的內(nèi)部成員,一個學(xué)生的某科成績無需被Student
的使用者了解瓦堵。即我們只關(guān)心學(xué)生的語文成績更改了多少基协,而不是關(guān)心學(xué)生語文成績本身是多少。2菇用,更改一個學(xué)生的語文成績本身就是和Student
結(jié)構(gòu)體內(nèi)部成員計算相關(guān)的事情堡掏,我們更希望達(dá)到形如:student7.changeChinese(num: 10)
的效果,因為只有學(xué)生本身清楚自己需要將語文成績更改多少(更像是面向?qū)ο蠓庋b的思想)刨疼。很明顯此時changeChinese(num:)
方法是Student
結(jié)構(gòu)體內(nèi)部的方法而不是外部的方法泉唁,所以我定義了一個修改某個學(xué)生數(shù)學(xué)成績的內(nèi)部方法用于和之前修改語文成績的外部方法對比:
struct Student {
var chinese: Int = 50
var math: Int = 50
var english: Int = 50
//修改數(shù)學(xué)成績
mutating func changeMath(num: Int) {
self.math += num
}
}
var student7 = Student(chinese: 20, math: 30, english: 40)
//更改分?jǐn)?shù)中語文學(xué)科的成績
func changeChinese(num: Int, student: inout Student){
student.chinese += num
}
changeChinese(num: 20, student: &student7)
student7.changeMath(num: 10)
盡管兩者都能達(dá)到同樣的效果,但是把修改結(jié)構(gòu)體成員的方法定義在結(jié)構(gòu)體內(nèi)部顯得更加合理同時滿足面向?qū)ο蠓庋b的特點揩慕。以上兩點就是我們?yōu)?code>Student結(jié)構(gòu)體內(nèi)部添加changeMath(num:)
的原因亭畜,他讓我們把類型相關(guān)的計算表現(xiàn)的更加自然和統(tǒng)一,即自己的事情應(yīng)該用自己的方法實現(xiàn)不應(yīng)該被別人關(guān)心迎卤。值得一提的是在結(jié)構(gòu)體內(nèi)部方法中如果修改了結(jié)構(gòu)體的成員拴鸵,那么該方法之前應(yīng)該加入:mutating
關(guān)鍵字。
由于結(jié)構(gòu)體是值類型蜗搔,Swift規(guī)定不能直接在結(jié)構(gòu)體的方法(初始化器除外)中修改成員劲藐。原因很簡單,結(jié)構(gòu)體作為值的一種表現(xiàn)類型怎么能提供改變自己值的方法呢樟凄,但是使用mutating
我們便可以辦到這點聘芜,當(dāng)然這也是和類的不同點。
-
常見的結(jié)構(gòu)體
Swift中很多的基礎(chǔ)數(shù)據(jù)類型都是結(jié)構(gòu)體類型缝龄,下面列舉的是一些常用的結(jié)構(gòu)體類型:
//表示數(shù)值類型的結(jié)構(gòu)體:
Int汰现,F(xiàn)loat,Double叔壤,CGFloat...
//表示字符和字符串類型的結(jié)構(gòu)體
Character瞎饲,String...
//位置和尺寸的結(jié)構(gòu)體
CGPoint,CGSize...
//集合類型結(jié)構(gòu)體
Array炼绘,Set嗅战,Dictionary...
很多時候你不細(xì)心觀察的話可能不會想到自己信手拈來的代碼中居然藏了這么多結(jié)構(gòu)體。另外有時候在使用類和結(jié)構(gòu)體的時候會出現(xiàn)下面的情況
// Person 類
class Person {
var name: String = "jack"
let life: Int = 1
}
var s1 = Person()
var s2 = s1
s2.name = "mike"
s1
// People 結(jié)構(gòu)體數(shù)據(jù)結(jié)構(gòu)
struct People {
var name: String = "jack"
let life: Int = 1
}
var p1 = People()
var p2 = p1
p2.name = "mike"
p1
細(xì)心的同學(xué)可能已經(jīng)發(fā)現(xiàn)了其中的詭異俺亮。變量s1
驮捍、s2
是Person
類的實例形庭,修改了s2
的name
屬性,s1
的name
也會改變厌漂;而p1
叛甫、p2
作為People
結(jié)構(gòu)體的實例争便,修改了p1
的name
屬性,p2
的name
并不會發(fā)生改變审姓。這是為什么呢囤踩?總結(jié)中告訴你旨椒。
總結(jié)
關(guān)于枚舉、結(jié)構(gòu)體的介紹這里僅僅是冰山一角堵漱,他們還有更加豐富的功能需要讀者在閱讀完本文后深入學(xué)習(xí)综慎。了解這些基礎(chǔ)內(nèi)容,可以幫助我們在Swift開發(fā)中更熟練的使用他們勤庐。這里根據(jù)官方文檔介紹結(jié)合自己的理解簡單的做一下總結(jié):
枚舉示惊、結(jié)構(gòu)體、類的共同點:
1愉镰,定義屬性和方法米罚;
2,下標(biāo)語法訪問值丈探;
3录择,初始化器;
4碗降,支持?jǐn)U展增加功能隘竭;
5,可以遵循協(xié)議讼渊;類特有的功能:
1动看,繼承;
2爪幻,允許類型轉(zhuǎn)換弧圆;
3,析構(gòu)方法釋放資源笔咽;
4搔预,引用計數(shù);類是引用類型
引用類型(reference types叶组,通常是類)被復(fù)制的時候其實復(fù)制的是一份引用拯田,兩份引用指向同一個對象。所以在修改一個實例的數(shù)據(jù)時副本的數(shù)據(jù)也被修改了(s1
甩十、s2
)船庇。枚舉吭产,結(jié)構(gòu)體是值類型
值類型(value types)的每一個實例都有一份屬于自己的數(shù)據(jù),在復(fù)制時修改一個實例的數(shù)據(jù)并不影響副本的數(shù)據(jù)(p1
鸭轮、p2
)臣淤。值類型和引用類型是這三兄弟最本質(zhì)的區(qū)別。-
我該如何選擇
關(guān)于在新建一個類型時如何選擇到底是使用值類型還是引用類型的問題其實在理解了兩者之間的區(qū)別后是非常簡單的窃爷,在這蘋果官方已經(jīng)做出了非常明確的指示(以下內(nèi)容引自蘋果官方文檔):當(dāng)你使用Cocoa框架的時候邑蒋,很多API都要通過NSObject的子類使用,所以這>時候必須要用到引用類型class按厘。在其他情況下医吊,有下面幾個準(zhǔn)則:
-
什么時候該用值類型:
要用==運算符來比較實例的數(shù)據(jù)時
你希望那個實例的拷貝能保持獨立的狀態(tài)時
數(shù)據(jù)會被多個線程使用時 -
什么時候該用引用類型(class):
要用==運算符來比較實例身份的時候
你希望有創(chuàng)建一個共享的、可變對象的時候
-
什么時候該用值類型:
文章最后
以上就是本人前段時間學(xué)習(xí)心得逮京,示例代碼在Swift3.0語法下都是編譯通過的卿堂,知識點比較少,部分描述引自官方的文檔懒棉。如果文中有任何紕漏或錯誤歡迎在評論區(qū)留言指出草描,本人將在第一時間修改過來;喜歡我的文章策严,可以關(guān)注我以此促進(jìn)交流學(xué)習(xí)陶珠; 如果覺得此文戳中了你的G點請隨手點贊;轉(zhuǎn)載請注明出處享钞,謝謝支持揍诽。