69

In .NET 4.5 / C# 5, IReadOnlyCollection<T> is declared with a Count property:

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

I am wondering, wouldn't it have made sense for ICollection<T> to implement the IReadOnlyCollection<T> interface as well:

public interface ICollection<T> : IEnumerable<T>, IEnumerable, *IReadOnlyCollection<T>*

This would've meant that classes implementing ICollection<T> would've automatically implemented IReadOnlyCollection<T>. This sounds reasonable to me.

The ICollection<T> abstraction can be viewed as an extension of the IReadOnlyCollection<T> abstraction. Note that List<T>, for example, implements both ICollection<T> and IReadOnlyCollection<T>.

However it has not been designed that way.

What am I missing here? Why would the current implementation have been chosen instead?


UPDATE

I'm looking for an answer that uses Object Oriented design reasoning to explain why:

  • A concrete class such as List<T> implementing both IReadOnlyCollection<T> and ICollection<T>

is a better design than:

  • ICollection<T> implementing IReadOnlyCollection<T> directly

Also please note that this is essentially the same question as:

  1. Why doesn't IList<T> implement IReadOnlyList<T>?
  2. Why doesn't IDictionary<T> implement IReadOnlyDictionary<T>?
3
  • @Servy For statically compiled code, overload resolution is already a solved problem. There are “targeting packs” or “reference assemblies” which contain only the types defined in the framework version you’re targeting. The compiler sees those and will do the overload resolution the same way regardless of what runtime your code runs on. Your issue is only relevant with dynamic code or reflection, both of which need extra special care and should be avoided if possible for this and other reasons. Getting different behavior when retargeting is expected by the programmer.
    – binki
    Aug 12, 2019 at 18:27
  • @binki Yes, If you target an older .NET runtime your code will continue to work, even if the computer running the code has a newer runtime. The breaking change I was referring to here is that if you take code compiled against, say, .NET 3, then change the runtime to .NET 4.5, you can make it no longer compile. That's a breaking change of the runtime that changing the codebase to that runtime can make the code no longer compile.
    – Servy
    Oct 1, 2019 at 19:25
  • 1
    Might for .NET 9: github.com/dotnet/runtime/issues/31001
    – n0099
    Oct 29, 2023 at 3:26

6 Answers 6

45

There are probably several reasons. Here are some:

  • Huge backwards compatibility problems

    How would you write the definition of ICollection<T>? This looks natural:

    interface ICollection<T> : IReadOnlyCollection<T>
    {
        int Count { get; }
    }
    

    But it has a problem, because IReadOnlyCollection<T> also declares a Count property (the compiler will issue a warning here). Apart from the warning, leaving it as-is (which is equivalent to writing new int Count) allows implementors to have different implementations for the two Count properties by implementing at least one explicitly. This might be "amusing" if the two implementations decided to return different values. Allowing people to shoot themselves in the foot is rather not C#'s style.

    OK, so what about:

    interface ICollection<T> : IReadOnlyCollection<T>
    {
        // Count is "inherited" from IReadOnlyCollection<T>
    }
    

    Well, this breaks all existing code that decided to implement Count explicitly:

    class UnluckyClass : ICollection<Foo>
    {
         int ICollection<Foo>.Count { ... } // compiler error!
    }
    

    Therefore it seems to me that there's no good solution to this problem: either you break existing code, or you force an error-prone implementation on everyone. So the only winning move is not to play.

