Generic method handles IEnumerable differently than generic type
Asked Answered
B

3

10

Please check the following codes segments:

public interface ICountable { }
public class Counter<T>
    where T : ICountable
{
    public int Count(IEnumerable<T> items)
    {
        return 0;
    }

    public int Count(T Item)
    {
        return 0;
    }
}

public class Counter
{
    public int Count<T>(IEnumerable<T> items)
        where T : ICountable
    {
        return 0;
    }

    public int Count<T>(T Item)
        where T : ICountable
    {
        return 0;
    }
}

The two versions of Counter differ only in the specification of the generic parameter. One of them defines as a generic type parameter, the other as a generic argument. Both restrict the method arguments to implement the ICountable interface. I will call them specific and non specific respectively.

Now, I am defining a class that implements the ICountable interface, and a collection of instances:

public class CItem : ICountable { }
var countables = new List<CItem>();

Then, I would like to use both Counter classes on the collection.

var specific = new Counter<CItem>();
var nonspecific = new Counter();

specific.Count(countables);
nonspecific.Count(countables);

The specific counter recognizes that the countables collection should fall into the signature int Count(IEnumerable), but the non specific version does not. I get the error:

The type 'System.Collections.Generic.List<CItem>' cannot be used as type parameter 'T' in the generic type or method 'Counter.Count<T>(T)'. There is no implicit reference conversion from List<CItem>' to ICountable.

It seems that the non specific version uses the wrong signature for the collection.

Why do they behave differently? How can the non specific version be specified in order to behave the same as the other?

Note: I know this example is not realistic. However, I faced this problem in a quite complicate scenario with extension methods. I use these classes for the sake of simplicity

Thanks in advance

Bean answered 16/12, 2015 at 10:44 Comment(3)
if you specify generic type explicitly on like nonspecific.Count<CItem>(countables); that will work tooWindsucking
if you define countables with IEnumerable<CItem> countables = new List<CItem>(); the correct overload is chosen.Yaron
The two Count() method overloads are ambiguous, both could take a List as an argument. However, one of them violates the constraint in the specific case of you passing a List. That's not good enough, if you'd use another type that implements both IEnumerable and ICountable then it is truly ambiguous. C# does not allow you to declare a generic type/method that can randomly fail to compile.Empennage
M
4

The problem with nonspecific class is that compiler doesn't know the type T in compile time that's why it cannot select correct overload for method Count<T>(). However if you set generic type constraints compiler now knows what type to expect...

If you'll comment out your method with signature public int Count<T>(T Item) it'll compile because it'll use method with correct signature (which is public int Count<T>(IEnumerable<T> items))

It'll also compile and run if you help compiler to infer type by casting your List to IEnumerable<CItem> explicitly :

nonspecific.Count(countables as IEnumerable<CItem>);

Have a look at simplified scenario :

    static string A<T>(IEnumerable<T> collection)
    {
        return "method for ienumerable";
    }

    static string A<T>(T item)
    {
        return "method for single element";
    }

    static void Main(string[] args)
    {
        List<int> numbers = new List<int>() { 5, 3, 7 };
        Console.WriteLine(A(numbers));
    }

Output : "method for single element"

Morette answered 16/12, 2015 at 10:54 Comment(2)
Yes, I experienced the same. In my scenario, I have different extension methods for collections and single instances of the same type. I would expect the compiler to select the signature with a collection parameter, when I pass a collection and the signature with an instance parameter, when I pass a signle instance. Is that an unrealistic expectation?Bean
@DanielLeiszen Yes because that's how Generic parameters work in C# Compiler doesn't know type on compile time, only at run-time... However if you set type constraints you 'tell compiler' what type it should expect.Morette
Y
2

If I remember correctly (will try to find a reference in the specification), the T method is chosen because it's an exact match for the type.

The type inference, correctly identifies that both generic methods are applicable, as Count<CItem>(IEnumerable<CItem> items) and Count<List<CItem>>(List<CItem> items). However, the first one loses in the overload resolution, as the second one is more specific. The constraints only come in play after that, so you get a compile time error.

If you declare your countables using

IEnumerable<CItem> countables = new List<CItem>();

then the choice becomes Count<CItem>(IEnumerable<CItem> items) and Count<IEnumerable<CItem>>(IEnumerable<CItem> items) and the first one wins the overload resolution.

Yaron answered 16/12, 2015 at 11:8 Comment(2)
Thanks for the post. I marked Fabjan's solution as answer, since that was the first. But your post also answers the question.Bean
@DanielLeiszen, no problem, it was an interesting question - now I have to dig through the spec to find why it works specifically in the second case.Yaron
M
1

In my opinion, the reason why the compiler thinks that you are calling Counter.Count(T) instead of Counter.Count< T >(IEnumerable< T >) is because the later one requires a conversion from List to IEnumerable. And that has a priority less than using the former signature Counter.Count(T), which result in an error.

I think it's better that you change the method name of the one taking an IEnumerble as the argument into something like CountAll. The some thing .NET framework does for List.Remove and List.RemoveAll. It's a good practice to make your code more specific rather than letting the compiler to do all the decisions.

Macadamia answered 16/12, 2015 at 11:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.