C# / Advanced: Lifetimes, Concurrency, and Runtime Design / Lesson 14
Span, Memory, and Borrowed Lifetimes
Use contiguous-memory views without losing the lifetime, mutation, and ownership rules of their underlying storage.
On this page
Lesson goals
- Explain Span as a borrowed view rather than an owner.
- Respect ref-struct escape and suspension restrictions.
- Choose Memory and owner protocols for asynchronous boundaries.
A span describes storage it does not own
Span<T> is a ref struct representing a contiguous region. It can view arrays, stack storage, or suitable unmanaged memory, but the view does not acquire ownership or extend the underlying resource lease. Slicing creates another view, not a copied sequence. ReadOnlySpan<T> restricts writes through that view; another alias can still mutate mutable backing storage.
Every operation needs valid bounds and a live backing region. Array-based views benefit from managed lifetime tracking, while pooled or unmanaged memory also has an explicit lease or release protocol. A span’s short syntax must not hide the question of who may reuse the bytes and when.
Observe a view’s aliasing directly
The first program creates a two-element span over the middle of an array. Updating the span changes the array’s corresponding element, and no element copy was requested. The result is deterministic from the slice boundaries. Bounds checks prevent indexing beyond the span length in safe code, but they do not create a semantic ownership boundary.
When an API promises independent data, return a copy instead of a span view. When it promises a borrowed view, document how long the caller may use it and whether concurrent mutation is allowed. Returning ReadOnlySpan does not freeze an array retained and modified elsewhere.
using System;
int[] values = { 1, 2, 3, 4 };
Span<int> middle = values.AsSpan(1, 2);
middle[0] = 9;
Console.WriteLine(string.Join(",", values));
Console.WriteLine($"length={middle.Length}");
1,9,3,4
length=2
The span begins at array index one, so its first write replaces the array’s two with nine.
Stack storage cannot escape its valid lifetime
The second program uses a fixed small stackalloc initializer and consumes it synchronously. Stack allocation is appropriate only for justified small bounded sizes; using untrusted lengths or repeatedly allocating large stack regions can exhaust the stack. The values are explicitly initialized before summing. Do not assume every stackalloc form supplies useful initialized application data automatically.
The compiler’s ref-safety rules prevent many escapes, such as returning a span over a local stack allocation. Keep intentionally invalid escape snippets non-runnable. Unsafe code can bypass protections and requires a separate correctness argument; this course’s examples need no unsafe blocks or raw pointers.
using System;
ReadOnlySpan<int> values = stackalloc int[] { 1, 2, 3 };
int total = 0;
foreach (int value in values) total += value;
Console.WriteLine($"sum={total}");
sum=6
All three elements are initialized and consumed before the scope ends, producing six.
Suspension rules are language-version sensitive
Older advice that ref structs can never appear in async methods is too broad for modern C#. C#13 relaxed some restrictions, while still preventing a ref-struct local from being used across an await boundary under the applicable block and ref-safety rules. Iterator restrictions also changed. The C#14 baseline must follow current escape analysis, not copy an obsolete blanket statement.
For a buffer that must survive an asynchronous boundary, Memory<T> or ReadOnlyMemory<T> can carry a heap-storable view. They still do not automatically own the storage. The operation must finish before the owner returns a pooled buffer, and a consumer must not retain memory beyond the documented lease.
Pooling creates a lease, not permanent ownership
ArrayPool<T>.Rent can return a larger array than requested, so track the logical length separately. Return a rented array to the same pool once the owning operation and all borrowers are finished. Returning it early can let unrelated work overwrite data still in use. Clear sensitive contents according to the security policy before reuse when required.
MemoryPool<T> and IMemoryOwner<T> make an owner object explicit, but callers must still honor disposal and borrowing rules. Test empty slices, boundary slices, overlapping copy behavior under the chosen API, cancellation, and asynchronous completion before return. Measure allocation savings and retained memory together; a pool can retain memory and complicate ownership.
Common errors and fixes
A read-only span appears to change.
- Why it happens
- Another alias mutates the backing array.
- How to fix it
- Use immutable ownership or an independent copy when stability is required.
- Prevent it next time
- Document read-only access versus immutable storage.
A span escape is rejected by the compiler.
- Why it happens
- The backing lifetime cannot safely outlive its scope.
- How to fix it
- Keep consumption within the lifetime or use an appropriate owned representation.
- Prevent it next time
- Treat ref-safety diagnostics as contract evidence.
An async operation reads a reused pooled buffer.
- Why it happens
- The lease ended before completion.
- How to fix it
- Await all borrowers before returning the buffer.
- Prevent it next time
- Test cancellation and early-return ownership paths.
Check your understanding
1. Does ReadOnlyMemory<byte> guarantee that its bytes cannot change while an async consumer uses them?
Show worked solution
No. It restricts writes through that view, but the owner or another alias may modify or return the underlying buffer. The contract must guarantee stable ownership or exclusive borrowing for the operation’s duration, or provide a copy. Memory being heap-storable solves a representation restriction, not synchronization or lease validity.
2. An ArrayPool rent requests one hundred bytes and returns an array of one hundred twenty-eight. Which length should the protocol process?
Show worked solution
Process only the declared one hundred-byte logical region after initializing the required contents. Extra capacity is not part of the record and can contain unrelated previous data. Keep the rented array until all consumers finish, then clear according to policy and return it once to the same pool. Do not serialize the whole physical array by accident.
Key takeaways
- A view does not own, freeze, or extend the lease of its backing storage.
- Respect current ref-safety rules and label version-sensitive advice.
- Pooling and asynchronous borrowing require explicit completion before reuse.