【C#】文字列操作完全ガイド|Substring・Split・Replace・Join・Trim・IndexOf・パフォーマンス最適化まで

C# の string 型には豊富な文字列操作メソッドが揃っています。SubstringSplitReplace の基本に加え、C# 8 のRange演算子Split の空要素除去オプション・大文字小文字を無視した検索・StringBuilder によるパフォーマンス最適化まで理解すると、実務レベルのテキスト処理が書けるようになります。

本記事では文字列操作メソッドを体系的に解説し、実践的なCSVパースの例も紹介します。

スポンサーリンク

まず知っておくべき:string の不変性(イミュータブル)

C# の string はイミュータブル(変更不可)です。ReplaceToUpper を呼んでも元の文字列は変わらず、常に新しい string が返されます。この特性を理解しないと「なぜ変わらないのか」で詰まります。

string の不変性
string s = "Hello";

// これは元の s を変えない
string upper = s.ToUpper();

Console.WriteLine(s);     // Hello(元のまま)
Console.WriteLine(upper); // HELLO(新しい文字列)

// 変数を上書きすれば見かけ上「変更」できる
s = s.Replace("Hello", "Hi");
Console.WriteLine(s); // Hi(新しい string を s に代入した)
文字列に大量の変更を加えるループ処理(+= "text" の繰り返しなど)は、毎回新しい string を生成するためパフォーマンスが悪化します。大量の連結には後述の StringBuilder を使いましょう。

文字列の抽出:Substring と Range 演算子

Substring(インデックスと長さで切り出し)

Substring の基本
string text = "2024-05-15";

// Substring(開始インデックス)          → 指定位置から末尾まで
string yearPart = text.Substring(0, 4); // "2024"
string dayPart  = text.Substring(8);    // "15"

Console.WriteLine(yearPart); // 2024
Console.WriteLine(dayPart);  // 15

// 範囲外を指定すると ArgumentOutOfRangeException が発生する
// text.Substring(100); // ← 例外

