ここではStringBuilderクラスついて見ていきます。 StringBuilderクラスを用いることにより、直接文字列を操作するよりも効率的に文字列の加工・編集を行うことができます。

§1 StringBuilderクラスの特徴

.NET FrameworkのStringクラスはインスタンスの持つ値を変更できないという性質を持つ不変な型です。 Stringクラスの文字列操作ではインスタンス自体が持っている値(=文字列)が変わることは無く、操作の度に結果が格納された新しい文字列のインスタンスが生成されます。

一方、StringBuilderクラスは内部にChar配列のバッファを保持し、文字列操作によってインスタンスの持つ値(=文字列)が変わる可変な型です。 StringBuilderクラスの文字列操作では、操作の際にバッファの内容のみが変更され、新しい文字列のインスタンスは生成されません。

このような特徴から、文字列の連結・挿入・削除・置換といった加工・編集を繰り返し行う操作の場合は、StringBuilderクラスを用いて行った方が適しています。 (実際にどの程度パフォーマンスに差が出るかは§.StringBuilderのパフォーマンスで検証しています。)

一方でStringBuilderクラスにはStartsWith・IndexOf・Equalsといった文字列の探索・比較を行うメソッドは用意されていません。 探索・比較の目的にはStringクラスのメソッドRegexクラスを使用します。



§2 StringBuilderクラスとStringクラスの違い

まずはStringBuilderクラスとStringクラスで同じ操作を行う場合の違いを見ておきます。 詳細な使い方は後述します。

Stringクラスでの文字列操作
using System;


class Sample {
  static void Main()
  {
    // インスタンスの作成
    string s = "The quick brown fox jumps over the lazy dog";

    Console.WriteLine(s);

    // 末尾の8文字を削除する
    s = s.Remove(s.Length - 8, 8);

    Console.WriteLine(s);

    // 文字列を末尾に追加する
    s = s + "silliy dog";

    Console.WriteLine(s);

    // 文字列を置換する
    s = s.Replace("The quick", "the clever");

    Console.WriteLine(s);
  }
}
StringBuilderクラスでの文字列操作
using System;
using System.Text;

class Sample {
  static void Main()
  {
    // インスタンスの作成
    StringBuilder sb = new StringBuilder("The quick brown fox jumps over the lazy dog");

    Console.WriteLine(sb);

    // 末尾の8文字を削除する
    sb.Remove(sb.Length - 8, 8);

    Console.WriteLine(sb);

    // 文字列を末尾に追加する
    sb.Append("silliy dog");

    Console.WriteLine(sb);

    // 文字列を置換する
    sb.Replace("The quick", "the clever");

    Console.WriteLine(sb);
  }
}
実行結果
The quick brown fox jumps over the lazy dog
The quick brown fox jumps over the 
The quick brown fox jumps over the silliy dog
the clever brown fox jumps over the silliy dog

このように、Stringクラスの文字列操作ではその都度新しいインスタンスが生成されますが、StringBuilderクラスでは単一のインスタンスを使いまわして文字列操作を繰り返すことができます。

StringBuilderクラスの内容は、StringBuilder.ToStringメソッドを使うことで通常の文字列として取得出来ます。

StringBuilderからStringへの変換
using System;
using System.Text;

class Sample {
  static void Main()
  {
    StringBuilder sb = new StringBuilder("The quick brown fox jumps");

    sb.Append(" over the lazy dog");

    // StringBuilderクラスが保持している文字列をStringとして取得する
    string s = sb.ToString();

    Console.WriteLine(s);
  }
}
実行結果
The quick brown fox jumps over the lazy dog

§3 StringBuilderによる文字列操作

§3.1 文字列の追加 (Append, AppendLine)

StringBuilder.Appendメソッドは、バッファの末尾に文字列を追加するメソッドで、String.Concatメソッドや加算演算子+による文字列の連結操作に相当します。

また、StringBuilder.AppendLineメソッドは、バッファの末尾に文字列を追加する点はAppendメソッドと同様ですが、自動的に改行文字も追加されます。 AppendLineメソッドに引数を指定しなかった場合は、改行文字のみが追加されます。

