Использование делегатов в C #

79

Не могли бы вы помочь мне в понимании делегатов в языке C # и .NET Framework? Я пытался проверить код и обнаружил, что полученные результаты были для меня неожиданными. Вот:

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. Почему?

user1859587
источник
12
@Rotem: Нет, он этого не сделал.
Daniel Hilgarth
3
@Rotem - это объявление делегата. Добавление ()вызовет ToString.
Oded
1
Извините, никогда не использовал Funcs, это было предположение :)
Rotem
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. Он зафиксирован во время создания.

Как указывает Флиндеберг в комментариях ниже, у каждого делегата есть цель и метод, который должен выполняться на цели.

В этом случае очевидно, что выполняемый ToStringметод - это метод. Интересной частью является экземпляр, на котором выполняется метод: это экземпляр Iво время создания, а это означает, что делегат не использует Iэкземпляр для использования, но сохраняет ссылку на сам экземпляр.

Позже вы измените Iзначение, присвоив ему новый экземпляр. Это не меняет волшебным образом экземпляр, захваченный в вашем делегате, зачем это нужно?

Чтобы получить ожидаемый результат, вам нужно будет изменить делегата на это:

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

Таким образом, делегат указывает на анонимный метод, который выполняется ToStringв текущем состоянии Iво время выполнения делегата.

В этом случае выполняемый метод - это анонимный метод, созданный в классе, в котором объявлен делегат. Экземпляр имеет значение null, поскольку это статический метод.

Взгляните на код, который компилятор генерирует для второй версии делегата:

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 для одного конкретного объекта. Неважно, ссылочный ли это тип или тип значения.
Daniel Hilgarth
3
@ user1859587: у делегата есть метод и цель (экземпляр), важно, захватывает ли он ваш экземпляр или экземпляр лямбда-функции, в свою очередь, содержит ссылки на экземпляр.
flindeberg
1
@ user1859587: Пожалуйста. Кстати: я попытался обновить ответ, чтобы было немного понятнее, что здесь происходит. Вы можете перечитать это :-)
Дэниел Хилгарт
3
Дэниел, просто чтобы подтвердить комментарий Флиндеберга: ваш ответ правильный, а ваши комментарии относительно бокса - нет. user1859587 правильно: наблюдаемое поведение является следствием того, что делегат перехватывает получателя вызова. Хотя получатель вызова ToString для int будет ссылкой на переменную int, делегат не имеет возможности поместить ссылку на переменную int в куче; ссылки на переменные могут поступать только во временное хранилище. Таким образом, он делает следующее лучшее: он помещает int и делает ссылку на это место в куче.
Эрик Липперт
10
Интересным следствием того факта, что получатель упакован в коробку, является то, что невозможно создать делегат для GetValueOrDefault () для обнуляемого int, поскольку упаковка с обнуляемым int создает упакованный int, а не упакованный в коробку int, допускающий значение NULL, а упакованный в коробку 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 # enable инкапсулирует как объект, так и экземпляр, и метод. Объявление делегата определяет класс, производный от класса 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).

Yshayy
источник
2
Ваше предположение неверно. Это не имеет ничего общего с типами значений и ссылочными типами. То же самое произойдет и со ссылочными типами.
Daniel Hilgarth
На самом деле это так, если, например, мы используем экземпляр класса, а 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