ValueArray: an immutable array wrapper with value equality semantics
$ dotnet add package JBlam.Collections.ValueArrayAn immutable array wrapper with value equality semantics.
using JBlam.Collections.ValueArray;
// Initialise with a collection expression.
ValueArray<int> arr = [1, 2, 3];
// Iterate like any other IEnumerable.
foreach (var value in arr)
Console.WriteLine(value);
// Collect IEnumerables into a ValueArray instance.
var second = Enumerable.Range(1, 3).ToValueArray();
// Value equality!
Console.WriteLine($"ValueArrays are equal? {arr == second}");
// Default value does not throw!
var empty = default(ValueArray<int>);
Console.WriteLine($"Am I safe from NullReferenceExceptions in value types now? {empty.Length == 0}");
// Implicit conversion to/from ImmutableArray<T> means you can use ImmutableArray.Builder.
var builder = System.Collections.Immutable.ImmutableArray.CreateBuilder<int>();
builder.Add(1);
builder.Add(2);
builder.Add(3);
System.Collections.Immutable.ImmutableArray<int> immutableArray = builder.ToImmutable();
// implicit conversion happens here ↓
ValueArray<int> third = immutableArray;
Console.WriteLine($"Converted from ImmutableArray? {arr == third}");
This is a drop in replacement for System.Collections.Immutable.ImmutableArray<T> which aims to resolve three issues:
default value is logically empty, rather than exploding with null reference exceptions.ToString for record types.ValueArray targets net8.0 which at time of writing was the LTS release. I've also multitargeted netstandard2.0 because I want to use it in Roslyn analysers and source-generators; and also net6.0 because I have some work projects which use that.
The System.Text.Json integration is not available for netstandard2.0 because it's outside my use-case. Minor bits of API are also excluded, but mostly I'm providing some shims.
The struct layout of ValueArray is identical to ImmutableArray. The type design is similar, except it special-cases the default value, provides value equality, and overrides ToString to print its contents.
Converting to and from ImmutableArray<T> is effectively free. (Roughly the cost of copying a single reference.)
I will release this with minimal API around it, on the understanding that if you want "immutable mutations" you can easily convert to the equivalent ImmutableArray, do your business, and convert back.
ImmutableArray<T> is a value type which wraps a hidden reference to a reference type, and as such default contains a null reference. This is a massive footgun, because the only safe way to interact with an ImmutableArray is to inspect it for IsDefault; that's incredibly unexpected because C# programmers don't normally inspect value types for null. It also hides the null reference from the nullable type annotation system, which otherwise makes NullReferenceException more-or-less a solved problem.
ValueArray<T> solves this issue by making default logically equal to an empty array.
But wait! if default is equal to [], doesn't that mean equality is broken? Well, have I got a story to tell you ...
ImmutableArray<T> is a value type which wraps a hidden reference to a reference type and doesn't manage equality meaning that ImmutableArray<T>.Equals is effectively reference-equality on the hidden reference. This is again highly surprising for the C# developer who assumes that value types support value equality, because they are values.
ValueArray<T> solves this issue by making Equals return sequence equality. Yes, that's O(n). I suggest not making ValueArray<T> wrap multiple gigabytes of data.