using System;
using System.Text;

class Sample {
  static void Main()
  {
    StringBuilder sb = new StringBuilder(); // 空のStringBuilderを作成

    Console.WriteLine("<{0}>", sb);

    // 文字列を追加
    sb.Append("The quick brown fox jumps");

    Console.WriteLine("<{0}>", sb);

    // 改行文字を付けて文字列を追加
    sb.AppendLine(" over the lazy dog");

    Console.WriteLine("<{0}>", sb);

    // 文字列を追加
    sb.Append("The quick brown fox jumps");

    Console.WriteLine("<{0}>", sb);

    // 改行文字のみを追加
    sb.AppendLine();

    Console.WriteLine("<{0}>", sb);
  }
}
実行結果
<>
<The quick brown fox jumps>
<The quick brown fox jumps over the lazy dog
>
<The quick brown fox jumps over the lazy dog
The quick brown fox jumps>
<The quick brown fox jumps over the lazy dog
The quick brown fox jumps
>

AppendLineメソッドでは改行文字に常にEnvironment.NewLineが使われます。 異なる改行文字を使用する方法については§.StringBuilderと改行文字で後述します。

§3.1.1 StringBuilderと改行文字

先に述べたとおり、AppendLineメソッドで追加される改行文字は常にEnvironment.NewLineが使用されます。 そのため、実行環境によって改行文字が変わると不都合がある困る場合や、CRまたはLFのみを追加したいといった場合は、AppendLineメソッドの代わりにAppendメソッドを使い、個別に改行文字を書き込む必要があります。

改行文字を書き込む例
using System;
using System.Text;

class Sample {
  static void Main()
  {
    StringBuilder sb = new StringBuilder();

    sb.Append("The quick brown fox jumps over the lazy dog");
    sb.AppendLine(); // 現在の環境での改行文字を追加

    sb.Append("The quick brown fox jumps over the lazy dog");
    sb.Append("\r"); // CRを追加

    sb.Append("The quick brown fox jumps over the lazy dog");
    sb.Append("\r"); // LFを追加

    sb.Append("The quick brown fox jumps over the lazy dog");
    sb.Append("\r\n"); // CRLFを追加
  }
}

これとは別に、StringWriterクラスを使うことによっても、StringBuilderで任意の改行文字を使うようにすることができます。 StreamWriter.NewLineプロパティを変更すると、書き込まれる改行文字を変更することができます。 次の例では、結果が分かりやすいように改行文字を"↵\r\n"に変更しています。 もちろん、改行文字にCRまたはLFのみを指定することもできます。

using System;
using System.IO;
using System.Text;

class Sample {
  static void Main()
  {
    StringBuilder sb = new StringBuilder(); // 空のStringBuilderを作成

    // StringBuilderに文字列を書き込むためのStringWriterを作成
    using (StringWriter writer = new StringWriter(sb)) {
      // 改行文字を変更する
      writer.NewLine = "↵\r\n";

      // 改行文字を付けて文字列を書き込む
      writer.WriteLine("The quick brown fox jumps");
      writer.WriteLine("over the lazy dog");

      // 改行文字のみを書き込む
      writer.WriteLine();

      // 数値を書き込む
      writer.WriteLine(16);
      writer.WriteLine(Math.PI);
    }

    Console.WriteLine(sb);
  }
}
実行結果
The quick brown fox jumps↵
over the lazy dog↵
↵
16↵
3.14159265358979↵

StringWriterクラスについて詳しくはStringReaderクラス/StringWriterクラスをご覧ください。

§3.2 書式を指定した文字列の追加 (AppendFormat)

StringBuilder.AppendFormatメソッドは、指定された書式に整形してからバッファに追加するメソッドです。 String.Formatメソッドによる書式化と文字列の連結を同時に行う操作に相当します。

AppendFormatメソッドでは、String.FormatメソッドやConsole.WriteLineメソッドなどと同様に書式指定子を指定することができます。

using System;
using System.Text;

