Swift 中 Substrings 與 String

沒故事的卓同學發表於2018-03-21

原作者 Greg Heo (@gregheo) | Twitter ,原文連結:Swift Substrings

為文字字串新增特性或者語法糖在各種程式語言中都很普遍。就拿大家都很熟悉的 C 語言舉例,C 字串本質是一個字元陣列(characters array),但是每次輸入字串的時候不用輸入 ['h','e','l','l','o'] ,直接打 hello 就可以了,因為這個操作編譯器幫你做了。 更高階的語言比如 Swift 處理字串就不僅僅是當做字元陣列了,String 是一個完整的型別,並且有各種特性。我們先來看一下 String 的一個特性:substrings。

簡單的看一下 Strings

首先粗略的瞭解一下字串的實現。下面的程式碼來自標準庫中 String.swift

public struct String {
  public var _core: _StringCore
}
複製程式碼

當然也有一些其他初始化設定,不過在宣告裡只有這一個儲存屬性!祕密一定都在 StringCore.swift 裡:

public struct _StringCore {
  public var _baseAddress: UnsafeMutableRawPointer?
  var _countAndFlags: UInt
  public var _owner: AnyObject?
}
複製程式碼

在這個型別裡還有很多其他東西,不過我們還是隻關注儲存屬性:

  • Base address — 一個指向內部儲存的指標
  • Count — 字串長度,UInt 型別,在一個 64 位的系統中,意味著有 62(64 - 2) 位的空間可以表示長度。這是一個非常大的數字。所以字串的長度不太可能溢位。
  • Flags — 兩個 bits 用來做標誌。第一位表示是否被 _StringBuffer 持有;第二位表示編碼格式是 ASCII 還是 UTF-16。 _StringCore 的真實情況比這裡提到的要複雜的多,但是通過上面的內容可以讓我們更容易理解字串的一些資訊:字串有一些內部儲存和儲存的大小(underlying storage and size)。

Substrings

Swift 中要怎麼建立一個 substring?最簡單的方式就是通過下標從 string 取一段:

let str = "Hello Swift!"
let slice = str[str.startIndex..<str.index(str.startIndex, offsetBy: 5)]
// "Hello"
複製程式碼

雖然很簡單,但是程式碼看起來不太優雅?。 String 的索引不是直觀的整型,所以擷取時的位置索引需要利用 startIndex 和 index(_:offsetBy:)獲取。如果是從字串開始位置擷取,可以省略掉 startIndex :

let withPartialRange = str[..<str.index(str.startIndex, offsetBy: 5)]
// still "Hello"
複製程式碼

或者用 collection 中的這個方法:

let slice = str.prefix(5)
// still "Hello"
複製程式碼

要記住字串也是 collection ,所以你可以用集合下的方法,比如 prefix(),suffix(), dropFirst() 等。

Substring 的內部原理

substring 一個神奇的地方是他們重用了父 string 的記憶體。你可以把 substring 理解為父 string 的其中一段。

Swift 中 Substrings 與 String

舉個例子,如果從一個 8000 個字元的字串中擷取 100 個字元,並不需要重新初始化 100 個字元的記憶體空間。 這也意味著你可能不小心就把父 string 的生命週期延長了。如果有一大段字串,然後你只是擷取了一小段,只要擷取的小段字串沒有釋放,大段的字串也不會被釋放。 Substring 內部到底是怎麼做到的呢?

public struct Substring {
  internal var _slice: RangeReplaceableBidirectionalSlice<String>
複製程式碼

內部的 _slice 屬性儲存著所有關於父字串的資訊:

// Still inside Substring
internal var _wholeString: String {
  return _slice._base
}
public var startIndex: Index { return _slice.startIndex }
public var endIndex: Index { return _slice.endIndex }
複製程式碼

計算屬性 _wholeString(返回整個父字串),startIndex 和 endIndex 都是通過內部的 _slice 返回。 也可以看出 slice 是如何引用父字串的。

Substring 轉換為 String

最後程式碼裡可能有很多 substring,但是函式的引數型別需要的是 string。Substring 轉換到 string 的過程也很簡單:

let string = String(substring)
複製程式碼

因為 substrings 和它的父字串共享同一個記憶體空間,猜測建立一個新字串應該會初始化一片新的儲存空間。那麼 string 的初始化到底過程是怎樣的呢。

extension String {
  public init(_ substring: Substring) {
    // 1
    let x = substring._wholeString
    // 2
    let start = substring.startIndex
    let end = substring.endIndex
    // 3
    let u16 = x._core[start.encodedOffset..<end.encodedOffset]
    // 4A
    if start.samePosition(in: x.unicodeScalars) != nil
    && end.samePosition(in: x.unicodeScalars) != nil {
      self = String(_StringCore(u16))
    }
    // 4B
    else {
      self = String(decoding: u16, as: UTF16.self)
    }
  }
}
複製程式碼
  1. 建立一個對原有父字串的引用
  2. 獲取 substring 在父字串中的開始和結束位置
  3. 獲取 UTF-16 格式的 substring 內容。_core 是 _StringCore 的一個例項。
  4. 判斷匹配的 unicode 編碼,生成一個新的字串例項 把 substring 轉換成 string 的步驟非常簡單,但是你可能要考慮是不是一需要這樣做。是不是進行 substring 操作的時候都要求型別是 string?如果對 substring 的操作都需要轉成 string,那麼輕量級的 substring 也就失去了意義。?

StringProtocol

StringProtocol 上場!StringProtocol 真是面向協議程式設計的一個優秀代表。StringProtocol 抽象了字串的場景功能,比如 uppercased(), lowercased(),還有 comparablecollection 等。String 和 Substring 都宣告瞭 StringProtocol。 也就是說你可以直接使用 == 對 substring 和 string 進行判等,不需要型別轉換:

let helloSwift = "Hello Swift"
let swift = helloSwift[helloSwift.index(helloSwift.startIndex, offsetBy: 6)...]

// comparing a substring to a string ?
swift == "Swift"  // true
複製程式碼

也可以遍歷 substring,或者從 substring 擷取子字串。 在標準庫裡也有一小部分函式使用 StringProtocol 型別作為引數。比如把一個字串轉換為整型就是:init(text: StringProtocol)。 雖然你可能不關心是 string 和 substring,但是使用 StringProtocol 作為引數型別,呼叫者就不用進行型別轉換,對他們會友好很多。

總結

  • 字串還是那個常見的字串。
  • Substring 是字串的一部分,和父字串共享同一塊記憶體空間,並且記錄了自己的開始和結束位置。
  • String 和 Substring 都宣告實現了 StringProtocol。StringProtocol 包含了一個字串的基本屬性和功能。
    Swift 中 Substrings 與 String

是不是覺得自己也可以自定義字串型別,實現 StringProtocol ?

/// Do not declare new conformances to `StringProtocol`. Only the `String` and
/// `Substring` types in the standard library are valid conforming types.
public protocol StringProtocol
複製程式碼

但是蘋果爸爸表示了拒絕。


相關文章