Подтвердить что ты не робот

Является ли хорошей идеей реализовать событие С# со слабой ссылкой под капотом?

Мне было интересно, стоит ли использовать слабые события (где они подходят), используя что-то вроде следующего (грубое доказательство кода концепции):

class Foo {

    private WeakEvent<EventArgs> _explodedEvent = new WeakEvent<EventArgs>();

    public event WeakEvent<EventArgs>.EventHandler Exploded {
        add { _explodedEvent += value; }
        remove { _explodedEvent -= value; }
    }

    private void OnExploded() {
        _explodedEvent.Invoke(this, EventArgs.Empty);
    }

    public void Explode() {
        OnExploded();
    }

}

Разрешить другим классам подписываться и отписываться от событий с более традиционным синтаксисом С#, а под капотом фактически реализуются со слабыми ссылками:

static void Main(string[] args) {
    var foo = new Foo();
    foo.Exploded += (sender, e) => Console.WriteLine("Exploded!");

    foo.Explode();
    foo.Explode();
    foo.Explode();

    Console.ReadKey();
}

Где вспомогательный класс WeakEvent<TEventArgs> определяется следующим образом:

public class WeakEvent<TEventArgs> where TEventArgs : EventArgs {

    public delegate void EventHandler(object sender, TEventArgs e);

    private List<WeakReference> _handlers = new List<WeakReference>();

    public void Invoke(object sender, TEventArgs e) {
        foreach (var handler in _handlers)
            ((EventHandler)handler.Target).Invoke(sender, e);
    }

    public static WeakEvent<TEventArgs> operator + (WeakEvent<TEventArgs> e, EventHandler handler) {
        e._handlers.Add(new WeakReference(handler));
        return e;
    }

    public static WeakEvent<TEventArgs> operator - (WeakEvent<TEventArgs> e, EventHandler handler) {
        e._handlers.RemoveAll(x => (EventHandler)x.Target == handler);
        return e;
    }

}

Это хороший подход? есть ли какие-либо нежелательные побочные эффекты для этого подхода?

4b9b3361

Ответ 1

Это плохая идея, потому что:

  • Ваша программа начинает становиться недетерминированной, поскольку побочные эффекты зависят от действий GC.
  • GCHandles имеют стоимость исполнения.

См. связанный ответ. Это 95% -ный дубликат, но недостаточно, чтобы закрыть вопрос, который я думаю. Я приведу наиболее важные части:


Также существует семантическая разница и недетерминированность, которая будет вызвана слабыми ссылками. Если вы подключите () => LaunchMissiles() к некоторому событию, вы можете обнаружить, что ракеты запускаются только иногда. В других случаях GC уже убрал обработчик. Это можно решить с помощью зависимых ручек, которые представляют еще один уровень сложности.

Я лично считаю редким, что серьезный реферирующий характер событий является проблемой. Часто события подключаются между объектами, которые имеют одинаковое или очень сходное время жизни. Например, вы можете подключать все события, которые вы хотите, в контексте HTTP-запроса в ASP.NET, потому что все будет иметь право на сбор при завершении запроса. Любые утечки ограничены по размеру и недолговечны.

Ответ 2

Несколько комментариев о вашей конкретной реализации:

  • Перед вызовом проверьте значение handler.Target для null, чтобы вы не пытались сделать это с объектом, который был удален.

  • У С# есть специальные правила доступа для использования событий. Вы не можете сделать a.Event1 = a.Event2 + SomeOtherMethod, если этот код не имеет частного доступа к событиям. Однако это разрешено для делегатов. Ваша реализация ведет себя скорее как делегат, а не событие. Это, вероятно, не вызывает серьезной озабоченности, но о чем-то думать.

  • Ваши методы-операторы должны возвращать новый объект вместо изменения первого аргумента и его возврата. Реализация operator + допускает синтаксис следующего вида: a = b + c, но в вашей реализации вы изменяете состояние b!. Это не кошерно для того, как можно было ожидать, что эти операторы будут работать; вам нужно вернуть новый объект вместо изменения существующего. (Кроме того, из-за этого ваша реализация не является потокобезопасной. Вызов оператора + из одного потока, в то время как другой поднимал событие, вызвал бы исключение, потому что коллекция была изменена во время foreach.)