Span, Memory, and Buffer Pooling
Process contiguous data with Span and Memory, understand stack-only lifetime restrictions, and rent buffers without leaking pooled contents or ownership.
Before this lesson
Explain Span and Memory lifetime differences
Slice data without allocating substrings or arrays
Use ArrayPool with safe ownership and cleanup
The short answer
Span<T> is a stack-only view over contiguous memory; Memory<T> is a storable, async-compatible representation. Use them after measurement shows slicing or buffer allocation matters, and make buffer ownership and returned lengths explicit.
Build the runtime mental model
A span does not own storage. It can view an array, stack allocation, string characters, or unmanaged memory, and slicing creates another view. Because a span may point at stack memory, the compiler prevents it from escaping to the heap or crossing an await boundary.
Advanced C# work improves when you separate language syntax, runtime behavior, and application policy. Write down which layer owns the guarantee in this lesson. Then identify the observable evidence—a compiler rejection, test result, generated query, trace, or measurement—that would prove the model correct.
Design the boundary deliberately
Memory<T> can be stored and passed through asynchronous APIs, while
Span<T> is obtained briefly for synchronous processing. APIs should return
how much of a buffer contains valid data; capacity is not content length.
ReadOnlySpan<T> communicates non-mutating intent.
The starter isolates one part of the mental model so it can run in the browser. The exercise moves the same rule into a current local .NET project where packages, framework hosting, diagnostics, and multi-file tests are available.
using System;
class Program
{
static int SumRange(int[] values, int start, int count)
{
int total = 0;
for (int i = start; i < start + count; i++) total += values[i];
return total;
}
static void Main()
{
int[] values = { 4, 8, 15, 16, 23, 42 };
Console.WriteLine(SumRange(values, 1, 3));
}
}Expected output
39
Diagnose failure and misuse
A rented ArrayPool buffer may be larger than requested and can contain previous data. Process only the valid range, clear sensitive elements when returning, and return exactly once in a finally block. Pooling tiny or infrequent buffers often adds complexity without benefit.
Classify each failure as a contract violation, transient operational failure, permanent dependency response, concurrency conflict, or programmer defect. That classification determines whether to reject, retry, compensate, cancel, or fail fast. A generic catch-and-continue policy destroys the information needed to make that decision.
| Question | Evidence to inspect | Decision |
|---|---|---|
| Is the input valid? | Validation result and boundary examples | Reject with a stable contract |
| Is the failure transient? | Typed status, exception, and policy context | Retry only when bounded and safe |
| Is state still consistent? | Invariant and transaction outcome | Commit, compensate, or abort |
| Is performance acceptable? | Representative latency and allocation data | Keep simple or optimize one cause |
Apply the concept in production
Benchmark the original allocating version against the span-based version with realistic payload sizes. Document ownership at every boundary: who supplied storage, which slice is valid, and whether the callee may retain it.
Finish by making the result operable. Add structured diagnostics at the boundary, propagate cancellation, avoid sensitive data, and record SDK and dependency versions. Test the public behavior instead of private implementation details. If a framework or provider performs translation, serialization, concurrency, or I/O, include at least one test against the real production technology.
A senior-level review should be able to answer four questions: what contract is promised, who owns lifetime and cleanup, how failures become visible, and what evidence supports the design. If any answer depends on “the framework probably handles it,” inspect the documentation or runtime behavior and turn the assumption into a checked decision.
Quick knowledge check
Answer before you reveal.
01Why can Span<T> not cross an await?
It may refer to stack storage, so the compiler prevents it from surviving beyond the stack frame that guarantees its lifetime.
02What must happen before adding complexity to this design?
State the requirement, preserve a correct baseline, collect evidence, and explain how the proposed mechanism improves a specific quality.
Exercise
Practice challenge
Implement a delimiter parser with ReadOnlySpan<char>, compare it with Split, and document buffer validity and ownership.
Requirements
- The implementation states its contract and ownership boundary explicitly
- Automated checks cover the successful path and at least two meaningful failures
- Diagnostics expose failure context without secrets or swallowed exceptions
- The project documents required SDK, packages, setup, run, and test commands
Optional extension: Measure or load-test the critical path and record whether the evidence justifies another optimization or abstraction.
Open in C# compilerLesson checkpoint
One small step locks it in
Mark this lesson complete, then keep the momentum going.
Clear up the details
Frequently asked questions
When should I use span, memory, and buffer pooling?
Use it when its explicit tradeoff solves a measured requirement or clarifies an owned boundary. Keep the simpler design when the additional mechanism does not improve correctness, operability, or changeability.
Does the browser compiler cover the complete production setup?
No. It runs the focused starter program. Framework, package, database, benchmark, and multi-project work requires a current local .NET SDK and the project commands described in the exercise.
What evidence should I keep after the exercise?
Keep the acceptance cases, automated tests, diagnostic or benchmark output where relevant, and a short decision note describing the chosen boundary and rejected alternative.