In this post, we delve into C# memory structures, focusing on ReadOnlyMemory<T> and byte[] types. We aim to understand how to convert ReadOnlyMemory to a byte array, specifically using the MemoryMarshal.AsBytes() method. We also explore scenarios where this conversion is appropriate.
So, let’s dive in!
Understanding ReadOnlyMemory and Byte Arrays
The ReadOnlyMemory struct in C# offers a read-only view of a contiguous memory region similar to an array. This structure shines in performance-critical scenarios, where we want to work with existing data without creating copies on the heap. It achieves this efficiency by referencing existing memory instead of allocating new space. Additionally, ReadOnlyMemory<T>
promotes data safety by preventing us from accidentally modifying our data while we work with a portion of an array, string, or any other memory segment.
Conversely, a byte array is a mutable data structure. It represents a fixed-size memory buffer for storing binary data in operations such as file I/O and network communications.
Now, let’s examine how we can derive a byte[]
from a ReadOnlyMemory<T>
.
Converting ReadOnlyMemory to a Byte Array
While ReadOnlyMemory<T>
provides a view of memory, it doesn’t directly expose the underlying bytes. So in this section, we explore how to get them for specific scenarios.
Enter MemoryMarshal
, which is a utility class in the System.Runtime.InteropServices
namespace, which provides functionalities for interacting with memory-related types like ReadOnlyMemory<T>
and Span<T>
. See MemoryMarshal for more information.
Let’s see how we can use the MemoryMarshal.AsBytes()
method, to convert a ReadOnlyMemory to a byte array:
public static byte[] ReadOnlyMemoryToByteArray<T>(ReadOnlyMemory<T> input) where T : struct { return MemoryMarshal.AsBytes(input.Span).ToArray(); }
Here we define a generic method, ReadOnlyMemoryToByteArray<T>()
, which accepts a ReadOnlyMemory<T>
and converts it to an array of bytes. The first thing to notice is our generic type constraint of struct
. MemoryMarshal.AsBytes()
is only valid for primitive types.
With our constraints set properly, we can move on to the meat of the process, which is to use the Span
property on the ReadOnlyMemory<T>
struct to get a ReadOnlySpan<T>
that we then pass on to MemoryMarshal.AsBytes()
. This method reinterprets our input as bytes, returning a ReadOnlySpan<byte>
over the same memory region. Our final step is to call ToArray()
on the span, which copies the span into a newly allocated byte[]
which we return.
Now, let’s test our method out:
var byteArray = ReadOnlyMemoryToByteArray<int>(new int[] { 1, 2, 3 }); Console.WriteLine(BitConverter.ToString(byteArray)); byteArray = ReadOnlyMemoryToByteArray("foo".AsMemory()); Console.WriteLine(BitConverter.ToString(byteArray));
Here we create a ReadOnlyMemory<int>
and ReadOnlyMemory<char>
, which we then pass on to our ReadOnlyMemoryToByteArray()
method. We then use the BitConverter.ToString()
to display the bytes as a string on the console:
01-00-00-00-02-00-00-00-03-00-00-00 66-00-6F-00-6F-00
So as we see in these code examples, the MemoryMarshal.AsBytes()
method simplifies the process of converting ReadOnlyMemory to a byte array. Next, let’s discuss some use cases for conversation.
Use Cases for Conversion
Some common scenarios where converting a ReadOnlyMemory<T>
to a byte[]
proves useful are:
- Interoperability with APIs expecting byte arrays
- Writing data to I/O streams
- Cryptography operations
When interoperating with older APIs or libraries, some may not support ReadOnlyMemory<T>
. Instead, they require data as a byte[]
. Although, recent versions of .NET have added method overloads that support ReadOnlyMemory<T>
and ReadOnlySpan<T>
, here, we focus on APIs that fit the criteria for this topic. In a real-world scenario, it is best to use the new overloads if possible.
Consider FileStream.Write()
before .NET Core 3.0, and SHA256.ComputeHash()
methods from the System.IO
and System.Security.Cryptography
namespaces, respectively.
Let’s explore an example of working with these two APIs:
byte[] SaveText(string path, ReadOnlyMemory<char> text) { using var stream = new FileStream(path, FileMode.Create, FileAccess.Write); byte[] byteArray = MemoryMarshal.AsBytes(text.Span).ToArray(); stream.Write(byteArray, 0, byteArray.Length); using var hashAlgorithm = SHA256.Create(); return hashAlgorithm.ComputeHash(byteArray); }
Here, our method converts some text to an array and saves it to disk, then returns the hash for it. We pass the text in as a ReadOnlyMemory<char>
. Next, we convert it to a byte[]
through the same process as our previous examples and write it to disk using the FileStream.Write()
method. Finally, we hash it with the SHA256.ComputeHash()
method and return the result.
Here is the computed hash result, converted a string using the BitConverter.ToString()
method:
3F-AE-AF-C2-8D-3C-31-E6-D7-BF-62-8A-53-43-8E-7D-24-A8-68-16-F1-5D-10-F6-BA-54-E1-8F-8F-EC-26-C2
Now, let’s go over some performance considerations for when to use ReadOnlyMemory<T>
versus a byte[]
.
Performance Considerations
It’s crucial to note that MemoryMarshal.AsBytes()
does not reallocate or copy the underlying memory of a ReadOnlyMemory<T>
. It merely provides a new view of the same, making it fast and efficient.
As a general guideline, we use a ReadOnlyMemory<T>
with data that will not be modified. Accessing the data occurs in place, without copying, which improves access speed. On the other hand, we use a byte[]
when we need a copy of the data, such as when saving it to disk or sending it over the network. In other words, we use a byte[]
at the end of the workflow when we are sure that we need to modify the data.
However, Memory<T>
and Span<T>
comes with many benefits over a byte[]
. So, if the API we’re using has an overload that supports any of them, we should consider using it instead. Memory<T>
and Span<T>
are structs that provide a mutable memory-efficient way to access an existing array. See Memory and Span for more information.
Conclusion
In this article, we explored how to convert a ReadOnlyMemory to a byte in C#. We used the MemoryMarshal.AsBytes() method to first reinterpret our ReadOnlyMemory<T> as a ReadOnlySpan<byte>. We then used ToArray() to make a copy of the underlying byte array. Finally, we examined performance considerations and scenarios where this type of conversion is useful.