Використання делегатів у C #


79

У мові C # та середовищі .NET, чи можете ви допомогти мені з розумінням делегатів? Я намагався перевірити якийсь код і виявив, що результати, які я отримав, були для мене несподіваними. Ось:

class Program
{
    public static int I = 0;

    static Func<string> del = new Func<string>(I.ToString);

    static void Main(string[] args)
    {
        I = 10;
        Console.WriteLine("{0}", del());
    }
}

Відповідь була 0, але ні 10. Чому?


12
@Rotem: Ні, він цього не зробив.
Даніель Хілгарт

3
@Rotem - Це декларація делегата. Додавання ()викликало б ToString.
Почато від

1
Вибачте, ніколи не використовував Funcs, було здогадом :)
Ротем

2
+1 за приємне запитання, добре поставлене. Чудовий приклад того, як, здавалося б, просте запитання може виділити недостатньо зрозумілу область мови / платформи.
Мартін

5
Екземпляр делегата (одноадресний) може вказувати або на метод екземпляра, або на staticметод. Коли він представляє метод екземпляра, делегат містить як "цільовий" об'єкт, для якого потрібно викликати метод, так і інформацію про метод. Отже, коли ви говорите del = I.ToString;, delутриматиме об'єкт, Iякий знаходиться тут Int32(незмінний тип значення). Коли ви використовуєте анонімну функцію, del = () => I.ToString();компілятор створює метод, static string xxx() { return I.ToString(); }а delоб'єкт утримує цей згенерований метод.
Jeppe Stig Nielsen

Відповіді:


79

Причина в наступному:

Те, як ви оголошуєте делегата, вказує безпосередньо на ToStringметод статичного екземпляра int. Він захоплюється під час створення.

Як зазначає flindeberg у коментарях нижче, кожен делегат має ціль та метод, який слід виконати на цілі.

У цьому випадку метод, який слід виконати, очевидно є ToStringметодом. Цікавою частиною є екземпляр, на якому виконується метод: це екземпляр Iна момент створення, тобто делегат не використовує, Iщоб змусити екземпляр використовувати, але він зберігає посилання на сам екземпляр.

Пізніше ви змінюєте Iінше значення, в основному призначаючи йому новий екземпляр. Це магічним чином не змінює екземпляр, захоплений вашим делегатом, навіщо це робити?

Щоб отримати очікуваний результат, вам слід змінити делегата на такий:

static Func<string> del = new Func<string>(() => I.ToString());

Подібно до цього, делегат вказує на анонімний метод, який виконується ToStringна поточному Iна момент виконання делегата.

У цьому випадку виконуваний метод - це анонімний метод, створений у класі, в якому оголошено делегат. Екземпляр є нульовим, оскільки це статичний метод.

Погляньте на код, який компілятор створює для другої версії делегата:

private static Func<string> del = new Func<string>(UserQuery.<.cctor>b__0);
private static string cctor>b__0()
{
    return UserQuery.I.ToString();
}

Як бачите, це звичайний метод, який щось робить . У нашому випадку він повертає результат виклику ToStringпоточного екземпляра I.


1
@flindeberg: Ви навіть можете використовувати власний клас замість int. Він все одно буде поводитися так само, оскільки основна причина не змінюється: делегат вказує на конкретне втілення ToString на одному конкретному об'єкті. Не має значення, це посилальний тип чи тип значення.
Даніель Хілгарт,

3
@ user1859587: У делегата є метод і ціль (екземпляр), важливо, чи захопить він ваш екземпляр або екземпляр лямбда-функції, у свою чергу містить посилання на екземпляр.
flindeberg

1
@ user1859587: Ласкаво просимо. ДО: Я намагався оновити відповідь, щоб трохи зрозуміти, що тут відбувається. Можливо, ви захочете його перечитати :-)
Даніель Хілгарт,

3
Даніель, лише на підтвердження коментаря Фліндеберга: ваша відповідь правильна, але ваші коментарі щодо боксу - ні. user1859587 правильний: спостережувана поведінка є наслідком того, що делегат захоплює приймача виклику. Хоча одержувач виклику ToString на int буде посиланням на змінну int, делегат не має можливості розмістити посилання на змінну int у купі; посилання на змінні можуть надходити лише на тимчасове зберігання. Отже, він робить наступне найкраще: він вводить int і робить посилання на те місце купи.
Eric Lippert

10
Цікавим наслідком того факту, що одержувач встановлений в коробці, є те, що неможливо зробити делегата GetValueOrDefault () на nullable int, оскільки boxing nullable int виробляє boxed int, а не box nullable int, і boxed int має немає методу GetValueOrDefault ().
Ерік Ліпперт,

4

Вам потрібно перейти Iдо своєї функції, щоб вона I.ToString()могла бути виконана у відповідний час (замість того, щоб функція часу була створена).

class Program
{
    public static int I = 0;

    static Func<int, string> del = num => num.ToString();

    static void Main(string[] args)
    {
        I = 10;
        Console.WriteLine("{0}", del(I));
    }
}

1

Ось як це слід зробити:

using System;

namespace ConsoleApplication1
{

    class Program
    {
        public static int I = 0;

        static Func<string> del = new Func<string>(() => {
            return I.ToString();
        });

        static void Main(string[] args)
        {
            I = 10;
            Console.WriteLine("{0}", del());
        }
    }
}

0

Делегат C # дозволяє інкапсулювати як об’єкт, так і екземпляр, а також метод. Оголошення делегата визначає клас, який походить від класу System.Delegate. Екземпляр делегата інкапсулює список викликів, який є одним або декількома методами списку, кожен з яких називається викличуваною сутністю.

дізнатися більше про форму

http://asp-net-by-parijat.blogspot.in/2015/08/what-is-delegates-in-c-how-to-declare.html


-2

Я здогадуюсь, оскільки int передається значеннями, а не посиланнями, і з цієї причини при створенні делегата це делегат методу ToString поточного значення "I" (0).


2
Ваше припущення невірно. Це не має нічого спільного з типами значень порівняно з еталонними типами. Точно те саме відбулося б і з еталонними типами.
Даніель Хільгарт,

Насправді це так, якщо, наприклад, ми використовуємо екземпляр класу і ToString маніпулює даними екземпляра для поверненого значення, це дасть повернене значення поточного стану класу, а не стану класу, коли був створений делегат. Функція не запускається, коли створюється делегат, і є лише один екземпляр класу.
Yshayy

Func <string> (() => I.ToString ()) Також має працювати, оскільки ми не використовуємо "I" до виклику методу.
Yshayy

Але це не рівнозначно тому, що тут відбувається. Якщо ви використовуєте клас Fooзамість intі змінили рядок I = 10на I = new Foo(10)вас, ви отримаєте точно такий же результат, як і з поточним кодом. I.Value = 10це щось зовсім інше. Це не призначає новий екземпляр I. Але присвоєння нового екземпляра Iє важливим моментом тут.
Даніель Хілгарт,

2
добре, отже, проблема полягає у перепризначенні "I", якби "I" було змінним, і ми змінювали об'єкт без перепризначення I, це спрацювало б. у цьому прикладі ми не можемо цього зробити, оскільки I є int (незмінний).
Yshayy
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.