通用方法处理IEnumerable与通用类型不同

10
请检查以下代码段:
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;
    }
}

Counter的两个版本仅在通用参数的规定方面存在差异。其中一个将其定义为通用类型参数,另一个将其定义为通用参数。两个版本都将方法参数限制为实现ICountable接口的对象。我将它们分别称为具体非具体。

现在,我正在定义一个实现ICountable接口的类和一组实例:

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

接下来,我想在集合上同时使用两个计数器类。

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

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

具体的计数器识别到 可计数 集合应该属于签名 int Count(IEnumerable),但非特定版本却不是这样。我得到了错误信息:

类型 'System.Collections.Generic.List<CItem>' 不能用作泛型类型或方法 'Counter.Count<T>(T)' 中的类型参数 'T'。没有从 List<CItem>ICountable 的隐式引用转换。

看起来非特定版本使用了错误的集合签名。
为什么它们的行为不同? 如何指定非特定版本以使其与其他版本行为相同? 注意: 我知道这个例子不现实。然而,在一个相当复杂的场景中,我遇到了这个问题与扩展方法。我使用这些类以简化说明。 谢谢您提前。

2
如果你在像 nonspecific.Count<CItem>(countables); 这样的地方明确指定了泛型类型,那也是可以的。 - farid bekran
1
如果您使用IEnumerable<CItem> countables = new List<CItem>();定义了countables,则会选择正确的重载。 - SWeko
1
两个Count()方法重载是有歧义的,因为它们都可以接受List作为参数。然而,在特定情况下,其中一个违反了约束条件。如果您使用另一种实现IEnumerable和ICountable的类型,则真正存在歧义。C#不允许您声明一个可能随机无法编译的通用类型/方法。 - Hans Passant
3个回答

4
使用非特定类的问题在于编译器不知道类型T在编译时是什么,因此无法为方法Count<T>()选择正确的重载方法。但是,如果您设置通用类型约束,编译器现在知道可以预期的类型...
如果您注释带有签名public int Count<T>(T Item) 的方法,它将编译,因为它将使用具有正确签名的方法(即public int Count<T>(IEnumerable<T> items))。
如果您将List显式转换为IEnumerable<CItem>,它也将编译并运行,以帮助编译器推断类型:
nonspecific.Count(countables as IEnumerable<CItem>);

请看以下简化场景:
    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));
    }

输出: "单个元素的方法"

是的,我也有同样的经历。在我的情况下,我有不同的扩展方法用于相同类型的集合和单个实例。当我传递一个集合时,我希望编译器选择具有集合参数的签名,当我传递单个实例时,选择具有实例参数的签名。这是一个不现实的期望吗? - Daniel Leiszen
@DanielLeiszen 是的,因为这就是 C# 中泛型参数的工作方式。编译器在编译时不知道类型,只有在运行时才知道。但是,如果设置类型约束,你会‘告诉编译器’它应该期望什么类型。 - Fabjan

2
如果我没记错的话(我会在规范中找到参考资料),选择“T”方法是因为它与类型完全匹配。
类型推断正确地确定两个通用方法都适用,如Count(IEnumerable items)和Count>(List items)。但是,第一个方法在重载决议中失去了,因为第二个方法更具体。约束只在此之后发挥作用,因此您会收到编译时错误。
如果您使用声明countables
IEnumerable<CItem> countables = new List<CItem>();

那么选择就变成了 Count<CItem>(IEnumerable<CItem> items)Count<IEnumerable<CItem>>(IEnumerable<CItem> items),第一个函数胜出。


1
感谢您的帖子。我将Fabjan的解决方案标记为答案,因为那是第一个。但是您的帖子也回答了这个问题。 - Daniel Leiszen
@DanielLeiszen,没问题,这是一个有趣的问题-现在我必须仔细研究规范,找出为什么它特别适用于第二种情况。 - SWeko

1
在我看来,编译器认为你调用的是Counter.Count(T),而不是Counter.Count< T >(IEnumerable< T >),原因是后者需要将List转换为IEnumerable。这个优先级低于使用前一个签名Counter.Count(T),这会导致错误。
我认为最好将接受IEnumerable作为参数的方法名称更改为CountAll之类的名称。.NET框架为List.Remove和List.RemoveAll所做的事情就是如此。让你的代码更加具体而不是让编译器做所有的决定是一种很好的实践。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接