Pull to refresh

Comments 60

Действительно, для новичков может быть полезно. Однако, автор забыл описать классы Handler_I и Handler_II и их реализации методов Message(). Там будет все просто, но для новичков это может быть не так то уж и очевидно.
Они в самом начале описаны.
Прошу прощения, пропустил. Читал статью, а думал больше о проекте (текущая задача адовая).
Поправьте пожалуйста имена по CamelCase стандарту: Handler1 вместо Handler_I и OnCount вместо onCount для события. А то новичкам такое вредно показывать.
Запомните! Если Вы не подписались на событие и его делегат пустой, возникнет ошибка.
Чтобы избежать этого, необходимо подписаться, или не вызывать событие вообще, как показано на примере (Т.к. событие — делегат, то его отсутствие является «нулевой ссылкой» null).


Пример, сам по себе, является образцом как не нужно делать, между проверкой и вызовом, может произойти отписывание от события и тогда получим NullReferenceException. Выходов из этой ситуации ровно два. Первый — копируем событие в локальную переменную и работаем с ней, второй — в месте инициализации класса добавляем пустой обработчик для события, исключая ситуацию, когда на него никто не подписан и оно равно null.
Полностью солидарен с вашим замечанием. Внесу свои 5 копеек для прояснения ситуации:

class ClassCounter  //Это класс - в котором производится счет.
{
        ...
        ...
        ...
        //Событие OnCount c типом делегата MethodContainer.
        public event MethodContainer onCount;
        ...
        ...
        ...
        public ClassCounter ()
        {
            onCount = () => { };
        }
}

После такой простой инициализации события в конструкторе уже можно не бояться словить NullReferenceException.
Многие прогрессивные посоны объявляют события так:

public event MethodContainer OnCount = delegate{};
Или даже вот так:

public event Action OnCount = delegate{};
Статья-то не для прогрессивных)))
А может даже так:
public event EventHandler MyEvent = delegate { }; 
В таком случае можно использовать в дополнение к проверке на null еще и try catch, чтобы исключить такую возможность. Не лежит у меня душа к созданию сущности ( delegate{} ), что не будет задействована в логике выполнения программы )
Если не сложно, поясните, пожалуйста, эту «магию» —
onCount = () => { };
Что происходит в этой строке?
Создание анонимной функции на основе лямбда-выражения и создание на ее основе делегата подходящего типа.
() => {} — это анонимный метод. Они необходимы в тех случаях, когда программисту незачем париться над именами методов и вызываться они будут, как правило в одном единственном месте. Пустыми круглыми скобками мы сообщаем компилятору, что наш анонимный метод не будет иметь параметров. Анонимные методы всегда имеют тип возврата void. Как правило цепочка события-делегаты-анонимные методы образуют единое звено, применение одного без другого мало чем может быть полезна. Такая нотация очень удобна, не приходится дробить логику класса на дополнительное объявление методов.
По большому счету onCount = () => { }; эквивалентна следующей:




void Method1()
{
// здеся пустота!
}




onCount = Method1;

Согласитесь, что первый вариант гораздо компактнее и удобнее? Не согласились?) Верю, новичкам немного сложно привыкнуть к ним. Советую почитать в интернете побольше на данную тематику статей, тема немаленькая, но интересная и очень часто в жизни это может пригодиться
Xored, если Вы начинающий, то лямбда для Вас, скорее всего лишнее. Уважаемый kin9pin, не плохо объяснил «что это». Огромное ему спасибо.
Отлично разжевано для новичков!

Примите совет: названия событий не должны начинаться с On… По принятой в .NET системе именования с On… начинаются названия методов, которые эти самые события генерят, например OnTextChanged генерирует событие TextChanged.
Не грех будет добавить что если следовать Framework Guidelines эти самые OnXXX методы должны быть virtual, как минимум protected для не-sealed классов и по возможности не содержать большого кол-ва логики кроме вызова события.
Спасибо за статью. А не могли бы вы еще рассказать про возможные утечки памяти при работе с событиями и про то, как с ними бороться?
В двух словах не расскажешь, ув. dzigoro. Может этого стоит отдельная статья. Но в двух словах: контролируйте время жизни циклов, методов, переменных. А также области видимости, чтобы «спецы» не поломали Ваш проект. А они не любят порой разбираться в Вашем коде.
Если временный (маложивущий) объект подпишется на событие долгоживущего объекта и забудет отписаться, то он останется в памяти до сборки долгоживущего. Его будет держать подписка. Это наиболее частый случай утечки.

Многие разработчики утверждают (и я с ними согласен), что главная проблема «недопонимания» событий — их специфическая область применения, а вследствие — мало доступных примеров. Ну и не забывайте о практике

Ну если вы являетесь веб разработчиком, то скорее всего не встретите. Мне на практике не приходилось.
Но вот если вы разрабатываете на WPF/Silverlight, то вам гарантированно придется работать с делегатами и эвентами. Ну если конечно вы не пишите банальные «hello, world» и rss фиды.
Спасибо за статью, для новичков самое оно.
Но, считаю, что не раскрыта два важных аспекта:
1) метод GetInvocationList зачем он нужен
2) конструкция public event EventHandler Changed

