C#入門編

C#入門編(20)文字列操作マスターガイド ~基本操作・StringBuilderによる高速化・正規表現まで~

今回は「C#における文字列処理」について解説します。

プログラミングにおいて文字列処理は非常に頻繁に行われる操作であり、効率的に扱えるようになることはプログラマーにとって重要なスキルです。

本記事では以下について説明します。

  • C#における文字列の基本概念
  • 基本的な文字列操作(分割・結合・検索・置換など)
  • 書式設定と変数埋め込み
  • StringBuilderによる高速な文字列連結
  • 正規表現の基本的な使い方

以下のような方に役立つ内容となっています。

  • 文字列処理の基本を理解したい初心者の方
  • 複雑な文字列処理を必要とするアプリを開発している方
  • 文字列処理のパフォーマンスを向上させたい方
プロ太

文字列処理はあらゆるアプリ開発で必要になるため、とても重要ですね。

今日は、C#における文字列処理の基本から最新の機能まで、一緒に理解を深めていきましょう!

YouTube動画も作成しています。

C#における文字列の基本

文字列の不変性

C#では、string型は不変(イミュータブル)です。これは、一度作成された文字列インスタンスの内容は変更できないということを意味します。

string message = "Hello";
message = message + " World"; // 新しい文字列オブジェクトが作成される

上記のコードでは、messageに「World」を追加しているように見えますが、実際には新しい文字列「Hello World」が作成されます。

以下のようなイメージです。

プロ太

この「不変性」という特性は、文字列操作のパフォーマンスに大きな影響を与えます。

頻繁に文字列を変更する場合は、後で説明するStringBuilderクラスを使用することでパフォーマンスを向上させることができます。

文字列リテラルの種類

C#では、複数の方法で文字列リテラルを記述できます。

以下は通常リテラルです。エスケープシーケンスを使って特殊文字も表現できます。

string normal = "This is a normal string.";

//エスケープシーケンスを使って特殊文字を表現します
string withEscape = "First line.\nSecond line."; // \nは改行
string path = "C:\\Program Files\\App"; // バックスラッシュは\\でエスケープ

//注意
// 「\」記号は「バックスラッシュ」と呼びます。
// 日本語環境の多くのフォントでは見た目が円記号(¥)のように表示されることがありますが、
// 実際にはバックスラッシュ「\」という文字です。

以下は逐語的文字列リテラル(Verbatim string literal)です。文字列の前に@記号を付けると、エスケープシーケンスを無視して、文字列をそのまま扱います。

string path = @"C:\Program Files\App"; // バックスラッシュをエスケープする必要なし
string multiLine = @"This is line 1.
This is line 2."; // 改行もそのまま含まれる

C# 11から導入された生文字列リテラル(Raw string literal)は、複数の二重引用符を使って開始と終了を指定することで、内部の書式やインデントを維持できます。

string json = """
{
    "name": "John",
    "age": 30,
    "city": "New York"
}
""";
プロ美

生文字列リテラルは、JSONやXMLなどの構造化データを表現するときに特に便利だね!

文字列、文字列リテラル、エスケープシーケンスなどについては、Microsoft Learnの「文字列と文字列リテラル」も参考にしてください。

基本的な文字列操作

文字列に対する基本的な操作について紹介します。

インデクスによる文字の取り出し

文字列は char の並びとして扱われるため、配列のようにインデクスで文字を取り出すことができます

string text = "Hello";
char firstChar = text[0];  // 'H'
char lastChar = text[text.Length - 1]; // 'o'

インデクスは配列と同様に0から開始されます。

また、インデクス範囲外へアクセスするとIndexOutOfRangeExceptionが発生するので注意しましょう。

C# 8.0以降では、インデクスに^を使って後ろから数えることもできます。

string text = "Hello";
char last = text[^1]; // 'o'(末尾から1文字目)
char secondLast = text[^2]; // 'l'

分割と結合

Splitメソッドを使用して、特定の区切り文字で文字列を分割できます。

string csv = "apple,banana,orange";
string[] fruits = csv.Split(',');
// fruits = { "apple", "banana", "orange" }

複数の区切り文字を指定することもできます。

string text = "apple,banana;orange|grape";
string[] fruits = text.Split(',', ';', '|');
// fruits = {"apple", "banana", "orange", "grape"}

+演算子Concatメソッドを使うと文字列を結合できます。

// + 演算子を使用した結合(最も基本的な方法)
string a = "Hello";
string b = "World";
string combined = a + " " + b;
// combined = "Hello World"

List<string> fruits = new List<string> { "apple", "banana", "orange" };
string combined = string.Concat(fruits);
// combined = "applebananaorange"

