32

I am working with a IReadOnlyCollection of objects.

Now I'm a bit surprised, because I can use linq extension method ElementAt(). But I don't have access to IndexOf().

This to me looks a bit illogical: I can get the element at a given position, but I cannot get the position of that very same element.

Is there a specific reason for it?

I've already read -> How to get the index of an element in an IEnumerable? and I'm not totally happy with the response.

Community
  • 1
  • 1
Lorenzo Santoro
  • 464
  • 1
  • 6
  • 16

5 Answers5

52

IReadOnlyCollection is a collection, not a list, so strictly speaking, it should not even have ElementAt(). This method is defined in IEnumerable as a convenience, and IReadOnlyCollection has it because it inherits it from IEnumerable. If you look at the source code, it checks whether the IEnumerable is in fact an IList, and if so it returns the element at the requested index, otherwise it proceeds to do a linear traversal of the IEnumerable until the requested index, which is inefficient.

So, you might ask why IEnumerable has an ElementAt() but not IndexOf(), but I do not find this question very interesting, because it should not have either of these methods. An IEnumerable is not supposed to be indexable.

Now, a very interesting question is why IReadOnlyList has no IndexOf() either.

IReadOnlyList<T> has no IndexOf() for no good reason whatsoever.

If you really want to find a reason to mention, then the reason is historical:

Back in the mid-nineties when C# was laid down, people had not quite started to realize the benefits of immutability and readonlyness, so the IList<T> interface that they baked into the language was, unfortunately, mutable.

The right thing would have been to come up with IReadOnlyList<T> as the base interface, and make IList<T> extend it, adding mutation methods only, but that's not what happened.

IReadOnlyList<T> was invented a considerable time after IList<T>, and by that time it was too late to redefine IList<T> and make it extend IReadOnlyList<T>. So, IReadOnlyList<T> was built from scratch.

They could not make IReadOnlyList<T> extend IList<T>, because then it would have inherited the mutation methods, so they based it on IReadOnlyCollection<T> and IEnumerable<T> instead. They added the this[i] indexer, but then they either forgot to add other methods like IndexOf(), or they intentionally omitted them since they can be implemented as extension methods, thus keeping the interface simpler. But they did not provide any such extension methods.

So, here, is an extension method that adds IndexOf() to IReadOnlyList<T>:

using Collections = System.Collections.Generic;

    public static int IndexOf<T>( this Collections.IReadOnlyList<T> self, T elementToFind )
    {
        int i = 0;
        foreach( T element in self )
        {
            if( Equals( element, elementToFind ) )
                return i;
            i++;
        }
        return -1;
    }

Be aware of the fact that this extension method is not as powerful as a method built into the interface would be. For example, if you are implementing a collection which expects an IEqualityComparer<T> as a construction (or otherwise separate) parameter, this extension method will be blissfully unaware of it, and this will of course lead to bugs. (Thanks to Grx70 for pointing this out in the comments.)

