ref struct / ref field
struct
we usually use a struct type as a local variable, so it typically is a stack allocated value. But it can be allocalted on heap.
struct is a value type, it will be allocated on somewhere it was declared:
- declared by a method local variable: [stack]
- declared inside another struct: [where the containing struct locates]
- declared inside an array: [heap]
- declared as a field of a class: [heap]
- be boxed to as a object or interface type: [heap]
struct could contain reference type value like string or other object, a reference to their heap addresss will be hold by struct.
ref struct
ref struct is a strictly restricted stack allocation type, it has some strict restrictions:
- cannot has reference type field like string or other object
- cannot be a field of a class or a regular struct
- cannot be an element of an array
- cannot be boxed
- cannot be used in async(await/yield) methods or lambda expressions
- cannot be returned by a method
All above restriction enfores the ref struct lifetime safety, to avoid dangling reference.
It allows a zero allocating manipulate memory on stack, heap, native buffers.
ref field
ref struct can has ref field, A ref field is a managed pointer that can point to a location on either the stack or the heap. This is the key to a ref struct‘s power.
behaves like a managed pointer but:
- type safety: the pointing object has to be with same type of the ref field decalreation
- this pointer cannot be manipulated arithmetically or created like unmanaged pointer
- lifetime of ref field <= the lifetime of object it points to
byref (managed by-reference) is a runtime type in IL that represents a reference to a specific memory location (variable, arrayelement, field) inside managed memory or stack. In C#,
- ref and out parameters
- ref locals
- ref returns
are implemented in IL as a byref. In IL, a byref is represented as a type with a trailing &.1
.method public void Example(int32& x){ ... } //int32& means a managed* reference to an int32
- what does the “managed” mean here: this reference will be updated when GC move the targeting object. this reference is safe.
Span<T>
Span<T> is a readonly ref struct that represents a contiguous block of memory on stack, heap or unmanaged memory, to mutate the data like string or any type of array. It’s a zero-allocation, safe, high-performance alternative to pointers.
Core fields:
1 | ref T _reference; // a IL byref to the first element |
they act like a safe, typed pointer + length.
src/libraries/System.Private.CoreLib/src/System/Span.cs
1 | // System/Span.cs |
Span<T> enables zero-copy slicing, stackalloc buffers, interop with native memory, and efficient APIs. Providing pointer-like power with compiler/runtime safety.
- defalut
the defalut value for a Span<T> is default(T) or default(7.1), which is all-zero bits for value type, null for reference types and NullRef
Types that Span<T> can point to
1 | // 1. managed arrays |
Constructors (Explicit Creation)
1 | public readonly ref struct Span<T> |
Implicit Conversions
1 | // From an array |
Get a normal array from a Span
Span<T>.ToArray() is to allocate a new managed array:
1 | Span<int> span = stackalloc int[3]{1,2,3}; //create a value type array on stack with stackalloc keyword. |
This operation is typically to compatible to some old API which only accepts T[], usually we don’t need to do this as Span
ArraySegement<T>
ArraySegment
- it’s a struct version of Span<T>, while Span<T> is a readonly ref struct
- it’s an old version of Span<T>
- it’s an heap storable version of Span<T>
1
2
3
4
5
6
7
8
9
10
11
12
13public readonly struct ArraySegment<T>
{
private readonly T[]? _array;
private readonly int _offset;
private readonly int _count;
public ArraySegment(T[] array);
public ArraySegment(T[] array, int offset, int count);
public T[]? Array { get; }
public int Offset { get; }
public int Count { get; }
}
ReadOnlySpan<T>
Even Span<T> is a readonly ref struct, this readonly only means its ref and length is immutable, but we still can manipulate the data the ref points to.
For a string object, it is immutable, so we cannot point it with a Span<T>, we can only reference it on stack with a ReadOnlySpan<T>
1 | // System/ReadOnlySpan.cs |
Strings & chars manipulation with Span<T>
reading operation for a string, with ReadOnlySpan<char>
1
2
3string s = "hello";
ReadOnlySpan<char> ro = s.AsSpan(); //"hello"
ReadOnlySpan<char> sub = s.AsSpan(1,3);//"ell"writing operation for a string, with Span<char>
- produce a string with Span
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//create empty Span<char>
Span<char> buf = stackalloc char[64];// writable buffer
int written = 0;
// the **TryFormat** method parses data into the stack buffer directly without creating a temporary string
// append an int literal as chars
if (!12345.TryFormat(buf[written..], out int w))
return;
written += w;
// append a string literal
" items - ".AsSpan().CopyTo(buf[written..]);
written += " items - ".Length;
// append a DateTime in ISO
DateTime.UtcNow.TryFormat(buf[written..], out w, "yyyy-MM-ddTHH:mm:ssZ");
written += w;
// final string (single alloc)
string result = new(buf[..written]); - transform a read-only data into a writable buffer
1
2
3
4
5//read a Span<char> from string
string s = "Hello, world!";
ReadOnlySpan<char> src = s.AsSpan();
Span<char> dst = stackalloc char[src.Length];
for (int i = 0; i < src.Length; i++) dst[i] = src[i]; - fast join of two spans
1
2
3
4
5
6
7
8
9ReadOnlySpan<char> a = "foo".AsSpan(), b = "bar".AsSpan();
Span<char> buf = stackalloc char[a.Length+1+b.Length];
int w = 0;
a.CopyTo(buf[w..]);
w += a.Length;
buf[w++] = '-';
b.CopyTo(buf[e..]);
w += b.Length;
string joined = new(buf[..w]);
- ReadOnlySpan<char> vs ReadOnlySpan<byte>
ReadOnlySpan<char> is to operate data originates as .NET string, while ReadOnlySpan<byte> is suit for data tranport.
- parsing utf-8 string in spans received via network or read from file
1
2
3
4
5
6
7ReadOnlySpan<byte> line = bufferSpan; // e.g., a slice of a file
int sep = line.IndexOf((byte)',');
var left = line[..sep];
var right = line[(sep + 1)..];
// decode only when need
string leftText = Encoding.UTF8.GetString(left); - formating date/time, numbers data into stack buffers directly withou temporary strings
1
2
3
4
5Span<char> tmp = stackalloc char[64];
if (DateTime.UtcNow.TryFormat(tmp, out int written, "yyyy-MM-dd"))
{
string s = new(tmp[..written]); // single alloc at the end
}
- string.Create()
string.Create is designed for zero-allocation performance-critical formatting, it is designed for hot-paths with known output length(fixed-date formats, IDs, hashes etc), to replace StringBuilder when know the size upfront.1
2
3
4
5public static string Create<TState>(
int length,
TState state,
SpanAction<char, TState> action
);example show power of string.Create(), producing “2025-09-01Z (id=12345)”1
2
3
4
5
6
7string message = string.Create(
20, // 1) Exact length of the string to create
DateTime.UtcNow, // 2) State object passed into the lambda
static (dst, dt) => // 3) Static lambda: writes into span
{
dt.TryFormat(dst, out _, "yyyy-MM-dd");
});
- traditional code
1
2
3
4
5
6
7
8
9string s1 = $"{DateTime.UtcNow:yyyy-MM-dd}Z (id={12345})"; //ofter temporary strings are created.
// or
var sb = new StringBuilder(); //but can resize, bring overhead.
sb.Append(DateTime.UtcNow.ToString("yyyy-MM-dd"))
.Append('Z')
.Append(" (id=")
.Append(12345)
.Append(')');
string s2 = sb.ToString(); - zeor-copy finalization with string.Createonly use when the target string length is known to take its advantage of high performance.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15string s3 = string.Create(
21,
(DateTime.UtcNow, 123456),
static(dst, st) =>
{
var (dt, id) = st;
dt.TryFormat(dst, out int w, "yyyy-MM-dd");
dst[w++] = 'Z';
" (id=".AsSpan().CopyTo(dst[w..]);
w += 5;
id.TryFormat(dst[w..], out int n);
w += n;
dst[w] = ')';
}
);
- int.TryParse
1 | public static bool TryParse(ReadOnlySpan<char> text, out int value) |
Operation and API
Indexing
1 | public ref T this[int index] |
1 | span[0] = 42; // modifies in-place |
Slicing
1 | public Span<T> Slice(int start, int length) |
1 | Span<int> s = new int[] {1,2,3,4,5}; |
Bulk Operations
Clear() set every element to default(0 for numbers, null for refs).
1
2Span<int> s = stackalloc int[3]{1,2,3};
s.Clear(); //[0,0,0]Fill(value) set every element to the same value
1
s.Fill(42)
CopyTo(destination) copy elements. Throws if destination too short.
TryCopyTo(destination) same to CopyTo(destination), but returns false instead of throwing.
1
2
3Span<int> src = stackalloc int[3]{1,2,3};
Span<int> dst = stackalloc int[3];
src.CopyTo(dst);
Pinning
Normally, the GC can move objects around in memory (to compact the heap), that means their addresses (pointers) aren’t stable. If we pass a pointer to native code (like a C API), the GC might move the object while the native code still uses the pointer -> crash.
Pinning mean telling the GC: Don’t move this object while someone is working with it.
- GetPinnableReference()
1
2
3
4
5
6
7
8
9
10Span<byte> buf = stackalloc byte[256];
unsafe
{
fixed (byte* p = buf) // fixed is a C# keyword that uses GetPinnableReference() under the hood.
{
// p is a stable pointer to buf[0]
// safe to pass to native API
}
// GC can move stuff again after the fixed block
}
Memory<T>
While Span<T> is writable view of memory, and ReadOnlySpan<T> is readonly view, they are fastest and zero-alloc, but they cannot be stored on the heap or used across async/await.
Memory<T> is writable can live on heap, and ReadOnlyMemory<T> is read-only and can live on the heap.
Memory<T> is slower than Span<T>, but we can store them in fields or keep them alive acorss await.
1 | class Holder |
When we need, Memory<T> can give a Span<T>
1 | Memory\<byte> mem = new byte[10]; |