《Swift 進階》讀書筆記 - 字符串

字符串

Swift 中的 String 是 Character 值的集合,而 Character 是人類在閱讀文字時所理解的單個字符,這與該字符由多少個 Unicode 標量組成無關。html

Swift裏面的String不支持隨機訪問,不能用相似str[999]來獲取字符串的第一千個字符。算法

當字符擁有可變寬度時,字符串並不知道第 n 個字符到底存儲在哪兒,它必須查看這個字符前面的全部字符,才能最終肯定對象字符的存儲位置,因此這不多是一個O(1)操做swift

Unicode,而非固定寬度

ASCII字符串就是由0到127之間的整數組成的序列。能夠把這種整數放到一個8比特的字節裏。數組

可是8個比特對於許多語言的編碼來講是不夠用的。安全

當固定寬度的編碼空間被用完後,有兩種選擇:markdown

  • 增長寬度(當初被定義成2個字節固定寬度的格式,2個字節不夠用,4個字節又過低效)
  • 切換到可變長的編碼(最終選擇這種可變長格式)

一些Unicode名詞關係

  • Swift裏說的「單個字符」 = Swift裏說的1個Character = 1個字位簇
  • 1個Unicode字符 = 1個字位簇 = 1個或多個Unicode標量
  • 1個Unicode標量 可編碼成 1個或多個編碼單元
  • 1個Unicode標量 大多數狀況下可理解成 1個編碼點

編碼點

  • Unicode中最基礎的原件叫作編碼點,編碼點是一個位於Unicode編碼空間 (從0到0x10FFFF,也就是十進制的 1,114,111) 中的整數。
  • Unicode中的每一個字符或其它語系單位或顏文字(emoji)都有1個惟一的編碼點。
  • 編碼點都會寫成帶有U+前綴的十六進制數,好比歐元符號 -> U+20AC,在Swift裏 -> "\u{20AC}" = "€"

編碼單元

  • Unicode數據,能夠用多種不一樣的編碼方式進行編碼,其中最廣泛使用的是8比特(UTF-8)和16比特(UTF-16)。
  • 編碼方式中使用的最小實體叫作編碼單元,也就是說UTF-8編碼單元的寬度是8比特,而UTF-16編碼單元的寬度是16比特。
  • UTF-8 提供的一個額外的好處就是爲使用8比特的ASCII編碼提供了向後兼容,正是這個特性,才讓 UTF-8接過了ASCII大旗,成爲了現現在Web和文件格式中最爲流行的編碼方式。
  • Swift裏,UTF-8和UTF-16使用的編碼單元的值分別用UInt8和UInt16表示 (它們還有兩個別名,分別是Unicode.UTF8.CodeUnit和Unicode.UTF16.CodeUnit)

字位簇和標準等價

合併標記

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"]] 
*/
複製代碼

結構精簡,無需跟蹤不少內部狀態,僅用一個布爾變量,經過一點額外的工做,咱們還能夠忽略空 行、忽略引號周圍的空格,並支持在字段的引號中經過轉義的方式繼續使用引號。

子字符串(Substring)

