Reputation: 4244
I am looking for some immutable collections in C#, specifically, a List/array type one, and a set/hashset type one. I'm clarifying if there are 'built in' .NET types that do what I want (I realize rolling my own wouldn't be hard)
By 'immutable' I mean you should not be able to modify the collection instance after instantiation, (no adding, removing, reordering items etc - not that I care about order)
Now, .NET has the ImmutableXXX collections, but these have the major drawback for me, of still including the mutation functions of their mutable equivalents. EG, ImmutableList
has an Add
function. What's worse is these functions "work" but do something completely different to what they do on the mutable versions; instead of modifying the current instance, they return a new collection instance which is a copy of the current one with the operation applied.
The problem with this, is it is setting your team up for not immediately obvious bugs. If you have a class that contains properties that are immutable collections, eg:
class Class
{
public ImmutableList<string> Students { get; init; }
}
Then someone using an instance of this class might not immediately realize that the collection, or the class, is immutable. And so might think they can go:
Programming101.Studends.Add("fred");
which they can but will not do what they are expecting it to do.
This is especially bad if you are refactoring a class to become immutable - a not too uncommon occurrence I would think.
What's more is the immutable collections seem a little scattered around .Net. For instance ImmutableXXX doesnt have a set or hashSet variant. There is a FrozenSet
, and at least that throws an exception if you try and call the Add method (implemented only to satisfy the ICollection
implementation) but the FrozenXXX collections only includes Set and Dictionary..
Another option would of course be to use the IReadonlyXXX interfaces, like IReadonlyList
.
But the problem with this is, if in my above example, I used it instead of ImmutableList
, then someone could initialize Students with a List<string>
, and then separately modify that list later on (that they maintained a reference to outside of the Class object), breaking Class' immutability.
The only other option would be to ensure that Students is always constructed within Class, but that means if you wanted to initialize an instance of Class with an existing collection of students, that collection would have to be copied. One of the benefits of Immutable objects (like the ImmutableXXX collections) is you can use them to initialize other immutable objects, and the new object can just reference the existing Immutable collection, knowing its impossible for it to change.
So after all that, in short: I'm looking for something functionally equivalent to the ImmutableXXX collections, but that don't have the mutation functions, or at very least throw exceptions if you try and call them. As such, FrozenSet
would fulfill my need for a set, but there doesn't seem to be any List/Array equivalent?
Upvotes: 3
Views: 189
Reputation: 4244
I phrased my question in the StackOverflow friendly way of 'should not be looking for opinion', but as its looking like the proper answer to my question might just be "No", I do appreciate the ideas that have been mentioned here, so I thought I'd put an idea im toying with up, thanks to TheodorZoulias advice to focus on my main concerns.
This idea requires an Immutable collection to initialize the class, but then returns it as an IReadOnly, alleviating my main concerns about both interfaces: I can be sure the data passed in will never be modified, and that users of the class won't be tripped up by mutation looking methods (eg Add)
public record class Class
{
private readonly ImmutableList<string> _students = ImmutableList<string>.Empty;
public IReadOnlyList<string> Students => _students;
// Bit weird.. mainly to allow 'with', which is a nice feature of records
public ImmutableList<string> StudentsInit { init => _students = value ?? ImmutableList<string>.Empty; }
public Class(){}
public Class(ImmutableList<string> students)
{
_students = students ?? ImmutableList<string>.Empty; // probably better to just throw if value is null..
}
}
As you can see there is some funkyness here that could be improved by just rolling my own immutable collections sans the mutation looking functions, but in lieu of that..
Upvotes: 0
Reputation: 82
After some research i came up with idea using ReadOnlySpan first. It provide immutability of elements. You can't modify items by index, only way is create a new instance of span. Here's example:
ReadOnlySpan<int> span = new ReadOnlySpan<int>(new[] { 1, 2, 3 });
//Error this is readonly
span[0] = 1;
But if you don't want struct type collection or something else you can create a new class to provide your collection features. I've created a one for example:
//Collection provide immutability of whole elements. IReadOnlyCollection provides IEnumerable and generic version interfaces
public class ImmutableCollection<T> : IReadOnlyCollection<T>
{
//Origin
private T[] _values;
//Constructors
public ImmutableCollection() { }
public ImmutableCollection(IEnumerable<T> values)
{
_values = values.ToArray();
}
//Pick item by index
public T this[int index]
{
get
{
return _values[index];
}
}
//Current count of collection
public int Count
{
get
{
return _values.Length;
}
}
//Get enumerators for foreach cycle
public IEnumerator<T> GetEnumerator()
{
return (IEnumerator<T>)_values.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
That's immutable too and you can't upcast to mutables. It works generally same as previous.
//Here's no Add and Remove methods. It can be only enumerated or replaced by new instance to it
ImmutableCollection<int>collection = new ImmutableCollection<int>(new int[] {1, 2, 3});
//That's doesn't work too
collection[0] = 1;
Edit: I can't comment due my reputation. The answer above is incorrect. It's mutable by upcast to IList
collection. Here's code:
//Initialize a data for collection
int[] array = new int[10];
//Upcast ReadOnlyCollection to IList. You can check via decompilation code via F12
IList<int> list = new ReadOnlyCollection<int>(array);
//That's works lol
list.Add(1);
Upvotes: 2
Reputation: 18320
I'm surprised no one has mentioned ReadOnlyCollection
. While technically not "immutable" in the same sense as ImmutableList
(ReadOnlyCollection
is not thread-safe and the underlying collection can still be modified if you have a reference to it), it functions more or less the same as an immutable list for the purposes you describe: No methods to "add" to the collection by creating a copy of it. You just have to make sure you throw away all your references to the original collection so that you can't modify it directly.
var originalNames = new List<string>();
originalNames.Add("Bob");
originalNames.Add("Alice");
originalNames.Add("James");
var names = new ReadOnlyCollection(originalNames);
// Make sure 'originalNames' can no longer be referenced after this point, nor any other variables pointing to it
// Doesn't work, compiler shows an error that you can't set elements in the collection
names[0] = "Mallory";
Additionally, there is also a ReadOnlyDictionary
. And ReadOnlySet
has been introduced with .NET 9.
Upvotes: 1