Header menu logo TDesu.FSharp

ValueStringBuilder Type

A stack-first string builder: writes into a caller-supplied Span{T} and only rents from the shared ArrayPool{T} once that buffer overflows. Call Dispose() once to return the rented array.

This is a ref struct, the same tradeoff that keeps the BCL's own System.Text.ValueStringBuilder internal. Three ways it bites:

  • Caught by the compiler: storing it in a field, capturing it in a closure, or using it across a let!/do!. There is therefore no closure-taking helper API — a parameter typed ValueStringBuilder byref -> 'R does not compile at all.
  • Not caught: passing it by value. A copy shares the same backing array, so if either side grows or disposes, the other is left pointing at memory the pool may have handed to somebody else. This is why no member returns this: fluent chaining would copy.
  • Not caught: a stack-allocated span outliving its frame. F# has no stackalloc expression, and a span built by hand from NativePtr.stackalloc is invisible to escape analysis — returning one compiles silently. Allocate in the function that uses it, never in a loop.

It has a plain Dispose() and does not implement IDisposable, so use does not apply — dispose from a try/finally. Dispose is idempotent.

When to use it: a hot path that builds one string per call and consumes it immediately. Measured against StringBuilder from 16 to 1,048,576 characters (ValueStringBuilderBenchmarks): never slower, and a third to a half of the allocations. When not to use it: anywhere the value must escape the building function — which is most code, since it cannot be returned, stored, or passed — or anywhere not hot enough to be worth the danger above. When in doubt use StringBuilder.

Example

The one supported shape — a local let mutable, plain statement calls, disposed in a finally:

 let describe (name: string) (count: int) =
     let buffer = Array.zeroCreate<char> 64
     let mutable sb = ValueStringBuilder(Span<char>(buffer))
     try
         sb.Append("name=")
         sb.Append(name)
         sb.ToString()
     finally
         sb.Dispose()
val describe: name: string -> count: int -> string
val name: string
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
val count: int
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
val buffer: char array
module Array from Microsoft.FSharp.Collections
val zeroCreate: count: int -> 'T array
Multiple items
val char: value: 'T -> char (requires member op_Explicit)

--------------------
type char = System.Char
val mutable sb: obj
System.Object.ToString() : string

Record fields

Record Field Description

_arrayToReturnToPool

Full Usage: _arrayToReturnToPool

Field type: char[]
Modifiers: mutable
Field type: char[]

_chars

Full Usage: _chars

Field type: Span<char>
Modifiers: mutable
Field type: Span<char>

_pos

Full Usage: _pos

Field type: int
Modifiers: mutable
Field type: int

Constructors

Constructor Description

ValueStringBuilder(initialCapacity)

Full Usage: ValueStringBuilder(initialCapacity)

Parameters:
    initialCapacity : int - The minimum initial capacity to rent.

Returns: ValueStringBuilder

Creates a builder backed entirely by a pooled array of at least initialCapacity characters, rented immediately from the shared ArrayPool{T}. Use this overload when there is no convenient stack buffer to hand in.

initialCapacity : int

The minimum initial capacity to rent.

Returns: ValueStringBuilder

ValueStringBuilder(initialBuffer)

Full Usage: ValueStringBuilder(initialBuffer)

Parameters:
    initialBuffer : Span<char> - The buffer to write into before growing into the pool.

Returns: ValueStringBuilder

Creates a builder that writes into initialBuffer first — for example a stackalloc'd span (see the type-level remarks for the exact, verified pattern and its dangers) — and only rents from the shared ArrayPool{T} once initialBuffer is full.

initialBuffer : Span<char>

The buffer to write into before growing into the pool.

Returns: ValueStringBuilder

Instance members

Instance member Description

this.Append

Full Usage: this.Append

Parameters:
    s : string - The string to append; null is ignored.

Appends s. A null string is a no-op, matching StringBuilder.

s : string

The string to append; null is ignored.

this.Append

Full Usage: this.Append

Parameters:

Appends every character of value, growing into the shared pool if needed.

value : ReadOnlySpan<char>

The characters to append.

this.Append

Full Usage: this.Append

Parameters:
    c : char - The character to append.

Appends a single character, growing into the shared pool if the buffer is full.

c : char

The character to append.

this.AppendLine

Full Usage: this.AppendLine

Appends the platform newline sequence ().

this.AsSpan

Full Usage: this.AsSpan

Returns: ReadOnlySpan<char>

The characters written so far, as a read-only view over the live buffer (no copy). Only valid until the next , , or call.

Returns: ReadOnlySpan<char>

this.Clear

Full Usage: this.Clear

Resets the write position to zero without releasing the current buffer, so the same instance — and, if it already grew, the same rented array — can be reused to build another string.

this.Dispose

Full Usage: this.Dispose

Returns the rented array (if any) to the shared pool. Safe to call more than once — every call after the first is a no-op. This type is not IDisposable (see the type-level remarks) — call this explicitly from a try/finally, never via use.

this.Length

Full Usage: this.Length

Returns: int

The number of characters written so far.

Returns: int

this.TryCopyTo

Full Usage: this.TryCopyTo

Parameters:
    destination : Span<char> - The buffer to copy into.
    charsWritten : byref<int> - Receives the number of characters copied, or 0 if the copy failed.

Returns: bool true if destination was large enough; otherwise false.

Attempts to copy the characters written so far into destination, without allocating a string.

destination : Span<char>

The buffer to copy into.

charsWritten : byref<int>

Receives the number of characters copied, or 0 if the copy failed.

Returns: bool

true if destination was large enough; otherwise false.

Type something to start searching.