在方法签名中使用委托和使用Func<T>/Action<T>有什么区别?

21

我一直在努力理解C#中的委托,但似乎没有掌握使用它们的要点。下面是从MSDN关于委托的页面中稍微重构过的代码:

using System;
using System.Collections;

namespace Delegates
{
    // Describes a book in the book list:
    public struct Book
    {
        public string Title;        // Title of the book.
        public string Author;       // Author of the book.
        public decimal Price;       // Price of the book.
        public bool Paperback;      // Is it paperback?

        public Book(string title, string author, decimal price, bool paperBack)
        {
            Title = title;
            Author = author;
            Price = price;
            Paperback = paperBack;
        }
    }

    // Declare a delegate type for processing a book:
    public delegate void ProcessBookDelegate(Book book);

    // Maintains a book database.
    public class BookDB
    {
        // List of all books in the database:
        ArrayList list = new ArrayList();

        // Add a book to the database:
        public void AddBook(string title, string author, decimal price, bool paperBack)
        {
            list.Add(new Book(title, author, price, paperBack));
        }

        // Call a passed-in delegate on each paperback book to process it:
        public void ProcessPaperbackBooksWithDelegate(ProcessBookDelegate processBook)
        {
            foreach (Book b in list)
            {
                if (b.Paperback)
                    processBook(b);
            }
        }

        public void ProcessPaperbackBooksWithoutDelegate(Action<Book> action)
        {
            foreach (Book b in list)
            {
                if (b.Paperback)
                    action(b);
            }
        }
    }

    class Test
    {

        // Print the title of the book.
        static void PrintTitle(Book b)
        {
            Console.WriteLine("   {0}", b.Title);
        }

        // Execution starts here.
        static void Main()
        {
            BookDB bookDB = new BookDB();
            AddBooks(bookDB);
            Console.WriteLine("Paperback Book Titles Using Delegates:");
            bookDB.ProcessPaperbackBooksWithDelegate(new ProcessBookDelegate(PrintTitle));
            Console.WriteLine("Paperback Book Titles Without Delegates:");
            bookDB.ProcessPaperbackBooksWithoutDelegate(PrintTitle);
        }

        // Initialize the book database with some test books:
        static void AddBooks(BookDB bookDB)
        {
            bookDB.AddBook("The C Programming Language",
               "Brian W. Kernighan and Dennis M. Ritchie", 19.95m, true);
            bookDB.AddBook("The Unicode Standard 2.0",
               "The Unicode Consortium", 39.95m, true);
            bookDB.AddBook("The MS-DOS Encyclopedia",
               "Ray Duncan", 129.95m, false);
            bookDB.AddBook("Dogbert's Clues for the Clueless",
               "Scott Adams", 12.00m, true);
        }
    }
}

如您在BookDB类中所见,我定义了2种不同的方法:

  1. 一个接受委托作为参数:ProcessPaperbackBooksWithDelegate
  2. 一个接受相应类型签名的操作作为参数:ProcessPaperbackBooksWithoutDelegate

调用它们中的任何一个都会返回相同的结果,那么委托有什么用处呢?

同一页上的第二个示例会带来更多的困惑;以下是代码:

delegate void MyDelegate(string s);

static class MyClass
{
    public static void Hello(string s)
    {
        Console.WriteLine("  Hello, {0}!", s);
    }

    public static void Goodbye(string s)
    {
        Console.WriteLine("  Goodbye, {0}!", s);
    }

    public static string HelloS(string s)
    {
        return string.Format("Hello, {0}!", s);
    }

    public static string GoodbyeS(string s)
    {
        return string.Format("Goodbye, {0}!", s);
    }

    public static void Main1()
    {
        MyDelegate a, b, c, d;
        a = new MyDelegate(Hello);
        b = new MyDelegate(Goodbye);
        c = a + b;
        d = c - a;

        Console.WriteLine("Invoking delegate a:");
        a("A");
        Console.WriteLine("Invoking delegate b:");
        b("B");
        Console.WriteLine("Invoking delegate c:");
        c("C");
        Console.WriteLine("Invoking delegate d:");
        d("D");
    }

    public static void Main2()
    {
        Action<string> a = Hello;
        Action<string> b = Goodbye;
        Action<string> c = a + b;
        Action<string> d = c - a;

        Console.WriteLine("Invoking delegate a:");
        a("A");
        Console.WriteLine("Invoking delegate b:");
        b("B");
        Console.WriteLine("Invoking delegate c:");
        c("C");
        Console.WriteLine("Invoking delegate d:");
        d("D");
    }

    public static void Main3()
    {
        Func<string, string> a = HelloS;
        Func<string, string> b = GoodbyeS;
        Func<string, string> c = a + b;
        Func<string, string> d = c - a;

        Console.WriteLine("Invoking function a: " + a("A"));
        Console.WriteLine("Invoking function b: " + b("B"));
        Console.WriteLine("Invoking function c: " + c("C"));
        Console.WriteLine("Invoking function d: " + d("D"));
    }
}

