|
| 1 | +--- |
| 2 | +uid: advanced_usage |
| 3 | +--- |
| 4 | + |
| 5 | +# Advanced usage |
| 6 | + |
| 7 | +This article goes a bit deeper than [Getting started](xref:getting_started) and shows patterns for squeezing the most performance out of `ValueStringBuilder` - mainly around providing your own buffer via `stackalloc`. |
| 8 | + |
| 9 | +## Using a stack-allocated buffer |
| 10 | + |
| 11 | +By default, `new ValueStringBuilder()` rents its initial buffer from `ArrayPool<char>.Shared`. Renting has a (small) cost, so if you know your string is short-lived and small, you can hand the builder a `stackalloc`'d buffer instead: |
| 12 | + |
| 13 | +```csharp |
| 14 | +using var stringBuilder = new ValueStringBuilder(stackalloc char[128]); |
| 15 | + |
| 16 | +stringBuilder.Append("Hello "); |
| 17 | +stringBuilder.Append("World"); |
| 18 | + |
| 19 | +Console.WriteLine(stringBuilder.ToString()); |
| 20 | +``` |
| 21 | + |
| 22 | +Because the buffer lives on the stack, this avoids the array-pool rent/return entirely for as long as the content fits. |
| 23 | + |
| 24 | +### What happens when the buffer is too small? |
| 25 | + |
| 26 | +`stackalloc` only reserves the initial capacity - it does **not** cap how much you can append. If the builder needs to grow beyond the buffer you gave it, [`EnsureCapacity`](xref:LinkDotNet.StringBuilder.ValueStringBuilder.EnsureCapacity(System.Int32)) transparently rents a bigger array from `ArrayPool<char>.Shared`, copies the existing content over, and continues from there: |
| 27 | + |
| 28 | +```csharp |
| 29 | +using var stringBuilder = new ValueStringBuilder(stackalloc char[4]); |
| 30 | + |
| 31 | +stringBuilder.Append("This is way longer than 4 characters"); // grows onto the array pool automatically |
| 32 | +``` |
| 33 | + |
| 34 | +This is why you should still `using`/`Dispose()` the builder even when you started it with a stack buffer, unless you can *guarantee* the content never exceeds the initial size. `Dispose()` only returns a pooled array if one was actually rented, so calling it on a builder that never grew is a cheap no-op. |
| 35 | + |
| 36 | +### Eliding `using` for guaranteed-small content |
| 37 | + |
| 38 | +If you control both the buffer size and every value being appended, you can skip the `using` statement altogether - see the "Fluent notation" section of [Known limitations](xref:known_limitations) for the full example. Do this only when growth is provably impossible (fixed-format output, bounded input, etc.); getting it wrong just means an extra rent/return, not a bug, but it defeats the purpose of using `stackalloc` in the first place. |
| 39 | + |
| 40 | +### Reusing one stack buffer across calls |
| 41 | + |
| 42 | +A common pattern is a small helper method that builds a short string entirely on the stack, without exposing the builder to the caller: |
| 43 | + |
| 44 | +```csharp |
| 45 | +private static string FormatCoordinate(int x, int y) |
| 46 | +{ |
| 47 | + Span<char> buffer = stackalloc char[32]; |
| 48 | + var stringBuilder = new ValueStringBuilder(buffer); |
| 49 | + |
| 50 | + stringBuilder.Append('('); |
| 51 | + stringBuilder.Append(x); |
| 52 | + stringBuilder.Append(", "); |
| 53 | + stringBuilder.Append(y); |
| 54 | + stringBuilder.Append(')'); |
| 55 | + |
| 56 | + return stringBuilder.ToString(); |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +As long as `buffer` is large enough for the expected input, this method never touches the heap except for the final `ToString()` call. |
| 61 | + |
| 62 | +> [!WARNING] |
| 63 | +> Never return a `stackalloc`-backed `ValueStringBuilder` (or a `ref` to it) from the method that declared the buffer - the buffer's stack frame is gone once the method returns. Pass the builder onward by `ref` to callees instead, as described in [Passing the ValueStringBuilder to a method](xref:pass_to_method). |
| 64 | +
|
| 65 | +## Avoiding boxing for value types |
| 66 | + |
| 67 | +`AppendJoin`, `Concat`, `AppendFormat`, `ReplaceGeneric`, and the interpolated-string `Append`/`AppendLine` overloads all special-case common value types (`int`, `long`, `double`, `decimal`, `DateTime`, `DateTimeOffset`, `TimeSpan`, `Guid`, and more) so they're formatted directly into the buffer via `ISpanFormattable` instead of being boxed to `object` first: |
| 68 | + |
| 69 | +```csharp |
| 70 | +using var stringBuilder = new ValueStringBuilder(); |
| 71 | + |
| 72 | +// No boxing for the int, double or Guid arguments below. |
| 73 | +stringBuilder.AppendJoin(", ", [1, 2, 3]); |
| 74 | +stringBuilder.AppendFormat($"{42:D5} {3.14:F2} {Guid.NewGuid()}"); |
| 75 | +``` |
| 76 | + |
| 77 | +For a type without a known fast path, the code falls back to the normal `ISpanFormattable`/`ToString()` path, so correctness is never sacrificed - only the hot, common types skip the allocation. See the [comparison](xref:comparison) article for the measured effect. |
| 78 | + |
| 79 | +## Pinning the buffer |
| 80 | + |
| 81 | +`GetPinnableReference()` lets the compiler-generated `fixed` pattern work directly against the builder's internal buffer, which is handy for interop: |
| 82 | + |
| 83 | +```csharp |
| 84 | +using var stringBuilder = new ValueStringBuilder(); |
| 85 | +stringBuilder.Append("Hello World"); |
| 86 | + |
| 87 | +fixed (char* buffer = stringBuilder) |
| 88 | +{ |
| 89 | + // buffer points at the first character; not guaranteed to be null-terminated |
| 90 | + // past stringBuilder.Length. |
| 91 | +} |
| 92 | +``` |
| 93 | + |
| 94 | +## Converting to and from `System.Text.StringBuilder` |
| 95 | + |
| 96 | +Sometimes you need the richer API of the "big brother" `StringBuilder`, or you're integrating with code that already hands you one. The [`ValueStringBuilderExtensions`](xref:LinkDotNet.StringBuilder.ValueStringBuilderExtensions) class covers both directions: |
| 97 | + |
| 98 | +```csharp |
| 99 | +using var stringBuilder = new ValueStringBuilder("Hello World"); |
| 100 | + |
| 101 | +System.Text.StringBuilder classic = stringBuilder.ToStringBuilder(); |
| 102 | +``` |
| 103 | + |
| 104 | +```csharp |
| 105 | +var classic = new System.Text.StringBuilder("Hello World"); |
| 106 | + |
| 107 | +using var stringBuilder = classic.ToValueStringBuilder(); |
| 108 | +``` |
| 109 | + |
| 110 | +Both conversions copy the underlying characters, so the two builders don't share a buffer afterward - mutating one has no effect on the other. |
| 111 | + |
| 112 | +## Iterating without allocating |
| 113 | + |
| 114 | +`ValueStringBuilder` implements `GetEnumerator()`, so it works directly in a `foreach` without ever materializing a `string`: |
| 115 | + |
| 116 | +```csharp |
| 117 | +using var stringBuilder = new ValueStringBuilder("Hello World"); |
| 118 | + |
| 119 | +foreach (var character in stringBuilder) |
| 120 | +{ |
| 121 | + // character is a char, no intermediate string or array is allocated |
| 122 | +} |
| 123 | +``` |
| 124 | + |
| 125 | +For anything beyond simple iteration, prefer [`AsSpan()`](xref:LinkDotNet.StringBuilder.ValueStringBuilder.AsSpan*) and the `Span<T>`/`ReadOnlySpan<T>` APIs directly. |
0 commit comments