C# Memory vs ReadOnlyMemory: Choosing the Right Type
c# memory vs readonlymemory: Understand the difference between Memory<T> and ReadOnlyMemory<T> in C#, when to use each, and how they relate to Span<T> for efficient da...
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 examines c# memory vs readonlymemory 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, strings (only ReadOnlyMemory<char> for strings), or memory obtained from a MemoryPool<T>. 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, making the contract explicit and eliminating entire classes of bugs.
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.
async Task ProcessAsync(ReadOnlyMemory<byte> data) { // Can't use data.Span across an await, so copy to a local var localSpan = data.Span; // Synchronous processing of localSpan is fine. // To use data after an await, work with the Memory itself. await Task.Delay(10); // data is still valid here; you can access data.Span again. }
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(/* some data */)) { int bytesRead = await stream.ReadAsync(memory); // Process the read data as ReadOnlyMemory<byte> ProcessData(memory.Slice(0, bytesRead)); } void ProcessData(ReadOnlyMemory<byte> data) { // Read-only processing }
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. However, creating a Memory<T> from an array is a simple operation that does not copy data. Because they are structs, they can be passed by value without causing pressure on the garbage collector.
When you use Memory<T> with a memory pool, you can avoid allocations altogether. The MemoryPool<T>.Shared provides rented Memory<T> instances that are reused. Since Memory<T> is mutable, you can write into it and then return it to the pool. ReadOnlyMemory<T> cannot be returned to the pool because the pool expects to give out mutable memory; you can only return a Memory<T>.
using IMemoryOwner<byte> owner = MemoryPool<byte>.Shared.Rent(256); Memory<byte> memory = owner.Memory; // Use memory... // 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 of a class without considering the lifetime of the underlying buffer. If you rent from a pool and store it in a field, you must manage the disposal carefully; otherwise, you might use memory that has been returned to the pool. Also, when you use Memory<T> in an async method, be careful not to use the Span property after an await, as the underlying memory might change; instead, work with the Memory<T> itself.
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, as they offer more compile-time safety against accidental capture, but 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 significantly 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.