文字列

【C#】LengthとStringInfoでわかる文字列の長さ計測とUTF-8バイト数取得の基本

C#で文字列の長さを知りたいときはLengthを確認すれば完了です。

多言語絵文字や異体字などサロゲートペアを含む場合は、new StringInfo(text).LengthInTextElementsで実際の文字数を数えると安全です。

バイト数が必要なときはEncoding.UTF8.GetByteCountを使う方法が定番です。

文字列長を測る必要性とユースケース

プログラミングにおいて文字列の長さを正確に把握することは、さまざまな場面で重要になります。

C#では文字列の長さを取得する方法が複数あり、それぞれの用途に応じて使い分けることが求められます。

ここでは、文字列長を測る必要がある代表的なユースケースを具体的に解説いたします。

UIレイアウトへの影響

ユーザーインターフェース(UI)を設計する際、文字列の長さはレイアウトの調整に大きく関わります。

例えば、ボタンやラベルの幅を文字数に合わせて動的に変更したい場合、文字列の長さを正確に取得することが必要です。

C#のstring.Lengthは文字列のコードユニット数を返しますが、絵文字や合成文字などの特殊な文字は複数のコードユニットで構成されているため、単純にLengthを使うとUIが崩れることがあります。

こうした場合はStringInfoクラスのLengthInTextElementsを使い、ユーザーが認識する「見た目の文字数」を取得することが望ましいです。

たとえば、絵文字を含む文字列をボタンに表示する場合、LengthInTextElementsで正確な文字数を取得し、その数に応じてボタンの幅を調整すると、UIの見た目が自然になります。

データベース制約チェック

データベースに文字列を保存する際、カラムの最大長が決まっていることが多いです。

例えば、VARCHAR(50)のカラムには最大50文字までしか保存できません。

ここで注意したいのは、データベースの文字数制限が「文字数」なのか「バイト数」なのかという点です。

多くのデータベースは文字数制限を設けていますが、UTF-8などの可変長エンコーディングを使う場合、1文字あたりのバイト数が異なるため、単純にstring.Lengthで文字数を測るだけでは不十分なことがあります。

また、サロゲートペアや合成文字を含む文字列の場合、Lengthプロパティはコードユニット数を返すため、実際の文字数より多くカウントされることがあります。

これにより、データベースの制約を超えてしまう誤判定や、逆に制約内に収まっているのにエラーになることもあります。

そのため、データベースに保存する前にStringInfoLengthInTextElementsで正確な文字数を取得し、さらに必要に応じてEncoding.UTF8.GetByteCountでバイト数を計測し、制約に合致しているかをチェックすることが重要です。

ネットワーク転送サイズ最適化

Web APIやネットワーク通信で文字列データを送受信する際、データのサイズは通信速度やコストに直結します。

特にモバイル環境や低速回線では、送信するデータのバイト数を最小限に抑えることが求められます。

C#のstring.Lengthは文字数を返しますが、UTF-8などのエンコーディングでのバイト数は異なります。

例えば、日本語の「こんにちは」は5文字ですが、UTF-8では15バイトになります。

英数字の「Hello」は5文字で5バイトです。

通信量を正確に把握し、必要に応じて文字列をトリミングしたり圧縮したりするためには、Encoding.UTF8.GetByteCountを使ってバイト数を計測することが欠かせません。

また、絵文字や特殊文字を含む場合は、1文字が複数バイトになることが多いため、単純に文字数だけで判断すると通信量の見積もりが甘くなります。

バイト数を正確に計測し、通信パケットのサイズ制限に合わせて文字列を調整することが通信の安定性向上につながります。

ユーザー入力バリデーション

ユーザーが入力するテキストの長さを制限することは、アプリケーションの品質向上やセキュリティ対策において重要です。

例えば、名前やコメント欄に入力できる文字数を制限することで、データベースのオーバーフローやUIの崩れを防止できます。

ここで注意したいのは、ユーザーが入力した文字列に絵文字や合成文字が含まれている場合です。

string.Lengthはコードユニット数を返すため、実際のユーザーが認識する文字数より多くカウントされることがあります。

そのため、ユーザー入力のバリデーションではStringInfoLengthInTextElementsを使い、ユーザーが見ている文字数で制限をかけることが望ましいです。

これにより、絵文字を含む名前やコメントも正しく制限内に収めることができます。

また、バイト数制限がある場合はEncoding.UTF8.GetByteCountでバイト数を計測し、送信前に超過していないかチェックすることも重要です。