class Sample {
  static void Main()
  {
    StringBuilder sb = new StringBuilder(); // 空のStringBuilderを作成

    // 書式を指定し、数値72を文字列化して追加
    sb.AppendFormat("{0:D} {0:D4} {0:N4}", 72);

    Console.WriteLine(sb);
  }
}
実行結果
72 0072 72.0000

AppendFormatメソッドでは任意の型の引数を書式化して追加することが出来ますが、Appendメソッドも同様に文字列以外の値を追加することが出来ます。 この際、引数として指定された値は既定の書式で自動的に文字列化されてから追加されます。

using System;
using System.Text;

class Sample {
  static void Main()
  {
    StringBuilder sb = new StringBuilder(); // 空のStringBuilderを作成

    Console.WriteLine("<{0}>", sb);

    // int型の数値を追加
    sb.Append(16);
    sb.AppendLine();

    Console.WriteLine("<{0}>", sb);

    // double型の数値を追加
    sb.Append(Math.PI);
    sb.AppendLine();

    Console.WriteLine("<{0}>", sb);

    // bool型の値を追加
    sb.Append(true);
    sb.Append(false);
    sb.AppendLine();

    Console.WriteLine("<{0}>", sb);
  }
}
実行結果
<>
<16
>
<16
3.14159265358979
>
<16
3.14159265358979
TrueFalse
>

書式化について詳しくは文字列と書式を参照してください。

§3.3 文字列の挿入・削除・置換 (Insert, Remove, Replace)

Stringクラスと同様、文字列の挿入・削除・置換にはStringBuilder.InsertStringBuilder.RemoveStringBuilder.Replaceの各メソッドが使えます。 引数と動作はStringクラスのものとほぼ同じですが、InsertメソッドはAppendメソッド同様に文字列以外も追加出来ます。 なお、String.Removeメソッドとは異なり、StringBuilder.Removeメソッドでは削除する文字数を省略することは出来ません。

using System;
using System.Text;

class Sample {
  static void Main()
  {
    StringBuilder sb = new StringBuilder("The quick brown fox jumps over the lazy dog");

    Console.WriteLine("<{0}>", sb);

    // 4文字目から6文字分を削除
    sb.Remove(4, 6);

    Console.WriteLine("<{0}>", sb);

    // 29文字目から4文字分を削除
    sb.Remove(29, 4);

    Console.WriteLine("<{0}>", sb);

    // 29文字目に数値3を追加
    sb.Insert(29, 3);

    Console.WriteLine("<{0}>", sb);

    // 30文字目に" white "を追加
    sb.Insert(30, " white");

    Console.WriteLine("<{0}>", sb);

    // 文字列"dog"を"dogs"に置換
    sb.Replace("dog", "dogs");

    Console.WriteLine("<{0}>", sb);

    // 文字'o'を'a'に置換
    sb.Replace('o', 'a');

    Console.WriteLine("<{0}>", sb);

    // 文字列"brawn"をnullに置換(=削除)
    sb.Replace("brawn ", null);

    Console.WriteLine("<{0}>", sb);
  }
}
実行結果
<The quick brown fox jumps over the lazy dog>
<The brown fox jumps over the lazy dog>
<The brown fox jumps over the  dog>
<The brown fox jumps over the 3 dog>
<The brown fox jumps over the 3 white dog>
<The brown fox jumps over the 3 white dogs>
<The brawn fax jumps aver the 3 white dags>
<The fax jumps aver the 3 white dags>

§3.4 メソッドチェイン

Append, AppendFormat, AppendLine, Insert, Remove, Replaceの各メソッドは、戻り値としてインスタンス自身を返します。 これにより、stringクラスと同様にメソッドチェインを記述出来ます。

using System;
using System.Text;

