为什么C#中的重载决策不能在Func<T, T>和Action<T>之间工作?

12

因此,对于IEnumerable而言,一个相当常见的扩展方法是Run:

public static IEnumerable<T> Run<T>(this IEnumerable<T> source, Action<T> action)
{
    foreach (var item in source)
    {
        action(item);
        yield return item;
    }
}

当我尝试使用它来添加DbSet时,例如:DbSet.Add:

invoice.Items.Run(db.InvoiceItems.Add);
// NB: Add method signature is
// public T Add(T item) { ... }

...编译器会抱怨它有错误的返回类型,因为它期望一个void方法。所以,在Run方法中添加一个重载,它接受一个Func而不是Action:

public static IEnumerable<T> Run<T>(this IEnumerable<T> source, Func<T, T> action)
{
    return source.Select(action).ToList().AsEnumerable();
}

现在编译器抱怨说“调用在以下方法之间存在歧义……”。那么我的问题是,当Action重载的Run方法对于该方法组无效时,如何使其造成歧义?

db.InvoiceItems.Add 的签名是什么? - leppie
简短回答:x => x.ToString() 这个 lambda 表达式应该只是调用 ToString 方法还是调用 ToString 方法并返回其结果?换句话说,这个 lambda 表达式应该被处理为一个 func 还是一个 action?编译器无法为您做出这个决定,因此会出现错误。 - Polity
@Polity 但这里没有lambda。将方法组转换为委托永远不会将返回值为something的方法更改为返回void的委托。 - svick
@svick - Lambda表达式在.NET生态系统中是一种奇怪的存在。它们仅依赖于声明的类型(因此不允许使用var)。将Lambda表达式分配给一个Action时,它根本不返回任何值。当重载决策必须在Action和Func之间进行选择时,这变得非常棘手。现在我假设*编译器会提前检测到这些问题并生成错误信息。 - Polity
1
@Polity 我相信Eric Lippert对roken链接的问题的回答非常好地描述了这个问题。 - svick
4个回答

5
这个问题已经被Eric和Jon在回答这个问题时解释过了。简而言之,这是C#编译器的工作方式;具体来说,在处理方法组转换时,决定将其转换为哪个委托会使用重载决策,但不考虑返回类型

原则上,确定方法组的可转换性需要使用重载决策从方法组中选择一个方法,而重载决策不考虑返回类型。

在你的示例中,编译器将Action<T>Func<T, T>都视为最佳匹配Add。这增加了两个可能的选择,由于只能选择一个,因此会发出适当的错误提示。

0

正确使用重载:

public static IEnumerable<TDest> Run<TSource, TDest>(this IEnumerable<TSource> source, 
    Func<TSource, TDest> action) 
{ 
 return source.Select(action).ToList(); 
} 

没有丝毫的区别。 - Mark Rendle
你应该在这里移除 .ToList,以避免执行查询。 - Steve B
@SteveB 方法名为Run,而不是LazyRun。 - Serj-Tm

0

我无法回答为什么,但为了消除歧义,您可以显式地转换您的函数:

invoice.Items.Run((Func<T,T>)db.InvoiceItems.Add); 

或者使用 lambda 表达式

invoice.Items.Run(x => db.InvoiceItems.Add(x));

0

我不知道为什么它不能自动解决,但这里有两个解决方法:

// with T replaced with the actual type:
invoice.Items.Run((Func<T, T>)db.InvoiceItems.Add);
invoice.Items.Run(new Func<T, T>(db.InvoiceItems.Add));

你到底为什么需要这些方法呢?直接使用以下代码不好吗:

foreach (var item in invoice.Items)
    db.InvoiceItems.Add(item);

这个的可读性要好得多。除非您有需要使用Run方法的充分理由,否则我建议不要使用它。据我所见,至少对于 Action<T> 重载来说是没有这样的理由。


运行是一种常见的功能式声明式操作,我不同意foreach形式更易读。此外,一旦加入大括号,代码就变成了四行而不是一行。我正在处理六个子集合;在每个之间添加一个空行,这就是大约30行代码,导致包含方法过长,所以我将每个foreach重构成单独的方法。然后我重构该方法以保持DRY原则,嘿哎呀,我已经得到了一个Run方法。 - Mark Rendle
1
@MarkRendle,你的Run()函数不太符合函数式编程的要求。函数式编程的重要部分是编写没有副作用的函数,而Run()函数仅对副作用有用。我同意Tim的观点:使用foreach更易读,而使用Run()这样的方法并不是一个很好的实践。 - svick
@svick 我恭敬地不同意。 - Mark Rendle
@svick 在许多编程语言和框架中,无论是函数式的还是其他类型的,都有类似于Run方法/函数的东西;比如Ruby和Python中的"each"方法,Javascript中的array.forEach方法,以及Scheme中的for-each标准库函数。 - Mark Rendle
@MarkRendle 而且也有一个原因,为什么在 IEnumerable<T> 上没有这样的(扩展)方法。 - svick

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