類型?什么是類型?
這是一個基礎(chǔ)的問題寒随,類型 (Types) 在 Swift 中是非常重要的概念卖丸,在 Swift 中類型是用來描述和定義一組數(shù)據(jù)的有效值纺且,以及指導它們?nèi)绾芜M行操作的一個藍圖。這個概念和其他編程語言中“類”的概念很相似稍浆。Swift 的類型分為命名類型和復合類型兩種载碌;命名類型比較簡單猜嘱,就是我們?nèi)粘S玫?類 (class)
,結(jié)構(gòu)體 (struct)
嫁艇,枚舉 (enum)
以及接口 (protocol)
朗伶。在 Swift 中,這四種命名類型為我們定義了所有的基本結(jié)構(gòu)步咪,它們都可以有自己的成員變量和方法论皆,這和其他一般的語言是不太一樣的(比如很少有語言的enum可以有方法,protocol可以有變量)猾漫。另外一種類型是復合類型点晴,包括函數(shù) (func)
和 多元組 (tuple)
。它們在使用的時候不會被命名悯周,而是由 Swift 內(nèi)部自己定義觉鼻。
我們在實際做開發(fā)時,一般會接觸很多的命名類型队橙。在 Swift 的世界中坠陈,一切看得到的東西,都一定屬于某一種類型捐康。在 PlayGround 或者是項目中仇矾,通過在某個實際的被命名的類型上 Cmd + 單擊
,我們就能看到它的定義解总。比如在 Swift 世界中的所有基本型 Int
贮匕,String
,Array
,Dictionay
等等型酥,其實它們都是結(jié)構(gòu)體椎例。而這些基本類型通過定義本身,以及眾多的 extension
敦锌,實現(xiàn)了很多接口,共同提供了基本功能佳簸。這也正是 Swift 的類型的一種很常見的組織方式乙墙。
而相對的,Cocoa 框架中的類生均,基本都被映射為了 Swift 的 class
听想。如果你有比較深厚的 objc 功底的話,應該會聽說過 objc 的類其實是一組包含了元數(shù)據(jù) (metadata) 的結(jié)構(gòu)體马胧,而在 objc 中我們可以使用 +class
來拿到某個 Class 的 isa汉买,從而確定類的組成和描述。而在 Swift 的 native 層面上佩脊,在 type safe 的基礎(chǔ)上蛙粘,不再需要 isa 來指導對象如何構(gòu)建朽色,而這個過程會通過確定的命名類型完成。正因為這個原因组题,Swift 中干脆把 NSObject 的 class
方法都拿掉葫男,因為 Swift 和 ObjC 在這個根本問題上的分歧,最終導致了在使用 Swift 調(diào)用 Cocoa 框架時的各種麻煩和問題崔列。
參照和值梢褐,Array和Dictionary背后的一些故事
2014 年 7 月 13 日更新 由于 beta 3 中
Array
被完全重寫,這一節(jié)關(guān)于Array
的一些行為和表述完全過時了赵讯。 關(guān)于Array
的用法現(xiàn)在簡化了很多盈咳,請參見新加的 “真 參照和值,Array和Dictionary背后的一些故事”
如果你堅持看到了這里边翼,那么恭喜你…本文最無趣和枯燥的部分已經(jīng)結(jié)束了(同時也應該嚇走了不少抱著玩玩看的心態(tài)來看待 Swift 的讀者吧..笑)鱼响,那么開始說一些細節(jié)的東西吧。
首先要明白的概念是组底,參照和值丈积。在 C 系語言里摸爬滾打過的同學都知道,我們在調(diào)用一個函數(shù)的時候债鸡,往里傳的參數(shù)有兩種可能江滨。一種是傳遞類似一個數(shù)字或者結(jié)構(gòu)體這樣的基本元素,這時候這個整數(shù)的值會被在內(nèi)存中復制一份然后傳到函數(shù)內(nèi)部厌均;另一種情況是傳遞一個對象唬滑,為了性能和內(nèi)存上的考慮,這時候一般不會去將對象的內(nèi)容復制一遍棺弊,而是會傳遞的一個指向同一塊內(nèi)存的指針晶密。
在 Swift 中一個與其他語言都不太一樣的地方是,它的 Collection 類型模她,也就是 Array
和 Dictionary
稻艰,并不是 class
類型,而是 struct
結(jié)構(gòu)體缝驳。那么按照我們以往的經(jīng)驗连锯,在傳值或者賦值的時候應該是會復制一份归苍。我們來試試看是不是這樣的~
var dic = [0:0, 1:0, 2:0]
var newDic = dic
//Check dic and newDic
dic[0] = 1
dic //[0: 1, 1: 0, 2: 0]
newDic //[0: 0, 1: 0, 2: 0]
var arr = [0,0,0]
var newArr = arr
arr[0] = 1
//Check arr and newArr
arr //[1, 0, 0]
newArr //[1, 0, 0]
Dictionary
的值沒有問題用狱,我們改變了 dic
中的值,但是 newDic
保持了原來的值拼弃,說明 newDic
確實被復制了一份夏伊。而當我們檢查到 Array
的時候,發(fā)生了一點神奇的事情吻氧。雖然 Array
是 struct
溺忧,但是當我們改變 arr
時咏连,新的 newArr
也發(fā)生了改變,也就是說鲁森,arr
和 newArr
其實是同一個參照祟滴。這里的原因其實在 Apple 的官方文檔中有一些說明。Swift 考慮到實際使用的情景歌溉,對 Array
做了特殊的處理垄懂。除非需要(比如 Array
的大小發(fā)生改變,或者顯式地要求進行復制)痛垛,否則 Array
在傳遞的時候會使用參照草慧。
在這里如果你想要只改變 arr
的值,而保持新賦予的 newArr
不變的話匙头,你需要顯式地對 arr
進行 copy()
漫谷,像下面這樣。
var arr = [0,0,0]
var copiedArr = arr.copy()
arr[0] = 1
arr //[1, 0, 0]
copiedArr //[0, 0, 0]
這時候 arr
和 copiedArr
將指向不同的內(nèi)存地址蹂析,對原來的數(shù)組重新賦值的時候舔示,就不會再影響新的數(shù)組了。另一種等效的做法是通過 Array
的初始化方法建立一個新的 Array
:
var arr = [0,0,0]
var newArr = Array(arr)
arr[0] = 1
arr //[1, 0, 0]
newArr //[0, 0, 0]
值得一提的是电抚,對于 Array
這個 struct
的這種特殊行為斩郎,Apple 還準備了另一個函數(shù) unshare()
給我們使用。unshare()
的作用是如果對象數(shù)組不是唯一參照喻频,則復制一份缩宜,并將作用的參照指向新的地址(這樣它就變成唯一參照,不會意外改變原來的別的同樣的參照了)甥温;而如果這個參照已經(jīng)是唯一參照了的話锻煌,就什么都不做。
var arr = [0,0,0]
var newArr = arr
//Breakpoint 1
arr.unshare()
//Breakpoint 2
arr[0] = 1
arr //[1, 0, 0]
newArr //[0, 0, 0]
這個設計的意圖是為了更安全地使用這個優(yōu)化過的行為奇怪的數(shù)組結(jié)構(gòu)體姻蚓。關(guān)于 unshare()
的行為宋梧,我們也可以通過使用 LLDB 斷點來觀察內(nèi)存地址的變化。參見下圖:
另外一個要加以注意的是狰挡,Array
在 copy 時執(zhí)行的不是深拷貝捂龄,所以 Array
中的參照類型在拷貝之后仍然會是參照。Array 中嵌套 Array 的情況亦是如此:對一個 Array 進行的 copy 只會將被拷貝的 Array
指向新的地址加叁,而保持其中所有其他 Array
的引用倦沧。當然你可以為 Array
(或者準確說是 Array<arraytype>)寫一個遞歸的深拷貝擴展,但這是另外一個故事了它匕。</arraytype>
真 參照和值展融,Array和Dictionary背后的一些故事
2014 年 7 月 13 日更新
Apple 在 beta 3 里重寫了 Array
,它的行為簡化了許多豫柬。首先 copy
和 unshare
兩個方法被刪掉了告希,而類似的行為現(xiàn)在以更合理的方式在幕后幫我們完成了扑浸。還是舉上面的那個例子:
var dic = [0:0, 1:0, 2:0]
var newDic = dic
//Check dic and newDic
dic[0] = 1
dic //[0: 1, 1: 0, 2: 0]
newDic //[0: 0, 1: 0, 2: 0]
var arr = [0,0,0]
var newArr = arr
arr[0] = 1
//Check arr and newArr
arr //[1, 0, 0]
newArr //before beta3:[1, 0, 0], after beta3:[0, 0, 0]
Dictionary
當然還是 OK,但是對于 Array
中元素的改變燕偶,在 beta 3 中發(fā)生了變化『仍耄現(xiàn)在不再存在作為一個值類型但是卻在賦值和改變時表現(xiàn)為參照類型的 Array
的特例,而是徹頭徹尾表現(xiàn)出了值類型的特點指么。這個改變避免了原來需要小心翼翼地對 Array
進行 copy
或者 unshare
這樣的操作仙逻,而 Apple 也承諾在性能上沒有問題。文檔中提到其實現(xiàn)在的行為和之前是一貫的涧尿,只不過對于數(shù)組的復制工作現(xiàn)在是在背后由 Apple 只在必要的時候才去做系奉。所以可以猜測其實在背后 Array
和 Dictionary
的行為并不是像其他 struct 那樣簡單的在棧上分配,而是類似參照那樣姑廉,通過棧上指向堆上位置的指針來實現(xiàn)的缺亮。而對于它的復制操作,也是在相對空間較為寬裕的堆上來完成的桥言。當然萌踱,現(xiàn)在還無法(或者說很難)拿到最后的匯編碼,所以這只是一個猜測而已号阿。最后如果能夠證實對錯的話并鸵,我會再進行更新。
總之扔涧,beta 3 之后园担,原來飄忽不定難以捉摸(其實真正理解之后還是很穩(wěn)定的,也很適合出筆試題)的 Array
現(xiàn)在徹底簡單化了枯夜⊥涮基本只需要記住它的行為在表面上和其他的值類型完全無異,而性能方面的考量可以交給 Apple 來做湖雹。
Array vs Slice
因為 Array
類型實在太重要了咏闪,因此不得不再多說兩句。查看 Array
在 Swift 中的定義摔吏,我們可以發(fā)現(xiàn)其實 Array
實現(xiàn)了兩個很重要的接口 MutableCollection
和 Sliceable
鸽嫂。第一個接口比較簡單,為 Array
實現(xiàn)了下標等特性征讲,通過 Collection
通用的一些概念据某,可以從數(shù)據(jù)結(jié)構(gòu)中獲取元素,比較簡單稳诚。而第二個接口 Sliceable
實現(xiàn)了通過 Range 來取出部分數(shù)組哗脖,這里稍微有點特殊。
Swift 引入了在其他很多語言中很流行的用 ..
和 ...
(beta3 中 ..
被改成了 ..<
扳还,雖說是為了更明確的意義才避,但是看起來會比較奇怪)來表示 Range 的概念。從一個數(shù)組里面取出一個子數(shù)組其實是蠻普遍的一個需求氨距,但是如果你足夠細心的話桑逝,可能會發(fā)現(xiàn)我們無法寫這樣的代碼:
var arr = [0,0,0]
var partOfArr: Array = arr[0...1]
//Could not find an overload for 'subscript' that accepts the supplied arguments
你會得到一個編譯錯誤,告訴你沒有重載下標俏让。在我們?nèi)サ粑覀儚娭萍由系?: Array
類型設置之后楞遏,編譯能通過了。這就告訴我們首昔,我們使用 Rang 從 Array 中取出來的東西寡喝,并不是 Array
類型。那它到底是個什么東西勒奇?使用 REPL 可以很容易看到预鬓,在使用 Range 從 Array 里取出來的其實是一個 Slice
,而不是一個 Array
赊颠。
1> var arr = [0,0,0]
arr: Int[] = size=3 {
[0] = 0
[1] = 0
[2] = 0
}
2> var slice = arr[0...1]
slice: Slice<Int> = size=2 {
[0] = 0
[1] = 0
}
So, what is a slice格二?查看 Slice
的定義,可以看到它幾乎和 Array
一模一樣竣蹦,實現(xiàn)了同樣的接口顶猜,擁有同樣的成員,那么為什么不直接干脆給個爽快痘括,而要新弄一個 Slice
呢长窄?Apple gets crazy?當然不是..Slice的存在當然有其自己的價值和含義纲菌,而這和我們剛才提到的值和引用有一些關(guān)系抄淑。
So, why is a slice?讓我們先嘗試 play with it驰后。接著上面的情況肆资,運行下面的代碼試試看:
var arr : Array = [0,0,0]
var slice = arr[0...1]
arr[0] = 1
arr //[1, 0, 0]
slice //[1, 0]
slice[1] = 2
arr //[1, 2, 0]
slice //[1, 2]
我想你已經(jīng)明白一些什么了吧?這里的 slice
和 arr
當然不可能是同一個引用(它們的類型都不一樣)灶芝,但是很有趣的是郑原,通過 Range 拿到的 Slice
中的元素,是指向原來的 Array
的夜涕。這個特性就非常有趣了犯犁,我們可以對感興趣的數(shù)組片段進行觀察或者操作,并且它們的值和原來的數(shù)組是對應的同步的女器。
理所當然的酸役,在對應著的 Array
或者 Slice
其中任意一個的內(nèi)存指向發(fā)生變化時(比如添加或移除了元素,重新賦值等等),這種關(guān)系就會被打破涣澡。
對于 Slice
和 Array
贱呐,其實是可以比較簡單地轉(zhuǎn)換的。因為 Collection
接口是實現(xiàn)了 +
重載的入桂,于是我們可以簡單地通過相加來生成一個 Array
(如果我們愿意的話)奄薇。不過,要是真的有需要的話抗愁,使用 Array
的初始化方法會是比較好的選擇:
var arr : Array = [0,0,0]
var slice = arr[0...1]
var result1 : Array = [] + slice
var result2 : Array = Array(slice)
使用 Range 下標的方式馁蒂,不僅可以取到這個 Range 內(nèi)的 Slice
,還可以對原來的數(shù)組進行批量”賦值”:
var arr : Array = [0,0,0]
arr[0...1] = [1,1]
arr //[1, 1, 0]
細心的同學可能注意到了蜘腌,這里我把“賦值”打上了雙引號沫屡。實際上這里做的是替換,數(shù)組的內(nèi)存已經(jīng)發(fā)生了變化撮珠。因為 Swift 沒有強制要求替換的時候 Range 的范圍要和用來替換的 Collection
的元素個數(shù)一致沮脖,所以其實這里一定會涉及內(nèi)存的分配和新的數(shù)組生成。我們可以看看下面的例子:
var arr : Array = [0,0,0]
var otherArr = arr
arr[0...1] = [1,1]
arr //[1, 1, 0]
otherArr //[0, 0, 0]
arr[0..1] = [1,1]
arr //[1, 1, 1, 0]
給一個數(shù)組進行 Range 賦值劫瞳,背后其實調(diào)用了數(shù)組的 replaceRange
方法倘潜,將取到的 Slice
,替換成了賦給它的 Array
或者 Slice
志于。而只要 Range 有效涮因,我們就可以很靈活地寫出類似這樣的所謂的插入方法:
var arr : Array = [0,0,0]
arr[1..1] = [1, 1]
arr //[0, 1, 1, 0, 0]
這里的 1..1
是一個起點為 1,長度為 0 的Range伺绽,于是它取到的是原來 [0, 0, 0]
中 index 為 1 的位置的一個空 Slice
养泡,將其替換為 [1, 1]
。清楚明白奈应。
既然都提到了這么多次 Range
澜掩,還是需要說明一下這個 Swift 里很重要的概念(其實在 objc 里 NSRange
也很重要,只不過沒有像 Swift 里這么普遍)杖挣。Range
結(jié)構(gòu)體中有兩個非常重要的值肩榕,startIndex
和 endIndex
,它表示了這個 Range 的范圍惩妇。而這個值永遠是右開的株汉,也就是說,它們會和 x..y
這樣的表示中 x
和 y
分別相等歌殃。對于 x < y
的情況下的 Range乔妈,是存在數(shù)學上的表達意義的,比如 2..1
這樣的 Range 表示從 2 開始往前數(shù) 1氓皱。但是在實際從 Array
或者 Slice
中取值時這種表達是沒有意義路召,并且會拋出一個運行時的 EXC_BAD_INSTRUCTION 的勃刨,在使用的時候還要加以注意。
顏文字很好股淡,但是…
有了上面的一些基礎(chǔ)身隐,我們可以來談談 String
了。當說到我們可以在原生的 String
中使用 UniCode 字符時揣非,全場一片歡呼抡医。沒錯躲因,以后我們可以把代碼寫成這樣了早敬!
let π = 3.14159
let 你好 = "你好世界"
let ???? = "????”
Cool…雖然我不認為有多少人會去把變量名弄成中文或者貓貓狗狗,但是畢竟字符串本身還是需要支持中文日文阿拉伯文甚至 emoji 的對吧大脉。
另外一個很贊的是搞监,Apple 把所有 NSString
的方法都“移植”到了 String
類型上,而且將 Cocoa 框架中所有涉及 NSString
的地方都換成了 String
镰矿。這是一件很棒的事情琐驴,這意味著我們可以無縫在 Swift 上像原來寫 objc 時候那樣使用 String
,而不必擔心 String
和 NSString
之間類型轉(zhuǎn)換等麻煩的問題秤标【可能看過 session 或者細讀了文檔的同學會發(fā)現(xiàn),新的 String
里苍姜,沒有了原來的 -length
方法牢酵,取而代之,Apple 推薦我們使用 countElements
來獲取字符串的長度衙猪。這是很 make sense 的一件事情馍乙,因為我們無法確定字符串中每個字符的字節(jié)長度,所以 Apple 為了幫助我們方便計算字符數(shù)垫释,給了這個 O(N) 的方法丝格。
這樣的字符串帶來了一個挺不方便的結(jié)果,那就是我們無法直接通過 Int
的下標來訪問 String
中的字符棵譬。我們查看 Swift 中 String
的定義显蝌,可以看到它其實是實現(xiàn)了 subscript (i: String.Index) -> Character { get }
的(其 Range 訪問也相應需要一個 Range<String.Index>
版本的泛型)。如果我們能知道字符對應的 String.Index
订咸,我們就可以寫出方便的下標訪問了曼尊。舉個例子,如果有下面這樣的兩個 String
算谈。
var str = "1234"
var imageStr = "????????"
我們現(xiàn)在想要通過拿到上面那個 ASCII 字符串的某個數(shù)字所在的 String.Index
涩禀,來獲取下面對應位置的圖標,比如 2 對應貓然眼,應該如何做呢艾船?一開始大概很容易想到這樣的代碼:
var range = str.rangeOfString("2") //記得導入 Cocoa 或者 UIKit
imageStr[range] //EXC_BAD_INSTRUCTION
很不幸,EXC_BAD_INSTRUCTION,這表示 Swift 中有一個 Assertion 阻止了我們繼續(xù)屿岂。其實 String.Index
和一般的 Int
之類的 index 不太一樣践宴,因為每一個 Index 代表的字節(jié)的長度是有差別的,所以它只能實現(xiàn) BidirectionalIndex
爷怀,而不能像其他的等長結(jié)構(gòu)那樣實現(xiàn) RandomAccessIndex
接口(關(guān)于這兩個接口分別是什么已經(jīng)做了什么阻肩,留給大家自己研究下吧)。于是运授,在不同字符串之間的 Index 進行轉(zhuǎn)換時烤惊,我們大概不得不使用一種笨辦法,那就是計算步長和差值吁朦。對于我們的例子柒室,我們會先算出在 str
中 2 與初始 Index 的距離,然后講這個距離在 imageStr
中以 imageStr
的 String.Index 進行套用逗宜,算出適合其的第二個字符的 Range雄右,然后進行 Range 的下標訪問,如下:
var range = str.rangeOfString("2")
var aDistance: Int = distance(str.startIndex, range.startIndex)
var imageStrStartIndex = advance(imageStr.startIndex, aDistance)
var range2 = imageStrStartIndex..imageStrStartIndex.successor()
var substring: String = imageStr[range2]
[圖片上傳中...(image-91b692-1524120743880-2)]
大部分時候其實我們不會這么來映射字符串纺讲,不過對于 Swift 字符串的實現(xiàn)和與 NSString 的差異擂仍,還是值得研究一番的。
幽靈一般的 Optional
Swift 引入的最不一樣的可能就是 Optional Value 了熬甚。在聲明時逢渔,我們可以通過在類型后面加一個 ?
來將變量聲明為 Optional 的。如果不是 Optional 的變量则涯,那么它就必須有值复局。而如果沒有值的話,我們使用 Optional 并且將它設置為 nil
來表示沒有值粟判。
//num 不是一個 Int
var num: Int?
//num 沒有值
num = nil //nil
//num 有值
num = 3 //{Some 3}
Apple 在 Session 上告訴我們亿昏,Optinal Value 其實就是一個盒子,你盒子里可能裝著實際的值档礁,也可能什么都沒裝角钩。
我們看到 Session 里或者文檔里天天說 Optional Optional,但是我們在代碼里基本一個 Optional 都沒有看到呻澜,這是為什么呢递礼?而且,上面代碼中給 num
賦值為 3 的時候的那個輸出為什么看起來有點奇怪羹幸?其實脊髓,在聲明類型時的這個 ?
僅僅只是 Apple 為了簡化寫法而提供的一個語法糖。實際上我們是有 Optional 類型的聲明栅受,就這里的 num
為例将硝,最正規(guī)的寫法應該是這樣的:
//真 Optional 聲明和使用
var num: Optional<Int>
num = Optional<Int>()
num = Optional<Int>(3)
沒錯恭朗,num
不是 Int
類型,它是一個 Optional
類型依疼。到底什么是 Optional
呢痰腮,點進去看看:
enum Optional<T> : LogicValue, Reflectable {
case None
case Some(T)
init()
init(_ some: T)
/// Allow use in a Boolean context.
func getLogicValue() -> Bool
/// Haskell's fmap, which was mis-named
func map<U>(f: (T) -> U) -> U?
func getMirror() -> Mirror
}
你也許會大吃一驚。我們每天和 Swift 打交道用的 Optional 居然是一個泛型枚舉 enum
律罢,而其實我們在使用這個枚舉時膀值,如果沒有值,我們就規(guī)定這個枚舉的是 .None
误辑,如果有沧踏,那么它就是 Some(value)
(帶值枚舉這里不展開了,有不明白的話請看文檔吧)稀余。而這個枚舉又恰好實現(xiàn)了 LogicValue
接口悦冀,這也就是為什么我們能使用 if
來對一個 Optinal 的值進行判斷并進一步進行 unwrap 的依據(jù)趋翻。
var num: Optional<Int> = 3
if num { //因為有 LogicValue睛琳,
//.None 時 getLogicValue() 返回 false
//.Some 時返回 true
var realInt = num!
realInt //3
}
既然 var num: Int? = nil
其實給 num
賦的值是一個枚舉的話,那這個 nil
到底又是什么踏烙?它被賦值到哪里去了师骗?一直注意的是,Swift 里的 nil 和 objc 里的 nil 完全不是一回事兒讨惩。objc 的 nil 是一個實實在在的指針辟癌,它指向一個空的對象。而這里的 nil 雖然代表空荐捻,但它只是一個語意上的概念黍少,確是有實際的類型的,看看 Swift 的 nil
到底是什么吧:
/// A null sentinel value.
var nil: NilType { get }
nil
其實只是 NilType
的一個變量处面,而且這個變量是一個 getter厂置。Swift 給了我們一個文檔注釋,告訴我們 nil
其實只是一個 null 的標記值魂角。實際上我們在聲明或者賦值一個 Optional 的變量時昵济,?
語法糖做的事情就是聲明一個 Optional<T>
,然后查看等號右邊是不是 nil 這個標記值野揪。如果不是访忿,則使用 init(_ some: T)
用等號右邊的類型 T 的值生成一個 .Some
枚舉并賦值給這個 Optional 變量;如果是 nil斯稳,將其賦為 None 枚舉海铆。
所以說,Optional背后的故事挣惰,其實被這個小小的 ?
隱藏了卧斟。
我想系草,Optional 討論到這里就差不多了,還有三個小問題需要說明唆涝。
首先找都,NilType
這個類型非常特殊,它似乎是個 built in 的類型廊酣,我現(xiàn)在沒有拿到關(guān)于它的任何資料能耻。我本身逆向是個小白,現(xiàn)在看起來 Swift 的逆向難度也比較大亡驰,所以關(guān)于 NilType
的一些行為還是只能猜測晓猛。而關(guān)于 nil
這一 NilType
的類型的變量來說,猜測的話凡辱,它可能是 Optional.None
的一種類似多型表現(xiàn)戒职,因為首先它確實是指向 0x0 的,并且與 Optional.None 的 content 的內(nèi)容指向一致透乾。但是具體細節(jié)還要等待挖掘或者公布了洪燥。
2014 年 7 月 13 日更新 從 beta3 開始
nil
是一個編譯關(guān)鍵字了,NilType
則被從 Swift 中移除了乳乌。這個改變解決了上面提到的很多懸而未決的問題捧韵,比如對 nil 的多次封裝以及如何實現(xiàn)自己的可 nil 的類等等。現(xiàn)在添加了一個叫做NilLiteralConvertible
的接口來使某個類可以使用 nil 語法汉操,而避免了原來的讓人費解的隱式轉(zhuǎn)換再来。但是現(xiàn)在還有一個問題,那就是 Optional 是實現(xiàn)了LogicValue
接口的磷瘤,這就是得像BOOL?
這樣的類型在使用的時候會一不小心就很危險芒篷。
其次,Apple 推薦我們在 unwrap 的時候使用一種所謂的隱式方法采缚,即下面這種方式來 unwrap:
var num: Int? = 3
if let n = num {
//have a num
} else {
//no num
}
最后针炉,這樣隱式調(diào)用足夠安全,性能上似乎應該也做優(yōu)化(有點忘了..似乎說過)仰担,推薦在 unwrap 的時候盡可能寫這樣的推斷糊识,而減少直接進行 unwrap 這種行為。
最后一個問題是 Optional 的變量也可以是 Optinal摔蓝。因為 Optional 就相當于一個黑盒子赂苗,可以知道盒子里有沒有東西 (通過 LogicValue)暇昂,也可以打開這個盒子 (unwrap) 來拿到里面的東西 (你要的類型的變量或者代表沒有東西的 nil)撇眯。請注意真慢,這里沒有任何規(guī)則限制一個 Optional 的量不能再次被 Optional僧须,比如下面這種情況是完全 OK 的:
var str: String? = "Hi" //.Some("Hi")
var anotherStr: String?? = str //.Some(.Some("Hi"))
這其實是沒有多少疑問的烁挟,很完美的兩層 Optional雕薪,使用的時候也一層層解開就好们妥。但是如果是 nil 的話世落,在這里就有點尷尬…
var str: String? = nil
var anotherStr: String?? = nil
因為我們在 LLDB 里輸出的時候,得到了兩個 nil
[圖片上傳中...(image-42c2ce-1524120743878-1)]
如果說 str
其實是 Optional<String>.None
昌犹,輸出是 nil 的話還可以理解坚芜,但是我們知道 (好吧,如果你認真讀了上面的 Optional 的內(nèi)容的話會知道)斜姥,anotherStr
其實是 Optional<Optional<String>>.Some(Optional<String>.None)
鸿竖,這是其實一個有效的非空 Optional
,至少第一層是铸敏。而如果放在 PlayGround 里缚忧,anotherStr
得到的輸出又是正確的 {nil}
。What hanppened? Another Apple bug?
答案是 No杈笔,這里不是 bug闪水。為了方便觀察,LLDB 會在輸出的時候直接幫我們盡可能地做隱式的 unwrap蒙具,這也就導致了我們在 LLDB 中輸出的值只剩了一個裸的 nil球榆。如果想要看到 Optional 本身的值,可以在 Xcode 的 variable 觀察窗口點右鍵店量,選中 Show Raw values
芜果,這樣就能顯示出 None 和 Some 了∪谑Γ或者我們可以直接使用 LLDB 的 fr v -R
命令來打印整個 raw 的值:
可以清楚看到,anotherStr
是 .Some
包了一個 .None
蚁吝。
(這里有個自動 unwrap 的小疑問旱爆,就是寫類似 var anotherStr: String? = str
這樣的代碼也能通過,應該是 ?
語法在這里有個隱式解包窘茁,需要進一步確認)
? 那是什么??怀伦,! 原來如此!!
問號和嘆號現(xiàn)在的用法都是原來 objc 中沒有的概念。說起來簡單也簡單山林,但是背后也還是不少玄機房待。原來就已經(jīng)存在的用法就不說了,這里把新用法從淺入深逐個總結(jié)一下吧驼抹。
首先是 ?
:
-
?
放在類型后面作為 Optional 類型的標記
這個用法上面已經(jīng)說過桑孩,其實就是一個 Optional<T>
的語法糖,自動將等號后面的內(nèi)容 wrap 成 Optional框冀。給個用例流椒,不再多說:
var num: Int? = nil //聲明一個 Int 的 Optional,并將其設為啥都沒有
var str: String? = "Hello" //聲明一個 String 的 Optional明也,并給它一個字符串
-
?
放在某個 Optional 變量后面宣虾,表示對這個變量進行判斷惯裕,并且隱式地 unwrap。比如說:
foo?.somemethod()
相比起一般的先判斷再調(diào)用绣硝,類似這樣的判斷的好處是一旦判斷為 nil
或者說是 false
蜻势,語句便不再繼續(xù)執(zhí)行,而是直接返回一個 nil鹉胖。上面的寫法等價于
if let maybeFoo = foo {
maybeFoo.somemethod()
}
這種寫法更存在價值的地方在于可以鏈式調(diào)用咙边,也就是所謂的 Optional Chaining,這樣可以避免一大堆的條件分支次员,而使代碼變得易讀簡潔败许。比如:
if let upper = john.residence?.address?.buildingIdentifier()?.uppercaseString {
println("John's uppercase building identifier is \(upper).")
}
注意最后 buildingIdentifier
后面的問號是在 ()
之后的,這代表了這個 Optional 的判斷對象是 buildingIdentifier()
的返回值淑蔚。
-
?
放在某個 optional 的 protocol 方法的括號前面市殷,以表示詢問是否可以對該方法調(diào)用
這中用法相當于以前 objc 中的 -respondsToSelector:
的判斷,如果對象響應這個方法的話刹衫,則進行調(diào)用醋寝。例子:
delegate?.questionViewControllerDidGetResult?(self, result)
中的第二個問號。注意和上面在 ()
后的問號不一樣带迟,這里是在 ()
之前的音羞,表示對方法的詢問。
其實在 Swift 中仓犬,默認的 potocol 類型是沒有 optional 的方法的嗅绰,因為基于這個前提,可以對類型安全進行確保搀继。但是 Cocoa 框架中的 protocol 還是有很多 optional 的方法窘面,對于這些可選的接口方法,或者你想要聲明一個帶有可選方法的接口時叽躯,必須要在聲明 protocol
時再其前面加上 @objc
關(guān)鍵字财边,并在可選方法前面加上 @optional
:
@objc protocol CounterDataSource {
@optional func optionalMethod() -> Int
func requiredMethod() -> Int
@optional var optionalGetter: Int { get }
}
然后是 !
新用法的總結(jié)
-
!
放在 Optional 變量的后面,表示強制的 unwrap 轉(zhuǎn)換:
foo!.somemethod()
這將會使一個 Optional<T>
的量被轉(zhuǎn)換為 T
点骑。但是需要特別注意酣难,如果這個 Optional 的量是 nil 的話,這種轉(zhuǎn)換會在運行時讓程序崩潰黑滴。所以在直接寫 !
轉(zhuǎn)換的時候一定要非常注意憨募,只有在有必死決心和十足把握時才做 !
強轉(zhuǎn)。如果待轉(zhuǎn)換量有可能是 nil 的話跷跪,我們最好使用 if let
的語法來做一個判斷和隱式轉(zhuǎn)換馋嗜,保證安全。
-
!
放在類型后面吵瞻,表示強制的隱式轉(zhuǎn)換葛菇。
這種情況下和 ?
放在類型后面的行為比較類似甘磨,都是一個類型聲明的語法糖。?
聲明的是 Optional
眯停,而 !
其實聲明的是一個 ImplicitlyUnwrappedOptional
類型济舆。首先需要明確的是,這個類型是一個 struct
莺债,其中關(guān)鍵部分是一個 Optional<T>
的 value滋觉,和一組從這個 value 里取值的 getter 和 方法:
struct ImplicitlyUnwrappedOptional<T> : LogicValue, Reflectable {
var value: T?
//...
static var None: T! { get }
static func Some(value: T) -> T!
//...
}
從外界來看,其實這和 Optional
的變量是類似的齐邦,有 Some
有 None
椎侠。其實從本質(zhì)上來說,ImplicitlyUnwrappedOptional
就是一個存儲了 Optional
措拇,實現(xiàn)了 Optional
對外的方法特性的一個類型我纪,唯一不同的是,Optional
需要我們手動進行進行 unwrap (不管是使用 var!
還是 let if
賦值丐吓,總要我們做點什么)浅悉,而 ImplicitlyUnwrappedOptional
則會在使用的時候自動地去 unwrap,并對繼續(xù)之后的操作調(diào)用券犁,而不必去增加一次手動的顯示/隱式操作术健。
為什么要這么設計呢?主要是基于 objc 的 Cocoa 框架的兩點考慮和妥協(xié)粘衬。
首先是 objc 中是有指向空對象的指針的荞估,就是我們所習慣的 nil
。在 Swift 中色难,為了處理和 objc 的 nil 的兼容泼舱,我們需要一個可為空的量。而因為 Swift 的目的就是打造一個完全類型安全的語言枷莉,因此不僅對于 class,對于其他的類型結(jié)構(gòu)我們也需要類型安全尺迂。于是很自然地笤妙,我們可以使用 Optional 的空來對 objc 做等效。因為 Cocoa 框架有大量的 API 都會返回 nil噪裕,因此我們在用 Swift 表達它們的時候蹲盘,也需要換成對應的既可以表示存在,也可以表示不存在的 Optional
膳音。
那這樣的話召衔,不是直接用 Optional
就好了么?為什么要弄出一個 ImplicitlyUnwrappedOptional
呢祭陷?因為易用性苍凛。如果全部用 Optional
包裝的話趣席,在調(diào)用很多 API 時我們就都需要轉(zhuǎn)來轉(zhuǎn)去,十分麻煩醇蝴。而對于 ImplicitlyUnwrappedOptional
因為編譯器為我們進行了很多處理宣肚,使得我們在確信返回值或者要傳遞的值不是空的時候,可以很方便的不需要做任何轉(zhuǎn)換悠栓,直接使用霉涨。但是對于那些 Cocoa 有可能返回 nil,我們本來就需要檢查的方法惭适,我們還是應該寫 if 來進行轉(zhuǎn)換和檢查笙瑟。
比如說,以下的寫法就會在運行時導致一個 EXC_BAD_INSTRUCTION
let formatter = NSDateFormatter()
let now = formatter.dateFromString("not_valid")
let soon = now.dateByAddingTimeInterval(5.0) // EXC_BAD_INSTRUCTION
因為 dateFromString
返回的是一個 NSDate!
癞志,而我們的輸入在原來會導致一個 nil
的返回往枷,這里我們在使用 now 之前需要進行檢查:
let formatter = NSDateFormatter()
let now = formatter.dateFromString("not_valid")
if let realNow = now {
realNow.dateByAddingTimeInterval(5.0)
} else {
println("Bad Date")
}
這和以前在 objc 時代做的事情差不多,或者今阳,用更 Swift 的方式做
let formatter = NSDateFormatter()
let now = formatter.dateFromString("not_valid")
let soon = now?.dateByAddingTimeInterval(5.0)
如何寫出正確的 Swift 代碼
現(xiàn)在距離 Swift 發(fā)布已經(jīng)接近小一周了师溅。很多開發(fā)者已經(jīng)開始嘗試用 Swift 寫項目。但是不管是作為練習還是作為真正的工程盾舌,現(xiàn)在看來大家在寫 Swift 時還是帶了濃重的 objc 的影子墓臭。就如何寫出帶有 Swift 范兒的代碼,在這里給出一點不成熟的小建議妖谴。
- 理解 Swift 的類型組織結(jié)構(gòu)窿锉。Swift 的基礎(chǔ)組織非常漂亮,主要的基礎(chǔ)類型大部分使用了
struct
來完成膝舅,然后在之上定義并且實現(xiàn)了各種接口嗡载,這樣的設計模式其實是值得學習和借鑒的。當然仍稀,在實際操作中可能會有很大難度洼滚,因為接口比之前靈活許多,可以繼承技潘,可以放變量等等遥巴,因此在定義接口時如何保持接口的單一性和擴展性是一個不小的考驗。 - 善用泛型享幽。很多時候 Swift 的 Generic 并