16

Update 2011-Jan-06:

Believe it or not, I went ahead and incorporated this interface into an open source library I've started, Tao.NET. I wrote a blog post explaining this library's IArray<T> interface, which not only addresses the issues I originally raised in this question (a year ago?!) but also provides a covariant indexed interface, something that's sorely lacking (in my opinion) in the BCL.


Question (in short):

I asked why .NET has IList<T>, which implements ICollection<T> and therefore provides methods to modify the list (Add, Remove, etc.), but doesn't offer any in-between interface such as IArray<T> to provide random access by index without any list modification.


EDIT 2010-Jan-21 2:22 PM EST:

In a comment to Jon Skeet's original answer (in which he questioned how often one would have any need for a contract such as IArray<T>), I mentioned that the Keys and Values properties of the SortedList<TKey, TValues> class are IList<TKey> and IList<Value>, respectively, to which Jon replied:

But in this case it's declared to be IList and you know to just use the indexers. . . . It's not hugely elegant, I agree - but it doesn't actually cause me any pain.

This is reasonable, but I would respond by saying that it doesn't cause you any pain because you just know you can't do it. But the reason you know isn't that it's clear from the code; it's that you have experience with the SortedList<TKey, TValue> class.

Visual Studio isn't going to give me any warnings if I do this:

SortedList<string, int> mySortedList = new SortedList<string, int>();

// ...

IList<string> keys = mySortedList.Keys;
keys.Add("newkey");

It's legal, according to IList<string>. But we all know, it's going to cause an exception.

Guillaume made an apt point as well:

Well, the interfaces aren't perfect but a dev can check the IsReadOnly property before calling Add/Remove/Set...

Again, this is reasonable, BUT: does this not strike you as a bit circuitous?

Suppose I defined an interface as follows:

public interface ICanWalkAndRun {
    bool IsCapableOfRunning { get; }

    void Walk();
    void Run();
}

Now, suppose as well that I made it a common practice to implement this interface, but only for its Walk method; in many cases, I would opt to set IsCapableOfRunning to false and throw a NotSupportedException on Run...

Then I might have some code that looked like this:

var walkerRunners = new Dictionary<string, ICanWalkAndRun>();

// ...

ICanWalkAndRun walkerRunner = walkerRunners["somekey"];

if (walkerRunner.IsCapableOfRunning) {
    walkerRunner.Run();
} else {
    walkerRunner.Walk();
}

Am I crazy, or is this kind of defeating the purpose of an interface called ICanWalkAndRun?


Original Post

I find it very peculiar that in .NET, when I am designing a class with a collection property that provides random access by index (or a method that returns an indexed collection, etc.), but should not or cannot be modified by adding/removing items, and if I want to "do the right thing" OOP-wise and provide an interface so that I can change the internal implementation without breaking the API, I have to go with IList<T>.

The standard approach, it seems, is to go with some implementation of IList<T> that explicitly defines the methods Add, Insert, etc. -- typically by doing something like:

private List<T> _items;
public IList<T> Items {
    get { return _items.AsReadOnly(); }
}

But I kind of hate this. If another developer is using my class, and my class has a property of type IList<T>, and the whole idea of an interface is: "these are some available properties and methods", why should I throw a NotSupportedException (or whatever the case may be) when he/she tries to do something that, according to the interface, should be completely legal?

I feel like implementing an interface and explicitly defining some of its members is like opening a restaurant and putting some items on the menu -- perhaps in some obscure, easy-to-miss part of the menu, but on the menu nonetheless -- that are simply never available.

It seems there ought to be something like an IArray<T> interface that provides very basic random access by index, but no adding/removing, like the following:

public interface IArray<T> {
    int Length { get; }
    T this[int index] { get; }
}

And then IList<T> could implement ICollection<T> and IArray<T> and add its IndexOf, Insert and RemoveAt methods.

Of course, I could always just write this interface and use it myself, but that doesn't help with all the pre-existing .NET classes that don't implement it. (And yes, I know I could write a wrapper that takes any IList<T> and spits out an IArray<T>, but ... seriously?)

Does anyone have any insight into why the interfaces in System.Collections.Generic were designed this way? Am I missing something? Is there a compelling argument against what I'm saying about my issues with the approach of explicitly defining members of IList<T>?

I'm not trying to sound cocky, as if I know better than the people who designed the .NET classes and interfaces; it just doesn't make sense to me. But I'm ready to acknowledge there's plenty I probably haven't taken into consideration.

Dan Tao
  • 125,917
  • 54
  • 300
  • 447
  • 3
    Good question. Since I do a lot of programming work in C++, I can immediately see the benefit this would have (cue random access iterator). – Konrad Rudolph Jan 21 '10 at 19:40
  • Can you link to the original post? –  Jan 21 '10 at 19:45
  • @yodaj007: This is the original post. I just added a lot to the beginning. – Dan Tao Jan 21 '10 at 19:47
  • I don't see "Jon Skeet's original answer". That's why I asked. –  Jan 21 '10 at 22:48
  • @yodaj007: Yes, it mysteriously disappeared. I figured at first that perhaps he was revising it; by now, though, I'd assume he's forgotten. – Dan Tao Jan 22 '10 at 04:09
  • 2
    It's a notable update that something almost identical to your `IArray` exists in .NET 4.5 as [`IReadOnlyList`](http://msdn.microsoft.com/en-us/library/hh192385.aspx) (and readonly `Collection` and `Dictionary` interfaces). – Tim S. Sep 26 '13 at 14:55
  • `IReadOnlyList` isn't perfect as it doesn't have a write indexer property, which arrays do have. – Dan Stevens Aug 10 '21 at 17:09

3 Answers3

6

Design questions are not always black and white.

