Обработчики событий в C # - Альтернативный подход [дубликат]

Вам нужно добавить зависимость buildscript Firebase Gradle в build.gradle (уровень проекта)

classpath 'com.google.gms:google-services:3.1.0'

и добавить плагин Firebase для Gradle в приложении / build.gradle

apply plugin: 'com.google.gms.google-services'

build.gradle will include these new dependencies:
    compile 'com.google.firebase:firebase-database:11.0.4'

Источник: Android Studio Assistant

34
задан Jérémie Bertrand 9 September 2010 в 09:23
поделиться

10 ответов

Существует небольшая вероятность, что SomethingHappened станет null после нулевой проверки, но до вызова. Однако MulticastDelagate s неизменяемы, поэтому, если вы сначала назначили переменную, нулевую проверку против переменной и вызовите ее, вы можете быть в безопасности от этого сценария (self plug: я написал сообщение в блоге об этом некоторое время назад).

Однако есть оборотная сторона монеты; если вы используете подход temp variable, ваш код защищен от NullReferenceException s, но может случиться так, что событие вызовет прослушиватели событий после того, как они были отделены от события . Это всего лишь кое-что, с чем можно справиться самым изящным способом.

Чтобы обойти это, у меня есть метод расширения, который я иногда использую:

public static class EventHandlerExtensions
{
    public static void SafeInvoke<T>(this EventHandler<T> evt, object sender, T e) where T : EventArgs
    {
        if (evt != null)
        {
            evt(sender, e);
        }
    }
}

Используя этот метод , вы можете вызвать такие события, как это:

protected void OnSomeEvent(EventArgs e)
{
    SomeEvent.SafeInvoke(this, e);
}
48
ответ дан Fredrik Mörk 21 August 2018 в 13:37
поделиться
  • 1
    Спасибо за отличный ответ (и сообщение в блоге). – Jérémie Bertrand 8 September 2010 в 16:13
  • 2
    Я также получил этот метод расширения в своей основной библиотеке с точно таким же именем, выполняю ту же работу точно так же! Мое имя параметра - eventHandler. – tia 8 September 2010 в 20:19
  • 3
    -1: There is a back side of the coin though; if you use the temp variable approach (...) it could be that the event will invoke event listeners after they have been detached from the event: это всегда возможность; это неизбежно. – ANeves 20 February 2014 в 13:57
  • 4
    В сообщении в блоге есть ссылка: последнее слово в вашем объяснении quirk должно быть «отсоединено», а не «прикреплено». – rism 5 October 2014 в 13:03
  • 5
    Стоит отметить, что с C # 6.0 мы теперь можем использовать оператор «elvis». (так называемый пусковой оператор), чтобы безопасно поднимать события. SomeEvent?.Invoke(sender, args); – Tim Long 10 November 2015 в 11:20

Это зависит от того, что вы подразумеваете под потокобезопасностью. Если ваше определение включает в себя предотвращение NullReferenceException, то первым примером является more safe. Однако, если вы переходите к более строгому определению, в котором обработчики событий должны быть вызываться, если они существуют, то ни один из них не является безопасным. Причина связана с сложностями модели памяти и барьеров. Может быть, на самом деле есть обработчики событий, привязанные к делегату, но поток всегда читает ссылку как null. Правильный способ исправить это - создать явный барьер памяти в точке, где ссылка делегата будет записана в локальную переменную.

  • Используйте ключевое слово lock (или любой механизм синхронизации).
  • Используйте ключевое слово volatile для переменной события.
  • Используйте Thread.MemoryBarrier.

Несмотря на проблему неудобного обзора, которая мешает вам выполнять однострочный инициализатор, я по-прежнему предпочитаю метод lock.

protected virtual void OnSomethingHappened(EventArgs e)           
{          
    EventHandler handler;
    lock (this)
    {
      handler = SomethingHappened;
    }
    if (handler != null)           
    {          
        handler(this, e);          
    }          
}          

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

