Подтвердить что ты не робот

В чем разница между использованием делегата и использованием Func <T>/Action <T> в сигнатуре метода?

Я пытаюсь разобраться с делегатами на С#, но я просто не понимаю, как их использовать. Вот несколько слегка реконструированный код на странице 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 Clues for the Clueless",
               "Scott Adams", 12.00m, true);
        }
    }
}

Как вы можете видеть в классе BookDB, я определил 2 разных метода:

  • Тот, который принимает делегат в качестве аргумента: 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<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 добавлены мной скрипты.

Как я и ожидал, Main1 и Main2 дают тот же результат, 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, однако, дает очень странный результат:

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.

Но ясно, что + на самом деле не является традиционным функциональным составом (реальный состав, по-моему, даже не работал бы на Action). Это многое видно из того факта, что он, похоже, не имеет сигнатуры типа:

(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

Возможно, это не связано с этим вопросом, но почему он не может этого сделать? Это похоже на довольно простой вывод типа.

4b9b3361

Ответ 1

Пользовательские типы делегатов vs Func и Action

Зачем использовать Func и/или Action, когда вы можете достичь тех же результатов с помощью delegate?

Потому что:

  • Это избавляет вас от необходимости создавать пользовательский тип делегата для каждой возможной сигнатуры метода. В коде меньше.
  • Различные пользовательские типы делегатов несовместимы, даже если их подписи точно совпадают. Вы можете обойти это, но он многословный.
  • Начиная с введения 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?

Потому что:

  • Вы ориентируетесь на раннюю версию С#, где эти типы не существуют.
  • Вы работаете со сложными сигнатурами функций. Никто не хотел бы вводить это более одного раза: Func<List<Dictionary<int, string>>, IEnumerable<IEnumerable<int>>>.

Так как я рассматриваю их как редкие случаи, в повседневном использовании практический ответ "вообще не причина".

Состав делегатов многоадресной передачи

Все делегаты на С# являются делегатами многоадресной рассылки, т.е. их вызов может потенциально вызывать любое количество методов с этой подписью. Операторы + и - не выполняют функцию композиции; они добавляют и удаляют делегат из делегата многоадресной передачи. Пример:

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"

Это описано в спецификации С# (§15.4, "вызов делегата" ):

Вызов экземпляра делегата, список вызовов которого содержит несколько записей поступают путем вызова каждого из методов в список вызовов, синхронно, в порядке. Каждый так называемый метод прошел тот же набор аргументов, который был передан делегату пример. Если такой вызов делегата включает эталонные параметры (§10.6.1.2), каждый вызов метода будет происходить со ссылкой на та же переменная; изменения этой переменной одним способом в список вызовов будет виден методам, расположенным дальше по вызову список. Если вызов делегата включает выходные параметры или возвращаемое значение, их конечное значение будет получено из вызова последнего делегата в списке.

Кроме того: "Невозможно присвоить группе методов неявно типизированную локальную переменную"

Прежде всего вам нужно знать, что такое группа методов. В спецификации указано:

Группа методов, представляющая собой набор перегруженных методов, поиск членов (§7.4). [...] Группа методов разрешено в выражении вызова (§7.6.5), a делегат-создание-выражение (§7.6.10.5) и как левая часть a 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, потому что единственной разумной задачей для компилятора будет создание делегата, но он не знает, к какому методу он должен указывать.

В частном случае, когда группа методов содержит ровно один метод, эта проблема разрешима. В верхней части моей головы я могу думать о двух причинах, по которым команда С# не позволяла ему работать:

  • Консистенция хороша.
  • приведет к поломке совершенно хорошего кода, если позже будет введена другая перегрузка. Представляем ошибку компиляции для кода, который вызывает IsInteresting(int), потому что вы добавили IsInteresting(string), оставив очень плохое впечатление.

Ответ 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<T> вторая подпись может быть заменена на Func<T, TResult>

третья подпись возвращает значение и поэтому может быть заменена только на Func<T1, T2, T3, T4, TResult>

Единственное отличие состоит в том, что делегат может передавать параметры по ссылке, где Action и Func могут передавать только параметры по значению

Удачи.

Ответ 3

    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

Ответ 4

Делегаты работают с С# 2.0. Lambdas с С# 3.0. Func и Action - это функции платформы .NET, которые были с .NET 3.5. Func и Action являются делегатами внизу и просто для удобства (хотя и крайне удобны). Они функционально одинаковы под ними, но не позволяют вам объявлять типы делегатов. Predicate - это общий делегат, возвращающий bool, и существует с .NET 2.0.

В то время, когда это было написано, было 2 ответа с помощью кодовых решений, но, надеюсь, вы найдете это полезным.