Joinメソッドを使うと区切り文字列を入れて結合できます。

string[] fruits = { "apple", "banana", "orange" };
string csv = string.Join(",", fruits);
// csv = "apple,banana,orange"

配列以外にもIEnumerable<string>を受け取れるため、LINQと組み合わせて使用できます。

List<string> fruits = new List<string> { "apple", "banana", "orange" };
string result = string.Join(" and ", fruits.Where(f => f.Length > 5));
// result = "banana and orange"

ジェネリック型、コレクション、そしてLINQの基本については、以下も参考にしてください。

C#入門編(13)コレクションとジェネリック型 ~リストと辞書で要素を動的に変更する~ 今回は「コレクションとジェネリック型」について解説します。 コレクションとは、複数のオブジェクトを一括して扱うための仕組みです。...
C#入門編(16)LINQ ~データ操作を効率的に行う~ 今回は「LINQ (Language Integrated Query)」について解説します。LINQは、C#でデータの操作や検索を効...
プロ太

文字列は不変であるため、操作をするたびに「新しい文字列が生成される」という点には注意しましょう。

部分文字列の取得

Substringメソッドを使用して、文字列の一部を取得できます。

string text = "Hello, World!";
string sub1 = text.Substring(7); // インデクス7から最後まで → "World!"
string sub2 = text.Substring(7, 5); // インデクス7から5文字 → "World"