6
ответ дан Brian Gideon 21 August 2018 в 13:37
поделиться

Я попытался высушить ответ Джесси С. Слайсер с помощью:

  • Возможность суб или отписки от любой нити во время рейза (состояние гонки удалено )
  • Перегрузки оператора для + = и - = на уровне класса
  • Определенные делегатом определенные делегаты
    public class ThreadSafeEventDispatcher<T> where T : class
    {
        readonly object _lock = new object();
    
        private class RemovableDelegate
        {
            public readonly T Delegate;
            public bool RemovedDuringRaise;
    
            public RemovableDelegate(T @delegate)
            {
                Delegate = @delegate;
            }
        };
    
        List<RemovableDelegate> _delegates = new List<RemovableDelegate>();
    
        Int32 _raisers;  // indicate whether the event is being raised
    
        // Raises the Event
        public void Raise(Func<T, bool> raiser)
        {
            try
            {
                List<RemovableDelegate> raisingDelegates;
                lock (_lock)
                {
                    raisingDelegates = new List<RemovableDelegate>(_delegates);
                    _raisers++;
                }
    
                foreach (RemovableDelegate d in raisingDelegates)
                {
                    lock (_lock)
                        if (d.RemovedDuringRaise)
                            continue;
    
                    raiser(d.Delegate);  // Could use return value here to stop.                    
                }
            }
            finally
            {
                lock (_lock)
                    _raisers--;
            }
        }
    
        // Override + so that += works like events.
        // Adds are not recognized for any event currently being raised.
        //
        public static ThreadSafeEventDispatcher<T> operator +(ThreadSafeEventDispatcher<T> tsd, T @delegate)
        {
            lock (tsd._lock)
                if (!tsd._delegates.Any(d => d.Delegate == @delegate))
                    tsd._delegates.Add(new RemovableDelegate(@delegate));
            return tsd;
        }
    
        // Override - so that -= works like events.  
        // Removes are recongized immediately, even for any event current being raised.
        //
        public static ThreadSafeEventDispatcher<T> operator -(ThreadSafeEventDispatcher<T> tsd, T @delegate)
        {
            lock (tsd._lock)
            {
                int index = tsd._delegates
                    .FindIndex(h => h.Delegate == @delegate);
    
                if (index >= 0)
                {
                    if (tsd._raisers > 0)
                        tsd._delegates[index].RemovedDuringRaise = true; // let raiser know its gone
    
                    tsd._delegates.RemoveAt(index); // okay to remove, raiser has a list copy
                }
            }
    
            return tsd;
        }
    }
    

Использование:

    class SomeClass
    {   
        // Define an event including signature
        public ThreadSafeEventDispatcher<Func<SomeClass, bool>> OnSomeEvent = 
                new ThreadSafeEventDispatcher<Func<SomeClass, bool>>();

        void SomeMethod() 
        {
            OnSomeEvent += HandleEvent; // subscribe

            OnSomeEvent.Raise(e => e(this)); // raise
        }

        public bool HandleEvent(SomeClass someClass) 
        { 
            return true; 
        }           
    }

Любые серьезные проблемы с этим подходом?

Код был только ненадолго протестирован и отредактирован немного на вставке. Подтвердите, что List & lt;> не отличный выбор, если много элементов.

1
ответ дан Community 21 August 2018 в 13:37
поделиться

На самом деле нет, второй пример не считается потокобезопасным. Событие SomethingHappened может вычисляться с ненулевым значением в условном выражении, затем при вызове должно быть null. Это классическое состояние гонки.

1
ответ дан Curt Nichols 21 August 2018 в 13:37
поделиться

