web-dev-qa-db-ja.com

StringBuilderクラスはどのように実装されていますか?追加するたびに、内部で新しい文字列オブジェクトが作成されますか?

StringBuilderクラスはどのように実装されていますか?追加するたびに、内部で新しい文字列オブジェクトが作成されますか?

50
Ram

.NET 2.0では、内部でStringクラスを使用します。 StringSystem名前空間の外部でのみ不変であるため、StringBuilderはそれを実行できます。

.NET 4.0では、Stringchar[]を使用するように変更されました。

2.0ではStringBuilderは次のようになりました

public sealed class StringBuilder : ISerializable
{
    // Fields
    private const string CapacityField = "Capacity";
    internal const int DefaultCapacity = 0x10;
    internal IntPtr m_currentThread;
    internal int m_MaxCapacity;
    internal volatile string m_StringValue; // HERE ----------------------
    private const string MaxCapacityField = "m_MaxCapacity";
    private const string StringValueField = "m_StringValue";
    private const string ThreadIDField = "m_currentThread";

しかし、4.0では次のようになります。

public sealed class StringBuilder : ISerializable
{
    // Fields
    private const string CapacityField = "Capacity";
    internal const int DefaultCapacity = 0x10;
    internal char[] m_ChunkChars; // HERE --------------------------------
    internal int m_ChunkLength;
    internal int m_ChunkOffset;
    internal StringBuilder m_ChunkPrevious;
    internal int m_MaxCapacity;
    private const string MaxCapacityField = "m_MaxCapacity";
    internal const int MaxChunkSize = 0x1f40;
    private const string StringValueField = "m_StringValue";
    private const string ThreadIDField = "m_currentThread";

したがって、明らかに、stringの使用からchar[]の使用に変更されました。

編集:.NET 4の変更を反映するように回答を更新しました(私が発見したばかりです)。

53
Brian Rasmussen

受け入れられた答えは、1マイルもマークを外します。 4.0でのStringBuilderへの重要な変更は、安全でないstringからchar[]への変更ではありません-StringBuilder実際にはStringBuilderインスタンスのリンクリストです。


この変更の理由は明らかです。バッファを再割り当てする必要はありません(より多くのメモリを割り当てるとともに、古いものからすべてのコンテンツをコピーする必要があるため、コストのかかる操作です。新しいものへのバッファ)

これは、最終的な文字列を計算する必要があるため、ToString()の呼び出しが少し遅くなることを意味しますが、多数のAppend()操作を実行することは大幅に もっと早く。これは、StringBuilderの一般的な使用例に適合します。Append()への多数の呼び出しと、それに続くToString()への単一の呼び出しです。


あなたはベンチマークを見つけることができます ここ 。結論?新しいリンクリストStringBuilderはわずかに多くのメモリを使用しますが、一般的なユースケースでは大幅に高速です。

実際にはそうではありません-それは内部文字バッファを使用します。バッファ容量が使い果たされた場合にのみ、新しいバッファが割り当てられます。追加操作はこのバッファに追加するだけで、ToString()メソッドが呼び出されたときに文字列オブジェクトが作成されます。これ以降、従来の文字列連結操作ごとに新しい文字列が作成されるため、多くの文字列連結に使用することをお勧めします。複数の割り当てを回避するための大まかなアイデアがある場合は、文字列ビルダーに初期容量を指定することもできます。

編集:私の理解が間違っていると人々が指摘しています。 答えを無視してください(私はそれを削除しません-それは私の無知の証拠として立つでしょう:-)

7
VinayC

StringBuilderが.NET4でどのように機能するかを示すために、小さなサンプルを作成しました。

public interface ISimpleStringBuilder
{
    ISimpleStringBuilder Append(string value);
    ISimpleStringBuilder Clear();
    int Lenght { get; }
    int Capacity { get; }
}

そしてこれは非常に基本的な実装です

public class SimpleStringBuilder : ISimpleStringBuilder
{
    public const int DefaultCapacity = 32;

    private char[] _internalBuffer;

    public int Lenght { get; private set; }
    public int Capacity { get; private set; }

    public SimpleStringBuilder(int capacity)
    {
        Capacity = capacity;
        _internalBuffer = new char[capacity];
        Lenght = 0;
    }

    public SimpleStringBuilder() : this(DefaultCapacity) { }

    public ISimpleStringBuilder Append(string value)
    {
        char[] data = value.ToCharArray();

        //check if space is available for additional data
        InternalEnsureCapacity(data.Length);

        foreach (char t in data)
        {
            _internalBuffer[Lenght] = t;
            Lenght++;
        }

        return this;
    }

    public ISimpleStringBuilder Clear()
    {
        _internalBuffer = new char[Capacity];
        Lenght = 0;
        return this;
    }

    public override string ToString()
    {
        //use only non-null ('\0') characters
        var tmp = new char[Lenght];
        for (int i = 0; i < Lenght; i++)
        {
            tmp[i] = _internalBuffer[i];
        }
        return new string(tmp);
    }

    private void InternalExpandBuffer()
    {
        //double capacity by default
        Capacity *= 2;

        //copy to new array
        var tmpBuffer = new char[Capacity];
        for (int i = 0; i < _internalBuffer.Length; i++)
        {
            char c = _internalBuffer[i];
            tmpBuffer[i] = c;
        }
        _internalBuffer = tmpBuffer;
    }

    private void InternalEnsureCapacity(int additionalLenghtRequired)
    {
        while (Lenght + additionalLenghtRequired > Capacity)
        {
            //not enough space in the current buffer    
            //double capacity
            InternalExpandBuffer();
        }
    }
}

このコードはスレッドセーフではなく、入力検証も行わず、System.Stringの内部(安全でない)マジックを使用していません。ただし、StringBuilderクラスの背後にある考え方を示しています。

いくつかのユニットテストと完全なサンプルコードは github にあります。

3
oleksii

可能な実装の1つ(v3.5までのMicrosoft実装で出荷されたものと同様)を確認したい場合は、githubで Monoのソース を確認できます。

2

.NET2で.NETReflectorを見ると、次のことがわかります。

public StringBuilder Append(string value)
{
    if (value != null)
    {
        string stringValue = this.m_StringValue;
        IntPtr currentThread = Thread.InternalGetCurrentThread();
        if (this.m_currentThread != currentThread)
        {
            stringValue = string.GetStringForStringBuilder(stringValue, stringValue.Capacity);
        }
        int length = stringValue.Length;
        int requiredLength = length + value.Length;
        if (this.NeedsAllocation(stringValue, requiredLength))
        {
            string newString = this.GetNewString(stringValue, requiredLength);
            newString.AppendInPlace(value, length);
            this.ReplaceString(currentThread, newString);
        }
        else
        {
            stringValue.AppendInPlace(value, length);
            this.ReplaceString(currentThread, stringValue);
        }
    }
    return this;
}

つまり、それは変異した文字列インスタンスです...

編集.NET4を除いて、それはchar[]

2
Yves M.