string.Lengthが「正しくない」時
C#において、string型の.Lengthプロパティは、その文字列に含まれるchar(16ビット文字)の数を返します。また、foreach (char c in str)ループは、char単位で文字を処理します。
ほとんどの英数字や基本的な日本語では、このchar(文字)と人間が認識する「1文字」は一致します。しかし、絵文字(例: “🧑🚀”)や一部の特殊な漢字(サロゲートペア)を扱う場合、この前提が崩れます。
C#のcharは16ビット(UTF-16)ですが、”🧑🚀”(宇宙飛行士)のような1つの絵文字(書記素クラスター、Grapheme Cluster)は、複数のchar(この場合は5つのchar)で構成されています。
string astronaut = "🧑🚀";
Console.WriteLine(astronaut.Length); // 5 が出力される
Lengthが5と返ったり、foreach (char c in astronaut)でループしたりすると、絵文字が意図せず破壊されてしまいます。
この記事では、C#でこれらの複雑な文字列を人間が認識する「1文字」単位で正しく扱うためのSystem.Globalization.StringInfoクラスについて解説します。
StringInfoと「テキスト要素」
この問題を解決するのが、System.Globalization名前空間にあるStringInfoクラスです。
StringInfoクラスは、char単位ではなく、「テキスト要素(Text Element)」単位で文字列を解析します。テキスト要素とは、人間が「1文字」として認識する単位(書記素クラスター)のことで、”🧑🚀”のような複雑な絵文字も「1つのテキスト要素」として正しく認識されます。
LengthInTextElements:正しい「文字数」の取得
StringInfoクラスのコンストラクタに文字列を渡すことで、その文字列を解析できます。
LengthInTextElementsプロパティは、string.Lengthとは異なり、テキスト要素の数(=人間が認識する文字数)を返します。
コード例1:文字数の比較
using System;
using System.Globalization; // StringInfo を使用するために必要
public class StringInfoLengthExample
{
public static void Main()
{
// 🧑🚀 (5 chars) + ! (1 char) = 6 chars
string complexString = "Hi 🧑🚀!";
// 1. 従来の string.Length (charの数)
int charCount = complexString.Length;
Console.WriteLine($"string.Length (char数): {charCount}");
// 2. StringInfo を使用 (テキスト要素の数)
var stringInfo = new StringInfo(complexString);
int textElementCount = stringInfo.LengthInTextElements;
Console.WriteLine($"StringInfo.LengthInTextElements (文字数): {textElementCount}");
}
}
出力結果:
string.Length (char数): 7
StringInfo.LengthInTextElements (文字数): 5
(H, i, , 🧑🚀, ! の合計5「文字」として正しくカウントされます。)
GetTextElementEnumerator:正しい「1文字ずつ」の処理
foreach (char c in ...)でループすると絵文字が破壊される問題は、StringInfo.GetTextElementEnumeratorを使用して「テキスト要素」単位で列挙することで解決できます。
ただし、GetTextElementEnumeratorは少し扱いが冗長になるため、一般的には拡張メソッド(Extension Method)としてラップするのが便利です。
コード例2:拡張メソッドによる安全な列挙
以下のTextElementExtensionsクラスは、string型に.AsTextElements()メソッドを追加し、foreachで安全にテキスト要素を列挙できるようにします。
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
// --------------------------------------------------
// 拡張メソッドの定義 (プロジェクトに1回定義する)
// --------------------------------------------------
public static class TextElementExtensions
{
/// <summary>
/// 文字列を「テキスト要素」(サロゲートペアや絵文字を考慮した単位)
/// ごとに列挙する IEnumerable<string> を返します。
/// </summary>
public static IEnumerable<string> AsTextElements(this string inputString)
{
if (string.IsNullOrEmpty(inputString))
{
yield break; // 空なら何もしない
}
// テキスト要素を列挙する機能を取得
TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(inputString);
// ループ
while (enumerator.MoveNext())
{
// 現在のテキスト要素 (例: "H" や "🧑🚀") を string 型で返す
yield return enumerator.GetTextElement();
}
}
}
// --------------------------------------------------
// 実行クラス
// --------------------------------------------------
public class EnumerateTextElementsExample
{
public static void Main()
{
string complexString = "Hi 🧑🚀!";
Console.WriteLine("--- 従来の foreach (char) ---");
// 絵文字が破壊される
foreach (char c in complexString)
{
Console.WriteLine($" char: {c}");
}
Console.WriteLine("\n--- 拡張メソッド (AsTextElements) ---");
// 拡張メソッドを使って安全に列挙
foreach (string element in complexString.AsTextElements())
{
// "🧑🚀" が1つの要素として正しく処理される
Console.WriteLine($" Element: {element}");
}
}
}
出力結果:
--- 従来の foreach (char) ---
char: H
char: i
char:
char: 🧑
char:
char: 🚀
char: !
(注: コンソール環境により上記の絵文字部分は 2 char または 1 char (ZWJ) に分割されます)
--- 拡張メソッド (AsTextElements) ---
Element: H
Element: i
Element:
Element: 🧑🚀
Element: !
拡張メソッドを使用することで、foreachループがテキスト要素("🧑🚀")を1つの単位として正しく扱えていることがわかります。
まとめ
C#で絵文字や特定の外国語(サロゲートペア)を安全に扱うには、string.Lengthやforeach (char ...)を避け、System.Globalization.StringInfoクラスを使用する必要があります。
- 文字数のカウント:
new StringInfo(str).LengthInTextElementsを使用します。 - 1文字ずつの処理:
StringInfo.GetTextElementEnumeratorを使用します(AsTextElements()のような拡張メソッドにラップするのが実用的です)。
このアプローチにより、C#アプリケーションが国際的な文字セットや絵文字を正しく処理できるようになります。
