こんにちは!FlutterやDartでの開発を楽しんでいますか?
今回は、Dartの文字列処理の裏側にある「文字コード」の仕組み、そして絵文字などを安全に扱うために欠かせない`Runes`型のお話です。
他のプログラミング言語からDartに入った方だと、「あれ、文字数がなんかズレるぞ?」「絵文字を途中でぶった切っちゃった……」なんて現象に直面したことが一度はあるはずです。
ここをクリアすれば、Dartの文字列ハンドリングはもうバッチリマスターできますよ。
それでは、Dart VMのメモリ上の動きまでイメージしながら、一緒に本質を紐解いていきましょう!
—
1. なぜ文字列でハマるのか?Dartの文字列は「UTF-16」であるという事実
まずは、Dartの内部構造からお話ししますね。
Dartの `String` 型は、UTF-16というエンコーディング方式でメモリ上に文字を保持しています。
UTF-16の世界では、基本的な文字(アルファベットや一般的な漢字など)は16ビット(2バイト)という「1つの箱(コードユニット)」で表現されます。これをDartでは `codeUnit` と呼びます。
しかし、世の中には数万字を超える漢字や、みんな大好き❤️や🚀といった絵文字が存在しますよね。これらは16ビットの箱1つには入りきりません。
そのため、UTF-16では「16ビットの箱を2つ使って1つの文字を表現する」という仕組みを使っています。これをサロゲートペア(代用対)と呼びます。
図解:メモリ上のイメージ
一般的な文字(例: `’A’`)
[ 0x0041 ] (2バイト / 1コードユニット)
絵文字(例: `’🚀’`)
[ 0xD83D ][ 0x8080 ] (計4バイト / 2つのコードユニットのペア)
この仕様を知らないと、次のような恐ろしいバグを踏んでしまいます。
void main() {
String rocket = ‘🚀’;
// .length は「コードユニットの数」を数えているだけ!
print(rocket.length); // 2 (えっ、1文字なのに!?)
}
そうなんです。`String.length` は文字の数(グラフェemeクラスタ数)ではなく、UTF-16のコードユニットの数を返します。そのため、絵文字を扱うと文字数が意図せず倍になってカウントされてしまうんですね。
—
2. 救世主登場!すべてのUnicodeを正しく扱う「Runes」
この問題を鮮やかに解決するのが、今回主役の`Runes`です。
Dartの `Runes` は、文字列をUTF-16のコードユニットではなく、Unicodeコードポイント(Rune)のシーケンスとして扱わせてくれます。Unicodeコードポイントとは、世界中のすべての文字に割り振られた「一意の通し番号(整数値)」のことです。
Runesの基本的な使い方
文字列から `Runes` を取得するには、`.runes` プロパティを使います。実際にコードを見てみましょう。
void main() {
String text = ‘Dart🚀’;
// runesを取得
Runes runes = text.runes;
print(‘— コードポイントの整数値 —‘);
for (var rune in runes) {
// 16進数のUnicode表現で出力してみます
print(‘U+${rune.toRadixString(16).toUpperCase()}’);
}
}
実行結果:
— コードポイントの整数値 —
U+44
U+61
U+72
U+74
U+1F680 <-- 🚀のUnicodeコードポイント(1つの整数として扱われている!)
`'🚀'` が、サロゲートペアに分解されることなく、ひとつの `0x1F680` という整数値として扱われているのがわかりますね。これが `Runes` の真価です。
---
3. 実践:Runesを使って「絵文字の文字数」や「切り出し」を正しく行う
実務では、「絵文字を含んだユーザー名文字数を正確に数えたい」「変なところで文字列をスライスしたくない」という場面によく遭遇します。
`Runes` を使った安全な文字列操作のパターンを覗いてみましょう。
① 絵文字を含めた正しい文字数を数える
void main() {
String message = ‘Hello 🚀 Dart!’;
// String.length だと絵文字を2文字としてカウントしてしまう
print(‘String.length: ${message.length}’); // 14
// Runesの長さを測れば、人間の目で見た「文字数」と一致する
print(‘Runes length: ${message.runes.length}’); // 13
}
② 数値(コードポイント)から文字列へ復元する
`Runes` を使って文字コードのリストをいじったあと、再び人間の読める文字列に戻すには、`String.fromCharCodes()` を使います。
void main() {
// 宇宙を表すUnicodeコードポイントのリスト
// 🚀 (U+1F680), 🌍 (U+1F30D), ✨ (U+2728)
List
// コードポイントから文字列を生成
String result = String.fromCharCodes(spaceEmojis);
print(result); // 🚀🌍✨
}
このメソッドを知っていれば、例えば「特殊なエンコード・デコード処理」や「カスタムの文字フィルター」を実装する際にも、文字化けを恐れずにロジックを組むことができます。
—
4. 陥りやすい文法エラーと注意点
ここで、現場でやりがちな「落とし穴」についても触れておきますね。
⚠️ 注意点1: `String` のインデックスアクセスとの混同
`String` 型に対して `[index]` でアクセスすると、それは文字単位ではなくUTF-16のコードユニット単位アクセスになります。
void main() {
String text = ‘🚀’;
// 危険:サロゲートペアの片割れだけを取り出してしまう可能性がある
print(text[0]); // 文字化け(あるいは不正な上位サロゲート単体)が出る
}
絵文字や特殊な多言語文字(拡張漢字など)が含まれる可能性がある文字列に対しては、安易に `[i]` でループを回したり部分文字列(`substring`)を作ったりしないよう注意してください。
もし安全に文字を走査したい場合は、`characters` パッケージ(Flutter SDKやDartのパッケージで利用可能)が提供する `Characters` クラス(Grapheme Clusterを意識したモダンなAPI)を使うのが現代のベストプラクティスですが、その基礎を支えているのが今回学んだ `Runes` です。
—
まとめ
いかがでしたでしょうか? 今回のポイントをサクッと整理しておきましょう。
1. Dartの文字列 (`String`) は UTF-16 でエンコードされている。
2. 絵文字などは16ビットの箱を2つ使う「サロゲートペア」になるため、`String.length` や `[index]` が意図通りに動かないことがある。
3. `Runes` を使えば、Unicodeコードポイント単位(人間が認識する文字単位に近い形)で安全にデータを扱える。
4. コードポイントから文字列に戻すときは `String.fromCharCodes()` が便利。
文字列の裏側の仕組み(メモリや文字コード)まで理解しているプログラマは、それだけで一目置かれる存在になります。
ここをクリアできれば、グローバル対応のアプリケーション開発も怖くありませんよ。
明日からのDartコーディングに、ぜひこの知識を活かしてみてくださいね!