範囲演算子(C# 8.0以降)を用いると以下のようにも記述できます。

// 範囲演算子を使用(C# 8.0以降)
string text = "Hello, World!";
string sub1 = text[7..];        // インデクス7から最後まで → "World!"
string sub2 = text[0..5];       // インデクス0から5まで(5は含まれない)→ "Hello"
string sub3 = text[^6..^1];     // 「末尾から6番目」から末尾から1番目まで → "World"

検索と置換

文字列内での検索には様々なメソッド(IndexOf、StartsWith、EndsWith、Contains)が用意されています。

string text = "Hello, World!";

// IndexOf - 指定した文字や部分文字列の位置を返す
int position = text.IndexOf("World"); // 7

// StartsWith, EndsWith - 文字列の先頭または末尾との一致を確認
bool startsWithHello = text.StartsWith("Hello"); // true
bool endsWithWorld = text.EndsWith("World"); // false("World!"で終わるため)

// Contains - 指定した部分文字列が含まれるかを確認
bool containsWorld = text.Contains("World"); // true

Replaceメソッドで文字や部分文字列を置換できます。

string text = "Hello, World!";
string newText = text.Replace("World", "C#"); // "Hello, C#!"

// 文字の置換も可能
string spaceless = text.Replace(' ', '_'); // "Hello,_World!"

比較

文字列比較には、==演算子Equalsメソッド、Compareメソッドなどを使用します。文字列の比較は辞書順で行われます。

string s1 = "Hello";
string s2 = "hello";

// == 演算子(大文字小文字を区別)
bool isEqual1 = (s1 == s2); // false

// Equals(大文字小文字を区別)
bool isEqual2 = s1.Equals(s2); // false

// 大文字小文字を区別しない比較
bool isEqual3 = s1.Equals(s2, StringComparison.OrdinalIgnoreCase); // true

// Compare(数値を返す:等しければ0、s1がs2より小さければ負の値、大きければ正の値)
int compareResult = string.Compare(s1, s2, ignoreCase: true); // 0(等しい)

Compareメソッドは辞書順での比較結果を数値で返すため、文字列の順序付けやソートに役立ちます。

書式設定と変数埋め込み

文字列補間

C# 6.0から導入された文字列補間(String Interpolation)を使うと、文字列内に変数を埋め込むことができます。

「$”…”」と記述します。文字列内で「{Variable}」とすることで変数を埋め込めます。

// 文字列内に変数を直接埋め込む
string name = "田中太郎";
int age = 30;
string message = $"私の名前は{name}で、{age}歳です。";
// "私の名前は田中太郎で、30歳です。"

// 式も埋め込める
string ageNextYear = $"来年は{age + 1}歳になります。";
// "来年は31歳になります。"

// 書式指定の簡単な例
double number = 123.456;
string formatted1 = $"小数点2桁: {number:F2}"; 
// "小数点2桁: 123.46"(四捨五入されます)

// 桁数指定(0埋め)
int code = 42;
string formatted2 = $"商品番号: {code:D5}";
// "商品番号: 00042"

// 日付のフォーマット
DateTime today = DateTime.Now;
string dateMessage = $"今日は{today:yyyy年MM月dd日}です。";
// 例: "今日は2025年05月19日です。"

string.Format

文字列補間が導入される前から使われているstring.Formatメソッドも同様の機能を提供します。

// インデクスを使って変数を参照
string name = "田中";
int age = 25;
string message = string.Format("私の名前は{0}で、{1}歳です。", name, age);
// "私の名前は田中で、25歳です。"

// 書式指定
DateTime now = DateTime.Now;
string formatted = string.Format("今日は{0:yyyy年MM月dd日}で、現在時刻は{0:HH時mm分}です。", now);
// "今日は2025年05月19日で、現在時刻は14時30分です。"
プロ太

文字列補間($構文)は内部的にはstring.Formatへ変換されますが、よりシンプルで読みやすいコードになります。

最近のコードでは文字列補間が好まれる傾向にありますね。

文字列の書式設定はMicrosoft Learnのこちらの記事を参考にしてください。

標準の数値書式指定文字列」、「標準の日付と時刻の形式の文字列」に、数値・日時の書式一覧があります。

StringBuilderによる連結

基本的な使い方

前述したように、通常の文字列連結操作(+演算子や+=演算子)では毎回新しい文字列インスタンスが生成されます。

大量の文字列連結を行う場合、これはメモリ使用量とパフォーマンスの面で効率が悪くなります。

StringBuilderクラスを使用すると、このような状況でのパフォーマンスを大幅に向上させることができます。

using System.Text; // StringBuilderを使用するには必要

// 連結したい文字列の配列
string[] names = { "田中", "佐藤", "鈴木", "高橋", "伊藤" };

// StringBuilderのインスタンスを作成
StringBuilder sb = new StringBuilder();

// forループで配列の要素を連結
sb.AppendLine("社員名簿:");
for (int i = 0; i < names.Length; i++)
{
    sb.Append(i + 1).Append(". ").AppendLine(names[i]);
}

// 最終的な文字列を取得
string result = sb.ToString();
/* 
result = 
社員名簿:
1. 田中
2. 佐藤
3. 鈴木
4. 高橋
5. 伊藤
*/

StringBuilderには様々な文字列操作メソッドが用意されています。

using System.Text;

StringBuilder sb = new StringBuilder();

// AppendFormat - string.Formatに似た書式設定
sb.AppendFormat("名前: {0}, 年齢: {1}", "田中", 40);
Console.WriteLine("1. AppendFormat後: " + sb.ToString());
// 出力: 名前: 田中, 年齢: 40

// Insert - 指定位置に文字列を挿入(先頭に挿入)
sb.Insert(0, "個人情報: ");
Console.WriteLine("2. Insert後: " + sb.ToString());
// 出力: 個人情報: 名前: 田中, 年齢: 40

// Replace - 文字列置換
sb.Replace("田中", "佐藤");
Console.WriteLine("3. Replace後: " + sb.ToString());
// 出力: 個人情報: 名前: 佐藤, 年齢: 40

// Remove - 一部を削除(文字位置11から5文字を削除)
sb.Remove(11, 5);
Console.WriteLine("4. Remove後: " + sb.ToString());
// 出力: 個人情報: 名前: 年齢: 40

StringBuilderクラスの機能についてはこちらも参考にしてください。

パフォーマンスの比較

StringBuilderと通常の文字列連結のパフォーマンスの違いを比較してみましょう。10万回の文字列連結の実行時間を比較します。

using System.Text;
using System.Diagnostics; // Stopwatchを使用するために必要

// 繰り返し回数を定数として定義
const int ITERATION_COUNT = 100000;

// 通常の文字列連結
Stopwatch sw1 = new Stopwatch();
sw1.Start();
string normalString = "";
for (int i = 0; i < ITERATION_COUNT; i++)
{
    normalString += i.ToString() + ", ";
}
sw1.Stop();
Console.WriteLine($"通常の文字列連結にかかった時間: {sw1.ElapsedMilliseconds}ms");

// StringBuilderを使用
Stopwatch sw2 = new Stopwatch();
sw2.Start();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < ITERATION_COUNT; i++)
{
    sb.Append(i.ToString()).Append(", ");
}
string builderString = sb.ToString();
sw2.Stop();
Console.WriteLine($"StringBuilderを使用した連結にかかった時間: {sw2.ElapsedMilliseconds}ms");
Console.WriteLine($"性能差: {(double)sw1.ElapsedMilliseconds / sw2.ElapsedMilliseconds}倍");

私の環境で動作させた結果は以下になりました。

通常の文字列連結にかかった時間: 10581ms
StringBuilderを使用した連結にかかった時間: 4ms
性能差: 2645.25倍

このような差が出るのは、文字列連結の方法が違うためです。

