C# Memory vs ReadOnlyMemory: Choosing the Right Type
Understand the difference between Memory<T> and ReadOnlyMemory<T> in C#, when to use each, and how they relate to Span<T> for efficient buffer handling.
When you start passing buffers around in C#, you'll quickly meet Memory<T> and ReadOnlyMemory<T>. These types represent a contiguous region of memory, but they differ in mutability and API surface. The core distinction is simple: Memory<T> allows read and write access, while ReadOnlyMemory<T> only allows read access. This article compares the two types and explains the practical impact of that difference on your code.
The Basic Difference Between Memory<T> and ReadOnlyMemory<T>
Memory<T> and ReadOnlyMemory<T> are both structs that describe a slice of memory. They can wrap arrays; ReadOnlyMemory<char> can also wrap a string. You can obtain Memory<T> from a MemoryPool<T>, then create a ReadOnlyMemory<T> view from it with an implicit conversion. The key difference is that Memory<T> provides a Span<T> property that you can write to, while ReadOnlyMemory<T> provides a ReadOnlySpan<T> property that only supports read operations.
byte[] buffer = new byte[1024]; Memory<byte> writable = buffer; ReadOnlyMemory<byte> readable = buffer; writable.Span[0] = 42; // Allowed // readable.Span[0] = 42; // Compilation error
This distinction is enforced at compile time. If a method only needs to read data, accepting a ReadOnlyMemory<T> prevents accidental mutation through that reference, making the contract explicit and eliminating an entire class of bugs. It does not guarantee that the underlying buffer is immutable; other code that holds a Memory<T> or an array reference can still change the data.
When to Pass ReadOnlyMemory<T> to Methods
Consider a method that processes a buffer without modifying it. If you declare the parameter as ReadOnlyMemory<T>, callers can pass either a ReadOnlyMemory<T> or a Memory<T> (implicit conversion exists). This gives flexibility while ensuring the method cannot alter the data.
int Sum(ReadOnlyMemory<int> numbers) { int total = 0; foreach (int n in numbers.Span) { total += n; } return total; } int[] data = { 1, 2, 3 }; int result = Sum(data); // Implicit conversion from int[] to ReadOnlyMemory<int>
Using ReadOnlyMemory<T> here is a strong statement: the method reads but never writes. This also allows the caller to pass a Memory<T> without an explicit cast, as the compiler inserts the conversion. For APIs that only need read access, ReadOnlyMemory<T> is almost always the right choice.
When to Use Memory<T> Instead
You need Memory<T> when the receiving code must modify the buffer. For example, a method that reads from a stream into a buffer, or a method that fills a slice with computed data.
void FillBytes(Memory<byte> target) { Random.Shared.NextBytes(target.Span); } byte[] buffer = new byte[256]; FillBytes(buffer); // Works because byte[] implicitly converts to Memory<byte>
Also, if you intend to return a mutable view of a buffer from a method, you must return Memory<T>. Returning ReadOnlyMemory<T> would hide the mutation capability, but if the caller expects to modify the data, the API is wrong. Choose the type that matches the operation's intent.
Relationship Between Memory<T> and Span<T>
Memory<T> and ReadOnlyMemory<T> are the heap-safe counterparts of Span<T> and ReadOnlySpan<T>. Span<T> is a ref struct that cannot be stored on the heap, so you cannot use it in async methods or as a field in a class. Memory<T> can be stored, used in async, and passed across awaits. When you need to work with the data synchronously, you access the Span property.
void ProcessData(ReadOnlyMemory<byte> data) { ReadOnlySpan<byte> span = data.Span; // Synchronous, read-only processing } async Task ProcessAsync(ReadOnlyMemory<byte> data) { // The Memory value can be used after an await if the buffer is still valid. await Task.Delay(10); ProcessData(data); }
The key point: use Memory<T> or ReadOnlyMemory<T> when you need to store the reference or use it in async code. Use Span<T> only for synchronous, stack-only operations.
Memory<T> and ReadOnlyMemory<T> in Async and Streaming Code
In asynchronous code, you frequently read from streams into a buffer. The Memory<T> type is essential here because the Stream.ReadAsync method accepts a Memory<byte> parameter. You cannot pass a Span<T> across an await, so Memory<T> fills that gap. For read-only buffers, ReadOnlyMemory<T> is more natural for the consumer side.
byte[] buffer = new byte[8192]; Memory<byte> memory = buffer; using (var stream = new MemoryStream(new byte[] { 1, 2, 3 })) { int bytesRead = await stream.ReadAsync(memory); // Process the read data as ReadOnlyMemory<byte> ProcessData(memory.Slice(0, bytesRead)); }
Note that ReadOnlyMemory<T> is not an argument to ReadAsync; you must have a mutable Memory<byte> to write into. After reading, you can pass a slice as ReadOnlyMemory<byte> to downstream methods.
Performance and Memory Allocation
Both Memory<T> and ReadOnlyMemory<T> are structs, so passing them does not allocate on the heap. They wrap an object reference and an index plus length. Accessing the Span property is cheap and does not allocate. Creating a Memory<T> from an array does not copy data. Because they are structs, they can be passed by value without causing garbage collector pressure.
When you use Memory<T> with a memory pool, you can avoid per-operation allocations. The MemoryPool<T>.Shared.Rent method returns an IMemoryOwner<T>; its Memory property gives you a buffer to write into. Disposing the owner returns the buffer to the pool. A ReadOnlyMemory<T> created from that Memory<T> is convenient for read-only consumers, but it does not replace the owner; you must still dispose the owner separately.
using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(256); Memory<byte> memory = owner.Memory; ReadOnlyMemory<byte> view = memory; // Use memory or view... // owner is disposed, returning the memory to the pool.
Here, memory is mutable because you write to it. After processing, you might want to expose the data as ReadOnlyMemory<byte> to consumers. The conversion from Memory<byte> to ReadOnlyMemory<byte> is implicit and allocation-free.
Common Pitfalls and How to Avoid Them
One common mistake is storing a Memory<T> in a field without also tracking the lifetime of the underlying buffer. If you rent from a pool, keep the IMemoryOwner<T> alongside the Memory<T> and dispose it at the right time; otherwise, you might use memory that has already been returned to the pool. In async code, don't keep a Span across an await; access the Span only when you are going to use it synchronously, and only while the underlying buffer is still valid.
Another pitfall is using ReadOnlyMemory<T> when you later need to modify the data. If you realize you need to mutate, you must change the API signature or keep a separate Memory<T> reference. This is why it's important to choose the correct type at the API design stage.
Final Considerations for Choosing the Right Type
When designing methods that accept buffers, start with ReadOnlyMemory<T> by default, and only switch to Memory<T> if the method must modify the data. This aligns with the principle of least privilege and makes your API harder to misuse. For synchronous, short-lived operations, consider using ReadOnlySpan<T> or Span<T> directly, because the compiler prevents them from being stored on the heap; note they cannot be used in async contexts or stored in fields.
One advanced pattern is to combine Memory<T> with custom pooling to reduce allocations. In high-throughput services, renting from MemoryPool<T> and returning the owner after processing can reduce garbage collection pressure. The ReadOnlyMemory<T> type is then used to hand off the read-only view to consumers without exposing the mutable owner. This approach works well when you control the entire pipeline and can guarantee that the owner is disposed correctly.
Ultimately, the choice between Memory<T> and ReadOnlyMemory<T> is about intent and mutability. By using ReadOnlyMemory<T> where possible, you make your code safer and more expressive. If you need to change data, Memory<T> is the answer. Understanding this distinction will help you write cleaner, more maintainable C# code, especially when dealing with buffers and performance-sensitive paths.