views:

198

answers:

6

Look at the specification of the ReadOnlyCollection class, it does implements the IList interface, right.

The IList interface have Add/Update/Read methods, which we call it pre-conditions of the interface. Anywhere in my application if I have an IList I should be able to do all this kind of operations.

But what about if I return a ReadOnlyCollection somewhere in my code and try to call the .Add(...) method? It throws a NotSupportedException. Do you think this is a good example of a bad design? Additionally, is this class breaking the Liskov Substitution Principle?

Why did Microsoft implemented this way? Should it be easier (and better) to make this ReadOnlyCollection implements only the IEnumerable interface (which is, by the way, already readonly)?

+2  A: 

IList has some read method and properties like Item, and IndexOf(..). If ReadOnlyCollection would implement IEnumerable only then you would miss out on those.


Whats the alternative? Having a readonly version of IList and a write version? That would complicate the entire BCL (not to talk about LINQ).


Also I don't think it violates the Liskov Substitution Principle because it is defined at the base level (of IList) that it can throw a not supported exception.

Shay Erlichmen
Yes, but that doesn’t contradict the observation that it is bad design for `ReadOnlyCollection` to implement `IList`.
Timwi
+4  A: 

Yes, it is bad design indeed. The collection interfaces are lacking in .NET: there are no read-only interfaces.

Did you know that string[] implements IList<string> (and ditto for other types)? This has the same problem: you would expect that you can call Add and Remove on the interface, but it would throw.

Unfortunately, this cannot be changed anymore without breaking backwards compatibility, but I agree with you that it is very bad design. A better design would have seen separate interfaces for the read-only capabilities.

Timwi
There is IsReadOnly property you could check before calling those methods.Is it also a bad design decision for Insert method of the same interface to throw ArgumentOutOfRangeException when index is negative or bigger or equal than Count property? Because, by your argumentation, it sure is.
Ivan Ferić
Couldn't they at least add an `[Obselete]` attribute above the `Add()` etc methods so it throws a compiler error instead of letting the problem code be caught at runtime?
Callum Rogers
You can't know wherever your IList variable is a readonly or not List at compile time.
Guilherme Oenning
@Ivan: Where does my answer talk about `Insert` or about `ArgumentOutOfRangeException`? Please don’t put things into my mouth that I didn’t say.
Timwi
@Timwi I didn't say that you said it but that your argumentation on NotSupportedException would lead to that conclusion.
Ivan Ferić
@Callum: `ReadOnlyCollection<>` hides the unsupported members using explicit interface implementation anyway. If your variable is typed as a `ReadOnlyCollection<>` then those members will be inaccessible. The problem lies when your variable is typed as `IList<>`: the unsupported members then become accessible, and even if the underlying implementation did mark them as obsolete the compiler couldn't/wouldn't warn about it.
LukeH
@Ivan: No, it doesn’t. It’s a false analogy.
Timwi
@Timwi: Is it? They both have a way to be checked if it's gonna throw an exception, they are both documented to be thrown (with conditions when they are to be thrown) and they are both in the same interface. What's the difference?
Ivan Ferić
Oh, I see the problem now.
Callum Rogers
@Ivan: One difference is that it *makes sense* and is *reasonably practical* to have a read-only interface and to reserve the writable interface to instances that are actually writable. Another difference is that it is *reasonably practical* for the client code to check array bounds before using the indexer, but to have to call `Add` just to check whether it throws is completely ridiculous and only serves to cause buggy code. (The `IsReadOnly` property is not reliable: `((IList)new byte[0]).IsReadOnly` returns `false`, but `((IList)new byte[0]).Add((byte)0)` still throws!)
Timwi
@Ivan: In general, just because you can’t think of a difference, doesn’t mean there isn’t any, and in particular, it doesn’t mean that someone’s argumentation somehow “leads to a conclusion”. You should be more careful making claims about what someone else said and implied.
Timwi
@Ivan: I just noticed that `IList` actually has a separate `IsFixedSize` property. How many more of these boolean properties should an interface have? Why don’t we have an interface with 100 methods and then 100 separate properties to say whether each of them throws? Because it’s bad design.
Timwi
@Timwi: First I want to say that I didn't mean to offend you in any way. Second, I didn't say that you said nor implied those things - I simply drew a parallel between your assertion and something that we both now is not true (that `IList.Insert(int, T)` is a bad design example for throwing `ArgumentOutOfRange` exception). And third, if what you say about `byte[]` is true, it still doesn't imply that `IList` is badly designed, but rather that implementation of `IList` in `Array` is bad.
Ivan Ferić
@Timwi, re: the 100 methods / 100 separate properties -- aren't you suggesting that the alternative is 100 separate interfaces? Not sure it's obvious that solution is better.
Kirk Woll
@Kirk: No, I’m not; see my first comment on bde’s answer.
Timwi
+2  A: 