Я поддерживаю этот фрагмент как ссылку для безопасного многопоточного доступа к событиям для установки и стрельбы:

    /// <summary>
    /// Lock for SomeEvent delegate access.
    /// </summary>
    private readonly object someEventLock = new object();

    /// <summary>
    /// Delegate variable backing the SomeEvent event.
    /// </summary>
    private EventHandler<EventArgs> someEvent;

    /// <summary>
    /// Description for the event.
    /// </summary>
    public event EventHandler<EventArgs> SomeEvent
    {
        add
        {
            lock (this.someEventLock)
            {
                this.someEvent += value;
            }
        }

        remove
        {
            lock (this.someEventLock)
            {
                this.someEvent -= value;
            }
        }
    }

    /// <summary>
    /// Raises the OnSomeEvent event.
    /// </summary>
    public void RaiseEvent()
    {
        this.OnSomeEvent(EventArgs.Empty);
    }

    /// <summary>
    /// Raises the SomeEvent event.
    /// </summary>
    /// <param name="e">The event arguments.</param>
    protected virtual void OnSomeEvent(EventArgs e)
    {
        EventHandler<EventArgs> handler;

        lock (this.someEventLock)
        {
            handler = this.someEvent;
        }

        if (handler != null)
        {
            handler(this, e);
        }
    }
13
ответ дан Jesse C. Slicer 21 August 2018 в 13:37
поделиться
  • 1
    Да, мы с тобой на одной странице. Принятый ответ имеет тонкую проблему с барьером памяти, которую разрешает наше решение. Использование пользовательских обработчиков add и remove, вероятно, не нужно, поскольку компилятор испускает блокировки в автоматических реализациях. Хотя, я думал, что я помню, что что-то изменилось с этим в .NET 4.0. – Brian Gideon 8 September 2010 в 17:21
  • 2
    @Brian - согласен, хотя до 4.0, блокировки находятся на объекте this, а это означает, что код, внешний для класса, может привести к запутыванию механизма путем блокировки экземпляра. Джон Скит представил вдохновение здесь csharpindepth.com/Articles/Chapter2/Events.aspx#threading . – Jesse C. Slicer 8 September 2010 в 17:42
  • 3
    Отличная ссылка. И да, я признаю все нюансы с блокировкой на this. У кого есть быстрая ссылка на изменения в .NET 4.0? Если нет, я просто подниму спецификацию. – Brian Gideon 8 September 2010 в 18:43
  • 4
    Вот достойная небольшая статья: blogs.msdn.com/b/cburrows/archive/2010/03/05/… – Jesse C. Slicer 9 September 2010 в 03:18
  • 5
    Мне интересно, почему бы не просто поднять обработчик, находясь под замком (не нужно также копировать)? Тогда после того, как он отменил подписку, нельзя было бы отказаться от подписчика. Разумеется, компромисс заключается в том, что субподъязывание запрещено, во время рейза, но, по крайней мере, не существует жесткой гонки. Правильно ли я понимаю, что в принятом ответе утверждается, что этот сценарий не может быть обработан? – crokusek 19 October 2013 в 04:38

Объявите ваше событие как это, чтобы получить безопасность потоков:

public event EventHandler<MyEventArgs> SomethingHappened = delegate{};

И вызывать его следующим образом:

protected virtual void OnSomethingHappened(MyEventArgs e)   
{  
    SomethingHappened(this, e);
} 

Хотя этот метод больше не нужен.

7
ответ дан jgauffin 21 August 2018 в 13:37
поделиться
  • 1
    Это может вызвать некоторые проблемы с производительностью, если у вас много событий, поскольку пустой делегат будет выполняться каждый раз независимо от того, есть ли законные подписчики на мероприятие или нет. Это небольшие накладные расходы, но теоретически может складываться. – Adam Lear♦ 8 September 2010 в 16:04
  • 2
    Это небольшие расходы на очень . В вашем коде есть большие проблемы, которые следует оптимизировать в первую очередь. – jgauffin 8 September 2010 в 18:37
  • 3
    этот защитник от ошибки nullreference, но не строго потокобезопасный, см. обсуждение stackoverflow.com/questions/786383/… – Ben 29 July 2018 в 10:10