Mike Nakis
  • 56,297
  • 11
  • 110
  • 142
  • 8
    It always amuses me how answers on stackoverflow are trying to provide justification for why a certain API is in a certain way or another, as if the API is always right, as if it has been built with perfect wisdom, so some justification must always be there to be found and to be explained to the presumed novice who asked the question. – Mike Nakis Feb 20 '20 at 10:19
  • 5
    (Who is usually not a novice at all, and they are usually asking the question because they see there is a problem with the API, and pointing a problem by asking a question is always considered more polite than ranting about it, which is what I generally prefer to do.) – Mike Nakis Feb 20 '20 at 10:19
  • 3
    There are way to many .NET APIs that are messed up and overly complicated for historical reasons. I would love to see a clean cut with any following major release. Compatibility is nice, but not at the price of dragging along design errors from the 90s – Toxantron Jun 16 '20 at 08:37
  • I agree with the main proposal of this answer, that there's no good reason for the lack of `int IndexOf(T)` method on `IReadOnlyList`. The proposed workaround however has a flaw in it, and I disagree with the statement that this method doesn't have to be included in the interface, because it can be "replaced" with an extension method. Imagine a collection that uses arbitrary `IEqualityComparer`, and you might end up with this extension returning `-1` while `Contains(T)` returns `true` (or the other way around), which will likely lead into trouble. – Grx70 Aug 09 '20 at 20:17
  • @Grx70 I am not sure I understand what is the flaw that you detected. Are you referring to the fact that my function is lacking an overload that accepts an `IEqualityComparer`? Are you referring to the fact that it does not appear to be making use of `IEquatable`? I could add those, but I think they are best left as an exercise to the reader. Programmers who are advanced enough to make actual use of these interfaces would find that trivial. Is there anything else? – Mike Nakis Aug 10 '20 at 11:16
  • 1
    @MikeNakis Let me give you an example. I actually stumbled upon this question while implementing custom ordered set (an `ISet` that supports indexing, i.e. also implements `IReadOnlyList`). Obviously, my class accepts arbitrary `IEqualityComparer`. Let's say my class is internal, and so is the comparer I use, and I only expose it to you as `IReadOnlyList` via dependency injection. Then it's impossible to write an extension method that would "always" return correct results, because you have no way of accessing the comparer, or even knowing that I use a custom one. – Grx70 Aug 10 '20 at 12:44
  • @MikeNakis My point is that `IReadOnlyList` itself does not provide enough information to implement an extension method that would cover all possible scenarios. Sure, you can accept additional parameters, but that precisely illustrates that you need some additional information that `IReadOnlyList` does not provide. – Grx70 Aug 10 '20 at 12:48
  • @Grx70 yes, I see. I amended my answer to indicate the caveat that you are pointing out. So what would be the solution to your situation? I suppose you would have to expose `IList` instead of `IReadOnlyList` so that there is an `IndexOf()` in the interface which you can implement to make use of your `IEqualityComparer`, right? – Mike Nakis Aug 10 '20 at 12:53
  • @MikeNakis I guess there isn't a single right way to approach this. You could either extend `IReadOnlyList` to include `IndexOf(T)`, or implement `IList` with `IsReadOnly` returning `true` and all mutating methods throwing an exception. My goal however was only to provide an argument for why `IndexOf(T)` **should** be part of `IReadOnlyList`. – Grx70 Aug 10 '20 at 13:04
  • I missed this nuance the first time myself, but the original question was about `IReadOnlyCollection`, and not `IReadOnlyList`. Lists extend collections, not the other way around, and a collection does indeed lack indexed access - by design, not by accident. This answer is probably accurate for read-only lists; it's illogical that they have indexed access but no `IndexOf`. However, it's not answering the same question. `IReadOnlyList` is a much newer concept than `IReadOnlyCollection` - I don't think the former even existed all the way back in 2016. – Aaronaught Jun 07 '21 at 17:43
  • @Aaronaught yes, now that I am looking at it again, you are right. And it is not like the question has been edited. So I missed it too. I amended my answer. – Mike Nakis Oct 20 '21 at 06:35
  • Inheriting IList from IReadOnlyList would violate the Liskov Substitution Principle. Imagine code that accepts two IReadOnlyList instances, but one happens to be a more-derived IList with mutation methods: while the method is iterating the immutable list, another thread is busily mutating the IList. This is easy enough to avoid when you control all code paths, but what if you're consuming a library where the library authors have assumed the IReadOnlyList is immutable? It's better to have a base class without mutation methods, then separately derive mutable and immutable versions. – user1969177 Jul 18 '22 at 16:16
  • 1
    @user1969177 You cannot assume that an interface is immutable. An interface can at best be "unmodifiable" or "readonly". It is only a class that can be immutable. – Mike Nakis Jul 18 '22 at 16:40
  • @MikeNakis A class that implements derived-type IList would be unsuitable for a parameter of type IReadOnlyList, even if covariance rules allow it, simply because the parameter type implies a behavior that IList contradicts. Really the problem is the name, since you should not give a name to an interface for behavior it does not implement. For example, you would be better to call the base interface an IList and its mutable, derived interface an IMutableList. You could also derive IReadOnlyList from IList and override any mutation methods. And give it IndexOf(). – user1969177 Jul 20 '22 at 00:28
  • 1
    @user1969177: I disagree - "read only" is something very different from "immutable". If something is "read only", it means *you* cannot (directly) change it. It does not mean that it cannot change on its own. As such, assuming that an `IReadOnlyList` is immutable is simply an error. – O. R. Mapper Jan 27 '23 at 21:54
  • 1
    Indeed, not all read-only lists are immutable, but to expect someone to make the distinction (or the author's intention) is a bit.. optimistic, at best. – user1969177 Jan 30 '23 at 17:44
4

IndexOf is a method defined on List, whereas IReadOnlyCollection inherits just IEnumerable.

This is because IEnumerable is just for iterating entities. However an index doesn't apply to this concept, because the order is arbitrary and is not guaranteed to be identical between calls to IEnumerable. Furthermore the interface simply states that you can iterate a collection, whereas List states you can perform adding and removing also.

The ElementAt method sure does exactly this. However I won't use it as it reiterates the whole enumeration to find one single element. Better use First or just a list-based approach.

Anyway the API design seems odd to me as it allows an (inefficient) approach on getting an element at n-th position but does not allow to get the index of an arbitrary element which would be the same inefficient search leading to up to n iterations. I'd agree with Ian on either both (which I wouldn't recommend) or neither.

