.NET: Как работает исправление состояния гонки EventHandler? - PullRequest
11 голосов
/ 04 февраля 2011

Существует следующий шаблон, который используется, чтобы избежать состояния гонки при возникновении событий в случае, если другой поток отписывается от 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, назначенный переменной 'handler', затем, когда кто-нибудь изменит MyEvent, объект, на который ссылается 'handler', также будет изменен.

Очевидно, что это не так, иначе этот изящный маленький паттерн не сработает.

Я изучил исходный код .NET и до сих пор не смог найти свой ответ там (он, вероятно, есть, но я искал около часа и не смог его найти, поэтому я здесь). Я также читал, что спецификация языка C # говорит о событиях и делегатах, но это не решает эту проблему.

Спасибо за ваше время.

Ответы [ 3 ]

9 голосов
/ 04 февраля 2011

Я ожидаю, что, как только я получу MyEvent делегат внутри «обработчика» ссылка, как только кто-то изменится MyEvent, что объект, который "обработчик" ссылки также будут изменены. [..] Обратите внимание, что System.Delegate является классом, а не структурой.

Хотя вы и правы, что типы делегатов являются ссылочными типами, они являются неизменяемыми ссылочными типами. От System.Delegate:

"Делегаты неизменны; один раз создан, список вызовов делегат не меняется. [...] Объединение операций, таких как Объединение и удалить, не изменять существующие делегаты. Вместо такого операция возвращает нового делегата, который содержит результаты операции, неизменный делегат или ничего.


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

6 голосов
/ 04 февраля 2011

Обновление

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

Первое: копирование ссылки.

x = y

На приведенной выше схеме ссылка , содержащаяся в y, копируется в x.Никто не говорит, что объект скопирован;имейте в виду - они указывают на один и тот же объект.

Второй: назначение новой ссылки на переменную.

y +=

Забудьте об операторе += длямомент;Я хочу подчеркнуть, что y присваивается другая ссылка для нового объекта.Это не влияет на x, потому что x является собственной переменной.Помните, только ссылка («адрес» на диаграмме) была скопирована в y.

Третье: то же самое, только в x.

x +=

На приведенных выше диаграммах изображены string объекты только потому, что их легко представить графически.Но то же самое с делегатами (и помните, стандартные события - это просто обертки вокруг полей делегатов).Вы можете видеть, как копируя ссылку в y в x выше, мы создали переменную, которая не будет затронута последующими присваиваниями y.

вся идея стандартного «исправления» состояния гонки EventHandler, с которым мы все знакомы.


Оригинальный ответ

Вероятно, вас смущает этот хитрый маленький синтаксис:

someObject.SomeEvent += SomeEventHandler;

Важно понимать, что, как указывает Ани в своем ответе , делегаты являются неизменяемыми ссылочными типами (подумайте: точно так же, как string).Многие разработчики ошибочно считают, что они изменчивы, потому что приведенный выше код выглядит так, будто я «добавляю» обработчик в какой-либо изменяемый список.Это не так;оператор += является оператором присваивания : он принимает возвращаемое значение оператора + и присваивает его переменной слева.

(Think: intявляется неизменным, и все же я могу сделать int x = 0; x += 1; правильно? Это то же самое.)


РЕДАКТИРОВАТЬ : Да, технически это не совсем правильно.Вот что на самом деле происходит.event на самом деле является оберткой вокруг поля делегата, которое доступно только (для внешнего кода) операторами += и -=, которые скомпилированы для вызовов кadd и remove соответственно.Таким образом, это очень похоже на свойство, которое (обычно) является оберткой вокруг поля , где доступ к свойству и вызов = скомпилированы для вызовов get и set.

Но суть остается: когда вы пишете +=, вызываемый метод add внутренне присваивает ссылку на новый объект внутреннему полю делегата.Я прошу прощения за упрощение этого объяснения в моем первоначальном ответе;но ключевой принцип, который нужно понять, тот же.

Кстати, я не охватываю пользовательские события, где вы можете поместить свою собственную логику в add и remove методы.Этот ответ относится только к «нормальному» случаю.


Другими словами, когда вы делаете это ...

EventHandler handler = SomeEvent;
if (handler != null)
{
    handler(this, EventArgs.Empty);
}

... вы действительно копируете ссылка в переменную.Теперь эта ссылка находится в локальной переменной и сама не будет изменена.Если он указывал на фактический объект во время присваивания, он будет продолжать указывать на тот же (неизменный) объект на следующей строке.Если это было , а не , указывающее на объект (null), то оно все равно не будет указывать на объект в следующей строке.

Так что, если код в другом месте подписался или отписался на событиеиспользуя +=, он действительно изменил исходную ссылку, чтобы указать на совершенно новый объект .Старый объект делегата все еще существует, и у вас есть ссылка на него: в вашей локальной переменной.

1 голос
/ 04 февраля 2011

Я хотел бы отметить, что сравнение этого инцидента со случаем 'int', вероятно, по своей сути неверно, поскольку, хотя 'int' является атомарным, это тип значения.решен случай:

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

Delegate.CombineImpl Метод показывает реализацию.

Я просмотрел реализациюДелегирование и MulticastDelegate в исходном коде .NET 4.Ни один из них не объявляет оператор + = или - =.Если подумать, в Visual Basic.NET их даже нет, вы используете AddHandler и т.д ...

Это означает, что компилятор C # реализует эту функциональность, а тип на самом деле нене имеет ничего общего с определением специализированных операторов.

Так что это приводит меня к логическому выводу, когда вы делаете:

EventHandler handler = MyEvent;

компилятор C # переводит его в

EventHandler handler = EventHandler.Combine(MyEvent)

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

Большое вам спасибо!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...