Начиная с C # 6.0 вы можете использовать монодичный оператор Null-conditional ?., чтобы проверять события с нулевым и повышающим потоком простым и потокобезопасным способом.

SomethingHappened?.Invoke(this, args);

Это поточно-безопасный, поскольку он оценивает левая сторона только один раз и сохраняет ее во временной переменной. Вы можете прочитать здесь здесь в разделе «Операторы с нулевым условием».

Обновление: на самом деле обновление 2 для Visual Studio 2015 теперь содержит рефакторинг для упрощения делегирования делегатов, тип обозначений. Вы можете прочитать об этом в этом объявлении .

27
ответ дан Krzysztof Branicki 21 August 2018 в 13:37
поделиться
  • 1
    Спасибо за обновление, всегда хорошо знать. – Jérémie Bertrand 6 September 2015 в 08:29
  • 2
    Что нас ждет? – jjxtra 11 August 2018 в 22:11
  • 3
    Я не уверен, где у вас null ситуация с await, но вы всегда можете комбинировать ее с ?? operator likethis: await (foo?.DoAsync() ?? Task.CompletedTask); – Krzysztof Branicki 14 August 2018 в 11:00

Чтобы любой из этих потоков был потокобезопасным, вы предполагаете, что все объекты, которые подписываются на событие, также являются потокобезопасными.

0
ответ дан Martin Brown 21 August 2018 в 13:37
поделиться

На самом деле, первый является потокобезопасным, а второй - нет. Проблема со вторым заключается в том, что делегат SomethingHappened может быть изменен на null между нулевой верификацией и вызовом. Для более полного объяснения см. http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx .

3
ответ дан Nicole Calinoiu 21 August 2018 в 13:37
поделиться

Для .NET 4.5 лучше использовать Volatile.Read для назначения временной переменной.

protected virtual void OnSomethingHappened(EventArgs e) 
{
    EventHandler handler = Volatile.Read(ref SomethingHappened);
    if (handler != null) 
    {
        handler(this, e);
    }
}

Обновление:

Это объясняется в этой статье: http: //msdn.microsoft.com/en-us/magazine/jj883956.aspx. Кроме того, это было объяснено в четвертом издании «CLR via C #».

Основная идея заключается в том, что JIT-компилятор может оптимизировать ваш код и удалить локальную временную переменную. Итак, этот код:

protected virtual void OnSomethingHappened(EventArgs e) 
{
    EventHandler handler = SomethingHappened;
    if (handler != null) 
    {
        handler(this, e);
    }
}

будет скомпилирован в это:

protected virtual void OnSomethingHappened(EventArgs e) 
{
    if (SomethingHappened != null) 
    {
        SomethingHappened(this, e);
    }
}

Это происходит в определенных особых обстоятельствах, однако это может произойти.

9
ответ дан rpeshkov 21 August 2018 в 13:37
поделиться
  • 1
    Я не знал, как использовать Volatile для этого. Не могли бы вы немного объяснить, почему это лучше? – Jérémie Bertrand 18 June 2013 в 12:33
  • 2
    Добавлено объяснение моего ответа. – rpeshkov 18 June 2013 в 17:23
  • 3
    Спасибо, это хорошо знать. – Jérémie Bertrand 18 June 2013 в 18:36
  • 4
    Опять же, он защищает только от исключения NullReferenceException, это не имеет ничего общего с более серьезной проблемой состояния гонки, где вы вызываете обработчики событий, которые только что были отписаны. – KumoKairo 11 October 2014 в 20:25
  • 5
    Я не понимаю, является ли Volatile.Read достаточно безопасным, или следует использовать Interlocked.CompareExchange (ref SomethingHappened, null, null) ?. Invoke (this, e); [Д0] codeblog.jonskeet.uk/2015/01/30/… – Eric Burcham 12 April 2017 в 19:51
Другие вопросы по тегам:

Похожие вопросы: