自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

聊聊 Swift 中的幻象類型

移動(dòng)開發(fā) iOS
讓我們來看看一種技術(shù),它可以讓我們利用 Swift 的類型系統(tǒng)在編譯時(shí)執(zhí)行更多種類的數(shù)據(jù)驗(yàn)證——消除更多潛在的歧義來源,并幫助我們?cè)谡麄€(gè)代碼庫中保持類型安全——通過使用幻象類型(phantom types)。

前言

模糊的數(shù)據(jù)可以說是一般應(yīng)用程序中最常見的錯(cuò)誤和問題的來源之一。雖然 Swift 通過其強(qiáng)大的類型系統(tǒng)和完善的編譯器幫助我們避免了許多含糊不清的來源——但只要我們無法在編譯時(shí)保證某個(gè)數(shù)據(jù)總是符合我們的要求,就總是有風(fēng)險(xiǎn),我們最終會(huì)處于含糊不清或不可預(yù)測(cè)的狀態(tài)。

本周,讓我們來看看一種技術(shù),它可以讓我們利用 Swift 的類型系統(tǒng)在編譯時(shí)執(zhí)行更多種類的數(shù)據(jù)驗(yàn)證——消除更多潛在的歧義來源,并幫助我們?cè)谡麄€(gè)代碼庫中保持類型安全——通過使用幻象類型(phantom types)。

定義良好,但仍然含糊不清

舉個(gè)例子,假設(shè)我們正在開發(fā)一個(gè)文本編輯器,雖然它最初只支持純文本文件——隨著時(shí)間的推移,我們還增加了對(duì)編輯HTML文檔的支持,以及PDF預(yù)覽。

為了能夠盡可能多地重復(fù)使用我們?cè)瓉淼奈臋n處理代碼,我們繼續(xù)使用與開始時(shí)相同的Document模型——只是現(xiàn)在它獲得了一個(gè)Format屬性,告訴我們正在處理什么樣的文檔:

struct Document {
enum Format {
case text
case html
case pdf
}
var format: Format
var data: Data
var modificationDate: Date
var author: Author
}

能夠避免代碼重復(fù)當(dāng)然是件好事,而且枚舉是當(dāng)我們?cè)谔幚硪粋€(gè)模型的不同格式或變體時(shí)一般情況下建模 的好方法,但是上述那種設(shè)置實(shí)際上最終會(huì)造成相當(dāng)多的模糊性。

例如,我們可能有一些API,只有在調(diào)用給定格式的文檔時(shí)才有意義——比如這個(gè)打開文本編輯器的函數(shù),它假定任何傳入它的Document都是文本文檔:

func openTextEditor(for document: Document) {
let text = String(decoding: document.data, as: UTF8.self)
let editor = TextEditor(text: text)
...
}

雖然如果我們不小心將一個(gè)HTML文檔傳遞給上述函數(shù)并不是世界末日(HTML畢竟只是文本),但試圖以這種方式打開一個(gè)PDF,很可能會(huì)導(dǎo)致呈現(xiàn)出完全無法理解的東西,我們的文本編輯功能將無法工作,我們的應(yīng)用程序甚至可能最終崩潰。

我們?cè)诰帉懭魏纹渌囟ǜ袷降拇a時(shí)都會(huì)不斷遇到同樣的問題,例如,如果我們想通過實(shí)現(xiàn)一個(gè)解析器和一個(gè)專門的編輯器來改善編輯HTML文檔的用戶體驗(yàn):

func openHTMLEditor(for document: Document) {
// 就像我們上面用于文本編輯的函數(shù)一樣,
// 這個(gè)函數(shù)假設(shè)它總是被傳遞給HTML文檔。
let parser = HTMLParser()
let html = parser.parse(document.data)
let editor = HTMLEditor(html: html)
...
}

一個(gè)關(guān)于如何解決上述問題的初步想法可能是編寫一個(gè)包裝函數(shù),切換到所傳遞文檔的格式,然后為每種情況打開正確的編輯器。然而,雖然這對(duì)文本和HTML文檔很有效,但由于PDF文檔在我們的應(yīng)用程序中是不可編輯的——當(dāng)遇到PDF時(shí),我們將被迫拋出一個(gè)錯(cuò)誤,觸發(fā)一個(gè)斷言,或以其他方式失敗:

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")
}
}

上述情況不是很好,因?yàn)樗笪覀冏鳛殚_發(fā)者始終跟蹤我們?cè)谌魏谓o定的代碼路徑中所處理的文件類型,而我們可能犯的任何錯(cuò)誤只能在運(yùn)行時(shí)被發(fā)現(xiàn)——編譯器根本沒有足夠的信息可以在編譯時(shí)進(jìn)行這種檢查。