これにより、サーバー側でのエラーを未然に防げます。

これらのユースケースを踏まえ、C#で文字列の長さを測る際は、単にLengthプロパティを使うだけでなく、StringInfoEncodingクラスを活用して正確な文字数やバイト数を取得することが求められます。

string.Lengthの基本動作

C#のstring.Lengthプロパティは、文字列の長さを取得する際に最も基本的な方法です。

しかし、このLengthが返す値は「文字数」ではなく「UTF-16コードユニットの数」である点に注意が必要です。

ここではstring.Lengthの動作の詳細を掘り下げていきます。

1コードユニットと1文字の違い

C#の文字列は内部的にUTF-16エンコーディングで表現されています。

UTF-16では、1文字(グラフィカルに1つの文字として認識されるもの)が1つのコードユニット(16ビット)で表される場合もあれば、2つのコードユニットで表される場合もあります。

string.Lengthはこのコードユニットの数を返します。

サロゲートペアとは何か

サロゲートペアは、UTF-16で基本多言語面(BMP)外の文字を表現するために使われる2つの16ビットコードユニットの組み合わせです。

BMP外の文字はUnicodeの範囲でU+10000以上のコードポイントであり、1つのコードユニットでは表現できません。

例えば、漢字の「𠮷」(U+20BB7)はサロゲートペアで表されます。

UTF-16ではこの文字は2つのコードユニットに分割されるため、string.Lengthは1文字でも2を返します。

以下のサンプルコードで確認できます。

Length: 5

この例では、文字列は4文字ですが、Lengthは5を返しています。

これは「𠮷」が2つのコードユニットで構成されているためです。

絵文字が2文字扱いになる理由

絵文字も多くがBMP外のコードポイントに属しており、UTF-16ではサロゲートペアで表現されます。

例えば、笑顔の絵文字「😊」(U+1F60A)は2つのコードユニットで構成されます。

そのため、絵文字を含む文字列のLengthは、見た目の文字数より多くなることがあります。

以下の例で確認しましょう。

using System;
class Program
{
    static void Main()
    {
        string emoji = "😊";
        Console.WriteLine($"Length: {emoji.Length}"); // 2を出力
    }
}
Length: 2

このように、絵文字は1文字に見えてもLengthは2を返します。

複数の絵文字や合成文字が連結された場合はさらに複雑になります。

パフォーマンス特性

string.Lengthは文字列の内部フィールドの値を返すだけなので、非常に高速です。

文字列の長さを取得する際に文字列全体を走査する必要がなく、定数時間(O(1))で結果が得られます。

そのため、パフォーマンスが重要な場面ではLengthを使うことが推奨されます。

ただし、前述のようにLengthはコードユニット数であり、ユーザーが認識する文字数とは異なる場合があるため、用途に応じて使い分ける必要があります。

Nullや空文字の扱い

string.Lengthnullの文字列に対しては呼び出せません。

nullの文字列に対してLengthを参照するとNullReferenceExceptionが発生します。

したがって、Lengthを使う前にnullチェックを行うことが重要です。

一方、空文字列("")の場合はLengthは0を返します。

空文字列は有効な文字列オブジェクトであり、長さが0であることを示します。

以下のコードで確認できます。

using System;
class Program
{
    static void Main()
    {
        string nullStr = null;
        string emptyStr = "";
        // nullチェックを行わずにLengthを呼ぶと例外になるためコメントアウト
        // Console.WriteLine(nullStr.Length);
        Console.WriteLine($"Empty string length: {emptyStr.Length}"); // 0を出力
    }
}
Empty string length: 0

このように、Lengthは空文字列に対しては安全に使えますが、nullの場合は例外になるため注意が必要です。

nullの可能性がある場合は、string.IsNullOrEmptystring.IsNullOrWhiteSpaceを使って事前にチェックすると安全です。

System.Globalization.StringInfoで正確な文字数を取得

string.LengthがUTF-16コードユニット数を返すのに対し、System.Globalization.StringInfoクラスはユーザーが認識する「文字数」を正確に取得するための機能を提供します。

特にLengthInTextElementsプロパティは、合成文字やサロゲートペアを考慮した文字数を返します。

LengthInTextElementsの仕組み

LengthInTextElementsは文字列を「テキスト要素(Text Element)」単位でカウントします。

テキスト要素とは、ユーザーが1文字として認識する最小単位のことです。

これには単一のUnicode文字だけでなく、合成文字や結合文字列も含まれます。

合成文字と結合文字列のカウント

合成文字とは、基本文字にダイアクリティカルマーク(アクセント記号など)が結合して1つの文字として表示されるものです。

例えば、「e」と「´」が結合して「é」になります。

UTF-16ではこれらは複数のコードユニットで表されることがありますが、LengthInTextElementsはこれらを1つのテキスト要素としてカウントします。

結合文字列は複数のUnicodeコードポイントが連結して1つの文字として表示される場合も含みます。

例えば、旗の絵文字は複数の地域指示子記号が結合して1つの絵文字を形成します。

以下のコードで合成文字の例を示します。

using System;
using System.Globalization;
class Program
{
    static void Main()
    {
        string composed = "é"; // 単一の合成文字(U+00E9)
        string decomposed = "e\u0301"; // 'e' + 結合アクセント(U+0301)
        Console.WriteLine($"Composed Length: {composed.Length}"); // 1
        Console.WriteLine($"Decomposed Length: {decomposed.Length}"); // 2
        var composedInfo = new StringInfo(composed);
        var decomposedInfo = new StringInfo(decomposed);
        Console.WriteLine($"Composed Text Elements: {composedInfo.LengthInTextElements}"); // 1
        Console.WriteLine($"Decomposed Text Elements: {decomposedInfo.LengthInTextElements}"); // 1
    }
}
Composed Length: 1
Decomposed Length: 2
Composed Text Elements: 1
Decomposed Text Elements: 1

この例では、decomposedは2つのコードユニットですが、LengthInTextElementsは1を返し、ユーザーが認識する文字数を正しく表しています。

例: 🇯🇵フラッグ絵文字は何文字か

国旗の絵文字は、2つの地域指示子記号(Regional Indicator Symbols)が組み合わさって1つの旗を表します。

例えば、日本の国旗「🇯🇵」は「🇯」(U+1F1EF)と「🇵」(U+1F1F5)の2つのコードポイントから成り立っています。

string.Lengthはこれらのコードユニット数を返すため、2以上になることがありますが、LengthInTextElementsはこれを1つのテキスト要素としてカウントします。

以下のコードで確認できます。

using System;
using System.Globalization;
class Program
{
    static void Main()
    {
        string flag = "🇯🇵"; // 日本の国旗絵文字
        Console.WriteLine($"Length: {flag.Length}"); // 4を出力(サロゲートペア2つ分)
        Console.WriteLine($"Text Elements: {new StringInfo(flag).LengthInTextElements}"); // 1を出力
    }
}
Length: 4
Text Elements: 1

このように、StringInfoを使うことで、複数のコードユニットからなる複雑な文字列も正確に1文字として扱えます。

使いどころと注意点

StringInfoLengthInTextElementsは、ユーザーが認識する文字数を正確に取得したい場合に有効です。

特に、UI表示の文字数制限や入力バリデーション、文字列のトリミング処理などで役立ちます。

ただし、StringInfoは文字列を解析するために内部で走査処理を行うため、string.Lengthに比べてパフォーマンスコストが高くなります。

大量の文字列を頻繁に処理する場合は注意が必要です。

また、StringInfoはテキスト要素単位での操作をサポートしますが、Unicodeのすべての特殊ケースを完全にカバーしているわけではありません。

例えば、複雑な結合文字列や一部の新しいUnicode仕様には対応が遅れることがあります。

.NET Frameworkと.NET Core/.NET 6以降の違い

StringInfoクラスは.NET Frameworkから存在していますが、.NET Coreや.NET 6以降では内部実装やUnicodeサポートが改善されています。

.NET Core以降はUnicodeの最新仕様により対応が進み、より正確なテキスト要素の解析が可能になっています。

特に、絵文字の結合や新しい合成文字の扱いが向上しています。

一方、古い.NET Frameworkでは一部の複雑なUnicode文字列の解析が不完全な場合があるため、最新の.NET環境での利用が推奨されます。

また、.NET 5以降ではSystem.Text.Rune構造体が導入され、Unicodeコードポイント単位での操作が可能になりました。

StringInfoと組み合わせて使うことで、より柔軟な文字列処理が実現できます。

UTF-8バイト数の取得

文字列のバイト数を正確に把握することは、通信やファイル保存、データベース格納など多くの場面で重要です。

C#ではEncoding.UTF8.GetByteCountメソッドを使うことで、文字列をUTF-8エンコーディングで表現した際のバイト数を簡単に取得できます。

Encoding.UTF8.GetByteCountの原理

Encoding.UTF8.GetByteCountは、指定した文字列をUTF-8に変換した場合に必要となるバイト数を計算します。

