C#における開発において、文字列操作は避けて通れない最重要テーマの一つです。
ユーザーインターフェースへの表示、ログの出力、外部APIとの通信、データベースへの保存など、あらゆる場面で文字列データがやり取りされます。
C#の文字列(string型)は非常に高機能ですが、その特性を正しく理解していないと、予期せぬバグやパフォーマンスの低下を招くことがあります。
本記事では、初心者から中級者までを対象に、C#の文字列操作の基本から最新の効率的な書き方までを徹底的に解説します。
C#における文字列の基本構造
C#の文字列は、System.Stringクラスのエイリアスであるstringキーワードで表されます。
これはUnicode文字(UTF-16)のシーケンスであり、内部的には文字の配列として管理されています。
しかし、単純な配列とは異なる重要な特性がいくつかあります。
文字列の不変性(Immutability)
C#の文字列において最も重要な概念は、不変性(イミュータブル)であることです。
一度作成された文字列オブジェクトの内容を後から変更することはできません。
例えば、ある文字列に別の文字列を連結した場合、元の文字列が書き換えられるのではなく、メモリ上の新しい領域に新しい文字列オブジェクトが作成されます。
この特性は、マルチスレッド環境での安全性を高める一方で、ループ内での頻繁な文字列操作においてメモリ消費を増大させる要因となります。
参照型としての振る舞いと等価性
文字列は参照型ですが、比較においては値型のような直感的な動作をします。
==演算子を使用すると、メモリ上のアドレス(参照先)ではなく、文字列の内容そのものを比較するようにオーバーロードされています。
using System;
class Program
{
static void Main()
{
string str1 = "Hello";
string str2 = "Hello";
string str3 = new string(new char[] { 'H', 'e', 'l', 'l', 'o' });
// 内容の比較
Console.WriteLine(str1 == str2); // True
Console.WriteLine(str1 == str3); // True
// 参照の比較
Console.WriteLine(object.ReferenceEquals(str1, str3)); // False
}
}
True
True
False
文字列の作成と連結のテクニック
文字列を作成し、複数を結合する方法は多岐にわたります。
シチュエーションに応じて最適な手法を選択することが、コードの可読性と実行速度の向上に繋がります。
基本的な連結操作
最も単純な方法は + 演算子を使用することです。
小規模な連結であれば直感的で分かりやすい手法です。
string firstName = "Taro";
string lastName = "Tanaka";
string fullName = firstName + " " + lastName;
しかし、大量の文字列を連結する場合、前述の不変性により、連結のたびに新しいオブジェクトが生成され、ガベージコレクション(GC)の負荷が高まります。
文字列補間(String Interpolation)
C# 6.0以降で導入された文字列補間($””)は、現在最も推奨される書き方です。
変数を直接文字列内に埋め込むことができ、可読性が飛躍的に向上します。
int age = 25;
string message = $"名前は {firstName} {lastName}、年齢は {age} 歳です。";
また、C# 10以降では、定数文字列に対しても文字列補間が使用可能になるなど、活用の幅が広がっています。
StringBuilderによる効率的な処理
ループ内で何百、何千回と文字列を書き換える場合は、System.Text.StringBuilderクラスを使用します。
StringBuilderは可変(ミュータブル)なバッファを持っており、内部で文字列を効率的に組み立てることができます。
using System;
using System.Text;
class Program
{
static void Main()
{
var sb = new StringBuilder();
for (int i = 0; i < 5; i++)
{
sb.Append("Step ");
sb.Append(i);
sb.AppendLine(":");
}
string result = sb.ToString();
Console.WriteLine(result);
}
}
Step 0:
Step 1:
Step 2:
Step 3:
Step 4:
検索と判定の必須メソッド
文字列の中に特定の文字が含まれているか、あるいはどのように始まっているかを調べる処理は頻出します。
存在確認と位置特定
| メソッド名 | 説明 |
|---|---|
Contains | 指定した文字列が含まれているか判定する |
StartsWith | 指定した文字列で始まっているか判定する |
EndsWith | 指定した文字列で終わっているか判定する |
IndexOf | 指定した文字・文字列が最初に出現する位置を返す |
LastIndexOf | 指定した文字・文字列が最後に出現する位置を返す |
大文字小文字を区別せずに検索したい場合は、引数に StringComparison を指定するのがベストプラクティスです。
string target = "DotNet Programming";
// 大文字小文字を無視して検索
bool result = target.Contains("dotnet", StringComparison.OrdinalIgnoreCase);
Console.WriteLine($"Found: {result}");
空文字列・nullの判定
C#では、文字列が null である場合と、空文字列 "" である場合を区別する必要があります。
これらをまとめて安全にチェックするには、以下の静的メソッドを使用します。
string.IsNullOrEmpty(str): null または “” の場合に真。string.IsNullOrWhiteSpace(str): null、””、または半角スペースやタブのみの場合に真。
基本的には、ユーザー入力のバリデーションなどでは IsNullOrWhiteSpace を使用する方が堅牢です。
文字列の加工と変形
文字列の一部を取り出したり、特定の文字を置換したりする操作について解説します。
Substringによる部分抽出
Substring(startIndex, length) は、指定した位置から指定した長さの文字列を切り出します。
string code = "ABC-12345";
string prefix = code.Substring(0, 3); // "ABC"
string number = code.Substring(4); // "12345" (最後まで)
最近のC#(C# 8.0以降)では、範囲演算子(..)を用いた書き方も可能です。
string code = "ABC-12345";
string prefix = code[0..3]; // インデックス0から3の直前まで
string lastFive = code[^5..]; // 後ろから5文字目から最後まで
分割(Split)と結合(Join)
CSV形式のデータ処理などでよく使われるのが Split メソッドです。
string csv = "apple,banana,orange";
string[] fruits = csv.Split(',');
// 結合
string joined = string.Join(" | ", fruits);
Console.WriteLine(joined);
apple | banana | orange
Splitを使用する際の注意点として、デリミタが連続している場合に空の要素が含まれることがあります。
これを防ぐには StringSplitOptions.RemoveEmptyEntries を指定します。
置換(Replace)とトリミング(Trim)
特定の文字を別の文字に置き換えるには Replace、前後の余計な空白を消すには Trim を使用します。
string text = " Hello C# World ";
string cleaned = text.Trim().Replace("C#", "DotNet");
Console.WriteLine($"[{cleaned}]"); // "[Hello DotNet World]"
高度な書式設定(フォーマット)
数値を金額形式にしたり、日付を特定のレイアウトで表示したりする場合、書式指定子を利用します。
数値と日付のフォーマット
文字列補間の中でコロン : に続けて書式コードを指定します。
decimal price = 12500.5m;
DateTime now = DateTime.Now;
// 通貨形式 (C) と カスタム日付形式
Console.WriteLine($"価格: {price:C}");
Console.WriteLine($"現在時刻: {now:yyyy年MM月dd日 HH:mm:ss}");
| 指定子 | 内容 | 例 (日本のロケール) |
|---|---|---|
| C | 通貨 | ¥12,500 |
| D | 10進数(パディング可) | 00123 |
| F | 固定小数点 | 123.46 |
| X | 16進数 | FF |
| N | 桁区切り | 12,345.67 |
最新C#機能による文字列操作の進化
C#はバージョンアップを重ねるごとに、文字列操作をより便利に、そして高速にする機能を導入してきました。
生文字列リテラル(Raw String Literals)
C# 11で導入されたこの機能は、引用符や改行をそのまま記述できる画期的な機能です。
JSONやSQLのクエリをコード内に書く際に非常に重宝します。
引用符3つ """ で囲みます。
string json = """
{
"name": "Taro",
"details": {
"job": "Engineer",
"skills": ["C#", "SQL"]
}
}
""";
これにより、エスケープシーケンス(" など)の地獄から解放されます。
UTF-8文字列リテラル
ネットワーク通信やファイルI/Oの多くはUTF-8を使用しますが、C#の標準文字列はUTF-16です。
C# 11以降、末尾に u8 を付けることで、直接 ReadOnlySpan<byte> 型のUTF-8データを生成できるようになりました。
// UTF-8のバイト配列として直接定義
var utf8Bytes = "Hello"u8;
これは、変換のオーバーヘッドを削減し、高パフォーマンスなアプリケーションを構築する際に役立ちます。
パフォーマンスを意識した書き方:Span<T>の活用
文字列操作において最大の敵は「ヒープメモリの割り当て(アロケーション)」です。
大量の文字列を切り出す際に、その都度新しいインスタンスを作るとメモリを圧迫します。
これを解決するのが ReadOnlySpan<char> です。
非破壊的なスライシング
Span や ReadOnlySpan を使うと、メモリをコピーすることなく文字列の特定部分を参照できます。
string longText = "Error:1001:Description of error";
ReadOnlySpan<char> span = longText.AsSpan();
// メモリコピーを発生させずに部分参照
ReadOnlySpan<char> errorCode = span.Slice(6, 4);
if (errorCode.SequenceEqual("1001"))
{
Console.WriteLine("特定のエラーコードを検出しました。");
}
この手法は、高頻度で呼ばれるパース処理などで劇的なパフォーマンス向上をもたらします。
文字列比較の詳細とカルチャの影響
文字列の比較は単純に見えて奥が深いです。
特にグローバル展開するアプリケーションでは、カルチャ(言語・地域設定)を考慮する必要があります。
StringComparisonの使い分け
比較を行う際は、常に StringComparison を明示的に指定する習慣をつけましょう。
- StringComparison.Ordinal: バイナリ値として比較。最も高速。内部的なIDやキーの比較に最適。
- StringComparison.OrdinalIgnoreCase: バイナリ値として大文字小文字を無視して比較。
- StringComparison.CurrentCulture: 実行環境の言語設定に基づいた比較。ユーザーに表示するリストのソートなどに使用。
string s1 = "file.txt";
string s2 = "FILE.TXT";
// 常にこれを意識する
if (s1.Equals(s2, StringComparison.OrdinalIgnoreCase))
{
// 一致
}
正規表現(Regex)の活用
複雑なパターンの抽出や置換には正規表現を使用します。
C#では System.Text.RegularExpressions.Regex クラスが提供されています。
ソース生成正規表現(Source Generator)
C# 11/ .NET 7以降、正規表現のパフォーマンスを向上させる「ソース生成(Source Generators)」が導入されました。
[GeneratedRegex] 属性を使用することで、実行時ではなくコンパイル時に解析コードが生成され、大幅に高速化します。
using System.Text.RegularExpressions;
public partial class MyValidator
{
// コンパイル時に正規表現エンジンが生成される
[GeneratedRegex(@"^[a-zA-Z0-9]+$")]
private static partial Regex AlphanumericRegex();
public bool IsValid(string input) => AlphanumericRegex().IsMatch(input);
}
エンコーディングの変換
外部システムとデータをやり取りする場合、文字エンコーディングの変換が必要になることがあります。
System.Text.Encoding クラスを使用します。
using System;
using System.Text;
class Program
{
static void Main()
{
string original = "こんにちは";
// UTF-8に変換
byte[] utf8Bytes = Encoding.UTF8.GetBytes(original);
// Shift-JISに変換 (CodePagesEncodingProviderが必要な場合あり)
// Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
// byte[] sjisBytes = Encoding.GetEncoding("Shift_JIS").GetBytes(original);
// バイト配列から文字列へ復元
string recovered = Encoding.UTF8.GetString(utf8Bytes);
Console.WriteLine(recovered);
}
}
まとめ
C#の文字列操作は、基本的なメソッドから最新の Span<T> や Raw String Literals まで多岐にわたります。
日常的な開発においては、文字列補間($””)を活用して可読性を高め、ループ内などの高負荷な場面では StringBuilder を適切に選択することが重要です。
また、大文字小文字の区別や空文字判定など、細かな仕様を理解しておくことで、堅牢なプログラムを作成できるようになります。
特に近年は、メモリ効率を意識した ReadOnlySpan<char> の活用や、コンパイル時最適化を利用した正規表現など、パフォーマンス面での進化が著しい分野でもあります。
これらの機能をバランスよく組み合わせ、クリーンで効率的なコードを目指しましょう。
