在Swift中,咱們可使用類,結構和枚舉,以及選項和結果,全部這些類型對咱們編寫的代碼有不一樣的含義。編程
咱們在編寫代碼時有很大的自由,但咱們應該考慮選擇類型並利用類型系統,以便咱們的代碼可以精確地模擬咱們正在使用的域的數據。swift
放置文本的庫可能會定義各類對齊文本的方法,它可使用整數來表示,其中0
表示左對齊,1
表示右對齊,2
表示居中。可是使用整數來表示文本對齊容許沒有任何意義的值 - 例如,庫應該如何處理值27
?使用枚舉定義可能的值是有意義的,從而確保只存在有效值。api
)在Foundation中,咱們找到一個API,其中使用的類型可能會引發一些混亂。當咱們URLSession
從URL加載數據時,咱們必須提供一個帶有三個參數的完成處理程序:數組
func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask`
複製代碼
這三個參數都是可選的,所以任何或全部參數均可以存在或不存在。這意味着理論上,完成處理程序必須處理八種可能的狀態。若是咱們以不一樣的方式寫出參數,咱們能夠更清楚地看到:bash
struct CallbackInfo {
var data: Data?
var response: URLResponse?
var error: Error?
}
複製代碼
實際上,並不是全部八種可能狀態均可能發生。咱們能夠閱讀文檔以得到關於如何調用回調的提示,可是文檔並無像編譯器和類型系統那樣給咱們提供保證。session
咱們能夠考慮哪些國家本身有意義。咱們能夠假設,若是咱們得到數據,那麼咱們就不會收到錯誤。反過來講:若是咱們收到錯誤,就沒有數據。咱們能夠將這兩種狀況建模爲枚舉:app
enum CallbackInfo2 {
case success(Data, URLResponse)
case failure(Error)
}
複製代碼
但咱們不肯定這個枚舉涵蓋了全部可能的狀態。在結構的八種可能狀態中CallbackInfo
,可能會出現一些但CallbackInfo2
枚舉沒法表達的狀態。編程語言
經過閱讀數據任務方法的文檔,很難分辨哪些狀況可能發生,哪些狀況永遠不會發生。能夠說,基於枚舉的方法CallbackInfo2
能夠更好地向用戶說明須要處理哪些狀況。函數
另外一方面,咱們不能說枚舉老是比可選參數更好。若是咱們處理四個或五個選項而且可能發生全部可能的組合,那麼咱們必須定義一個包含16或32個案例的巨大枚舉。這樣作可能不會使這種API的使用變得更簡單。佈局
咱們已經能夠用本身的例子說明這個問題。假設失敗案例還附帶一個可選項Data?
:
enum CallbackInfo2 {
case success(Data, URLResponse)
case failure(Data?, Error)
}
複製代碼
鑑於這兩種狀況均可以包含數據,所以將數據做爲可選屬性提供而不是隱藏在枚舉的關聯值中會更有意義,由於這會使訪問變得更加困難。
枚舉並不老是優於一組選項,反之亦然,但它取決於咱們試圖建模的可能狀態。
第二個例子來自Apple關於Swift的書,Swift編程語言:
struct Session {
var user: User?
var expired: Bool
}
複製代碼
這裏咱們有一個用戶會話。該user
屬性是可選的,由於可能沒有註冊用戶。用戶會話的這個模型容許四種可能的狀態:用戶能夠存在與否,而且expired
布爾屬性能夠是true
或false
。
而後Swift書繼續說咱們能夠選擇將會話建模爲枚舉,從而消除沒有發生的狀態,咱們沒有用戶和過時的會話:
enum Session1 {
case loggedIn(User)
case expired(User)
case notRegistered
}
複製代碼
枚舉版本比結構更精確地模擬域,由於它只能表示用戶會話的可能狀態。
可是和前面的例子同樣,咱們如今有兩個共享相關值的狀況,咱們必須切換枚舉以便User
從會話中提取a 。第三種方法是對會話進行建模,這樣能夠更輕鬆地訪問用戶,而不會變得不那麼精確:
struct Session {
var user: User
var expired: Bool
}
var session: Session?
複製代碼
這裏咱們再次使用一個結構,但此次user屬性不是可選的。相反,會話自己存儲在可選變量中。一種可能的狀態是session
是nil
,這意味着相同 notRegistered
的狀況下Session1
枚舉。在其餘兩個狀態中,存在會話,所以也是用戶,而且會話已過時或未過時。
咱們遇到了不少這樣的狀況:當枚舉的多個case共享相同的關聯值時,咱們一般能夠將enum包裝在struct中,並將關聯的值拉出到struct的屬性中。
讓咱們看另外一個例子。假設咱們有一個文件名數組做爲字符串,咱們正在編寫一個映射數組並從文件返回數據的函數。該函數的結果類型應該是什麼?
該函數能夠簡單地返回一個數組Data
:
func readFiles(_ fileNames: [String]) -> [Data] {
// ... }
複製代碼
這樣可行,但若是其中一個文件不存在或者沒法讀取會發生什麼?該函數能夠省去該文件的數據並返回其他的數據,但做爲用戶,咱們沒法知道哪些文件失敗。
結果類型也能夠是一個選項數組:
func readFiles(_ fileNames: [String]) -> [Data?] {
// ... }
複製代碼
這樣咱們就能夠嘗試找出哪些文件成功加載了,但咱們不能徹底肯定,由於咱們沒法保證結果數組的排序方式與輸入數組相同。
咱們可能想要報告有關丟失文件的錯誤,所以函數可能應該返回文件名以及可選數據值,並結合在元組中:
func readFiles(_ fileNames: [String]) -> [(String, Data?)] {
// ... }
複製代碼
另外一個選擇是使整個數組可選。這使得結果所有或所有:咱們要麼從全部請求的文件中獲取數據,要麼使用其中一個文件失敗,咱們根本得不到任何結果:
func readFiles(_ fileNames: [String]) -> [(String, Data)]? {
// ... }
複製代碼
即便是像上面這樣的簡單功能,咱們也能夠輕鬆地想到七種變化。例如,咱們能夠決定返回一個Result
而不是一個可選項,或者咱們想要包含一個描述不一樣類型失敗的自定義枚舉。在類型之間進行選擇徹底取決於對應用程序最有意義的內容。
咱們能夠更進一步,並嘗試強制readFiles
函數的輸入和輸出數組應具備相同的長度。有一些編程語言可讓你表達這一點,但在Swift中咱們也有一些能夠提供幫助的技巧。
咱們能夠嘗試以某種方式標記一個長度的數組。而後咱們能夠定義一個保留長度並使用此映射來實現的map函數readFiles
。可是咱們會推進咱們能夠將多少信息投入到類型中,咱們應該問本身增長的複雜性是否值得。
類型不太嚴格意味着咱們須要更多地信任一段代碼的實現。咱們老是能夠編寫測試來檢查代碼在咱們提供大量樣本輸入時的行爲方式。
標準庫有不少例子,爲了簡化使用,使用的類型並非描述它們所作的最精確的類型。首先,a Array
由整數(Int
)索引,而不是由無符號整數(UInt
)索引,即便索引從不爲負數。
對於count
數組或字符串也是如此。金額永遠不能是負數,所以使用UInt
而不是更精確Int
。可是這會使count
屬性更難以使用,由於在大多數狀況下,咱們必須將類型轉換爲將其Int
傳遞給其餘API。
選擇正確的類型意味着要在精確性和易用性之間進行權衡。最好的方法是探索不一樣的類型,並肯定一種最準確,最好的類型描述咱們描述的任何類型,但不包含任何可能表明不可能狀態的垃圾值。
在咱們關於 使用Brandon Kase的幻像類型的情節中,咱們討論了標記類型的概念,以使它們更具描述性,而且更具限制性,意圖使用類型系統來幫助防止錯誤地使用咱們的API。
咱們能夠找到在自動佈局中使用的幻像類型的實際示例。在那裏,視圖的 錨點用幻像類型標記以區分水平和垂直錨點。這使得例如將前導錨固定到頂部錨點是不可能的,這是沒有意義的。
準確建模特定域數據的問題自動出如今強類型語言的社區中。其中一個主要的榆樹開發商理查德·費爾德曼,舉行一提及作不可能的狀態是不可能的。關於F#,OCaml和Haskell也有相似的討論,全部討論如何找到數據表示,只容許你定義有意義的函數。