mirror of
https://github.com/modernuo/ModernUO
synced 2026-08-11 22:23:06 -04:00
## Summary
- **Add a self-referencing `InterpolationHandler` to `ValueStringBuilder`** that writes directly into the builder's buffer — zero intermediate allocation, works with `stackalloc`-backed builders
- **Replace all `System.Text.StringBuilder` usage** across the codebase with `ValueStringBuilder`
- **Convert `ValueStringBuilder.Create()` to `stackalloc`** at 10 sites where output length is provably bounded
- **Convert manual `Dispose()` to `using var`** where possible, and hoist loop-scoped builders outside loops with `Reset()`
- **Convert verbose `Append()` chains to `Append($"...")`** interpolation for readability
- **Add comprehensive documentation** for string handling patterns
## InterpolationHandler Design
`ValueStringBuilder` is a `ref struct`, which creates challenges for C#'s interpolated string handler pattern:
- **`ref` fields to ref structs are not allowed** (CS9050)
- **`[InterpolatedStringHandlerArgument("")]` passes struct receivers by value**, not by ref
- **`ISelfInterpolatedStringHandler` requires boxing** ref structs into interface fields
**Solution: Copy-and-reconcile pattern.** The handler receives a value copy of the builder. The copy shares the same underlying `char` buffer (`Span` points to the same `stackalloc`/pooled memory), so writes go to the original buffer. `Append()` reconciles by `this = handler._builder`, updating `_length` and any buffer references changed by `Grow()`.
This is safe because:
- The game loop is single-threaded — no concurrent access between handler construction and reconciliation
- If `Grow()` occurs in the copy, the original's stale buffer isn't accessed until `Append()` replaces it
- `Dispose()` correctly returns the reconciled buffer to the pool
## Changes by Category
### ValueStringBuilder (`Projects/Server/Buffers/ValueStringBuilder.cs`)
- Added nested `InterpolationHandler` ref struct with copy-and-reconcile pattern
- Added `Append([InterpolatedStringHandlerArgument("")] scoped ref InterpolationHandler)` method
- Removed `RawInterpolatedStringHandler` overloads (new handler replaces them)
- All `AppendFormatted` overloads delegate to existing `Append` methods (no code duplication)
- Alignment support via direct private field access (nested type privilege)
### StringBuilder → ValueStringBuilder (15 files)
Replaced all `new StringBuilder()` with `ValueStringBuilder.Create()` or `stackalloc`:
- ConPVP games: KingOfTheHill, DoubleDom, CTF, BombingRun, TourneyMatch
- ConPVP infrastructure: Tournament, Participant, TourneyParticipant
- ConPVP gumps: ArenaGump, TournamentBracketGump, AcceptTeamGump, ConfirmSignupGump
- Commands: Handlers, Logging, Add
- Other: TownCrier, SpeechLogGump, TestCenter
Key patterns:
- `sb = new StringBuilder()` reassignment → `sb.Reset()`
- `sb.AppendFormat("{0:N0}", value)` → `sb.Append($"{value:N0}")`
- `sb.Append(x).Append(y)` chains → separate statements (VSB returns void)
### Create() → stackalloc (10 files)
Converted heap-allocated builders to stackalloc where output is bounded:
- ClientVersion (32), MapSelection (160), HouseRaffleStone (48)
- HolySense (96), UnholySense (96), ClientVerification (192)
- AcceptTeamGump (64), ConfirmSignupGump (64)
- BaseWeapon (160), BaseArmor (128)
### Loop optimizations (2 files)
Hoisted `ValueStringBuilder` creation outside loops with `Reset()` per iteration:
- TourneyMatch.cs: `using var` inside for loop → stackalloc before loop
- ArenaGump.cs: `Create()` + `Dispose()` per iteration → stackalloc before loop
### Append chain → interpolation (5 files)
Converted multi-line `Append()` chains to `Append($"...")`:
- BountyMessage.cs: title switch (6 cases), paragraph (15→1 Append), description lines, closing
- AcceptTeamGump, ConfirmSignupGump, TournamentBracketGump: tournament type strings
- AdminGump: comment/tag formatting in loops
### Documentation
- `dev-docs/string-handling.md`: Full reference — construction, interpolation, disposal, decision guide
- `dev-docs/claude-skills/modernuo-string-handling.md`: Claude skill with quick reference
- `CLAUDE.md`: Added rule 17 (no StringBuilder), dev-docs table entry, skills table entry
- `dev-docs/code-standards.md`: Updated memory management section
## Test Plan
- [x] `dotnet build` — 0 errors, 0 warnings
- [x] `dotnet test` — 940/940 tests pass
- [x] 28 ValueStringBuilder tests covering all reconciliation scenarios:
- Stackalloc no-grow, stackalloc with grow (→pool transition)
- Heap no-grow, heap with grow, heap double grow
- Pre-existing content with and without grow
- Sequential multiple `Append($"...")` calls
- Mixed plain + interpolated Append
- Empty interpolation, literal-only, format specifiers
- Null string holes, ISpanFormattable types
- Dispose after stackalloc→pool grow
714 lines
21 KiB
C#
714 lines
21 KiB
C#
/*************************************************************************
|
|
* ModernUO *
|
|
* Copyright 2019-2026 - ModernUO Development Team *
|
|
* Email: hi@modernuo.com *
|
|
* File: LocalizationEntry.cs *
|
|
* *
|
|
* This program is free software: you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation, either version 3 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
* You should have received a copy of the GNU General Public License *
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
*************************************************************************/
|
|
|
|
using System;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text.RegularExpressions;
|
|
using Server.Buffers;
|
|
using Server.Text;
|
|
using Server.Collections;
|
|
|
|
namespace Server;
|
|
|
|
public class LocalizationEntry
|
|
{
|
|
private static readonly Regex _textRegex = new(
|
|
@"~(\d+)[_\w]+~",
|
|
RegexOptions.Compiled |
|
|
RegexOptions.IgnoreCase |
|
|
RegexOptions.Singleline |
|
|
RegexOptions.CultureInvariant
|
|
);
|
|
|
|
public string Language { get; }
|
|
public int Number { get; }
|
|
public string Text { get; }
|
|
public string[] TextSlices { get; }
|
|
public string StringFormatter { get; }
|
|
|
|
public LocalizationEntry(string lang, int number, string text)
|
|
{
|
|
Language = lang;
|
|
Number = number;
|
|
Text = text;
|
|
|
|
ParseText(text, out var textSlices, out var stringFormatter);
|
|
TextSlices = textSlices;
|
|
StringFormatter = stringFormatter;
|
|
}
|
|
|
|
private static void ParseText(string text, out string[] textSlices, out string stringFormatter)
|
|
{
|
|
using var sb = ValueStringBuilder.Create(256);
|
|
using var queue = PooledRefQueue<string>.Create();
|
|
|
|
var hasMatch = false;
|
|
var prevIndex = 0;
|
|
foreach (Match match in _textRegex.Matches(text))
|
|
{
|
|
if (prevIndex < match.Index)
|
|
{
|
|
var substr = text[prevIndex..match.Index];
|
|
sb.Append(substr);
|
|
|
|
queue.Enqueue(substr);
|
|
}
|
|
|
|
queue.Enqueue(null);
|
|
hasMatch = true;
|
|
sb.Append($"{{{int.Parse(match.Groups[1].Value) - 1}}}");
|
|
prevIndex = match.Index + match.Length;
|
|
}
|
|
|
|
if (prevIndex < text.Length - 1)
|
|
{
|
|
var substr = prevIndex == 0 ? text : text[prevIndex..];
|
|
sb.Append(substr);
|
|
queue.Enqueue(substr);
|
|
}
|
|
|
|
textSlices = queue.ToArray();
|
|
stringFormatter = hasMatch ? sb.ToString() : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a formatted string of the localization entry.
|
|
/// Uses string interpolation under the hood. This method is preferably relative to the object array method signature.
|
|
/// Example:
|
|
/// Format($"{totalItems}{maxItems}{totalWeight}");
|
|
/// </summary>
|
|
/// <param name="handler">interpolated string handler used by the compiler as a string builder during compilation</param>
|
|
/// <returns>A copy of the localization text where the placeholder arguments have been replaced with string representations of the provided interpolation arguments</returns>
|
|
public PooledArraySpanFormattable Format(
|
|
[InterpolatedStringHandlerArgument("")]
|
|
ref LocalizationInterpolationHandler handler
|
|
)
|
|
{
|
|
var chars = handler.ToPooledArray(out var length);
|
|
handler = default; // Defensive clear
|
|
return new PooledArraySpanFormattable(chars, length);
|
|
}
|
|
|
|
[InterpolatedStringHandler]
|
|
public ref struct LocalizationInterpolationHandler
|
|
{
|
|
private static string[] _empty = Array.Empty<string>();
|
|
|
|
private char[]? _arrayToReturnToPool;
|
|
private Span<char> _chars;
|
|
private int _pos;
|
|
|
|
private int _index;
|
|
private string?[] _slices;
|
|
private string? _current;
|
|
private string _lang;
|
|
|
|
public LocalizationInterpolationHandler(int literalLength, int formattedCount, LocalizationEntry entry, out bool isValid)
|
|
{
|
|
_slices = entry.TextSlices;
|
|
_chars = _arrayToReturnToPool = STArrayPool<char>.Shared.Rent(256);
|
|
isValid = true;
|
|
|
|
_pos = 0;
|
|
_index = 0;
|
|
_current = null;
|
|
_lang = entry.Language;
|
|
}
|
|
|
|
public LocalizationInterpolationHandler(
|
|
int literalLength, int formattedCount, int number, out bool isValid
|
|
) : this(literalLength, formattedCount, number, Localization.FallbackLanguage, out isValid)
|
|
{
|
|
}
|
|
|
|
public LocalizationInterpolationHandler(
|
|
int literalLength, int formattedCount, int number, string lang, out bool isValid
|
|
)
|
|
{
|
|
if (Localization.TryGetLocalization(lang, number, out var entry))
|
|
{
|
|
_slices = entry.TextSlices;
|
|
_chars = _arrayToReturnToPool = STArrayPool<char>.Shared.Rent(256);
|
|
isValid = true;
|
|
}
|
|
else
|
|
{
|
|
_slices = _empty;
|
|
_chars = _arrayToReturnToPool = default;
|
|
isValid = false;
|
|
}
|
|
|
|
_pos = 0;
|
|
_index = 0;
|
|
_current = null;
|
|
_lang = lang;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private bool MoveNext()
|
|
{
|
|
if ((uint)_index >= (uint)_slices.Length)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_current = _slices[_index++];
|
|
return true;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private bool ReadyToAppend()
|
|
{
|
|
if (!MoveNext())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (_current == null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
AppendStringDirect(_current);
|
|
return MoveNext();
|
|
}
|
|
|
|
public void AppendLiteral(string value)
|
|
{
|
|
}
|
|
|
|
private void AppendStringDirect(string value)
|
|
{
|
|
if (value.TryCopyTo(_chars[_pos..]))
|
|
{
|
|
_pos += value.Length;
|
|
}
|
|
else
|
|
{
|
|
GrowThenCopyString(value);
|
|
}
|
|
}
|
|
|
|
public void AppendFormatted<T>(T value)
|
|
{
|
|
if (!ReadyToAppend())
|
|
{
|
|
return;
|
|
}
|
|
|
|
string? s;
|
|
if (value is IFormattable)
|
|
{
|
|
// If the value can format itself directly into our buffer, do so.
|
|
if (value is ISpanFormattable)
|
|
{
|
|
int charsWritten;
|
|
while (!((ISpanFormattable)value).TryFormat(_chars[_pos..], out charsWritten, default, default)) // constrained call avoiding boxing for value types
|
|
{
|
|
Grow();
|
|
}
|
|
|
|
_pos += charsWritten;
|
|
return;
|
|
}
|
|
|
|
s = ((IFormattable)value).ToString(format: null, default); // constrained call avoiding boxing for value types
|
|
}
|
|
else
|
|
{
|
|
s = value?.ToString();
|
|
}
|
|
|
|
if (s is not null)
|
|
{
|
|
AppendStringDirect(s);
|
|
}
|
|
}
|
|
|
|
// Each numeric needs its own override
|
|
public void AppendFormatted(int value, string? format)
|
|
{
|
|
if (!ReadyToAppend())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!TryAppendCliloc(value, format))
|
|
{
|
|
AppendFormattedDirect(value, format);
|
|
}
|
|
}
|
|
|
|
public void AppendFormatted(uint value, string? format)
|
|
{
|
|
if (!ReadyToAppend())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!TryAppendCliloc((int)value, format))
|
|
{
|
|
AppendFormattedDirect(value, format);
|
|
}
|
|
}
|
|
|
|
public void AppendFormatted(long value, string? format)
|
|
{
|
|
if (!ReadyToAppend())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!TryAppendCliloc((int)value, format))
|
|
{
|
|
AppendFormattedDirect(value, format);
|
|
}
|
|
}
|
|
|
|
public void AppendFormatted(ulong value, string? format)
|
|
{
|
|
if (!ReadyToAppend())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!TryAppendCliloc((int)value, format))
|
|
{
|
|
AppendFormattedDirect(value, format);
|
|
}
|
|
}
|
|
|
|
public void AppendFormatted<T>(T value, string? format)
|
|
{
|
|
if (ReadyToAppend())
|
|
{
|
|
AppendFormattedDirect(value, format);
|
|
}
|
|
}
|
|
|
|
private void AppendFormattedDirect<T>(T value, string? format)
|
|
{
|
|
string? s;
|
|
if (value is IFormattable)
|
|
{
|
|
// If the value can format itself directly into our buffer, do so.
|
|
if (value is ISpanFormattable)
|
|
{
|
|
int charsWritten;
|
|
while (!((ISpanFormattable)value).TryFormat(_chars[_pos..], out charsWritten, format, default)) // constrained call avoiding boxing for value types
|
|
{
|
|
Grow();
|
|
}
|
|
|
|
_pos += charsWritten;
|
|
return;
|
|
}
|
|
|
|
s = ((IFormattable)value).ToString(format, default); // constrained call avoiding boxing for value types
|
|
}
|
|
else
|
|
{
|
|
s = value?.ToString();
|
|
}
|
|
|
|
if (s is not null)
|
|
{
|
|
AppendStringDirect(s);
|
|
}
|
|
}
|
|
|
|
public void AppendFormatted<T>(T value, int alignment)
|
|
{
|
|
if (!ReadyToAppend())
|
|
{
|
|
return;
|
|
}
|
|
|
|
var startingPos = _pos;
|
|
AppendFormatted(value);
|
|
if (alignment != 0)
|
|
{
|
|
AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
|
|
}
|
|
}
|
|
|
|
// Each numeric needs its own override
|
|
public void AppendFormatted(int value, int alignment, string? format)
|
|
{
|
|
if (!ReadyToAppend())
|
|
{
|
|
return;
|
|
}
|
|
|
|
var startingPos = _pos;
|
|
if (TryAppendCliloc(value, format))
|
|
{
|
|
AppendFormatted(value, format);
|
|
if (alignment != 0)
|
|
{
|
|
AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AppendFormattedDirect(value, alignment, format);
|
|
}
|
|
}
|
|
|
|
public void AppendFormatted(uint value, int alignment, string? format)
|
|
{
|
|
if (!ReadyToAppend())
|
|
{
|
|
return;
|
|
}
|
|
|
|
var startingPos = _pos;
|
|
if (TryAppendCliloc((int)value, format))
|
|
{
|
|
AppendFormatted(value, format);
|
|
if (alignment != 0)
|
|
{
|
|
AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AppendFormattedDirect(value, alignment, format);
|
|
}
|
|
}
|
|
|
|
public void AppendFormatted(long value, int alignment, string? format)
|
|
{
|
|
if (!ReadyToAppend())
|
|
{
|
|
return;
|
|
}
|
|
|
|
var startingPos = _pos;
|
|
if (TryAppendCliloc((int)value, format))
|
|
{
|
|
AppendFormatted(value, format);
|
|
if (alignment != 0)
|
|
{
|
|
AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AppendFormattedDirect(value, alignment, format);
|
|
}
|
|
}
|
|
|
|
public void AppendFormatted(ulong value, int alignment, string? format)
|
|
{
|
|
if (!ReadyToAppend())
|
|
{
|
|
return;
|
|
}
|
|
|
|
var startingPos = _pos;
|
|
if (TryAppendCliloc((int)value, format))
|
|
{
|
|
AppendFormatted(value, format);
|
|
if (alignment != 0)
|
|
{
|
|
AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AppendFormattedDirect(value, alignment, format);
|
|
}
|
|
}
|
|
|
|
public void AppendFormatted<T>(T value, int alignment, string? format)
|
|
{
|
|
if (!ReadyToAppend())
|
|
{
|
|
return;
|
|
}
|
|
|
|
AppendFormattedDirect(value, alignment, format);
|
|
}
|
|
|
|
private void AppendFormattedDirect<T>(T value, int alignment, string? format)
|
|
{
|
|
var startingPos = _pos;
|
|
AppendFormatted(value, format);
|
|
if (alignment != 0)
|
|
{
|
|
AppendOrInsertAlignmentIfNeeded(startingPos, alignment);
|
|
}
|
|
}
|
|
|
|
public void AppendFormatted(ReadOnlySpan<char> value)
|
|
{
|
|
if (!ReadyToAppend() || TryAppendClilocByNumericString(value))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (value.TryCopyTo(_chars[_pos..]))
|
|
{
|
|
_pos += value.Length;
|
|
}
|
|
else
|
|
{
|
|
GrowThenCopySpan(value);
|
|
}
|
|
}
|
|
|
|
public void AppendFormatted(ReadOnlySpan<char> value, int alignment, string? format = null)
|
|
{
|
|
if (!ReadyToAppend())
|
|
{
|
|
return;
|
|
}
|
|
|
|
var leftAlign = false;
|
|
if (alignment < 0)
|
|
{
|
|
leftAlign = true;
|
|
alignment = -alignment;
|
|
}
|
|
|
|
var paddingRequired = alignment - value.Length;
|
|
if (paddingRequired <= 0)
|
|
{
|
|
// The value is as large or larger than the required amount of padding,
|
|
// so just write the value.
|
|
AppendFormatted(value);
|
|
return;
|
|
}
|
|
|
|
// Write the value along with the appropriate padding.
|
|
EnsureCapacityForAdditionalChars(value.Length + paddingRequired);
|
|
if (leftAlign)
|
|
{
|
|
value.CopyTo(_chars[_pos..]);
|
|
_pos += value.Length;
|
|
_chars.Slice(_pos, paddingRequired).Fill(' ');
|
|
_pos += paddingRequired;
|
|
}
|
|
else
|
|
{
|
|
_chars.Slice(_pos, paddingRequired).Fill(' ');
|
|
_pos += paddingRequired;
|
|
value.CopyTo(_chars[_pos..]);
|
|
_pos += value.Length;
|
|
}
|
|
}
|
|
|
|
public void AppendFormatted(object? value, int alignment = 0, string? format = null)
|
|
{
|
|
if (value is int i)
|
|
{
|
|
AppendFormatted(i, alignment, format);
|
|
}
|
|
else if (value is uint ui)
|
|
{
|
|
AppendFormatted(ui, alignment, format);
|
|
}
|
|
else if (value is long l)
|
|
{
|
|
AppendFormatted(l, alignment, format);
|
|
}
|
|
else if (value is ulong ul)
|
|
{
|
|
AppendFormatted(ul, alignment, format);
|
|
}
|
|
else
|
|
{
|
|
AppendFormatted<object?>(value, alignment, format);
|
|
}
|
|
}
|
|
|
|
public void AppendFormatted(string? value)
|
|
{
|
|
if (!ReadyToAppend() || TryAppendClilocByNumericString(value))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (value?.TryCopyTo(_chars[_pos..]) == true)
|
|
{
|
|
_pos += value.Length;
|
|
}
|
|
else
|
|
{
|
|
AppendFormattedSlow(value);
|
|
}
|
|
}
|
|
|
|
public void AppendFormatted(string? value, int alignment, string? format = null) =>
|
|
AppendFormatted<string?>(value, alignment, format);
|
|
|
|
public bool TryAppendCliloc(int number, string? format)
|
|
{
|
|
if (format != "#" || !Localization.TryGetLocalization(_lang, number, out var entry))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var text = entry.Text;
|
|
|
|
if (text.TryCopyTo(_chars[_pos..]))
|
|
{
|
|
_pos += text.Length;
|
|
}
|
|
else
|
|
{
|
|
AppendFormattedSlow(text);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool TryAppendClilocByNumericString(ReadOnlySpan<char> value) =>
|
|
value[0] == '#' && long.TryParse(value[1..], out var number) && TryAppendCliloc((int)number, "#");
|
|
|
|
private void AppendOrInsertAlignmentIfNeeded(int startingPos, int alignment)
|
|
{
|
|
var charsWritten = _pos - startingPos;
|
|
|
|
var leftAlign = false;
|
|
if (alignment < 0)
|
|
{
|
|
leftAlign = true;
|
|
alignment = -alignment;
|
|
}
|
|
|
|
var paddingNeeded = alignment - charsWritten;
|
|
if (paddingNeeded > 0)
|
|
{
|
|
EnsureCapacityForAdditionalChars(paddingNeeded);
|
|
|
|
if (leftAlign)
|
|
{
|
|
_chars.Slice(_pos, paddingNeeded).Fill(' ');
|
|
}
|
|
else
|
|
{
|
|
_chars.Slice(startingPos, charsWritten).CopyTo(_chars[(startingPos + paddingNeeded)..]);
|
|
_chars.Slice(startingPos, paddingNeeded).Fill(' ');
|
|
}
|
|
|
|
_pos += paddingNeeded;
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private void AppendFormattedSlow(string? value)
|
|
{
|
|
if (value is not null)
|
|
{
|
|
EnsureCapacityForAdditionalChars(value.Length);
|
|
value.CopyTo(_chars[_pos..]);
|
|
_pos += value.Length;
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void EnsureCapacityForAdditionalChars(int additionalChars)
|
|
{
|
|
if (_chars.Length - _pos < additionalChars)
|
|
{
|
|
Grow(additionalChars);
|
|
}
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private void GrowThenCopyString(string value)
|
|
{
|
|
Grow(value.Length);
|
|
value.CopyTo(_chars[_pos..]);
|
|
_pos += value.Length;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
|
private void GrowThenCopySpan(ReadOnlySpan<char> value)
|
|
{
|
|
Grow(value.Length);
|
|
value.CopyTo(_chars[_pos..]);
|
|
_pos += value.Length;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)] // keep consumers as streamlined as possible
|
|
private void Grow(int additionalChars)
|
|
{
|
|
GrowCore((uint)_pos + (uint)additionalChars);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.NoInlining)] // keep consumers as streamlined as possible
|
|
private void Grow()
|
|
{
|
|
GrowCore((uint)_chars.Length + 1);
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
private void GrowCore(uint requiredMinCapacity)
|
|
{
|
|
var newCapacity = Math.Max(requiredMinCapacity, Math.Min((uint)_chars.Length * 2, 0x3FFFFFDF));
|
|
var arraySize = (int)Math.Clamp(newCapacity, 256, int.MaxValue);
|
|
|
|
var newArray = STArrayPool<char>.Shared.Rent(arraySize);
|
|
_chars[.._pos].CopyTo(newArray);
|
|
|
|
var toReturn = _arrayToReturnToPool;
|
|
_chars = _arrayToReturnToPool = newArray;
|
|
|
|
if (toReturn is not null)
|
|
{
|
|
STArrayPool<char>.Shared.Return(toReturn);
|
|
}
|
|
}
|
|
|
|
internal ReadOnlySpan<char> Text => _chars[.._pos];
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
internal void Clear()
|
|
{
|
|
var toReturn = _arrayToReturnToPool;
|
|
this = default; // defensive clear
|
|
if (toReturn is not null)
|
|
{
|
|
STArrayPool<char>.Shared.Return(toReturn);
|
|
}
|
|
}
|
|
|
|
public string ToStringAndClear()
|
|
{
|
|
if (MoveNext() && _current != null)
|
|
{
|
|
AppendStringDirect(_current);
|
|
}
|
|
|
|
var result = new string(Text);
|
|
Clear();
|
|
return result;
|
|
}
|
|
|
|
public char[] ToPooledArray(out int length)
|
|
{
|
|
if (MoveNext() && _current != null)
|
|
{
|
|
AppendStringDirect(_current);
|
|
}
|
|
|
|
length = _pos;
|
|
return _arrayToReturnToPool;
|
|
}
|
|
}
|
|
}
|