class Class {
EventHandler сhanged;
public event EventHandler Changed {
add {
changed += value;
}
remove {
chnaged -=value;
}
}
}
А также не раскрыта разница между
public event EventHandler Changed
и
public EventHandler Changed
поскольку в обоих случаях можно подписаться на событие, но ключевое слово event вносит свои тонкости

Кроме того неплохо бы описать для новичков наличие стандартных делегатов Func<> и Action<>
Ув. urrri. Я писал статью с минимальным погружением в .NET. Это базовый уровень.
Ну уж разница между использование и неиспользованием ключевого слова это совсем основа. Иначе вы рассказали не о эвенте а о делегате — в вашем коде можно спокойно убрать все слова event и ничего не изменится
Я имел в виду Вашу просьбу об описании Func<> и Action<>. Не путайте базовый C# и базовый .NET, основанный на С#.
Помните, очень важно не только подписываться от событий, но и отписываться потом. События хранят ссылки на объекты подписки и пренебрежение отписыванием делает невозможной работу мусорщика. Часто бывает, что делают одно событие которое живет долго и подписывают на него кучу короткоживущих объектов. Не будете отписываться после вызова — вся эта котовасия начнет вам забивать оперативную память.
Тема, пожалуй, полезная, но слишком уж поверхностно раскрыта. В комментариях, в целом все проблемы озвучены:
  • возможность null-reference
  • не упомянуты операции подписки add/remove
  • не упомянуты удобные helper-ы в виде EventHandler и EventHandler(T);

Итого, как мне кажется, для совсем новичков информации маловато, а для более-менее ушедших от «новичков» — недостаточно.
Тут самое главное не написано — в чем, собственно, разница между событием и полем или свойством делегатного типа.
Если Вы пишете статью для новичков — пожалуйста, упомянтье о майкросотвовских стандартах для сигнатур и именовании методов, кидающих события и самих событий и приведите код в примерах к этим стандартам.
Я не хотел в этой статье употреблять ни стандарты, ни «фишки» .NET-a, ничего лишнего. Программист все равно за определенный промежуток времени приобретет свой стиль и научится писать код, как ему удобно (или как на его конторе хотят). У каждого в голове за столько лет — свой стандарт.
Ключевое слово event — это исключительно фигка C#, его к сожалению нету в питоне, джаве, плюсах и многих других меинстримовых языках. Если Вам хотелось рассказать про общий паттерн Observer, то имело смысл отвязаться как от слова event, так и от делегатов, так как анонимных методов тоже много где нет.
В таком виде, как оно представлено в посте, оно применимо только для C#, поэтому лучше использовать общепринятые соглашения для этого языка.
Как показывает практика, мало кому в голову приходит, почему EventHandler имеет такую странную сигнатуру, и зачем он вообще нужен, если есть Action. А ведь он такой не зря, просто его достоинства не слишком очевидны. Это я к чему, настоятельно рекомендовал бы начинающим писать события так, как предложено умными дядьками из Microsoft, а ещё лучше разобраться, почему они так предлагают.
Да, вот это действительно отличная статья по событиям. Понятная и довольно полная.
Если что, то я похвалил не вашу статью, а ту, что приведена в ссылке выше.
На мой взгляд, новичку гораздо проще понять события, если вообще забить на эти попытки абстрагирования и просто научиться воспринимать функцию как сущность, с которой можно работать как с обычной переменной — присваивать ее, передавать как аргумент и т.д. Тем более, что для современного шарпа это понимание важно и практично. Ведь вся суть событий сводится к тому, что функция куда-то передается и потом вызывается уже в том месте.
Согласен. А также многие программисты были бы еще лучше, если бы понимали суть программирования в целом. Многие заучивают книги перед собеседованиями, боятся все забыть и не могут отличить, «пардон», абстрактный от статического. Беда.
Не сочтите за пиар, но для более полного понимания/углублённого изучения я бы посоветовал эту статью.
Так как события в .NET реализованы криво, прогрессивное человечество использует Rx.
Решили похвастаться в статье для новичков? Аплодисменты в студию.
Автор, ты какой-то мнительный.
А что кривого в событиях C#? Опишите чем конкретно недовольно прогрессивное человечество
А что кривого в событиях C#? Опишите чем конкретно недовольно прогрессивное человечество


События в .NET являются синтаксическим сахаром для паттерна Observer. Причём сахаром довольно никчемным, потому что упрощения от них чуть, а ограничения весьма существенные.

Как правило среди наблюдателей (observers) и наблюдаемого (observable) большим сроком жизни (областью видимости) обладает последний. Это значит, что в их взаимодействии критически важной становится отписка от событий. В противном случае мы сталкиваемся с утечками памяти — подвисанием короткоживущих объектов, которые после своего использования недоступны сборщику мусора, даже если отсутствуют прямые ссылки на них в пользовательском коде (поскольку продолжают оставаться неявные ссылки из источника событий).