UTF-8は可変長エンコーディングであり、1文字あたり1~4バイトで表現されます。

具体的には、ASCII文字(U+0000~U+007F)は1バイト、ラテン文字以外の多くの文字は2~3バイト、絵文字や一部の特殊文字は4バイトでエンコードされます。

このメソッドは文字列全体を走査し、各文字のUnicodeコードポイントに基づいて必要なバイト数を合計します。

内部的には、サロゲートペアを正しく処理し、1つのUnicodeコードポイントとして扱うため、正確なバイト数が得られます。

以下のサンプルコードで確認しましょう。

using System;
using System.Text;
class Program
{
    static void Main()
    {
        string str = "こんにちは"; // 日本語の挨拶
        int byteCount = Encoding.UTF8.GetByteCount(str);
        Console.WriteLine($"UTF-8バイト数: {byteCount}"); // 15を出力
    }
}
UTF-8バイト数: 15

この例では、5文字の日本語「こんにちは」がUTF-8で15バイトになることがわかります。

1文字あたり3バイトでエンコードされているためです。

エンコーディング変更時の差異

文字列のバイト数はエンコーディングによって大きく異なります。

UTF-8以外にもUTF-16やShift_JISなど多様なエンコーディングが存在し、それぞれのバイト数の計算方法も異なります。

UTF-16との比較

C#の内部文字列はUTF-16で表現されています。

UTF-16では基本多言語面(BMP)の文字は2バイト(1コードユニット)で表されますが、BMP外の文字はサロゲートペアとして4バイト(2コードユニット)になります。

UTF-16でのバイト数はstring.Length * 2で概算できますが、サロゲートペアの扱いに注意が必要です。

Encoding.Unicode.GetByteCountを使うと正確なバイト数が得られます。

以下のコードでUTF-8とUTF-16のバイト数を比較します。

using System;
using System.Text;
class Program
{
    static void Main()
    {
        string str = "𠮷野家"; // 「𠮷」はサロゲートペアの文字
        int utf8ByteCount = Encoding.UTF8.GetByteCount(str);
        int utf16ByteCount = Encoding.Unicode.GetByteCount(str);
        Console.WriteLine($"UTF-8バイト数: {utf8ByteCount}");   // 10を出力
        Console.WriteLine($"UTF-16バイト数: {utf16ByteCount}"); // 10を出力
    }
}
UTF-8バイト数: 10
UTF-16バイト数: 10

この例では、UTF-8とUTF-16で同じバイト数になっていますが、一般的にはUTF-8は可変長であり、文字によってバイト数が異なります。

UTF-16は基本的に2バイト単位で表現されるため、文字数に応じてバイト数が変わります。

実運用での活用例

UTF-8バイト数の取得は、以下のような実運用シーンで役立ちます。

  • 通信データのサイズ制限

APIやWebサービスで送信可能なペイロードサイズが制限されている場合、送信前にUTF-8バイト数を計測し、制限を超えないように文字列をトリミングしたり分割したりします。

  • データベースのバイト制限チェック

一部のデータベースカラムはバイト数で制限されていることがあります。

UTF-8バイト数を計測して、保存可能なサイズかどうかを事前に検証します。

  • ファイル保存時の容量管理

テキストファイルをUTF-8で保存する際、ファイルサイズの見積もりに使えます。

特にログファイルやメッセージファイルの容量制限を管理する場合に便利です。

  • 文字列圧縮や暗号化前のサイズ確認

圧縮や暗号化処理の前に、元の文字列のバイト数を把握して処理負荷やメモリ使用量を予測します。

以下は、API送信前に文字列のUTF-8バイト数をチェックし、制限を超えた場合にトリミングする例です。

using System;
using System.Text;
class Program
{
    static void Main()
    {
        string input = "これはテストメッセージです。絵文字も含みます😊";
        int maxBytes = 30;
        int byteCount = Encoding.UTF8.GetByteCount(input);
        Console.WriteLine($"元のバイト数: {byteCount}");
        if (byteCount > maxBytes)
        {
            // バイト数制限内に収まるようにトリミング
            int length = input.Length;
            while (length > 0 && Encoding.UTF8.GetByteCount(input.Substring(0, length)) > maxBytes)
            {
                length--;
            }
            string trimmed = input.Substring(0, length);
            Console.WriteLine($"トリミング後の文字列: {trimmed}");
            Console.WriteLine($"トリミング後のバイト数: {Encoding.UTF8.GetByteCount(trimmed)}");
        }
        else
        {
            Console.WriteLine("バイト数は制限内です。");
        }
    }
}
元のバイト数: 49
トリミング後の文字列: これはテストメッセージです。
トリミング後のバイト数: 30