Range 演算子(C# 8以降):より直感的なスライス

C# 8 から導入された Range 演算子(..)と Index(^)を使うと、Python のスライス記法に近い直感的な書き方ができます。

Range 演算子と ^ インデックス
string text = "Hello, World!";

// text[start..end]  : start から end(含まない)まで
string hello = text[0..5];   // "Hello"
string world = text[7..12];  // "World"

// ^ は末尾からのインデックス(^1 = 最後の1文字)
string last    = text[^1..]; // "!"
string lastFive = text[^5..]; // "orld!"(末尾から5文字)

// start.. : 指定位置から末尾まで
string fromComma = text[5..]; // ", World!"

Console.WriteLine(hello);    // Hello
Console.WriteLine(world);    // World
Console.WriteLine(lastFive); // orld!

// 日付パースへの応用
string date = "2024-05-15";
string year  = date[..4];   // "2024"
string month = date[5..7];  // "05"
string day   = date[8..];   // "15"
text[2..5]text.Substring(2, 3)(開始2、長さ3)と同じ結果ですが、Range 演算子の方が「2文字目から5文字目まで」という意味が読み取りやすい点が利点です。C# 8以降のプロジェクトでは積極的に活用しましょう。

文字列の検索

Contains / StartsWith / EndsWith

Contains / StartsWith / EndsWith
string text = "Hello, World!";

// 含むかどうか
Console.WriteLine(text.Contains("World"));   // True
Console.WriteLine(text.Contains("world"));   // False(大文字小文字区別)

// 大文字小文字を無視して検索(StringComparison 指定)
Console.WriteLine(text.Contains("world", StringComparison.OrdinalIgnoreCase)); // True

// 先頭・末尾の確認
Console.WriteLine(text.StartsWith("Hello")); // True
Console.WriteLine(text.EndsWith("!"));       // True

// こちらも StringComparison で大文字小文字を無視できる
Console.WriteLine(text.StartsWith("hello", StringComparison.OrdinalIgnoreCase)); // True

IndexOf / LastIndexOf(位置を取得)

IndexOf / LastIndexOf
string text = "banana";

// 最初に見つかった位置(0始まり)。見つからなければ -1
int first = text.IndexOf('a');     // 1
int last  = text.LastIndexOf('a'); // 5
int none  = text.IndexOf('z');     // -1

Console.WriteLine(first); // 1
Console.WriteLine(last);  // 5

// 開始位置を指定して検索
int fromIndex3 = text.IndexOf('a', 3); // 3 以降で最初の 'a' → 3

// 文字列で検索
string log = "ERROR: file not found. ERROR: retry failed.";
int firstError = log.IndexOf("ERROR");                    // 0
int secondError = log.IndexOf("ERROR", firstError + 1);  // 23

// 実用例: 部分文字列を安全に抽出
string header = "Content-Type: application/json";
int colonPos = header.IndexOf(':');
if (colonPos >= 0)
{
    string key   = header[..colonPos].Trim();           // "Content-Type"
    string value = header[(colonPos + 1)..].Trim();     // "application/json"
    Console.WriteLine($"{key} → {value}");
}

文字列の分割:Split

基本的な Split

Split の基本
string csv = "Alice,Bob,,Carol,";

// 単純な分割(空要素が含まれる)
string[] basic = csv.Split(',');
Console.WriteLine(basic.Length); // 5(空文字列の要素あり)
// ["Alice", "Bob", "", "Carol", ""]

// 空要素を除去する(StringSplitOptions.RemoveEmptyEntries)
string[] noEmpty = csv.Split(',', StringSplitOptions.RemoveEmptyEntries);
Console.WriteLine(noEmpty.Length); // 3
// ["Alice", "Bob", "Carol"]

// .NET 5 以降: TrimEntries で前後の空白も自動トリム
string spacedCsv = "Alice , Bob ,  Carol  ";
string[] trimmed = spacedCsv.Split(',',
    StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
// ["Alice", "Bob", "Carol"](各要素の空白が除去されている)
複数の区切り文字・最大分割数
// 複数の区切り文字を同時に指定
string data = "Apple;Banana Orange	Mango";
string[] fruits = data.Split(new char[] { ';', ' ', '	' },
    StringSplitOptions.RemoveEmptyEntries);
// ["Apple", "Banana", "Orange", "Mango"]

// 文字列の区切り文字(<br> など)
string html = "line1<br>line2<br>line3";
string[] lines = html.Split("<br>", StringSplitOptions.None);
// ["line1", "line2", "line3"]

// 最大分割数を指定(以降はまとめる)
string path = "a/b/c/d/e";
string[] parts = path.Split('/', 3); // 最大3つに分割
// ["a", "b", "c/d/e"]   ← 3つ目以降は連結して1要素

文字列の置換:Replace

Replace の基本と大文字小文字非区別
string text = "The cat sat on the mat. The cat is cute.";

// 通常の Replace(大文字小文字を区別)
string replaced = text.Replace("cat", "dog");
Console.WriteLine(replaced);
// "The dog sat on the mat. The dog is cute."

// 複数の置換を連鎖させる
string result = text
    .Replace("cat",  "dog")
    .Replace("mat",  "rug")
    .Replace("cute", "fluffy");

// 文字(char)の置換
string clean = "Hello World".Replace(' ', '_'); // "Hello_World"

// StringComparison で大文字小文字を無視した置換(C# は標準では非対応)
// → OrdinalIgnoreCase で Replace したい場合は Regex を使う
string lower = "Hello HELLO hello";
// 大文字小文字を無視して全 "hello" → "Hi" に置換
string regexReplaced = System.Text.RegularExpressions.Regex.Replace(
    lower, "hello", "Hi", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
Console.WriteLine(regexReplaced); // "Hi Hi Hi"
標準の string.Replace() は大文字小文字を区別します。大文字小文字を無視した置換が必要な場合は Regex.Replace()RegexOptions.IgnoreCase を指定します。

文字列の結合:Join・Concat・+ 演算子

結合方法の使い分け
// string.Join: 区切り文字付きでコレクションを連結(最も実用的)
string[] fruits = { "Apple", "Banana", "Orange" };
string joined = string.Join(", ", fruits); // "Apple, Banana, Orange"
Console.WriteLine(joined);

// 任意の IEnumerable<T> も連結できる
var numbers = Enumerable.Range(1, 5); // 1,2,3,4,5
string nums = string.Join("-", numbers); // "1-2-3-4-5"

// string.Concat: 区切り文字なしで連結
string s = string.Concat("Hello", " ", "World"); // "Hello World"

// + 演算子: 少数の連結なら読みやすい(多用禁止)
string name = "Taro";
string greeting = "Hello, " + name + "!"; // 少数ならOK

// 複数行の連結には string.Join が最適
string[] rows = { "header", "body", "footer" };
string html = string.Join("
", rows);

文字列の整形:Trim・Pad・大文字小文字変換

Trim / TrimStart / TrimEnd

Trim 系メソッド
string padded = "   Hello, World!   ";

Console.WriteLine(padded.Trim());       // "Hello, World!"(前後の空白を除去)
Console.WriteLine(padded.TrimStart()); // "Hello, World!   "(先頭のみ)
Console.WriteLine(padded.TrimEnd());   // "   Hello, World!"(末尾のみ)

// 特定の文字を除去
string url = "---example.com---";
string trimDash = url.Trim('-'); // "example.com"

// 複数の文字を指定
string mixed = "...##Hello##...";
string trimmed = mixed.Trim('.', '#'); // "Hello"

PadLeft / PadRight(桁揃え)

PadLeft / PadRight
// PadLeft: 左側にパディング(右揃え)
string num = "42";
Console.WriteLine(num.PadLeft(6));        // "    42"(スペース4つ)
Console.WriteLine(num.PadLeft(6, '0'));   // "000042"(ゼロ埋め)

// PadRight: 右側にパディング(左揃え)
string name = "Taro";
Console.WriteLine(name.PadRight(10));     // "Taro      "
Console.WriteLine(name.PadRight(10, '.')); // "Taro......"

// 実用例: 固定幅レポート出力
var items = new[] { ("Apple", 150), ("Banana", 80), ("Orange", 200) };
foreach (var (item, price) in items)
    Console.WriteLine($"{item.PadRight(10)}{price.ToString().PadLeft(6)}");
// Apple         150
// Banana         80
// Orange        200

ToUpper / ToLower(カルチャーの注意)

ToUpper / ToLower とカルチャーの問題
string s = "hello world";

// ToUpper() / ToLower(): 現在のカルチャーに依存(トルコ語など特殊な環境で問題が起きる)
string upper = s.ToUpper(); // "HELLO WORLD"
string lower = "HELLO".ToLower(); // "hello"

// 推奨: ToUpperInvariant / ToLowerInvariant(カルチャーに依存しない)
string safeUpper = s.ToUpperInvariant(); // "HELLO WORLD"(どの環境でも同じ結果)
string safeLower = "HELLO".ToLowerInvariant(); // "hello"

// 大文字小文字を無視した比較
bool eq1 = "Hello".Equals("hello", StringComparison.OrdinalIgnoreCase); // True
bool eq2 = string.Equals("A", "a", StringComparison.OrdinalIgnoreCase); // True

// 文字列の比較には == より StringComparison を使う(文化依存を避けるため)
string a = "café";
string b = "café"; // NFD形式(e + 合成アクセント)
// == はバイト比較 → False になる場合がある
bool normalized = string.Equals(a, b, StringComparison.CurrentCultureIgnoreCase);
ToUpper() はトルコ語カルチャーで “i” が “İ”(ドット付き大文字I)になるという有名なバグがあります(「トルコのI問題」)。ファイル名・URL・設定キーなどカルチャーに依存しない文字列処理では必ず ToUpperInvariant() / ToLowerInvariant() を使いましょう。

パフォーマンス:StringBuilder と string.Create

StringBuilder(大量連結に最適)

StringBuilder vs + 演算子のパフォーマンス比較
// NG: ループ内で += を使う(毎回新しい string が生成される)
string result = "";
for (int i = 0; i < 10_000; i++)
{
    result += i.ToString(); // 10,000 個の string オブジェクトが生成される
}

// OK: StringBuilder を使う(内部バッファで効率的に連結)
var sb = new System.Text.StringBuilder();
for (int i = 0; i < 10_000; i++)
{
    sb.Append(i);
}
string efficient = sb.ToString(); // 最後だけ string に変換
StringBuilder の主要メソッド
var sb = new System.Text.StringBuilder();

sb.Append("Hello");           // 末尾に追加
sb.Append(", ");
sb.AppendLine("World!");      // 末尾に追加 + 改行
sb.AppendFormat("今日は {0:yyyy-MM-dd} です", DateTime.Today); // 書式指定
sb.Insert(0, ">>> ");         // 指定位置に挿入
sb.Replace("Hello", "Hi");    // 置換(元の string と同様)

Console.WriteLine(sb.Length); // 現在の文字数
Console.WriteLine(sb.ToString()); // 結果を string として取得

// 複数回使う場合はクリアして再利用できる
sb.Clear();
sb.Append("再利用");
Console.WriteLine(sb.ToString()); // 再利用

string.Join でコレクション連結(StringBuilder の代替)

大量のコレクション連結は Join が最も速い
// NG: LINQ で文字列を連結(内部で StringBuilder 相当の処理だが冗長)
var items = Enumerable.Range(1, 1000).Select(i => i.ToString());
string bad = items.Aggregate((a, b) => a + "," + b);

// OK: string.Join が最も効率的で読みやすい
string good = string.Join(",", Enumerable.Range(1, 1000));

// 条件付き要素を含む場合は List に集めて Join
var parts = new List<string>();
parts.Add("base");
if (true) parts.Add("extension1");
if (false) parts.Add("extension2"); // 条件を満たさない
string final = string.Join("+", parts); // "base+extension1"

実践例:CSVパース / ログ整形

シンプルな CSV パース
string csvText = """
名前,年齢,部署
Alice,30,開発
Bob,,営業
Carol,25,人事
""";

var records = csvText
    .Split('
', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
    .Skip(1) // ヘッダーをスキップ
    .Select(line => line.Split(','))
    .Where(cols => cols.Length == 3)
    .Select(cols => new
    {
        Name       = cols[0].Trim(),
        Age        = int.TryParse(cols[1].Trim(), out var age) ? age : (int?)null,
        Department = cols[2].Trim()
    })
    .ToList();

foreach (var r in records)
    Console.WriteLine($"{r.Name} ({r.Age?.ToString() ?? "不明"}歳) - {r.Department}");
// Alice (30歳) - 開発
// Bob (不明歳) - 営業
// Carol (25歳) - 人事
ログ行の解析
string[] logLines =
{
    "[2024-05-01 10:23:15] INFO  Server started",
    "[2024-05-01 10:23:20] ERROR Connection refused: timeout",
    "[2024-05-01 10:23:25] WARN  Retry attempt 1",
};

foreach (var line in logLines)
{
    // "[2024-05-01 10:23:15] INFO  Server started" を解析
    int closeBracket = line.IndexOf(']');
    string timestamp = line[1..closeBracket]; // "2024-05-01 10:23:15"
    string rest      = line[(closeBracket + 2)..].Trim(); // "INFO  Server started"

    string[] levelAndMsg = rest.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
    string level   = levelAndMsg[0];           // "INFO"
    string message = levelAndMsg.Length > 1
                     ? levelAndMsg[1].Trim()
                     : "";                      // "Server started"

    Console.WriteLine($"時刻={timestamp} | レベル={level} | メッセージ={message}");
}

主要文字列メソッド一覧

メソッド 用途 ポイント
Substring(start, len) 部分文字列の抽出 "Hello"[1..3]"el"
[start..end] Range 演算子(C# 8) text[^3..] → 末尾3文字
Contains(s, cmp) 含むか確認 StringComparison.OrdinalIgnoreCase で大文字小文字無視
StartsWith / EndsWith 先頭・末尾の確認 StringComparison 指定可
IndexOf / LastIndexOf 位置を返す(なければ -1) 開始位置の指定可
Split(sep, opts) 文字列を分割して配列 StringSplitOptions.RemoveEmptyEntries | TrimEntries
Replace(old, new) 文字列を置換 大文字小文字無視は Regex.Replace
Join(sep, arr) コレクションを区切り文字で連結 大量要素の連結に最適
Trim / TrimStart / TrimEnd 空白・指定文字を除去 特定文字の指定も可
PadLeft / PadRight 桁揃え(ゼロ埋めなど) "42".PadLeft(6, '0')"000042"
ToUpperInvariant / ToLowerInvariant 大文字小文字変換(カルチャー非依存) ToUpper() より安全
StringBuilder.Append 大量連結のパフォーマンス最適化 ループ内での += の代替

よくある落とし穴と注意点

Split で空要素に気づかない

"a,,b".Split(',')["a", "", "b"] の3要素を返します。空要素の存在に気づかないまま配列のインデックスを決め打ちすると、予期しないデータを読んでしまいます。StringSplitOptions.RemoveEmptyEntriesTrimEntries を組み合わせてデータをクリーニングしてから処理しましょう。

IndexOf の戻り値が -1 のときに Substring を呼ぶ

IndexOf が -1 を返したとき(見つからなかったとき)に、その値をそのまま Substring や Range 演算子に渡すと ArgumentOutOfRangeException が発生します。IndexOf を使う際は必ず if (index >= 0) でガードしてください。

ループ内で += で文字列を連結する

文字列はイミュータブルなので result += text は毎回新しい文字列を生成します。10〜20 件程度なら問題ありませんが、数百件以上のループではパフォーマンスが急激に悪化します。StringBuilderstring.Join を使いましょう。

ToUpper() / ToLower() でカルチャーを指定しない

トルコ語ロケール環境では "i".ToUpper()"I" ではなく "İ"(ドット付き大文字I)になります。URLや設定キーなどの非自然言語文字列には必ず ToUpperInvariant() / ToLowerInvariant() を使いましょう。

よくある質問

QSubstring と Range 演算子([start..end])はどちらを使うべきですか?
AC# 8 以降のプロジェクトなら Range 演算子を推奨します。text[2..5] の方が「2文字目から5文字目まで」という意味が直感的で読みやすいです。^ インデックスと組み合わせることで末尾からの操作も簡潔に書けます。ただし古い .NET Framework や C# 7 以前のコードでは Substring を使う必要があります。
Q文字列の比較に == を使っても良いですか?
AC# の string では == は値(内容)の比較で、参照比較ではありません。ただし大文字小文字の区別や文化依存の比較は行えません。大文字小文字を無視したい場合や、カルチャーに依存しない安全な比較が必要な場合は string.Equals(a, b, StringComparison.OrdinalIgnoreCase) を使いましょう。
Qnull と空文字列(””)の確認はどうすればいいですか?
Astring.IsNullOrEmpty(s) で null または空文字列をまとめてチェックできます。さらに空白のみの文字列(" ")も除外したい場合は string.IsNullOrWhiteSpace(s) を使います。現場では IsNullOrWhiteSpace を使うことが多いです。
Q文字列の長さは .Length で取得できますが、全角文字も1文字として数えますか?
AC# の string.Length は UTF-16 コードユニットの数を返します。全角文字(日本語など)も通常1つのコードユニットなので "あいう".Length は 3 です。ただし絵文字など一部の文字はサロゲートペア(2コードユニット)になるため、Length が見た目の文字数と異なる場合があります。正確な Unicode スカラー値の数は StringInfo.GetTextElementEnumeratorSystem.Text.Rune を使います。
QStringBuilder はいつ使うべきですか?目安はありますか?
A目安としてループ内で文字列を連結する場合(特に繰り返し回数が10回以上)か、連結後の文字列が数KBを超える場合は StringBuilder を検討します。少数の固定的な連結(3〜4個程度)は string.Concat$"..." の方が読みやすく、コンパイラが最適化するため StringBuilder は不要です。コレクションを連結する場合は string.Join が最も効率的です。

まとめ

目的 推奨メソッド 注意点
部分文字列の抽出 text[start..end](C# 8)/ Substring 範囲外でArgumentOutOfRangeException
含むか確認 Contains(s, OrdinalIgnoreCase) 大文字小文字無視は StringComparison 指定
位置の取得 IndexOf / LastIndexOf 見つからなければ -1。必ずガードを入れる
文字列の分割 Split(sep, RemoveEmptyEntries | TrimEntries) 空要素に注意
文字列の置換 Replace / Regex.Replace 大文字小文字無視は Regex.Replace
コレクションの結合 string.Join(sep, collection) 最も効率的な連結方法
空白除去・桁揃え Trim / PadLeft / PadRight 特定文字の除去も可
大文字小文字変換 ToUpperInvariant / ToLowerInvariant ToUpper/ToLower はカルチャー依存
大量連結(ループ) StringBuilder.Append ループ内 += は毎回新オブジェクト生成

文字列補間($"...")の詳細は文字列補間の基本と活用例を、LINQ と組み合わせた文字列コレクション処理はLINQの基本を参照してください。