通常の文字列連結では、連結するたびに新しい文字列インスタンスが生成されます。

それに対し、StringBuilderではあらかじめ用意しておいたバッファへの追記で済むため高速です。

プロ美

速度が全然違うんだね!

プロ太

データが小さく、少ない文字列の連結ならば、通常の文字列連結操作でも構いません。

多くの文字列連結操作が行われる場合、特にループ内で文字列連結を行う場合にはStringBuilderの使用を検討するとよいでしょう。

正規表現の基本

アプリを作っていると「この文章から電話番号だけを抽出したい」「メールアドレスが正しい形式かどうか確認したい」といったことをしたくなることがありますね。

このような文字列のパターンを扱う作業を文字列操作の組み合わせで行うと非常に複雑で長いコードになってしまいます。

そこで登場するのが正規表現(Regular Expression)です。正規表現には以下の2つの使い方があります。

  • パターンにマッチするかチェックする – 文字列が特定のパターンに合致するか確認する
  • パターンに合う部分を抽出する – 文字列から特定のパターンに合致する部分を取り出す
  • パターンに合う部分を置換する – 文字列の特定のパターンに合致する部分を別の文字列に置き換える

今回、正規表現の基本について簡単にみてみましょう。

よく使う正規表現パターン

パターンを記述するための基本的な記号を紹介します。

  • ^ → 文字列の先頭
  • $ → 文字列の末尾
  • . → 任意の1文字
  • * → 直前の要素の0回以上の繰り返し
  • + → 直前の要素の1回以上の繰り返し
  • ? → 直前の要素の0回または1回の出現
  • \d → 数字1文字
  • \w → 単語文字(英数字またはアンダースコア)
  • \s → 空白文字
  • [] → 角括弧内の任意の1文字にマッチ(文字クラス)
    • [abc] – a、b、cのいずれか1文字
    • [a-z] – 小文字アルファベットの任意の1文字
    • [A-Z] – 大文字アルファベットの任意の1文字
  • [^] → 角括弧内に指定されていない任意の1文字にマッチ(否定文字クラス)
    • [^abc] – a、b、c以外の1文字
  • {n} → 直前の要素をちょうどn回繰り返す
  • {n,m} → 直前の要素をn回以上m回以下繰り返す
  • | → OR演算子(いずれかのパターンにマッチ)
    • cat|dog – “cat”または”dog”にマッチ

正規表現のすべてのパターンはMicrosoft Learnの「正規表現言語 – クイック リファレンス」を参考にしてください。

基本パターンの実例

以下のコードで基本的なパターンの使い方を確認してみましょう。

using System.Text.RegularExpressions;

// ●例1: ^(先頭)と$(末尾)
string text1 = "Hello World";
bool startsWithHello = Regex.IsMatch(text1, "^Hello");
bool endsWithWorld = Regex.IsMatch(text1, "World$");
// 結果: startsWithHello = True, endsWithWorld = True

// ●例2: \d(数字)- 電話番号パターンの抽出
string text2 = "電話番号は090-1234-5678です";
string phonePattern = @"\d{3}-\d{4}-\d{4}";
// @は「逐語的文字列リテラル」で、\をエスケープせずそのまま正規表現として使える
Match phoneMatch = Regex.Match(text2, phonePattern);
// 結果: phoneMatch.Value = "090-1234-5678"

// ●例3: +(1回以上の繰り返し)と*(0回以上の繰り返し)
string text3 = "cat ct caat";

// 'c' + 'a'(1回以上) + 't' のパターン
MatchCollection plusMatches = Regex.Matches(text3, "ca+t");
// 結果: plusMatches = { "cat", "caat" }

// 'c' + 'a'(0回以上) + 't' のパターン
MatchCollection starMatches = Regex.Matches(text3, "ca*t");
// 結果: starMatches = { "cat", "ct", "caat" }

// ●例4: [](文字クラス)
string text4 = "can ban dan";
MatchCollection classMatches = Regex.Matches(text4, "[cb]an");
// 結果: classMatches = { "can", "ban" }

// ●例5: 置換 - 改行コードの統一
string text5 = "行1\r\n行2\r行3\n行4";
// \r\n(Windows)、\r(古いMac)、\n(Unix/Linux)を全て\nに統一
string unifiedText = Regex.Replace(text5, @"\r\n|\r|\n", "\n");
// 結果: "行1\n行2\n行3\n行4"

// ●例6: メールアドレスのパターン
string text6 = "お問い合わせはinfo@example.comまでどうぞ";
string emailPattern = @"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+";
Match emailMatch = Regex.Match(text6, emailPattern);
// 結果: emailMatch.Value = "info@example.com"