15
  • 5
    Not sure I understand your second reason. I think the code sample you have provided would be possible, valid, and compilable. What do you mean by "opt-in"?
    – Zaid Masud
    Sep 27, 2012 at 13:48
  • 6
    But in the current implementation, the boolean condition (new List<string>() is IReadOnlyCollection<Foo>) evaluates to true.
    – Zaid Masud
    Sep 27, 2012 at 14:01
  • 8
    @Jon, perhaps you are confusing read-only and immutable? The point of IReadOnlyCollection<T> is to enable you to write a method that declares “I only need to read this collection, I promise I won’t add or remove any items” (ignoring the possibility of runtime casting, ew). Very useful for writing generic code which doesn’t care if the collection of items is mutable or not. As another example, a file opened read-only by a process has the ability to change behind the process’s back. Yet this process won’t open the file in append mode because it wanted to perform read operations only.
    – binki
    Dec 15, 2014 at 21:25
  • 9
    @Jon, point #2 is not valid for multiple reasons. Most importantly, it misrepresents the purpose of the IReadOnly* interfaces - they present a read-only view of the collection, giving an indication of how the collection is intended to be used in a particular context. It's not an indication that the collection can't or won't change. Second, there is the ICollection.IsReadOnly property which serves the purpose you seem to be after. Run-time type checks are the wrong mechanism for the job.
    – Darryl
    Nov 3, 2015 at 17:12
  • 3
    Downvoted for point #2 for the reason Darryl mentions. Your answer propagates the too common misinterpretation of the meaning of the IReadOnly* interfaces. Dec 29, 2015 at 13:33
19

Jon was right here https://stackoverflow.com/a/12622784/395144 , you should mark his reply as the answer:

int ICollection<Foo>.Count { ... } // compiler error!

Since interfaces can have explicit implementations, extracting base interfaces is not backward compatible (with base classes you don't have this problem).

That's why...

Collection<T> : IReadOnlyCollection<T>
List<T> : IReadOnlyList<T>
Dictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>

... but not their interfaces.

IMHO, they did a design error initially, quite unresolvable now (without breaking things).

EDIT: hiding doesn't help, old (explicit) implementations won't still build (without modifying the code):

interface INew<out T> { T Get(); }

interface IOld<T> : INew<T>
{
    void Set(T value);
    new T Get();
}

class Old<T> : IOld<T>
{
    T IOld<T>.Get() { return default(T); }
    void IOld<T>.Set(T value) { }
}

'Sample.Old' does not implement interface member 'Sample.INew.Get()'

6
  • Interesting point, but you're forgetting that both ICollection and IReadOnlyCollection can define Count (the derived class will hide the base class member with the new keyword). You can see an example of where this happens in this answer by Eric Lippert. So, although a good point, in itself your answer is not a good enough reason to pick the design they did.
    – Zaid Masud
    Feb 18, 2013 at 20:52
  • 1
    @ZaidMasud for the hiding, I've answered inside an edit (comments are too short) :) For the design reason... as I wrote, I think that was an (initial) error by Microsoft (a readonly, compile time safe sub-interface is WAY better than an Add method that throws NotSupportedException if ICollection.IsReadOnly is true). Feb 18, 2013 at 21:37
  • 1
    You are right. Too bad you missed the bounty period :) Although Jon states a similar reason, he looks at it from the perspective of "allows implementors to have different, implementations for the two Count" rather than show that it causes a breaking change, making it non backwards compatible.
    – Zaid Masud
    Feb 18, 2013 at 21:42
  • 1
    @Zaid: well, IMHO Jon answered with its "UnluckyClass" ;) Always on topic: see the "old" ReadOnlyCollection<T> wrapper class which implements IList<T> and not ICollection<T>... apart from name, it forces you to have a list instead of a icollection, and to write bad code like this (*): ICollection<T> c = ...; var ro = new ReadOnlyCollection<T>(c as IList<T> ?? c.ToArray()) So... MS make errors too :) Feb 18, 2013 at 21:52
  • 1
    It seems there's a general problem here... an interface cannot specify that e.g. IOld<T>.Get is defined to be the exact same function as INew<T>.Get, and references to either should be treated equivalently by the runtime. What if there were some new syntax so that this could be declared... e.g. replacing new T Get(); with something like using INew<T>.Get(); (broadly similar to a 'using' syntax that was present in C++).
    – Steve
    Jul 7, 2017 at 8:54
-2

It would be semantically wrong, because obviously, not every ICollection is read-only.

That said, they could have called the interface IReadableCollection, while an implementation could be called ReadOnlyCollection.

However, they didn't go that route. Why? I saw a BCL team member write that they didn't want the collections API to become too convoluted. (Although it already is, frankly.)

