.NET: Как работает исправление состояния гонки EventHandler?
Существует следующий шаблон, который используется, чтобы избежать состояния гонки при поднятии событий в случае, если другой поток не подписывается из MyEvent, делая его нулевым.
class MyClass
{
public event EventHandler MyEvent;
public void F()
{
EventHandler handler = MyEvent;
if(handler != null)
handler(this, EventArgs.Empty);
}
}
в противоположность неправильному способу его выполнения, который склонен к этому состоянию гонки:
class MyClass
{
public event EventHandler MyEvent;
public void F()
{
if(MyEvent != null)
MyEvent(this, EventArgs.Empty);
}
}
Мой вопрос заключается в том, что System.Delegate
является ссылочным типом: в случае, если MyEvent не равен null, как получилось
EventHandler handler = MyEvent;
похоже, скопирует свой список вызовов вместо получения ссылки.
Я ожидал бы, что с делегатом MyEvent, назначенным переменной "обработчик", после того, как кто-то изменит MyEvent, будет также изменен объект, который ссылается на обработчик.
Очевидно, что это не так, иначе этот отличный шаблон не будет работать.
Я изучил исходный код .NET и до сих пор не смог найти ответ (возможно, там, но я искал около часа и не мог найти его, так что я здесь).
Я также прочитал, что спецификация языка С# должна сказать о событиях и делегатах, но это не касается этого вопроса.
Спасибо за ваше время.
Ответы
Ответ 1
Я бы ожидал, что как только я получу Делегат MyEvent внутри "обработчика" ссылку, как только кто-то изменится MyEvent, что объект, который "обработчик" ссылки также будут изменены. [..] Обратите внимание, что System.Delegate - это класс, а не структура.
Хотя вы правы, что типы делегатов являются типами ссылок, они являются неизменяемыми ссылочными типами. Из System.Delegate
:
"Делегаты неизменяемы, один раз созданный, список вызовов делегат не изменяется. [...] Объединение операций, таких как Combine и Удалить, не изменяйте существующие делегаты. Вместо этого операция возвращает нового делегата, который содержит результаты операции, неизменный делегат или ничего.
В другой заметке единственной проблемой, с которой обращаются эти шаблоны, является предотвращение попытки вызова нулевой ссылки-делегата. События склонны к гонкам, несмотря на это "исправление".
Ответ 2
Update
Вот несколько диаграмм, которые, как мы надеемся, должны устранить путаницу в отношении копирования ссылок и присваивания.
Во-первых: копирование ссылки.
![x = y]()
В приведенной выше схеме ссылка, содержащаяся в y
, копируется в x
. Никто не говорит, что объект скопирован; Имейте в виду - они указывают на один и тот же объект.
Во-вторых: назначение новой ссылки на переменную.
![y += "!"]()
Забудьте об операторе +=
на мгновение; то, что я хочу подчеркнуть выше, заключается в том, что y
присваивается другая ссылка, новому объекту. Это не влияет на x
, потому что x
- его собственная переменная. Помните, что только ссылка ( "адрес" на диаграмме) была скопирована на y
.
Третий: то же самое, только до x
.
![x += "?"]()
На приведенных выше диаграммах изображены объекты string
, только потому, что их легко представить графически. Но это то же самое с делегатами (и помните, что стандартные события - это всего лишь обертки вокруг полей делегатов). Вы можете увидеть, как, скопировав ссылку в y
на x
выше, мы создали переменную, на которую последующие присваивания не будут влиять на y
.
Вот и вся идея стандартного гоночного условия EventHandler
"fix", с которым мы все знакомы.
Оригинальный ответ
Вероятно, вас смущает этот сложный небольшой синтаксис:
someObject.SomeEvent += SomeEventHandler;
Что важно понимать, так как Ани указывает в своем ответе, делегаты являются неизменными ссылочными типами (подумайте: точно так же, как string
). Многие разработчики ошибочно считают, что они изменяемы, потому что вышеприведенный код выглядит так, как будто я добавляю обработчик к некоторому изменяемому списку. Это не так; оператор +=
является оператором присваивания : он принимает возвращаемое значение оператора +
и назначает его переменной слева.
(Думаю: int
неизменен, и все же я могу сделать int x = 0; x += 1;
правильно? Это то же самое.)
EDIT: Хорошо, технически это не совсем правильно. Вот что действительно происходит. event
фактически представляет собой оболочку вокруг поля делегата, доступного только (внешнему коду) операторами +=
и -=
, которые скомпилированы для вызовов на add
и remove
соответственно. Таким образом, это очень похоже на свойство, которое (как правило) является оболочкой вокруг поля, где доступ к свойству и вызов =
скомпилированы для вызовов на get
и set
.
Но точка все еще остается: когда вы пишете +=
, метод add
, который вызывается, внутренне назначает ссылку на новый объект во внутреннее поле делегата. Прошу прощения за упрощение этого объяснения в моем первоначальном ответе; но ключевой принцип для понимания тот же.
Кстати, я не рассматриваю пользовательские события, где вы можете поместить свою собственную логику внутри методов add
и remove
. Этот ответ относится только к "нормальному" случаю.
Другими словами, когда вы это делаете...
EventHandler handler = SomeEvent;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
... вы действительно копируете ссылку в переменную. Теперь эта ссылка находится в локальной переменной и сама не будет изменена. Если бы он указывал на фактический объект во время назначения, он будет продолжать указывать на тот же (неизменный) объект на следующей строке. Если он не указывал на объект (null
), он все равно не будет указывать на объект на следующей строке.
Итак, если код в другом месте, подписанный или отменивший подписку на событие с помощью +=
, то, что он действительно сделал, это изменить исходную ссылку, чтобы указать на совершенно новый объект. Старый объект делегирования по-прежнему существует, и у вас есть ссылка на него: в вашей локальной переменной.
Ответ 3
Я хотел бы указать, что сравнение этого инцидента с "int" случаем, по-видимому, является неправильным, поскольку, хотя "int" является атомарным, это тип значения.
Но я думаю, что мы решили случай:
Объединение операций, таких как Combine и Удалить, не изменяйте существующие делегаты. Вместо этого такая операция возвращает новый делегат, содержащий результаты операции, неизменный делегат или null. объединение операции возвращает null, когда результатом операции является делегат, который не ссылается на хотя бы один метод. Объединение операция возвращает неизменную делегировать, когда запрашиваемая операция не имеет эффекта.
Метод Delegate.CombineImpl
показывает реализацию.
Я просмотрел реализацию Delegate и MulticastDelegate в исходном коде .NET 4.
Ни один из них не объявляет оператор + = или - =. Подумав об этом, в Visual Basic.NET у вас их даже нет, вы используете AddHandler и т.д.
Это означает, что компилятор С# реализует эту функциональность и что тип не имеет ничего общего с определением специализированных операторов.
Итак, это приводит меня к логическому выводу, который есть, когда вы делаете:
EventHandler handler = MyEvent;
компилятор С# переводит его на
EventHandler handler = EventHandler.Combine(MyEvent)
Я удивлен, насколько быстро этот вопрос был решен благодаря вашей помощи.
Спасибо вам большое!