通用重载方法解析问题

7

我有两个带有以下签名的方法:

void Method<T>(T data)
{
}

void Method<T>(IEnumerable<T> data)
{
}

在同一个方法中,它既可以接收单个对象,也可以接收对象列表。如果我尝试将List<'T>传递给它,它会解析为第一个方法,但显然我想要的是第二个方法。我必须使用list.AsEnumerable()才能使其解析为第二个方法。是否有任何方法可以使其无论列表是T[]、IList<'T>、List<'T>、Collection<'T>、IEnumerable<'T>等类型都解析为第二个方法。


第一个方法的意义是什么? - leppie
4
“当然我想要第二个” - 对谁来说是显而易见的?编译器如何区分呢? - Oded
3
根据其重载决策机制的规则,当然是这样。 - Thomas
第二种方法是否会将第一种方法应用于 IEnumerable 中的所有项?如果是这样,请放弃第二种方法,而在调用站点处使用 foreach 循环。 - Thomas
@Vash:Method<U> (U data) 会与 Method<T>(T data) 冲突。 - Jaroslav Jandek
显示剩余4条评论
6个回答

10
最好的解决方案是:一开始就不要去那里。这是一个糟糕的设计。你会注意到,没有一个框架类会这样做。列表有两种方法:Add 和 AddRange。第一个添加单个项,第二个添加一系列项。
这是一个糟糕的设计,因为你正在编写一个自动产生错误的设备。再考虑一个可变列表的例子。
List<object> myqueries = new List<object>();
myqueries.Add(from c in customers select c.Name);
myqueries.Add(from o in orders where o.Amount > 10000.00m select o);

你预期要添加两个查询到查询列表中;如果Add被重载为采用一个序列,那么它将把查询结果添加到列表中,而不是查询本身。你需要能够区分一个查询和它的结果;它们在逻辑上完全不同。
最好的方法是使这些方法有两个不同的名称。
如果你执意要使用这种糟糕的设计,那么,如果你这样做感到疼痛,就不要这样做。重载解析旨在找到最佳匹配。不要试图打击重载解析,以便它执行更差的任务。再次强调,这会令人困惑并导致错误。请使用其他机制来解决问题。例如:
static void Frob(IEnumerable ts) // not generic!
{
    foreach(object t in ts) Frob<object>(t);
}
static void Frob<T>(T t)
{
    if (t is IEnumerable)
        Frob((IEnumerable) t);
    else
        // otherwise, frob a single T.
}

现在无论用户给你什么 - 一个T类型的数组,一个包含T类型列表的数组,或其他任何东西,你最终只会处理单个T类型。
但是,这几乎肯定是一个不好的想法。不要这样做。具有不同语义的两种方法应该有不同的名称。

System.Xml.Linq类确实使用了这种设计。像XElement.Add()这样的方法接受objectobject[]和(在某些情况下)IEnumerable。可以说,添加单个子元素与添加多个子元素的语义是相同的 - 但这样的论点也可以扩展到List.Add()上。总的来说,我同意做不同事情的方法应该有不同的名称 - 我只是认为在某些情况下这是微妙的,并且决定两个操作是否真正不同并不总是显而易见的。但是,在这种情况下,我倾向于赞同您对OP的回复。 - LBushkin

1

这取决于你正在做什么:

var list = new List<Something>();
Method<List<Something>>(list); // Will resolve to the first overload.

或者:

var list = new List<Something>();
Method<Something>(list); // Will resolve to the second overload.

发生这种情况的原因是编译器会选择它能找到的最具体的方法,所以当使用您的通用 Method<T>(T data) 时,它会被编译为 Method<List<Something>>(List<Something> data),这比 IEnumerable<Something> 更具体。

1

重载决策将尝试找到最佳匹配的重载。

IEnumerable<T> 重载的情况下,您确实需要显式转换或使用 IEnumerable<T>,因为那将是最佳匹配。

否则,简单的通用重载将被视为更好的匹配。

要了解更多详细信息,请阅读 Eric Lippert 的 "overload resolution" 博客文章。


0

有时候会出现这样的情况:一个方法可以接受单个对象或者一组对象,这不是很常见,对吧?为什么不只写

void MethodSingle<T>(T data)
{
    Method(new T[] { data });
}

void Method<T>(IEnumerable<T> data)
{
}

我建议更清晰地表达,让编译器和读者都能理解。


+1:在这种情况下最好不要使用重载,这样更清晰。没有无限循环。 - Henrik
@Jaroslav 请解释一下。这段代码编译并按预期运行。请注意,这是原问题中的代码(即原始的Method(T data)不再存在)的替代方案。 - AakashM
抱歉,我错过了“MethodSingle”,看到的是“Method”。无论如何,这仍然没有解决任何问题,相反 - 它使情况变得更糟。您将枚举一个只有一个项目的数组。对于“List<X>”的情况,它将枚举“List<X>[]”,而不是预期的“List<X>”! - Jaroslav Jandek
@Jaroslav 对不起,我不理解你的意思。当你有单个项目时,你调用 "MethodSingle"; 当你有列表时,你调用 "Method"。 - AakashM
可能是方法的名称使它变得糟糕。例如,如果它是AddAddRange,那么它会更有意义=> MethodSingleMethodMultiple。在MethodMultiple中调用MethodSingle(而不是反过来)也更有意义。此外,他甚至可能有不同的逻辑在两种情况下进行操作,这表明名称应该不同。方法的名称确实很重要(这就是我所说的)。 - Jaroslav Jandek

0

问:

有没有办法使其解析到第二个,而不管列表是T[]、IList<'T>、List<'T>、Collection<'T>、IEnumerable<'T>等类型。

T[]不是列表而是数组,所以可能不行。

我不确定这是否有帮助,但您可以尝试创建一个有限制的方法。

public void Method<U,T> (U date) where U : IList<T> { /* ... */ }

它并没有解决任何问题。第一个重载仍然会被调用,因为它是最佳匹配项。 - Jaroslav Jandek

-1
为什么不用这种方式扩展该方法呢:
void Method<T>(T data)
{
    var enumerable = data as IEnumerable<T>;
    if(enumerable != null)
    {
        Method(enumerable);
        return;
    }

    ...
}

对于 List<X>,可以这样做:var enumerable = data as IEnumerable<List<X>> - Jaroslav Jandek

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