53

If I have a method that requires a parameter that,

  • Has a Count property
  • Has an integer indexer (get-only)

What should the type of this parameter be? I would choose IList<T> before .NET 4.5 since there was no other indexable collection interface for this and arrays implement it, which is a big plus.

But .NET 4.5 introduces the new IReadOnlyList<T> interface and I want my method to support that, too. How can I write this method to support both IList<T> and IReadOnlyList<T> without violating the basic principles like DRY?

Edit: Daniel's answer gave me some ideas:

public void Foo<T>(IList<T> list)
    => Foo(list, list.Count, (c, i) => c[i]);

public void Foo<T>(IReadOnlyList<T> list)
    => Foo(list, list.Count, (c, i) => c[i]);

private void Foo<TList, TItem>(
    TList list, int count, Func<TList, int, TItem> indexer)
    where TList : IEnumerable<TItem>
{
    // Stuff
}

Edit 2: Or I could just accept an IReadOnlyList<T> and provide a helper like this:

public static class CollectionEx
{
    public static IReadOnlyList<T> AsReadOnly<T>(this IList<T> list)
    {
        if (list == null)
            throw new ArgumentNullException(nameof(list));

        return list as IReadOnlyList<T> ?? new ReadOnlyWrapper<T>(list);
    }

    private sealed class ReadOnlyWrapper<T> : IReadOnlyList<T>
    {
        private readonly IList<T> _list;

        public ReadOnlyWrapper(IList<T> list) => _list = list;

        public int Count => _list.Count;

        public T this[int index] => _list[index];

        public IEnumerator<T> GetEnumerator() => _list.GetEnumerator();

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }
}

Then I could call it like Foo(list.AsReadOnly())


Edit 3: Arrays implement both IList<T> and IReadOnlyList<T>, so does the List<T> class. This makes it pretty rare to find a class that implements IList<T> but not IReadOnlyList<T>.

3

4 Answers 4

38

You are out of luck here. IList<T> doesn't implement IReadOnlyList<T>. List<T> does implement both interfaces, but I think that's not what you want.

However, you can use LINQ:

  • The Count() extension method internally checks whether the instance in fact is a collection and then uses the Count property.
  • The ElementAt() extension method internally checks whether the instance in fact is a list and than uses the indexer.
13
  • 2
    So the answer would be changing the method in a way that it accepts an IEnumerable<T>, which is supported by any object that implements wheter IList<T> or IReadOnlyList<T>. Oct 11, 2012 at 11:25
  • 2
    If it is important to only allow IList<T> and IReadOnlyList<T> as input, I would create two public overloads, one for IList<T> and one for IReadOnlyList<T>, and create a private overload with IEnumerable<T>. Otherwise I would just create one public version with IEnumerable<T>. Oct 11, 2012 at 11:29
  • Thank you, I decided not to use LINQ but it gave me some ideas. I updated the question with the solution I'm going to use. Oct 11, 2012 at 12:00
  • 5
    @JeppeStigNielsen: While any reasonable list type which is implemented in the future should implement both IList<T> and IReadOnlyList<T>, many good classes which implement IList<T> were written before IReadOnlyList<T> existed. I have long wished for a means by which IList<T> could be made to inherit from a covariant read-only interface with an indexed getter, with the run-time--if necessary--automatically creating a read-only indexer which would wrap the read-write one, but because no such means exists it will for now be necessary to accept either IList<T> or IReadOnlyList<T>.
    – supercat
    Nov 30, 2012 at 16:26
  • 1
    LINQ extension methods only check if your instance implements IList. So if you have an instance of a class which implements only IReadOnlyList and not IList, you're out of luck and LINQ will dumbly iterate through the whole collection (tested on .net 4.7, and checked sources of .net core). Luckily it's quite uncommon case but can happen with custom implementations.
    – Elephantik
    Feb 28, 2018 at 13:31
5

Since IList<T> and IReadOnlyList<T> do not share any useful "ancestor", and if you don't want your method to accept any other type of parameter, the only thing you can do is provide two overloads.

If you decide that reusing codes is a top priority then you could have these overloads forward the call to a private method that accepts IEnumerable<T> and uses LINQ in the manner Daniel suggests, in effect letting LINQ do the normalization at runtime.

However IMHO it would probably be better to just copy/paste the code once and just keep two independent overloads that differ on just the type of argument; I don't believe that micro-architecture of this scale offers anything tangible, and on the other hand it requires non-obvious maneuvers and is slower.