Main1是已经在示例中的函数。Main2Main3是我添加的fiddles。

正如我所预料的那样,Main1Main2给出了相同的结果:

Invoking delegate a:
  Hello, A!
Invoking delegate b:
  Goodbye, B!
Invoking delegate c:
  Hello, C!
  Goodbye, C!
Invoking delegate d:
  Goodbye, D!

Main3的结果非常奇怪:

Invoking function a: Hello, A!
Invoking function b: Goodbye, B!
Invoking function c: Goodbye, C!
Invoking function d: Goodbye, D!
如果 + 实际上执行的是函数组合,那么结果(对于 Main3)应该是什么:
Invoking function a: Hello, A!
Invoking function b: Goodbye, B!
Invoking function c: Hello, Goodbye, C!!
Invoking function d: //God knows what this should have been.

但很明显,+实际上并不是传统的函数组合(对于动作(Action)而言,真正的组合甚至都行不通)。因为它似乎没有以下类型签名:

(T2 -> T3) -> (T1 -> T2) -> T1 -> T3

相反,类型签名似乎是:

(T1 -> T2) -> (T1 -> T2) -> (T1 -> T2)

那么+-到底代表什么意思呢?

旁注:我试着在Main2中使用var a = Hello;...,但是出现了错误:

test.cs(136,14): error CS0815: Cannot assign method group to an implicitly-typed
    local variable

可能与这个问题无关,但为什么不能这样做呢?这似乎是一种非常直接的类型推断。


1
你的问题标题和实际问题之间存在不一致。 - James
FuncAction是.NET 3.5中添加的通用委托,作为一种方便的功能。 - Dustin Kingen
可能是手动声明委托,使用Func<T>或Action<T>?的重复问题。 - nawfal
4个回答

45

自定义委托类型 vs FuncAction

为什么要使用 Func 和/或 Action,当你可以通过使用 delegate 实现相同的结果呢?

因为:

  • 它避免了为每种可能的方法签名创建自定义委托类型的麻烦。在代码中,尽量少写多余的东西。
  • 不同的自定义委托类型是不兼容的,即使它们的签名完全匹配。你可以绕过这个问题,但是比较冗长。
  • 自从引入了FuncAction,这已经成为编写代码的惯用方式。除非有令人信服的理由相反,否则你应该遵循大众的做法。

让我们看看问题在哪里:

// Delegates: same signature but different types
public delegate void Foo();
public delegate void Bar();

// Consumer function -- note it accepts a Foo
public void Consumer(Foo f) {}

尝试一下:

Consumer(new Foo(delegate() {})); // works fine
Consumer(new Bar(delegate() {})); // error: cannot convert "Bar" to "Foo"

最后一行存在问题:从技术上讲,它是可行的。但编译器会将 FooBar 视为不同的类型并拒绝它。这可能会导致摩擦,因为如果你只有一个 Bar,你必须写成:

var bar = new Bar(delegate() {});
Consumer(new Foo(bar)); // OK, but the ritual isn't a positive experience

为什么要使用委托而不是Func和/或Action

因为:

  • 您正在针对早期版本的C#,这些类型不存在。
  • 您正在处理复杂的函数签名。没有人希望多次输入此内容:Func<List<Dictionary<int, string>>, IEnumerable<IEnumerable<int>>>

由于我认为这两种情况都很少见,在日常使用中实际上“没有任何理由”。

合成多路广播委托

C#中所有委托都是多路广播委托——也就是说,调用它们可能会调用具有该签名的任意数量的方法。运算符+-不执行函数组合;它们向多路广播委托添加和删除委托。例如:

void Foo() {}
void Bar() {}

var a = new Action(Foo) + Bar;
a(); // calls both Foo() and Bar()

您可以使用operator-从多路委托中删除委托,但必须传递完全相同的委托。如果右侧操作数尚未成为多路委托的一部分,则不会发生任何事情。例如:

var a = new Action(Foo);
a();      // calls Foo()
a -= Bar; // Bar is not a part of the multicast delegate; nothing happens
a();      // still calls Foo() as before

多播委托返回值

使用非void返回类型调用多播委托将导致返回最后添加成员的值。例如:

public int Ret1() { return 1; }
public int Ret2() { return 2; }

Console.WriteLine((new Func<int>(Ret1) + Ret2)()); // prints "2"
Console.WriteLine((new Func<int>(Ret2) + Ret1)()); // prints "1"