この例では、UTF-8バイト数が30バイトを超えたため、文字列をバイト数制限内に収まるようにトリミングしています。

絵文字などの複数バイト文字も正しく考慮されているため、安全にサイズ制限を守れます。

実践的な組み合わせテクニック

文字列の長さやバイト数を扱う際、単独での計測だけでは不十分なケースが多くあります。

特にユーザー入力の制限や通信データの最適化では、文字数とバイト数の両方を考慮しつつ、サロゲートペアを切断しないようにトリミングするなどの工夫が必要です。

ここでは、実践的に役立つテクニックを紹介します。

文字数とバイト数の同時チェック

ユーザー入力やデータ送信の際に、文字数とバイト数の両方を制限したい場合があります。

例えば、UI上では「最大20文字まで」としつつ、通信では「最大100バイトまで」という制約があるケースです。

このような場合、StringInfo.LengthInTextElementsで文字数を正確に取得し、Encoding.UTF8.GetByteCountでバイト数を計測して両方の条件を満たすかチェックします。

以下のサンプルコードは、文字数とバイト数の両方を制限し、超過した場合はトリミングする例です。

using System;
using System.Globalization;
using System.Text;
class Program
{
    static void Main()
    {
        string input = "こんにちは😊世界🌏"; // 文字数とバイト数が異なる例
        int maxChars = 10;  // 最大文字数
        int maxBytes = 30;  // 最大バイト数
        // 文字数をテキスト要素単位で取得
        var stringInfo = new StringInfo(input);
        int textElements = stringInfo.LengthInTextElements;
        Console.WriteLine($"元の文字数: {textElements}");
        Console.WriteLine($"元のバイト数: {Encoding.UTF8.GetByteCount(input)}");
        // 文字数制限を超えている場合はトリミング
        if (textElements > maxChars)
        {
            input = stringInfo.SubstringByTextElements(0, maxChars);
            Console.WriteLine($"文字数制限後の文字列: {input}");
        }
        // バイト数制限を超えている場合はさらにトリミング
        while (Encoding.UTF8.GetByteCount(input) > maxBytes)
        {
            var si = new StringInfo(input);
            if (si.LengthInTextElements == 0) break;
            input = si.SubstringByTextElements(0, si.LengthInTextElements - 1);
        }
        Console.WriteLine($"最終文字列: {input}");
        Console.WriteLine($"最終文字数: {new StringInfo(input).LengthInTextElements}");
        Console.WriteLine($"最終バイト数: {Encoding.UTF8.GetByteCount(input)}");
    }
}
元の文字数: 8
元のバイト数: 34
文字数制限後の文字列: こんにちは😊世界🌏
最終文字列: こんにちは😊世界
最終文字数: 7
最終バイト数: 27

この例では、最初に文字数制限を適用し、その後バイト数制限を満たすまでトリミングしています。

StringInfo.SubstringByTextElementsを使うことで、サロゲートペアや合成文字を切断せずに安全に文字列を切り詰められます。

サロゲートペアを切断しないトリミング

UTF-16のサロゲートペアは2つのコードユニットで1文字を表すため、単純にstring.Substringで文字列を切るとサロゲートペアの途中で切断され、文字化けや例外の原因になります。

これを防ぐには、StringInfoクラスのSubstringByTextElementsメソッドを使い、テキスト要素単位で切り出す方法が有効です。

これにより、サロゲートペアや合成文字を丸ごと保持したままトリミングできます。

以下のコードは、サロゲートペアを含む文字列を安全にトリミングする例です。

using System;
using System.Globalization;
class Program
{
    static void Main()
    {
        string input = "𠮷野家😊"; // 「𠮷」と「😊」はサロゲートペア
        var stringInfo = new StringInfo(input);
        // 最初の3文字(テキスト要素)を取得
        string trimmed = stringInfo.SubstringByTextElements(0, 3);
        Console.WriteLine($"元の文字列: {input}");
        Console.WriteLine($"トリミング後の文字列: {trimmed}");
        Console.WriteLine($"元のLength: {input.Length}");
        Console.WriteLine($"トリミング後のLength: {trimmed.Length}");
    }
}
元の文字列: 𠮷野家😊
トリミング後の文字列: 𠮷野家
元のLength: 7
トリミング後のLength: 5