例6は少し複雑ですね。要点は以下になります。

  • [a-zA-Z0-9_.+-]+ → ローカル部分(@の前):英数字とドット、アンダースコア、プラス、ハイフンが1回以上
  • @ → アットマーク
  • [a-zA-Z0-9-]+ → ドメイン名:英数字とハイフンが1回以上
  • \. → ドット(エスケープ必要)
  • [a-zA-Z0-9-.]+ → トップレベルドメイン:英数字、ハイフン、ドットが1回以上

注意:実際のメールアドレスの仕様はもっと複雑で、完全に準拠したパターンはさらに長くなります。例6は簡易に表現した1例となります。

C#の正規表現について、詳しくはMicrosoft Learnの記事も参考にしてください。

プロ太

今回は正規表現の初歩について紹介しました。

正規表現はとても奥が深いため、別途、正規表現に関する記事を作成したいと思います。

正規表現で複雑なパターンを作るのは難しいため、最初はChatGPTなどのAIを活用して(解説も含めて)作ってもらい理解を深めるのもよいでしょう。

プログラミング学習におけるChatGPT活用については以下も参考にしてください。

プログラミング初心者のためのチャット型AI活用ガイド【ChatGPT入門】 プログラミング学習において、ChatGPTなどのチャット型AIは非常に便利なツールです。 しかし、プログラミング初心者がAIを使...

文字列についてさらに学びたい方へ

以下のトピックは、より高度な文字列処理テクニックです。基本をマスターした後に学習することをお勧めします。

Spanによる効率的な文字列処理

メモリの新規割り当てを行わずに文字列の一部を参照できる機能で、大量のテキスト処理や高速なパーサー作成時に必須です。

特にパフォーマンスが重要なアプリケーションで文字列操作のメモリ使用量を削減したい場合に役立ちます。

参考:Microsoft Learn記事

文字列の国際化と文化依存の比較

複数言語をサポートするアプリで必要な技術で、CultureInfoクラスを使って言語や地域ごとの日付、数字、大文字小文字の違いを適切に扱えます。

参考:Microsoft Learn記事

エンコーディング(UTF-8, UTF-16など)

異なるシステム間でテキストを交換する際や特殊文字を含むテキストファイルを処理する場合に重要です。

ファイル読み書きやネットワーク通信で文字化けを防ぐために理解しておくべき概念です。

参考:Microsoft Learn記事

パターンマッチングと文字列

複雑な文字列パターンを簡潔に検出・抽出できる新しい構文で、特に文字列の解析や検証ロジックをより読みやすく保守しやすくしたい場合に便利です。

参考:Microsoft Learn記事

まとめ

本記事では、C#における文字列処理の基本から応用までを解説しました。

C#の文字列には不変性があり、一度作成されたstring型のインスタンスは変更できないという特徴があります。

文字列リテラルには通常リテラル、逐語的リテラル(@)、生文字列リテラル(“””)などの種類があります。

文字列処理の操作として、Split、Joinによる分割と結合、Substringや範囲演算子による部分文字列の取得、IndexOfなどの検索メソッド、Replaceによる置換方法を学びました。

変数の埋め込みには$記号を使った文字列補間が便利で、数値や日付の書式指定も簡単に行えます。これは従来のstring.Formatより読みやすいコードになります。

大量の文字列連結を行う場合は、StringBuilderクラスを使用することでパフォーマンスを大幅に向上させることができます。

正規表現を使えば、複雑なパターンマッチングや、電話番号やメールアドレスなどの抽出・検証を簡潔に記述できます。

さらなる高度なトピックとして、Spanによる効率的な処理、国際化と文化依存の比較、エンコーディング、パターンマッチングなどがあります。

プロ太

文字列処理はプログラミングの基本中の基本です。この記事で学んだ技術を活用して、より効率的で読みやすいコードを書いていきましょう!

引き続き、一緒にC#プログラミングを学んでいきましょう!

ABOUT ME
プロ太
●仕事:現在は個人事業主(メンター・情報発信等)、大手IT企業で技術者・マネージャ(15年以上)、大学の外部講師、学生時代は学習塾で非常勤講師(約4年間) ●博士(工学)の学位取得 ●高校生の頃に独学で始め、プログラミング歴20年以上 ●言語:C# 、Java、C/C++、Python、JavaScript/TypeScript等 ●分野:Webアプリ、テスト自動化、生成AI、デバッガ、コード解析、ドメイン特化言語

ご依頼・ご相談について

プログラミング学習のご相談、お仕事のご依頼については、
こちらのお問い合わせページをご確認ください。