因此,盡管我們的 "Document "模型乍一看可能非常優(yōu)雅和完善,但事實(shí)證明,它并不完全是手頭情況的正確解決方案。

看起來我們需要一個(gè)協(xié)議!

解決上述問題的一個(gè)方法是把Document變成一個(gè)協(xié)議,而不是作為一個(gè)具體的類型,把它的所有屬性(除了format)都作為要求:

protocol Document {
var data: Data { get }
var modificationDate: Date { get }
var author: Author { get }
}

有了上述變化,我們現(xiàn)在可以為我們的三種文檔格式中的每一種實(shí)現(xiàn)專門的類型,并讓這些類型都符合我們新的文檔協(xié)議——比如這樣:

struct TextDocument: Document {
var data: Data
var modificationDate: Date
var author: Author
}

上述方法的好處是,它使我們既能實(shí)現(xiàn)可以對(duì)任何Document進(jìn)行操作的通用功能,又能實(shí)現(xiàn)只接受某種具體類型的特定API:

// 這個(gè)函數(shù)可以保存任何文件,
// 所以它接受任何符合我們的新文檔協(xié)議。
func save(_ document: Document) {
...
}
// 我們現(xiàn)在只能向我們的函數(shù)傳遞文本文件,
// 即打開一個(gè)文本編輯器。
func openTextEditor(for document: TextDocument) {
...
}

我們?cè)谏厦嫠龅幕旧鲜菍⒁郧霸谶\(yùn)行時(shí)進(jìn)行的檢查轉(zhuǎn)為在編譯時(shí)進(jìn)行驗(yàn)證——因?yàn)榫幾g器現(xiàn)在能夠檢查我們是否總是向我們的每個(gè)API傳遞正確格式的文件,這是一個(gè)很大的進(jìn)步。

然而,通過執(zhí)行上述改變,我們也失去了我們最初實(shí)現(xiàn)的優(yōu)點(diǎn)——代碼重用。由于我們現(xiàn)在使用一個(gè)協(xié)議來表示所有的文檔格式,我們將需要為我們的三種文檔類型中的每一種編寫完全重復(fù)的模型實(shí)現(xiàn),以及為我們將來可能增加的任何其他格式提供支持。

引入幻象類型

如果我們能找到一種方法,既能為所有格式重用相同的Document模型,又能在編譯時(shí)驗(yàn)證我們特定格式的代碼,豈不妙哉?事實(shí)證明,我們之前的一行代碼實(shí)際上可以給我們一個(gè)實(shí)現(xiàn)這一目標(biāo)的提示:

let text = String(decoding: document.data, as: UTF8.self)

當(dāng)把Data轉(zhuǎn)換為String時(shí),就像我們上面做的那樣,我們通過傳遞對(duì)該類型本身的引用來傳遞我們希望字符串被解碼的編碼——在本例中是UTF8。這真的很有趣。如果我們?cè)偕钊胍稽c(diǎn),就會(huì)發(fā)現(xiàn) Swift 標(biāo)準(zhǔn)庫將我們上面提到的UTF8類型定義為另一個(gè)類似命名空間的枚舉中的一個(gè)無大小寫枚舉,稱為Unicode。

enum Unicode {
enum UTF8 {}
...
}
typealias UTF8 = Unicode.UTF8

請(qǐng)注意,如果你看一下UTF8類型的實(shí)際實(shí)現(xiàn),它確實(shí)包含一個(gè)私有case,只是為了向后兼容 Swift 3 而存在。

我們?cè)谶@里看到的是一種被稱為幻象類型的技術(shù)——當(dāng)類型被用作標(biāo)記,而不是被實(shí)例化來表示值或?qū)ο髸r(shí)。事實(shí)上,由于上述枚舉都沒有任何公開的情況,它們甚至不能被實(shí)例化!

讓我們看看是否可以用同樣的技術(shù)來解決我們的Document困境。我們首先將Document還原成一個(gè)結(jié)構(gòu)體,只是這次我們將刪除它的format屬性(以及相關(guān)的枚舉),而將它變成一個(gè)覆蓋任何Format類型的泛型——比如這樣:

struct Document<Format> {
var data: Data
var modificationDate: Date
var author: Author
}

受標(biāo)準(zhǔn)庫的Unicode枚舉及其各種編碼的啟發(fā),我們將定義一個(gè)類似的枚舉——DocumentFormat——作為三個(gè)無大小寫的枚舉的命名空間,每種格式都有一個(gè):

enum DocumentFormat {
enum Text {}
enum HTML {}
enum PDF {}
}

請(qǐng)注意,這里不涉及任何協(xié)議——任何類型都可以被用作格式,因?yàn)榫拖馭tring和它的各種編碼一樣,我們將只使用文檔的Format類型作為編譯時(shí)的標(biāo)記。這將使我們能夠像這樣寫出我們特定格式的API:

