67

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>?
Zaid Masud
  • 13,225
  • 9
  • 67
  • 88
  • @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 '19 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 01 '19 at 19:25

6 Answers6

43

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.

Martin Ba
  • 37,187
  • 33
  • 183
  • 337
Jon
  • 428,835
  • 81
  • 738
  • 806
  • 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 '12 at 13:48
  • 2
    @ZaidMasud: The writing was in need of some improvement there. Opt-in = you need to explicitly say you are a read only collection -- the opposite of "everyone implements this because the BCL team says so". – Jon Sep 27 '12 at 13:52
  • 6
    But in the current implementation, the boolean condition `(new List() is IReadOnlyCollection)` evaluates to `true`. – Zaid Masud Sep 27 '12 at 14:01
  • 1
    @ZaidMasud: Well, both possible choices could be argued for and against there. I suppose #2 was not a good enough reason in the eyes of the BCL team. – Jon Sep 27 '12 at 14:05
  • 2
    this is the real answer: "int ICollection.Count { ... } // compiler error!" – Notoriousxl Feb 18 '13 at 19:40
  • 2
    Does C# not consider an explicit interface declaration using the name of a derived interface which inherits a member as synonymous with one using the name of the base? VB.NET accepts either name without complaint. – supercat Jun 19 '14 at 01:57
  • @supercat: It does not allow it. I don't know VB, but what you said is somewhat surprising. What if later on `ICollection` declares its own `Count` property? `UnluckyClass` would still compile, but now it would behave differently than before. – Jon Jun 19 '14 at 08:38
  • 1
    @Jon: If both the base and derived interfaces include separate members (with the derived one shadowing the base) then the VB code will only implement the derived method and as a consequence fail compilation, but since adding a method to an interface should require adding a method to the implementation, that should be expected. – supercat Jun 21 '14 at 06:04
  • 1
    But everyone *wants* the behavior you disagree with in reason #2! – binki Dec 12 '14 at 15:48
  • @binki: Sorry, what do you mean? In #2 I am saying that the test would be practically equivalent to `if(true)`, so worthless. – Jon Dec 15 '14 at 17:03
  • 2
    @jon, I think I too am confused about what you mean by “opt-in”. On reading again, I realize that you are describing the current behavior (`List` “opts-in” by implementing both `IReadOnlyCollection` and `ICollection` instead of just `ICollection`). You say that `if (collection is IReadOnlyCollection)` is only useful if `ICollection` does not implement `IReadOnlyCollection`. I fail to see when this behavior is “useful in practice”—it only makes calling a method that accepts `IReadOnlyCollection` counterintuitively harder (e.g., when you want to pass `IList` to it). – binki Dec 15 '14 at 20:54
  • @binki: And it's a good thing it's harder, because `IList` and `IReadOnlyCollection` are semantically very very different. There is absolutely no guarantee that something which "is a list" also "is a read-only collection", so why should the conversion be automatic? I know it can seem awkward at times, but IMO that is simply an indication of an imperfect design. – Jon Dec 15 '14 at 21:00
  • 8
    @Jon, perhaps you are confusing read-only and immutable? The point of `IReadOnlyCollection` 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 '14 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 03 '15 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. – Søren Boisen Dec 29 '15 at 13:33
18

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()'

Community
  • 1
  • 1
Notoriousxl
  • 1,540
  • 1
  • 16
  • 27
  • 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](http://stackoverflow.com/a/3570703/374420) by [Eric Lippert](http://stackoverflow.com/users/88656/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 '13 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). – Notoriousxl Feb 18 '13 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 '13 at 21:42
  • See also http://www.infoq.com/news/2011/10/ReadOnly-WInRT ... it seems that MS "defended" its choises (against .NET community requests) 'till they was forced by WinRT... And the official MS point of view: http://msdn.microsoft.com/en-us/magazine/jj133817.aspx : "Our position has been that this particular aspect (being read-only) is best modeled using the optional feature pattern [...] Over the years, we came to the conclusion that adding a read-only collection interface is worth the added complexity" – Notoriousxl Feb 18 '13 at 21:46
  • 1
    @Zaid: well, IMHO Jon answered with its "UnluckyClass" ;) Always on topic: see the "old" ReadOnlyCollection wrapper class which implements IList and not ICollection... apart from name, it forces you to have a list instead of a icollection, and to write bad code like this (*): ICollection c = ...; var ro = new ReadOnlyCollection(c as IList ?? c.ToArray()) So... MS make errors too :) – Notoriousxl Feb 18 '13 at 21:52
  • It seems there's a general problem here... an interface cannot specify that e.g. IOld.Get is defined to be the exact same function as INew.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.Get();` (broadly similar to a 'using' syntax that was present in C++). – Steve Jul 07 '17 at 08:54
-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!

Rob
  • 5,223
  • 5
  • 41
  • 62