class Sample {
  static void Main()
  {
    string s = "The quick brown fox jumps over the lazy dog";
    StringBuilder sb = new StringBuilder("The quick brown fox jumps over the lazy dog");

    Console.WriteLine("String       : <{0}>", s);
    Console.WriteLine("StringBuilder: <{0}>", sb);

    s  =  s.Remove(4, 6).Remove(29, 4).Insert(29, "3").Insert(30, " white").Replace("dog", "dogs").Replace('o', 'a').Replace("brawn ", null);
    sb = sb.Remove(4, 6).Remove(29, 4).Insert(29,  3 ).Insert(30, " white").Replace("dog", "dogs").Replace('o', 'a').Replace("brawn ", null);

    Console.WriteLine("String       : <{0}>", s);
    Console.WriteLine("StringBuilder: <{0}>", sb);
  }
}
実行結果
String       : <The quick brown fox jumps over the lazy dog>
StringBuilder: <The quick brown fox jumps over the lazy dog>
String       : <The fax jumps aver the 3 white dags>
StringBuilder: <The fax jumps aver the 3 white dags>

§4 バッファの操作

インスタンスの内容が不変であるStringクラスとは異なり、内容が可変であるStringBuilderクラスでは内部にサイズが変更可能なバッファを備えています。 ここではStringBuilderのバッファの操作を行うプロパティ・メソッドについて見ていきます。

§4.1 文字列の長さとバッファのクリア (Length, Clear)

StringBuilderに格納されている文字数はLengthプロパティで取得できます。 String.Lengthプロパティとは異なり、Lengthプロパティを設定することで文字列の長さを変更することができます。 Lengthを現在の長さより小さくすると文字列が切り詰められ、大きくするとヌル文字(U+0000)で埋められます。 また、Lengthプロパティを0にすることにより、バッファの内容をクリアすることが出来ます。

using System;
using System.Text;

class Sample {
  static void Main()
  {
    StringBuilder sb = new StringBuilder("The quick brown fox jumps over the lazy dog");

    Console.WriteLine("<{0}> (Length={1})", sb, sb.Length);

    // 長さを18文字分短くする
    sb.Length -= 18;

    Console.WriteLine("<{0}> (Length={1})", sb, sb.Length);

    // 長さを0にする(バッファをクリアする)
    sb.Length = 0;

    Console.WriteLine("<{0}> (Length={1})", sb, sb.Length);

    // 文字列を追加
    sb.Append("The quick brown fox");

    Console.WriteLine("<{0}> (Length={1})", sb, sb.Length);

    // 長さを30にする
    sb.Length = 30;

    Console.WriteLine("<{0}> (Length={1})", sb, sb.Length);

    // ヌル文字を'.'に置換
    sb.Replace('\0', '.');

    Console.WriteLine("<{0}> (Length={1})", sb, sb.Length);
  }
}
実行結果
<The quick brown fox jumps over the lazy dog> (Length=43)
<The quick brown fox jumps> (Length=25)
<> (Length=0)
<The quick brown fox> (Length=19)
<The quick brown fox> (Length=30)
<The quick brown fox...........> (Length=30)

.NET Framework 4からは、バッファのクリアにStringBuilder.Clearメソッドを使うことが出来ます。 結果はLengthに0を指定した場合と同じです。

using System;
using System.Text;

class Sample {
  static void Main()
  {
    StringBuilder sb = new StringBuilder("The quick brown fox jumps over the lazy dog");

    Console.WriteLine("<{0}> (Length={1})", sb, sb.Length);

    // Clearメソッドでバッファをクリアする
    sb.Clear();

    Console.WriteLine("<{0}> (Length={1})", sb, sb.Length);

    // 文字列を追加
    sb.Append("The quick brown fox");

    Console.WriteLine("<{0}> (Length={1})", sb, sb.Length);

    // 長さを0にする
    sb.Length = 0;

    Console.WriteLine("<{0}> (Length={1})", sb, sb.Length);

    // 文字列を追加
    sb.Append("The quick brown fox");

    Console.WriteLine("<{0}> (Length={1})", sb, sb.Length);
  }
}
実行結果
<The quick brown fox jumps over the lazy dog> (Length=43)
<> (Length=0)
<The quick brown fox> (Length=19)
<> (Length=0)
<The quick brown fox> (Length=19)

§4.2 バッファの容量 (Capacity, EnsureCapacity)

StringBuilderクラスでは、常に文字列の実際の長さ以上のバッファが確保されています。 Capacityプロパティはバッファのサイズ(容量)を取得するためのプロパティです。 このプロパティを設定することでバッファのサイズを変更することが出来ます。 ただし、現在の文字列の長さ未満にすることは出来ません。