func openTextEditor(for document: Document<DocumentFormat.Text>) {
...
}
func openHTMLEditor(for document: Document<DocumentFormat.HTML>) {
...
}
func openPreview(for document: Document<DocumentFormat.PDF>) {
...
}

當(dāng)然,我們?nèi)匀豢梢跃帉懖恍枰魏翁囟ǜ袷降耐ㄓ么a。例如,這里我們可以把之前的saveAPI變成一個(gè)完全通用的函數(shù):

func save<F>(_ document: Document<F>) {
...
}

然而,總是輸入Document來引用一個(gè)文本文檔是相當(dāng)乏味的,所以讓我們也使用類型別名為每種格式定義速記。這將給我們提供漂亮的、有語義的名字,而不需要任何重復(fù)的代碼:

typealias TextDocument = Document<DocumentFormat.Text>
typealias HTMLDocument = Document<DocumentFormat.HTML>
typealias PDFDocument = Document<DocumentFormat.PDF>

在涉及到特定格式的擴(kuò)展時(shí),幻象類型也確實(shí)大放異彩,現(xiàn)在可以直接使用 Swift 強(qiáng)大的泛型系統(tǒng)和泛型型約束來實(shí)現(xiàn)。例如,我們可以用一個(gè)生成NSAttributedString的方法來擴(kuò)展所有文本文檔:

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é)議作為約束條件。這里有大量的可能性。

一個(gè)標(biāo)準(zhǔn)的模式

起初,幻象類型在 Swift 中可能看起來有點(diǎn) "格格不入"。然而,雖然 Swift 并沒有像更多的純函數(shù)式語言(如Haskell)那樣為幻象類型提供一流的支持,但在標(biāo)準(zhǔn)庫和蘋果平臺(tái)SDK的許多不同地方都可以找到這種模式。

例如,F(xiàn)oundation的Measurement API使用幻象類型來確保在傳遞各種測(cè)量值時(shí)的類型安全——例如度數(shù)、長度和重量:

let meters = Measurement<UnitLength>(value: 5, unit: .meters)
let degrees = Measurement<UnitAngle>(value: 90, unit: .degrees)

通過使用幻影類型,上述兩個(gè)測(cè)量值不能被混合,因?yàn)槊總€(gè)值是哪種單位,都被編碼到該值的類型中。這可以防止我們不小心將一個(gè)長度傳遞給一個(gè)接受角度的函數(shù),反之亦然——就像我們之前防止文檔格式被混淆一樣。

結(jié)論

使用幻象類型是一種非常強(qiáng)大的技術(shù),它可以讓我們利用類型系統(tǒng)來驗(yàn)證一個(gè)特定值的不同變體。雖然使用幻象類型通常會(huì)使API更加冗長,而且確實(shí)伴隨著泛型的復(fù)雜性——當(dāng)處理不同的格式和變體時(shí),它可以讓我們減少對(duì)運(yùn)行時(shí)檢查的依賴,而讓編譯器來執(zhí)行這些檢查。

就像一般的泛型一樣,我認(rèn)為在部署幻象類型之前,首先要仔細(xì)評(píng)估當(dāng)前的情況,這很重要。就像我們最初的Document模型并不是手頭任務(wù)的正確選擇,盡管它的結(jié)構(gòu)很好,但如果部署在錯(cuò)誤的情況下,幻象類型會(huì)使簡(jiǎn)單的設(shè)置變得更加復(fù)雜。像往常一樣,它歸結(jié)為為工作選擇正確的工具。

責(zé)任編輯:姜華 來源: Swift社區(qū)
相關(guān)推薦

2022-05-25 09:15:01

Swift 5.6占位符

2022-06-13 09:02:06

Swift類型占位符

2022-07-04 08:54:39

Swift處理器項(xiàng)目

2021-07-07 11:41:38

Swift key paths

2022-04-06 09:10:03

抽象類型普通類型Swift

2021-03-02 21:52:48

Hive數(shù)據(jù)類型

2024-01-18 07:09:10

2022-06-17 06:23:23

Oracle壓縮類型

2024-08-12 08:50:17

2022-03-31 09:01:10

Swift類型擦除類型安全性

2022-11-04 09:01:33

SwiftPlottable

2024-05-20 08:58:13

Java引用類型垃圾回收器

2021-06-29 09:01:50

Swift閉包語言

2020-11-05 09:54:13

5G

2023-11-09 11:56:28

MySQL死鎖

2021-08-31 07:54:24

SQLDblink查詢

2024-04-26 00:00:00

Rust檢查器代碼

2021-11-17 08:11:35

MySQL

2015-01-21 16:25:29

Swift指針

2015-11-23 10:07:19

Swift模式匹配
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)