События — это не поля, я не понимаю

В C# подробно (на данный момент отличная книга) , Скит объясняет, что события не являются полями. Я читал этот раздел много раз и не понимаю, почему это различие имеет какое-то значение.

Я один из тех разработчиков, которые путают события и делегирующие экземпляры. На мой взгляд, они одинаковы. Разве оба не просто форма косвенности? Мы можем мультикастировать оба. Событие настроено как поле как стенография... конечно. Но мы добавляем или удаляем обработчики. Складываем их для вызова при возникновении события. Разве мы не делаем то же самое с делегатами, складывая их в стопку и вызывая Invoke?


person P.Brian.Mackey    schedule 28.04.2012    source источник
comment
Ух ты; вы привлекли довольно много веса, чтобы ответить на ваш вопрос! ;)   -  person Andrew Barber    schedule 29.04.2012
comment
Я очень ценю точку зрения каждого высококвалифицированного специалиста.   -  person P.Brian.Mackey    schedule 30.04.2012
comment
Обратите внимание, что, покопавшись в Errata, я обнаружил, что Джон действительно разъясняет эту тему (просто ссылка, данная в книге, неверна): csharpindepth.com/Articles/Chapter2/Events.aspx   -  person P.Brian.Mackey    schedule 01.05.2012


Ответы (5)


Другие ответы в основном правильные, но вот еще один способ взглянуть на это:

Я один из тех разработчиков, которые путают события и делегирующие экземпляры. На мой взгляд, они одинаковы.

На ум приходит старая поговорка о том, что за деревьями леса не видно. Различие, которое я делаю, заключается в том, что события находятся на более высоком «семантическом уровне», чем поле экземпляра делегата. Событие сообщает потребителю типа «привет, я из тех, кто любит сообщать вам, когда что-то происходит». Тип является источником события; это часть его публичного контракта.

Как, в качестве детали реализации, этот класс решает отслеживать, кто заинтересован в прослушивании этого события, и что и когда сообщать подписчикам о том, что событие происходит, - это дело класса. Обычно это происходит с многоадресным делегатом, но это деталь реализации. Это настолько общая деталь реализации, что разумно их спутать, но на самом деле у нас есть две разные вещи: общедоступная поверхность и частная деталь реализации.

Точно так же свойства описывают семантику объекта: у клиента есть имя, поэтому класс Customer имеет свойство Name. Вы можете сказать, что «их имя» — это свойство клиента, но вы никогда не скажете, что «их имя» — это поле клиента; это деталь реализации конкретного класса, а не факт о бизнес-семантике. То, что свойство обычно реализуется как поле, является частной деталью механики класса.

person Eric Lippert    schedule 29.04.2012
comment
Итак, 1. что этот делегат не может делать, а события могут (или наоборот) 2. что должны делать события, а делегат не должен. Это может сделать diff более кристально чистым. - person Dhananjay; 30.04.2012
comment
@Dhananjay Events должны каким-то образом использовать делегатов, поскольку они реализованы как пара методов (добавить и удалить), которые принимают делегата в качестве параметра. Однако они не обязаны сопоставляться непосредственно с полем делегата. Вместо этого они могут хранить делегаты каким-либо другим способом, или они могут создать делегат-оболочку и сохранить его вместо этого и т. д. Пользователи класса не должны различать автоматическое событие, использующее резервное поле delgate, и специально закодированное событие, поэтому разумные варианты реализации немного ограничены. - person Kevin Cathcart; 30.04.2012
comment
Tangential: Если бы вы могли перезапустить C# и .NET с нуля, вы бы заменили события/делегаты чем-то вроде IObservable‹T›? - person Judah Gabriel Himango; 01.05.2012
comment
Вы можете использовать делегатов вместо событий, но события обеспечивают большую безопасность, например защиту делегатов от установки нулевого значения из-за пределов определяющего класса. Я считаю, что это самая большая разница. - person Tarik; 05.05.2012

Свойства также не являются полями, хотя и кажутся таковыми. На самом деле это пара методов (геттер и сеттер) со специальным синтаксисом.

События также представляют собой пару методов (подписаться и отказаться от подписки) со специальным синтаксисом.

В обоих случаях у вас обычно есть частное «резервное поле» внутри вашего класса, которое содержит значение, управляемое методами getter/setter/subscribe/unsubscribe. И есть автоматически реализуемый синтаксис как для свойств, так и для событий, когда компилятор генерирует для вас вспомогательное поле и методы доступа.

Цель та же: свойства предоставляют ограниченный доступ к полю, где выполняется некоторая логика проверки перед сохранением нового значения. А событие предоставляет ограниченный доступ к полю делегата, где потребители могут только подписаться или отказаться от подписки, но не читать список подписчиков и не заменять весь список сразу.

