Swift Digest
SE-0178 | Swift Evolution

Add unicodeScalars property to Character

Proposal
SE-0178
Authors
Ben Cohen
Review Manager
Ted Kremenek
Status
Implemented (Swift 4.0)

01 何が問題だったのか

Swift の String は Unicode の extended grapheme cluster を単位とするコレクションで、その要素型が Character です。Character は人間が1文字として認識する単位を表しますが、内部的には複数の Unicode scalar(Unicode.Scalar)から構成されることがあります。たとえば "é" は単一のスカラーで表すこともできますし、"e" と結合用アクセント記号の2スカラーで表すこともできます。絵文字の一部や異体字セレクタを伴う文字も、複数スカラーの組み合わせです。

Swift 4.0 より前の Character は、比較・リテラル初期化・String.init の引数として使うくらいしか用途のない、ほぼ中身の見えない型でした。一方で String には unicodeScalars という Unicode scalar 列のビューがあり、個々のスカラーを調べられるようになっています。

このため、文字列を「文字ごとに」走査しつつ各文字を構成するスカラーを検査したい、という自然な処理が書きにくい状態でした。たとえば、任意の空白文字(ASCII の空白だけでなく、改行や Unicode の各種空白を含む)を区切りとして分割したいとき、String レベルでは次のように書けます。

let s = "one two three"
s.split(separator: " ")

しかし、CharacterSet.whitespacesAndNewlines のような Unicode scalar 単位の集合を使って「空白スカラーを含む文字」で分割しようとすると、Character からそのスカラー列に直接アクセスする手段がないため、次のような書き方はできませんでした。

let ws = CharacterSet.whitespacesAndNewlines
s.split { $0.unicodeScalars.contains(where: ws.contains) } // 書けない

ASCII 判定のように「grapheme を構成するスカラーを調べればわかること」の多くも、同じ理由で Character 単体では表現しづらくなっていました。

02 どのように解決されるのか

Character に、その文字を構成する Unicode scalar をたどるための読み取り専用ビュー unicodeScalars プロパティを追加します。String.unicodeScalars に相当するものの、Character 版です。

使い方

CharacterunicodeScalars を使えば、Motivation で挙げられていた「空白文字で分割する」処理が次のように書けるようになります。

import Foundation

let s = "one two\tthree\nfour"
let ws = CharacterSet.whitespacesAndNewlines
let parts = s.split { $0.unicodeScalars.contains(where: ws.contains) }
// parts == ["one", "two", "three", "four"]

ASCII かどうかの判定のように、grapheme を構成するスカラーを見ればわかる性質も、Character 単体で扱えます。

extension Character {
    var isASCII: Bool {
        return unicodeScalars.count == 1 && unicodeScalars.first!.value < 128
    }
}

Character("A").isASCII // true
Character("あ").isASCII // false
Character("\u{65}\u{301}").isASCII // false("é" を "e" + アクセントで構成)

ビューは BidirectionalCollection に適合するので、for ループや countfirst / last、逆順走査などがそのまま使えます。

let c: Character = "\u{1F1EF}\u{1F1F5}" // 🇯🇵(国旗は2つの地域指示子スカラーで構成される)
for scalar in c.unicodeScalars {
    print(String(format: "U+%04X", scalar.value))
}
// U+1F1EF
// U+1F1F5

ビューの性質

Character.UnicodeScalarViewCharacter にネストされた型として定義され、次のような形をしています。

extension Character {
    public struct UnicodeScalarView: BidirectionalCollection {
        public struct Index { /* ... */ }
        public var startIndex: Index { get }
        public var endIndex: Index { get }
        public func index(after i: Index) -> Index
        public func index(before i: Index) -> Index
        public subscript(i: Index) -> UnicodeScalar
    }
    public var unicodeScalars: UnicodeScalarView { get }
}

String.unicodeScalars とは異なり、このビューは 読み取り専用 です。Character はちょうど1つの grapheme を保持するという不変条件があるため、スカラー単位で書き換えられるとこれが壊れかねません。非リテラルな Character を作成・加工したいときは、これまで通り String を経由します。UnicodeScalarView のイニシャライザも internal で、このビューは常に Character から取り出す形で利用します。

また CustomStringConvertible などの利便プロトコルにも適合します。

utf8 / utf16 ビューについて

Characterutf8utf16 のビューを追加することも検討されましたが、本提案のスコープには含まれていません。必要な場合は String の対応するビューを使う前提です。isASCII のような便利プロパティを Character に追加することも、unicodeScalars という土台ができた上で将来追加的に行える範囲とされています(あくまで見通しであり、約束ではありません)。