length プロパティが無い!?
たいていのプログラミング言語の文字列には length
というプロパティやメンバ関数があって文字列の長さを取得できます。ところが驚くことに Swift の文字列には length
プロパティがありません。Objective-C 由来の NSString にだってあるのにこれはどういうことでしょう?
これは真面目に向き合うと、とても複雑な Unicode に Swift が真面目に向き合っていることに起因します。
Unicode
昔々、コンピュータは地域ごとに、酷いとメーカーごとに異なる文字コードを使っていました。これでは地域やメーカーを超えた文章ファイルのやりとりは色々と面倒なことになります。また、欧米の文字は 1 文字 1 バイトなのに対し日本をはじめとした東アジアの文字は 1 文字 2 バイトで表すことが多く文字列処理が煩雑という問題もありました。これを解決すべく 1 文字 2 バイト固定で世界中の全ての文字を表現することができる統一文字コードを作るという理想のもとに策定されたのが Unicode です。
2 バイトあれば 65536 個もの文字を表すことができ、十分に世界中の全ての文字を表せるだろうと考えたわけです。しかし Unicode の改定時にたくさんの文字の追加リクエストが来てすぐに 2 バイトでは足りなくなり、全ての文字を 2 バイト固定で表すという理想は諦めることになりました。現在の Unicode は 1 文字 2 バイトではありません。1 文字 2 バイトだったのは Unicode 1.x だけです。
UTF-16
コンピュータは突き詰めると数値しか扱えません。文字コードとはそんなコンピュータでも文字を扱えるよう文字に番号を割り振っているものという見方もできます。全ての文字の文字番号が 255 以下なら全ての文字を 1 バイトで表すことができ、話は単純です。しかし Unicode はそれよりも多くの文字を扱うので 1 バイトでは表せません。複数のバイトを使って文字を表現しなければなりません。その表現方法の一つが UTF-16 です。後述の UTF-32, UTF-8 もそうです。
オリジナルの Unicode では 2 バイトを 1 単位として文字を扱っていましたが、文字を追加した結果 65535 より大きな文字番号を持つものが出て来ました。これは 2 バイトでは表せません。その場合は 2 単位(4バイト)使って表すことにしたのが UTF-16 です。
UTF-16 は複数のバイトを使って文字番号を表現する方法の一つです。2 バイトを 1 単位として、1 つの文字番号を 1 単位(2 バイト)または 2 単位(4 バイト)で表します。
文字列を UTF-16 で表した時の単位数がいくつになるかを表すのが Swift の String.utf16.count
です。
「猫」という字はオリジナルの Unicode に含まれていたものなので単位数は 1 ですが、後から追加された絵文字の「🐱」は 2 単位です。
"猫".utf16.count // -> 1
"🐱".utf16.count // -> 2
また Objective-C 由来の NSString は UTF-16 で文字列を表すための型なので、その長さは UTF-16 での単位数になります。NSString は UTF-16 の事しか考慮に入れていないので話が単純なわけです。
UTF-32
Unicode はもともと全ての文字を固定長で扱うことも目指していましたが、UTF-16 では 1 つの文字番号が 2 バイト、もしくは 4 バイトで可変長になってしまっています。固定長で扱えるようにするため、4 バイトを 1 単位とし、全ての文字番号を 1 単位で表すのが UTF-32 です。固定長だとプログラムから扱いやすくなりますよね。
UTF-32 は 4 バイトを 1 単位として文字番号を表す方法で、全ての文字番号を 1 単位で表します。
これに対応するのは String.unicodeScalars.count
です。文字列を UTF-32 で表した場合の単位数を返します。
"猫".unicodeScalars.count // -> 1
"🐱".unicodeScalars.count // -> 1
UTF-8
見落としがちですが UTF-16, UTF-32 では英数字も 2 バイト、4 バイトになります。実際のところはほとんど英数字しか扱わないファイルでも UTF-16, UTF-32 で保存したり、送信したりするのはかなり容量の無駄があります。そこで大雑把に言えば、英数字のようなよく使う文字は 1 バイトで、マイナーな文字は複数バイトで扱うようにしたのが UTF-8 です。固定長で扱うことは諦めています。また英数字だけの UTF-8 のバイト列は ASCII コードでのバイト列と等しくなるように設計されており、ASCII コードしか考慮していないプログラムでも処理できるメリットがあります。
UTF-8 は 1 バイトを 1 単位とし、全ての文字番号を 1 から 4 単位で表します。
これに対応するのは String.utf8.count
です。文字列を UTF-8 で表した場合の単位数(=バイト数)を返します。
"猫".utf8.count // -> 3
"🐱".utf8.count // -> 4
見た目の文字数
さて、これまで 1 つの文字番号が 1 つの文字を表すように書いてきましたが、実は複数の文字番号を使って 1 文字を表す場合があります。 例えば「前の文字に濁点をつける文字部品」というものがあります。これを使うと「あ」+「前の文字に濁点をつける文字部品」で「あ゙」を表すことができます。これは見た目は 1 文字ですが、2 つの文字番号から作られています。
"\u{3042}\u{3099}" // -> あ゙ (3042は「あ」、3099は「前の文字に濁点をつける文字部品」の文字番号)
"\u{3042}\u{3099}".unicodeScalars.count // -> 2
この見た目の文字数を表すのが String.characters.count
1 です。
"\u{3042}\u{3099}".characters.count // -> 1
Unicode には複数の文字番号から構成される文字があるため文字番号の個数と見た目の文字数が一致しない場合がある。
この見た目の文字数というのは Apple が適当に決めているわけではなく、Grapheme Cluster として Unicode で規定されています。
結局どれを使えばいいのか?
残念ながら簡単な答えはありません。どのような長さが欲しいのか?を考えて選ばないといけませんが、次が目安になるのではないかと思います。
プロパティ | 値の意味 | 用途 |
---|---|---|
String.utf16.count | UTF-16 で表した場合の単位数 UTF-16 は 1 単位 2 バイトで 1 つの文字番号を 1~2 単位で表す |
NSString#length の値と計算を行う場合 NSString の位置の指定を行う場合 |
String.unicodeScalars.count | UTF-32 で表した場合の単位数 UTF-32 は 1 単位 4 バイトで 1 つの文字番号を 1 単位で表す |
(あまり使わない?) |
String.utf8.count | UTF-8 で表した場合の単位数 UTF-8 は 1 単位 1 バイトで 1 つの文字番号を 1~4 単位で表す |
UTF-8 にシリアライズして保存、送信する場合のバイト数 C 言語の関数に文字列の長さを渡す場合 |
String.characters.count | 見た目の文字数 | UI に表示した文字数を数える場合 |
私は特に文字コードに詳しいわけではないのでおかしいところがあればご指摘ください。
-
実はこれを使っても一部の絵文字("👩👩👧👦")でおかしい場合があり、enumerateSubstrings を使うとうまくいそうですSwift4.0,3.2 では "👩👩👧👦".characters.count も 1 になるようになりました(@takabosoft さん @monoqlo さん、ありがとうございます)。 ↩