CharacterSet
はCharacter
の set ではない
「そんなの知ってる」と思った貴方は基本的にこの記事を読む必要はありません。一方で「それってどういう意味?」と思った方がいらっしゃれば、ここではCharacterSet
周りでバグを作らないために知っておいた方が良いだろう内容を書いておきますので、多少はお役に立てるかもしれません。
CharacterSet
は将来的に本当にCharacter
のsetになる可能性はあるので、その時のために備えておきましょうという意味もあります。
おさらい: Unicode.Scalar
とCharacter
の違い
Swiftが登場した当初、String
を扱おうとした猛者たちの阿鼻叫喚が多数聞かれたかと思いますが、それもSwiftのバージョンが4.0となった今はだいぶ落ち着いたことでしょう。それでも一応はUnicode.Scalar
とCharacter
の違いをおさらいしておきましょう。ただ、そもそも、Unicode自体を知っていれば理解することは特に難しいことではありません。
Unicode.Scalar
その名の通りUnicode.Scalar
は「ユニコードスカラ値」を示すstruct
です。ユニコードスカラ値とは、~~Wikipediaをみてください~~Unicodeが文字または文字の部品に割り当てた数値のことで、0から1114111までの1114112個あります。たとえばアルファベットの"A"はU+0041(十進数で65)です。
ただ、上にも書いた通り、スカラ値は文字だけでなく"文字の部品"にも割り当てられているというところに注意が必要です。
ひらがなの「が」を考えてみましょう。UnicodeにあるHIRAGANA LETTER GA
にはU+304C
が割り当てられています。一方で、「が」は「か」に「゛(濁点)」がついたものでもあります。「か(HIRAGANA LETTER KA
)」にはU+304B
、「゛(COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK
)」はU+3099
が割り当てられており1、これらを並べたものU+304B U+3099
も「が」なのです。すなわち、ユニコードスカラ値を複数並べて一文字を表すということもあるのです。そして、U+304C
とU+304B U+3099
はどちらも「が」なので同じであるとみなしてほしいと思うのが人の子でしょう。そこで登場するのがCharacter
なのです。
Character
上で説明した通り、ユニコードスカラ値が1個であろうと2個であろうと5個であろうと、それが一文字を表すのであれば一文字として扱いたいですよね?そのためのstruct
がCharacter
というわけです。Character
では、その文字がいくつのユニコードスカラ値から成り立っているかは関係なく、同じ文字を表しているならば同じとみなされます:
let ga: Character = "\u{304C}"
let kaAndVoicedSoundMark: Character = "\u{304B}\u{3099}"
print(ga == kaAndVoicedSoundMark) // Prints "true"
CharacterSet
という名前は勘違いさせやすい
CharacterSet
という名前はあたかもCharacter
の集合を表していそうですね。しかも、init(charactersIn string: String)
なんていうイニシャライザもあります:
import Foundation
let aToZ = CharacterSet(charactersIn:"ABCDEFGHIJKLMNOPQRSTUVWXYZ")
print(aToZ.contains("A")) // Prints "true"
print(aToZ.contains("!")) // Prints "false"
このコードでは、CharacterSet
がCharacter
の集合として振る舞っているようにもみえます。
では、さっきの「が」を使った次のコード例をみてみましょう。
import Foundation
let gaSet = CharacterSet(charactersIn:"\u{304B}\u{3099}")
print(gaSet.contains("\u{304C}")) // Prints what...?
"\u{304B}\u{3099}"
は上述した通り「か」+「゛」です。"\u{304C}"
は「が」です。Character
の項でも示した通り、これらは同一とみなされます。即ち、CharacterSet
がCharacter
の集合であれば、結果は"true"
となるはずです。しかし、このコードを実行して表示されるのは"false"
です。同じとみなされる文字が含まれているはずなのに、なぜ"false"
なのでしょうか。
実はCharacterSet
の正体はUnicodeScalarSet
だった
CharacterSet
は歴史的な経緯からそういう名前になっているだけで、実は中身はユニコードスカラ値の集合なのです。
CharacterSet
の別のイニシャライザやメソッドを見てみるとそれが明らかです。
convenience init<S>(_ sequence: S) where S : Sequence, Unicode.Scalar == S.Element
convenience init(arrayLiteral: Unicode.Scalar...)
init(charactersIn range: Range<Unicode.Scalar>)
init(charactersIn range: ClosedRange<Unicode.Scalar>)
func contains(_ member: Unicode.Scalar) -> Bool
@discardableResult mutating func insert(_ character: Unicode.Scalar) -> (inserted: Bool, memberAfterInsert: Unicode.Scalar)
mutating func insert(charactersIn range: ClosedRange<Unicode.Scalar>)
mutating func insert(charactersIn range: Range<Unicode.Scalar>)
@discardableResult mutating func remove(_ character: Unicode.Scalar) -> Unicode.Scalar?
mutating func remove(charactersIn range: ClosedRange<Unicode.Scalar>)
mutating func remove(charactersIn range: Range<Unicode.Scalar>)
@discardableResult mutating func update(with character: Unicode.Scalar) -> Unicode.Scalar?
うわぁ…。引数がUnicode.Scalar
のメソッドはいっぱいあっても、Character
が引数になっているメソッドは一切ありません。
ちなみに、var bitmapRepresentation: Data { get }
というプロパティもありますが、それが表すのはCharacterSet
に含まれる**ユニコードスカラ値を表すData
**で、先頭からnビット目が1であれば、ユニコードスカラ値 == nが当該CharacterSet
に含まれるということになります。
さて、以上を踏まえて、上の"がセット.swift"を見返して見ましょう。
まずlet gaSet = CharacterSet(charactersIn:"\u{304B}\u{3099}")
で作られるCharacterSet
は「が」というCharacter
を持つ集合ではなく、U+304B
とU+3099
という2つのUnicode.Scalar
を含むことを表す集合です。
そして、次のgaSet.contains("\u{304C}")
ですが、ここでの"\u{304C}"
はCharacter
ではなくUnicode.Scalar
です^ ExpressibleByStringLiteral。即ち、gaSet.contains("\u{304C}")
はU+304C
というユニコードスカラ値がgaSet
に含まれているかどうかを調べているに過ぎません。となると、当然、U+304C
はU+304B
でもU+3099
でもないので、結果は"false"
となります。
一方、gaSet.contains("か")
は"true"
になります。何故かは分かりますよね?
実態に即したコードは…
というわけで、CharacterSet
は本当はUnicodeScalarSet
だった訳です。本来は次のようなコードを書くことができれば勘違いは生まれないはずです:
// Swift 4.0現在 実装されていません。
let gaScalars = UnicodeScalarSet(unicodeScalarsIn:"\u{304B}\u{3099}")
print(gaScalars.contains("\u{304C}")) // Prints "false"
let gaCharacters = CharacterSet(charactersIn:"\u{304B}\u{3099}")
print(gaCharacters.contains("\u{304C}")) // Prints "true"
実は公式でも触れられている
StringManifestoの"Character and CharacterSet"に正にそういった問題が書いてあります(YOCKOWによる拙訳):
その名前にもかかわらず、
CharacterSet
は現在のところSwiftにおけるUnicodeScalar
型に基づいて機能しています。即ち、String
で使えるのはunicode scalar view
を通した時のみです。短期間でこの不調和を解決するのであれば、CharacterSet
をUnicodeScalarSet
にリネームすればいいでしょう。長期的には、拡張書記素クラスタ(訳注: Unicodeにおける"一文字"を表すもので、SwiftでいうCharacter
)のために同様の機能を提供するCharacterSet
を導入するのが適切かもしれません。
おまけ
CharacterSet
が実はUnicode.Scalar
の集合だったということが分かったところで、次のコードを見てください:
let jpFlag: Character = "🇯🇵" // Flag of Japan
let amFlag: Character = "🇦🇲" // Flag of Armenia
let paFlag: Character = "🇵🇦" // Flag of Panama
let japanAndArmenia: String = "\(jpFlag)\(amFlag)"
let paFlagSet = CharacterSet(charactersIn:"\(paFlag)")
let whichFlag = japanAndArmenia.components(separatedBy:paFlagSet).joined()
print(whichFlag) // Prints "🇯🇲" (Flag of Jamaica)
日本とアルメニアとパナマの国旗を文字列操作したらジャマイカの国旗が生まれましたとさ。何故そうなるのか?もうお分かりですね?
-
ちなみに、
U+309B
にも濁点(KATAKANA-HIRAGANA VOICED SOUND MARK)がありますが、これは"濁点という記号を表す文字"なのでU+304B U+309B
と並べても「が」ではなく「か゛」という二文字となります。 ↩