在方法签名中使用委托和使用Func / Action 有什么区别?
我一直在尝试用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 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
类中看到的,我定义了两种不同的方法:
- 将委托作为参数的一个:
ProcessPaperbackBooksWithDelegate
- 将相应类型签名的操作作为参数的一个:
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 a = Hello; Action b = Goodbye; Action c = a + b; Action 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 a = HelloS; Func b = GoodbyeS; Func c = a + b; Func 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
是示例中已有的function。 Main2
和Main3
是我添加的小提琴。
正如我所料, Main1
和Main2
给出了相同的结果,即:
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.
但很显然, +
实际上并不是传统的function组合(我认为真正的构图甚至不适用于动作)。 从它似乎没有类型签名的事实可以看出这一点:
(T2 -> T3) -> (T1 -> T2) -> T1 -> T3
相反,类型签名似乎是:
(T1 -> T2) -> (T1 -> T2) -> (T1 -> T2)
那么+
和-
真正意味着什么呢?
Main2
:我试图在Main2
使用var a = Hello;...
但是得到了错误:
test.cs(136,14): error CS0815: Cannot assign method group to an implicitly-typed local variable
它可能与此问题无关,但为什么不能这样做呢? 这似乎是一个非常直接的类型演绎。
自定义委托类型与Func
和Action
为什么在使用delegate
可以获得相同结果时使用Func
和/或Action
?
因为:
- 它为您节省了为每个可能的方法签名创建自定义委托类型的麻烦。 在代码中,少即是多。
- 不同的自定义委托类型不兼容,即使它们的签名完全匹配。 你可以解决这个问题,但它很冗长。
- 自从引入
Func
和Action
以来,这是编写代码的惯用方法。 除非有相反的令人信服的理由,否则你想像罗马人那样做。
让我们看看问题是什么:
// 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"
最后一行是有问题的:没有技术上的原因它无法工作,但编译器将Foo
和Bar
视为它们的不同类型并且不允许它。 这可能导致摩擦,因为如果你只有一个Bar
你就得写
var bar = new Bar(delegate() {}); Consumer(new Foo(bar)); // OK, but the ritual isn't a positive experience
为什么在Func
和/或Action
使用委托?
因为:
- 您的目标是C#的早期版本,其中不存在这些类型。
- 您正在使用复杂的function签名。 没人会想要多次输入:
Func
。- >, IEnumerable
>>
由于我认为这两种情况都是罕见的,在日常使用中,实际的答案是“毫无理由”。
组成多播委托
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(Ret1) + Ret2)()); // prints "2" Console.WriteLine((new Func (Ret2) + Ret1)()); // prints "1"
这在C#规范(第15.4节“委托调用”)中有记录:
通过按顺序同步调用调用列表中的每个方法来调用其调用列表包含多个条目的委托实例。 所谓的每个方法都传递给委托实例的同一组参数。 如果这样的委托调用包含引用参数(第10.6.1.2节),则每个方法调用都将引用同一个变量; 通过调用列表中的一个方法对该变量的更改将对调用列表中的下一个方法可见。 如果委托调用包括输出参数或返回值,则它们的最终值将来自列表中最后一个委托的调用 。
旁白:“无法将方法组分配给隐式类型的局部变量”
首先,您需要知道方法组是什么。 规范说:
方法组,它是由成员查找(第7.4节)产生的一组重载方法。 […]在invocation-expression(第7.6.5节),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 f1 = IsInteresting; Func f2 = IsInteresting;
以外var f = IsInteresting
的术语来说,编写var f = IsInteresting
是没有意义的,因为编译器唯一合理的做法是创建一个委托,但它不知道应该指向哪个方法。
在方法组只包含一个方法的特殊情况下,此问题是可解决的。 在我的脑海中,我可以想到为什么C#团队不允许它工作的两个原因:
- 一致性很好。
- 如果稍后引入另一个重载,将导致完美的代码破坏。 将编译错误引入调用
IsInteresting(int)
代码,因为您添加了IsInteresting(string)
会给人留下非常糟糕的印象。
委托是回调方法的函数签名。
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
替换,第二个签名可以用Func
替换
第三个签名返回一个值,因此只能用Func
替换
唯一的区别是委托可以通过引用传递参数,其中Action
和Func
只能按值传递参数
玩得开心。
代表们自C#2.0以来一直存在。 自C#3.0以来的Lambdas。 Func和Action是.NET框架的特性,自.NET 3.5以来就已存在。 Func和Action是下面的代表,仅仅是为了方便(尽管极端方便)。 它们在function上相同,但是可以避免声明委托类型。 Predicate是一个返回bool的generics委托,并且自.NET 2.0以来一直存在。
在编写本文时,已经有2个代码解决方案的答案,但希望您觉得这很有帮助。
Func a = HelloS; Func b = GoodbyeS; Func c = a + b; Func 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 c1 = (s) => { a(s); returns b(s); };//equivalent to c