none
New interfaces IReadOnlyList and IReadOnlyDictionary RRS feed

  • Question

  • Hello,

    I apploud to the long-awaited intoduction of interfaces for read-only collections.

    However, I also have some questions. First of all, why there's no general IReadOnlyCollection interface.

     

        public interface IReadOnlyCollection<out T> : IEnumerable<T>
        {
            int Count { get; }
        }

    While I understand the resistance to add another interface for collections, there's a real lack of an interface for non-lazy read-only collection. IEnumerable may not serve that role. Introduction of IReadOnlyList and IReadOnlyDictionary is the last chance to also introduce IReadOnlyCollection, it would be almost impossible to introduce it afterwards, as it would mean that some classes that explicitly implement IList, IReadOnlyList and (independently of IReadOnlyList) IReadOnlyCollection have to implement Count trice.

     

    Second question is about LINQ support for these interfaces. Will Enumerable.Count() be updated to check for these interfaces on sequences that do not support ICollection? Failing to do so, would mean that there's no way to have immutable collections with immutability visible from their interface (as collections will be forced to implement ICollection to support Enumerable.Count efficiently). 

    And the third one. I understand that the support for these interfaces was driven by WinRT requirements. However these interfaces are usefull without WinRT as well. So, will other collections besides List<> and Dictionary<> be updated to support these interfaces?


    • Edited by GregorySh Sunday, September 18, 2011 8:56 AM
    Sunday, September 18, 2011 8:55 AM