この例では、サロゲートペアの途中で切断されることなく、最初の3文字を安全に取得できています。

string.Substringを使うと途中で切れてしまう可能性があるため、StringInfoの利用が推奨されます。

入力制限での具体的実装例

ユーザー入力フォームで、文字数とバイト数の両方を制限しつつ、サロゲートペアを切断しないトリミングを行う具体的な実装例を示します。

以下のコードは、入力文字列を最大20文字かつ最大50バイトに制限し、超過した場合は安全にトリミングして返すメソッドです。

using System;
using System.Globalization;
using System.Text;
class Program
{
    static void Main()
    {
        string userInput = "テスト入力😊𠮷野家長い文字列をここに追加します";
        string limitedInput = LimitInput(userInput, 20, 50);
        Console.WriteLine($"元の入力: {userInput}");
        Console.WriteLine($"制限後の入力: {limitedInput}");
        Console.WriteLine($"文字数: {new StringInfo(limitedInput).LengthInTextElements}");
        Console.WriteLine($"バイト数: {Encoding.UTF8.GetByteCount(limitedInput)}");
    }
    static string LimitInput(string input, int maxChars, int maxBytes)
    {
        var stringInfo = new StringInfo(input);
        // 文字数制限
        if (stringInfo.LengthInTextElements > maxChars)
        {
            input = stringInfo.SubstringByTextElements(0, maxChars);
            stringInfo = new StringInfo(input);
        }
        // バイト数制限
        while (Encoding.UTF8.GetByteCount(input) > maxBytes)
        {
            if (stringInfo.LengthInTextElements == 0) break;
            input = stringInfo.SubstringByTextElements(0, stringInfo.LengthInTextElements - 1);
            stringInfo = new StringInfo(input);
        }
        return input;
    }
}
元の入力: テスト入力😊𠮷野家長い文字列をここに追加します
制限後の入力: テスト入力😊𠮷野家長い文字列を
文字数: 20
バイト数: 50

この実装では、まず文字数制限を適用し、その後バイト数制限を満たすまで1文字ずつ削っていきます。

StringInfoを使うことで、サロゲートペアや合成文字を壊さずに安全にトリミングできるため、ユーザーに違和感のない入力制限が可能です。

文字列長計測時の落とし穴

文字列の長さを計測する際には、単純にstring.Lengthを使うだけでは思わぬトラブルや誤解が生じることがあります。

Unicodeの複雑な仕様や言語特有の表現、エンコーディングの違いなどが影響するため、注意が必要です。

ここでは、文字列長計測時に陥りやすい落とし穴を具体的に解説します。

正規化(NFC/NFD)の影響

Unicode文字列は同じ見た目でも複数の表現方法が存在します。

これを「正規化」と呼び、主にNFC(Normalization Form C)とNFD(Normalization Form D)の2種類があります。

  • NFC(合成正規化)

可能な限り合成文字(単一のコードポイント)で表現する形式。

例えば「é」はU+00E9の単一コードポイント。

  • NFD(分解正規化)

合成文字を分解し、基本文字と結合文字(ダイアクリティカルマーク)に分けて表現する形式。

例えば「é」は「e」(U+0065)と「´」(U+0301)の2つのコードポイント。

この違いにより、string.LengthStringInfo.LengthInTextElementsの結果が変わることがあります。

NFDでは分解された文字が複数のコードポイントになるため、Lengthは増えますが、LengthInTextElementsは1のままになることが多いです。

以下の例で違いを確認します。

using System;
using System.Globalization;
using System.Text;
class Program
{
    static void Main()
    {
        string nfc = "é"; // U+00E9
        string nfd = nfc.Normalize(NormalizationForm.FormD); // 分解
        Console.WriteLine($"NFC Length: {nfc.Length}"); // 1
        Console.WriteLine($"NFD Length: {nfd.Length}"); // 2
        Console.WriteLine($"NFC Text Elements: {new StringInfo(nfc).LengthInTextElements}"); // 1
        Console.WriteLine($"NFD Text Elements: {new StringInfo(nfd).LengthInTextElements}"); // 1
    }
}
NFC Length: 1
NFD Length: 2
NFC Text Elements: 1
NFD Text Elements: 1

このように、正規化形式によってLengthは変わるため、文字列長を比較したり制限をかけたりする場合は、正規化を統一してから処理することが望ましいです。

特にユーザー入力のバリデーションやデータベース保存前には、string.NormalizeメソッドでNFCなどに統一することを推奨します。