また、EnsureCapacityメソッドでもバッファのサイズを変更することが出来ますが、このメソッドでは容量を減らすことは出来ず、現在の容量よりも多く確保したい場合に使用します。 EnsureCapacityメソッドでバッファを確保する場合、実際に確保される容量が指定した容量よりも大きくなる場合があります。

StringBuilderのコンストラクタでも容量の初期値を指定してインスタンスを生成することが出来ます(指定しない場合は初期値16でバッファが確保されます)。 StringBuilderに追加しようとする文字列の量をあらかじめ予測出来る場合は、あらかじめ適切な容量のバッファを確保しておくことでバッファ確保によるオーバーヘッドを減らすことが出来ます。

using System;
using System.Text;

class Sample {
  static void Main()
  {
    // 容量8のバッファを確保してインスタンスを作成
    StringBuilder sb = new StringBuilder(8);

    Console.WriteLine("(Length={1}, Capacity={2}) <{0}>", sb, sb.Length, sb.Capacity);

    // バッファのサイズを超える長さの文字列を追加
    sb.Append("The quick brown fox");

    Console.WriteLine("(Length={1}, Capacity={2}) <{0}>", sb, sb.Length, sb.Capacity);

    // 容量を現在の長さの3/2(1.5倍)にする
    sb.Capacity = (sb.Length * 3) / 2;

    Console.WriteLine("(Length={1}, Capacity={2}) <{0}>", sb, sb.Length, sb.Capacity);

    // 文字列を追加
    sb.Append(" jumps over the lazy dog");

    Console.WriteLine("(Length={1}, Capacity={2}) <{0}>", sb, sb.Length, sb.Capacity);

    // 容量を現在の長さまで減らす
    sb.Capacity = sb.Length;

    Console.WriteLine("(Length={1}, Capacity={2}) <{0}>", sb, sb.Length, sb.Capacity);

    // 最小で100の容量を確保する
    sb.EnsureCapacity(100);

    Console.WriteLine("(Length={1}, Capacity={2}) <{0}>", sb, sb.Length, sb.Capacity);

    // 文字列を追加
    sb.Append(" The quick brown fox jumps over the lazy dog");

    Console.WriteLine("(Length={1}, Capacity={2}) <{0}>", sb, sb.Length, sb.Capacity);
  }
}
実行結果
(Length=0, Capacity=8) <>
(Length=19, Capacity=19) <The quick brown fox>
(Length=19, Capacity=38) <The quick brown fox>
(Length=43, Capacity=43) <The quick brown fox jumps over the lazy dog>
(Length=43, Capacity=43) <The quick brown fox jumps over the lazy dog>
(Length=43, Capacity=100) <The quick brown fox jumps over the lazy dog>
(Length=87, Capacity=100) <The quick brown fox jumps over the lazy dog The quick brown fox jumps over the lazy dog>

§4.3 バッファの最大容量 (MaxCapacity)

StringBuilderが確保するバッファの最大容量を指定することも出来ます。 最大容量を指定するには、コンストラクタで指定する必要があります。 特に指定しない場合は初期値Int32.MaxValue(2,147,483,647)が最大容量に指定されます。 インスタンスの最大容量はMaxCapacityプロパティで取得することが出来ます。

文字列を追加しようとしたときやバッファのサイズを変更しようとしたときなど、確保しようとしているバッファの容量が最大容量を超える場合はArgumentOutOfRangeExceptionがスローされます。

using System;
using System.Text;