12
  • 16
    As Zaid's comment on my answer highlights, semantic correctness could not have been the deciding reason here -- otherwise they wouldn't have made List<T> implement IReadOnlyCollection<T>.
    – Jon
    Sep 27, 2012 at 14:07
  • 6
    -1 The problem with this reasoning is that it means that the implementation of List<T> is semantically wrong, because it implements IReadOnlyCollection<T>, and obviously, List<T> is not read-only.
    – Zaid Masud
    Oct 9, 2012 at 8:28
  • 14
    Ideally ICollection<T> should inherit from IReadableCollection<out T> and IWritableCollection<in T> enabling co- and contravariance on every collection if "viewed" through these interfaces. Oct 10, 2012 at 22:40
  • 4
    Yet another answer that confuses read-only with immutable. As @binki says in a comment in another answer, "the whole point of a common IReadOnlyCollection<T> is that you can treat both immutable and mutable lists as a read-only list." I don't care if I can change the list, I only care that I can read the list regardless of whether modification privileges are also supported.
    – Darryl
    Nov 3, 2015 at 17:27
  • 6
    Downvoted. So sad to see a blatantly wrong answer attract this amount of upvotes. You only have to know that List<T> implements IReadOnlyList<T> to see that the logic in this answer is wrong. Naming of these interfaces may be bad but their usage in the BCL demonstrates their meaning quite clearly - IReadOnlyCollection represents a read-only VIEW of a collection that itself could be mutable. Dec 29, 2015 at 13:31
-3

When I first read your question I thought "Hey, he's right! That would make the most sense." But the point of an interface is to describe a contract - A description of behavior. If your class implements IReadOnlyCollection, it should be read-only. Collection<T> isn't. So for a Collection<T> to implement an interface that implied read-only could lead to some pretty nasty problems. Consumers of an IReadOnlyCollection are going to make certain assumptions about that underlying collection - like iterating over it is safe becuase no one can be modifying it, etc. etc. If it was structured as you suggest that might not be true!

8
  • 3
    Collection classes such as List<T> have been modified to implement both IReadOnlyCollection and ICollection... so wouldn't consumers of List in the current design have the same problem you have stated?
    – Zaid Masud
    Sep 27, 2012 at 13:34
  • 2
    That's an excellent point, Zaid. In fact, the last line of this: visualstudiomagazine.com/articles/2012/08/07/… hints that the BEHAVIOR is not a critieria for implementing or consuming the interface!
    – n8wrl
    Sep 27, 2012 at 13:42
  • 3
    There would be substantial usefulness to a type IReadableCollection<out T> which made no promise of being immutable, or an interface IImmutableCollection<T> which promised to forevermore contain the same set of T's. I can't think of much use for an interface which promises that the object is "read-only" but doesn't guarantee that it encapsulates an immutable sequence. IReadOnlyCollection<T> is used for the functionality that should have been called IReadableCollection<T>.
    – supercat
    Jun 19, 2014 at 2:02
  • 3
    The whole point of a common IReadOnlyCollection<T> is that you can treat both immutable and mutable lists as a read-only list. It lets you define consumers which only need to read an indexed lsit of values work with every type of list, because even mutable lists have the ability to remain static if you access them through an IReadOnly interface. Why wouldn’t you want to be able to pass your mutable list to a function that performs some useful task with a readonly indexed list as an input?
    – binki
    Dec 12, 2014 at 15:54
  • 4
    This answer is poorly reasoned. Any class that implements IReadOnlyCollection implements a sufficient API to treat it as a read-only collection. If accessed through a reference of type IReadOnlyCollection, you will only be able to use it as a read-only collection. Whether it implements further API to act as a writeable collection, a database client, or a giraffe is completely irrelevant from an OOP standpoint. Sep 7, 2015 at 12:52
-4

This interface is more a description interface then really functional thing.

In suggested approach every collection must be understood as something that you can onlly read ad is always the same. So you could not add or remove a thing after creation.

We could ask our self why the interface is called IReadOnlyCollection, not 'IReadOnlyEnumerable' as it use IEnumerable<T>, IEnumerable. The answer is easy this type of interface would not have any senece, because Enumerable is already 'read only'.

So lets look in doc IReadOnlyCollection(T)

The first (and last) senetece in description says:

Represents a strongly-typed, read-only collection of elements.

And I think that this explain everything if you know what strong-typing is for.

