Created
November 25, 2025 20:36
-
-
Save willnationsdev/484be663db90ef24432685b7d154afbf to your computer and use it in GitHub Desktop.
A modified ValueStringBuilder that is compatible with netstandard2.0 projects, adapted from Roslyn source code.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Licensed to the .NET Foundation under one or more agreements. | |
| // The .NET Foundation licenses this file to you under the MIT license. | |
| // See the LICENSE file in the project root for more information. | |
| #pragma warning disable IDE0290 | |
| #pragma warning disable IDE0130 | |
| using System.Buffers; | |
| using System.Diagnostics; | |
| using System.Runtime.CompilerServices; | |
| using System.Runtime.InteropServices; | |
| namespace System.Text; | |
| internal ref struct ValueStringBuilder | |
| { | |
| private char[]? _arrayToReturnToPool; | |
| private Span<char> _chars; | |
| private int _pos; | |
| public ValueStringBuilder(Span<char> initialBuffer) | |
| { | |
| _arrayToReturnToPool = null; | |
| _chars = initialBuffer; | |
| _pos = 0; | |
| } | |
| public int Length | |
| { | |
| get => _pos; | |
| set | |
| { | |
| Debug.Assert(value <= _chars.Length); | |
| _pos = value; | |
| } | |
| } | |
| public int Capacity => _chars.Length; | |
| public void EnsureCapacity(int capacity) | |
| { | |
| if (capacity > _chars.Length) | |
| Grow(capacity - _chars.Length); | |
| } | |
| /// <summary> | |
| /// Get a pinnable reference to the builder. | |
| /// </summary> | |
| /// <param name="terminate">Ensures that the builder has a null char after <see cref="Length"/></param> | |
| public ref char GetPinnableReference(bool terminate = false) | |
| { | |
| if (terminate) | |
| { | |
| EnsureCapacity(Length + 1); | |
| _chars[Length] = '\0'; | |
| } | |
| return ref MemoryMarshal.GetReference(_chars); | |
| } | |
| public ref char this[int index] | |
| { | |
| get | |
| { | |
| Debug.Assert(index < _pos); | |
| return ref _chars[index]; | |
| } | |
| } | |
| public override string ToString() | |
| { | |
| unsafe | |
| { | |
| fixed (char* ptr = &_chars.GetPinnableReference()) | |
| { | |
| var s = new string(ptr, 0, _pos); | |
| Dispose(); | |
| return s; | |
| } | |
| } | |
| } | |
| /// <summary> | |
| /// Returns a span around the contents of the builder. | |
| /// </summary> | |
| /// <param name="terminate">Ensures that the builder has a null char after <see cref="Length"/></param> | |
| public ReadOnlySpan<char> AsSpan(bool terminate) | |
| { | |
| if (terminate) | |
| { | |
| EnsureCapacity(Length + 1); | |
| _chars[Length] = '\0'; | |
| } | |
| return _chars[.._pos]; | |
| } | |
| public ReadOnlySpan<char> AsSpan() => _chars[.._pos]; | |
| public ReadOnlySpan<char> AsSpan(int start) => _chars.Slice(start, _pos - start); | |
| public ReadOnlySpan<char> AsSpan(int start, int length) => _chars.Slice(start, length); | |
| public bool TryCopyTo(Span<char> destination, out int charsWritten) | |
| { | |
| if (_chars[.._pos].TryCopyTo(destination)) | |
| { | |
| charsWritten = _pos; | |
| Dispose(); | |
| return true; | |
| } | |
| charsWritten = 0; | |
| Dispose(); | |
| return false; | |
| } | |
| public void Insert(int index, char value, int count) | |
| { | |
| if (_pos > _chars.Length - count) | |
| { | |
| Grow(count); | |
| } | |
| var remaining = _pos - index; | |
| _chars.Slice(index, remaining).CopyTo(_chars.Slice(index + count)); | |
| _chars.Slice(index, count).Fill(value); | |
| _pos += count; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void Append(char c) | |
| { | |
| var pos = _pos; | |
| if (pos < _chars.Length) | |
| { | |
| _chars[pos] = c; | |
| _pos = pos + 1; | |
| } | |
| else | |
| { | |
| GrowAndAppend(c); | |
| } | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void Append(string s) | |
| { | |
| var pos = _pos; | |
| if (s.Length == 1 && pos < _chars.Length) // very common case, e.g. appending strings from NumberFormatInfo like separators, percent symbols, etc. | |
| { | |
| _chars[pos] = s[0]; | |
| _pos = pos + 1; | |
| } | |
| else | |
| { | |
| AppendSlow(s); | |
| } | |
| } | |
| private void AppendSlow(string s) | |
| { | |
| var pos = _pos; | |
| if (pos > _chars.Length - s.Length) | |
| { | |
| Grow(s.Length); | |
| } | |
| s.AsSpan().CopyTo(_chars[pos..]); | |
| _pos += s.Length; | |
| } | |
| public void Append(char c, int count) | |
| { | |
| if (_pos > _chars.Length - count) | |
| { | |
| Grow(count); | |
| } | |
| var dst = _chars.Slice(_pos, count); | |
| for (var i = 0; i < dst.Length; i++) | |
| { | |
| dst[i] = c; | |
| } | |
| _pos += count; | |
| } | |
| public unsafe void Append(char* value, int length) | |
| { | |
| var pos = _pos; | |
| if (pos > _chars.Length - length) | |
| { | |
| Grow(length); | |
| } | |
| var dst = _chars.Slice(_pos, length); | |
| for (var i = 0; i < dst.Length; i++) | |
| { | |
| dst[i] = *value++; | |
| } | |
| _pos += length; | |
| } | |
| public void Append(ReadOnlySpan<char> value) | |
| { | |
| var pos = _pos; | |
| if (pos > _chars.Length - value.Length) | |
| { | |
| Grow(value.Length); | |
| } | |
| value.CopyTo(_chars[_pos..]); | |
| _pos += value.Length; | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public Span<char> AppendSpan(int length) | |
| { | |
| var origPos = _pos; | |
| if (origPos > _chars.Length - length) | |
| { | |
| Grow(length); | |
| } | |
| _pos = origPos + length; | |
| return _chars.Slice(origPos, length); | |
| } | |
| [MethodImpl(MethodImplOptions.NoInlining)] | |
| private void GrowAndAppend(char c) | |
| { | |
| Grow(1); | |
| Append(c); | |
| } | |
| [MethodImpl(MethodImplOptions.NoInlining)] | |
| private void Grow(int requiredAdditionalCapacity) | |
| { | |
| Debug.Assert(requiredAdditionalCapacity > 0); | |
| var poolArray = ArrayPool<char>.Shared.Rent(Math.Max(_pos + requiredAdditionalCapacity, _chars.Length * 2)); | |
| _chars.CopyTo(poolArray); | |
| var toReturn = _arrayToReturnToPool; | |
| _chars = _arrayToReturnToPool = poolArray; | |
| if (toReturn != null) | |
| { | |
| ArrayPool<char>.Shared.Return(toReturn); | |
| } | |
| } | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public void Dispose() | |
| { | |
| var toReturn = _arrayToReturnToPool; | |
| this = default; // for safety, to avoid using pooled array if this instance is erroneously appended to again | |
| if (toReturn != null) | |
| { | |
| ArrayPool<char>.Shared.Return(toReturn); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment