詳解Swift?中的幻象類型
前言
模糊的數(shù)據(jù)可以說是一般應(yīng)用程序中最常見的錯誤和問題的來源之一。雖然 Swift 通過其強大的類型系統(tǒng)和完善的編譯器幫助我們避免了許多含糊不清的來源——但只要我們無法在編譯時保證某個數(shù)據(jù)總是符合我們的要求,就總是有風險,我們最終會處于含糊不清或不可預(yù)測的狀態(tài)。
本周,讓我們來看看一種技術(shù),它可以讓我們利用 Swift 的類型系統(tǒng)在編譯時執(zhí)行更多種類的數(shù)據(jù)驗證——消除更多潛在的歧義來源,并幫助我們在整個代碼庫中保持類型安全——通過使用幻象類型(phantom types)。
定義良好,但仍然含糊不清
舉個例子,假設(shè)我們正在開發(fā)一個文本編輯器,雖然它最初只支持純文本文件——隨著時間的推移,我們還增加了對編輯HTML文檔的支持,以及PDF預(yù)覽。
為了能夠盡可能多地重復使用我們原來的文檔處理代碼,我們繼續(xù)使用與開始時相同的Document
模型——只是現(xiàn)在它獲得了一個Format
屬性,告訴我們正在處理什么樣的文檔:
struct Document { enum Format { case text case html case pdf } var format: Format var data: Data var modificationDate: Date var author: Author }
能夠避免代碼重復當然是件好事,而且枚舉是當我們在處理一個模型的不同格式或變體時一般情況下建模 的好方法,但是上述那種設(shè)置實際上最終會造成相當多的模糊性。
例如,我們可能有一些API,只有在調(diào)用給定格式的文檔時才有意義——比如這個打開文本編輯器的函數(shù),它假定任何傳入它的Document
都是文本文檔:
func openTextEditor(for document: Document) { let text = String(decoding: document.data, as: UTF8.self) let editor = TextEditor(text: text) ... }
雖然如果我們不小心將一個HTML文檔傳遞給上述函數(shù)并不是世界末日(HTML畢竟只是文本),但試圖以這種方式打開一個PDF,很可能會導致呈現(xiàn)出完全無法理解的東西,我們的文本編輯功能將無法工作,我們的應(yīng)用程序甚至可能最終崩潰。
我們在編寫任何其他特定格式的代碼時都會不斷遇到同樣的問題,例如,如果我們想通過實現(xiàn)一個解析器和一個專門的編輯器來改善編輯HTML文檔的用戶體驗:
func openHTMLEditor(for document: Document) { // 就像我們上面用于文本編輯的函數(shù)一樣, // 這個函數(shù)假設(shè)它總是被傳遞給HTML文檔。 let parser = HTMLParser() let html = parser.parse(document.data) let editor = HTMLEditor(html: html) ... }
一個關(guān)于如何解決上述問題的初步想法可能是編寫一個包裝函數(shù),切換到所傳遞文檔的格式,然后為每種情況打開正確的編輯器。然而,雖然這對文本和HTML文檔很有效,但由于PDF文檔在我們的應(yīng)用程序中是不可編輯的——當遇到PDF時,我們將被迫拋出一個錯誤,觸發(fā)一個斷言,或以其他方式失敗:
func openEditor(for document: Document) { switch document.format { case .text: openTextEditor(for: document) case .html: openHTMLEditor(for: document) case .pdf: assertionFailure("Cannot edit PDF documents") } }
上述情況不是很好,因為它要求我們作為開發(fā)者始終跟蹤我們在任何給定的代碼路徑中所處理的文件類型,而我們可能犯的任何錯誤只能在運行時被發(fā)現(xiàn)——編譯器根本沒有足夠的信息可以在編譯時進行這種檢查。
因此,盡管我們的 "Document "模型乍一看可能非常優(yōu)雅和完善,但事實證明,它并不完全是手頭情況的正確解決方案。
看起來我們需要一個協(xié)議!
解決上述問題的一個方法是把Document
變成一個協(xié)議,而不是作為一個具體的類型,把它的所有屬性(除了format
)都作為要求:
protocol Document { var data: Data { get } var modificationDate: Date { get } var author: Author { get } }
有了上述變化,我們現(xiàn)在可以為我們的三種文檔格式中的每一種實現(xiàn)專門的類型,并讓這些類型都符合我們新的文檔協(xié)議——比如這樣:
struct TextDocument: Document { var data: Data var modificationDate: Date var author: Author }
上述方法的好處是,它使我們既能實現(xiàn)可以對任何Document
進行操作的通用功能,又能實現(xiàn)只接受某種具體類型的特定API:
// 這個函數(shù)可以保存任何文件, // 所以它接受任何符合我們的新文檔協(xié)議。 func save(_ document: Document) { ... } // 我們現(xiàn)在只能向我們的函數(shù)傳遞文本文件, // 即打開一個文本編輯器。 func openTextEditor(for document: TextDocument) { ... }
我們在上面所做的基本上是將以前在運行時進行的檢查轉(zhuǎn)為在編譯時進行驗證——因為編譯器現(xiàn)在能夠檢查我們是否總是向我們的每個API傳遞正確格式的文件,這是一個很大的進步。
然而,通過執(zhí)行上述改變,我們也失去了我們最初實現(xiàn)的優(yōu)點——代碼重用。由于我們現(xiàn)在使用一個協(xié)議來表示所有的文檔格式,我們將需要為我們的三種文檔類型中的每一種編寫完全重復的模型實現(xiàn),以及為我們將來可能增加的任何其他格式提供支持。
引入幻象類型
如果我們能找到一種方法,既能為所有格式重用相同的Document
模型,又能在編譯時驗證我們特定格式的代碼,豈不妙哉?事實證明,我們之前的一行代碼實際上可以給我們一個實現(xiàn)這一目標的提示:
let text = String(decoding: document.data, as: UTF8.self)
當把Data
轉(zhuǎn)換為String
時,就像我們上面做的那樣,我們通過傳遞對該類型本身的引用來傳遞我們希望字符串被解碼的編碼——在本例中是UTF8。這真的很有趣。如果我們再深入一點,就會發(fā)現(xiàn) Swift 標準庫將我們上面提到的UTF8類型定義為另一個類似命名空間的枚舉中的一個無大小寫枚舉,稱為Unicode
。
enum Unicode { enum UTF8 {} ... } typealias UTF8 = Unicode.UTF8
請注意,如果你看一下
UTF8
類型的實際實現(xiàn),它確實包含一個私有case,只是為了向后兼容 Swift 3 而存在。
我們在這里看到的是一種被稱為幻象類型的技術(shù)——當類型被用作標記,而不是被實例化來表示值或?qū)ο髸r。事實上,由于上述枚舉都沒有任何公開的情況,它們甚至不能被實例化!
讓我們看看是否可以用同樣的技術(shù)來解決我們的Document
困境。我們首先將Document
還原成一個結(jié)構(gòu)體,只是這次我們將刪除它的format
屬性(以及相關(guān)的枚舉),而將它變成一個覆蓋任何Format
類型的泛型——比如這樣:
struct Document<Format> { var data: Data var modificationDate: Date var author: Author }
受標準庫的Unicode
枚舉及其各種編碼的啟發(fā),我們將定義一個類似的枚舉——DocumentFormat
——作為三個無大小寫的枚舉的命名空間,每種格式都有一個:
enum DocumentFormat { enum Text {} enum HTML {} enum PDF {} }
請注意,這里不涉及任何協(xié)議——任何類型都可以被用作格式,因為就像String
和它的各種編碼一樣,我們將只使用文檔的Format
類型作為編譯時的標記。這將使我們能夠像這樣寫出我們特定格式的API:
func openTextEditor(for document: Document<DocumentFormat.Text>) { ... } func openHTMLEditor(for document: Document<DocumentFormat.HTML>) { ... } func openPreview(for document: Document<DocumentFormat.PDF>) { ... }
當然,我們?nèi)匀豢梢跃帉懖恍枰魏翁囟ǜ袷降耐ㄓ么a。例如,這里我們可以把之前的save
API變成一個完全通用的函數(shù):
func save<F>(_ document: Document<F>) { ... }
然而,總是輸入Document<DocumentFormat.Text>
來引用一個文本文檔是相當乏味的,所以讓我們也使用類型別名為每種格式定義速記。這將給我們提供漂亮的、有語義的名字,而不需要任何重復的代碼:
typealias TextDocument = Document<DocumentFormat.Text> typealias HTMLDocument = Document<DocumentFormat.HTML> typealias PDFDocument = Document<DocumentFormat.PDF>
在涉及到特定格式的擴展時,幻象類型也確實大放異彩,現(xiàn)在可以直接使用 Swift 強大的泛型系統(tǒng)和泛型型約束來實現(xiàn)。例如,我們可以用一個生成NSAttributedString
的方法來擴展所有文本文檔:
extension Document where Format == DocumentFormat.Text { func makeAttributedString(withFont font: UIFont) -> NSAttributedString { let string = String(decoding: data, as: UTF8.self) return NSAttributedString(string: string, attributes: [ .font: font ]) } }
由于我們的幻象類型在最后只是普通的類型——我們也可以讓它們遵守協(xié)議,并使用這些協(xié)議作為泛型約束。例如,我們可以讓我們的一些DocumentFormat
類型遵守Printable
協(xié)議,然后我們可以在打印代碼中使用這些協(xié)議作為約束條件。這里有大量的可能性。
一個標準的模式
起初,幻象類型在 Swift 中可能看起來有點 "格格不入"。然而,雖然 Swift 并沒有像更多的純函數(shù)式語言(如Haskell)那樣為幻象類型提供一流的支持,但在標準庫和蘋果平臺SDK的許多不同地方都可以找到這種模式。
例如,Foundation
的Measurement
API使用幻象類型來確保在傳遞各種測量值時的類型安全——例如度數(shù)、長度和重量:
let meters = Measurement<UnitLength>(value: 5, unit: .meters) let degrees = Measurement<UnitAngle>(value: 90, unit: .degrees)
通過使用幻影類型,上述兩個測量值不能被混合,因為每個值是哪種單位,都被編碼到該值的類型中。這可以防止我們不小心將一個長度傳遞給一個接受角度的函數(shù),反之亦然——就像我們之前防止文檔格式被混淆一樣。
結(jié)論
使用幻象類型是一種非常強大的技術(shù),它可以讓我們利用類型系統(tǒng)來驗證一個特定值的不同變體。雖然使用幻象類型通常會使API更加冗長,而且確實伴隨著泛型的復雜性——當處理不同的格式和變體時,它可以讓我們減少對運行時檢查的依賴,而讓編譯器來執(zhí)行這些檢查。
就像一般的泛型一樣,我認為在部署幻象類型之前,首先要仔細評估當前的情況,這很重要。就像我們最初的Document
模型并不是手頭任務(wù)的正確選擇,盡管它的結(jié)構(gòu)很好,但如果部署在錯誤的情況下,幻象類型會使簡單的設(shè)置變得更加復雜。像往常一樣,它歸結(jié)為為工作選擇正確的工具。
到此這篇關(guān)于Swift 中的幻象類型的文章就介紹到這了,更多相關(guān)Swift 幻象類型內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
用Swift構(gòu)建一個簡單的iOS郵件應(yīng)用的方法
這篇文章主要介紹了用Swift構(gòu)建一個簡單的iOS郵件應(yīng)用的方法,包括查看和標記已讀等基本的郵件應(yīng)用功能,需要的朋友可以參考下2015-07-07Swift高階函數(shù)contains?allSatisfy?reversed?lexicographicallyPr
這篇文章主要為大家介紹了Swift高階函數(shù)contains?allSatisfy?reversed?lexicographicallyPrecedes用法示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-06-06Swift中static和class關(guān)鍵字的深入講解
這篇文章主要給大家介紹了關(guān)于Swift中static和class關(guān)鍵字的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學習或者使用Java具有一定的參考學習價值,需要的朋友們下面來一起學習學習吧2019-03-03