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

时间:2013-09-11 12:09:53

标签: c# delegates

我一直在尝试用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
  3. 对其中任何一个的调用都会返回相同的结果;那么代表解决了什么目的呢?

    同一页面上的第二个例子导致更多混乱;这是代码:

    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)
    

    那么+-究竟是什么意思?

    旁白:我尝试在var a = Hello;...中使用Main2,但收到错误:

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

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

4 个答案:

答案 0 :(得分:32)

自定义委托类型与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),每个方法调用都会在引用时发生   相同的变量;通过一个方法更改该变量   调用列表将在调用后面的方法中可见   名单。 如果委托调用包含输出参数或a   返回值,它们的最终值将来自于调用   列表中的最后一位代表

除此之外:“无法将方法组分配给隐式类型的局部变量”

首先,您需要知道方法组是什么。规范说:

  

一个方法组,它是一组由a产生的重载方法   成员查找(第7.4节)。 [...]方法组是   在invocation-expression(第7.6.5节)中允许,a   delegate-creation-expression(§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 :(得分:3)

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

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<T> 第二个签名可以替换为Func<T, TResult>

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

替换

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

玩得开心。

答案 2 :(得分: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

答案 3 :(得分:0)

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

在编写本文时,已经有2个代码解决方案的答案,但希望您觉得这很有帮助。