Learning outcome
You should be able to explain, in interview terms, why ReadOnlySpan<T> is a non-owning, stack-only view and why ReadOnlyMemory<T> can cross an await boundary while still not owning the underlying buffer. You should also be able to describe buffer ownership separately from buffer access: a method may consume a buffer without owning it, and an async method may hold a ReadOnlyMemory<T> reference while another component remains responsible for keeping the rented array alive.
Intuition
Think of a span as a lightweight view of a region, not a copy or a transfer of ownership. A span referring to managed memory participates in GC tracking, but it does not enforce a pool lease or prevent another owner from returning a rented buffer. A ReadOnlySpan<T> is a ref struct, which is why it behaves like a stack-only view. That design makes it ideal for short-lived parsing, slicing, and inspection inside a single synchronous flow, and compiler ref-safety rules prevent references to shorter-lived storage from escaping. A span over valid heap-backed storage can be returned from a method; a span over that method's stack-allocated storage cannot.
ReadOnlyMemory<T> solves a different problem. It is also a view over a contiguous region, but it is not a byref-like type, so it can live on the managed heap and be stored in fields, returned from methods, or carried across asynchronous work. That does not mean ownership changes. A ReadOnlyMemory<T> instance can still point at an array rented from a pool, and the owner of that rented array is still the component that must dispose or return it.
A useful mental model is this timeline:
Producer rents buffer -> fills data -> hands out ReadOnlyMemory<T> -> async consumer awaits -> consumer reads later -> producer/owner returns buffer only after all consumers finish
The key distinction is that holding a memory view is not the same as owning the buffer. If a rented array is returned to the pool too early, later consumers may observe corrupted or unrelated data.
Deep dive
ReadOnlySpan<T> is optimized for synchronous, short-lived use. Because it is a ref struct, it cannot be boxed, captured by a lambda, stored in a field of a normal class, or preserved across an await in the way a heap object can. Even though newer C# versions have relaxed some local-variable restrictions in async methods, the important interview answer is unchanged: a span must not be preserved across an await boundary.
ReadOnlyMemory<T> is the async-friendly sibling. You can use it when a method needs to keep a view around while it awaits I/O or other asynchronous work. However, the view still references the same underlying storage, so the storage must remain valid for the entire lease of every consumer.
This leads to the ownership rule that interviewers care about:
- Owner: responsible for lifetime management of the actual buffer.
- Consumer: reads or processes the buffer.
- Lease: the time window during which the consumer is allowed to use it.
In practice, if you rent an array from a pool and pass a ReadOnlyMemory<T> to an async consumer, you must keep the array rented until that consumer is done. ReadOnlyMemory<T> does not transfer ownership, and it does not make the array immutable. It only makes the view read-only.
That distinction matters because the backing storage might still be mutable through another reference. ReadOnlyMemory<T> prevents mutation through that specific API surface, but it does not magically freeze the underlying buffer.
Failure modes
Watch for these interview traps:
- Treating
ReadOnlyMemory<T>as if it makes the backing data immutable. It does not. - Returning a rented array to a pool before all async consumers finish using the memory view.
- Trying to use
ReadOnlySpan<T>as if it were a durable async payload. - Confusing “read-only view” with “ownership transfer.”
- Assuming a span is always invalid in async methods; the more precise rule is that it cannot be preserved across an
awaitboundary. - Forgetting that a consumer may be different from the owner.
A good failure checklist to say out loud in an interview is:
- What owns the buffer?
- Who is allowed to consume it right now?
- Does any
awaitoutlive the current span lease? - If the buffer was rented, when is it returned?
- Could another reference still mutate the underlying storage?
Interview drill
If asked to choose between the two types, answer like this:
- Use
ReadOnlySpan<T>for fast, synchronous, stack-bound inspection. - Use
ReadOnlyMemory<T>when a view must be stored in ordinary object fields or preserved acrossawait, while keeping the underlying buffer valid for the consumer's entire lease. - If the data comes from a pool, keep ownership with the renter until every consumer has finished.
A strong explanation combines type shape and lifetime semantics: spans are byref-like stack views; memory is heap-storable view data; neither type changes who owns the buffer.
Revision checklist
ReadOnlySpan<T>is a non-owning, stack-only view.ReadOnlyMemory<T>can be held acrossawait.ReadOnlyMemory<T>does not transfer ownership.ReadOnlyMemory<T>does not guarantee the backing array is immutable.- A rented buffer must remain rented until all async consumers are done.
- If the lesson is about async boundaries, always mention lease and ownership separately from read-only access.
Fact-check notes
Version-sensitive points to verify against official docs: ReadOnlySpan<T> is a ref struct and cannot escape to the managed heap; ReadOnlyMemory<T> is not a byref-like type and can be stored on the heap; span-like usage guidelines distinguish ownership, consumption, and lease. Relevant official pages: https://learn.microsoft.com/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelines, https://learn.microsoft.com/en-us/dotnet/api/system.readonlyspan-1?view=net-10.0, https://learn.microsoft.com/en-us/dotnet/api/system.readonlymemory-1?view=net-10.0.