右から左(RTL)言語の特殊ケース

アラビア語やヘブライ語などの右から左(Right-to-Left, RTL)言語では、文字列の表示順序と内部のコードポイント順序が異なることがあります。

これにより、文字列の長さ計測や文字単位の操作で混乱が生じることがあります。

例えば、RTL言語では文字の結合や形態変化が頻繁に起こり、1つの見た目の文字が複数のコードポイントで構成されることが多いです。

string.Lengthはコードユニット数を返すため、見た目の文字数と異なる場合があります。

また、文字列の反転や選択範囲の計算など、UI操作においてもRTL特有の処理が必要です。

文字列長の計測だけでなく、文字列操作全般でRTL対応を考慮しなければなりません。

C#のStringInfoはテキスト要素単位での計測をサポートしますが、RTL言語の複雑な結合や形態変化すべてを完全にカバーしているわけではありません。

RTL言語を扱う場合は、専用のライブラリやフレームワークのサポートを利用することも検討してください。

コードページ依存性

文字列のバイト数を計測する際、使用するエンコーディング(コードページ)によって結果が大きく異なります。

特にShift_JISやISO-2022-JPなどの日本語特有のコードページは、1文字あたりのバイト数が可変であり、同じ文字列でもバイト数が変わることがあります。

Encoding.UTF8.GetByteCountはUTF-8エンコーディングでのバイト数を返しますが、他のエンコーディングを使う場合はEncoding.GetEncodingで指定したコードページのバイト数を計測します。

コードページ依存性を考慮しないと、バイト数制限のチェックで誤判定が起きることがあります。

特にレガシーシステムや外部APIとの連携時は、正しいエンコーディングを指定してバイト数を計測することが重要です。

以下はShift_JISでのバイト数計測例です。

using System;
using System.Text;

class Program
{
    static void Main()
    {
        // Shift_JISエンコーディングを使用するためにプロバイダを登録する
        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

        string str = "こんにちは";

        // UTF-8エンコードされたバイト数を取得
        int utf8Bytes = Encoding.UTF8.GetByteCount(str);

        // Shift_JISエンコードされたバイト数を取得
        int sjisBytes = Encoding.GetEncoding("shift_jis").GetByteCount(str);

        Console.WriteLine($"UTF-8バイト数: {utf8Bytes}");   // 15
        Console.WriteLine($"Shift_JISバイト数: {sjisBytes}"); // 10
    }
}
UTF-8バイト数: 15
Shift_JISバイト数: 10

このように、エンコーディングによってバイト数が異なるため、用途に応じて適切なエンコーディングを選択してください。

例外発生パターンと対策

文字列長の計測や操作時に例外が発生するケースもあります。

代表的なものを挙げ、対策を説明します。

  • NullReferenceException

stringnullの状態でLengthStringInfoのメソッドを呼ぶと発生します。

必ずnullチェックを行い、string.IsNullOrEmptystring.IsNullOrWhiteSpaceを活用してください。

  • ArgumentOutOfRangeException

StringInfo.SubstringByTextElementsなどで範囲外のインデックスを指定すると発生します。

文字列の長さを超えないように、事前にLengthInTextElementsを確認してから操作してください。

  • EncoderFallbackException

エンコーディング変換時に変換できない文字があると発生します。

EncodingEncoderFallbackプロパティを設定し、例外を回避したり代替文字に置き換えたりできます。

  • InvalidOperationException

一部の文字列操作で内部状態が不正な場合に発生することがあります。

通常は文字列の整合性を保つことで防げます。

例外を防ぐためには、入力値の検証や例外処理を適切に実装し、文字列操作の前後で状態を確認することが重要です。

特に外部からの入力を扱う場合は堅牢なエラーハンドリングを心がけてください。

パフォーマンスとメモリ最適化

文字列の長さ計測や操作を行う際、特に大量のデータを扱う場合はパフォーマンスとメモリ効率が重要になります。

C#では効率的に文字列を扱うためのテクニックやAPIが用意されており、これらを活用することで処理速度の向上やメモリ消費の削減が可能です。

ループ内再計算の回避

文字列の長さやバイト数をループ内で何度も計算すると、無駄な処理が増えてパフォーマンスが低下します。

特にEncoding.UTF8.GetByteCountStringInfo.LengthInTextElementsは内部で文字列を走査するため、頻繁に呼び出すとコストが高くなります。

対策としては、ループの外で一度だけ計算し、その結果を変数に保持して使い回すことが基本です。

