Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
255 views
in Technique[技术] by (71.8m points)

c# - What is the difference between using a delegate and using Func<T>/Action<T> in a method signature?

I have been trying to get my head around delegates in C#, but I just don't seem to get the point of using them. Here is some slightly reconstructed code from the MSDN page on delegates:

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);
        }
    }
}

As you can see in the BookDB class, I have defined 2 different methods:

  1. One which takes a delegate as an argument: ProcessPaperbackBooksWithDelegate
  2. One which takes an action of the corresponding type signature as argument: ProcessPaperbackBooksWithoutDelegate

A call to either of them returns the same result; so what purpose does a delegate solve?

The second example on the same page leads to lot more confusion; here is the code:

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 is the function which was already in the example. Main2 and Main3 are fiddles added by me.

As I expected, Main1 and Main2 give the same result i.e.:

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

Main3 however, gives a very strange result:

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

If + was actually performing function composition then the result(for Main3) should have been:

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.

But it is clear that + isn't actually the traditional functional composition(real composition wouldn't even work for an Action, I guess). That much is evident from the fact that it doesn't seem to have a type signature of:

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

instead, the type signature seems to be:

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

So what do + and - really mean?

Aside: I tried to use var a = Hello;... in Main2 but got the error:

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

It may not be related to this question, but why can't it do so? It seems like a pretty straight forward type deduction.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Custom delegate types vs Func and Action

Why use Func and/or Action when you can achieve the same results with a delegate?

Because:

  • It saves you the trouble of creating a custom delegate type for each possible method signature. In code, less is more.
  • Different custom delegate types are incompatible, even if their signatures exactly match. You can work around this but it's verbose.
  • Since the introduction of Func and Action this is the idiomatic way to write code. Unless there is compelling reason for the opposite, you want to do as the Romans do.

Let's see what the problem is:

// 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) {}

Trying it out:

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

The last line is problematic: there is no technical reason why it cannot work, but the compiler treats Foo and Bar as the distinct types they are and disallows it. This can lead to friction because if all you have is a Bar you would have to write

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

Why use a delegate over Func and/or Action?

Because:

  • You are targeting an early version of C# where these types do not exist.
  • You are working with complicated function signatures. Noone would want to type this more than once: Func<List<Dictionary<int, string>>, IEnumerable<IEnumerable<int>>>.

Since I consider both of these as rare occurrences, in everyday usage the practical answer is "no reason at all".

Composing multicast delegates

All delegates in C# are multicast delegates -- that is, invoking them can potentially invoke any number of methods with that signature. The operators + and - do not perform function composition; they add and remove a delegate from a multicast delegate. An example:

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

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

You can remove a delegate from a multicast delegate with operator-, but you must pass the exact same delegate in. If right-hand-side operand was not already part of the multicast delegate then nothing happens. For example:

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

Multicast delegate return values

Invoking a multicast delegate with a non-void return type results in the value returned by the last added member of the multicast delegate. For example:

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"

This is documented in the C# spec (§15.4, "delegate invocation"):

Invocation of a delegate instance whose invocation list contains multiple entries proceeds by invoking each of the methods in the invocation list, synchronously, in order. Each method so called is passed the same set of arguments as was given to the delegate instance. If such a delegate invocation includes reference parameters (§10.6.1.2), each method invocation will occur with a reference to the same variable; changes to that variable by one method in the invocation list will be visible to methods further down the invocation list. If the delegate invocation includes output parameters or a return value, their final value will come from the invocation of the last delegate in the list.

Aside: "Cannot assign method group to an implicitly-typed local variable"

First of all you need to know what a method group is. The specification says:

A method group, which is a set of overloaded methods resulting from a member lookup (§7.4). [...] A method group is permitted in an invocation-expression (§7.6.5), a delegate-creation-expression (§7.6.10.5) and as the left hand side of an is operator, and can be implicitly converted to a compatible delegate type (§6.6). In any other context, an expression classified as a method group causes a compile-time error.

So, given a class with these two methods:

public bool IsInteresting(int i) { return i != 0; }
public bool IsInteresting(string s) { return s != ""; }

When the token IsInteresting appears in the source, it's a method group (note that a method group can of course consist of one single method, as in your example).

The compile-time error is expected (the spec mandates it) because you are not trying to convert it to a compatible delegate type. Being more explicit solves the problem:

// both of these convert the method group to the "obviously correct" delegate
Func<int, bool> f1 = IsInteresting;
Func<string, bool> f2 = IsInteresting;

In layman's terms it's not meaningful to write var f = IsInteresting because the only reasonable thing for the compiler would be to create a delegate, but it does not know which method it should point to.

In the special case where the method group contains exactly one method this problem is solvable. Off the top of my head I can think of two reasons why the C# team did not allow it to work:

  1. Consistency is good.
  2. Would lead to breakage of perfectly good code if another overload is introduced later. Introducing a compile error to code that calls IsInteresting(int) because you added an IsInteresting(string) would leave a really bad impression.

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...