12
  • 4
    I strongly disagree with copy/paste, even on this level. Oct 11, 2012 at 11:27
  • 4
    @DanielHilgarth: I 'm a believer in "refactoring on seeing a third copy". Dismissing copy/paste unconditionally does not really sound like good engineering.
    – Jon
    Oct 11, 2012 at 11:30
  • 4
    The only difference is the input type, everything else is completly the same. Using copy and paste you open up yourself to subtle bugs. You might fix one version but forget the other. I don't see why it is not good engineering to prevent something like this from the beginning. What is the advantage of your approach to allow two copies of the same code? What is the disadvantage of my approach to not do it? Oct 11, 2012 at 11:33
  • 2
    @DanielHilgarth: Sure, but the requirements mentioned in the answer hint that it isn't going to be that bad (how complicated can you get with only Count and an indexer?). If we 're talking about 5 lines of code I would copy/paste and document. Otherwise some other solution would probably be preferable. I 'm just saying that outright banning copy/paste is not a very good idea.
    – Jon
    Oct 11, 2012 at 11:38
  • 1
    First: I never talked about "outright banning copy/paste", that is your false interpretation of my strong disagreement with using it. For me it is a "tool" on the same level as goto: In rare cases, it has its use. Second: You still didn't bring forward any reasons for why you would copy and paste with all the problems that come with it. You even write you would document it. What reason for the copy would this documentation contain when there is an easy fix available? Oct 11, 2012 at 11:42
4

If you're more concerned with maintaining the principal of DRY over performance, you could use dynamic, like so:

public void Do<T>(IList<T> collection)
{
    DoInternal(collection, collection.Count, i => collection[i]);
}
public void Do<T>(IReadOnlyList<T> collection)
{
    DoInternal(collection, collection.Count, i => collection[i]);
}

private void DoInternal(dynamic collection, int count, Func<int, T> indexer)
{
    // Get the count.
    int count = collection.Count;
}

However, I can't say in good faith that I'd recommend this as the pitfalls are too great:

  • Every call on collection in DoInternal will be resolved at run time. You lose type safety, compile-time checks, etc.
  • Performance degradation (while not severe, for the singular case, but can be when aggregated) will occur

Your helper suggestion is the most useful, but I think you should flip it around; given that the IReadOnlyList<T> interface was introduced in .NET 4.5, many API's don't have support for it, but have support for the IList<T> interface.

That said, you should create an AsList wrapper, which takes an IReadOnlyList<T> and returns a wrapper in an IList<T> implementation.

However, if you want to emphasize on your API that you are taking an IReadOnlyList<T> (to emphasize the fact that you aren't mutating the data), then the AsReadOnlyList extension that you have now would be more appropriate, but I'd make the following optimization to AsReadOnly:

public static IReadOnlyList<T> AsReadOnly<T>(this IList<T> collection)
{
    if (collection == null)
        throw new ArgumentNullException("collection");

    // Type-sniff, no need to create a wrapper when collection
    // is an IReadOnlyList<T> *already*.
    IReadOnlyList<T> list = collection as IReadOnlyList<T>;

    // If not null, return that.
    if (list != null) return list;

    // Wrap.
    return new ReadOnlyWrapper<T>(collection);
}
7
  • +1 for insight and thanks for suggestion, I went with IList<T> to IReadOnlyList<T> because it felt wrong to throw NotSupportedExceptions from explicitly implemented interfaces and to my suprise, it accepts arrays too. (It's odd since Array doesn't implement it, IReadOnlyList<T> doesn't even have a non-generic version like IList) I wonder what do you think about my first sample though. I was thinking about using dynamic then but as you have said, it could hurt the performance (I'll call this method a lot in a short time)... Oct 12, 2012 at 6:40
  • ...So I assumed the Count would never change (thread-safety is not intended) and calling indexer's get accessor using a delegate would be nearly as fast as calling it directly. So I guess the performence difference in those approaches is between creating an instance of ReadOnlyWrapper<T> and creating an instance of Func<int, T> everytime it is called (And it sounds like micro-optimization). Oct 12, 2012 at 6:41
  • 1
    @ŞafakGür ... I've also updated the answer with an optimization on your AsReadOnlyList implementation. It's slight, but doesn't hurt. No need to wrap something with IReadOnlyList<T> when it already is a IReadOnlyList<T>. This kind of type-sniffing is done all the time in LINQ.
    – casperOne
    Oct 12, 2012 at 11:44
  • 1
    Thanks for the trick, I'll use that. And yes arrays implement IList<T> but oddly, new int[0] is IReadOnlyList<int> returns true. I can use arrays to call methods that accept IReadOnlyList<T>. Oct 12, 2012 at 12:20
  • 3
    Yes, there's no need for all this trouble. Just make the method take an IReadOnlyList<T>. Any class that implements IList<T> will also implement IReadOnlyList<T>. This includes BCL types like T[] and List<T>, and should include your own list types (if you have written any). Nov 29, 2012 at 23:59
0

What you need is the IReadOnlyCollection<T> available in .Net 4.5 which is essentially an IEnumerable<T> which has Count as the property but if you need indexing as well then you need IReadOnlyList<T> which would also give an indexer.

I don't know about you but I think this interface is a must have that had been missing for a very long time.

3
  • I think this interface is a must have- It is a class, not an interface. It implements both IList<T> and IReadOnlyList<T>. Jul 1, 2014 at 7:17
  • 1
    What I meant was the IReadOnlyCollection I somehow missed the "I"
    – MaYaN
    Jul 1, 2014 at 11:37
  • 3
    IReadOnlyCollection<T> has the exact same problem as IReadonlyList<T>: ICollection<T> does not implement it.
    – binki
    Apr 17, 2015 at 1:46

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.