20

我一直在尝试了解 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是示例中已经存在的函数。Main2并且Main3是我添加的小提琴。

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

如果+实际上是在执行函数组合,那么结果(for 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;...inMain2但出现错误:

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

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

4

4 回答 4

41

自定义委托类型 vsFuncAction

为什么使用Func和/或Action何时可以使用 a 获得相同的结果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"

最后一行是有问题的:它不能工作没有技术原因,但编译器将FooandBar视为它们是不同的类型并禁止它。这可能会导致摩擦,因为如果你只有 aBar你就必须写

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)因为添加了 an而在调用的代码中引入编译错误IsInteresting(string)会留下非常糟糕的印象。
于 2013-09-11T12:19:13.277 回答
4

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

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>

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

玩得开心。

于 2013-09-11T12:24:58.857 回答
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")thenb("C")返回last 的结果Func,即b;

Func<string, string> c1 = (s) => { a(s); returns b(s); };//equivalent to c
于 2013-09-11T12:19:41.023 回答
0

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

在写这篇文章的时间里,已经有 2 个关于代码解决方案的答案,但希望你觉得这很有帮助。

于 2013-09-11T12:21:16.693 回答