Swift 中的 String 是 Character 值的集合,而 Character 是人類在閱讀文字時所理解的單個字符,這與該字符由多少個 Unicode 標量組成無關。html
Swift裏面的String不支持隨機訪問,不能用相似str[999]來獲取字符串的第一千個字符。算法
當字符擁有可變寬度時,字符串並不知道第 n 個字符到底存儲在哪兒,它必須查看這個字符前面的全部字符,才能最終肯定對象字符的存儲位置,因此這不多是一個O(1)操做swift
ASCII字符串就是由0到127之間的整數組成的序列。能夠把這種整數放到一個8比特的字節裏。數組
可是8個比特對於許多語言的編碼來講是不夠用的。安全
當固定寬度的編碼空間被用完後,有兩種選擇:markdown
String網絡
let single = "Pok\u{00E9}mon" // Pokémon
let double = "Poke\u{0301}mon" // Pokémon
(single, double) // ("Pokémon", "Pokémon")
single.count // 7
double.count // 7
//默認就是按照標準等價的方式進行比較
single == double // true
//經過比較組成字符串的Unicode標量
single.unicodeScalars.count // 7
double.unicodeScalars.count // 8
//經過比較字符串的utf8
single.utf8.elementsEqual(double.utf8) // false
複製代碼
let chars: [Character] = [ "\u{1ECD}\u{300}", // ọ́
"\u{F2}\u{323}", // ọ́
"\u{6F}\u{323}\u{300}", // ọ́
"\u{6F}\u{300}\u{323}" // ọ́ ]
let allEqual = chars.dropFirst().allSatisfy { $0 == chars.first } // true
複製代碼
NSString數據結構
let nssingle = single as NSString
nssingle.length // 7
let nsdouble = double as NSString
nsdouble.length // 8
nssingle == nsdouble // false
//按照標準等價的方式進行比較兩個 NSString,就得使用 NSString.compare(_:) 方法
複製代碼
Java或者C#裏,會認爲"😂"是兩個「字符」長,Swift 則能正確處理這種狀況:閉包
let oneEmoji = "😂"// U+1F602
oneEmoji.count // 1
複製代碼
這裏重要的是,字符串是如何呈如今程序中的,而不是它是如何存儲在內存中的。app
有些顏文字還能夠由多個Unicode標量組合而成:
let flags = "🇧🇷🇳🇿"
flags.count // 2
複製代碼
要觀察組成字符串的 Unicode 標量,咱們可使用字符串的 unicodeScalars 視圖,這裏,我 們將標量值格式化爲編碼點經常使用的十六進制格式:
fags.unicodeScalars.map {
"U+\(String($0.value, radix: 16, uppercase: true))"
}
// ["U+1F1E7", "U+1F1F7", "U+1F1F3", "U+1F1FF"]
複製代碼
把五種膚色修飾符 (好比 🏽,或者其餘四種膚色修飾符之一) 和一個像是👧的基礎角色組合起來,就能夠獲得相似👧🏽這樣的帶有膚色的角色。再一次,Swift 能正確對其處理:
let skinTone = "👧🏽" // 👧 + 🏽
skinTone.count // 1
複製代碼
對於這種表達人羣的顏文字,不管是性別仍是人數都存在着數不清的組合,爲其中每一種組合都單獨定義一個編碼點很是容易出問題。若是再把這些組合考慮上膚色的維度,讓每種狀況都有對應的編碼點簡直就成了一件不可能的事情。
對此,Unicode 的解決方案是把這種複雜的顏 文字表示成一個簡單顏文字的序列,序列中的顏文字則經過一個標量值爲 U+200D 的不可見零寬鏈接字符 (zero-width joiner,ZWJ) 鏈接
ZWJ 的存在,是對操做系統的提示,代表 若是可能的話,把 ZWJ 鏈接的字符當成一個字形符號 (glyph) 處理。
let family1 = "👨👩👧👦"
let family2 = "👨\u{200D}👩\u{200D}👧\u{200D}👦"
family1 == family2 // true
family1.count // 1
family2.count // 1
複製代碼
String 是 Character 值的集合
Swift4之後:將兩個集合鏈接的時候,你可能會假設所獲得的集合的長度是兩個用來鏈接的集合長度之和。可是對於字符串來講,若是第一個集合的末尾和第二個集合的開頭可以造成一個字位簇的話,它們就再也不相等。
let flagLetterC = "🇨"
let flagLetterN = "🇳"
let flag = flagLetterC + flagLetterN // 🇨🇳
flag.count // 1
flag.count == flagLetterC.count + flagLetterN.count // false
複製代碼
String 並非一個能夠隨機訪問的集合。就算知道給定字符串中第 n 個字符的位置,也並不會對計算這個字符以前有多少個 Unicode 標量有任何幫助。String 只實現了 BidirectionalCollection,你能夠從字符串的頭或者尾開始,向後或者向前移動,代碼會察看毗鄰字符的組合,跳過正確的字節數。無論怎樣,你每次只能迭代一個字符。
當你在書寫一些字符串處理的代碼時,須要將這個性能影響時刻牢記在心。那些須要隨機訪問 才能維持其性能保證的算法對於Unicode字符串來講並非一個好的選擇
prefix 老是要從頭開始工做,而後在字符串上通過所須要的字符個數,在一個線性複雜度的處理中運行另外一個線性複雜度的操做,意味着算法複雜度將會是 O(n^2)。
extension String {
var allPrefixes1: [Substring] {
return (0...count).map(prefix)
}
}
let hello = "Hello"
hello.allPrefixes1 // ["", "H", "He", "Hel", "Hell", "Hello"]
複製代碼
須要迭代一次字符串,以獲取索引的集合indices,map中的下標操做就是O(1)複雜度的,這使得整個算法的複雜度得以保持在 O(n)。
extension String {
var allPrefixes2: [Substring] {
return [""] + indices.map { index in self[...index] }
}
}
let hello = "Hello"
hello.allPrefixes2 // ["", "H", "He", "Hel", "Hell", "Hello"]
複製代碼
String 還知足 RangeReplaceableCollection 協議
首先找到字符串索引中一個恰當的範圍,而後經過調用 replaceSubrange 來完成字符串替換
var greeting = "Hello, world!"
if let comma = greeting.index(of: ",") {
greeting[..<comma] // Hello
greeting.replaceSubrange(comma..., with: " again.")
}
greeting // Hello again.
複製代碼
和以前同樣,要注意用於替換的字符串有可能與原字符串相鄰的字符造成新的字位簇。
String的索引類型是String.Index,本質上是一個存儲了從字符串開頭的字節偏移量的不透明值。
計算第 n 個字符所對應的索引 -> 花費 O(n) 的時間
經過索引下標訪問字符串 -> 花費 O(1) 的時間
操做字符串索引的 API 與你在遇到其餘任何集合時使用的索引操做是同樣的,它們都基於 Collection 協議。
index(after:)
let s = "abcdef"
let second = s.index(after: s.startIndex)
s[second] // b
複製代碼
index(_:offsetBy:)
// 步進 4 個字符
let sixth = s.index(second, offsetBy: 4)
s[sixth] // f
複製代碼
limitedBy: 參數
let safeIdx = s.index(s.startIndex, offsetBy: 400, limitedBy: s.endIndex)
safeIdx // nil
複製代碼
有些簡單的需求,使用索引,看起來都比較麻煩:
s[..<s.index(s.startIndex, offsetBy: 4)] // abcd
複製代碼
可是能夠經過 Collection 的接口來訪問字符串
s.prefx(4) // abcd
複製代碼
let date = "2019-09-01"
date.split(separator: "-")[1] // 09
date.dropFirst(5).prefx(2) // 09
複製代碼
var hello = "Hello!"
if let idx = hello.frstIndex(of: "!") {
hello.insert(contentsOf: ", world", at: idx)
}
hello // Hello, world!
複製代碼
有一些字符串操做的任務是沒法經過 Collection API 完成的,好比解析 CSV 文件:
func parse(csv: String) -> [[String]] {
var result: [[String]] = [[]]
var currentField = ""
var inQuotes = false
for c in csv {
switch (c, inQuotes) {
case (",", false):
result[result.endIndex-1].append(currentField)
currentField.removeAll()
case ("\n", false):
result[result.endIndex-1].append(currentField)
currentField.removeAll()
result.append([])
case ("\"", _):
inQuotes = !inQuotes
default:
currentField.append(c)
}
}
result[result.endIndex-1].append(currentField)
return result
}
複製代碼
//字符串用 ## 包圍起來。這樣就能夠在字符串中直接使用引號而不用轉義了
let csv = #"""
"Values in quotes","can contain , characters"
"Values without quotes work as well:",42
"""#
parse(csv: csv)
/*
[["Values in quotes", "can contain , characters"], ["Values without quotes work as well:", "42"]]
*/
複製代碼
結構精簡,無需跟蹤不少內部狀態,僅用一個布爾變量,經過一點額外的工做,咱們還能夠忽略空 行、忽略引號周圍的空格,並支持在字段的引號中經過轉義的方式繼續使用引號。
以原始字符串內容爲基礎,用不一樣起始和結束位置標記的視圖。
let sentence = "The quick brown fox jumped over the lazy dog."
let frstSpace = sentence.index(of: " ") ?? sentence.endIndex
let frstWord = sentence[..<frstSpace] // The
type(of: frstWord) // Substring
//建立 firstWord 並不會致使昂貴的複製操做或者內存申請
複製代碼
[Substring]
。let poem = """
Over the wintry
forest, winds howl in rage
with no leaves to blow.
"""
let lines = poem.split(separator: "\n")
lines// ["Over the wintry", "forest, winds howl in rage", "with no leaves to blow."]
type(of: lines) // Array<Substring>
//整個過程當中沒有發生對輸入字符串的複製
複製代碼
extension String {
func wrapped(after maxLength: Int = 70) -> String {
var lineLength = 0
let lines = self.split(omittingEmptySubsequences: false) { character in
if character.isWhitespace && lineLength >= maxLength {
lineLength = 0
return true
} else {
lineLength += 1
return false
}
}
return lines.joined(separator: "\n")
}
}
let sentence = "The quick brown fox jumped over the lazy dog."
sentence.wrapped(after: 15)
/*
The quick brown
fox jumped over
the lazy dog.
*/
複製代碼
extension Collection where Element: Equatable {
func split<S: Sequence>(separators: S) -> [SubSequence]
where Element == S.Element {
return split { separators.contains($0) }
}
}
"Hello, world!".split(separators: ",! ") // ["Hello", "world"]
複製代碼
Substring 和 String 的接口幾乎徹底同樣,由於他們都遵循StringProtocol協議。
幾乎全部的字符串 API 都被定義在StringProtocol 上,對於 Substring,你徹底能夠僞裝將它看做就是一個 String。
和全部的切片同樣,Substring的設計意圖是用於短時間存儲,以免在操做過程當中發生昂貴的複製。
當這個操做結束,應該經過初始化方法從 Substring 建立一個新的 String。不鼓勵長期存儲子字符串的根本緣由在於,子字符串會一直持有整個原始字符串,形成內存泄漏。
func lastWord(in input: String) -> String? {
// 處理輸⼊,操做⼦字符串
let words = input.split(separators: [",", " "])
guard let lastWord = words.last else { return nil }
// 轉換爲字符串並返回
return String(lastWord)
}
lastWord(in: "one, two, three, four, five") // Optional("five")
複製代碼
// 使⽤原字符串開頭索引和結尾索引做爲範圍的⼦字符串
let substring = sentence[...]
複製代碼
Swift不建議將你的全部 API 從接受 String 實例轉換爲遵照 StringProtocol 的類型,建議是堅持使用 String。
若是你想要擴展 String 爲其添加新的功能,將這個擴展放在 StringProtocol 會是一個好主意,這能夠保持 String 和 Substring API 的統一性。StringProtocol 設計之初就是爲了在你想要對String 擴展時來使用的。若是你想要將已有的擴展從 String 移動到 StringProtocol 的話,惟一須要作的改動是將傳入其餘 API 的 self 經過 String(self) 換爲具體的 String 類型實例。
不要聲明任何新的遵照 StringProtocol 協議的類型。只有標準庫中的 String 和 Substring 是有效的適配類型。
有時候Character字符沒法知足須要時,咱們還能夠向下到好比Unicode標量
或者編碼單元
這樣更低的層次中進行查看和操做。
String 爲此提供了三種視圖:unicodeScalars
,utf16
和 utf8
。
爲何你會想要對某個視圖進行訪問和操做?
Twitter之前的字符計算算法是基於NFC歸一化標量:
let tweet = "☕️e\u{301}🇫🇷☀️"
print(tweet.count) // 1+1+1+1=4
var characterCount = tweet.unicodeScalars.count
print(characterCount) //2+2+2+2=8
characterCount = tweet.precomposedStringWithCanonicalMapping.unicodeScalars.count
print(characterCount) //2+1+2+2=7
//precomposedStringWithCanonicalMapping: 按照C標準進行字符串歸一化
//NFC 歸一能夠對基礎字母及合併標記進行轉換,好比 "cafe\u{301}" 中的 e 和變音符能夠被正
確預組起來。
複製代碼
UTF-8 是用來存儲或者在網絡上發送文本的事實標準。由於 utf8 視圖是一個集合,你能夠用它來將字符串的 UTF-8 字節傳遞給任一接受一串字節的其餘 API,例如 Data 或者 Array 的初始化方法:
let tweet = "☕️e\u{301}🇫🇷☀️"
let utf8Bytes = Data(tweet.utf8)
print(utf8Bytes.count) // 6+3+8+6=23
複製代碼
UTF-8 是 String 全部編碼單元視圖中,系統開銷最低的。由於它是 Swift 字符串在內存中的原生存儲格式。
utf8 集合不包含字符串尾部的 null 字節。若是你須要用 null 表示結尾的話,可使用 String 的 withCString
方法或者 utf8CString
屬性。後者會返回一個字節的數組。
let tweet = "☕️e\u{301}🇫🇷☀️"
let withCStringCount = tweet.withCString { _ in strlen(tweet) }
print(withCStringCount) // 23
let nullTerminatedUTF8 = tweet.utf8CString
print(nullTerminatedUTF8.count) // 24
複製代碼
這些視圖都沒有提供咱們想要的隨機訪問特性。這樣形成的後果是,那些要求隨機訪問的算法將不能很好地運行在 String 和它的視圖上。
若是你真的須要隨機存儲的話,你依然能夠把字符串自身或着它的視圖轉換爲數組,例如:Array(str) 或 Array(str.utf8),而後對它們進行操做。
字符串
和它們的視圖
共享一樣的索引類型,String.Index。能夠從字符串中獲取一個索引,而後將它用在某個視圖的下標訪問中。let pokemon = "Poke\u{301}mon" // Pokémon
if let index = pokemon.index(of: "é") {
let scalar = pokemon.unicodeScalars[index] // e
String(scalar) // e
}
複製代碼
字符
,到標量
,再到 UTF-16
或 UTF-8
編碼單元這個方向上的話,這麼作不會有什麼問題。可是另外一個方向的話就不必定正確了,由於並非每一個編碼單元視圖中的有效索引都會在 Character 的邊界上。let family = "👨👩👧👦"
let someUTF16Index = String.Index(utf16Offset: 2, in: family)
family[someUTF16Index] //Crash
複製代碼
samePosition(in:)
輸入的索引在給定的視圖中沒有對應的位置,將返回 nillet pokemon = "Poke\u{301}mon" // Pokémon
if let accentIndex = pokemon.unicodeScalars.firstIndex(of: "\u{301}") {
accentIndex.samePosition(in: pokemon) // nil
}
複製代碼
String實例和NSString實例能夠經過as進行轉化。
Swift 5.0 中,String 依然缺乏不少NSString 中所擁有的功能。String 受到了編譯器的特殊對待,引入 Foundation 後,NSString 的成員就均可以在 String 實例上進行訪問了。
兩個庫有一些重疊的特性,有時候會有兩個名字徹底不一樣的 API,可是它們作的事情卻幾乎同樣。
標準庫中的 split
方法和 Foundation 裏的components(separatedBy:)
標準庫是圍繞布爾值
來設計斷言的,Foundation 使用ComparisonResult
來表示比較斷言的結果。
assert
let valueId = "666"
assert(valueId.isEmpty == true) // crash
複製代碼
ComparisonResult
let result = valueId.compare("777")
print(result.rawValue) // -1
複製代碼
enumerateSubstrings(in:options:_:)
這個使用字符串和範圍來對輸入字符串按照字位簇、單詞、句子或者段落進行迭代的超級強力的方法,在 Swift 中對應的 API 使用的是子字符串
let sentence = """
The quick brown fox jumped
over the lazy dog.
"""
var words: [String] = []
sentence.enumerateSubstrings(in: sentence.startIndex..., options: .byLines) { (word, range, _, _) in
guard let word = word else { return }
words.append(word)
}
print(words)//["The quick brown fox jumped", "over the lazy dog."]
複製代碼
Swift 字符串在內存中的原生編碼是 UTF-8
,NSString 是 UTF-16
,會致使Swift 字符串橋接到 NSString 時會有一些額外的性能開銷。好比enumerateSubstrings(in:options:using:)
中傳遞NSString
會比傳遞String
快。由於NSString在以 UTF-16 計算的偏移上移動位置消耗的是常量時間
,而在String上是一個花費線性時間的操做。
原生的 NSString API 對於 Swift 字符串來講,是使用起來最方便的 API。由於編譯器爲你完成了大部分的橋接工做。
其餘不少Foundation中處理字符串的API,由於 Apple尚未爲它們創造特殊的 Swift 封裝層,使用起來就有點不友好了。好比NSAttributedString
//爲字符串中"Click here"添加了一個連接
let text = "👉 Click here for more info."
let linkTarget = URL(string: "https://www.youtube.com/watch?v=DLzxrzFCyOs")!
// 儘管使用了 `let`,對象依然是可變的 (引用語義)
let formatted = NSMutableAttributedString(string: text)
// 修改文本的部分屬性
if let linkRange = formatted.string.range(of: "Click here") {
// 將 Swift 範圍轉換爲 NSRange
// 注意範圍的起始值爲 3,由於文本前面的顏文字沒法在單個 UTF-16 編碼單元中被表示
let nsRange = NSRange(linkRange, in: formatted.string) // {3, 10}
// 添加屬性
formatted.addAttribute(.link, value: linkTarget, range: nsRange)
}
複製代碼
//經過特定的字符位置,來查詢屬性字符串中的格式屬性
// 查詢單詞 "here" 開始的屬性
if let queryRange = formatted.string.range(of: "here") {
// 把 Swift range 轉換成 NSRange
let nsRange = NSRange(queryRange, in: formatted.string)
// 準備用來接收屬性影響範圍的 NSRange 變量
var attributesRange = NSRange()
// 執行查詢
let attributes = formatted.attributes(at: nsRange.location, effectiveRange: &attributesRange)
attributesRange // {3, 10}
// 把 NSRange 再變回 Range<String.Index>
if let effectiveRange = Range(attributesRange, in: formatted.string) {
// 被查詢到的屬性包圍的子字符串
formatted.string[effectiveRange] // Click here
}
}
複製代碼
這樣的代碼距離真正的 Swift 慣用寫法,還相去甚遠。
沒法遍歷字符範圍
let lowercaseLetters = ("a" as Character)..."z"
//ClosedRange<Character>
for c in lowercaseLetters { // 錯誤
...
}
//這裏將 「a」 轉換爲 Character 是必要的,不然字符串字面量的默認類型將是 String
//Character並無實現Strideable協議,而只有實現了這個協議的範圍纔是可數的集合
複製代碼
對於一個字符範圍,惟一可以進行的操做是將它和其餘字符進行比較。
let lowercaseLetters = ("a" as Character)..."z"
lowercaseLetters.contains("A") // false
lowercaseLetters.contains("é") // false
複製代碼
對 Unicode.Scalar 類型來講,當你保持在 ASCII 或者其餘一些有很好排序的 Unicode類別的子集時,可數範圍的概念就有意義了。Unicode 標量的順序是經過它們的代碼點的值進行定義的,因此在兩個邊界之間,必定存在的有限個數的標量。
extension Unicode.Scalar: Strideable {
public typealias Stride = Int
public func distance(to other: Unicode.Scalar) -> Int {
return Int(other.value) - Int(self.value)
}
public func advanced(by n: Int) -> Unicode.Scalar {
return Unicode.Scalar(UInt32(Int(value) + n))!
}
}
複製代碼
//經過它建立一個可數的 Unicode 標量範圍,生成一個字符數組
let lowercase = ("a" as Unicode.Scalar)..."z"
for c in lowercase {} //不報錯了
print(Array(lowercase.map(Character.init)))
/*
["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n",
"o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
*/
複製代碼
是個Foundation 類型,這個結構體實際上應該被叫作 UnicodeScalarSet,由於它確實就是一個表示一系列 Unicode 標量的數據結構體,徹底和 Character 類型不兼容。
let favoriteEmoji = CharacterSet("👩🚒👨🎤".unicodeScalars)
favoriteEmoji.contains("🚒") // true
//由於女消防員的顏文字其實是女人 + ZWJ + 消防車的組合
複製代碼
在 Swift 5 裏,再也不須要Foundation 中的類型來測試一個標量是否屬於某個官方的Unicode分類
了,如今只要直接訪問 Unicode.Scalar 中的某個屬性就行了,例如:isEmoji 或者 isWhiteSpace。爲了不在Unicode.Scalar 中塞入過多的成員,全部 Unicode 屬性都放在了 properties 這個名字空間裏。
("😀" as Unicode.Scalar).properties.isEmoji // true
("∬" as Unicode.Scalar).properties.isMath // true
複製代碼
如今列出字符串中每個標量的編碼點、名稱和通常分類只須要對字符串作一點格式化就好了
"I’m a 👩🏽🚒.".unicodeScalars.map { scalar -> String in
let codePoint = "U+\(String(scalar.value, radix: 16, uppercase: true))"
let name = scalar.properties.name ?? "(no name)"
return "\(codePoint): \(name) – \(scalar.properties.generalCategory)"
}.joined(separator: "\n")
/*
U+49: LATIN CAPITAL LETTER I – uppercaseLetter
U+2019: RIGHT SINGLE QUOTATION MARK – finalPunctuation
U+6D: LATIN SMALL LETTER M – lowercaseLetter
U+20: SPACE – spaceSeparator
U+61: LATIN SMALL LETTER A – lowercaseLetter
U+20: SPACE – spaceSeparator
U+1F469: WOMAN – otherSymbol
U+1F3FD: EMOJI MODIFIER FITZPATRICK TYPE-4 – modifierSymbol
U+200D: ZERO WIDTH JOINER – format
U+1F692: FIRE ENGINE – otherSymbol
U+2E: FULL STOP – otherPunctuation
*/
複製代碼
Unicode標量的這些屬性很是底層,它們主要是爲表達Unicode中那些不爲人熟知的術語而定義的。若是在更爲經常使用的Character這個層面也提供一些相似的分類。
Character("4").isNumber // true
Character("$").isCurrencySymbol // true
Character("\n").isNewline // true
複製代碼
字符串是寫時複製的。(建立一個字符串的複製
,或者建立一個子字符串
時,全部這些實例都共享
一樣的緩衝區。字符數據只有當與另一個或多個實例共享緩衝區,且某個實例被改變
時,纔會被複制)
Swift 5 裏,Swift 原生字符串 在內存中是用 UTF-8 格式表示的,經過它能夠獲取理論上字符串處理的最佳性能,由於遍歷 UTF-8 視圖要比遍歷 UTF-16 或 Unicode 標量視圖更快。
從 Objective-C 接收到的字符串則是經過一個 NSString 表示的,在這種時候,爲了讓橋接儘量高效,一個基於 NSString 的 String 在被改變時,將會被轉換爲原生的 Swift 字符串。
對於那些小於16個 UTF-8 編碼單元的小型字符串,做爲特別優化,Swift 並不會爲其建立專門的存儲緩衝區。因爲字符串最多隻有 16 字節,這些編碼單元能夠用內連
的方式存儲。
能夠經過實現 ExpressibleByStringLiteral
協議讓你本身的類型支持經過字符串字面量進行初始化。
當使用 SafeHTML 值的時候,咱們能夠確保它表示的字符串中,全部有潛在風險的 HTML 標籤都已經被轉義了,優勢:能夠避免招致一些安全問題。缺點:要在調用這些 API 以前寫不少包裝字符串的代碼。
extension String {
var htmlEscaped: String {
return replacingOccurrences(of: "<", with: "<")
.replacingOccurrences(of: ">", with: ">")
}
}
struct SafeHTML {
private(set) var value: String
init(unsafe html: String) {
self.value = html.htmlEscaped
}
}
let safe: SafeHTML = SafeHTML(unsafe: "<p>Angle brackets in literals are not escaped</p>")
print(safe)//SafeHTML(value: "<p>Angle brackets in literals are not escaped</p>")
複製代碼
SafeHTML 實現 ExpressibleByStringLiteral
,保證安全的同時,免去複雜的代碼處理。
extension SafeHTML: ExpressibleByStringLiteral {
public init(stringLiteral value: StringLiteralType) {
self.value = value
}
}
let safe: SafeHTML = "<p>Angle brackets in literals are not escaped</p>"
print(safe)//SafeHTML(value: "<p>Angle brackets in literals are not escaped</p>")
複製代碼
可讓咱們在字符串字面量
中插入表達式 例如:"a * b = \(a * b)"
Swift 5 則進一步開放了公共 API,能夠支持在構建自定義類型
時使用字符串插值。
let input = ... // 這部分由⽤戶輸⼊,不安全!
let html = "<li>Username: \(input)</li>"
複製代碼
上述代碼,input 中的內容必須被轉義後使用,由於它的來源並不安全。但 html 變量中字面量的分段不該發生變化,由於咱們在這裏就是要寫入帶有 HTML 標籤的值。爲了實現這個邏輯,咱們能夠給SafeHTML 建立一個自定義的字符串插值規則。
Swift 的字符串插值 API 由兩個協議組成:ExpressibleByStringInterpolation
和 StringInterpolationProtocol
。
demo:
final class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let unsafeInput = "<script>alert('Oops!')</script>"
let safe2: SafeHTML = "<li>Username: \(unsafeInput)</li>"
print(safe2)//SafeHTML(value: "<li>Username: <script>alert(\'Oops!\')</script></li>")
let star = "<sup>*</sup>"
let safe3: SafeHTML = "<li>Username\(raw: star): \(unsafeInput)</li>"
print(safe3)
}
}
//MARK: - 字符串插值
extension SafeHTML: ExpressibleByStringInterpolation {
//StringInterpolationProtocol的方法執行完,纔會調用此init方法建立一個對象
public init(stringInterpolation: SafeHTML) {
value = stringInterpolation.value
}
}
extension SafeHTML: StringInterpolationProtocol {
/*插值類型大約須要多少空間存儲全部要合併的字面量,以及指望的插值數量。
若是咱們關注插值操做的性能,仍是要經過這兩個參數告知編譯器關於預留空間的信息*/
init(literalCapacity: Int, interpolationCount: Int) {
value = ""
}
//非插值部分
mutating func appendLiteral(_ literal: String) {
value += (literal)
}
//插值部分
mutating func appendInterpolation<T>(_ x: T) {
value += (String(describing: x).htmlEscaped)
}
}
extension SafeHTML {
//僅僅是擴展appendInterpolation方法
mutating func appendInterpolation<T>(raw x: T) {
self.value += String(describing: x)
}
}
複製代碼
自定義SafeHTML
類型,用print
打印:
struct SafeHTML {
private(set) var value: String
init(unsafe html: String) {
value = html
}
}
let safe: SafeHTML = SafeHTML(unsafe: "<p>Hello, World!</p>")
print(safe) // SafeHTML(value: "<p>Hello, World!</p>")
print(String(describing: safe)) // SafeHTML(value: "<p>Hello, World!</p>")
print(String(reflecting: safe)) // SafeHTML(value: "<p>Hello, World!</p>")
複製代碼
讓SafeHTML
遵循CustomStringConvertible
協議:
extension SafeHTML: CustomStringConvertible {
var description: String {
return value
}
}
let safe: SafeHTML = SafeHTML(unsafe: "<p>Hello, World!</p>")
print(safe) // <p>Hello, World!</p>
print(String(describing: safe)) // <p>Hello, World!</p>
print(String(reflecting: safe)) // <p>Hello, World!</p>
複製代碼
讓SafeHTML
遵循CustomDebugStringConvertible
協議:
extension SafeHTML: CustomDebugStringConvertible {
var debugDescription: String {
return "Debug: \(value)"
}
}
let safe: SafeHTML = SafeHTML(unsafe: "<p>Hello, World!</p>")
print(safe) // Debug: <p>Hello, World!</p>
print(String(describing: safe)) // Debug: <p>Hello, World!</p>
print(String(reflecting: safe)) // Debug: <p>Hello, World!</p>
複製代碼
若是讓SafeHTML
同時遵循上述CustomStringConvertible
和CustomDebugStringConvertible
協議:
let safe: SafeHTML = SafeHTML(unsafe: "<p>Hello, World!</p>")
print(safe) // <p>Hello, World!</p>
print(String(describing: safe)) // <p>Hello, World!</p>
print(String(reflecting: safe)) // Debug: <p>Hello, World!</p>
複製代碼
結論: 但若是你沒有實現 CustomDebugStringConvertible,String(reflecting:) 就會選擇使用CustomStringConvertible 提供的結果,若是你的類型沒有實現CustomStringConvertible,String(describing:) 會選擇使用CustomDebugStringConvertible 提供的結果。
做者的建議:
Array 老是會打印它包含的元素的調試版信息
,即便你把它傳遞給String(describing:)。這是由於數組的普通字符串描述永遠都不該該對用戶呈現
。(由於好比空字符串 "",String.description 會忽略包圍字符串的引號)
let str = ""
print(str) // (啥都沒有)
print(String(describing: str)) // (啥都沒有)
print(String(reflecting: str)) // ""
let array: [String] = ["", "", ""]
print(array) // ["", "", ""]
print(String(describing: array)) // ["", "", ""]
print(String(reflecting: array)) // ["", "", ""]
複製代碼
遵循TextOutputStream
協議的,可做爲輸出目標
標準庫中的 print
和 dump
函數會把文本記錄到標準輸出中。兩個函數的默認實現調用了print(_:to:)
和 dump(_:to:)
。to
參數就是輸出的目標,它能夠是任何實現了 TextOutputStream
協議的類型。
String
是標準庫中惟一的輸出流類型
。
var s = ""
let numbers = [1, 2, 3, 4]
print(numbers, to: &s)
print(s) // [1, 2, 3, 4]
複製代碼
建立本身的輸出流:遵循 TextOutputStream
協議,建立一個接受字符串的變量,並將它寫到流中的 write
方法:
struct ArrayStream: TextOutputStream {
var buffer: [String] = []
mutating func write(_ string: String) {
buffer.append(string)
}
}
var stream = ArrayStream()
print("Hello", to: &stream)
print("World", to: &stream)
print(stream.buffer) // ["", "Hello", "\n", "", "World", "\n"]
//文檔明確容許那些將輸出寫到輸出流的函數在每次寫操做時能夠屢次調用 write(_:),因此出現了""、"\n"等。
複製代碼
擴展 Data
類型,讓它接受流輸入,並輸出 UTF-8 編碼的結果。
extension Data: TextOutputStream {
mutating public func write(_ string: String) {
self.append(contentsOf: string.utf8)
}
}
var utf8Data = Data()
utf8Data.write("café")
print(Array(utf8Data)) // [99, 97, 102, 195, 169]
複製代碼
使用print,將和上述獲得結果同樣:
var utf8Data = Data()
print("café", to: &utf8Data)
print(Array(utf8Data)) // [99, 97, 102, 195, 169]
複製代碼
遵循TextOutputStreamable
協議的,可做爲輸出源
Demo:
struct ReplacingStream: TextOutputStream, TextOutputStreamable {
let toReplace: KeyValuePairs<String, String> //使用KeyValuePairs的目的:不會去掉重複的鍵、不會將全部鍵從新排序。
private var output = ""
init(replacing toReplace: KeyValuePairs<String, String>) {
self.toReplace = toReplace
}
mutating func write(_ string: String) {
let toWrite = toReplace.reduce(string) { partialResult, pair in
partialResult.replacingOccurrences(of: pair.key, with: pair.value)
}
print(toWrite, terminator: "", to: &output)
}
func write<Target>(to target: inout Target) where Target : TextOutputStream {
output.write(to: &target)
}
}
var replacer = ReplacingStream(replacing: ["in the cloud": "on someone else's computer"])
let source = "People find it convenient to store their data in the cloud."
print(source, terminator: "", to: &replacer)
var finalSource = ""
print(replacer, terminator: "", to: &finalSource)
print(finalSource) // People find it convenient to store their data on someone else's computer.」
複製代碼
執行過程:
ReplacingStream
遵循2個協議,因此它可做爲輸出源
、輸出目標
。