以原始字符串內容爲基礎,用不一樣起始和結束位置標記的視圖。

  • 子字符串和原字符串共享文本存儲,好處:對字符串切片成爲了很是高效的操做。
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 並不會致使昂貴的複製操做或者內存申請
複製代碼
  • split(字符串分割),它會返回一個[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>

//整個過程當中沒有發生對輸入字符串的複製
複製代碼
  • split接受閉包做爲參數。
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.
*/
複製代碼
  • split接受含有多個分隔符的序列做爲參數。
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"]
複製代碼

StringProtocol

  • 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")
複製代碼
  • 多數函數接受String類型或者StringProtocol,不多接受Substring類型的,若是須要傳遞 Substring,能夠這麼作:
// 使⽤原字符串開頭索引和結尾索引做爲範圍的⼦字符串 
let substring = sentence[...]
複製代碼
  • Swift不建議將你的全部 API 從接受 String 實例轉換爲遵照 StringProtocol 的類型,建議是堅持使用 String。

    • 泛型自己也會帶來開銷。
    • String會更加簡單和清晰。
    • 用戶在有限的幾個場合對 String 進行轉換,也不會帶來太大的負擔。
  • 若是你想要擴展 String 爲其添加新的功能,將這個擴展放在 StringProtocol 會是一個好主意,這能夠保持 String 和 Substring API 的統一性。StringProtocol 設計之初就是爲了在你想要對String 擴展時來使用的。若是你想要將已有的擴展從 String 移動到 StringProtocol 的話,惟一須要作的改動是將傳入其餘 API 的 self 經過 String(self) 換爲具體的 String 類型實例。

  • 不要聲明任何新的遵照 StringProtocol 協議的類型。只有標準庫中的 String 和 Substring 是有效的適配類型。

編碼單元視圖

有時候Character字符沒法知足須要時,咱們還能夠向下到好比Unicode標量或者編碼單元這樣更低的層次中進行查看和操做。

  • String 爲此提供了三種視圖:unicodeScalarsutf16utf8

  • 爲何你會想要對某個視圖進行訪問和操做?

    • 在一個 UTF-8 編碼的網頁中進行渲染。
    • 和某個只接受某種特定編碼的非 Swift API 進行交互。
    • 你須要字符串某種特定格式下的信息等。
    • 相比於操做完整的字符來講,對編碼單元進行操做會更快一些
  • 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-16UTF-8 編碼單元這個方向上的話,這麼作不會有什麼問題。可是另外一個方向的話就不必定正確了,由於並非每一個編碼單元視圖中的有效索引都會在 Character 的邊界上。
let family = "👨‍👩‍👧‍👦"
let someUTF16Index = String.Index(utf16Offset: 2, in: family)
family[someUTF16Index] //Crash
複製代碼
  • samePosition(in:)輸入的索引在給定的視圖中沒有對應的位置,將返回 nil
let pokemon = "Poke\u{301}mon" // Pokémon
if let accentIndex = pokemon.unicodeScalars.firstIndex(of: "\u{301}") { 
    accentIndex.samePosition(in: pokemon) // nil 
}
複製代碼

字符串和 Foundation

  • 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上是一個花費線性時間的操做。

其餘基於字符串的 Foundation API

  • 原生的 NSString API 對於 Swift 字符串來講,是使用起來最方便的 API。由於編譯器爲你完成了大部分的橋接工做。

  • 其餘不少Foundation中處理字符串的API,由於 Apple尚未爲它們創造特殊的 Swift 封裝層,使用起來就有點不友好了。好比NSAttributedString

    • NSAttributedString(不可變字符串),NSMutableAttributedString(可變字符串),都遵照引用語義。
    • NSAttributedString 的 API 原來接受的是 NSString,可是它如今接受一個Swift.String。不過整個 API 的基礎仍是 NSString 的 UTF-16 編碼單元集合的概念。頻繁地在 String 和 NSString 之間發生橋接可能會帶來意外的性能開銷。
    //爲字符串中"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協議,而只有實現了這個協議的範圍纔是可數的集合
    複製代碼

    image.png

  • 對於一個字符範圍,惟一可以進行的操做是將它和其餘字符進行比較。

    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"]
    */
    複製代碼

CharacterSet

是個Foundation 類型,這個結構體實際上應該被叫作 UnicodeScalarSet,由於它確實就是一個表示一系列 Unicode 標量的數據結構體,徹底和 Character 類型不兼容。

image.png

let favoriteEmoji = CharacterSet("👩‍🚒👨‍🎤".unicodeScalars)
favoriteEmoji.contains("🚒") // true

//由於女消防員的顏文字其實是女人 + ZWJ + 消防車的組合
複製代碼

Unicode 屬性

在 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
複製代碼

String 和 Character 的內部結構

  • 字符串是寫時複製的。(建立一個字符串的複製,或者建立一個子字符串時,全部這些實例都共享一樣的緩衝區。字符數據只有當與另一個或多個實例共享緩衝區,且某個實例被改變時,纔會被複制)

  • 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: "&lt;")
            .replacingOccurrences(of: ">", with: "&gt;")
    }
}

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: "&lt;p&gt;Angle brackets in literals are not escaped&lt;/p&gt;")
複製代碼

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 由兩個協議組成:ExpressibleByStringInterpolationStringInterpolationProtocol

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: &lt;script&gt;alert(\'Oops!\')&lt;/script&gt;</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同時遵循上述CustomStringConvertibleCustomDebugStringConvertible協議:

    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 提供的結果。

    做者的建議:

    • 若是你的自定義類型比簡單,就不必實現 CustomDebugStringConvertible。
    • 若是你的定義類型是個容器,讓它實現CustomDebugStringConvertible 則是一種更爲友好的行爲,經過它,能夠打印容器中每一個元素在調試模式的信息。
    • 當你爲了調試打印結果以後還要作一些特別的處理,也應該經過實現 CustomDebugStringConvertible 完成。
    • 若是你爲description 和debugDescription 提供的結果相同,那麼兩者選其一去實現就行了。
  • 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協議的,可做爲輸出目標

標準庫中的 printdump 函數會把文本記錄到標準輸出中。兩個函數的默認實現調用了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個協議,因此它可做爲輸出源輸出目標
  • print(source, terminator: "", to: &replacer): 將source通過處理後,傳輸給ReplacingStream裏面的output。(ReplacingStream作輸出目標)
  • print(replacer, terminator: "", to: &finalSource):將ReplacingStream中的output傳輸給外部的finalSource。(ReplacingStream作輸出源)
相關文章
相關標籤/搜索