17

Before immutability, IEnumerable was the go-to interface in many APIs since this had the advantage that the API was insensitive to the actual type of the passed object.

public void DoSomeEnumerationsWithACollection(IEnumerable<Thing> collectionOfThings)
{ 
   foreach (var thing in collectionOfThings) doSomethingWith(thing);
   foreach (var thing in collectionOfThings) doSomethingElseWith(thing);
}

Of course there are at least two downsides to this:

  1. The code behind the API can't rely on the immutability of collectionOfThings and may encounter a "collection modified" exception or hit other other subtle issues.

  2. We don't know whether collectionOfThings is real collection or simply a deferred query. If we assume it's a real collection and it isn't we run the risk of degrading performance by running multiple enumerations. If we assume it's a deferred query and it's actually a real collection then turning it into a local list or other frozen collection incurs unnecessary cost although it does help protect us against the first problem (there's still a race condition whilst performing the "ToList" operation). Obviously we can write a small amount of code to check for this and try to do the "right thing" but this is annoying extra clutter.

I must admit I have never found a satisfactory pattern to address this other than using naming conventions. The pragmatic approach seemed to be that IEnumerable was the lowest friction approach for passing around collections, despite the downsides.

Now, with immutable collections, the situation is much improved...

public void DoSomeEnumerationsWithACollection(ImmutableList<Thing> collectionOfThings)
{ 

There is no longer a risk of collection modification and there's no ambiguity about the performance impact of multiple enumerations.

However, we have apparently lost flexibility on the API since we now have to pass in an ImmutableList. If our client had some other kind of enumerable immutable collection, it would have to be copied into an ImmutableList in order to be consumed even though all we want to do is enumerate it.

Ideally we'd be able to use an interface like

public void DoSomeEnumerationsWithACollection(IImmutableEnumerable<Thing> collectionOfThings)

but of course, an interface can't enforce semantics like immutability except by convention.

Using a base class might work

public void DoSomeEnumerationsWithACollection(ImmutableEnumerableBase<Thing> collectionOfThings)

except that it's considered bad form to create unsealed immutable classes lest a subclass introduce mutability. And in any case, this hasn't been done in the BCL.

Or we could just keep using IEnumerable in the API and using a naming convention to make it clear our code relies upon an immutable collection to be passed in.

So... my question is what patterns are considered best when passing around immutable collections? Is ImmutableList the "new IEnumerable" once we start using immutablity or is there a better way?

Update

IReadOnlyCollection (suggested by Yuval Itzchakov below ) is a distinct improvement over IEnumerable but still does not fully protect the consumer against uncontrolled changes in the collection. It's notable that the Roslyn codebase makes heavy use of immutability (mainly via ImmutableArray) and appears to use explicit typing when passing these into other methods though there are a couple of locations where ImmutableLists are passed into methods that accept IEnumerable.

NeilMacMullen
  • 3,339
  • 2
  • 20
  • 22

2 Answers2

5

After a few years of using immutable collections heavily, I have settled on the convention of using ImmutableArray almost everywhere. This doesn't address the original desire to allow flexibility in the API but in practice it's rare that I use ImmutableList or another list-like structure and when I do, it's usually not much overhead to call ToImmutableArray to get the data across the interface.

NeilMacMullen
  • 3,339
  • 2
  • 20
  • 22
  • 1
    What's the advantage of `ImmutableArray` over `ImmutableList`? As far as I can see from glancing at both implementations `ImmutableArray` seems to always copy when `Add()`ing to it etc. whereas an `ImmutableList` can avoid copying sometimes. – Good Night Nerd Pride Aug 23 '19 at 14:01
  • @GoodNightNerdPride - that's a good link. My own preference for ImmutableArray over ImmutableList is mainly based on that fact that ImmutableArray offers direct indexed access (a[x]) and is closer conceptually to how I want to treat the data being passed. That it is more performant is a bonus! – NeilMacMullen Aug 24 '19 at 21:26
  • 2
    @GoodNightNerdPride Here is a link about the performance ImmutableArray vs ImmutableList: https://www.infoq.com/news/2013/06/ImmutableArray/ ImmutableArray isn’t complex at all. It is literally just a struct that wraps an array. According to ildasm, there aren’t any other fields. This means that reading from the immutable array occurs in O(1) time. Conversely, adding to the immutable array requires a full copy of the underlying array, making it an O(n) operation. – Karel Kral Jan 10 '23 at 09:09
1

what patterns are considered best when passing around immutable collections?

I think the answer to your question is IReadOnlyCollection<T>, which will be spreading in .NET 4.6. By passing a read only collection, you can both keep immutability, and still work with regular collections implementing that interface.

Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
  • 2
    Thanks Yuval. That seems like a nice suggestion though I wonder about how good it really is at enforcing immutablity. ReadOnlyCollection obviously implements the IReadOnlyCollection interface but the MSDN documentation (https://msdn.microsoft.com/en-us/library/ms132474(v=vs.110).aspx) makes it clear that ReadOnlyCollection is only a wrapper around a mutable object. Therefore using IReadOnlyCollection would still appear to allow a mutable object to be passed across the API. – NeilMacMullen May 16 '15 at 13:32
  • @NeilMacMullen I think passing around am IROC is obvious in saying "this shoyld only be used as a read copy". Would anyone really attempt to mutate such an interface? – Yuval Itzchakov May 16 '15 at 15:31
  • 2
    The provider of an IROC is effectively protecting themselves against the consumer inadvertently mutating the collection. However, the consumer does not get the same guarantee. In that sense, it's weaker contract than a fully immutable API which offers assurances to both consumer and provider. – NeilMacMullen May 16 '15 at 15:47
  • @NeilMacMullen Not really. Even if you're providing an immutable list, it doesn't mean the consumer wont effectively make a mutable coopy for themselfs. It is stronger in the sense the consumer won't try to cast a IROC collection to an array or any other mutable class implementing that interface. But are your consumers really that stubbern to try and do so? Sounds unlikely to me. – Yuval Itzchakov May 16 '15 at 15:51
  • 3
    If I provide an immutable list, the consumer _can't_ mutate my copy and I _can't_ mutate the consumers copy. If I provide a ROC to the consumer, it protects me against my copy being mutated by them. However, if I call the consumer on a background thread then mutate the underlying collection for the ROC, then I _have_ changed the consumers copy. Using IROC in the API unfortunately allows this scenario. – NeilMacMullen May 16 '15 at 15:58
  • @NeilMacMullen You're right, but as the producer of the ROC, why would you do that? Also, you can encapsulate the creation of the ROC and only expose it via IROC as well. You're right, it is of course not as strong as an immutable list, but it seems it less strict them an immutablelist. BTW, `ImmutableList` also implements IROC. – Yuval Itzchakov May 16 '15 at 16:35
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/78003/discussion-between-neilmacmullen-and-yuval-itzchakov). – NeilMacMullen May 17 '15 at 09:23