5
  • 5
    -1 I found it really difficult to follow your argument. I cannot fathom any relationship between strong typing and this discussion. Both cases are completely strongly typed. In fact, the entire C# language is strongly typed.
    – Zaid Masud
    Sep 27, 2012 at 16:33
  • The strong typing concept, in simple word can presented as. If A is something, and B is something else. A cannot be B. So if something is Collection can not be ReadOnlyCollection. Therefore this have some logic. Oct 16, 2012 at 13:49
  • 2
    So if A=List (is something) and B=IEnumerable (is something else), then List cannot be IEnumerable? Please don't try to manufacture logic out of something that makes no sense whatsoever.
    – Zaid Masud
    Oct 16, 2012 at 15:45
  • No then i say that enumerable can not be list. If you think that you can write on something that is read only its your law to think so. Even if is not rational whatsover. Even if my logic is for you strange, it fits the designers idea. your not deal with it. EOT. Oct 16, 2012 at 18:32
  • 5
    I have to go with Zaid on this one, The whole point of object orientated programming, and concepts like DRY is reusability of code. If IGiraffe cannot be treated like a IMammal, and IMammal cannot be treated like IAnimal, then there is really no point in ever having a base class or base interface.
    – Joebone
    Nov 23, 2012 at 16:11
-4

The Object Oriented design reasoning would stem from the "Is A" relationship between a class and any interfaces that the class implements.

In .NET 4.5/ c# 5.0 List<T> is both an ICollection<T> and IReadOnlyCollection<T>. You can instantiate a List<T> as either type depending on how you want your collection to be used.

While having ICollection<T> implement IReadOnlyCollection<T> would give you the ability to have List<T> be either an ICollection<T> or IReadOnlyCollection<T>, doing so would be saying that ICollection<T> "Is A" IReadOnlyCollection<T> which it is not.

Updated

If every class that inherited from ICollection<T> implemented IReadOnlyCollection<T> then I would say it makes more sense to have ICollection<T> implement the interface. Since that is not the case, think about what it would look like if ICollection<T> were to implement IReadOnlyCollection<T>:

public interface IReadOnlyCollection<T> : IEnumerable<T>, IEnumerable{}
public interface ICollection<T> : IReadOnlyCollection<T> {}

If you were to look at the signature for ICollection<T>, it looks like it IS-A read-only collection. Wait, let's look at IReadOnlyCollection and ahh...it implements IEnumerable<T> and IEnumerable so it doesn't necessarily have to be a read-only collection. Functionally the implementations proposed by the original question are the same, but semantically I believe it is more correct to push an interface implementation as high as it can go.

6
  • 1
    You cannot implement a List<T> as either type; it is a class provided by the Framework and already implemented for you.
    – Zaid Masud
    Oct 10, 2012 at 22:41
  • Sure you can. A simple not-very-useful example would be IReadOnlyCollection<T> collection = new List<T>();. If you were designing a framework where you only wanted to return a read-only collection you might have a Property or Method return an IReadOnlyCollection instead of a List, ICollection, IEnumerable, etc.
    – jeuton
    Oct 10, 2012 at 23:15
  • 1
    OK so you mean "declare" or "instantiate" rather than "implement".. I see your edit. But if you agree that List<T> IS-A IReadOnlyCollection<T>, then by the same reasoning it follows that ICollection<T> IS-A IReadOnlyCollection<T>
    – Zaid Masud
    Oct 11, 2012 at 8:50
  • (I edited my answer to use the correct term). I don't follow your logic. List<T> IS-A IReadOnlyCollection<T> because List<T> implements IReadOnlyCollection<T>. ICollection<T> does not implement IReadOnlyCollection<T>, therefore ICollection<T> IS-NOT-A IReadOnlyCollection<T> (and it shouldn't be).
    – jeuton
    Oct 11, 2012 at 13:44
  • 3
    Think about this a little bit... by your last reasoning if ICollection<T> did implement IReadOnlyCollection<T> then it would be a IReadOnlyCollection<T>? Why should List be read-only and ICollection not be? What's so read-only about a List that isn't also the case about an ICollection?
    – Zaid Masud
    Oct 11, 2012 at 14:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.