Reputation: 3473
I'm trying to get the idea, what would be the best way to publish a Readonly List of objects as a public method?
From Eric Lippert's Blog, Arrays are kinda bad, because someone could easily add a new Entry. So one would have to pass a new Array every time the method is called.
He suggests, to pass IEnumerable<T>
, since this is per definition read only (no add, remove methods), which I practiced for quite sometime.
But in our new project, people even started to create Arrays of these IEnumerables
, because they don't know the DataSource behind, so they get a : Handling warning for possible multiple enumeration of IEnumerable
I'm interested in a technical approach, how one would solve this puzzle. The only solution I came up so far would be to use a IReadOnlyCollection
, but this would be way more explicit than an IEnumerable
.
What is best practice to publish such lists, which shouldn't be changed, but should be declared as In-Memory Lists?
Upvotes: 26
Views: 6673
Reputation: 25351
I've been programming for longer than I care to remember, and never had such a requirement to protect a list from getting modified. I'm not saying it's not a possible requirement, I'm just saying it is very rare to need such a requirement. If you have a list circulating around in your app, then most likely you have to fix your design. If you need help with that, let us how you're using the list.
The examples you're giving in your question and comments are not good examples for when to require an immutable or read-only list. Let's discuss them one by one.
You mentioned publishing it as an API. By definition, anything you return from an API is no yours anymore and you shouldn't care how it is used. In fact, once it leaves your API, it is now in the API client's domain, and they can do whatever they want with it. Aside from the fact that you cannot protect it once it is in their domain, you should not dictate how they will use it. More importantly, you should never accept anything as input in your API, even if it is the same list that you returned earlier and you think that you protected. All input MUST be validated appropriately.
Perhaps, you did not really mean API, but more like a DLL library that you share in your projects. Whether it is a DLL or just a class in your project, the same principle applies. When you return something, it is up to the user how to use it. And you should never accept the same thing (list or whatever) back without validation. Similarly, you should never expose an internal member of your class, whether it's a list or a single value. After all, that's the whole idea behind using properties instead of marking the members as public. When you create a property for a single-value member, a copy of the value is returned. Similarly, you should create a property for your list and return a copy, not the list itself.
If you really need a list that is globally accessible, and you want to load it only once, expose it to other classes, protect it against modification, and it is too big to make a copy of it. You can look into some designs like wrapping it in a Singleton and/or make it read-only as per Antonín Lejsek's answer. In fact, his answer can be easily converted to a Singleton by marking the constructor as private, thanks to the simplified Singleton implementation in C#.
Upvotes: 0
Reputation: 64943
Usually - and since a while - this solved using immutable collections.
Your public properties should be, for example, of type IImmutableList<T>
, IImmutableHashSet<T>
and so on.
Any IEnumerable<T>
can be converted to an immutable collection:
someEnumerable.ToImmutableList();
someEnumerable.ToImmutableHashSet();
This way you can work with private properties using mutable collections and provide a public surface of immutable collections only.
For example:
public class A
{
private List<string> StringListInternal { get; set; } = new List<string>();
public IImmutableList<string> StringList => StringListInternal.ToImmutableList();
}
There's also an alternate approach using interfaces:
public interface IReadOnlyA
{
IImmutableList<string> StringList { get; }
}
public class A : IReadOnlyA
{
public List<string> StringList { get; set; } = new List<string>();
IImmutableList<string> IReadOnlyA.StringList => StringList.ToImmutableList();
}
Check that IReadOnlyA
has been explicitly-implemented, thus both mutable and immutable StringList
properties can co-exist as part of the same class.
When you want to expose an immutable A
, then you return your A
objects upcasted to IReadOnlyA
and upper layers won't be able to mutate the whole StringList
in the sample above:
public IReadOnlyA DoStuff()
{
return new A();
}
IReadOnlyA a = DoStuff();
// OK! IReadOnly.StringList is IImmutableList<string>
IImmutableList<string> stringList = a.StringList;
It should be a possible solution to avoid converting the source list into immutable list each time immutable one is accessed.
If type of items overrides Object.Equals
and GetHashCode
, and optionally implements IEquatable<T>
, then both public immutable list property access may look as follows:
public class A : IReadOnlyA
{
private IImmutableList<string> _immutableStringList;
public List<string> StringList { get; set; } = new List<string>();
IImmutableList<string> IReadOnlyA.StringList
{
get
{
// An intersection will verify that the entire immutable list
// contains the exact same elements and count of mutable list
if(_immutableStringList.Intersect(StringList).Count == StringList.Count)
return _immutableStringList;
else
{
// the intersection demonstrated that mutable and
// immutable list have different counts, thus, a new
// immutable list must be created again
_immutableStringList = StringList.ToImmutableList();
return _immutableStringList;
}
}
}
}
Upvotes: 15
Reputation: 6103
I do not think immutable is the way to go
int[] source = new int[10000000];//uses 40MB of memory
var imm1 = source.ToImmutableArray();//uses another 40MB
var imm2 = source.ToImmutableArray();//uses another 40MB
List behaves the same way. If I want to make full copy every time, I do not have to care about what user does with that array. Making it immutable does not protect content of objects in the collection either, they can be changed freely. @HansPassant suggestion seems to be best
public class A
{
protected List<int> list = new List<int>(Enumerable.Range(1, 10000000));
public IReadOnlyList<int> GetList
{
get { return list; }
}
}
Upvotes: 6
Reputation: 18954
IEnumerable
is a read-only interface which hides implementation from user. Some can argue, that user may cast IEnumerable
to list and add new items, but that means two things:
IEnumerable
describes behavior, while List
is an implementation of that behavior. When you use IEnumerable
, you give the compiler a chance to defer work until later, possibly optimizing along the way. If you use ToList()
you force the compiler to prepare the results right away.
I use IEnumerable
Whenever working with LINQ expressions, because by only specifying the behavior, I give LINQ a chance to defer evaluation and possibly optimize the program.
To prevent any modifications to the List<T>
object, expose it only through the ReadOnlyCollection<T>
wrapper which does not expose methods that modify the collection. However, if changes are made to the underlying List<T>
object, the read-only collection reflects those changes as described in MSDN.
Take a look at The New Read-Only Collections in .NET 4.5 please.
Upvotes: 5
Reputation: 152566
For a collection that you don't intend to modify, IEnumerable<T>
is still probably the safest option, plus it allows any collection type to be pased in, not just arrays. The reason for that warning is because of the possibility that the IEnumerable
represents a query that uses deferred execution, meaning that a potentially expensive operation could be executed multiple times.
Note that there's not an interface that distinguish in-memory collections versus potentially deferred-execution wrappers. That question has been asked before.
The fix for that is do not enumerate the source multiple times. If the code needs perform multiple iteartions (which may be legitimate) then hydrate the collection to a List<T>
before iterating.
Upvotes: 5