n8wrl
  • 19,439
  • 4
  • 63
  • 103
  • 3
    Collection classes such as `List` 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 '12 at 13:34
  • 2
    That's an excellent point, Zaid. In fact, the last line of this: http://visualstudiomagazine.com/articles/2012/08/07/new-read-only-collection-interfaces-for-net.aspx hints that the BEHAVIOR is not a critieria for implementing or consuming the interface! – n8wrl Sep 27 '12 at 13:42
  • Here is another interesting article relating it to COM: http://www.infoq.com/news/2011/10/ReadOnly-WInRT. Maybe there is some technicality there? – n8wrl Sep 27 '12 at 13:43
  • 3
    There would be substantial usefulness to a type `IReadableCollection` which made no promise of being immutable, or an interface `IImmutableCollection` 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` is used for the functionality that should have been called `IReadableCollection`. – supercat Jun 19 '14 at 02:02
  • 3
    The whole point of a common `IReadOnlyCollection` 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 '14 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. – Asad Saeeduddin Sep 07 '15 at 12:52
  • @AsadSaeeduddin: I couldn't disagree more. I may get an API for a read-only collection, but it might NOT be read-only. Someone else might be using the 'writable API' and modifying it while I'm doing something, like iterating, which I assume to be safe since I have a read-only API. – n8wrl Oct 22 '15 at 13:26
  • 2
    The interface doesn't say "ImmutableCollection". It says "IReadOnlyCollection", which is what you're getting. If you're assuming the collection is immutable, that's your own mistake. – Asad Saeeduddin Oct 22 '15 at 15:01
-3

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.)

herzmeister
  • 11,101
  • 2
  • 41
  • 51
  • 2
    @ZaidMasud There is a difference between being able to read it, and **only** being able to read from it. IRead**Only**Collection. Also note that while you can't mutate from that interface, the underlying collection could still be mutated elsewhere from an actual reference. The point if the interface is that you can assume the underlying collection will never change. – Servy Sep 27 '12 at 13:52
  • edited and added a link to the statement by the BCL team member in the comments to the article – herzmeister Sep 27 '12 at 13:54
  • 16
    As Zaid's [comment](http://stackoverflow.com/questions/12622539/why-doesnt-generic-icollection-derive-from-ireadonlycollection-in-net-4-5/12622784#comment17019450_12622784) on my answer highlights, semantic correctness could not have been the deciding reason here -- otherwise they wouldn't have made `List` implement `IReadOnlyCollection`. – Jon Sep 27 '12 at 14:07
  • 6
    -1 The problem with this reasoning is that it means that the implementation of `List` is *semantically* wrong, because it implements `IReadOnlyCollection`, and obviously, `List` is not read-only. – Zaid Masud Oct 09 '12 at 08:28
  • yes, then it *is* semantically wrong, so be it. The BCL team had to make some compromises as said, I don't like it either. – herzmeister Oct 09 '12 at 08:47
  • 3
    @herzmeister in that case for your answer to be logically correct you need to show why the implementation suggested in the question is *more* semantically wrong than the [*actual*](http://msdn.microsoft.com/en-us/library/6sh2ey19.aspx) implementation. – Zaid Masud Oct 09 '12 at 10:04
  • 2
    This seems a very logical answer to me, it just begs the question: Why does `List` implement `IReadOnlyCollection` – Sconibulus Oct 09 '12 at 20:39
  • 13
    Ideally `ICollection` should inherit from `IReadableCollection` and `IWritableCollection` enabling co- and contravariance on every collection if "viewed" through these interfaces. – Olivier Jacot-Descombes Oct 10 '12 at 22:40
  • yeah something like that would have been nice. Or maybe `ICollectionReader` and `ICollectionWriter` then. – herzmeister Oct 11 '12 at 09:30
  • 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 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 03 '15 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 implements IReadOnlyList 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. – Søren Boisen Dec 29 '15 at 13:31
-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
    -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](http://msdn.microsoft.com/en-us/library/ms173104.aspx). – Zaid Masud Sep 27 '12 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. – Damian Leszczyński - Vash Oct 16 '12 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 '12 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. – Damian Leszczyński - Vash Oct 16 '12 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 '12 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.

jeuton
  • 543
  • 3
  • 7
  • 1
    You cannot implement a `List` as either type; it is a class provided by the Framework and already implemented for you. – Zaid Masud Oct 10 '12 at 22:41
  • Sure you can. A simple not-very-useful example would be `IReadOnlyCollection collection = new List();`. 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 '12 at 23:15
  • 1
    OK so you mean "declare" or "instantiate" rather than "implement".. I see your edit. But if you agree that `List` IS-A `IReadOnlyCollection`, then by the same reasoning it follows that `ICollection` IS-A `IReadOnlyCollection` – Zaid Masud Oct 11 '12 at 08:50
  • (I edited my answer to use the correct term). I don't follow your logic. `List` IS-A `IReadOnlyCollection` because `List` implements `IReadOnlyCollection`. `ICollection` does not implement `IReadOnlyCollection`, therefore `ICollection` IS-NOT-A `IReadOnlyCollection` (and it shouldn't be). – jeuton Oct 11 '12 at 13:44
  • 3
    Think about this a little bit... by your last reasoning if `ICollection` **did** implement `IReadOnlyCollection` then it would be a `IReadOnlyCollection`? 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 '12 at 14:13
  • Edited my answer with some additional comments. – jeuton Oct 11 '12 at 16:01