Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,44 @@ public StringBuilder Clear()
return this;
}

/// <summary>
/// Creates a new <see cref="StringBuilder"/> instance initialized to the same state as
/// <paramref name="source"/>, and resets <paramref name="source"/> to an empty, usable state
/// with no allocated buffers.
/// </summary>
/// <param name="source">The <see cref="StringBuilder"/> whose chunks should be moved to the
/// returned instance.</param>
/// <returns>A new <see cref="StringBuilder"/> instance that owns the chunks previously held
/// by <paramref name="source"/>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="source"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// In contrast to <see cref="Clear"/>, which retains the existing internal buffer,
Comment thread
tannergooding marked this conversation as resolved.
/// this method releases all internal buffers from <paramref name="source"/>. Ownership of
/// the chunks is transferred in O(1) to the returned <see cref="StringBuilder"/>; the
/// underlying character data is not copied.
/// </para>
/// <para>
/// After the call, <paramref name="source"/> has <see cref="Length"/> and
/// <see cref="Capacity"/> of zero but retains its original <see cref="MaxCapacity"/>.
/// It remains fully usable; subsequent append or insert operations will allocate new
/// buffers as needed.
/// </para>
/// </remarks>
Comment thread
tannergooding marked this conversation as resolved.
public static StringBuilder MoveChunks(StringBuilder source)
Comment thread
tannergooding marked this conversation as resolved.
{
ArgumentNullException.ThrowIfNull(source);

StringBuilder destination = new StringBuilder(source);
Comment thread
eiriktsarpalis marked this conversation as resolved.

source.m_ChunkChars = [];
source.m_ChunkPrevious = null;
source.m_ChunkLength = 0;
source.m_ChunkOffset = 0;

return destination;
}

/// <summary>
/// Gets or sets the length of this builder.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Runtime/ref/System.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16225,6 +16225,7 @@ public StringBuilder(string? value, int startIndex, int length, int capacity) {
public System.Text.StringBuilder AppendLine(string? value) { throw null; }
public System.Text.StringBuilder AppendLine([System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute("")] ref System.Text.StringBuilder.AppendInterpolatedStringHandler handler) { throw null; }
public System.Text.StringBuilder Clear() { throw null; }
public static System.Text.StringBuilder MoveChunks(System.Text.StringBuilder source) { throw null; }
public void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count) { }
public void CopyTo(int sourceIndex, System.Span<char> destination, int count) { }
public int EnsureCapacity(int capacity) { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Tests;
using Microsoft.DotNet.RemoteExecutor;
using Xunit;
Expand Down Expand Up @@ -2129,6 +2131,123 @@ public static void Clear_StringBuilderHasTwoChunks_OneChunkIsEmpty_ClearReducesC
Assert.Equal(initialCapacity, sb.Capacity);
}

[Fact]
public static void MoveChunks_NullSource_ThrowsArgumentNullException()
{
AssertExtensions.Throws<ArgumentNullException>("source", () => StringBuilder.MoveChunks(null));
}

[Fact]
public static void MoveChunks_Empty_ProducesEmptyDestinationAndDrainsSource()
{
var source = new StringBuilder(32, 64);
char[] originalChars = GetChunkCharsField(source);

StringBuilder destination = StringBuilder.MoveChunks(source);

Assert.NotNull(destination);
Assert.NotSame(source, destination);
Assert.Equal(0, destination.Length);
Assert.Equal(32, destination.Capacity);
Assert.Equal(64, destination.MaxCapacity);
Assert.Same(originalChars, GetChunkCharsField(destination));

AssertSourceIsDrained(source);
}

[Fact]
public static void MoveChunks_SingleChunk_TransfersContentsAndDrainsSource()
{
var source = new StringBuilder(16, 100);
source.Append("Hello");
char[] originalChars = GetChunkCharsField(source);

StringBuilder destination = StringBuilder.MoveChunks(source);

Assert.Equal("Hello", destination.ToString());
Assert.Equal(100, destination.MaxCapacity);
Assert.Same(originalChars, GetChunkCharsField(destination));

AssertSourceIsDrained(source);
}

[Fact]
public static void MoveChunks_MultipleChunks_TransfersChainAndDrainsSource()
{
StringBuilder source = StringBuilderWithMultipleChunks();
string expected = source.ToString();

// Capture the backing char[] arrays by identity to verify no-copy move semantics.
List<(char[] Array, int Offset, int Count)> originalChunks = new List<(char[], int, int)>();
foreach (ReadOnlyMemory<char> chunk in source.GetChunks())
{
Assert.True(MemoryMarshal.TryGetArray(chunk, out ArraySegment<char> segment));
originalChunks.Add((segment.Array!, segment.Offset, segment.Count));
}

StringBuilder destination = StringBuilder.MoveChunks(source);

Assert.Equal(expected, destination.ToString());

int i = 0;
foreach (ReadOnlyMemory<char> chunk in destination.GetChunks())
{
Assert.True(MemoryMarshal.TryGetArray(chunk, out ArraySegment<char> segment));
Assert.Same(originalChunks[i].Array, segment.Array);
Assert.Equal(originalChunks[i].Offset, segment.Offset);
Assert.Equal(originalChunks[i].Count, segment.Count);
i++;
}
Assert.Equal(originalChunks.Count, i);

AssertSourceIsDrained(source);
}

[Fact]
public static void MoveChunks_DrainedSource_RemainsUsable()
{
var source = new StringBuilder("abc");
int originalMaxCapacity = source.MaxCapacity;
StringBuilder destination = StringBuilder.MoveChunks(source);

Assert.Equal("abc", destination.ToString());
Assert.Equal(originalMaxCapacity, source.MaxCapacity);

// source is empty but fully usable; subsequent appends allocate new buffers.
source.Append('x');
Assert.Equal("x", source.ToString());
}
Comment thread
tannergooding marked this conversation as resolved.

[Fact]
public static void MoveChunks_AlreadyDrainedSource_ProducesEmptyDestination()
{
var source = new StringBuilder("abc");
int originalMaxCapacity = source.MaxCapacity;
_ = StringBuilder.MoveChunks(source);

// MoveChunks on an already-drained (empty) source produces an empty destination.
StringBuilder destination = StringBuilder.MoveChunks(source);

Assert.Equal(0, destination.Length);
Assert.Equal(0, destination.Capacity);
Assert.Equal(originalMaxCapacity, destination.MaxCapacity);
AssertSourceIsDrained(source);
}

private static readonly FieldInfo s_chunkCharsField = typeof(StringBuilder).GetField("m_ChunkChars", BindingFlags.Instance | BindingFlags.NonPublic)!;

private static char[] GetChunkCharsField(StringBuilder builder)
{
return (char[])s_chunkCharsField.GetValue(builder)!;
}

private static void AssertSourceIsDrained(StringBuilder source)
{
Assert.Equal(0, source.Length);
Assert.Equal(0, source.Capacity);
Assert.Same(Array.Empty<char>(), GetChunkCharsField(source));
}

[Theory]
[InlineData("Hello", 0, new char[] { '\0', '\0', '\0', '\0', '\0' }, 5, new char[] { 'H', 'e', 'l', 'l', 'o' })]
[InlineData("Hello", 0, new char[] { '\0', '\0', '\0', '\0' }, 4, new char[] { 'H', 'e', 'l', 'l' })]
Expand Down
Loading