Edited at

Swiftにおける「文字」に関する型がとても多い件について

More than 3 years have passed since last update.


三行まとめ


  • Swiftの文字型には、Character / UnicodeScalar / UInt8 / UInt16 / CChar がある

  • 第一の文字型であるCharacterは他の表現では複数の値になる可能性がある

  • それぞれの型は暗黙の変換は原則としてできないし、明示的な変換も注意が必要


解説

$ xcrun swift --version

Swift version 1.0 (swift-600.0.51.3)
Target: x86_64-apple-darwin13.3.0

SwiftのStringはよく抽象化されていていいと思うのですが、それだけに概念がとても複雑なのでちゃんと理解しておく必要があります。

まずSwiftの「文字」の構成に関わる型は5つあります。それぞれ簡単に説明します。


  • Character

  • UnicodeScalar

  • UInt8

  • UInt16

  • CChar


Character

CharacterはStringの第一の構成要素で、 String#generate() はCharacterを扱います。つまり、Stringをfor-in loopで列挙するとCharacterごとに処理が分割されます。

CharacterはSwiftにおける「一文字」を表現する型ですが、後述するようにCharacterだけが文字の表現ではありません。StringをCharacter以外のデータ列としてアクセスするメソッドもあります。

func countSequence<T: SequenceType>(s: T) -> Int {

var count = 0
for _ in s {
count++
}
return count
}

// U+29E3Dは𩸽(ホッケ)。実際にソースコードでは、文字列リテラルのなかに直接書ける。

println(countSequence("\u{29E3D}")) // => 1
for c in "\u{29E3D}"{
println(c) // 𩸽
}

なお、String#substringFromIndex() の引数として、ランダムアクセス可能なIndex(つまりInt)をとることができず、 String.Index しか受け付けられません。Character以外の文字のデータ列もランダムアクセスできないため、スライスを得るのは若干面倒です。

let s = "Hello, world!"

// s[s.startIndex + 7 ..< s.endIndex] // compile error!
s[advance(s.startIndex, 7) ..< s.endIndex]


UnicodeScalar

UnicodeScalarは21bitの数値による表現で、単一のユニコード文字を表します。一方、Characterは任意の数のUnicodeScalarからなる合成文字でも一つのCharacterです。言い換えれば、 Characterは単一のユニコード文字とは限らないので注意してください。

たとえば、「か」+「U+29E3D (濁点)」の結合文字は、Characterでは1文字、UnicodeScalarでは2文字にカウントされます。

文字をUnicodeScalarごとに列挙するには、String#unicodeScalars を使います。

println(countSequence("か\u{3099}"))                // => 1

println(countSequence("か\u{3099}".unicodeScalars)) // => 2


UInt8

UTF8でエンコードした1文字で、 Stirng#utf8 で列挙したときの要素の型です。

println(countSequence("か\u{3099}".utf8)) // => 2

println(countSequence("\u{29E3D}.utf8")) // => 4


UInt16

UTF16でエンコードされた1文字で、String#utf16 で列挙したときの要素の型です。U+29E3Dのカウントが2になるのはサロゲートペアのためです。

println(countSequence("か\u{3099}".utf16)) // => 2

println(countSequence("\u{29E3D}".utf16)) // => 2


CChar

Cのcharに対応する型です。pure Swiftなコードで使うことは少ないのですが、 String.fromCString(UnsafePointer<CChar>) で Cの関数から得る const char* からSwiftの文字列に変換したり、 String#cStringUsingEncoding(NSStringEncoding)[CChar] に変換したりできます。


それぞれの値の相互変換

Swiftの型システムは非常に厳密で、原則として暗黙の型変換を認めません。

ただし、リテラルの場合のみ、情報量を落とさないかぎりにおいて暗黙の型変換が認められるケースがあります。

let c: Character = "a" // OK

let u: UnicodeScalar = "a" // OK

情報量が同じ型でも、たとえばCharとUInt8の変換は簡単ではありません。同じビット表現でも、値が変化する型変換は実行時例外(クラッシュ)になるからです。

let copyrightSign: UInt8 = 0xA9 // © (copyright sign) 

CChar(copyrightSign) // crash!

あまりいい方法をまだ見つけられていないのですが、以下のように泥臭い方法でCと同じ意味でのキャストができます(※ もっといい方法があります。コメント欄参照のこと)。

// typealias Byte = UInt8 がデフォルトで宣言されている

func byte2cchar(b: Byte) -> CChar {
if b < 0x80 {
return CChar(b)
} else {
return -0x80 + CChar(b & ~Byte(0x80))
}
}

byte2cchar(copyrightSign) // => -87

byte2ccha()String#utf8 でバラしたバイト列を String.fromCString() で文字列にしなおすときに必要になるかもしれません。このあたりはまだ調べているところで、もしかしたらもっといい方法があるかもしれないので鵜呑みにしないでください。

なお、 UnsafePointer<T> であれば、 unsafeBitCast()ポインタ型の再解釈 ができます。


参考文献


  • “Unicode Representations of Strings” - Excerpt From: Apple Inc. “The Swift Programming Language.” iBooks. https://itun.es/jp/jEUH0.l