diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs b/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs
index 83f9306b16ee71..28a6c1e32125b3 100644
--- a/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/Text/StringBuilder.cs
@@ -381,6 +381,44 @@ public StringBuilder Clear()
return this;
}
+ ///
+ /// Creates a new instance initialized to the same state as
+ /// , and resets to an empty, usable state
+ /// with no allocated buffers.
+ ///
+ /// The whose chunks should be moved to the
+ /// returned instance.
+ /// A new instance that owns the chunks previously held
+ /// by .
+ /// is .
+ ///
+ ///
+ /// In contrast to , which retains the existing internal buffer,
+ /// this method releases all internal buffers from . Ownership of
+ /// the chunks is transferred in O(1) to the returned ; the
+ /// underlying character data is not copied.
+ ///
+ ///
+ /// After the call, has and
+ /// of zero but retains its original .
+ /// It remains fully usable; subsequent append or insert operations will allocate new
+ /// buffers as needed.
+ ///
+ ///
+ public static StringBuilder MoveChunks(StringBuilder source)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+
+ StringBuilder destination = new StringBuilder(source);
+
+ source.m_ChunkChars = [];
+ source.m_ChunkPrevious = null;
+ source.m_ChunkLength = 0;
+ source.m_ChunkOffset = 0;
+
+ return destination;
+ }
+
///
/// Gets or sets the length of this builder.
///
diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs
index e5cb2b3b05da45..9041d080579cd4 100644
--- a/src/libraries/System.Runtime/ref/System.Runtime.cs
+++ b/src/libraries/System.Runtime/ref/System.Runtime.cs
@@ -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 destination, int count) { }
public int EnsureCapacity(int capacity) { throw null; }
diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/StringBuilderTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/StringBuilderTests.cs
index e9a9741a4b1fc1..5adc464a126507 100644
--- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/StringBuilderTests.cs
+++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/Text/StringBuilderTests.cs
@@ -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;
@@ -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("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 chunk in source.GetChunks())
+ {
+ Assert.True(MemoryMarshal.TryGetArray(chunk, out ArraySegment segment));
+ originalChunks.Add((segment.Array!, segment.Offset, segment.Count));
+ }
+
+ StringBuilder destination = StringBuilder.MoveChunks(source);
+
+ Assert.Equal(expected, destination.ToString());
+
+ int i = 0;
+ foreach (ReadOnlyMemory chunk in destination.GetChunks())
+ {
+ Assert.True(MemoryMarshal.TryGetArray(chunk, out ArraySegment 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());
+ }
+
+ [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(), 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' })]