person Ben Voigt    schedule 28.04.2012
comment
Классический пример, помогающий понять это, - это использование таких вещей, как winforms - есть EventHandlerList, который поддерживает большинство событий (и который доступен через .Events для использования подклассами). Всего 1 поле - много много событий. - person Marc Gravell; 29.04.2012
comment
Я считаю, что это делается путем переопределения методов add и remove для события - например. второй пример msdn.microsoft.com/en-us/library/ak9w5846. aspx - person dsolimano; 29.04.2012
comment
Переопределение @dsolimano, вероятно, является запутанным термином для использования здесь, поскольку он обычно относится к полиморфизму; предоставление индивидуального add/remove, конечно - немного похоже на разницу между автоматически реализованным свойством и обычным свойством (термин для событий, похожих на автоматически реализованное свойство, - это событие, подобное полю, кстати) - person Marc Gravell; 29.04.2012
comment
Это потрясающий ответ +1! - person nawfal; 06.05.2013

Рассмотрим два способа объявления событий.

Либо вы объявляете событие, используя явный метод add/remove, либо вы объявляете событие без таких методов.

Другими словами, вы объявляете событие следующим образом:

public event EventHandlerType EventName
{
    add
    {
        // some code here
    }
    remove
    {
        // some code here
    }
}

или вы объявляете это так:

public event EventHandlerType EventName;

Дело в том, что в чем-то они одинаковы, а в чем-то совершенно разные.

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

Разница в том, что во втором примере кода выше эти методы будут предоставлены компилятором для вас, однако это все равно будет так. Чтобы подписаться на событие, вы вызываете метод.

Синтаксис для этого в C#, однако, одинаков, вы делаете либо:

objectInstance.EventName += ...;

or:

objectInstance.EventName -= ...;

Таким образом, с «внешней точки зрения» эти два пути ничем не отличаются.

Однако внутри класса есть разница.

Если вы пытаетесь получить доступ к EventNameидентификатору внутри класса, вы на самом деле ссылаетесь на field, который поддерживает свойство, но только если вы используете синтаксис, который явно не объявляет add /remove метод.

Типичный шаблон выглядит следующим образом:

public event EventHandlerType EventName;

protected void OnEventName()
{
    var evt = EventName;
    if (evt != null)
        evt(this, EventArgs.Empty);
}

В этом случае, когда вы имеете в виду EventName, вы на самом деле имеете в виду поле, содержащее делегата типа EventHandlerType.

Однако, если вы явно объявили методы add/remove, ссылка на идентификатор EventName внутри класса будет такой же, как и вне класса, поскольку компилятор не может гарантировать, что он знает поле или любой другой механизм, в котором вы сохранить подписку.

person Lasse V. Karlsen    schedule 28.04.2012

Событие — это аксессор для делегата. Так же, как свойство является средством доступа к полю. С помощью той же самой утилиты он предотвращает взаимодействие кода с объектом делегата. Как свойство имеет методы доступа get и set, так и событие имеет методы доступа add и remove.

Это ведет себя несколько иначе, чем свойство, если вы не пишете методы доступа add и remove самостоятельно, тогда компилятор автоматически генерирует их. Включая частное резервное поле, в котором хранится объект делегата. Подобно автоматическому свойству.

Вы не делаете это часто, но это, конечно, не является чем-то необычным. Платформа .NET довольно часто делает это, например, события элементов управления Winforms хранятся в EventHandlerList и методы доступа add/remove манипулируют этим списком с помощью методов AddHandler() и RemoveHandler(). С тем преимуществом, что для всех событий (а их много) требуется только одно поле в классе.

person Hans Passant    schedule 28.04.2012
comment
Событие — это средство доступа для поля, тип которого является типом делегата. - person Ben Voigt; 29.04.2012
comment
Прочтите ответ, чтобы убедиться, что это не так. Последний абзац. - person Hans Passant; 29.04.2012
comment
Это так же верно, как и второе предложение в вашем ответе. - person Ben Voigt; 29.04.2012
comment
Хм, нет, разные истины. Смысл не позволять компилятору генерировать реализацию по умолчанию с резервным полем заключается в том, что вам не нужно или не нужно резервное поле. Точно такие же рассуждения для свойств. В этом суть Скита: события не являются полями. - person Hans Passant; 29.04.2012
comment
Суть Скита в том, что события — это методы, предоставляющие доступ к значению, а не к самому значению. Так же, как свойство, но с другими операциями. Значение обычно, но не всегда, хранится в поле. Опять же, как собственность. Уровень педантизма, связанный с заявлением о том, что событие может не иметь резервного поля, также исключает общее утверждение о том, что свойство является средством доступа к полю. - person Ben Voigt; 29.04.2012
comment
Ух ты плохо троллишь. Нет байта. - person Hans Passant; 29.04.2012

Я могу добавить к прежним ответам, что делегаты могут быть объявлены внутри области пространства имен (вне класса), а события могут быть объявлены только внутри класса. Это потому, что делегат — это класс!

Еще одно отличие состоит в том, что для событий только содержащий их класс может их запустить. Вы можете подписаться/отписаться от него через содержащий класс, но не можете запустить его (в отличие от делегатов). Так что, может быть, теперь вы понимаете, почему соглашение состоит в том, чтобы обернуть его внутри protected virtual OnSomething(object sender, EventArgs e). Это для потомков, чтобы иметь возможность отменить реализацию стрельбы.

person graumanoz    schedule 02.05.2012