【问题标题】:What is the difference between using a delegate and using Func<T>/Action<T> in a method signature?在方法签名中使用委托和使用 Func<T>/Action<T> 有什么区别?
【发布时间】:2013-09-11 12:09:52
【问题描述】:

我一直试图了解 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 类中看到的,我定义了两种不同的方法:

  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 是我添加的小提琴。

正如我所料,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.

但很明显,+ 实际上并不是传统的函数式组合(我猜,真正的组合甚至不适用于动作)。从它似乎没有类型签名的事实可以看出这一点:

(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

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

【问题讨论】:

标签: c# delegates


【解决方案1】:

自定义委托类型与 FuncAction

既然可以使用delegate 获得相同的结果,为什么还要使用Func 和/或Action

因为:

  • 它省去了为每个可能的方法签名创建自定义委托类型的麻烦。在代码中,少即是多。
  • 不同的自定义委托类型是不兼容的,即使它们的签名完全匹配。您可以解决这个问题,但它很冗长。
  • 自从引入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&lt;List&lt;Dictionary&lt;int, string&gt;&gt;, IEnumerable&lt;IEnumerable&lt;int&gt;&gt;&gt;

由于我认为这两种情况都很少发生,因此在日常使用中,实际的答案是“完全没有理由”。

组成多播委托

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 节)中允许,a 委托创建表达式(第 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(string) 而在调用IsInteresting(int) 的代码中引入编译错误会留下非常糟糕的印象。

【讨论】:

  • ... results in the value returned by the last added member of the multicast delegate. 我的朋友必须只有亲身经历,这是我从未想过的极端情况。
  • @neoistheone:它在规范中定义,虽然我是最近才知道的。添加了对答案的引用。
  • 谢谢,一切都清楚了。最后一点return type results in the value returned by the last added member,有这个常见的用例吗?我不明白为什么有人需要对相同的值执行 n 个函数只返回一个结果。另请注意,我的问题更像是“当我有 Action/Func 时,为什么我需要一个代表?”;我的印象是 Func 和 Action 排在代表之前。如果您可以添加一个部分来解释何时在 Action/Func 上使用委托,那就太好了。谢谢。
  • @Likhit:就我个人而言,我从来没有理由使用多播委托的返回类型,所以可能不会。 FuncAction 是在很久以后才引入的;代表从一开始就在 C# 中。我更新了答案。
  • 对于非事件委托,您可以遍历调用列表并实质上将一个委托转换为 n 元返回值,其中 n 是调用列表长度。 msdn.microsoft.com/en-us/library/…
【解决方案2】:

委托是回调方法的函数签名。

Action 和 Func 都是委托,但它们是特定委托类型的简写。

动作必须有一个参数并且不能返回值。 Func 必须有一个参数,并且必须返回一个值。

考虑以下委托签名:

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

第一个签名可以替换为Action&lt;T&gt; 第二个签名可以替换为Func&lt;T, TResult&gt;

第三个签名返回一个值,因此只能替换为Func&lt;T1, T2, T3, T4, TResult&gt;

唯一的区别是delegate可以通过引用传递参数,其中ActionFunc只能通过值传递参数

玩得开心。

【讨论】:

    【解决方案3】:

    代表从 C# 2.0 开始就已经存在。从 C# 3.0 开始的 Lambda。 Func 和 Action 是 .NET 框架的功能,自 .NET 3.5 起就已存在。 Func 和 Action 是下面的代表,只是为了方便(尽管非常方便)。它们在下面的功能上是相同的,但可以避免你声明委托类型。 Predicate 是一个返回 bool 的通用委托,自 .NET 2.0 以来一直存在。

    在写这篇文章的过程中,已经有 2 个关于代码解决方案的答案,但希望对您有所帮助。

    【讨论】:

    • 实际上,委托是在 C# 1.0 中。我想你的意思是泛型是在 C# 2.0 中添加的。
    【解决方案4】:
        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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-06-24
      • 1970-01-01
      • 1970-01-01
      • 2011-10-12
      相关资源
      最近更新 更多