neonblitzer
  • 124
  • 1
  • 2
  • 10
MakePeaceGreatAgain
  • 35,491
  • 6
  • 60
  • 111
  • 2
    I totally agree with you with the fact that Enumerables are not meant to be accessed by index, but still I can use ElementAt(). To me it's a bit silly I get the element at a given position, but not the position of the element. It's more of a philosophical point :) – Lorenzo Santoro May 25 '16 at 08:46
  • 1
    This is indeed a weird method. I assume it simply exists because anyone needed this future, however until now there wasn´t high demad towards an `IndexOf`-method. – MakePeaceGreatAgain May 25 '16 at 08:48
4

It is because the IReadOnlyCollection (which implements IEnumerable) does not necessarily implement indexing, which often required when you want to numerically order a List. IndexOf is from IList.

Think of a collection without index like Dictionary for example, there is no concept of numeric index in Dictionary. In Dictionary, the order is not guaranteed, only one to one relation between key and value. Thus, collection does not necessarily imply numeric indexing.

Another reason is because IEnumerable is not really two ways traffic. Think of it this way: IEnumerable may enumerate the items x times as you specify and find the element at x (that is, ElementAt), but it cannot efficiently know if any of its element is located in which index (that is, IndexOf).

But yes, it is still pretty weird even you think it this way as would expect it to have either both ElementAt and IndexOf or none.

Ian
  • 30,182
  • 19
  • 69
  • 107
3

IReadOnlyCollection<T> has ElementAt<T>() because it is an extension to IEnumerable<T>, which has that method. ElementAt<T>() iterates over the IEnumerable<T> a specified number of iterations and returns value as that position.

IReadOnlyCollection<T> lacks IndexOf<T>() because, as an IEnumerable<T>, it does not have any specified order and thus the concept of an index does not apply. Nor does IReadOnlyCollection<T> add any concept of order.

I would recommend IReadOnlyList<T> when you want an indexable version of IReadOnlyCollection<T>. This allows you to correctly represent an unchangeable collection of objects with an index.

Licht
  • 1,079
  • 1
  • 12
  • 27
  • "Don't use it" isn't really an answer, but there is some value in what you say. Maybe you could make this look more like an answer by indicating that `ICollection` doesn't have an `IndexOf()` method either, whereas `IList` has. So it's merely a question of semantics, i.e. a collection is seen as an order-less bag (*probably*, but that's always the problem with these "why" questions: who knows?). – Gert Arnold Jan 29 '19 at 20:47
  • @GertArnold You're right. I was relying on the user reading my comment last. Now that you've pointed it out I can see the problem with that. I will edit my answer to be more complete. – Licht Jan 29 '19 at 20:49
  • 6
    The main problem is that `IReadOnlyList` *also* doesn't have an `IndexOf()` function implemented. `IReadOnlyCollection` not having it isn't that big a deal, since it doesn't have indexing semantics, but the entire point of using a list is that it's supposed to be ordered and indexable. `List` and `IList` both have it, but `IReadOnlyList` does not. It's rather frustrating. – dsmith Jul 12 '19 at 03:04
2

This extension method is almost the same as Mike's. The only difference is that it includes a predicate, so you can use it like this: var index = list.IndexOf(obj => obj.Id == id)

public static int IndexOf<T>(this IReadOnlyList<T> self, Func<T, bool> predicate)
{
    for (int i = 0; i < self.Count; i++)
    {
        if (predicate(self[i]))
            return i;
    }

    return -1;
}
mfluehr
  • 2,832
  • 2
  • 23
  • 31
  • 2
    Hi, welcome to StackOverflow! Thank you for contributing. Please look into Mike Nakis answer and consider if your answer is kind of a duplicate. If not please edit your answer and provide a little extra explanation and a code example calling your function, especially concerning the predicate parameter. Thank you! – fose Jul 10 '20 at 14:23
  • It's almost the same as Mike's. The only difference is it's using predicate, so you can use it like this: var index = list.IndexOf(obj => obj.Id == id) – George Tachev Jul 14 '20 at 19:45