В нормальных реализациях паттерна Observer для отписки используются токены подписки (subscriptions), желательно совместимые с идиомой RAII (отписка через деструкторы в C++ или через IDisposable в C#). Именно так это сделано в Boost.Signals2 в C++ или в Rx в C#. Правильный синтаксический сахар в C# должен был выглядеть так (псевдокод):

    private IDisposable _subscription;
    ...
    _subscription = publisher.SomeEvents += subscriber.HandleSomeEvent;
    ...
    _subscription.Dispose();


Токен подписки не требует тащить всю дорогу явную ссылку на обработчик, чтобы отписаться. Но в C# последнее сделано иначе:

    publisher.SomeEvents -= subscriber.HandleSomeEvent;


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

    publisher.SomeEvents += (sender, e) => { Debug.Log(“Some message.”); subscriber.HandleSomeEvent(sender, e); }


потому что:

    publisher.SomeEvents -= ???


Обычно observers и observable друг о друге ничего не знают, а подписку инициирует какая-то третья сторона (назовём её «менеджер»).

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

    _subscription = observable.SomeEvents.Subscribe(observer.HandleSomeEvent);


Для того, чтобы отписаться, ему не нужны ни подписчик, ни источник, достаточно безликого поля IDisposable:

    _subscription.Dispose();


В случае отписки от событий C# всё сильно хуже, зачем-то нужны и подписчик, и издатель:

    observable.SomeEvent -= observer.HandleSomeEvent;


То есть безосновательно увеличена связность.

Выше уже писали про кривой синтаксис возбуждения событий (raise events). В отсутствие подписчиков (зауряднейшая ситуация) вместо простого «ничего» получаем на ровном месте `NullReferenceException`. Причём нельзя просто так взять, и проверить на `null`, как в статье, потому что между проверкой и возбуждением события может отписаться последний подписчик, и вместо ожидаемого пустого invokation list получаем `null`; выше уже указывали на это.

Далее, события в C# не могут сигнализировать об окончании потока `OnComplete()` или об ошибке `OnError(exception)`. Не могут на лету комбинироваться и преобразовываться Linq-методами. Не предусмотрено библиотечного механизма для разделения «холодных» и «горячих» потоков событий, буферизующих, и т.д.
Внушает. После такого фундаментального комментария осталось только написать про то, как эти проблемы решены в упомянутом Rx.
Ещё забыл упомянуть: при возбуждении исключения в одном из обработчиков стандартных событий C#, последующие обработчики не будут выполнены. Ну нелепо же.

как эти проблемы решены в упомянутом Rx.

Они не то чтобы решены, они просто изначально не заложены в дизайн.
Как-то мне кажется дизайн приложения хромает. Событие onCount должно бы файриться на каждой итерации цикла со значением i в EventArgs, а уже сами подписчики должны решать, что им с этим чудом делать. :)
Недавно начал изучать C# после многих лет на плюсах. И вот, обнаруживаю, что в C# существует стандартный механизм сигналов, очень для меня полезный. Но вдруг выясняется, что я не могу просто привязать некий объект к событиям и заставить его отписаться от событий во время его уничтожения без каких-то дополнительных действий, воспользовавшись этим стандартным механизмом. А как же RAII? Видимо, я что-то не понимаю в концепции языка.

В каких случаях чаще всего используются события с C#? Кто контролирует отписку?
> А как же RAII? Видимо, я что-то не понимаю в концепции языка.

Аналогом плюсового деструктора в C# является метод `Dispose()` интерфейса `IDisposable` (не путать с тильдой-финализатором). RAII в C# реализуется через паттерн Disposable. Аналогом плюсового локального скоупа является синтаксис `using (var foo = new SomeDisposable()) {… }` — метод `foo.Dispose()` вызовется при покидании скоупа при достижении конца, `return` или исключении.

> Кто контролирует отписку?

Для контролируемой отписки а-ля RAII как в boost::signals2 проще использовать не стандартные события, а Rx, см. мой комментарий выше.

> В каких случаях чаще всего используются события с C#?

Сырые события .NET удобно использовать в ситуациях, когда время жизни подписчика заведомо не меньше времени жизни источника событий, тогда отписка не нужна. При желании можно вручную сконструировать «токен подписки», `Dispose()` которого вызовет отписку:

EventHandler<MyArgs> handler = (sender, e) => { Debug.Log(“Some message.”); subscriber.DoSomething(e.SomeProperty, e.AnotherProperty); }
publisher.SomeEvent += handler;
// Замыкаем подписчика и источник событий в безликом `Action`:
Action actionToBeCalledOnDispose = () => { publisher.SomeEvent -= handler; }
_subscription = Disposable.Create(actionToBeCalledOnDispose);
Просто, доступно понятно. Пускай полежит у меня в избранном
Sign up to leave a comment.

Articles