【実務・中級編】DartのRunes型で扱うUnicodeと、文字列処理における注意点 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

コードレビューの現場から:その「文字数カウント」、本当に合っていますか?

テックリードの私だ。君たちが日々何気なく書いているFlutterアプリやWebフロントエンドのコード、そしてバックエンドとのAPI連携において、文字列処理で致命的なバグを踏み抜いていないか?

例えば、ユーザーがプロフィール名に絵文字(👨‍👩‍👧‍👦 や 🚀)を入力したとき、文字数制限バリデーションが突然クラッシュしたり、バックエンド側でサロゲートペアが分断されて文字化けを起こしたことはないだろうか。

JavaScript出身のエンジニアや、表面的なDartのチュートリアルしか見ていない者は、平気でこう書く。

// ⚠️ 現場で絶対にやってはいけないアンチパターン
var name = “👨‍👩‍👧‍👦Dart”;
print(name.length); // 出力: 11 (人間が認識する文字数は「2文字」なのに!)

なぜこうなるのか。Dartの `String` は、内部的に UTF-16コードユニット(Code Unit)の列 として表現されている。
これはJavaScript(V8)やJavaの設計思想を継承したものだが、現代のマルチバイト文字、特にサロゲートペアやゼロ幅接合子(ZWJ)を含む絵文字を扱う場合、この仕様を理解していないとプロダクション環境で致命傷を負う。

今回は、Dartのコアである `Runes` とUnicodeの深層に踏み込み、実務で絶対に破綻しない文字列処理の設計パターンを授けよう。

—

1. Dart VMとStringの裏側:Code Unit vs Code Point

Dartの `String` 型を構成する最小単位は、16ビットの符号なし整数である `int`、すなわち `Code Unit` だ。
BMP(基本多言語面)に収まる文字(通常の英数字や一般的な日本語の漢字など)であれば、1文字は1つのCode Unit(16ビット)で表現され、`string.length` は直感的な文字数と一致する。

しかし、Unicodeのコードポイントが $U+FFFF$ を超える文字(補助多言語面:SMPの文字)は、サロゲートペアと呼ばれる2つのCode Unit(高位サロゲートと低位サロゲート)のペアに分割して格納される。さらに、家族の絵文字(👨‍👩‍👧‍👦)のように複数の絵文字やモディファイアがゼロ幅接合子(ZWJ)で連結されたものは、数個のコードポイントが複雑に絡み合う。

ここで `String.length` や `String[index]` を安易に使うと、サロゲートペアの途中で文字列が分断され、不正なバイト列や表示崩れを引き起こす。

正しい文字の捉え方:RunesとCharacters

Dartで真のUnicode文字(コードポイント単位)を扱うには、`String.runes` を利用して `Runes` オブジェクトを取得する必要がある。さらに、人間が認識する「1文字(書記素クラスター:Grapheme Cluster)」単位で操作したい場合は、パッケージ(Flutterでは標準装備されている)の `characters` 拡張を使用するのが現代のベストプラクティスだ。

—

2. 実践:Runesを活用した堅牢な文字列処理設計

では、実務の現場でどのように `Runes` を扱い、堅牢なバリデーションや切り出し処理を実装すべきか。プロダクションコードを見ていこう。

以下のコードは、サロゲートペアや絵文字を含む文字列を安全に扱い、文字数制限のバリデーションと、安全な切り出し(サロゲート破壊を防ぐ)を行うコンポーネントの設計例だ。

import ‘package:characters/characters.dart’;