例えば、文字列のバイト数をループ内で毎回計算するのではなく、事前に計算しておき、必要に応じて更新する形にします。

以下の例は悪い例と良い例を比較しています。

using System;
using System.Text;
class Program
{
    static void Main()
    {
        string[] inputs = { "こんにちは", "Hello", "😊" };
        // 悪い例: ループ内で毎回バイト数を計算
        foreach (var input in inputs)
        {
            int byteCount = Encoding.UTF8.GetByteCount(input);
            Console.WriteLine($"{input} のバイト数: {byteCount}");
        }
        // 良い例: 事前に計算して変数に保持
        foreach (var input in inputs)
        {
            int byteCount = Encoding.UTF8.GetByteCount(input);
            // ここでbyteCountを使い回す処理があれば効率的
            Console.WriteLine($"{input} のバイト数: {byteCount}");
        }
    }
}
こんにちは のバイト数: 15
Hello のバイト数: 5
😊 のバイト数: 4
こんにちは のバイト数: 15
Hello のバイト数: 5
😊 のバイト数: 4

この例では単純な表示のみですが、実際の処理でバイト数を複数回使う場合は、計算結果を変数に保存して再利用することでパフォーマンスが向上します。

Span<char>/ReadOnlySpan<char>の併用

Span<char>ReadOnlySpan<char>は、C# 7.2以降で導入された構造体で、文字列や配列の部分的なビューを効率的に扱えます。

これらはヒープ割り当てを伴わず、スタック上でデータを参照するため、メモリ割り当てを減らし高速な処理が可能です。

文字列の一部を切り出して処理したい場合、Substringを使うと新しい文字列オブジェクトが生成されますが、ReadOnlySpan<char>を使うと元の文字列のメモリを共有しつつ部分的にアクセスできます。

以下はReadOnlySpan<char>を使った例です。

using System;
class Program
{
    static void Main()
    {
        string input = "こんにちは世界";
        ReadOnlySpan<char> span = input.AsSpan(0, 5); // 最初の5文字(コードユニット単位)
        Console.WriteLine($"部分文字列: {span.ToString()}");
        Console.WriteLine($"長さ: {span.Length}");
    }
}
部分文字列: こんにちは
長さ: 5

Span<char>は書き込み可能なビューで、ReadOnlySpan<char>は読み取り専用です。

これらを活用することで、文字列のコピーを減らし、パフォーマンスとメモリ効率を改善できます。

ただし、Span<char>はコードユニット単位で扱うため、サロゲートペアや合成文字の切断に注意が必要です。

テキスト要素単位での操作が必要な場合はStringInfoなどと組み合わせて使うことが望ましいです。

string.Createによる高速生成

string.Createは、.NET Core 2.1以降で利用可能なメソッドで、指定した長さの文字列を効率的に生成できます。

通常のstring生成は一旦文字列を作成してからコピーする処理が入ることがありますが、string.Createは直接バッファに書き込むため高速です。

string.Createは、生成する文字列の長さと、初期化用のデリゲートを受け取ります。

デリゲート内でSpan<char>を使って文字列の内容を直接書き込めるため、余計なコピーや中間オブジェクトの生成を避けられます。

以下はstring.Createを使って文字列を生成する例です。

using System;
class Program
{
    static void Main()
    {
        string result = string.Create(5, "ABCDE", (span, state) =>
        {
            for (int i = 0; i < state.Length; i++)
            {
                span[i] = state[i];
            }
        });
        Console.WriteLine(result); // ABCDE
    }
}
ABCDE

この方法は、文字列の生成と初期化を一度に行うため、パフォーマンスが求められる場面で有効です。

特に大量の文字列を動的に生成する処理や、文字列の一部を効率的に置換・加工する場合に役立ちます。

これらのテクニックを組み合わせることで、C#での文字列長計測や操作のパフォーマンスとメモリ効率を大幅に改善できます。

特に大量データやリアルタイム処理が求められる環境では積極的に活用すると良いでしょう。

まとめ

C#で文字列の長さを正確に計測するには、string.Lengthだけでなく、StringInfo.LengthInTextElementsEncoding.UTF8.GetByteCountを適切に使い分けることが重要です。

サロゲートペアや合成文字、エンコーディングの違いによる誤差を防ぎ、UI表示やデータ制限、通信最適化に役立てられます。

また、パフォーマンス向上にはループ内再計算の回避やSpan<char>string.Createの活用が効果的です。

これらを理解し実践することで、正確かつ効率的な文字列処理が可能になります。

関連記事

Back to top button