More corelib cleanup (dotnet/coreclr#26993)
[mono-project.git] / netcore / System.Private.CoreLib / shared / System / Text / StringBuilder.cs
blob4b054024fe799c566fc791c767253bc2335c63e4
1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 using System.Runtime.Serialization;
6 using System.Runtime.CompilerServices;
7 using System.Runtime.InteropServices;
8 using System.Diagnostics;
9 using System.Collections.Generic;
11 namespace System.Text
13 // This class represents a mutable string. It is convenient for situations in
14 // which it is desirable to modify a string, perhaps by removing, replacing, or
15 // inserting characters, without creating a new String subsequent to
16 // each modification.
18 // The methods contained within this class do not return a new StringBuilder
19 // object unless specified otherwise. This class may be used in conjunction with the String
20 // class to carry out modifications upon strings.
21 [Serializable]
22 [System.Runtime.CompilerServices.TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
23 public sealed partial class StringBuilder : ISerializable
25 // A StringBuilder is internally represented as a linked list of blocks each of which holds
26 // a chunk of the string. It turns out string as a whole can also be represented as just a chunk,
27 // so that is what we do.
29 /// <summary>
30 /// The character buffer for this chunk.
31 /// </summary>
32 internal char[] m_ChunkChars;
34 /// <summary>
35 /// The chunk that logically precedes this chunk.
36 /// </summary>
37 internal StringBuilder? m_ChunkPrevious;
39 /// <summary>
40 /// The number of characters in this chunk.
41 /// This is the number of elements in <see cref="m_ChunkChars"/> that are in use, from the start of the buffer.
42 /// </summary>
43 internal int m_ChunkLength;
45 /// <summary>
46 /// The logical offset of this chunk's characters in the string it is a part of.
47 /// This is the sum of the number of characters in preceding blocks.
48 /// </summary>
49 internal int m_ChunkOffset;
51 /// <summary>
52 /// The maximum capacity this builder is allowed to have.
53 /// </summary>
54 internal int m_MaxCapacity;
56 /// <summary>
57 /// The default capacity of a <see cref="StringBuilder"/>.
58 /// </summary>
59 internal const int DefaultCapacity = 16;
61 private const string CapacityField = "Capacity"; // Do not rename (binary serialization)
62 private const string MaxCapacityField = "m_MaxCapacity"; // Do not rename (binary serialization)
63 private const string StringValueField = "m_StringValue"; // Do not rename (binary serialization)
64 private const string ThreadIDField = "m_currentThread"; // Do not rename (binary serialization)
66 // We want to keep chunk arrays out of large object heap (< 85K bytes ~ 40K chars) to be sure.
67 // Making the maximum chunk size big means less allocation code called, but also more waste
68 // in unused characters and slower inserts / replaces (since you do need to slide characters over
69 // within a buffer).
70 internal const int MaxChunkSize = 8000;
72 /// <summary>
73 /// Initializes a new instance of the <see cref="StringBuilder"/> class.
74 /// </summary>
75 public StringBuilder()
77 m_MaxCapacity = int.MaxValue;
78 m_ChunkChars = new char[DefaultCapacity];
81 /// <summary>
82 /// Initializes a new instance of the <see cref="StringBuilder"/> class.
83 /// </summary>
84 /// <param name="capacity">The initial capacity of this builder.</param>
85 public StringBuilder(int capacity)
86 : this(capacity, int.MaxValue)
90 /// <summary>
91 /// Initializes a new instance of the <see cref="StringBuilder"/> class.
92 /// </summary>
93 /// <param name="value">The initial contents of this builder.</param>
94 public StringBuilder(string? value)
95 : this(value, DefaultCapacity)
99 /// <summary>
100 /// Initializes a new instance of the <see cref="StringBuilder"/> class.
101 /// </summary>
102 /// <param name="value">The initial contents of this builder.</param>
103 /// <param name="capacity">The initial capacity of this builder.</param>
104 public StringBuilder(string? value, int capacity)
105 : this(value, 0, value?.Length ?? 0, capacity)
109 /// <summary>
110 /// Initializes a new instance of the <see cref="StringBuilder"/> class.
111 /// </summary>
112 /// <param name="value">The initial contents of this builder.</param>
113 /// <param name="startIndex">The index to start in <paramref name="value"/>.</param>
114 /// <param name="length">The number of characters to read in <paramref name="value"/>.</param>
115 /// <param name="capacity">The initial capacity of this builder.</param>
116 public StringBuilder(string? value, int startIndex, int length, int capacity)
118 if (capacity < 0)
120 throw new ArgumentOutOfRangeException(nameof(capacity), SR.Format(SR.ArgumentOutOfRange_MustBePositive, nameof(capacity)));
122 if (length < 0)
124 throw new ArgumentOutOfRangeException(nameof(length), SR.Format(SR.ArgumentOutOfRange_MustBeNonNegNum, nameof(length)));
126 if (startIndex < 0)
128 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_StartIndex);
131 if (value == null)
133 value = string.Empty;
135 if (startIndex > value.Length - length)
137 throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_IndexLength);
140 m_MaxCapacity = int.MaxValue;
141 if (capacity == 0)
143 capacity = DefaultCapacity;
145 capacity = Math.Max(capacity, length);
147 m_ChunkChars = new char[capacity];
148 m_ChunkLength = length;
150 unsafe
152 fixed (char* sourcePtr = value)
154 ThreadSafeCopy(sourcePtr + startIndex, m_ChunkChars, 0, length);
159 /// <summary>
160 /// Initializes a new instance of the <see cref="StringBuilder"/> class.
161 /// </summary>
162 /// <param name="capacity">The initial capacity of this builder.</param>
163 /// <param name="maxCapacity">The maximum capacity of this builder.</param>
164 public StringBuilder(int capacity, int maxCapacity)
166 if (capacity > maxCapacity)
168 throw new ArgumentOutOfRangeException(nameof(capacity), SR.ArgumentOutOfRange_Capacity);
170 if (maxCapacity < 1)
172 throw new ArgumentOutOfRangeException(nameof(maxCapacity), SR.ArgumentOutOfRange_SmallMaxCapacity);
174 if (capacity < 0)
176 throw new ArgumentOutOfRangeException(nameof(capacity), SR.Format(SR.ArgumentOutOfRange_MustBePositive, nameof(capacity)));
179 if (capacity == 0)
181 capacity = Math.Min(DefaultCapacity, maxCapacity);
184 m_MaxCapacity = maxCapacity;
185 m_ChunkChars = new char[capacity];
188 private StringBuilder(SerializationInfo info, StreamingContext context)
190 if (info == null)
192 throw new ArgumentNullException(nameof(info));
195 int persistedCapacity = 0;
196 string? persistedString = null;
197 int persistedMaxCapacity = int.MaxValue;
198 bool capacityPresent = false;
200 // Get the data
201 SerializationInfoEnumerator enumerator = info.GetEnumerator();
202 while (enumerator.MoveNext())
204 switch (enumerator.Name)
206 case MaxCapacityField:
207 persistedMaxCapacity = info.GetInt32(MaxCapacityField);
208 break;
209 case StringValueField:
210 persistedString = info.GetString(StringValueField);
211 break;
212 case CapacityField:
213 persistedCapacity = info.GetInt32(CapacityField);
214 capacityPresent = true;
215 break;
216 default:
217 // Ignore other fields for forwards-compatibility.
218 break;
222 // Check values and set defaults
223 if (persistedString == null)
225 persistedString = string.Empty;
227 if (persistedMaxCapacity < 1 || persistedString.Length > persistedMaxCapacity)
229 throw new SerializationException(SR.Serialization_StringBuilderMaxCapacity);
232 if (!capacityPresent)
234 // StringBuilder in V1.X did not persist the Capacity, so this is a valid legacy code path.
235 persistedCapacity = Math.Min(Math.Max(DefaultCapacity, persistedString.Length), persistedMaxCapacity);
238 if (persistedCapacity < 0 || persistedCapacity < persistedString.Length || persistedCapacity > persistedMaxCapacity)
240 throw new SerializationException(SR.Serialization_StringBuilderCapacity);
243 // Assign
244 m_MaxCapacity = persistedMaxCapacity;
245 m_ChunkChars = new char[persistedCapacity];
246 persistedString.CopyTo(0, m_ChunkChars, 0, persistedString.Length);
247 m_ChunkLength = persistedString.Length;
248 m_ChunkPrevious = null;
249 AssertInvariants();
252 void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
254 if (info == null)
256 throw new ArgumentNullException(nameof(info));
259 AssertInvariants();
260 info.AddValue(MaxCapacityField, m_MaxCapacity);
261 info.AddValue(CapacityField, Capacity);
262 info.AddValue(StringValueField, ToString());
263 // Note: persist "m_currentThread" to be compatible with old versions
264 info.AddValue(ThreadIDField, 0);
267 [System.Diagnostics.Conditional("DEBUG")]
268 private void AssertInvariants()
270 Debug.Assert(m_ChunkOffset + m_ChunkChars.Length >= m_ChunkOffset, "The length of the string is greater than int.MaxValue.");
272 StringBuilder currentBlock = this;
273 int maxCapacity = this.m_MaxCapacity;
274 while (true)
276 // All blocks have the same max capacity.
277 Debug.Assert(currentBlock.m_MaxCapacity == maxCapacity);
278 Debug.Assert(currentBlock.m_ChunkChars != null);
280 Debug.Assert(currentBlock.m_ChunkLength <= currentBlock.m_ChunkChars.Length);
281 Debug.Assert(currentBlock.m_ChunkLength >= 0);
282 Debug.Assert(currentBlock.m_ChunkOffset >= 0);
284 StringBuilder? prevBlock = currentBlock.m_ChunkPrevious;
285 if (prevBlock == null)
287 Debug.Assert(currentBlock.m_ChunkOffset == 0);
288 break;
290 // There are no gaps in the blocks.
291 Debug.Assert(currentBlock.m_ChunkOffset == prevBlock.m_ChunkOffset + prevBlock.m_ChunkLength);
292 currentBlock = prevBlock;
296 public int Capacity
298 get => m_ChunkChars.Length + m_ChunkOffset;
301 if (value < 0)
303 throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_NegativeCapacity);
305 if (value > MaxCapacity)
307 throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_Capacity);
309 if (value < Length)
311 throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_SmallCapacity);
314 if (Capacity != value)
316 int newLen = value - m_ChunkOffset;
317 char[] newArray = new char[newLen];
318 Array.Copy(m_ChunkChars, 0, newArray, 0, m_ChunkLength);
319 m_ChunkChars = newArray;
324 /// <summary>
325 /// Gets the maximum capacity this builder is allowed to have.
326 /// </summary>
327 public int MaxCapacity => m_MaxCapacity;
329 /// <summary>
330 /// Ensures that the capacity of this builder is at least the specified value.
331 /// </summary>
332 /// <param name="capacity">The new capacity for this builder.</param>
333 /// <remarks>
334 /// If <paramref name="capacity"/> is less than or equal to the current capacity of
335 /// this builder, the capacity remains unchanged.
336 /// </remarks>
337 public int EnsureCapacity(int capacity)
339 if (capacity < 0)
341 throw new ArgumentOutOfRangeException(nameof(capacity), SR.ArgumentOutOfRange_NegativeCapacity);
344 if (Capacity < capacity)
346 Capacity = capacity;
348 return Capacity;
351 public override string ToString()
353 AssertInvariants();
355 if (Length == 0)
357 return string.Empty;
360 string result = string.FastAllocateString(Length);
361 StringBuilder? chunk = this;
362 unsafe
364 fixed (char* destinationPtr = result)
368 if (chunk.m_ChunkLength > 0)
370 // Copy these into local variables so that they are stable even in the presence of race conditions
371 char[] sourceArray = chunk.m_ChunkChars;
372 int chunkOffset = chunk.m_ChunkOffset;
373 int chunkLength = chunk.m_ChunkLength;
375 // Check that we will not overrun our boundaries.
376 if ((uint)(chunkLength + chunkOffset) <= (uint)result.Length && (uint)chunkLength <= (uint)sourceArray.Length)
378 fixed (char* sourcePtr = &sourceArray[0])
379 string.wstrcpy(destinationPtr + chunkOffset, sourcePtr, chunkLength);
381 else
383 throw new ArgumentOutOfRangeException(nameof(chunkLength), SR.ArgumentOutOfRange_Index);
386 chunk = chunk.m_ChunkPrevious;
388 while (chunk != null);
390 return result;
395 /// <summary>
396 /// Creates a string from a substring of this builder.
397 /// </summary>
398 /// <param name="startIndex">The index to start in this builder.</param>
399 /// <param name="length">The number of characters to read in this builder.</param>
400 public string ToString(int startIndex, int length)
402 int currentLength = this.Length;
403 if (startIndex < 0)
405 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_StartIndex);
407 if (startIndex > currentLength)
409 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_StartIndexLargerThanLength);
411 if (length < 0)
413 throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_NegativeLength);
415 if (startIndex > currentLength - length)
417 throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_IndexLength);
420 AssertInvariants();
421 string result = string.FastAllocateString(length);
422 unsafe
424 fixed (char* destinationPtr = result)
426 this.CopyTo(startIndex, new Span<char>(destinationPtr, length), length);
427 return result;
432 public StringBuilder Clear()
434 this.Length = 0;
435 return this;
438 /// <summary>
439 /// Gets or sets the length of this builder.
440 /// </summary>
441 public int Length
443 get => m_ChunkOffset + m_ChunkLength;
446 // If the new length is less than 0 or greater than our Maximum capacity, bail.
447 if (value < 0)
449 throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_NegativeLength);
452 if (value > MaxCapacity)
454 throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_SmallCapacity);
457 if (value == 0 && m_ChunkPrevious == null)
459 m_ChunkLength = 0;
460 m_ChunkOffset = 0;
461 return;
464 int delta = value - Length;
465 if (delta > 0)
467 // Pad ourselves with null characters.
468 Append('\0', delta);
470 else
472 StringBuilder chunk = FindChunkForIndex(value);
473 if (chunk != this)
475 // Avoid possible infinite capacity growth. See https://github.com/dotnet/coreclr/pull/16926
476 int capacityToPreserve = Math.Min(Capacity, Math.Max(Length * 6 / 5, m_ChunkChars.Length));
477 int newLen = capacityToPreserve - chunk.m_ChunkOffset;
478 if (newLen > chunk.m_ChunkChars.Length)
480 // We crossed a chunk boundary when reducing the Length. We must replace this middle-chunk with a new larger chunk,
481 // to ensure the capacity we want is preserved.
482 char[] newArray = new char[newLen];
483 Array.Copy(chunk.m_ChunkChars, 0, newArray, 0, chunk.m_ChunkLength);
484 m_ChunkChars = newArray;
486 else
488 // Special case where the capacity we want to keep corresponds exactly to the size of the content.
489 // Just take ownership of the array.
490 Debug.Assert(newLen == chunk.m_ChunkChars.Length, "The new chunk should be larger or equal to the one it is replacing.");
491 m_ChunkChars = chunk.m_ChunkChars;
494 m_ChunkPrevious = chunk.m_ChunkPrevious;
495 m_ChunkOffset = chunk.m_ChunkOffset;
497 m_ChunkLength = value - chunk.m_ChunkOffset;
498 AssertInvariants();
500 Debug.Assert(Length == value, "Something went wrong setting Length.");
504 [IndexerName("Chars")]
505 public char this[int index]
509 StringBuilder? chunk = this;
510 while (true)
512 int indexInBlock = index - chunk.m_ChunkOffset;
513 if (indexInBlock >= 0)
515 if (indexInBlock >= chunk.m_ChunkLength)
517 throw new IndexOutOfRangeException();
519 return chunk.m_ChunkChars[indexInBlock];
521 chunk = chunk.m_ChunkPrevious;
522 if (chunk == null)
524 throw new IndexOutOfRangeException();
530 StringBuilder? chunk = this;
531 while (true)
533 int indexInBlock = index - chunk.m_ChunkOffset;
534 if (indexInBlock >= 0)
536 if (indexInBlock >= chunk.m_ChunkLength)
538 throw new ArgumentOutOfRangeException(nameof(index), SR.ArgumentOutOfRange_Index);
540 chunk.m_ChunkChars[indexInBlock] = value;
541 return;
543 chunk = chunk.m_ChunkPrevious;
544 if (chunk == null)
546 throw new ArgumentOutOfRangeException(nameof(index), SR.ArgumentOutOfRange_Index);
552 /// <summary>
553 /// GetChunks returns ChunkEnumerator that follows the IEnumerable pattern and
554 /// thus can be used in a C# 'foreach' statements to retrieve the data in the StringBuilder
555 /// as chunks (ReadOnlyMemory) of characters. An example use is:
557 /// foreach (ReadOnlyMemory&lt;char&gt; chunk in sb.GetChunks())
558 /// foreach (char c in chunk.Span)
559 /// { /* operation on c }
561 /// It is undefined what happens if the StringBuilder is modified while the chunk
562 /// enumeration is incomplete. StringBuilder is also not thread-safe, so operating
563 /// on it with concurrent threads is illegal. Finally the ReadOnlyMemory chunks returned
564 /// are NOT guarenteed to remain unchanged if the StringBuilder is modified, so do
565 /// not cache them for later use either. This API's purpose is efficiently extracting
566 /// the data of a CONSTANT StringBuilder.
568 /// Creating a ReadOnlySpan from a ReadOnlyMemory (the .Span property) is expensive
569 /// compared to the fetching of the character, so create a local variable for the SPAN
570 /// if you need to use it in a nested for statement. For example
572 /// foreach (ReadOnlyMemory&lt;char&gt; chunk in sb.GetChunks())
573 /// {
574 /// var span = chunk.Span;
575 /// for (int i = 0; i &lt; span.Length; i++)
576 /// { /* operation on span[i] */ }
577 /// }
578 /// </summary>
579 public ChunkEnumerator GetChunks() => new ChunkEnumerator(this);
581 /// <summary>
582 /// ChunkEnumerator supports both the IEnumerable and IEnumerator pattern so foreach
583 /// works (see GetChunks). It needs to be public (so the compiler can use it
584 /// when building a foreach statement) but users typically don't use it explicitly.
585 /// (which is why it is a nested type).
586 /// </summary>
587 public struct ChunkEnumerator
589 private readonly StringBuilder _firstChunk; // The first Stringbuilder chunk (which is the end of the logical string)
590 private StringBuilder? _currentChunk; // The chunk that this enumerator is currently returning (Current).
591 private readonly ManyChunkInfo? _manyChunks; // Only used for long string builders with many chunks (see constructor)
593 /// <summary>
594 /// Implement IEnumerable.GetEnumerator() to return 'this' as the IEnumerator
595 /// </summary>
596 [ComponentModel.EditorBrowsable(ComponentModel.EditorBrowsableState.Never)] // Only here to make foreach work
597 public ChunkEnumerator GetEnumerator() { return this; }
599 /// <summary>
600 /// Implements the IEnumerator pattern.
601 /// </summary>
602 public bool MoveNext()
604 if (_currentChunk == _firstChunk)
606 return false;
610 if (_manyChunks != null)
612 return _manyChunks.MoveNext(ref _currentChunk);
615 StringBuilder next = _firstChunk;
616 while (next.m_ChunkPrevious != _currentChunk)
618 Debug.Assert(next.m_ChunkPrevious != null);
619 next = next.m_ChunkPrevious;
621 _currentChunk = next;
622 return true;
625 /// <summary>
626 /// Implements the IEnumerator pattern.
627 /// </summary>
628 public ReadOnlyMemory<char> Current
632 if (_currentChunk == null)
634 ThrowHelper.ThrowInvalidOperationException_InvalidOperation_EnumOpCantHappen();
637 return new ReadOnlyMemory<char>(_currentChunk.m_ChunkChars, 0, _currentChunk.m_ChunkLength);
641 #region private
642 internal ChunkEnumerator(StringBuilder stringBuilder)
644 Debug.Assert(stringBuilder != null);
645 _firstChunk = stringBuilder;
646 _currentChunk = null; // MoveNext will find the last chunk if we do this.
647 _manyChunks = null;
649 // There is a performance-vs-allocation tradeoff. Because the chunks
650 // are a linked list with each chunk pointing to its PREDECESSOR, walking
651 // the list FORWARD is not efficient. If there are few chunks (< 8) we
652 // simply scan from the start each time, and tolerate the N*N behavior.
653 // However above this size, we allocate an array to hold pointers to all
654 // the chunks and we can be efficient for large N.
655 int chunkCount = ChunkCount(stringBuilder);
656 if (8 < chunkCount)
658 _manyChunks = new ManyChunkInfo(stringBuilder, chunkCount);
662 private static int ChunkCount(StringBuilder? stringBuilder)
664 int ret = 0;
665 while (stringBuilder != null)
667 ret++;
668 stringBuilder = stringBuilder.m_ChunkPrevious;
670 return ret;
673 /// <summary>
674 /// Used to hold all the chunks indexes when you have many chunks.
675 /// </summary>
676 private class ManyChunkInfo
678 private readonly StringBuilder[] _chunks; // These are in normal order (first chunk first)
679 private int _chunkPos;
681 public bool MoveNext(ref StringBuilder? current)
683 int pos = ++_chunkPos;
684 if (_chunks.Length <= pos)
686 return false;
688 current = _chunks[pos];
689 return true;
692 public ManyChunkInfo(StringBuilder? stringBuilder, int chunkCount)
694 _chunks = new StringBuilder[chunkCount];
695 while (0 <= --chunkCount)
697 Debug.Assert(stringBuilder != null);
698 _chunks[chunkCount] = stringBuilder;
699 stringBuilder = stringBuilder.m_ChunkPrevious;
701 _chunkPos = -1;
704 #endregion
707 /// <summary>
708 /// Appends a character 0 or more times to the end of this builder.
709 /// </summary>
710 /// <param name="value">The character to append.</param>
711 /// <param name="repeatCount">The number of times to append <paramref name="value"/>.</param>
712 public StringBuilder Append(char value, int repeatCount)
714 if (repeatCount < 0)
716 throw new ArgumentOutOfRangeException(nameof(repeatCount), SR.ArgumentOutOfRange_NegativeCount);
719 if (repeatCount == 0)
721 return this;
724 // this is where we can check if the repeatCount will put us over m_MaxCapacity
725 // We are doing the check here to prevent the corruption of the StringBuilder.
726 int newLength = Length + repeatCount;
727 if (newLength > m_MaxCapacity || newLength < repeatCount)
729 throw new ArgumentOutOfRangeException(nameof(repeatCount), SR.ArgumentOutOfRange_LengthGreaterThanCapacity);
732 int index = m_ChunkLength;
733 while (repeatCount > 0)
735 if (index < m_ChunkChars.Length)
737 m_ChunkChars[index++] = value;
738 --repeatCount;
740 else
742 m_ChunkLength = index;
743 ExpandByABlock(repeatCount);
744 Debug.Assert(m_ChunkLength == 0);
745 index = 0;
749 m_ChunkLength = index;
750 AssertInvariants();
751 return this;
754 /// <summary>
755 /// Appends a range of characters to the end of this builder.
756 /// </summary>
757 /// <param name="value">The characters to append.</param>
758 /// <param name="startIndex">The index to start in <paramref name="value"/>.</param>
759 /// <param name="charCount">The number of characters to read in <paramref name="value"/>.</param>
760 public StringBuilder Append(char[]? value, int startIndex, int charCount)
762 if (startIndex < 0)
764 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_GenericPositive);
766 if (charCount < 0)
768 throw new ArgumentOutOfRangeException(nameof(charCount), SR.ArgumentOutOfRange_GenericPositive);
771 if (value == null)
773 if (startIndex == 0 && charCount == 0)
775 return this;
778 throw new ArgumentNullException(nameof(value));
780 if (charCount > value.Length - startIndex)
782 throw new ArgumentOutOfRangeException(nameof(charCount), SR.ArgumentOutOfRange_Index);
785 if (charCount == 0)
787 return this;
790 unsafe
792 fixed (char* valueChars = &value[startIndex])
794 Append(valueChars, charCount);
795 return this;
801 /// <summary>
802 /// Appends a string to the end of this builder.
803 /// </summary>
804 /// <param name="value">The string to append.</param>
805 public StringBuilder Append(string? value)
807 if (value != null)
809 // We could have just called AppendHelper here; this is a hand-specialization of that code.
810 char[] chunkChars = m_ChunkChars;
811 int chunkLength = m_ChunkLength;
812 int valueLen = value.Length;
813 int newCurrentIndex = chunkLength + valueLen;
815 if (newCurrentIndex < chunkChars.Length) // Use strictly < to avoid issues if count == 0, newIndex == length
817 if (valueLen <= 2)
819 if (valueLen > 0)
821 chunkChars[chunkLength] = value[0];
823 if (valueLen > 1)
825 chunkChars[chunkLength + 1] = value[1];
828 else
830 unsafe
832 fixed (char* valuePtr = value)
833 fixed (char* destPtr = &chunkChars[chunkLength])
835 string.wstrcpy(destPtr, valuePtr, valueLen);
840 m_ChunkLength = newCurrentIndex;
842 else
844 AppendHelper(value);
848 return this;
851 // We put this fixed in its own helper to avoid the cost of zero-initing `valueChars` in the
852 // case we don't actually use it.
853 private void AppendHelper(string value)
855 unsafe
857 fixed (char* valueChars = value)
859 Append(valueChars, value.Length);
864 /// <summary>
865 /// Appends part of a string to the end of this builder.
866 /// </summary>
867 /// <param name="value">The string to append.</param>
868 /// <param name="startIndex">The index to start in <paramref name="value"/>.</param>
869 /// <param name="count">The number of characters to read in <paramref name="value"/>.</param>
870 public StringBuilder Append(string? value, int startIndex, int count)
872 if (startIndex < 0)
874 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
876 if (count < 0)
878 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_GenericPositive);
881 if (value == null)
883 if (startIndex == 0 && count == 0)
885 return this;
887 throw new ArgumentNullException(nameof(value));
890 if (count == 0)
892 return this;
895 if (startIndex > value.Length - count)
897 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
900 unsafe
902 fixed (char* valueChars = value)
904 Append(valueChars + startIndex, count);
905 return this;
910 public StringBuilder Append(StringBuilder? value)
912 if (value != null && value.Length != 0)
914 return AppendCore(value, 0, value.Length);
916 return this;
919 public StringBuilder Append(StringBuilder? value, int startIndex, int count)
921 if (startIndex < 0)
923 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
926 if (count < 0)
928 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_GenericPositive);
931 if (value == null)
933 if (startIndex == 0 && count == 0)
935 return this;
937 throw new ArgumentNullException(nameof(value));
940 if (count == 0)
942 return this;
945 if (count > value.Length - startIndex)
947 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
950 return AppendCore(value, startIndex, count);
953 private StringBuilder AppendCore(StringBuilder value, int startIndex, int count)
955 if (value == this)
957 return Append(value.ToString(startIndex, count));
960 int newLength = Length + count;
962 if ((uint)newLength > (uint)m_MaxCapacity)
964 throw new ArgumentOutOfRangeException(nameof(Capacity), SR.ArgumentOutOfRange_Capacity);
967 while (count > 0)
969 int length = Math.Min(m_ChunkChars.Length - m_ChunkLength, count);
970 if (length == 0)
972 ExpandByABlock(count);
973 length = Math.Min(m_ChunkChars.Length - m_ChunkLength, count);
975 value.CopyTo(startIndex, new Span<char>(m_ChunkChars, m_ChunkLength, length), length);
977 m_ChunkLength += length;
978 startIndex += length;
979 count -= length;
982 return this;
985 public StringBuilder AppendLine() => Append(Environment.NewLine);
987 public StringBuilder AppendLine(string? value)
989 Append(value);
990 return Append(Environment.NewLine);
993 public void CopyTo(int sourceIndex, char[] destination, int destinationIndex, int count)
995 if (destination == null)
997 throw new ArgumentNullException(nameof(destination));
1000 if (destinationIndex < 0)
1002 throw new ArgumentOutOfRangeException(nameof(destinationIndex), SR.Format(SR.ArgumentOutOfRange_MustBeNonNegNum, nameof(destinationIndex)));
1005 if (destinationIndex > destination.Length - count)
1007 throw new ArgumentException(SR.ArgumentOutOfRange_OffsetOut);
1010 CopyTo(sourceIndex, new Span<char>(destination).Slice(destinationIndex), count);
1013 public void CopyTo(int sourceIndex, Span<char> destination, int count)
1015 if (count < 0)
1017 throw new ArgumentOutOfRangeException(nameof(count), SR.Arg_NegativeArgCount);
1020 if ((uint)sourceIndex > (uint)Length)
1022 throw new ArgumentOutOfRangeException(nameof(sourceIndex), SR.ArgumentOutOfRange_Index);
1025 if (sourceIndex > Length - count)
1027 throw new ArgumentException(SR.Arg_LongerThanSrcString);
1030 AssertInvariants();
1032 StringBuilder? chunk = this;
1033 int sourceEndIndex = sourceIndex + count;
1034 int curDestIndex = count;
1035 while (count > 0)
1037 Debug.Assert(chunk != null);
1038 int chunkEndIndex = sourceEndIndex - chunk.m_ChunkOffset;
1039 if (chunkEndIndex >= 0)
1041 chunkEndIndex = Math.Min(chunkEndIndex, chunk.m_ChunkLength);
1043 int chunkCount = count;
1044 int chunkStartIndex = chunkEndIndex - count;
1045 if (chunkStartIndex < 0)
1047 chunkCount += chunkStartIndex;
1048 chunkStartIndex = 0;
1050 curDestIndex -= chunkCount;
1051 count -= chunkCount;
1053 // We ensure that chunkStartIndex + chunkCount are within range of m_chunkChars as well as
1054 // ensuring that curDestIndex + chunkCount are within range of destination
1055 ThreadSafeCopy(chunk.m_ChunkChars, chunkStartIndex, destination, curDestIndex, chunkCount);
1057 chunk = chunk.m_ChunkPrevious;
1061 /// <summary>
1062 /// Inserts a string 0 or more times into this builder at the specified position.
1063 /// </summary>
1064 /// <param name="index">The index to insert in this builder.</param>
1065 /// <param name="value">The string to insert.</param>
1066 /// <param name="count">The number of times to insert the string.</param>
1067 public StringBuilder Insert(int index, string? value, int count)
1069 if (count < 0)
1071 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_NeedNonNegNum);
1074 int currentLength = Length;
1075 if ((uint)index > (uint)currentLength)
1077 throw new ArgumentOutOfRangeException(nameof(index), SR.ArgumentOutOfRange_Index);
1080 if (string.IsNullOrEmpty(value) || count == 0)
1082 return this;
1085 // Ensure we don't insert more chars than we can hold, and we don't
1086 // have any integer overflow in our new length.
1087 long insertingChars = (long)value.Length * count;
1088 if (insertingChars > MaxCapacity - this.Length)
1090 throw new OutOfMemoryException();
1092 Debug.Assert(insertingChars + this.Length < int.MaxValue);
1094 StringBuilder chunk;
1095 int indexInChunk;
1096 MakeRoom(index, (int)insertingChars, out chunk, out indexInChunk, false);
1097 unsafe
1099 fixed (char* valuePtr = value)
1101 while (count > 0)
1103 ReplaceInPlaceAtChunk(ref chunk!, ref indexInChunk, valuePtr, value.Length);
1104 --count;
1107 return this;
1112 /// <summary>
1113 /// Removes a range of characters from this builder.
1114 /// </summary>
1115 /// <remarks>
1116 /// This method does not reduce the capacity of this builder.
1117 /// </remarks>
1118 public StringBuilder Remove(int startIndex, int length)
1120 if (length < 0)
1122 throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_NegativeLength);
1125 if (startIndex < 0)
1127 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_StartIndex);
1130 if (length > Length - startIndex)
1132 throw new ArgumentOutOfRangeException(nameof(length), SR.ArgumentOutOfRange_Index);
1135 if (Length == length && startIndex == 0)
1137 Length = 0;
1138 return this;
1141 if (length > 0)
1143 StringBuilder chunk;
1144 int indexInChunk;
1145 Remove(startIndex, length, out chunk, out indexInChunk);
1148 return this;
1151 public StringBuilder Append(bool value) => Append(value.ToString());
1153 public StringBuilder Append(char value)
1155 if (m_ChunkLength < m_ChunkChars.Length)
1157 m_ChunkChars[m_ChunkLength++] = value;
1159 else
1161 Append(value, 1);
1164 return this;
1167 [CLSCompliant(false)]
1168 public StringBuilder Append(sbyte value) => AppendSpanFormattable(value);
1170 public StringBuilder Append(byte value) => AppendSpanFormattable(value);
1172 public StringBuilder Append(short value) => AppendSpanFormattable(value);
1174 public StringBuilder Append(int value) => AppendSpanFormattable(value);
1176 public StringBuilder Append(long value) => AppendSpanFormattable(value);
1178 public StringBuilder Append(float value) => AppendSpanFormattable(value);
1180 public StringBuilder Append(double value) => AppendSpanFormattable(value);
1182 public StringBuilder Append(decimal value) => AppendSpanFormattable(value);
1184 [CLSCompliant(false)]
1185 public StringBuilder Append(ushort value) => AppendSpanFormattable(value);
1187 [CLSCompliant(false)]
1188 public StringBuilder Append(uint value) => AppendSpanFormattable(value);
1190 [CLSCompliant(false)]
1191 public StringBuilder Append(ulong value) => AppendSpanFormattable(value);
1193 private StringBuilder AppendSpanFormattable<T>(T value) where T : ISpanFormattable
1195 if (value.TryFormat(RemainingCurrentChunk, out int charsWritten, format: default, provider: null))
1197 m_ChunkLength += charsWritten;
1198 return this;
1201 return Append(value.ToString());
1204 internal StringBuilder AppendSpanFormattable<T>(T value, string? format, IFormatProvider? provider) where T : ISpanFormattable, IFormattable
1206 if (value.TryFormat(RemainingCurrentChunk, out int charsWritten, format, provider))
1208 m_ChunkLength += charsWritten;
1209 return this;
1212 return Append(value.ToString(format, provider));
1215 public StringBuilder Append(object? value) => (value == null) ? this : Append(value.ToString());
1217 public StringBuilder Append(char[]? value)
1219 if (value?.Length > 0)
1221 unsafe
1223 fixed (char* valueChars = &value[0])
1225 Append(valueChars, value.Length);
1229 return this;
1232 public StringBuilder Append(ReadOnlySpan<char> value)
1234 if (value.Length > 0)
1236 unsafe
1238 fixed (char* valueChars = &MemoryMarshal.GetReference(value))
1240 Append(valueChars, value.Length);
1244 return this;
1247 public StringBuilder Append(ReadOnlyMemory<char> value) => Append(value.Span);
1249 #region AppendJoin
1251 public unsafe StringBuilder AppendJoin(string? separator, params object?[] values)
1253 separator ??= string.Empty;
1254 fixed (char* pSeparator = separator)
1256 return AppendJoinCore(pSeparator, separator.Length, values);
1260 public unsafe StringBuilder AppendJoin<T>(string? separator, IEnumerable<T> values)
1262 separator ??= string.Empty;
1263 fixed (char* pSeparator = separator)
1265 return AppendJoinCore(pSeparator, separator.Length, values);
1269 public unsafe StringBuilder AppendJoin(string? separator, params string?[] values)
1271 separator ??= string.Empty;
1272 fixed (char* pSeparator = separator)
1274 return AppendJoinCore(pSeparator, separator.Length, values);
1278 public unsafe StringBuilder AppendJoin(char separator, params object?[] values)
1280 return AppendJoinCore(&separator, 1, values);
1283 public unsafe StringBuilder AppendJoin<T>(char separator, IEnumerable<T> values)
1285 return AppendJoinCore(&separator, 1, values);
1288 public unsafe StringBuilder AppendJoin(char separator, params string?[] values)
1290 return AppendJoinCore(&separator, 1, values);
1293 private unsafe StringBuilder AppendJoinCore<T>(char* separator, int separatorLength, IEnumerable<T> values)
1295 Debug.Assert(separator != null);
1296 Debug.Assert(separatorLength >= 0);
1298 if (values == null)
1300 ThrowHelper.ThrowArgumentNullException(ExceptionArgument.values);
1303 Debug.Assert(values != null);
1304 using (IEnumerator<T> en = values.GetEnumerator())
1306 if (!en.MoveNext())
1308 return this;
1311 T value = en.Current;
1312 if (value != null)
1314 Append(value.ToString());
1317 while (en.MoveNext())
1319 Append(separator, separatorLength);
1320 value = en.Current;
1321 if (value != null)
1323 Append(value.ToString());
1327 return this;
1330 private unsafe StringBuilder AppendJoinCore<T>(char* separator, int separatorLength, T[] values)
1332 if (values == null)
1334 ThrowHelper.ThrowArgumentNullException(ExceptionArgument.values);
1337 Debug.Assert(values != null);
1338 if (values.Length == 0)
1340 return this;
1343 if (values[0] != null)
1345 Append(values[0]!.ToString()); // TODO-NULLABLE: Indexer nullability tracked (https://github.com/dotnet/roslyn/issues/34644)
1348 for (int i = 1; i < values.Length; i++)
1350 Append(separator, separatorLength);
1351 if (values[i] != null)
1353 Append(values[i]!.ToString()); // TODO-NULLABLE: Indexer nullability tracked (https://github.com/dotnet/roslyn/issues/34644)
1356 return this;
1359 #endregion
1361 public StringBuilder Insert(int index, string? value)
1363 if ((uint)index > (uint)Length)
1365 throw new ArgumentOutOfRangeException(nameof(index), SR.ArgumentOutOfRange_Index);
1368 if (value != null)
1370 unsafe
1372 fixed (char* sourcePtr = value)
1373 Insert(index, sourcePtr, value.Length);
1376 return this;
1379 public StringBuilder Insert(int index, bool value) => Insert(index, value.ToString(), 1);
1381 [CLSCompliant(false)]
1382 public StringBuilder Insert(int index, sbyte value) => Insert(index, value.ToString(), 1);
1384 public StringBuilder Insert(int index, byte value) => Insert(index, value.ToString(), 1);
1386 public StringBuilder Insert(int index, short value) => Insert(index, value.ToString(), 1);
1388 public StringBuilder Insert(int index, char value)
1390 unsafe
1392 Insert(index, &value, 1);
1394 return this;
1397 public StringBuilder Insert(int index, char[]? value)
1399 if ((uint)index > (uint)Length)
1401 throw new ArgumentOutOfRangeException(nameof(index), SR.ArgumentOutOfRange_Index);
1404 if (value != null)
1406 Insert(index, value, 0, value.Length);
1408 return this;
1411 public StringBuilder Insert(int index, char[]? value, int startIndex, int charCount)
1413 int currentLength = Length;
1414 if ((uint)index > (uint)currentLength)
1416 throw new ArgumentOutOfRangeException(nameof(index), SR.ArgumentOutOfRange_Index);
1419 if (value == null)
1421 if (startIndex == 0 && charCount == 0)
1423 return this;
1425 throw new ArgumentNullException(nameof(value), SR.ArgumentNull_String);
1428 if (startIndex < 0)
1430 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_StartIndex);
1433 if (charCount < 0)
1435 throw new ArgumentOutOfRangeException(nameof(charCount), SR.ArgumentOutOfRange_GenericPositive);
1438 if (startIndex > value.Length - charCount)
1440 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
1443 if (charCount > 0)
1445 unsafe
1447 fixed (char* sourcePtr = &value[startIndex])
1448 Insert(index, sourcePtr, charCount);
1451 return this;
1454 public StringBuilder Insert(int index, int value) => Insert(index, value.ToString(), 1);
1456 public StringBuilder Insert(int index, long value) => Insert(index, value.ToString(), 1);
1458 public StringBuilder Insert(int index, float value) => Insert(index, value.ToString(), 1);
1460 public StringBuilder Insert(int index, double value) => Insert(index, value.ToString(), 1);
1462 public StringBuilder Insert(int index, decimal value) => Insert(index, value.ToString(), 1);
1464 [CLSCompliant(false)]
1465 public StringBuilder Insert(int index, ushort value) => Insert(index, value.ToString(), 1);
1467 [CLSCompliant(false)]
1468 public StringBuilder Insert(int index, uint value) => Insert(index, value.ToString(), 1);
1470 [CLSCompliant(false)]
1471 public StringBuilder Insert(int index, ulong value) => Insert(index, value.ToString(), 1);
1473 public StringBuilder Insert(int index, object? value) => (value == null) ? this : Insert(index, value.ToString(), 1);
1475 public StringBuilder Insert(int index, ReadOnlySpan<char> value)
1477 if ((uint)index > (uint)Length)
1479 throw new ArgumentOutOfRangeException(nameof(index), SR.ArgumentOutOfRange_Index);
1482 if (value.Length > 0)
1484 unsafe
1486 fixed (char* sourcePtr = &MemoryMarshal.GetReference(value))
1487 Insert(index, sourcePtr, value.Length);
1490 return this;
1493 public StringBuilder AppendFormat(string format, object? arg0) => AppendFormatHelper(null, format, new ParamsArray(arg0));
1495 public StringBuilder AppendFormat(string format, object? arg0, object? arg1) => AppendFormatHelper(null, format, new ParamsArray(arg0, arg1));
1497 public StringBuilder AppendFormat(string format, object? arg0, object? arg1, object? arg2) => AppendFormatHelper(null, format, new ParamsArray(arg0, arg1, arg2));
1499 public StringBuilder AppendFormat(string format, params object?[] args)
1501 if (args == null)
1503 // To preserve the original exception behavior, throw an exception about format if both
1504 // args and format are null. The actual null check for format is in AppendFormatHelper.
1505 string paramName = (format == null) ? nameof(format) : nameof(args);
1506 throw new ArgumentNullException(paramName);
1509 return AppendFormatHelper(null, format, new ParamsArray(args));
1512 public StringBuilder AppendFormat(IFormatProvider? provider, string format, object? arg0) => AppendFormatHelper(provider, format, new ParamsArray(arg0));
1514 public StringBuilder AppendFormat(IFormatProvider? provider, string format, object? arg0, object? arg1) => AppendFormatHelper(provider, format, new ParamsArray(arg0, arg1));
1516 public StringBuilder AppendFormat(IFormatProvider? provider, string format, object? arg0, object? arg1, object? arg2) => AppendFormatHelper(provider, format, new ParamsArray(arg0, arg1, arg2));
1518 public StringBuilder AppendFormat(IFormatProvider? provider, string format, params object?[] args)
1520 if (args == null)
1522 // To preserve the original exception behavior, throw an exception about format if both
1523 // args and format are null. The actual null check for format is in AppendFormatHelper.
1524 string paramName = (format == null) ? nameof(format) : nameof(args);
1525 throw new ArgumentNullException(paramName);
1528 return AppendFormatHelper(provider, format, new ParamsArray(args));
1531 private static void FormatError()
1533 throw new FormatException(SR.Format_InvalidString);
1536 // Undocumented exclusive limits on the range for Argument Hole Index and Argument Hole Alignment.
1537 private const int IndexLimit = 1000000; // Note: 0 <= ArgIndex < IndexLimit
1538 private const int WidthLimit = 1000000; // Note: -WidthLimit < ArgAlign < WidthLimit
1540 internal StringBuilder AppendFormatHelper(IFormatProvider? provider, string format, ParamsArray args)
1542 if (format == null)
1544 throw new ArgumentNullException(nameof(format));
1547 int pos = 0;
1548 int len = format.Length;
1549 char ch = '\x0';
1551 ICustomFormatter? cf = null;
1552 if (provider != null)
1554 cf = (ICustomFormatter?)provider.GetFormat(typeof(ICustomFormatter));
1557 while (true)
1559 while (pos < len)
1561 ch = format[pos];
1563 pos++;
1564 // Is it a closing brace?
1565 if (ch == '}')
1567 // Check next character (if there is one) to see if it is escaped. eg }}
1568 if (pos < len && format[pos] == '}')
1570 pos++;
1572 else
1574 // Otherwise treat it as an error (Mismatched closing brace)
1575 FormatError();
1578 // Is it a opening brace?
1579 if (ch == '{')
1581 // Check next character (if there is one) to see if it is escaped. eg {{
1582 if (pos < len && format[pos] == '{')
1584 pos++;
1586 else
1588 // Otherwise treat it as the opening brace of an Argument Hole.
1589 pos--;
1590 break;
1593 // If it neither then treat the character as just text.
1594 Append(ch);
1598 // Start of parsing of Argument Hole.
1599 // Argument Hole ::= { Index (, WS* Alignment WS*)? (: Formatting)? }
1601 if (pos == len)
1603 break;
1607 // Start of parsing required Index parameter.
1608 // Index ::= ('0'-'9')+ WS*
1610 pos++;
1611 // If reached end of text then error (Unexpected end of text)
1612 // or character is not a digit then error (Unexpected Character)
1613 if (pos == len || (ch = format[pos]) < '0' || ch > '9') FormatError();
1614 int index = 0;
1617 index = index * 10 + ch - '0';
1618 pos++;
1619 // If reached end of text then error (Unexpected end of text)
1620 if (pos == len)
1622 FormatError();
1624 ch = format[pos];
1625 // so long as character is digit and value of the index is less than 1000000 ( index limit )
1627 while (ch >= '0' && ch <= '9' && index < IndexLimit);
1629 // If value of index is not within the range of the arguments passed in then error (Index out of range)
1630 if (index >= args.Length)
1632 throw new FormatException(SR.Format_IndexOutOfRange);
1635 // Consume optional whitespace.
1636 while (pos < len && (ch = format[pos]) == ' ') pos++;
1637 // End of parsing index parameter.
1640 // Start of parsing of optional Alignment
1641 // Alignment ::= comma WS* minus? ('0'-'9')+ WS*
1643 bool leftJustify = false;
1644 int width = 0;
1645 // Is the character a comma, which indicates the start of alignment parameter.
1646 if (ch == ',')
1648 pos++;
1650 // Consume Optional whitespace
1651 while (pos < len && format[pos] == ' ') pos++;
1653 // If reached the end of the text then error (Unexpected end of text)
1654 if (pos == len)
1656 FormatError();
1659 // Is there a minus sign?
1660 ch = format[pos];
1661 if (ch == '-')
1663 // Yes, then alignment is left justified.
1664 leftJustify = true;
1665 pos++;
1666 // If reached end of text then error (Unexpected end of text)
1667 if (pos == len)
1669 FormatError();
1671 ch = format[pos];
1674 // If current character is not a digit then error (Unexpected character)
1675 if (ch < '0' || ch > '9')
1677 FormatError();
1679 // Parse alignment digits.
1682 width = width * 10 + ch - '0';
1683 pos++;
1684 // If reached end of text then error. (Unexpected end of text)
1685 if (pos == len)
1687 FormatError();
1689 ch = format[pos];
1690 // So long a current character is a digit and the value of width is less than 100000 ( width limit )
1692 while (ch >= '0' && ch <= '9' && width < WidthLimit);
1693 // end of parsing Argument Alignment
1696 // Consume optional whitespace
1697 while (pos < len && (ch = format[pos]) == ' ') pos++;
1700 // Start of parsing of optional formatting parameter.
1702 object? arg = args[index];
1704 ReadOnlySpan<char> itemFormatSpan = default; // used if itemFormat is null
1705 // Is current character a colon? which indicates start of formatting parameter.
1706 if (ch == ':')
1708 pos++;
1709 int startPos = pos;
1711 while (true)
1713 // If reached end of text then error. (Unexpected end of text)
1714 if (pos == len)
1716 FormatError();
1718 ch = format[pos];
1720 if (ch == '}')
1722 // Argument hole closed
1723 break;
1725 else if (ch == '{')
1727 // Braces inside the argument hole are not supported
1728 FormatError();
1731 pos++;
1734 if (pos > startPos)
1736 itemFormatSpan = format.AsSpan(startPos, pos - startPos);
1739 else if (ch != '}')
1741 // Unexpected character
1742 FormatError();
1745 // Construct the output for this arg hole.
1746 pos++;
1747 string? s = null;
1748 string? itemFormat = null;
1750 if (cf != null)
1752 if (itemFormatSpan.Length != 0)
1754 itemFormat = new string(itemFormatSpan);
1756 s = cf.Format(itemFormat, arg, provider);
1759 if (s == null)
1761 // If arg is ISpanFormattable and the beginning doesn't need padding,
1762 // try formatting it into the remaining current chunk.
1763 if (arg is ISpanFormattable spanFormattableArg &&
1764 (leftJustify || width == 0) &&
1765 spanFormattableArg.TryFormat(RemainingCurrentChunk, out int charsWritten, itemFormatSpan, provider))
1767 m_ChunkLength += charsWritten;
1769 // Pad the end, if needed.
1770 int padding = width - charsWritten;
1771 if (leftJustify && padding > 0)
1773 Append(' ', padding);
1776 // Continue to parse other characters.
1777 continue;
1780 // Otherwise, fallback to trying IFormattable or calling ToString.
1781 if (arg is IFormattable formattableArg)
1783 if (itemFormatSpan.Length != 0)
1785 itemFormat ??= new string(itemFormatSpan);
1787 s = formattableArg.ToString(itemFormat, provider);
1789 else if (arg != null)
1791 s = arg.ToString();
1794 // Append it to the final output of the Format String.
1795 if (s == null)
1797 s = string.Empty;
1799 int pad = width - s.Length;
1800 if (!leftJustify && pad > 0)
1802 Append(' ', pad);
1805 Append(s);
1806 if (leftJustify && pad > 0)
1808 Append(' ', pad);
1810 // Continue to parse other characters.
1812 return this;
1815 /// <summary>
1816 /// Replaces all instances of one string with another in this builder.
1817 /// </summary>
1818 /// <param name="oldValue">The string to replace.</param>
1819 /// <param name="newValue">The string to replace <paramref name="oldValue"/> with.</param>
1820 /// <remarks>
1821 /// If <paramref name="newValue"/> is <c>null</c>, instances of <paramref name="oldValue"/>
1822 /// are removed from this builder.
1823 /// </remarks>
1824 public StringBuilder Replace(string oldValue, string? newValue) => Replace(oldValue, newValue, 0, Length);
1826 /// <summary>
1827 /// Determines if the contents of this builder are equal to the contents of another builder.
1828 /// </summary>
1829 /// <param name="sb">The other builder.</param>
1830 public bool Equals(StringBuilder? sb)
1832 if (sb == null)
1834 return false;
1836 if (Length != sb.Length)
1838 return false;
1840 if (sb == this)
1842 return true;
1844 StringBuilder? thisChunk = this;
1845 int thisChunkIndex = thisChunk.m_ChunkLength;
1846 StringBuilder? sbChunk = sb;
1847 int sbChunkIndex = sbChunk.m_ChunkLength;
1848 while (true)
1850 --thisChunkIndex;
1851 --sbChunkIndex;
1853 while (thisChunkIndex < 0)
1855 thisChunk = thisChunk.m_ChunkPrevious;
1856 if (thisChunk == null)
1858 break;
1860 thisChunkIndex = thisChunk.m_ChunkLength + thisChunkIndex;
1863 while (sbChunkIndex < 0)
1865 sbChunk = sbChunk.m_ChunkPrevious;
1866 if (sbChunk == null)
1868 break;
1870 sbChunkIndex = sbChunk.m_ChunkLength + sbChunkIndex;
1873 if (thisChunkIndex < 0)
1875 return sbChunkIndex < 0;
1877 if (sbChunkIndex < 0)
1879 return false;
1882 Debug.Assert(thisChunk != null && sbChunk != null);
1883 if (thisChunk.m_ChunkChars[thisChunkIndex] != sbChunk.m_ChunkChars[sbChunkIndex])
1885 return false;
1890 /// <summary>
1891 /// Determines if the contents of this builder are equal to the contents of <see cref="ReadOnlySpan{Char}"/>.
1892 /// </summary>
1893 /// <param name="span">The <see cref="ReadOnlySpan{Char}"/>.</param>
1894 public bool Equals(ReadOnlySpan<char> span)
1896 if (span.Length != Length)
1898 return false;
1901 StringBuilder? sbChunk = this;
1902 int offset = 0;
1906 int chunk_length = sbChunk.m_ChunkLength;
1907 offset += chunk_length;
1909 ReadOnlySpan<char> chunk = new ReadOnlySpan<char>(sbChunk.m_ChunkChars, 0, chunk_length);
1911 if (!chunk.EqualsOrdinal(span.Slice(span.Length - offset, chunk_length)))
1913 return false;
1916 sbChunk = sbChunk.m_ChunkPrevious;
1917 } while (sbChunk != null);
1919 Debug.Assert(offset == Length);
1920 return true;
1923 /// <summary>
1924 /// Replaces all instances of one string with another in part of this builder.
1925 /// </summary>
1926 /// <param name="oldValue">The string to replace.</param>
1927 /// <param name="newValue">The string to replace <paramref name="oldValue"/> with.</param>
1928 /// <param name="startIndex">The index to start in this builder.</param>
1929 /// <param name="count">The number of characters to read in this builder.</param>
1930 /// <remarks>
1931 /// If <paramref name="newValue"/> is <c>null</c>, instances of <paramref name="oldValue"/>
1932 /// are removed from this builder.
1933 /// </remarks>
1934 public StringBuilder Replace(string oldValue, string? newValue, int startIndex, int count)
1936 int currentLength = Length;
1937 if ((uint)startIndex > (uint)currentLength)
1939 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
1941 if (count < 0 || startIndex > currentLength - count)
1943 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Index);
1945 if (oldValue == null)
1947 throw new ArgumentNullException(nameof(oldValue));
1949 if (oldValue.Length == 0)
1951 throw new ArgumentException(SR.Argument_EmptyName, nameof(oldValue));
1954 newValue ??= string.Empty;
1956 int[]? replacements = null; // A list of replacement positions in a chunk to apply
1957 int replacementsCount = 0;
1959 // Find the chunk, indexInChunk for the starting point
1960 StringBuilder chunk = FindChunkForIndex(startIndex);
1961 int indexInChunk = startIndex - chunk.m_ChunkOffset;
1962 while (count > 0)
1964 Debug.Assert(chunk != null, "chunk was null in replace");
1965 // Look for a match in the chunk,indexInChunk pointer
1966 if (StartsWith(chunk, indexInChunk, count, oldValue))
1968 // Push it on the replacements array (with growth), we will do all replacements in a
1969 // given chunk in one operation below (see ReplaceAllInChunk) so we don't have to slide
1970 // many times.
1971 if (replacements == null)
1973 replacements = new int[5];
1975 else if (replacementsCount >= replacements.Length)
1977 Array.Resize(ref replacements, replacements.Length * 3 / 2 + 4); // Grow by ~1.5x, but more in the begining
1979 replacements[replacementsCount++] = indexInChunk;
1980 indexInChunk += oldValue.Length;
1981 count -= oldValue.Length;
1983 else
1985 indexInChunk++;
1986 --count;
1989 if (indexInChunk >= chunk.m_ChunkLength || count == 0) // Have we moved out of the current chunk?
1991 // Replacing mutates the blocks, so we need to convert to a logical index and back afterwards.
1992 int index = indexInChunk + chunk.m_ChunkOffset;
1994 // See if we accumulated any replacements, if so apply them.
1995 Debug.Assert(replacements != null || replacementsCount == 0, "replacements was null and replacementsCount != 0");
1996 ReplaceAllInChunk(replacements, replacementsCount, chunk, oldValue.Length, newValue);
1997 // The replacement has affected the logical index. Adjust it.
1998 index += ((newValue.Length - oldValue.Length) * replacementsCount);
1999 replacementsCount = 0;
2001 chunk = FindChunkForIndex(index);
2002 indexInChunk = index - chunk.m_ChunkOffset;
2003 Debug.Assert(chunk != null || count == 0, "Chunks ended prematurely!");
2007 AssertInvariants();
2008 return this;
2011 /// <summary>
2012 /// Replaces all instances of one character with another in this builder.
2013 /// </summary>
2014 /// <param name="oldChar">The character to replace.</param>
2015 /// <param name="newChar">The character to replace <paramref name="oldChar"/> with.</param>
2016 public StringBuilder Replace(char oldChar, char newChar)
2018 return Replace(oldChar, newChar, 0, Length);
2021 /// <summary>
2022 /// Replaces all instances of one character with another in this builder.
2023 /// </summary>
2024 /// <param name="oldChar">The character to replace.</param>
2025 /// <param name="newChar">The character to replace <paramref name="oldChar"/> with.</param>
2026 /// <param name="startIndex">The index to start in this builder.</param>
2027 /// <param name="count">The number of characters to read in this builder.</param>
2028 public StringBuilder Replace(char oldChar, char newChar, int startIndex, int count)
2030 int currentLength = Length;
2031 if ((uint)startIndex > (uint)currentLength)
2033 throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
2036 if (count < 0 || startIndex > currentLength - count)
2038 throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Index);
2041 int endIndex = startIndex + count;
2042 StringBuilder chunk = this;
2044 while (true)
2046 int endIndexInChunk = endIndex - chunk.m_ChunkOffset;
2047 int startIndexInChunk = startIndex - chunk.m_ChunkOffset;
2048 if (endIndexInChunk >= 0)
2050 int curInChunk = Math.Max(startIndexInChunk, 0);
2051 int endInChunk = Math.Min(chunk.m_ChunkLength, endIndexInChunk);
2052 while (curInChunk < endInChunk)
2054 if (chunk.m_ChunkChars[curInChunk] == oldChar)
2055 chunk.m_ChunkChars[curInChunk] = newChar;
2056 curInChunk++;
2059 if (startIndexInChunk >= 0)
2061 break;
2064 Debug.Assert(chunk.m_ChunkPrevious != null);
2065 chunk = chunk.m_ChunkPrevious;
2068 AssertInvariants();
2069 return this;
2072 /// <summary>
2073 /// Appends a character buffer to this builder.
2074 /// </summary>
2075 /// <param name="value">The pointer to the start of the buffer.</param>
2076 /// <param name="valueCount">The number of characters in the buffer.</param>
2077 [CLSCompliant(false)]
2078 public unsafe StringBuilder Append(char* value, int valueCount)
2080 // We don't check null value as this case will throw null reference exception anyway
2081 if (valueCount < 0)
2083 throw new ArgumentOutOfRangeException(nameof(valueCount), SR.ArgumentOutOfRange_NegativeCount);
2086 // this is where we can check if the valueCount will put us over m_MaxCapacity
2087 // We are doing the check here to prevent the corruption of the StringBuilder.
2088 int newLength = Length + valueCount;
2089 if (newLength > m_MaxCapacity || newLength < valueCount)
2091 throw new ArgumentOutOfRangeException(nameof(valueCount), SR.ArgumentOutOfRange_LengthGreaterThanCapacity);
2094 // This case is so common we want to optimize for it heavily.
2095 int newIndex = valueCount + m_ChunkLength;
2096 if (newIndex <= m_ChunkChars.Length)
2098 ThreadSafeCopy(value, m_ChunkChars, m_ChunkLength, valueCount);
2099 m_ChunkLength = newIndex;
2101 else
2103 // Copy the first chunk
2104 int firstLength = m_ChunkChars.Length - m_ChunkLength;
2105 if (firstLength > 0)
2107 ThreadSafeCopy(value, m_ChunkChars, m_ChunkLength, firstLength);
2108 m_ChunkLength = m_ChunkChars.Length;
2111 // Expand the builder to add another chunk.
2112 int restLength = valueCount - firstLength;
2113 ExpandByABlock(restLength);
2114 Debug.Assert(m_ChunkLength == 0, "A new block was not created.");
2116 // Copy the second chunk
2117 ThreadSafeCopy(value + firstLength, m_ChunkChars, 0, restLength);
2118 m_ChunkLength = restLength;
2120 AssertInvariants();
2121 return this;
2124 /// <summary>
2125 /// Inserts a character buffer into this builder at the specified position.
2126 /// </summary>
2127 /// <param name="index">The index to insert in this builder.</param>
2128 /// <param name="value">The pointer to the start of the buffer.</param>
2129 /// <param name="valueCount">The number of characters in the buffer.</param>
2130 private unsafe void Insert(int index, char* value, int valueCount)
2132 if ((uint)index > (uint)Length)
2134 throw new ArgumentOutOfRangeException(nameof(index), SR.ArgumentOutOfRange_Index);
2137 if (valueCount > 0)
2139 StringBuilder chunk;
2140 int indexInChunk;
2141 MakeRoom(index, valueCount, out chunk, out indexInChunk, false);
2142 ReplaceInPlaceAtChunk(ref chunk!, ref indexInChunk, value, valueCount);
2146 /// <summary>
2147 /// Replaces strings at specified indices with a new string in a chunk.
2148 /// </summary>
2149 /// <param name="replacements">The list of indices, relative to the beginning of the chunk, to remove at.</param>
2150 /// <param name="replacementsCount">The number of replacements to make.</param>
2151 /// <param name="sourceChunk">The source chunk.</param>
2152 /// <param name="removeCount">The number of characters to remove at each replacement.</param>
2153 /// <param name="value">The string to insert at each replacement.</param>
2154 /// <remarks>
2155 /// This routine is very efficient because it does replacements in bulk.
2156 /// </remarks>
2157 private void ReplaceAllInChunk(int[]? replacements, int replacementsCount, StringBuilder sourceChunk, int removeCount, string value)
2159 if (replacementsCount <= 0)
2161 return;
2164 unsafe
2166 fixed (char* valuePtr = value)
2168 Debug.Assert(replacements != null, "replacements was null when replacementsCount > 0");
2169 // calculate the total amount of extra space or space needed for all the replacements.
2170 long longDelta = (value.Length - removeCount) * (long)replacementsCount;
2171 int delta = (int)longDelta;
2172 if (delta != longDelta)
2174 throw new OutOfMemoryException();
2177 StringBuilder targetChunk = sourceChunk; // the target as we copy chars down
2178 int targetIndexInChunk = replacements[0];
2180 // Make the room needed for all the new characters if needed.
2181 if (delta > 0)
2183 MakeRoom(targetChunk.m_ChunkOffset + targetIndexInChunk, delta, out targetChunk, out targetIndexInChunk, true);
2186 // We made certain that characters after the insertion point are not moved,
2187 int i = 0;
2188 while (true)
2190 // Copy in the new string for the ith replacement
2191 ReplaceInPlaceAtChunk(ref targetChunk!, ref targetIndexInChunk, valuePtr, value.Length);
2192 int gapStart = replacements[i] + removeCount;
2193 i++;
2194 if (i >= replacementsCount)
2196 break;
2199 int gapEnd = replacements[i];
2200 Debug.Assert(gapStart < sourceChunk.m_ChunkChars.Length, "gap starts at end of buffer. Should not happen");
2201 Debug.Assert(gapStart <= gapEnd, "negative gap size");
2202 Debug.Assert(gapEnd <= sourceChunk.m_ChunkLength, "gap too big");
2203 if (delta != 0) // can skip the sliding of gaps if source an target string are the same size.
2205 // Copy the gap data between the current replacement and the next replacement
2206 fixed (char* sourcePtr = &sourceChunk.m_ChunkChars[gapStart])
2207 ReplaceInPlaceAtChunk(ref targetChunk!, ref targetIndexInChunk, sourcePtr, gapEnd - gapStart);
2209 else
2211 targetIndexInChunk += gapEnd - gapStart;
2212 Debug.Assert(targetIndexInChunk <= targetChunk.m_ChunkLength, "gap not in chunk");
2216 // Remove extra space if necessary.
2217 if (delta < 0)
2219 Remove(targetChunk.m_ChunkOffset + targetIndexInChunk, -delta, out targetChunk, out targetIndexInChunk);
2225 /// <summary>
2226 /// Returns a value indicating whether a substring of a builder starts with a specified prefix.
2227 /// </summary>
2228 /// <param name="chunk">The chunk in which the substring starts.</param>
2229 /// <param name="indexInChunk">The index in <paramref name="chunk"/> at which the substring starts.</param>
2230 /// <param name="count">The logical count of the substring.</param>
2231 /// <param name="value">The prefix.</param>
2232 private bool StartsWith(StringBuilder chunk, int indexInChunk, int count, string value)
2234 for (int i = 0; i < value.Length; i++)
2236 if (count == 0)
2238 return false;
2241 if (indexInChunk >= chunk.m_ChunkLength)
2243 chunk = Next(chunk)!;
2244 if (chunk == null)
2246 return false;
2248 indexInChunk = 0;
2251 if (value[i] != chunk.m_ChunkChars[indexInChunk])
2253 return false;
2256 indexInChunk++;
2257 --count;
2260 return true;
2263 /// <summary>
2264 /// Replaces characters at a specified location with the contents of a character buffer.
2265 /// This function is the logical equivalent of memcpy.
2266 /// </summary>
2267 /// <param name="chunk">
2268 /// The chunk in which to start replacing characters.
2269 /// Receives the chunk in which character replacement ends.
2270 /// </param>
2271 /// <param name="indexInChunk">
2272 /// The index in <paramref name="chunk"/> to start replacing characters at.
2273 /// Receives the index at which character replacement ends.
2274 /// </param>
2275 /// <param name="value">The pointer to the start of the character buffer.</param>
2276 /// <param name="count">The number of characters in the buffer.</param>
2277 private unsafe void ReplaceInPlaceAtChunk(ref StringBuilder? chunk, ref int indexInChunk, char* value, int count)
2279 if (count != 0)
2281 while (true)
2283 Debug.Assert(chunk != null, "chunk should not be null at this point");
2284 int lengthInChunk = chunk.m_ChunkLength - indexInChunk;
2285 Debug.Assert(lengthInChunk >= 0, "Index isn't in the chunk.");
2287 int lengthToCopy = Math.Min(lengthInChunk, count);
2288 ThreadSafeCopy(value, chunk.m_ChunkChars, indexInChunk, lengthToCopy);
2290 // Advance the index.
2291 indexInChunk += lengthToCopy;
2292 if (indexInChunk >= chunk.m_ChunkLength)
2294 chunk = Next(chunk);
2295 indexInChunk = 0;
2297 count -= lengthToCopy;
2298 if (count == 0)
2300 break;
2302 value += lengthToCopy;
2307 /// <remarks>
2308 /// This method prevents out-of-bounds writes in the case a different thread updates a field in the builder just before a copy begins.
2309 /// All interesting variables are copied out of the heap into the parameters of this method, and then bounds checks are run.
2310 /// </remarks>
2311 private static unsafe void ThreadSafeCopy(char* sourcePtr, char[] destination, int destinationIndex, int count)
2313 if (count > 0)
2315 if ((uint)destinationIndex <= (uint)destination.Length && (destinationIndex + count) <= destination.Length)
2317 fixed (char* destinationPtr = &destination[destinationIndex])
2318 string.wstrcpy(destinationPtr, sourcePtr, count);
2320 else
2322 throw new ArgumentOutOfRangeException(nameof(destinationIndex), SR.ArgumentOutOfRange_Index);
2327 private static unsafe void ThreadSafeCopy(char[] source, int sourceIndex, Span<char> destination, int destinationIndex, int count)
2329 if (count > 0)
2331 if ((uint)sourceIndex > (uint)source.Length || count > source.Length - sourceIndex)
2333 throw new ArgumentOutOfRangeException(nameof(sourceIndex), SR.ArgumentOutOfRange_Index);
2336 if ((uint)destinationIndex > (uint)destination.Length || count > destination.Length - destinationIndex)
2338 throw new ArgumentOutOfRangeException(nameof(destinationIndex), SR.ArgumentOutOfRange_Index);
2341 fixed (char* sourcePtr = &source[sourceIndex])
2342 fixed (char* destinationPtr = &MemoryMarshal.GetReference(destination))
2343 string.wstrcpy(destinationPtr + destinationIndex, sourcePtr, count);
2347 /// <summary>
2348 /// Gets the chunk corresponding to the logical index in this builder.
2349 /// </summary>
2350 /// <param name="index">The logical index in this builder.</param>
2351 /// <remarks>
2352 /// After calling this method, you can obtain the actual index within the chunk by
2353 /// subtracting <see cref="m_ChunkOffset"/> from <paramref name="index"/>.
2354 /// </remarks>
2355 private StringBuilder FindChunkForIndex(int index)
2357 Debug.Assert(0 <= index && index <= Length);
2359 StringBuilder result = this;
2360 while (result.m_ChunkOffset > index)
2362 Debug.Assert(result.m_ChunkPrevious != null);
2363 result = result.m_ChunkPrevious;
2366 Debug.Assert(result != null);
2367 return result;
2370 /// <summary>
2371 /// Gets the chunk corresponding to the logical byte index in this builder.
2372 /// </summary>
2373 /// <param name="byteIndex">The logical byte index in this builder.</param>
2374 private StringBuilder FindChunkForByte(int byteIndex)
2376 Debug.Assert(0 <= byteIndex && byteIndex <= Length * sizeof(char));
2378 StringBuilder result = this;
2379 while (result.m_ChunkOffset * sizeof(char) > byteIndex)
2381 Debug.Assert(result.m_ChunkPrevious != null);
2382 result = result.m_ChunkPrevious;
2385 Debug.Assert(result != null);
2386 return result;
2389 /// <summary>Gets a span representing the remaining space available in the current chunk.</summary>
2390 private Span<char> RemainingCurrentChunk
2392 [MethodImpl(MethodImplOptions.AggressiveInlining)]
2393 get => new Span<char>(m_ChunkChars, m_ChunkLength, m_ChunkChars.Length - m_ChunkLength);
2396 /// <summary>
2397 /// Finds the chunk that logically succeeds the specified chunk.
2398 /// </summary>
2399 /// <param name="chunk">The chunk whose successor should be found.</param>
2400 /// <remarks>
2401 /// Each chunk only stores the pointer to its logical predecessor, so this routine has to start
2402 /// from the 'this' pointer (which is assumed to represent the whole StringBuilder) and work its
2403 /// way down until it finds the specified chunk (which is O(n)). Thus, it is more expensive than
2404 /// a field fetch.
2405 /// </remarks>
2406 private StringBuilder? Next(StringBuilder chunk) => chunk == this ? null : FindChunkForIndex(chunk.m_ChunkOffset + chunk.m_ChunkLength);
2408 /// <summary>
2409 /// Transfers the character buffer from this chunk to a new chunk, and allocates a new buffer with a minimum size for this chunk.
2410 /// </summary>
2411 /// <param name="minBlockCharCount">The minimum size of the new buffer to be allocated for this chunk.</param>
2412 /// <remarks>
2413 /// This method requires that the current chunk is full. Otherwise, there's no point in shifting the characters over.
2414 /// It also assumes that 'this' is the last chunk in the linked list.
2415 /// </remarks>
2416 private void ExpandByABlock(int minBlockCharCount)
2418 Debug.Assert(Capacity == Length, nameof(ExpandByABlock) + " should only be called when there is no space left.");
2419 Debug.Assert(minBlockCharCount > 0);
2421 AssertInvariants();
2423 if ((minBlockCharCount + Length) > m_MaxCapacity || minBlockCharCount + Length < minBlockCharCount)
2425 throw new ArgumentOutOfRangeException("requiredLength", SR.ArgumentOutOfRange_SmallCapacity);
2428 // - We always need to make the new chunk at least as big as was requested (`minBlockCharCount`).
2429 // - We'd also prefer to make it at least at big as the current length (thus doubling capacity).
2430 // - But this is only up to a maximum, so we stay in the small object heap, and never allocate
2431 // really big chunks even if the string gets really big.
2432 int newBlockLength = Math.Max(minBlockCharCount, Math.Min(Length, MaxChunkSize));
2434 // Check for integer overflow (logical buffer size > int.MaxValue)
2435 if (m_ChunkOffset + m_ChunkLength + newBlockLength < newBlockLength)
2437 throw new OutOfMemoryException();
2440 // Allocate the array before updating any state to avoid leaving inconsistent state behind in case of out of memory exception
2441 char[] chunkChars = new char[newBlockLength];
2443 // Move all of the data from this chunk to a new one, via a few O(1) pointer adjustments.
2444 // Then, have this chunk point to the new one as its predecessor.
2445 m_ChunkPrevious = new StringBuilder(this);
2446 m_ChunkOffset += m_ChunkLength;
2447 m_ChunkLength = 0;
2449 m_ChunkChars = chunkChars;
2451 AssertInvariants();
2454 /// <summary>
2455 /// Creates a new chunk with fields copied from an existing chunk.
2456 /// </summary>
2457 /// <param name="from">The chunk from which to copy fields.</param>
2458 /// <remarks>
2459 /// <para>
2460 /// This method runs in O(1) time. It does not copy data within the character buffer
2461 /// <paramref name="from"/> holds, but copies the reference to the character buffer itself
2462 /// (plus a few other fields).
2463 /// </para>
2464 /// <para>
2465 /// Callers are expected to update <paramref name="from"/> subsequently to point to this
2466 /// chunk as its predecessor.
2467 /// </para>
2468 /// </remarks>
2469 private StringBuilder(StringBuilder from)
2471 m_ChunkLength = from.m_ChunkLength;
2472 m_ChunkOffset = from.m_ChunkOffset;
2473 m_ChunkChars = from.m_ChunkChars;
2474 m_ChunkPrevious = from.m_ChunkPrevious;
2475 m_MaxCapacity = from.m_MaxCapacity;
2477 AssertInvariants();
2480 /// <summary>
2481 /// Creates a gap at a logical index with the specified count.
2482 /// </summary>
2483 /// <param name="index">The logical index in this builder.</param>
2484 /// <param name="count">The number of characters in the gap.</param>
2485 /// <param name="chunk">Receives the chunk containing the gap.</param>
2486 /// <param name="indexInChunk">The index in <paramref name="chunk"/> that points to the gap.</param>
2487 /// <param name="doNotMoveFollowingChars">
2488 /// - If <c>true</c>, then room must be made by inserting a chunk before the current chunk.
2489 /// - If <c>false</c>, then room can be made by shifting characters ahead of <paramref name="index"/>
2490 /// in this block forward by <paramref name="count"/> provided the characters will still fit in
2491 /// the current chunk after being shifted.
2492 /// - Providing <c>false</c> does not make a difference most of the time, but it can matter when someone
2493 /// inserts lots of small strings at a position in the buffer.
2494 /// </param>
2495 /// <remarks>
2496 /// <para>
2497 /// Since chunks do not contain references to their successors, it is not always possible for us to make room
2498 /// by inserting space after <paramref name="index"/> in case this chunk runs out of space. Thus, we make room
2499 /// by inserting space before the specified index, and having logical indices refer to new locations by the end
2500 /// of this method.
2501 /// </para>
2502 /// <para>
2503 /// <see cref="ReplaceInPlaceAtChunk"/> can be used in conjunction with this method to fill in the newly created gap.
2504 /// </para>
2505 /// </remarks>
2506 private void MakeRoom(int index, int count, out StringBuilder chunk, out int indexInChunk, bool doNotMoveFollowingChars)
2508 AssertInvariants();
2509 Debug.Assert(count > 0);
2510 Debug.Assert(index >= 0);
2512 if (count + Length > m_MaxCapacity || count + Length < count)
2514 throw new ArgumentOutOfRangeException("requiredLength", SR.ArgumentOutOfRange_SmallCapacity);
2517 chunk = this;
2518 while (chunk.m_ChunkOffset > index)
2520 chunk.m_ChunkOffset += count;
2521 Debug.Assert(chunk.m_ChunkPrevious != null);
2522 chunk = chunk.m_ChunkPrevious;
2524 indexInChunk = index - chunk.m_ChunkOffset;
2526 // Cool, we have some space in this block, and we don't have to copy much to get at it, so go ahead and use it.
2527 // This typically happens when someone repeatedly inserts small strings at a spot (usually the absolute front) of the buffer.
2528 if (!doNotMoveFollowingChars && chunk.m_ChunkLength <= DefaultCapacity * 2 && chunk.m_ChunkChars.Length - chunk.m_ChunkLength >= count)
2530 for (int i = chunk.m_ChunkLength; i > indexInChunk;)
2532 --i;
2533 chunk.m_ChunkChars[i + count] = chunk.m_ChunkChars[i];
2535 chunk.m_ChunkLength += count;
2536 return;
2539 // Allocate space for the new chunk, which will go before the current one.
2540 StringBuilder newChunk = new StringBuilder(Math.Max(count, DefaultCapacity), chunk.m_MaxCapacity, chunk.m_ChunkPrevious);
2541 newChunk.m_ChunkLength = count;
2543 // Copy the head of the current buffer to the new buffer.
2544 int copyCount1 = Math.Min(count, indexInChunk);
2545 if (copyCount1 > 0)
2547 unsafe
2549 fixed (char* chunkCharsPtr = &chunk.m_ChunkChars[0])
2551 ThreadSafeCopy(chunkCharsPtr, newChunk.m_ChunkChars, 0, copyCount1);
2553 // Slide characters over in the current buffer to make room.
2554 int copyCount2 = indexInChunk - copyCount1;
2555 if (copyCount2 >= 0)
2557 ThreadSafeCopy(chunkCharsPtr + copyCount1, chunk.m_ChunkChars, 0, copyCount2);
2558 indexInChunk = copyCount2;
2564 // Wire in the new chunk.
2565 chunk.m_ChunkPrevious = newChunk;
2566 chunk.m_ChunkOffset += count;
2567 if (copyCount1 < count)
2569 chunk = newChunk;
2570 indexInChunk = copyCount1;
2573 AssertInvariants();
2576 /// <summary>
2577 /// Used by <see cref="MakeRoom"/> to allocate another chunk.
2578 /// </summary>
2579 /// <param name="size">The size of the character buffer for this chunk.</param>
2580 /// <param name="maxCapacity">The maximum capacity, to be stored in this chunk.</param>
2581 /// <param name="previousBlock">The predecessor of this chunk.</param>
2582 private StringBuilder(int size, int maxCapacity, StringBuilder? previousBlock)
2584 Debug.Assert(size > 0);
2585 Debug.Assert(maxCapacity > 0);
2587 m_ChunkChars = new char[size];
2588 m_MaxCapacity = maxCapacity;
2589 m_ChunkPrevious = previousBlock;
2590 if (previousBlock != null)
2592 m_ChunkOffset = previousBlock.m_ChunkOffset + previousBlock.m_ChunkLength;
2595 AssertInvariants();
2598 /// <summary>
2599 /// Removes a specified number of characters beginning at a logical index in this builder.
2600 /// </summary>
2601 /// <param name="startIndex">The logical index in this builder to start removing characters.</param>
2602 /// <param name="count">The number of characters to remove.</param>
2603 /// <param name="chunk">Receives the new chunk containing the logical index.</param>
2604 /// <param name="indexInChunk">
2605 /// Receives the new index in <paramref name="chunk"/> that is associated with the logical index.
2606 /// </param>
2607 private void Remove(int startIndex, int count, out StringBuilder chunk, out int indexInChunk)
2609 AssertInvariants();
2610 Debug.Assert(startIndex >= 0 && startIndex < Length);
2612 int endIndex = startIndex + count;
2614 // Find the chunks for the start and end of the block to delete.
2615 chunk = this;
2616 StringBuilder? endChunk = null;
2617 int endIndexInChunk = 0;
2618 while (true)
2620 if (endIndex - chunk.m_ChunkOffset >= 0)
2622 if (endChunk == null)
2624 endChunk = chunk;
2625 endIndexInChunk = endIndex - endChunk.m_ChunkOffset;
2627 if (startIndex - chunk.m_ChunkOffset >= 0)
2629 indexInChunk = startIndex - chunk.m_ChunkOffset;
2630 break;
2633 else
2635 chunk.m_ChunkOffset -= count;
2638 Debug.Assert(chunk.m_ChunkPrevious != null);
2639 chunk = chunk.m_ChunkPrevious;
2641 Debug.Assert(chunk != null, "We fell off the beginning of the string!");
2643 int copyTargetIndexInChunk = indexInChunk;
2644 int copyCount = endChunk.m_ChunkLength - endIndexInChunk;
2645 if (endChunk != chunk)
2647 copyTargetIndexInChunk = 0;
2648 // Remove the characters after `startIndex` to the end of the chunk.
2649 chunk.m_ChunkLength = indexInChunk;
2651 // Remove the characters in chunks between the start and the end chunk.
2652 endChunk.m_ChunkPrevious = chunk;
2653 endChunk.m_ChunkOffset = chunk.m_ChunkOffset + chunk.m_ChunkLength;
2655 // If the start is 0, then we can throw away the whole start chunk.
2656 if (indexInChunk == 0)
2658 endChunk.m_ChunkPrevious = chunk.m_ChunkPrevious;
2659 chunk = endChunk;
2662 endChunk.m_ChunkLength -= (endIndexInChunk - copyTargetIndexInChunk);
2664 // SafeCritical: We ensure that `endIndexInChunk + copyCount` is within range of `m_ChunkChars`, and
2665 // also ensure that `copyTargetIndexInChunk + copyCount` is within the chunk.
2667 // Remove any characters in the end chunk, by sliding the characters down.
2668 if (copyTargetIndexInChunk != endIndexInChunk) // Sometimes no move is necessary
2670 ThreadSafeCopy(endChunk.m_ChunkChars, endIndexInChunk, endChunk.m_ChunkChars, copyTargetIndexInChunk, copyCount);
2673 Debug.Assert(chunk != null, "We fell off the beginning of the string!");
2674 AssertInvariants();