C# の string 型には豊富な文字列操作メソッドが揃っています。Substring・Split・Replace の基本に加え、C# 8 のRange演算子・Split の空要素除去オプション・大文字小文字を無視した検索・StringBuilder によるパフォーマンス最適化まで理解すると、実務レベルのテキスト処理が書けるようになります。
本記事では文字列操作メソッドを体系的に解説し、実践的なCSVパースの例も紹介します。
まず知っておくべき:string の不変性(イミュータブル)
C# の string はイミュータブル(変更不可)です。Replace や ToUpper を呼んでも元の文字列は変わらず、常に新しい 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(インデックスと長さで切り出し)
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 のスライス記法に近い直感的な書き方ができます。
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
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(位置を取得)
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
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
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
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: 左側にパディング(右揃え)
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(カルチャーの注意)
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(大量連結に最適)
// 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 に変換
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 の代替)
// 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パース / ログ整形
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.RemoveEmptyEntries と TrimEntries を組み合わせてデータをクリーニングしてから処理しましょう。
IndexOf の戻り値が -1 のときに Substring を呼ぶ
IndexOf が -1 を返したとき(見つからなかったとき)に、その値をそのまま Substring や Range 演算子に渡すと ArgumentOutOfRangeException が発生します。IndexOf を使う際は必ず if (index >= 0) でガードしてください。
ループ内で += で文字列を連結する
文字列はイミュータブルなので result += text は毎回新しい文字列を生成します。10〜20 件程度なら問題ありませんが、数百件以上のループではパフォーマンスが急激に悪化します。StringBuilder か string.Join を使いましょう。
ToUpper() / ToLower() でカルチャーを指定しない
トルコ語ロケール環境では "i".ToUpper() が "I" ではなく "İ"(ドット付き大文字I)になります。URLや設定キーなどの非自然言語文字列には必ず ToUpperInvariant() / ToLowerInvariant() を使いましょう。
よくある質問
text[2..5] の方が「2文字目から5文字目まで」という意味が直感的で読みやすいです。^ インデックスと組み合わせることで末尾からの操作も簡潔に書けます。ただし古い .NET Framework や C# 7 以前のコードでは Substring を使う必要があります。string では == は値(内容)の比較で、参照比較ではありません。ただし大文字小文字の区別や文化依存の比較は行えません。大文字小文字を無視したい場合や、カルチャーに依存しない安全な比較が必要な場合は string.Equals(a, b, StringComparison.OrdinalIgnoreCase) を使いましょう。string.IsNullOrEmpty(s) で null または空文字列をまとめてチェックできます。さらに空白のみの文字列(" ")も除外したい場合は string.IsNullOrWhiteSpace(s) を使います。現場では IsNullOrWhiteSpace を使うことが多いです。string.Length は UTF-16 コードユニットの数を返します。全角文字(日本語など)も通常1つのコードユニットなので "あいう".Length は 3 です。ただし絵文字など一部の文字はサロゲートペア(2コードユニット)になるため、Length が見た目の文字数と異なる場合があります。正確な Unicode スカラー値の数は StringInfo.GetTextElementEnumerator や System.Text.Rune を使います。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の基本を参照してください。