От переводчика
Часто начинающие разработчики спрашивают, зачем при вызове обработчика нужно копировать его в локальную переменную, а как показывает код ревью, даже опытные разработчики забывают об этом. В C# 6 разработчики языка добавили много синтаксического сахара, в том числе null-conditional operator (null-условный оператор или Элвис-оператор — ?.), который позволяет нам избавиться от ненужного (на первый взгляд) присваивания. Под катом объяснения от Джона Скита — одного из самых известных дот нет гуру.
Проблема
Вызов обработчика в языке C# всегда сопровождался не самым очевидным кодом, потому что событие, у которого нет подписчиков, представлено в виде null ссылки. Из-за этого мы обычно писали так:
public event EventHandler Foo; public void OnFoo() { EventHandler handler = Foo; if (handler != null) { handler(this, EventArgs.Empty); } }
Локальную переменную handler нужно использовать потому, что без нее к обработчику события Foo доступ идет 2 раза (при проверке на null и при самом вызове). В таком случае есть вероятность, что последний подписчик удалится как раз между этими доступами к Foo.
// Плохой код, не делайте так! if (Foo != null) { // Foo может быть null, если доступ // к классу идет из нескольких потоков. Foo(this, EventArgs.Empty); }
Этот код можно упростить, создав метод расширения:
public static void Raise(this EventHandler handler, object sender, EventArgs args) { if (handler != null) { handler(sender, args); } }
Тогда используя этот метод расширения, первый вызов перепишется:
public void OnFoo() { Foo.Raise(this, EventArgs.Empty); }
Минус данного подхода в том, что метод расширения придется писать для каждого типа обработчика.
C# 6 нас спасет!
Null-условный оператор (?.), появившийся в C# 6, может использоваться не только для доступа к свойствам, но и для вызова методов. Компилятор вычисляет выражение только один раз, поэтому код можно писать без использования метода расширения:
public void OnFoo() { Foo?.Invoke(this, EventArgs.Empty); }
Ура! Этот код никогда не выбросит NullReferenceException, и нам не нужны вспомогательные классы.
Конечно, было бы лучше, если бы мы могли написать Foo?(this, EventArgs.Empty), но тогда это был бы уже не ?. оператор, что немного усложнило бы язык. Поэтому дополнительный вызов Invoke меня не сильно беспокоит.
Что это за штука — потокобезопасность?
Написанный нами код является «потокобезопасным» в том смысле, что ему все равно, что делают другие потоки — мы никогда не получим NullReferenceException. Однако, если другие потоки подписываются или отменяют подписку на событие, мы можем не увидеть самые последние изменения в списке подписчиков события. Это происходит из-за сложностей в реализации общей модели памяти.
В C# 4 события реализованы с помощью метода Interlocked.CompareExchange, поэтому мы просто можем использовать правильный метод Interlocked.CompareExchange, чтобы убедиться, что получим самое последнее значение. Теперь мы можем объединить эти 2 подхода и написать:
public void OnFoo() { Interlocked.CompareExchange(ref Foo, null, null)?.Invoke(this, EventArgs.Empty); }
Теперь без написания дополнительного кода мы можем уведомить самый последний набор подписчиков, без риска свалиться с NullReferenceException. Спасибо David Fowler за напоминание о такой возможности.
Конечно, вызов CompareExchange выглядит некрасиво. Начиная с .NET 4.5 и выше существует метод Volatile.Read, который может решить нашу проблему, но мне не до конца ясно (если читать документацию), делает ли этот метод то, что нужно. (В описании метода говорится, что он запрещает ставить последующие операции чтения/записи до этого метода, в нашем же случае нужно запретить ставить предшествующие операции записи после этого изменяемого чтения).
public void OnFoo() { // .NET 4.5+, может быть потокобезопасно, а может и не быть... Volatile.Read(ref Foo)?.Invoke(this, EventArgs.Empty); }
Такой подход мне не нравится, потому что я не уверен, что все предусмотрел. Продвинутые читатели, возможно, смогут подсказать, почему такой подход не верен и не попал в BCL.
Альтернативный подход
В прошлом я пользовался таким альтернативным решением: создаем пустой фиктивный обработчик события, используя одно преимущество анонимных методов, которое у них есть по сравнению с лямбда-выражениями — возможность не указывать список параметров:
public event EventHandler Foo = delegate {} public void OnFoo() { // Foo will never be null Volatile.Read(ref Foo).Invoke(this, EventArgs.Empty); }
При таком подходе все еще остаются проблемы с тем, что мы можем вызывать не самый последний список подписчиков, но зато нам не надо волноваться о проверке на null и NullReferenceException.
Исследуем MSIL
От переводчика: этой части нет в статье Джона, это мои личный изыскания в ildasm’е.
Посмотрим, какой MSIL код генерируется в разных случаях.
public event EventHandler Foo; public void OnFoo() { if (Foo != null) { Foo(this, EventArgs.Empty); } } .method public hidebysig instance void OnFoo() cil managed { // Code size 35 (0x23) .maxstack 3 .locals init ([0] bool V_0) IL_0000: nop IL_0001: ldarg.0 // кладем this в стек IL_0002: ldfld class [mscorlib]System.EventHandler A::Foo // кладем в стек поле Foo IL_0007: ldnull // кладем в стек null IL_0008: cgt.un // сравниваем 2 верхних значения в стеке (Foo и null) - если равны, то кладем в стек 0 (false) IL_000a: stloc.0 // сохраняем результат во временную локальную переменную типа bool IL_000b: ldloc.0 // кладем ее в стек IL_000c: brfalse.s IL_0022 // если в стеке лежит false, то переходим к IL_0022 (return) IL_000e: nop IL_000f: ldarg.0 // кладем в стек this IL_0010: ldfld class [mscorlib]System.EventHandler A::Foo // кладем в стек поле Foo - !!!Вот тут можем положить уже null IL_0015: ldarg.0 // кладем в стек this IL_0016: ldsfld class [mscorlib]System.EventArgs [mscorlib]System.EventArgs::Empty // кладем в стек System.EventArgs::Empty IL_001b: callvirt instance void [mscorlib]System.EventHandler::Invoke(object, class [mscorlib]System.EventArgs) // вызываем Foo(this, EventArgs.Empty) IL_0020: nop IL_0021: nop IL_0022: ret } // end of method A::OnFoo
В этом коде мы дважды обращаемся к полю Foo: для сравнения с null (IL_0002: ldfld) и собственно вызова (IL_0010: ldfld). Между тем, как мы проверили Foo на равенство null, и тем, как заново получили к нему доступ, положили в стек и вызвали метод, от события могли отписаться последние подписчики, и второй раз загружен будет null — здравствуй, NullReferenceException.
Посмотрим, как решится проблема с помощью использования дополнительной локальной переменной.
public event EventHandler Foo; public void OnFoo() { EventHandler handler = Foo; if (handler != null) { handler(this, EventArgs.Empty); } } .method public hidebysig instance void OnFoo() cil managed { // Code size 32 (0x20) .maxstack 3 .locals init ([0] class [mscorlib]System.EventHandler 'handler', [1] bool V_1) IL_0000: nop IL_0001: ldarg.0 // кладем this в стек IL_0002: ldfld class [mscorlib]System.EventHandler A::Foo //ищем поле Foo, теперь оно наверху стека IL_0007: stloc.0 // сохраняем Foo в переменную handler IL_0008: ldloc.0 // кладем в стек handler IL_0009: ldnull // кладем в стек null IL_000a: cgt.un // сравниваем 2 верхних значения в стеке (handler и null) - если равны, то кладем в стек 0 (false) IL_000c: stloc.1 // сохраняем результат во временную локальную переменную типа bool IL_000d: ldloc.1 // кладем ее в стек IL_000e: brfalse.s IL_001f // если в стеке лежит false, то переходим к IL_001f (return) IL_0010: nop IL_0011: ldloc.0 // кладем в стек handler IL_0012: ldarg.0 // кладем в стек this IL_0013: ldsfld class [mscorlib]System.EventArgs [mscorlib]System.EventArgs::Empty // кладем в стек System.EventArgs::Empty IL_0018: callvirt instance void [mscorlib]System.EventHandler::Invoke(object, class [mscorlib]System.EventArgs) // вызываем handler(this, EventArgs.Empty) IL_001d: nop IL_001e: nop IL_001f: ret } // end of method A::OnFoo
В этом случае все просто: доступ к Foo происходит один раз (IL_0002: ldfld), потом вся работа идет с переменной handler, поэтому опасности получить NullReferenceException нет.
Теперь решение с использованием оператора ?..
public event EventHandler Foo; public void OnFoo() { Foo?.Invoke(this, EventArgs.Empty); } .method public hidebysig instance void OnFoo() cil managed { // Code size 26 (0x1a) .maxstack 8 IL_0000: nop IL_0001: ldarg.0 // кладем в стек this IL_0002: ldfld class [mscorlib]System.EventHandler A::Foo // кладем в стек поле Foo IL_0007: dup // дублируем в стеке Foo IL_0008: brtrue.s IL_000d // если в стеке лежит true или не null и не 0, то переходим к IL_000d (вызов метода) IL_000a: pop // очищаем стек - вытаскиваем из него Foo (мы попали сюда, если Foo == null) IL_000b: br.s IL_0019 // выходим из метода IL_000d: ldarg.0 // кладем в стек this (мы пришли сюда, если Foo != null) IL_000e: ldsfld class [mscorlib]System.EventArgs [mscorlib]System.EventArgs::Empty // кладем в стек EventArgs::Empty IL_0013: callvirt instance void [mscorlib]System.EventHandler::Invoke(object, class [mscorlib]System.EventArgs) // вызываем Invoke IL_0018: nop IL_0019: ret } // end of method A::OnFoo
В C# 6 с использованием оператора ?. все становится интереснее. Мы кладем в стек поле Foo, дублируем его (IL_0007: dup — вся магия тут), потом если оно не null — то идем к IL_000d и вызываем метод Invoke. Если же Foo == null, то очищаем стек и выходим (IL_000b: br.s IL_0019). Мы действительно всего один раз считываем Foo, поэтому NullReferenceException не произойдет.
Используем оператор ?. и Interlocked.CompareExchange.
public event EventHandler Foo; public void OnFoo() { Interlocked.CompareExchange(ref Foo, null, null)?.Invoke(this, EventArgs.Empty); } .method public hidebysig instance void OnFoo() cil managed { // Code size 33 (0x21) .maxstack 8 IL_0000: nop IL_0001: ldarg.0 // кладем в стек this IL_0002: ldflda class [mscorlib]System.EventHandler A::Foo // кладем в стек адрес поля Foo IL_0007: ldnull // кладем в стек null IL_0008: ldnull // кладем в стек null IL_0009: call !!0 [mscorlib]System.Threading.Interlocked::CompareExchange<class [mscorlib]System.EventHandler>(!!0&, !!0, !!0) // вызываем Interlocked::CompareExchange IL_000e: dup // дублируем в стеке Foo - последнюю версию, полученную через Interlocked::CompareExchange IL_000f: brtrue.s IL_0014 // если в стеке лежит true или не null и не 0, то переходим к IL_0014 (вызов метода) IL_0011: pop // очищаем стек - вытаскиваем из него Foo (мы попали сюда, если Foo == null) IL_0012: br.s IL_0020 // выходим из метода IL_0014: ldarg.0 // кладем в стек this IL_0015: ldsfld class [mscorlib]System.EventArgs [mscorlib]System.EventArgs::Empty // кладем в стек EventArgs::Empty IL_001a: callvirt instance void [mscorlib]System.EventHandler::Invoke(object, class [mscorlib]System.EventArgs) // вызываем Invoke IL_001f: nop IL_0020: ret } // end of method A::OnFoo
Этот код отличается от предыдущего только вызовом Interlocked.CompareExchange (IL_0009: call !!0 [mscorlib]System.Threading.Interlocked::CompareExchange), потом код точно такой же, как и в предыдущем методе (начиная с IL_000e).
Используем оператор ?. и Volatile.Read.
public event EventHandler Foo; public void OnFoo() { Volatile.Read(ref Foo)?.Invoke(this, EventArgs.Empty); } .method public hidebysig instance void OnFoo() cil managed { // Code size 31 (0x1f) .maxstack 8 IL_0000: nop IL_0001: ldarg.0 // кладем в стек this IL_0002: ldflda class [mscorlib]System.EventHandler A::Foo // кладем в стек адрес поля Foo IL_0007: call !!0 [mscorlib]System.Threading.Volatile::Read<class [mscorlib]System.EventHandler>(!!0&) // вызываем Volatile::Read IL_000c: dup // дублируем в стеке Foo - последнюю версию, полученную через Volatile::Read IL_000d: brtrue.s IL_0012 // если в стеке лежит true или не null и не 0, то переходим к IL_0012 (вызов метода) IL_000f: pop // очищаем стек - вытаскиваем из него Foo (мы попали сюда, если Foo == null) IL_0010: br.s IL_001e // выходим из метода IL_0012: ldarg.0 // кладем в стек this IL_0013: ldsfld class [mscorlib]System.EventArgs [mscorlib]System.EventArgs::Empty // кладем в стек EventArgs::Empty IL_0018: callvirt instance void [mscorlib]System.EventHandler::Invoke(object, class [mscorlib]System.EventArgs) // вызываем Invoke IL_001d: nop IL_001e: ret } // end of method A::OnFoo
В этом случае вызов Interlocked.CompareExchange меняется на вызов Volatile.Read, а потом (начиная с IL_000c: dup) все без изменений.
Все решения с использованием оператора ?. отличаются тем, что доступ к полю происходит один раз, для вызова обработчика используется его копия (MSIL команда dup), поэтому мы вызываем Invoke для точной копии объекта, который и сравнивали с null — NullReferenceException произойти не может. В остальном методы отличаются только тем, насколько быстро они подхватывают изменения в многопоточной среде.
Заключение
Да, C# 6 рулит — и не в первый раз. И нам уже доступна стабильная версия!
ссылка на оригинал статьи http://habrahabr.ru/post/272571/
Добавить комментарий