这在C#规范中有所说明(§15.4,“委托调用”):
对于委托实例的调用,其调用列表包含多个条目,通过按顺序同步地调用调用列表中的每个方法来执行。每次被调用的方法都会传递与委托实例给出的相同的一组参数。 如果这样的委托调用包括引用参数(§10.6.1.2),则每个方法调用将使用对同一变量的引用;在调用列表中的一个方法更改该变量时,对于调用列表中后面的方法也将可见。
如果委托调用包括输出参数或返回值,则它们的最终值将来自于列表中最后一个委托的调用。
附: "无法将方法组分配给隐含类型的局部变量"
首先,您需要知道什么是方法组。规范如下所述:
方法组是由成员查找(§7.4)产生的一组重载方法。[...] 方法组允许在调用表达式(§7.6.5)、委托创建表达式(§7.6.10.5)和作为is运算符的左操作数,并且可以隐式转换为兼容的委托类型(§6.6)。在任何其他上下文中,将被分类为方法组的表达式会导致编译时错误。
因此,给定一个具有以下两个方法的类:
public bool IsInteresting(int i) { return i != 0; }
public bool IsInteresting(string s) { return s != ""; }

当源代码中出现IsInteresting标记时,它是一个方法组(注意,一个方法组当然可以由一个单一的方法组成,就像你的例子一样)。
编译时错误是预期的(规范要求),因为您没有尝试将其转换为兼容的委托类型。更明确地指定解决了这个问题:
// both of these convert the method group to the "obviously correct" delegate
Func<int, bool> f1 = IsInteresting;
Func<string, bool> f2 = IsInteresting;

通俗易懂地说,写var f = IsInteresting是没有意义的,因为编译器只会创建一个委托,但它不知道应该指向哪个方法。
特殊情况下,如果方法组只包含一个方法,这个问题是可以解决的。我能想到两个原因,为什么C#团队不允许它工作:
1. 保持一致性是好的。 2. 如果以后引入另一个重载,会破坏完全正常的代码。在调用IsInteresting(int)的代码中引入编译错误,因为你添加了一个IsInteresting(string),会留下非常不好的印象。

1
“...结果是多路广播委托的最后一个添加成员返回的值。” 现在,这只能通过经验获得,我的朋友,那是我从未想过的一个边缘情况。 - Mike Perrenoud
1
@neoistheone:虽然我是最近才学到的,但它在规范中有定义。已在答案中添加了参考。 - Jon
谢谢,这澄清了事情。最后一个点返回类型是由最后添加成员返回的值,这个有常见用例吗?我看不出为什么有人需要在同一个值上执行n个函数,只返回其中一个结果。 还要注意,我的问题更多地是关于“为什么我需要委托当我有Action / Func时?”; 我的印象是Func和Action在委托之前就存在了。如果您能添加一个说明何时使用委托而不是Action / Func的部分,那就太好了。谢谢。 - Likhit
@Likhit:就我个人而言,我从来没有使用过多路广播委托的返回类型,所以可能不需要。FuncAction是后来引入的;委托从一开始就在C#中存在。我已经更新了答案并添加了更多内容。 - Jon
1
对于非事件委托,您可以遍历调用列表,并将一个委托转换为n元返回值,其中n是调用列表的长度。http://msdn.microsoft.com/en-us/library/system.delegate.getinvocationlist.aspx - Gusdor
显示剩余2条评论

5

代理是回调方法的函数签名。

无论Action还是Func都是代表委托,但它们是特定委托类型的简写。

Action必须有一个参数并且不返回值。Func必须有一个参数并返回一个值。

请考虑以下代理签名:

delegate void DisplayMessage( string message);
delegate string FormatTime( DateTime date);
delegate bool IsAValidAddress( string addressLine1, string addressLine2, int postCode, string country);

第一个签名可以用 Action<T> 替代。

第二个签名可以用 Func<T, TResult> 替代。

第三个签名返回一个值,因此只能用 Func<T1, T2, T3, T4, TResult> 替代。

唯一的区别是委托可以通过引用传递参数,而 ActionFunc 只能通过值传递参数。

玩得开心。


唯一的区别是委托可以通过引用传递参数,而ActionFunc只能通过值传递参数。我认为这是唯一的区别。 - Soner from The Ottoman Empire

0
    Func<string, string> a = HelloS;
    Func<string, string> b = GoodbyeS;
    Func<string, string> c = a + b;
    Func<string, string> d = c - a;

    Console.WriteLine("Invoking function a: " + a("A"));
    Console.WriteLine("Invoking function b: " + b("B"));
    Console.WriteLine("Invoking function c: " + c("C"));
    Console.WriteLine("Invoking function d: " + d("D"));

c("C") 执行 a("C") 然后执行 b("C")返回最后一个 Func 的结果,即 b

Func<string, string> c1 = (s) => { a(s); returns b(s); };//equivalent to c

0

委托自C# 2.0以来就存在。Lambda表达式自C# 3.0以来就存在。Func和Action是.NET框架的特性,自.NET 3.5以来就存在。Func和Action都是委托,只是为了方便而存在(尽管非常方便)。它们在底层上具有相同的功能,但可以避免声明委托类型。Predicate是一个返回布尔值的通用委托,自.NET 2.0以来就存在。

在编写此文本的时间内,已经有两个带有代码解决方案的答案,但希望这篇文章对您有所帮助。


实际上,委托在C# 1.0中就已经存在了。我想你指的是泛型是在C# 2.0中添加的。 - Bartosz

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