All replies

  • I'm confused.  Did someone from the BCL team announce an IReadOnlyDictionary?  There's no value to such an interface because ICollection has an IsReadOnly property which indicates whether or not the instance is read-only, and they said as much in 2004.

    The principal difference between a collection and an enumeration is that a collection can be modified, and thus has an Add and Remove method.  Creating an IReadOnlyCollection that does not implement ICollection would be extremely confusing.

    What does it mean for a list to implement both IList<T> and IReadOnlyList<T>?  Presumably, IReadOnlyList<T> would implement IList<T>, since a read-only list is a list, after all.  Then doesn't that make the IsReadOnly property obsolete?

    It sounds like your principal concern is the fact that you don't like that you need to enumerate an enumeration to know how many items are in it and you want a Count property instead of a Count() extension method.  Feel free to create an extension of IEnumerable<T> that exposes that property as a data member.  We don't need it in the BCL.

    Why do you need to know how many items are in your enumeration in advance?  If you do, you're doing it wrong.

    Evan

    Monday, September 19, 2011 3:04 AM
  • >I'm confused. Did someone from the BCL team announce an IReadOnlyDictionary?

    They were announced at Build. And they were included Visual Studio 11 Developer Preview.

    > There's no value to such an interface because ICollection has an IsReadOnly property which indicates whether or not the instance is read-only, andthey said as much in 2004.

    And now they changed their minds. Actually there's a value in such interface, as returning it from method would indicate that collection should not be modified. The difference with ReadOnlyCollection<> and IReadOnlyList is that  because Add, Remove and the like is not present IReadOnlyList can be made  covariant and it's an interface which is better for unit-testing. I guess the change was driven by the fact that WinRT needed read-only collection types.

    >The principal difference between a collection and an enumeration is that a collection can be modified, and thus has an Add and Remove method. Creating an IReadOnlyCollection that does not implement ICollection would be extremely confusing.

    No, the principal difference between collection and enumeration is that one is fully materialized and other is lazy. The fact that collection is fully materialized means that some of collections (but not all) may support modification, and they may support more effficient access to their properties (as Count) and/or elements.

    It may come as surprise to you but IReadOnlyList does not inherit from IList, and IList does not inherit from IReadOnlyList.

    The first option is not desirable - the purpose of IReadOnlyList is to provide read-only access to List, so there's should not be Add/Remove methods.

    The second one is desirable but is not possible. It's a breaking change in the case of explicit implementation of IList (an explicit implementation of IList.Count is not an explicit implementation of IReadOnlyList.Count).

    So they created indepenent interface.

    Think of IList as IModifiableList and of ICollection as IModifiableCollection. And IReadOnlyList does not mean IImmutableList, it means IReadOnlyViewOnList.

    > It sounds like your principal concern is the fact that you don't like that you need to enumerate an enumeration to know how many items are in it and you want a Count property instead of a Count() extension method.

    Actually Enumerable.Count checks for whether its parameter supports ICollection, and uses Count property if it does. My concern is that because IReadOnlyList is an independent interface, they may forgot to update Enumerable.Count to also check for IReadOnlyList.

    My biggest concern however is that driven by good intentions these changes may create even bigger mess than we have now. To be clear, the most conceptually beautiful collection library I've used in imperative language is C++ STL (probably D has better library, but I've never used it). And C# is still imperative language. I'm biggest proponent of LINQ, but sometimes you need imperative stuff for your collections, and then BCL collections seem so inadequate comparing to C++ STL.

    Monday, September 19, 2011 9:31 AM
  • also, why doesn't

    - IList<T> derive from IReadOnlyList<T> ?

    - IDictionary<T> derive from IReadOnlyDictionary<T> ?

    - ICollection<T> derive from IReadOnlyCollection<T> ?

     

    surely a mutable thing is like an immutable thing but with more capabilities (the mutation methods) ?

    Wednesday, September 28, 2011 6:27 PM
  • Unfortunately deriving IList<> from IReadOnlyList<> is a breaking change and thus is not possible at the current stage.

    Though IReadOnlyList<> does not contain new methods, if IList<> is explicitly implemented then implementation mentions IList<>.Count and not IReadOnlyList<>.Count. Allowing IList<>.Count to be treated as IReadOnlyList<>.Count is a significant change on CLI level, and I'm not sure that anyone understands full implications of that change. So that probably would not be implemented.

    I've seen post of Eric Lippert on that matter, but now I cannot find it.

    Thursday, September 29, 2011 6:53 AM
  • I apploud to the long-awaited intoduction of interfaces for read-only collections.

    Thanks! Glad you like them. This thread asks a bunch of great question. Let me take one by one.

    Why is there no IReadOnlyCollection<T>?

    We considered this design, but we felt adding a type that only provides a Count property does not add much value to the BCL. In the BCL team we believe that an API start at minus a thousand points and thus providing some value is not good enough to justify being added. The reason that adding new APIs also has cost, for example developers have more concepts to choose from. Initially we thought that adding this type would allow code to gain better perf in scenarios where you just want to get the count and then do some interesting stuff with it. For example, bulk adding to an existing collection. However, for those scenarios we already encourage people to just take an IEnumerable<T> and special case having the instance implementing ICollection<T> too. Since all of our built-in collection types implement this interface there are no perf gains in the most common scenarios. BTW, the Count() extension methods on IEnumerable<T> do this as well.

    Will Enumerable.Count() be updated to check for these interfaces on sequences that do not support ICollection?

    Currently, that is not the case and there no plans on doing so because the only implementations that we ship in the framework would also implement ICollection/ICollection<T>. We thought about the case of immutable collections but it seems likely that immutable collections would end up having a different interface. Immutability means that once you have a reference to a collection, you will never observe any changes. This is a guarantee that read-only collections do not make. They only disallow the consumer of these interfaces to change the collection. The owner of the collection might still change them. For a deeper discussion about this topic I highly recommend this blog post.

    Will other collections besides List<> and Dictionary<> be updated to support these interfaces?

    Absolutely. In fact, all of our built-in collection types already implement IReadOnlyList<> and IReadOnlyDictionary<>. This means, you can directly pass an instance of List<T>, T[] or Dictionary<> to an API that takes an IReadOnly-version of it.

    Why did we change our minds (see this post from 2004)?

    As I said earlier, we only provide abstractions if there is value in having them. In .NET 4, C# and VB started to support covariance and contra variance. Since they only work on interfaces and delegate types shipping an interface for a read-only list provides value because we can make it covariant.

    For example, if a method takes an IReadOnlyList<Person> you can now pass an instance of List<Employee>, assuming Employee is derived from Person.

    What does it mean for a list to implement both IList<T> and IReadOnlyList<T>? 

    That depends on the implementation. For example, ReadOnlyCollection<T> implements both interfaces. IList<T> is implemented in a way that it does not allow modifications (i.e. it throws an exception), so it effectively protects the underlying collection from being modified. It is truly read-only in the sense that nobody, except for the owner of the underlying collection, can modify it.

    On the other hand, List<T> also implements IReadOnlyList<T> although everybody can change it. The rationale behind this decision was that you can “view” every collection as being read-only. This allows scenarios in which you can create methods that state in their contract “I only want to look, but not touch”. This also enables the method to accept list of more derived types, because IReadOnlyList<T> is covariant.

    Why did we not change the existing interfaces to extend the read-only interfaces?

    It looks like a reasonable assumption that it works because the read-only interfaces are purely a subset of the read-write interfaces. Unfortunately, it is incompatible because at the metadata level every method on every interface has its own slot (which makes explicit interface implementations work).


    Immo Landwerth | .NET Framework Team (BCL) | http://blogs.msdn.com/b/bclteam/
    Friday, September 30, 2011 8:33 PM
  • >Will Enumerable.Count() be updated to check for these interfaces on >sequences that do not support ICollection?

    >Currently, that is not the case and there no plans on doing so because the only >implementations that we ship in the framework would also implement >ICollection/ICollection<T>.

    But IReadOnlyList/IReadOnlyDictionary is covariant while ICollection<T> is not. And Count() checks for ICollection<T>, not for ICollection (as far as I can see non-generic ICollection is considered obsolete). So, if we have List<string> cast to IEnumerable<object> or IReadOnlyList<object> then running Enumerable.Count() on it surprisingly would take linear time instead of constant time.

    And that is one of the reasons to introduce IReadOnlyCollection. Because that situation also exists for collections that could not implement IReadOnlyList or IReadOnlyDictionary (HashSet, LinkedList etc). 


    Update: you may say that, it isn't the problem as Count() always worked as it works now. However, until .net 4.0 there's quite reasonable assumption that if collection supports IEnumerable<> and ICollection<> then they agree on type parameter. In .net 4.0 IEnumerable<> is covariant (which is right) and ICollection<> is not (which is also right), and that assumption is no longer valid.

    There are several possible solutions of this problem:

    1) do nothing on API level, but explain to people that in case when they expect constant time of Count(), they should use property and not the method or make sure that parameter of IEnumerable is of the most derived type. (personally I do not think this is a good idea).

    2) introduce IReadOnlyCollection with Count (my favourite)

    3) do not introduce IReadOnlyCollection but introduce IReadOnlyLinkedList, IReadOnlySet etc (but that would mean intoducing even more interfaces)

    4) introduce non-generic ICountable

    5) use reflection to check whether sequence supports some ICollection<> (but cost of reflection is quite high compared to as operator).




    • Edited by GregorySh Monday, October 3, 2011 3:23 PM
    Monday, October 3, 2011 10:37 AM
  • >Will Enumerable.Count() be updated to check for these interfaces on >sequences that do not support ICollection?

    >Currently, that is not the case and there no plans on doing so because the only >implementations that we ship in the framework would also implement >ICollection/ICollection<T>.

    But IReadOnlyList/IReadOnlyDictionary is covariant while ICollection<T> is not. And Count() checks for ICollection<T>, not for ICollection

    The implementation of Enumerable.Count() actually tests for both, ICollection<T> as well as ICollection. So for the existing collection types this will work -- even if the collection type has been casted to IEnumerable<Base>.

    You are correct that we obsoleted the non-generic collection types and thus have removed them from .NET Core. However, since the implementation is still .NET 4.5, the check will succeed at runtime as, for example, List<T> still implements ICollection.


    Immo Landwerth | .NET Framework Team (BCL) | http://blogs.msdn.com/b/bclteam/
    Tuesday, November 8, 2011 11:36 PM
  • But IReadOnlyList/IReadOnlyDictionary is covariant while ICollection<T> is not.



     

     

    IReadOnlyDictionary does not look covariant to me: http://msdn.microsoft.com/en-us/library/hh136548(v=VS.110).aspx

    Had been wondering about this myself, as KeyValuePair isn't covariant. They'd have to introduce a ICovariantPair also.

     

    Thursday, December 15, 2011 10:37 AM
  • Why do you need to know how many items are in your enumeration in advance?  If you do, you're doing it wrong.

    Evan

    Why not ask the designers of Enumerable.ToArray() and Enumerable.ToList() that question?  According to your comment above, both of those methods are 'doing it wrong'.

    In case it isn't obvious, when we do something that involves having to allocate memory, where the amount of memory that must be allocated is indirectly or directly proportional to the number of elements in an IEnumerable, knowing the number of elements in advance, allows us to allocate all the required memory in a single step, rather than having to incrementally allocate memory as needed (as both the ToArray() and ToList() methods must do, if they fail to acquire a count when their source does not implement ICollection<T> or ICollection).

    Furthermore, there are quite a few scenarios where LINQ expressions are theoretically able to deduce the count of their resulting sequences in advance, but do not, and that is only because the plumbing/infrastructure that would permit that is not there.

    For example:

    ICollection<KeyValuePair<string,int>> source = ... int[] sorted = source.OrderBy( p => p.Key ).Select( p => p.Value ).ToArray();

    What is significant in the above example, is that the number of items in the source ICollection<T> is equal to the number of elements in the resulting array of int[]. So, in that case, it is entirely possible for .ToArray() to determine in advance, the number of items that will be in the resulting array, because both OrderBy() and Select() are correspondent (e.g., the number of items that go in always equals the number of items that come out). Currently, given the result of the Select() call above, ToArray() must incrementally allocate memory (e.g., like the List<T>( IEnumerable<T> ) constructor does), which is entirely unnecessary.

    The precise mechanism by which ToArray(), ToList(), and Count() would use to obtain the number of items in the source ICollection<T> in the case of the above example, is merely an implementation detail. Myself and others have outlined various ways it can be done, one of them is by having the iterators that Select(), OrderBy() and others return implement an interface that would allow the downstream consumer of the items to query it and its upstream iterators for a count, which may or may not succeed, but is certainly worth attempting.

    For more on this, please see LINQ optimization with (non-generic) ICollections










    Saturday, February 25, 2012 4:49 AM
  • > Will other collections besides List<> and Dictionary<> be updated to support these interfaces?

    > Absolutely. In fact, all of our built-in collection types already implement IReadOnlyList<> and IReadOnlyDictionary<>. This means, you can directly pass an instance of List<T>, T[] or Dictionary<> to an API that takes an IReadOnly-version of it.

    Why collections from System.dll do no implement IReadOnly-* interfaces? I mean HashSet`1, Queue`1, etc.


    • Edited by hazzik Tuesday, August 7, 2012 6:48 PM
    Tuesday, August 7, 2012 6:48 PM
  • I think the fundamental problem here is that the .NET 2.0 framework did not start with covariance and contravariance, so it wasn't a consideration when creating the initial interfaces.

    If the framework had started with covariance, I would hope the base list interfaces would look more like:
    IReadableList<out T>, IWritableList<in T> which are both inherited by IList<T>.

    Dictionaries interfaces would look like:
    IReadableDictionary<in, TKey, out TValue>, IWritableDictionary<in TKey, in TValue> which are both inherited by IDictionary<TKey, TValue>.

    I can see many merits to this design pattern.
    For one, you know it is writable because it is a List<T> or IWritableList<T>, and conversely for readable.  If you want both you demand the IList<T>, but the capabilities of the collection are clear, and when using them in a parameter, your purpose is also clear.  Not to mention the covariance support.
    The same would go for streams, but don't get me started there.

    I think the fact that IReadOnlyDictionary only supports some types, and that it has no common ground with IDictionary, makes it a clear sign that it was made too late and created wrong.  It is clear that IDictionary shouldn't inherit from IReadOnlyDictionary, that would convey the wrong idea, but IReadOnlyDictionary could inherit from IReadableDictionary quite easily and would still prevent casts to IDictionary, retaining its conveyance of immutability, which is the point.  However, someone who wants a dictionary to read from, but doesn't care if it's immutable, shouldn't be locked into IDictionary or IReadOnlyDictionary with no common ground.  Either one would lock them into the wrong path.

    It would make much more sense to refactor your architecture to separate the read/write concerns, then maybe .NET 5.0 will have useful read/write collection interfaces.
    This does not have to be a breaking change, but I think it is quite late to expect anything to improve in the BCL now.

    Friday, January 31, 2014 8:59 PM
  • One problem with joining IReadableList<out T> and IWritableList<in T> into an IList<T> is that you cannot merge an indexer having only a getter and an indexer having only a setter into one having both. I wish we could.
    Wednesday, November 4, 2015 8:05 PM