One side is exact interfaces for each situation, which makes the whole process of actually implementing interfaces a real pain.

The other is few(er) multi-purpose interfaces which aren't always fully supported by the implementor but make many things easier, such as passing instances around which are similar but would not get the same interfaces assigned in the "exact interface" design.

So the BCL designers chose to go the second way. Sometimes I also wish that interfaces were a little less multi-purpose, especially for the collections and with the C#4 interface co-/contravariance features (which cannot be applied to most collection interfaces escept for IEnumerable<> because they contain both co- as well as contravariant parts).

Also, it's a shame that the base classes such as string and the primitive types do not support some interfaces such as ICharStream (for strings, which could be used for regex etc. to allow using other sources than string instances for pattern matching) or IArithmetic for numeric primitives, so that generic math would be possible. But I guess that all frameworks have some weak points.

Lucero
  • 59,176
  • 9
  • 122
  • 152
  • Just going back through some past questions to make sure I've accepted answers that deserved to be accepted. This one was definitely thoughtful and provided some much-appreciated insight. – Dan Tao Mar 10 '10 at 03:57
  • If IList were defined as inheriting IIndexable, which in turn inherited IReadableByIndex, which in turn inherited IEnumerable, how would that add any grief to implementers? Simply type "Inherits IList" and all the necessary routines would be filled in, and would only have to be defined once each. – supercat Dec 17 '10 at 19:49
  • It would be confusing to need to know 100 different interfaces and the detailed differences between them; not to mention the fact that interface inheritance doesn't work quite as orthogonally as you might wish; e.g. adding a property *setter* in a sub-interface or inheriting the same method from two base interfaces. Google's Go has the better solution here; but that's water under the bridge for .NET. – Eamon Nerbonne Jan 06 '11 at 15:59
  • Having lots of interfaces wouldn't be a problem with the language system as it is, since if interface X derives from Y which contains function foo(), an implementation of X.foo will automatically be regarded as an implementation of Y.foo. Interfaces like IList may have been designed before that was known, however. IMHO, at minimum the covariant, contravariant, and type-independent aspects of each interface, should be split out in likely combinations. The biggest tricky bit would be something like IList.Contains method, which should in some sense be type-independent: ... – supercat Jul 18 '11 at 23:12
  • If an IReadableList is passed to some code that expects an IReadableList and that code wants to know if it contains a particular Zebra, the IReadableList shouldn't reject the request because it's not a Giraffe; it should simply answer "No". – supercat Jul 18 '11 at 23:14
2

Well, the interfaces aren't perfect but a dev can check the IsReadOnly property before calling Add/Remove/Set...

Guillaume
  • 12,824
  • 3
  • 40
  • 48
  • 1
    `IsReadOnly` is pretty much broken … because it doesn’t differentiate between the list itself and its elements (“does not allow … modification of elements”). Hence, most classes which do *not* allow `Add`/`Remove` *still* are not read-only in the sense of `IsReadOnly`. Yeah, this sucks. – Konrad Rudolph Jan 21 '10 at 19:38
  • 1
    IList (non generic) contains a IsFixedSize property. – Guillaume Jan 22 '10 at 10:13
  • yes but `IList` doesn’t inherit from `IList` – you’d need to implement both. Sucks. – Konrad Rudolph Jan 25 '10 at 21:37
  • @KonradRudolph: If `T` is a reference type, an `IList` serves to *identify* a number of `T` instances (as being the first, second, third, etc.) Since the `IList` serves to *identify*, rather than *contain*, instances of `T`, none of the properties of those instances for any part of the `IList`'s state; there's no "ambiguity" as to whether a "read-only" list of a mutable class type `T` restricts mutations to its members. It *CAN'T*, so it doesn't. The real problem is the lack of a property to say whether an `IList` is immutable (immutable implies read-only, but not vice versa). – supercat Nov 19 '12 at 18:58
  • @supercat Not sure I understand this. If I remember correctly, what I said was that there are `IList` implementations which forbid `Add` and `Remove`, yet `IsReadOnly` returns `false` because they provide an element setter. This was based off of some other discussion here which I cannot now find. (Or maybe it was the other way round; the point is that implementations *can* distinguish, based on the existence of a setter for the `this[]` property, whether its elements are re-assignable or not; this is not adequately covered by the `IsReadOnly` property.) – Konrad Rudolph Nov 19 '12 at 19:02
  • @KonradRudolph: I thought by "modification of elements" you were referring to the ability to mutate the objects to which elements refer, rather than simply referring to the existence of the indexed property setter. Your complaint is the omission of `IsFixedSize`? I wonder why Microsoft left that out, since it was present in `IList`? – supercat Nov 19 '12 at 19:07
  • @supercat Precisely, my complaint is the omission of `IsFixedSize`. – Konrad Rudolph Nov 19 '12 at 19:18
  • @KonradRudolph: Gotcha. I suppose that's one of the bigger head-scratchers in .net, since `IList` did include that property, and since the most common "implementation" of `IList` (i.e. a `T[]`) is writable but not resizable. – supercat Nov 19 '12 at 19:25
  • @KonradRudolph: Incidentally, a `T[]` cast to `IList` will return `True` for `IsReadOnly`, even though it will allow storage by index of elements whose type is compatible with the underlying array. Note that if a `String[]` may be cast to an `IList`, an attempt to store a `string` instance will succeed, but an attempt to store any other object instance will throw an `ArrayTypeMismatchException`, even though that's not an exception associated with the interface. – supercat Nov 19 '12 at 20:26
0

The closest you can get is to return IEnumerable< T > and then the client (a.k.a. caller) can call .ToArray() themselves.

Jan Bannister
  • 4,859
  • 8
  • 38
  • 45