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.
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 late 1990s to early 2000s 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 way they originally designed things was problematic, because they made arrays implement IList
, but IList
exposes Add()
, Insert()
, Remove()
, and Clear()
methods, which, if invoked on an IList
backed by an array, will throw an exception at your face.)
The right thing would have been to come up with IReadOnlyList<T>
as the base interface, make arrays implement IReadOnlyList<T>
instead of IList<T>
, and then make IList<T>
extend IReadOnlyList<T>
, adding mutation methods only. Unfortunately, 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 IndexOf()
, or they intentionally omitted it, so as to keep the interface covariant, as mentioned by user Spi in another, now deleted, answer. IndexOf()
could have been implemented as an extension method, but they did not provide any such extension method.
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.)