class Sample {
  static void Main()
  {
    // 容量24のバッファを確保し、最大容量に32を指定してインスタンスを作成
    StringBuilder sb = new StringBuilder(24, 32);

    Console.WriteLine("MaxCapacity: {0}", sb.MaxCapacity);
    Console.WriteLine("(Length={1}, Capacity={2}) <{0}>", sb, sb.Length, sb.Capacity);

    // 文字列を追加
    sb.Append("The quick brown fox");

    Console.WriteLine("(Length={1}, Capacity={2}) <{0}>", sb, sb.Length, sb.Capacity);

    // 追加することで最大容量を超える長さの文字列を追加
    try {
      sb.Append(" jumps over the lazy dog");
    }
    catch (ArgumentException) {
      Console.WriteLine("ArgumentException!!");
    }

    Console.WriteLine("(Length={1}, Capacity={2}) <{0}>", sb, sb.Length, sb.Capacity);
  }
}
実行結果
MaxCapacity: 32
(Length=0, Capacity=24) <>
(Length=19, Capacity=24) <The quick brown fox>
ArgumentException!!
(Length=19, Capacity=19) <The quick brown fox>

§5 StringBuilderのパフォーマンス

StringクラスとStringBuilderクラスの最大の違いは、文字列操作の都度インスタンスが生成されるかどうかですが、これによりパフォーマンスにも違いが現れます。 StringクラスとStringBuilderクラスを用いて文字列の追加を行い、実行時間にどれくらいの差があるかを比較してみます。 次のコードでは、文字列の追加を10,000回行っています。 あらかじめStringBuilderの容量を指定している点にも注目してください。

検証に使ったコード
using System;
using System.Diagnostics;
using System.Text;

class Sample {
  static void Main()
  {
    const int repeat = 10 * 1000;
    const string text = "The quick brown fox jumps over the lazy dog";

    for (var t = 0; t < 3; t++) {
      // Stringクラス
      Stopwatch sw1 = Stopwatch.StartNew();
      string s = string.Empty;

      for (var i = 0; i < repeat; i++) {
        s = s + text;
      }

      sw1.Stop();

      // StringBuilderクラス
      Stopwatch sw2 = Stopwatch.StartNew();
      StringBuilder sb = new StringBuilder(repeat * text.Length);

      for (var i = 0; i < repeat; i++) {
        sb.Append(text);
      }

      sw2.Stop();

      Console.WriteLine(sw1.Elapsed);
      Console.WriteLine(sw2.Elapsed);
      Console.WriteLine("{0:P2}", (double)sw2.ElapsedTicks / (double)sw1.ElapsedTicks);
    }
  }
}
実行結果の一例
00:00:10.8776387
00:00:00.0066486
0.06%
00:00:11.2358833
00:00:00.0066315
0.06%
00:00:11.2715232
00:00:00.0066818
0.06%

結果のとおり、実行時間に大幅な差があります。

続いて、文字列の置換で比較してみます。 次のコードでは文字列の置換を10,000回行っています。 文字列の中間にある文字を置換し、かつ置換した結果文字列が長くなるようにしています。

検証に使ったコード
using System;
using System.Diagnostics;
using System.Text;

class Sample {
  static void Main()
  {
    const int repeat = 10 * 1000;

    for (var t = 0; t < 3; t++) {
      // Stringクラス
      Stopwatch sw1 = Stopwatch.StartNew();
      string s = "foobarbaz";

      for (var i = 0; i < repeat; i++) {
        s = s.Replace("bar", "foobarbaz");
      }

      sw1.Stop();

      // StringBuilderクラス
      Stopwatch sw2 = Stopwatch.StartNew();
      StringBuilder sb = new StringBuilder("foobarbaz", 7 * repeat);

      for (var i = 0; i < repeat; i++) {
        sb.Replace("bar", "foobarbaz");
      }

      sw2.Stop();

      Console.WriteLine(sw1.Elapsed);
      Console.WriteLine(sw2.Elapsed);
      Console.WriteLine("{0:P2}", (double)sw2.ElapsedTicks / (double)sw1.ElapsedTicks);
    }
  }
}
実行結果の一例
00:00:03.3487843
00:00:02.0426295
61.00%
00:00:03.2005363
00:00:01.9556713
61.10%
00:00:03.3387661
00:00:01.8794274
56.29%

先の例ほどの差にはなりませんが、それでもStringBuilderを用いたほうがStringよりも実行時間が短くなっています。 このように文字列操作を繰り返し行う場合は、StringBuilderを使うことでより短い時間で処理を終わらせることが期待できます。