8

I have a method which I'd like to take all list-like objects in my solution. Before .NET 4.5, this was simple:

public static T Method<T>(IList<T> list)
{
    // elided
}

However, .NET 4.5 introduced IReadOnlyList<T>, which this method should also apply to.

I can't just change the signature to take an IReadOnlyList<T>, as there are places where I apply the method to something specifically typed as an IList<T>.

The algorithm can't run on IEnumerable<T>, and it's used too frequently (and with too large objects) to take an IEnumerable<T> and create a new List<T> on every call.

I've tried adding an overload:

public static T Method<T>(IReadOnlyList<T> list)
{
    // elided
}

... but this won't compile for anything which implements both interfaces (T[], List<T>, and numerous other types), as the compiler can't determine which method to use (particularly annoying as they have the same body, so it doesn't matter).

I don't want to have to add overloads of Method which take T[], and List<T>, and every other type which implements both interfaces.

How should I accomplish this?

Simon W
  • 91
  • 1
  • 7
  • 2
    use `IEnumerable`? or `ICollection` if you want the ability to specify the collections should support add/remove operations – Ric Mar 26 '18 at 10:21
  • I don't think you'll achieve what you need unless you resort to using `IEnumerable` – Jim Mar 26 '18 at 10:21
  • 2
    Possible duplicate of [IList and IReadOnlyList](https://stackoverflow.com/questions/12838122/ilistt-and-ireadonlylistt) -- not an _exact_ dupe of the goal, but same underlying issue and tons of work-arounds and ideas in the answers. – McGuireV10 Mar 26 '18 at 10:23
  • Ric and Jim - I've edited the question to explain why that won't work. Sorry I forgot to mention it, thanks for pointing it out. – Simon W Mar 26 '18 at 10:29
  • @SimonW: Sorry, but you're out of luck here. As long as `IList<>` doesn't implement `IReadOnlyList<>`, but all implementations implementing both interfaces you are just in trouble. – Oliver Mar 26 '18 at 10:37
  • @mjwills I think that might be the best bet, although there are a lot of `IList` in the solution so it'd be a big job. – Simon W Mar 26 '18 at 10:37
  • 2
    Be lucky if all is in one solution. In that case, just search replace and compile. Fix all errors. Done. – Oliver Mar 26 '18 at 10:38
  • 1
    You can accept `IEnumerable` BUT check if what has been passed implements `IList` or `IReadOnlyList` or whatever other interfaces you might need. If it does - do whatever you need using `IList` or `IReadOnlyList`. If not - construct new `List` from `IEnumerable` and do what you need with it. That way you won't have perfomance penalty from constructing new list from `IEnumerable` when it's not necessary. – Evk Mar 26 '18 at 10:55
  • Since you won't actually find an satisfying answer with the information provided, I would be very interested in the details of the algorithm and see if it really cannot be refactored to take an `IEnumerable`... – nozzleman Mar 26 '18 at 11:03

4 Answers4

3

This might be one of those occasions where actually checking the runtime type is useful:

public static T Method<T>(IEnumerable<T> source)
{
    if (source is IList<T> list)
        return Method(list);

    if (source is IReadOnlyList<T> readOnly)
        return Method(readOnly);

    return Method(source.ToList() as IList<T>);
}

private static T Method<T>(IReadOnlyList<T> list) { ... }
private static T Method<T>(IList<T> list) { ... }

You still have to duplicate code in the sense that you need seperate implementations for IList and IReadOnlyList because there is no common interface you can leverage, but you at least avoid the ambigous call issue.

Community
  • 1
  • 1
InBetween
  • 32,319
  • 3
  • 50
  • 90
  • I like your approach. I have a similar problem for a binary search (lower bound) extension method. However, the point is that binary search uses O(log(n)) instead of O(n), so calling .ToList() kind of cancels the functionality. Do you have a recommendation for that case? I thought about throwing a NotSupportedException. – pschill Dec 15 '22 at 15:56
2

Your likely best bet is to do a global search and replace of IList to IReadOnlyList. If there are no compiler errors then you should be fine.

You should only receive compiler errors if you are using IList.Add - which is foolhardy anyway, since arrays don't support Add.

mjwills
  • 23,389
  • 6
  • 40
  • 63
  • The problem is that `IList` does not implement directly or indirectly `IReadOnlyList` so this solution is not valid. [IList](https://msdn.microsoft.com/en-us/library/5y536ey6(v=vs.110).aspx) – InBetween Mar 26 '18 at 11:58
  • 1
    Yep, I am aware of that - thanks for mentioning it @InBetween. But given the OP appears to be using inbuilt types (`List`, arrays etc) - they **do** implement both interfaces. You are right in the more general sense - this solution won't work for _everybody_, since they may be using a type that implements `IList` but not `IReadOnlyList`. Nonetheless, even in those cases, the compiler should pick up the issue instantly (hence my `compiler errors` comment in my answer). – mjwills Mar 26 '18 at 12:03
  • ah..sorry. I falsely thought `List` didn't implement `IReadOnlyList`. Yeah this could be a lesser evil, but it all depends on how its used. – InBetween Mar 26 '18 at 12:09
0

Can you change the code of Method calling? What if you create a method like this:

    public static T1 Method<T1, T2>(T2 list) where T2 : IList<T1>, IReadOnlyList<T1>
    {
        return default(T1);
    }

In this case the calls look like this:

List<string> listA = new List<String>();
ReadOnlyCollection<string> listB = listA.AsReadOnly();

string outVar1 = Method<string, List<string>>(listA);
string outVar2 = Method<string, ReadOnlyCollection<string>>(listB);

Another way to create two extension methods for IList and IReadOnlyList this way:

    public static T Test<T>(this IList<T> source)
    {
        return default(T);
    }

    public static T Test<T>(this IReadOnlyList<T> source)
    {
        return default(T);
    }

And call them like this:

    string outVar1 = (listA as IReadOnlyList<string>).Test();
    string outVar2 = (listB as IList<string>).Test();
-1

Maybe your best solution is to look into why your algorithm can't run on an IEnumerable and change that. Are you using IList<T> or IReadOnlyList<T> -specific members that you could replace with members available in IEnumerable<T>? Eg:

// instead of
int c = list.Count;

// use
int c = list.Count();

EDIT: ignore the nonsense below. I am leaving it so that the comments continue to make sense.

You should not implement both IList<T> and IReadOnlyList<T> in any class. The only additional members in the IList specification are for writing to the list. You would not need to do that if your list is read only. I think you need to change any classes that implement both so that the correct method can be selected when using them.

However, As all members of IReadOnlyList<T> are included in IList<T> (along with those derived from IReadOnlyCollection<T>) I wonder if the IList<T> in .Net should actually be changed so that it inherits the IReadOnlyList<T> interface rather than duplicating the members. Not that that helps you now.

Wai Ha Lee
  • 8,598
  • 83
  • 57
  • 92
Steve Harris
  • 5,014
  • 1
  • 10
  • 25
  • `You should not implement both IList and IReadOnlyList in any class.` Does `List` implement both of them? – mjwills Mar 26 '18 at 10:46
  • Yes it does and I cannot imagine why! – Steve Harris Mar 26 '18 at 10:48
  • 2
    There are many MSFT classes which implement both `IList` and `IReadOnlyList`, including `T[]` and `List`. `IReadOnlyList` couldn't inherit from `IList` when it was introduced since that would have been a breaking change; see [here](https://stackoverflow.com/a/35940240/5985508). – Simon W Mar 26 '18 at 10:50
  • 1
    @SteveHarris "I wonder if the `IList` in .Net should actually be changed so that it inherits the `IReadOnlyList` interface rather than duplicating the members." -- [Yes, but that was impossible without breaking backwards compatibility.](https://stackoverflow.com/q/35938995) –  Mar 26 '18 at 10:52
  • 2
    I see. It's quite a complicated matter then. So really, a time machine is required to solve that problem. – Steve Harris Mar 26 '18 at 10:56