[Swift] string.data(using: .utf8)ってnilになるの？

# 結論

**絶対になりません。**
※ただしSwift 5以降に限る。


# 説明

## `data(using:)`ってどんなメソッド？

### シグネチャ

`StringProtocol`で規定されている`data(using:allowLossyConversion:)`というメソッドに由来します[^StringProtocol-data]。実際には、

```Swift
func data(using encoding: String.Encoding, allowLossyConversion: Bool = false) -> Data?
```

というように`allowLossyConversion`にデフォルト引数が設定されているため、そちらは省略できます。即ち、`string.data(using: .utf8)`は`string.data(using: .utf8, allowLossyConversion: false)`のことです。

ちなみに`Data`は`Foundation`で定義されている型なので、実際のコードには`import Foundation`が必要です。

[^StringProtocol-data]: https://developer.apple.com/documentation/swift/stringprotocol/3126754-data


### 返り値

上記で見た通り返り値の型は`Data?`とオプショナル型になっています。メソッドは、指定したエンコーディングでエンコードされたデータを返すことになりますが、変換できない文字が含まれていると`nil`となります。
たとえば、[最近流行の(?)絵文字"💩"](https://qiita.com/odanny/items/e6ec88c11d5ff6be6828)をUS-ASCIIにしようとすると`nil`が返ってきます：


```Swift
import Foundation

let string: String = "💩"
let data: Data? = string.data(using: .ascii)

print(data.debugDescription) // -> nil
```


## `String`は内部でどのように文字列データを保持しているか？

実はここが大事なところです。
Swift 4までは基本的にUTF-16でデータが保持されていました。これは、Swiftが当初からObjective-CのFoundationをそのまま活用しようとしていたことから来ています。歴史の長い`NSString`は原則UTF-16で文字列データを保持していました[^NSString]。当然`NSString`と`Swift.String`を"toll-free"で相互変換することを考えるとUTF-16を内部データ型に選択するのは当然だったでしょう。

[^NSString]: 実際には`NSString`は抽象クラスとして定義され、`NSConcreteString`などのサブクラスが文字列操作を担当するという、いわゆる「クラスクラスタ」を形成しています。

しかし、世の中どの程度UTF-16が使われているでしょうか？ほとんどUTF-8が使われているのではないでしょうか。W3Techs - World Wide Web Technology Surveysによれば、今やWebサイトのうち95%以上がUTF-8だそうです[^web-site-encoding]。この統計はWebサイトだけですが、それ以外の分野でも似たようなものではないでしょうか。

[^web-site-encoding]: 2020年10月現在: https://w3techs.com/technologies/overview/character_encoding

そうなると、内部データをUTF-16で保持していても、実際にデータを読み込んだりデータとして出力したりする場合、UTF-8⇄UTF-16の変換コストを伴うことがほとんどということになります。
であれば、最初から内部データはUTF-8として保持しておくほうがいい、となるのは自然なことでしょう。

というわけで、Swift 5からは`String`の内部データがUTF-8になったのでした。詳細は https://swift.org/blog/utf8-string/ に書かれています。内部のUTF-8については`var utf8: Self.UTF8View { get }`を通してアクセス(read-only)できます。


## UTF-8のデータをそのまま`Data`にできないか？

できます。
`UTF8View`は`Collection`プロトコルに準拠しており`Element`は`UInt8`なので、`let data = Data(string.utf8)`とするだけで、UTF-8でエンコードされた文字列のデータを得られます。
ここで注目すべきは`Data(string.utf8)`は必ず成功するということです(∵failable initializerではない)。
つまり、次の例で言うと

```Swift
let data_1: Data? = string.data(using: .utf8)
let data_2: Data = Data(string.utf8)
```

`data_1`はオプショナル型ですが、`data_2`は必ず`Data`型です。
では、常に`Data(string.utf8)`を使えば良いのではないか、と思いませんか。

正解です。


## `data(using:)`の実装を見てみよう

Swiftはオープンソースなので、どのように実装しているか見放題です。"NSStringAPI.swift"に実装を見ることができます。該当箇所[^data-implementation]を引用してみます：

[^data-implementation]: https://github.com/apple/swift/blob/a353176e1eb570a56809cf4202f5f30aa8905840/stdlib/public/Darwin/Foundation/NSStringAPI.swift#L791-L811

```Swift:NSStringAPI.swift
public func data(
  using encoding: String.Encoding,
  allowLossyConversion: Bool = false
) -> Data? {
  switch encoding {
  case .utf8:
    return Data(self.utf8)
  default:
    return _ns.data(
      using: encoding.rawValue,
      allowLossyConversion: allowLossyConversion)
  }
}
```

なんと！
`encoding`が`.utf8`のときは`Data(self.utf8)`をそのまま返しています(ちなみに、Gitで変更履歴を辿ってもらえるとわかるかと思いますが、Swift 4以前はどんな`encoding`であろうと`NSString`にデータへの変換をお願いする形になっていました)。
これで答えが出ましたね。


# 最後にもう一度結論

Q. `string.data(using: .utf8)`って`nil`になるの？
A. **絶対になりません。** ※ただしSwift 5以降に限る。


# おまけ(注意点)

`var utf8: Self.UTF8View { get }`は内部にUTF-8として不正なバイト列を持ってしまっていても、そのままそのバイト列を返してきます。従って、Swift 4では`nil`になっていた`string.data(using: .utf8)`がSwift 5では`nil`にならないということも起き得ます。