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
;
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
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.
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.
30 /// The character buffer for this chunk.
32 internal char[] m_ChunkChars
;
35 /// The chunk that logically precedes this chunk.
37 internal StringBuilder
? m_ChunkPrevious
;
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.
43 internal int m_ChunkLength
;
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.
49 internal int m_ChunkOffset
;
52 /// The maximum capacity this builder is allowed to have.
54 internal int m_MaxCapacity
;
57 /// The default capacity of a <see cref="StringBuilder"/>.
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
70 internal const int MaxChunkSize
= 8000;
73 /// Initializes a new instance of the <see cref="StringBuilder"/> class.
75 public StringBuilder()
77 m_MaxCapacity
= int.MaxValue
;
78 m_ChunkChars
= new char[DefaultCapacity
];
82 /// Initializes a new instance of the <see cref="StringBuilder"/> class.
84 /// <param name="capacity">The initial capacity of this builder.</param>
85 public StringBuilder(int capacity
)
86 : this(capacity
, int.MaxValue
)
91 /// Initializes a new instance of the <see cref="StringBuilder"/> class.
93 /// <param name="value">The initial contents of this builder.</param>
94 public StringBuilder(string? value)
95 : this(value, DefaultCapacity
)
100 /// Initializes a new instance of the <see cref="StringBuilder"/> class.
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
)
110 /// Initializes a new instance of the <see cref="StringBuilder"/> class.
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
)
120 throw new ArgumentOutOfRangeException(nameof(capacity
), SR
.Format(SR
.ArgumentOutOfRange_MustBePositive
, nameof(capacity
)));
124 throw new ArgumentOutOfRangeException(nameof(length
), SR
.Format(SR
.ArgumentOutOfRange_MustBeNonNegNum
, nameof(length
)));
128 throw new ArgumentOutOfRangeException(nameof(startIndex
), SR
.ArgumentOutOfRange_StartIndex
);
133 value = string.Empty
;
135 if (startIndex
> value.Length
- length
)
137 throw new ArgumentOutOfRangeException(nameof(length
), SR
.ArgumentOutOfRange_IndexLength
);
140 m_MaxCapacity
= int.MaxValue
;
143 capacity
= DefaultCapacity
;
145 capacity
= Math
.Max(capacity
, length
);
147 m_ChunkChars
= new char[capacity
];
148 m_ChunkLength
= length
;
152 fixed (char* sourcePtr
= value)
154 ThreadSafeCopy(sourcePtr
+ startIndex
, m_ChunkChars
, 0, length
);
160 /// Initializes a new instance of the <see cref="StringBuilder"/> class.
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
);
172 throw new ArgumentOutOfRangeException(nameof(maxCapacity
), SR
.ArgumentOutOfRange_SmallMaxCapacity
);
176 throw new ArgumentOutOfRangeException(nameof(capacity
), SR
.Format(SR
.ArgumentOutOfRange_MustBePositive
, nameof(capacity
)));
181 capacity
= Math
.Min(DefaultCapacity
, maxCapacity
);
184 m_MaxCapacity
= maxCapacity
;
185 m_ChunkChars
= new char[capacity
];
188 private StringBuilder(SerializationInfo info
, StreamingContext context
)
192 throw new ArgumentNullException(nameof(info
));
195 int persistedCapacity
= 0;
196 string? persistedString
= null;
197 int persistedMaxCapacity
= int.MaxValue
;
198 bool capacityPresent
= false;
201 SerializationInfoEnumerator enumerator
= info
.GetEnumerator();
202 while (enumerator
.MoveNext())
204 switch (enumerator
.Name
)
206 case MaxCapacityField
:
207 persistedMaxCapacity
= info
.GetInt32(MaxCapacityField
);
209 case StringValueField
:
210 persistedString
= info
.GetString(StringValueField
);
213 persistedCapacity
= info
.GetInt32(CapacityField
);
214 capacityPresent
= true;
217 // Ignore other fields for forwards-compatibility.
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
);
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;
252 void ISerializable
.GetObjectData(SerializationInfo info
, StreamingContext context
)
256 throw new ArgumentNullException(nameof(info
));
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
;
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);
290 // There are no gaps in the blocks.
291 Debug
.Assert(currentBlock
.m_ChunkOffset
== prevBlock
.m_ChunkOffset
+ prevBlock
.m_ChunkLength
);
292 currentBlock
= prevBlock
;
298 get => m_ChunkChars
.Length
+ m_ChunkOffset
;
303 throw new ArgumentOutOfRangeException(nameof(value), SR
.ArgumentOutOfRange_NegativeCapacity
);
305 if (value > MaxCapacity
)
307 throw new ArgumentOutOfRangeException(nameof(value), SR
.ArgumentOutOfRange_Capacity
);
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
;
325 /// Gets the maximum capacity this builder is allowed to have.
327 public int MaxCapacity
=> m_MaxCapacity
;
330 /// Ensures that the capacity of this builder is at least the specified value.
332 /// <param name="capacity">The new capacity for this builder.</param>
334 /// If <paramref name="capacity"/> is less than or equal to the current capacity of
335 /// this builder, the capacity remains unchanged.
337 public int EnsureCapacity(int capacity
)
341 throw new ArgumentOutOfRangeException(nameof(capacity
), SR
.ArgumentOutOfRange_NegativeCapacity
);
344 if (Capacity
< capacity
)
351 public override string ToString()
360 string result
= string.FastAllocateString(Length
);
361 StringBuilder
? chunk
= this;
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
);
383 throw new ArgumentOutOfRangeException(nameof(chunkLength
), SR
.ArgumentOutOfRange_Index
);
386 chunk
= chunk
.m_ChunkPrevious
;
388 while (chunk
!= null);
396 /// Creates a string from a substring of this builder.
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
;
405 throw new ArgumentOutOfRangeException(nameof(startIndex
), SR
.ArgumentOutOfRange_StartIndex
);
407 if (startIndex
> currentLength
)
409 throw new ArgumentOutOfRangeException(nameof(startIndex
), SR
.ArgumentOutOfRange_StartIndexLargerThanLength
);
413 throw new ArgumentOutOfRangeException(nameof(length
), SR
.ArgumentOutOfRange_NegativeLength
);
415 if (startIndex
> currentLength
- length
)
417 throw new ArgumentOutOfRangeException(nameof(length
), SR
.ArgumentOutOfRange_IndexLength
);
421 string result
= string.FastAllocateString(length
);
424 fixed (char* destinationPtr
= result
)
426 this.CopyTo(startIndex
, new Span
<char>(destinationPtr
, length
), length
);
432 public StringBuilder
Clear()
439 /// Gets or sets the length of this builder.
443 get => m_ChunkOffset
+ m_ChunkLength
;
446 // If the new length is less than 0 or greater than our Maximum capacity, bail.
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)
464 int delta
= value - Length
;
467 // Pad ourselves with null characters.
472 StringBuilder chunk
= FindChunkForIndex(value);
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
;
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
;
500 Debug
.Assert(Length
== value, "Something went wrong setting Length.");
504 [IndexerName("Chars")]
505 public char this[int index
]
509 StringBuilder
? chunk
= this;
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
;
524 throw new IndexOutOfRangeException();
530 StringBuilder
? chunk
= this;
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;
543 chunk
= chunk
.m_ChunkPrevious
;
546 throw new ArgumentOutOfRangeException(nameof(index
), SR
.ArgumentOutOfRange_Index
);
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<char> 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<char> chunk in sb.GetChunks())
574 /// var span = chunk.Span;
575 /// for (int i = 0; i < span.Length; i++)
576 /// { /* operation on span[i] */ }
579 public ChunkEnumerator
GetChunks() => new ChunkEnumerator(this);
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).
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)
594 /// Implement IEnumerable.GetEnumerator() to return 'this' as the IEnumerator
596 [ComponentModel
.EditorBrowsable(ComponentModel
.EditorBrowsableState
.Never
)] // Only here to make foreach work
597 public ChunkEnumerator
GetEnumerator() { return this; }
600 /// Implements the IEnumerator pattern.
602 public bool MoveNext()
604 if (_currentChunk
== _firstChunk
)
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
;
626 /// Implements the IEnumerator pattern.
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
);
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.
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
);
658 _manyChunks
= new ManyChunkInfo(stringBuilder
, chunkCount
);
662 private static int ChunkCount(StringBuilder
? stringBuilder
)
665 while (stringBuilder
!= null)
668 stringBuilder
= stringBuilder
.m_ChunkPrevious
;
674 /// Used to hold all the chunks indexes when you have many chunks.
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
)
688 current
= _chunks
[pos
];
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
;
708 /// Appends a character 0 or more times to the end of this builder.
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
)
716 throw new ArgumentOutOfRangeException(nameof(repeatCount
), SR
.ArgumentOutOfRange_NegativeCount
);
719 if (repeatCount
== 0)
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;
742 m_ChunkLength
= index
;
743 ExpandByABlock(repeatCount
);
744 Debug
.Assert(m_ChunkLength
== 0);
749 m_ChunkLength
= index
;
755 /// Appends a range of characters to the end of this builder.
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
)
764 throw new ArgumentOutOfRangeException(nameof(startIndex
), SR
.ArgumentOutOfRange_GenericPositive
);
768 throw new ArgumentOutOfRangeException(nameof(charCount
), SR
.ArgumentOutOfRange_GenericPositive
);
773 if (startIndex
== 0 && charCount
== 0)
778 throw new ArgumentNullException(nameof(value));
780 if (charCount
> value.Length
- startIndex
)
782 throw new ArgumentOutOfRangeException(nameof(charCount
), SR
.ArgumentOutOfRange_Index
);
792 fixed (char* valueChars
= &value[startIndex
])
794 Append(valueChars
, charCount
);
802 /// Appends a string to the end of this builder.
804 /// <param name="value">The string to append.</param>
805 public StringBuilder
Append(string? value)
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
821 chunkChars
[chunkLength
] = value[0];
825 chunkChars
[chunkLength
+ 1] = value[1];
832 fixed (char* valuePtr
= value)
833 fixed (char* destPtr
= &chunkChars
[chunkLength
])
835 string.wstrcpy(destPtr
, valuePtr
, valueLen
);
840 m_ChunkLength
= newCurrentIndex
;
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)
857 fixed (char* valueChars
= value)
859 Append(valueChars
, value.Length
);
865 /// Appends part of a string to the end of this builder.
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
)
874 throw new ArgumentOutOfRangeException(nameof(startIndex
), SR
.ArgumentOutOfRange_Index
);
878 throw new ArgumentOutOfRangeException(nameof(count
), SR
.ArgumentOutOfRange_GenericPositive
);
883 if (startIndex
== 0 && count
== 0)
887 throw new ArgumentNullException(nameof(value));
895 if (startIndex
> value.Length
- count
)
897 throw new ArgumentOutOfRangeException(nameof(startIndex
), SR
.ArgumentOutOfRange_Index
);
902 fixed (char* valueChars
= value)
904 Append(valueChars
+ startIndex
, count
);
910 public StringBuilder
Append(StringBuilder
? value)
912 if (value != null && value.Length
!= 0)
914 return AppendCore(value, 0, value.Length
);
919 public StringBuilder
Append(StringBuilder
? value, int startIndex
, int count
)
923 throw new ArgumentOutOfRangeException(nameof(startIndex
), SR
.ArgumentOutOfRange_Index
);
928 throw new ArgumentOutOfRangeException(nameof(count
), SR
.ArgumentOutOfRange_GenericPositive
);
933 if (startIndex
== 0 && count
== 0)
937 throw new ArgumentNullException(nameof(value));
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
)
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
);
969 int length
= Math
.Min(m_ChunkChars
.Length
- m_ChunkLength
, count
);
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
;
985 public StringBuilder
AppendLine() => Append(Environment
.NewLine
);
987 public StringBuilder
AppendLine(string? 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
)
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
);
1032 StringBuilder
? chunk
= this;
1033 int sourceEndIndex
= sourceIndex
+ count
;
1034 int curDestIndex
= count
;
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
;
1062 /// Inserts a string 0 or more times into this builder at the specified position.
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
)
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)
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
;
1096 MakeRoom(index
, (int)insertingChars
, out chunk
, out indexInChunk
, false);
1099 fixed (char* valuePtr
= value)
1103 ReplaceInPlaceAtChunk(ref chunk
!, ref indexInChunk
, valuePtr
, value.Length
);
1113 /// Removes a range of characters from this builder.
1116 /// This method does not reduce the capacity of this builder.
1118 public StringBuilder
Remove(int startIndex
, int length
)
1122 throw new ArgumentOutOfRangeException(nameof(length
), SR
.ArgumentOutOfRange_NegativeLength
);
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)
1143 StringBuilder chunk
;
1145 Remove(startIndex
, length
, out chunk
, out indexInChunk
);
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;
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
;
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
;
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)
1223 fixed (char* valueChars
= &value[0])
1225 Append(valueChars
, value.Length
);
1232 public StringBuilder
Append(ReadOnlySpan
<char> value)
1234 if (value.Length
> 0)
1238 fixed (char* valueChars
= &MemoryMarshal
.GetReference(value))
1240 Append(valueChars
, value.Length
);
1247 public StringBuilder
Append(ReadOnlyMemory
<char> value) => Append(value.Span
);
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);
1300 ThrowHelper
.ThrowArgumentNullException(ExceptionArgument
.values
);
1303 Debug
.Assert(values
!= null);
1304 using (IEnumerator
<T
> en
= values
.GetEnumerator())
1311 T
value = en
.Current
;
1314 Append(value.ToString());
1317 while (en
.MoveNext())
1319 Append(separator
, separatorLength
);
1323 Append(value.ToString());
1330 private unsafe StringBuilder AppendJoinCore
<T
>(char* separator
, int separatorLength
, T
[] values
)
1334 ThrowHelper
.ThrowArgumentNullException(ExceptionArgument
.values
);
1337 Debug
.Assert(values
!= null);
1338 if (values
.Length
== 0)
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)
1361 public StringBuilder
Insert(int index
, string? value)
1363 if ((uint)index
> (uint)Length
)
1365 throw new ArgumentOutOfRangeException(nameof(index
), SR
.ArgumentOutOfRange_Index
);
1372 fixed (char* sourcePtr
= value)
1373 Insert(index
, sourcePtr
, value.Length
);
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)
1392 Insert(index
, &value, 1);
1397 public StringBuilder
Insert(int index
, char[]? value)
1399 if ((uint)index
> (uint)Length
)
1401 throw new ArgumentOutOfRangeException(nameof(index
), SR
.ArgumentOutOfRange_Index
);
1406 Insert(index
, value, 0, value.Length
);
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
);
1421 if (startIndex
== 0 && charCount
== 0)
1425 throw new ArgumentNullException(nameof(value), SR
.ArgumentNull_String
);
1430 throw new ArgumentOutOfRangeException(nameof(startIndex
), SR
.ArgumentOutOfRange_StartIndex
);
1435 throw new ArgumentOutOfRangeException(nameof(charCount
), SR
.ArgumentOutOfRange_GenericPositive
);
1438 if (startIndex
> value.Length
- charCount
)
1440 throw new ArgumentOutOfRangeException(nameof(startIndex
), SR
.ArgumentOutOfRange_Index
);
1447 fixed (char* sourcePtr
= &value[startIndex
])
1448 Insert(index
, sourcePtr
, charCount
);
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)
1486 fixed (char* sourcePtr
= &MemoryMarshal
.GetReference(value))
1487 Insert(index
, sourcePtr
, value.Length
);
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
)
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
)
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
)
1544 throw new ArgumentNullException(nameof(format
));
1548 int len
= format
.Length
;
1551 ICustomFormatter
? cf
= null;
1552 if (provider
!= null)
1554 cf
= (ICustomFormatter
?)provider
.GetFormat(typeof(ICustomFormatter
));
1564 // Is it a closing brace?
1567 // Check next character (if there is one) to see if it is escaped. eg }}
1568 if (pos
< len
&& format
[pos
] == '}')
1574 // Otherwise treat it as an error (Mismatched closing brace)
1578 // Is it a opening brace?
1581 // Check next character (if there is one) to see if it is escaped. eg {{
1582 if (pos
< len
&& format
[pos
] == '{')
1588 // Otherwise treat it as the opening brace of an Argument Hole.
1593 // If it neither then treat the character as just text.
1598 // Start of parsing of Argument Hole.
1599 // Argument Hole ::= { Index (, WS* Alignment WS*)? (: Formatting)? }
1607 // Start of parsing required Index parameter.
1608 // Index ::= ('0'-'9')+ WS*
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();
1617 index
= index
* 10 + ch
- '0';
1619 // If reached end of text then error (Unexpected end of text)
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;
1645 // Is the character a comma, which indicates the start of alignment parameter.
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)
1659 // Is there a minus sign?
1663 // Yes, then alignment is left justified.
1666 // If reached end of text then error (Unexpected end of text)
1674 // If current character is not a digit then error (Unexpected character)
1675 if (ch
< '0' || ch
> '9')
1679 // Parse alignment digits.
1682 width
= width
* 10 + ch
- '0';
1684 // If reached end of text then error. (Unexpected end of text)
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.
1713 // If reached end of text then error. (Unexpected end of text)
1722 // Argument hole closed
1727 // Braces inside the argument hole are not supported
1736 itemFormatSpan
= format
.AsSpan(startPos
, pos
- startPos
);
1741 // Unexpected character
1745 // Construct the output for this arg hole.
1748 string? itemFormat
= null;
1752 if (itemFormatSpan
.Length
!= 0)
1754 itemFormat
= new string(itemFormatSpan
);
1756 s
= cf
.Format(itemFormat
, arg
, provider
);
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.
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)
1794 // Append it to the final output of the Format String.
1799 int pad
= width
- s
.Length
;
1800 if (!leftJustify
&& pad
> 0)
1806 if (leftJustify
&& pad
> 0)
1810 // Continue to parse other characters.
1816 /// Replaces all instances of one string with another in this builder.
1818 /// <param name="oldValue">The string to replace.</param>
1819 /// <param name="newValue">The string to replace <paramref name="oldValue"/> with.</param>
1821 /// If <paramref name="newValue"/> is <c>null</c>, instances of <paramref name="oldValue"/>
1822 /// are removed from this builder.
1824 public StringBuilder
Replace(string oldValue
, string? newValue
) => Replace(oldValue
, newValue
, 0, Length
);
1827 /// Determines if the contents of this builder are equal to the contents of another builder.
1829 /// <param name="sb">The other builder.</param>
1830 public bool Equals(StringBuilder
? sb
)
1836 if (Length
!= sb
.Length
)
1844 StringBuilder
? thisChunk
= this;
1845 int thisChunkIndex
= thisChunk
.m_ChunkLength
;
1846 StringBuilder
? sbChunk
= sb
;
1847 int sbChunkIndex
= sbChunk
.m_ChunkLength
;
1853 while (thisChunkIndex
< 0)
1855 thisChunk
= thisChunk
.m_ChunkPrevious
;
1856 if (thisChunk
== null)
1860 thisChunkIndex
= thisChunk
.m_ChunkLength
+ thisChunkIndex
;
1863 while (sbChunkIndex
< 0)
1865 sbChunk
= sbChunk
.m_ChunkPrevious
;
1866 if (sbChunk
== null)
1870 sbChunkIndex
= sbChunk
.m_ChunkLength
+ sbChunkIndex
;
1873 if (thisChunkIndex
< 0)
1875 return sbChunkIndex
< 0;
1877 if (sbChunkIndex
< 0)
1882 Debug
.Assert(thisChunk
!= null && sbChunk
!= null);
1883 if (thisChunk
.m_ChunkChars
[thisChunkIndex
] != sbChunk
.m_ChunkChars
[sbChunkIndex
])
1891 /// Determines if the contents of this builder are equal to the contents of <see cref="ReadOnlySpan{Char}"/>.
1893 /// <param name="span">The <see cref="ReadOnlySpan{Char}"/>.</param>
1894 public bool Equals(ReadOnlySpan
<char> span
)
1896 if (span
.Length
!= Length
)
1901 StringBuilder
? sbChunk
= this;
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
)))
1916 sbChunk
= sbChunk
.m_ChunkPrevious
;
1917 } while (sbChunk
!= null);
1919 Debug
.Assert(offset
== Length
);
1924 /// Replaces all instances of one string with another in part of this builder.
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>
1931 /// If <paramref name="newValue"/> is <c>null</c>, instances of <paramref name="oldValue"/>
1932 /// are removed from this builder.
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
;
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
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
;
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!");
2012 /// Replaces all instances of one character with another in this builder.
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
);
2022 /// Replaces all instances of one character with another in this builder.
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;
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
;
2059 if (startIndexInChunk
>= 0)
2064 Debug
.Assert(chunk
.m_ChunkPrevious
!= null);
2065 chunk
= chunk
.m_ChunkPrevious
;
2073 /// Appends a character buffer to this builder.
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
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
;
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
;
2125 /// Inserts a character buffer into this builder at the specified position.
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
);
2139 StringBuilder chunk
;
2141 MakeRoom(index
, valueCount
, out chunk
, out indexInChunk
, false);
2142 ReplaceInPlaceAtChunk(ref chunk
!, ref indexInChunk
, value, valueCount
);
2147 /// Replaces strings at specified indices with a new string in a chunk.
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>
2155 /// This routine is very efficient because it does replacements in bulk.
2157 private void ReplaceAllInChunk(int[]? replacements
, int replacementsCount
, StringBuilder sourceChunk
, int removeCount
, string value)
2159 if (replacementsCount
<= 0)
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.
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,
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
;
2194 if (i
>= replacementsCount
)
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
);
2211 targetIndexInChunk
+= gapEnd
- gapStart
;
2212 Debug
.Assert(targetIndexInChunk
<= targetChunk
.m_ChunkLength
, "gap not in chunk");
2216 // Remove extra space if necessary.
2219 Remove(targetChunk
.m_ChunkOffset
+ targetIndexInChunk
, -delta
, out targetChunk
, out targetIndexInChunk
);
2226 /// Returns a value indicating whether a substring of a builder starts with a specified prefix.
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
++)
2241 if (indexInChunk
>= chunk
.m_ChunkLength
)
2243 chunk
= Next(chunk
)!;
2251 if (value[i
] != chunk
.m_ChunkChars
[indexInChunk
])
2264 /// Replaces characters at a specified location with the contents of a character buffer.
2265 /// This function is the logical equivalent of memcpy.
2267 /// <param name="chunk">
2268 /// The chunk in which to start replacing characters.
2269 /// Receives the chunk in which character replacement ends.
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.
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
)
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
);
2297 count
-= lengthToCopy
;
2302 value += lengthToCopy
;
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.
2311 private static unsafe void ThreadSafeCopy(char* sourcePtr
, char[] destination
, int destinationIndex
, int count
)
2315 if ((uint)destinationIndex
<= (uint)destination
.Length
&& (destinationIndex
+ count
) <= destination
.Length
)
2317 fixed (char* destinationPtr
= &destination
[destinationIndex
])
2318 string.wstrcpy(destinationPtr
, sourcePtr
, count
);
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
)
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
);
2348 /// Gets the chunk corresponding to the logical index in this builder.
2350 /// <param name="index">The logical index in this builder.</param>
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"/>.
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);
2371 /// Gets the chunk corresponding to the logical byte index in this builder.
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);
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
);
2397 /// Finds the chunk that logically succeeds the specified chunk.
2399 /// <param name="chunk">The chunk whose successor should be found.</param>
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
2406 private StringBuilder
? Next(StringBuilder chunk
) => chunk
== this ? null : FindChunkForIndex(chunk
.m_ChunkOffset
+ chunk
.m_ChunkLength
);
2409 /// Transfers the character buffer from this chunk to a new chunk, and allocates a new buffer with a minimum size for this chunk.
2411 /// <param name="minBlockCharCount">The minimum size of the new buffer to be allocated for this chunk.</param>
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.
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);
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
;
2449 m_ChunkChars
= chunkChars
;
2455 /// Creates a new chunk with fields copied from an existing chunk.
2457 /// <param name="from">The chunk from which to copy fields.</param>
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).
2465 /// Callers are expected to update <paramref name="from"/> subsequently to point to this
2466 /// chunk as its predecessor.
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
;
2481 /// Creates a gap at a logical index with the specified count.
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.
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
2503 /// <see cref="ReplaceInPlaceAtChunk"/> can be used in conjunction with this method to fill in the newly created gap.
2506 private void MakeRoom(int index
, int count
, out StringBuilder chunk
, out int indexInChunk
, bool doNotMoveFollowingChars
)
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
);
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
;)
2533 chunk
.m_ChunkChars
[i
+ count
] = chunk
.m_ChunkChars
[i
];
2535 chunk
.m_ChunkLength
+= count
;
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
);
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
)
2570 indexInChunk
= copyCount1
;
2577 /// Used by <see cref="MakeRoom"/> to allocate another chunk.
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
;
2599 /// Removes a specified number of characters beginning at a logical index in this builder.
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.
2607 private void Remove(int startIndex
, int count
, out StringBuilder chunk
, out int indexInChunk
)
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.
2616 StringBuilder
? endChunk
= null;
2617 int endIndexInChunk
= 0;
2620 if (endIndex
- chunk
.m_ChunkOffset
>= 0)
2622 if (endChunk
== null)
2625 endIndexInChunk
= endIndex
- endChunk
.m_ChunkOffset
;
2627 if (startIndex
- chunk
.m_ChunkOffset
>= 0)
2629 indexInChunk
= startIndex
- chunk
.m_ChunkOffset
;
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
;
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!");