/// 堅牢な文字列ユーティリティクラス
class RobustStringUtil {
/// 不正なサロゲートペアの分断を防ぎつつ、安全に文字数を取得する。
/// DartのString.lengthではなく、Unicodeコードポイントベースでカウントする。
static int getTrueLength(String input) {
// runesはUnicodeコードポイントのイテラブルを返す
return input.runes.length;
}

/// 人間が認識する「見た目の文字数(書記素ベース)」を取得する。
/// 家族の絵文字(👨‍👩‍👧‍👦)などを「1文字」として正確にカウントしたい場合に最適。
static int getGraphemeLength(String input) {
return input.characters.length;
}

/// サロゲートペアを考慮した安全な文字列の切り出し(Substring)
/// 従来の string.substring だとサロゲートペアの途中で切れて文字化けするリスクがある。
static String safeSubstring(String input, int start, int end) {
// characters拡張を使うことで、書記素クラスター単位での安全な切り出しが可能
return input.characters.skip(start).take(end – start).toString();
}

/// 文字列内にサロゲートペア(補助多言語面の文字)が含まれているか検証する
static bool containsSupplementaryCharacters(String input) {
for (var rune in input.runes) {
if (rune > 0xFFFF) {
return true; // BMPを超えるコードポイントが存在する
}
}
return false;
}
}

void main() {
// テストケース:通常のASCII、日本語、サロゲートペア、ZWJ絵文字の混在
const String complexText = “Dart言語🚀 👨‍👩‍👧‍👦”;

print(“— 解析結果 —“);
print(“生の length (UTF-16コードユニット数): ${complexText.length}”);
print(“Runesの数 (Unicodeコードポイント数): ${RobustStringUtil.getTrueLength(complexText)}”);
print(“Charactersの数 (人間が認識する文字数): ${RobustStringUtil.getGraphemeLength(complexText)}”);

print(“補助文字(絵文字など)の含有: ${RobustStringUtil.containsSupplementaryCharacters(complexText)}”);

// 安全な切り出しの検証
// 先頭から6文字(見た目の文字単位)を取得する
String sliced = RobustStringUtil.safeSubstring(complexText, 0, 6);
print(“安全に切り出した文字列: $sliced”);
}

このコードのアーキテクチャ上の優位性

1. サロゲートペアの破壊を完全防止:
`String.substring` はUTF-16のインデックスを直接操作するため、絵文字の真ん中でスライスされると、VM上で不正なバイトシーケンス(孤立したサロゲート)が生まれ、UI描画時やAPI送信時に例外や文字化けを誘発する。`characters` や `runes` を経由することで、このリスクをコンパイルレベル・ランタイムレベルで排除している。
2. パフォーマンスへの配慮:
`runes` は遅延評価(Lazy Evaluation)されるイテラブルであるため、巨大なテキストデータを処理する際も、必要最小限のメモリ消費で走査・検証が可能だ。

—

3. パフォーマンスとコンパイル時の注意点

Dart VMやAOTコンパイラ(dart2native)において、文字列操作はメモリ割り当て(Allocation)を伴うため、高頻度で実行されるホットパス(Hot Path)では注意が必要だ。

  • `String` の不変性(Immutability):

Dartの文字列はイミュータブルであるため、ループ内で `+=` 演算子を使って文字列を結合すると、毎回新しいメモリ領域が割り当てられ、GC(ガベージコレクション)の負荷が急増する。大量のテキストを構築する場合は、必ず `StringBuffer` を使用すること。

  • `Runes` 走査のコスト:

`string.runes` は、UTF-16のエンコーディングをオンザフライでUTF-32(Unicodeコードポイント)にデコードしながらイテレートする。頻繁に全文字を走査する処理をUIのビルドメソッド(`build` 内など)に直書きするのは、フレームドロップ(カクつき)の原因となるため厳禁である。重い解析処理はバックグラウンドの `Isolate` へオフロードするか、状態管理層でキャッシュ設計を行うべきだ。

—

チーフアーキテクトからの総括

プログラミング言語のプリミティブな型を舐めてはならない。特に `String` とその裏側にあるエンコーディングの仕様を理解していないエンジニアは、グローバル展開するプロダクトや、リッチなテキストエディタ、チャットアプリといった複雑なフロントエンドを構築する資格はない。

今日から君たちのプロジェクトにおける文字列バリデーションや文字数制限のコードを見直しなさい。`String.length` を安易に使っている箇所があれば、それは技術的負債だ。

型を掌握し、文字の裏側にあるバイトとコードポイントの挙動まで見通すこと。それこそが、一流のDart/Flutterエンジニアへの絶対的な条件なのだ。

タイトルとURLをコピーしました