I think it's a good example of a trade off between abstraction and specialization.

You want the flexibility of IList, but you also want to impose some constraints, so what do you do? The way it's designed is a little awkward, and probably technically violates some design principles, but I'm not sure what would be better and still give you the same functionality and simplicity.

In this case it may have been better to have a separate IListReadOnly interface. However, it is easy to go down the path to crazy one time use interface proliferation land and make things very confusing.

bde
I agree that it would be bad design to have a separate interface for every single capability (`IAddableCollection`, `IRemovableCollection`, `IIndexableCollection`, ...), but I would have thought that *readonlyness* is a sufficiently fundamental property of a collection type to warrant separation. (Besides, it would enable covariance!)
Timwi
@Timwi: Maybe so. It would be interesting to hear about the design decisions that went into the collection interfaces.
bde
I wouldn't say that the path you mention is 'crazy' in any way. A well designed framework would expose `NSArray` as well as `NSMutableArray`, `NSDictionary` and `NSMutableDictionary`, `NSSet` and `NSMutableSet` etc etc...
Remus Rusanu
@Remus: I'm not saying that it is crazy to expose mutable and nonmutable versions of things, and in this case that would probably have been better. I am saying that you can take things too far by making specialized interfaces, and the result is very confusing.
bde
+3  A: 

It's not a great design, but a necessary evil in my opinion.

It's unfortunate that Microsoft didn't include something like IReadableList<> and IWriteableList<> in the framework and have IList<> itself implement both of those (or even skip IList<> altogether and have IWriteableList<> implement IReadableList<>). Problem solved.

But it's too late to change now, and if you have a situation where you need your collection to have list semantics and you'd prefer to throw an exception at runtime rather than allow mutations, then ReadOnlyCollection<> is, unfortunately, your best option.

LukeH
+1 .That's a good suggestion, I liked it.
Guilherme Oenning
+6  A: 

Although IList<T> interface defines Add(T) and Insert(int,T) methods, it also defines IsReadOnly property and if you read carefully definition of IList.Insert(int,T) and IList.Add(T) methods on MSDN, you can see that they both specify that methods could throw NotSupportedException if list is read-only.

Saying that it's bad design for that reason is like saying that it is also bad design because Insert(int, T) can throw ArgumentOutOfRangeException when index is negative or bigger than the size of collection.

Ivan Ferić
+1  A: 

I think if there is a bad design going on it is a habit of adding to an IList without checking the ReadOnly property. The habit of programmers to ignore portions of an interface doesn't mean the interface is poor.

The truth is that few of us programmers ever bother to read the specifications. And truthfully there are many things that seem more exciting to me than sitting down and reading through the entire specification document. (Things like seeing if one really can hold the eyes open with toothpicks for example.) Besides, I have the limitation that I wouldn't remember everything anyway.

Having said that, one should not use an interface without at least looking at the list of properties and methods. And just what purpose do you think a boolean property named "ReadOnly" is for? Perhaps because the list can be read only for one reason or another. And if you are taking a list passed from someplace outside your own code you should check that the list is not read only before you try to add to it.

Kirk