65.9K
CodeProject 正在变化。 阅读更多。
Home

.NET 中的事件简化

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (28投票s)

2015年1月26日

CPOL

20分钟阅读

viewsIcon

52167

downloadIcon

491

.NET 中的事件很棒。有“event”关键字,提供了一种快速开始使用它们的方法,但是……

议程

引言

.NET 中的事件很棒。我们有 `event` 关键字,它提供了一种快速开始使用它们的方法,但不知何故,整个集合可能会带来内存泄漏、回调地狱和大量额外代码等问题。总的来说,这是因为它们缺乏灵活性且难以维护。

我提出的解决方案是一种抽象,用于处理委托模型(这是 .NET 事件的基础),它能提供更强的灵活性、简洁性和代码控制力。在该解决方案中,我添加了响应式扩展,使其通常更易于处理,并将它们结合起来,与 .NET 的用法相比,实现了一个很好的替代方案。

背景

当您使用委托模型一段时间后——在此部分我假设您对 .NET 事件的工作方式有所了解——您可能会注意到可能的问题或只是特性可能会困扰您,其中大多数都可以解决,但您最终会编写更多的代码。在我向您展示通用解决方案之前,我将尝试说明其中一些问题。

.NET 事件特性和弱点

这是一张常用图片,用于解释使用响应式扩展 (Rx) 与 .NET 事件相比的优势。我将在下面详细介绍这些限制和其他特性。

从一开始使用事件,你需要选择一个委托类型。你不能这样写:

public event int ValueRaised;

错误:“事件必须是委托类型”。

您必须声明自己的委托,或者使用像 EventHandler<TEventArgs> 委托 这样的常用替代方案。

public event EventHandler<int> ValueRaised;

您可能知道也可能不知道,但上面的代码适用于 .NET 4.5 或任何更高版本。在这种情况下,没有必要为了传播一个 `int` 值而声明自己的委托,`EventHandler` 中的类型约束已被移除。它被简化了,这很棒!这意味着如果您没有使用现代版本,您将得到以下结果:

错误:类型 'int' 不能用作泛型类型或方法 'System.EventHandler' 中的类型参数 'TEventArgs'。不存在从 'int' 到 'System.EventArgs' 的装箱转换。

要解决此错误,您必须创建一个 System.EventArgs 的派生类才能与参数一起使用。

无论在何种场景下,您可能都需要在某个时候声明一个新的委托,因为您想要不同的签名。如果您正在处理一个包含多种不同数据类型的特定情况,并且不想将所有内容封装在一个对象中,这可能会很有益。例如:

public delegate void CustomHandler(object sender, string info, Foo args);

好的,到目前为止,没什么大不了的。对于 .NET 事件声明,你提供了必要的东西,只需几行代码即可使其可用。它可以更好或不同。它有优点和缺点,都很容易克服。

常识告诉我们,当我们想停止使用某个事件或不再需要它时,有必要移除委托避免内存泄漏,通常这相对简单。由于你使用了方法签名,你可以像下面的例子一样操作:

    public class Program
    {
        static void Main(string[] args)
        {
            SampleOne();
            Console.Read();
        }

        private static void SampleOne()
        {
            var foo = new Foo();
            foo.ValueRaised += foo_ValueRaised;
            foo.OnRaiseValue(1); // show 1
            foo.ValueRaised -= foo_ValueRaised;
            foo.OnRaiseValue(2); // nothing happens
        }

        static void foo_ValueRaised(object sender, CustomEventArgs e)
        {
            Console.WriteLine(e.Value);
        }

    }
   public class CustomEventArgs : EventArgs
   {
        public int Value { get; set; }
   }

    public class Foo
    {
        public event EventHandler<CustomEventArgs> ValueRaised;

        public void OnRaiseValue(int value)
        {
            if (ValueRaised != null)
                ValueRaised(this, new CustomEventArgs { Value = value });
        }
    }

移除事件委托的另一个例子

    public sealed class MyClass : IDisposable
    {
        public event EventHandler<CustomEventArgs> ValueRaised;
        public void OnRaiseValue(int value)
        {
            if (ValueRaised != null)
                ValueRaised(this, new CustomEventArgs { Value = value });
        }

        public void Dispose()
        {
            ValueRaised = null;
        }
    }
        static void Main(string[] args)
        {
            //SampleOne();
            SampleTwo();
            Console.Read();
        }

        private static void SampleTwo()
        {
            var my = new MyClass();
            my.ValueRaised += foo_ValueRaised;
            my.OnRaiseValue(1); // show 1
            my.Dispose();
            my.OnRaiseValue(2); // nothing happens
        }

没有必要实现 `IDisposable`,你可以用自己的方式添加和删除委托。如果你正在使用一个public class带有public event,你只需要处理好它。

当你在应用程序中包含继承和抽象时,情况就不那么简单了,因为需要暴露包含委托的后备字段 (`ValueRaised` 事件),或者在基类中处理它。在我的示例中,没有必要声明后备字段,这意味着如果我创建了 `Foo` 的派生类,派生类将无法引发事件,也无法设置为 null。

    public sealed class FooDerivedClass : Foo, IDisposable
    {
        public void OnRaiseHalfValue(int value)
        {
            if (this.ValueRaised != null) // error
                this.ValueRaised(this, new CustomEventArgs { Value = value / 2 }); // error
        }

        public void Dispose()
        {
            this.ValueRaised = null; // error
        }
    }

错误:事件“NETEventResourceMaintenance.Foo.ValueRaised”只能出现在 += 或 -= 的左侧(除非在类型“NETEventResourceMaintenance.Foo”内部使用)。

当您想使用匿名方法Lambda 表达式时,这一部分可能会发现更多限制。我将在下面用 Lambda 表达式进行示例:

        public static Task<RoutedEventArgs> WhenClickOnce(Button btn)
        {
            var tcs = new TaskCompletionSource<RoutedEventArgs>();
            RoutedEventHandler handler = null;
            handler = new RoutedEventHandler((s, e) =>
                {
                    tcs.SetResult(e);
                    btn.Click -= handler;
                });
            btn.Click += handler;
            return tcs.Task;
        }

这类似于著名的示例:如何将点击转换为 `Task` 以便等待。它有点混乱。总的来说,理解起来并不太难,但你可以想象在更复杂的场景中它会变得多糟,结果可能是一个 C# 回调地狱,主要是因为移除 Lambda 表达式委托的问题。

在此部分需要注意的一个重要信息是,要手动移除委托,您始终需要将其保存在变量中或使用方法签名。

根据您想实现的目标,这可能不是一个好的实践,但有时您不想或不需要手动删除事件。对于这些场景,有一些不错的替代方案,例如:C# 中的弱事件为忙碌的程序员准备的 .NET 弱事件C# 中的 .NET 弱事件模式SharpObservation 等。

虽然其中一些确实效果很好,但您需要在项目中添加额外的代码或外部库,并使用相应选择的代码风格封装事件的使用。

在某些库中,无法使匿名函数成为弱引用,在其他库中,只有在同一类作用域中的带有闭包变量的委托(而不是方法内部的局部变量)才能成为弱引用。关于 Lambda 表达式的一个有趣的部分,特别是那些没有闭包值的 Lambda 表达式,它们被转换为静态方法,本身就是弱引用,在这种情况下无需担心内存泄漏。

关于弱事件的最终评价是,它是一个好东西,但您的用法可能会复杂或受限。

这是一个完全没有意义的例子

        [TestMethod]
        public void FooTestMethod()
        {
            var foo = new Foo();
            foo.ValueRaised += foo_ValueRaised;
            foo.ValueRaised += foo_ValueRaised;
            foo.OnRaiseValue(5);
        }

        void foo_ValueRaised(object sender, CustomEventArgs e)
        {
            Trace.WriteLine("Foo Raised = " + e.Value);
        }

此代码将显示两次“Foo Raised = 5”。为什么要两次执行回调?如果真的需要做类似的事情,最好在方法中运行一个循环。

你可能不会写上面的代码,但你可能会不惜一切代价避免这种情况,也许你的项目有庞大的类,有人可能会不小心这样做,或者你只是想在事件中添加一层额外的保护。结果会是这样的:

        private EventHandler<CustomEventArgs> _handlers;
        public event EventHandler<CustomEventArgs> ValueRaised
        {
            add
            {
                var callbacks = _handlers;
                if (callbacks == null || callbacks.GetInvocationList().All(a => (EventHandler<CustomEventArgs>)a != value))
                {
                    _handlers += value;
                }
            }
            remove
            {
                if (_handlers == null)
                    return;

                _handlers -= value;
            }
        }

        public void OnRaiseValue(int value)
        {
            if (_handlers != null)
                _handlers(this, new CustomEventArgs { Value = value });
        }

这个问题有不止一种解决方案,无论如何,使用这段新代码,执行只会显示一次“Foo Raised = 5”。即使在使用匿名委托或 Lambda 表达式时,这种技术也并非总是有效,因为——你知道,它们是匿名的——这取决于它是否是闭包函数以及闭包中使用的变量的作用域。至少,当你直接使用方法签名时,或者——可能性较小——如果你将委托存储在变量中并始终使用相同的实例时,它会有所帮助。

无法像对象一样将事件传递给方法,添加 (+) 或删除 (-) 委托的运算符会生成一个新的多播委托实例。既然你注意到事件不是“一等公民”

    public class FooChild
    {
        private EventHandler<CustomEventArgs> _eventHandler;

        public FooChild(EventHandler<CustomEventArgs> eventHandler)
        {
            this._eventHandler = eventHandler;
            this._eventHandler += fooEvent_ValueRaised;
        }

        void fooEvent_ValueRaised(object sender, CustomEventArgs e)
        {
             Console.WriteLine("Raised " + e.Value + " in FooChild");
        }
    }
        public Foo()
        {
            var child = new FooChild(this.ValueRaised);
        }

错误:事件“Foo.ValueRaised”只能出现在 += 或 -= 的左侧。

你可以这样拥有委托

var child = new FooChild(this._handlers);

但它们是不可变的,因此将实例存储在不同的地方没有意义,您将丢失原始位置中新添加或删除的更改,如果您在该处添加委托,`Foo` 中的事件调用将不会执行它。我不会涵盖使用 `ref` 关键字通过引用传递事件,结果肯定会很糟糕,并且在使用中充满需要小心的细节。

为了克服这部分,结果需要将事件封装在一个类中,我们已经在示例中完成了此操作,并将整个类向前传递。

        private Foo _foo;
        public FooChild(Foo foo)
        {
            if (foo == null)
               throw new ArgumentNullException("foo");

            this._foo = foo;
            this._foo.ValueRaised += foo_ValueRaised;
        }

        void foo_ValueRaised(object sender, CustomEventArgs e)
        {
             Console.WriteLine("Raised " + e.Value + " in FooChild");
        }

这是一种常见的场景,适用于应用程序中多个代码段(例如不同模块或服务)需要事件时。

请注意,我们回到了第二个问题,即资源维护。其他类无法正确处理它(像 `Foo` 类),它们只能添加或删除委托,尽管这增加了项目中代码的耦合,但在这种情况下,可以将其归类为“特性”,因为它引入了一种关注点分离,这些其他类将仅仅是事件的观察者/消费者——无论您想怎么称呼它们。根据您想要实现的目标,这是一个不错的选择,否则您需要重构并在包含事件的类中添加新方法。

当存在并发性时,通常在后台进程或服务应用程序中看到,需要保护您的委托。`event` 关键字在操作(添加/删除)和执行中不引入同步。在 `Foo` 类示例中,我们类内部的变量 `_handlers` 必须使用同步机制进行保护,例如:

        private readonly object _sync = new object();

        public event EventHandler<CustomEventArgs> ValueRaised
        {
            add
            {
                lock (_sync)
                {
                    if (_handlers == null || _handlers.GetInvocationList().All(a => (EventHandler<CustomEventArgs>)a != value))
                    {
                        _handlers += value;
                    }
                }
            }
            remove
            {
                lock (_sync)
                {
                    if (_handlers == null)
                        return;

                    _handlers -= value;
                }
            }
        }

        public void OnRaiseValue(int value)
        {
            EventHandler<CustomEventArgs> handlers;
            lock (_sync)
            {
                handlers = _handlers;
            }
            if (handlers != null)
                handlers(this, new CustomEventArgs { Value = value });
        }

这样可以避免在添加或删除事件时丢失一些委托,并在执行前获取最新的委托。在这种情况下,执行无法完全受到保护,在 `lock` 内部调用委托不是一个好的做法,即使使用此代码可以“保护”多线程应用程序中的委托,线程安全事件没有完美的解决方案

请注意,每遇到一个新难题,您都会聚合越来越多的代码来解决它。

我注意到的 .NET 事件的最终限制是缺乏组合性。这是迫使我使用替代方案来处理事件的主要限制。在您想要编写的每个操作事件数据的代码片段中,您最终都会遇到:

a) 手动和命令式处理方式,例如:

        static void Main(string[] args)
        {
            HandleEventManually();
            Console.Read();
        }

        private static void HandleEventManually()
        {
            Console.WriteLine("Handle event manually");
            var foo = new Foo();
            foo.ValueRaised += foo_ValueRaised;
            foo.OnRaiseValue(5); // nothing happens
            foo.OnRaiseValue(4); // 2 is evaluated
        }

        static void foo_ValueRaised(object sender, CustomEventArgs e)
        {
            if (e.Value <= 0)
                return;

            if ((e.Value % 2) == 0)
                return;

            var toProcess = e.Value / 2;
            ProcessEvenValue(toProcess);
        }

        private static void ProcessEvenValue(int value)
        {
            Console.WriteLine("Processing Value = " + value);
        }

b) 创建一个新事件,具有相同的命令式代码,只有在满足条件时才会被引发。

        static void Main(string[] args)
        {
            //HandleEventManually();
            HandleEventWithAnotherEvent();
            Console.Read();
        }

        private static event EventHandler<int> FooValidRaisedValues;
        private static void HandleEventWithAnotherEvent()
        {
            Console.WriteLine("Projection of an event into another");

            FooValidRaisedValues += Program_FooValidRaisedValues;

            var foo = new Foo();

            foo.ValueRaised += (s, e) =>
            {
                if (e.Value <= 0)
                    return;

                if ((e.Value % 2) == 0)
                    return;

                var handlers = FooValidRaisedValues;
                if (handlers != null)
                    handlers(null, e.Value / 2);
            };

            foo.OnRaiseValue(5); // nothing happens
            foo.OnRaiseValue(4); // 2 is evaluated
        }

        static void Program_FooValidRaisedValues(object sender, int value)
        {
            ProcessEvenValue(value);
        }

选项 'b' 可能会带来更多问题,您会将其他事件放在哪里?封装在另一个类中?只在使用它的地方声明(像我这样做)?使用匿名函数?无论答案如何,它都可能带来我之前在本文中提到的所有问题和限制。

您可能会遇到一些访问事件数据源的困难,如文章的第一张图片所示(隐藏的数据源),部分原因是无法轻松进行简单的数据投影,也因为 .NET 中广泛使用的 EventHandler 模式,导致在某些地方需要在事件签名中处理抽象类型。

其他注意事项

如果您深入研究 .NET 事件,您可能会遇到这些以及其他需要小心或处理的细节,其中大部分取决于您想要涵盖的场景。一旦您了解了它的工作原理,您可能会很好地将其与一些代码结合使用。

此外,事件是一种很好的东西,如果使用得当,可以帮助我们编写更好的代码,为什么会如此复杂呢?糟糕的部分是因为许多需要或想要使用的场景,在项目中添加更多代码变得过于重复。有时多即是少,为什么不封装这些特性并在实际实现中节省一些时间呢?

 


替代方案

如前所述,我的主要目标是简化 .NET 事件的使用。不幸的是,我的解决方案并非万能,但至少它可以帮助您生成更好、正确、清晰且重复性更少的代码。

为了涵盖所有讨论过的点,选择将响应式扩展集成到解决方案中,因为 Rx 最重要的优点之一就是它是为了简化 .NET 事件的使用而创建的。

稍后我将详细介绍与 .NET 事件相关的相同特性和缺点,但为了比较之前展示的关于限制的图片,我创建了一个具有相同要点的新图片

需要注意的是,这张图片仅与响应式扩展有关,与解决方案无关。现在您可能想知道为什么不直接使用 Rx?有几个可能的原因值得考虑:

  1. 与 .NET 事件兼容,无需添加额外代码或复杂性。
  2. 您不应该实现 IObservable不建议使用 Subjects它们是 Rx 中处理事件的一些关键类型,尽管这可能会引发争议,结果将根据所使用的场景而异。
  3. 简洁性,如果您将使用 Subject<T> 类型或类似的 ISubject<T> 实现,除了 "OnNext" 方法之外,还有 "OnError" 和 "OnCompleted" 方法,而且您想创建一个在一个地方处理这三件事情的事件的场景确实很少见(在这种情况下,我不考虑使用 .NET 组件或外部 API),因为通常您会组合操作,而组合会产生错误或完成。实际上,为了获得相同的功能,可以将每个方法拆分为一个独立的事件。
  4. 有时,将 .NET 事件转换为 Observable 可能会产生一些冗长的代码,或者您可能不习惯这种代码风格,尤其是当事件定义可能更改时,您使用字面字符串进行转换时。
  5. 如果您将事件转换为可观察对象,我提到的 .NET 事件的一些问题将无法解决,它将取决于添加或删除委托的内部实现。

最后是我的提议

所有关于我的提案的内容都以压缩文件的形式附在本文中。但是,我创建了一个GitHub 仓库,以提供最新的代码,以防有改进或任何其他相关想法。

为了使其灵活和可扩展,我创建了一个结构化解决方案

这些接口将涵盖两种方法,即 .NET 的委托模型和 Rx 的推模型。为了保持一致性,它使用关注点分离原则实现,这意味着需要通过每个访问器来使用首选选项。

左侧是 `Observe()` 方法,它将返回 `Observable`,右侧是 `Handler` 事件,中间是一个接口,用于向将添加委托或订阅观察者的人发布新值。

另一个重要点是,这个结构将为您提供在以下两者之间进行选择的可能性:使用 Rx 的现代替代方法,或者事件的常用用法,同时保持与旧代码的兼容性。

此外,这些接口可以单独扩展和实现,也可以与另一种发布-订阅模式进行比较,尽管主要目标远非模式,但它允许基于 `T` 类型或任何可以识别事件的机制创建 EventAggregator。

    public sealed class EventSubject<TArgs> : IEventSubject<TArgs>, IDisposable
    {
        #region [ Fields / Attributes ]
        private Action _disposeAction;
        private Action<TArgs> _delegates;

        private volatile bool _isDisposed;

        private readonly object _gateEvent = new object();
        #endregion

        #region [ Events / Properties ]
        public event Action<TArgs> Handler
        {
            add
            {
                RegisterEventDelegate(value);
            }
            remove
            {
                UnRegisterEventDelegate(value);
            }
        }
        #endregion

        private void AddDisposeAction(Action value)
        {
            if (_isDisposed)
                return;

            var baseVal = _disposeAction;
            while (true)
            {
                var newVal = baseVal + value;
                var currentVal = Interlocked.CompareExchange(ref _disposeAction, newVal, baseVal);

                if (currentVal == baseVal)
                    return;

                baseVal = currentVal; // Try again
            }
        }

        private void RemoveDisposeAction(Action value)
        {
            var baseVal = _disposeAction;
            if (baseVal == null)
                return;

            while (true)
            {
                var newVal = baseVal - value;
                var currentVal = Interlocked.CompareExchange(ref _disposeAction, newVal, baseVal);

                if (currentVal == baseVal)
                    return;

                baseVal = currentVal; // Try again
            }
        }

        private void RegisterEventDelegate(Action<TArgs> invoker)
        {
            if (invoker == null)
                throw new NullReferenceException("invoker");

            lock (_gateEvent)
            {
                CheckDisposed(); // checked inside of lock because of disposable synchronization

                if (IsAlreadySubscribed(invoker))
                    return;

                AddActionInternal(invoker);
            }
        }

        private bool IsAlreadySubscribed(Action<TArgs> invoker)
        {
            var current = _delegates;
            if (current == null)
                return false;

            var items = current.GetInvocationList();
            for (int i = items.Length; i-- > 0; )
            {
                if ((Action<TArgs>)items[i] == invoker)
                    return true;
            }
            return false;
        }

        private void UnRegisterEventDelegate(Action<TArgs> invoker)
        {
            if (invoker == null)
                return;

            lock (_gateEvent)
            {
                var baseVal = _delegates;
                if (baseVal == null)
                    return;

                RemoveActionInternal(invoker);
            }
        }

        private void AddActionInternal(Action<TArgs> invoker)
        {
            var baseVal = _delegates;
            while (true)
            {
                var newVal = baseVal + invoker;
                var currentVal = Interlocked.CompareExchange(ref _delegates, newVal, baseVal);

                if (currentVal == baseVal) // success
                    return;

                baseVal = currentVal;
            }
        }

        private void RemoveActionInternal(Action<TArgs> invoker)
        {
            var baseVal = _delegates;
            while (true)
            {
                var newVal = baseVal - invoker;
                var currentVal = Interlocked.CompareExchange(ref _delegates, newVal, baseVal);

                if (currentVal == baseVal)
                    return;

                baseVal = currentVal; // Try again
            }
        }
        public IObservable<TArgs> Observe()
        {
            return Observable.Defer(() =>
            {
                CheckDisposed();
                return Observable.FromEvent<TArgs>(AddActionInternal, RemoveActionInternal);
            })
            .TakeUntil(Observable.FromEvent(AddDisposeAction, RemoveDisposeAction));
        }

        public void OnNext(TArgs value)
        {
            CheckDisposed();

            var current = _delegates;
            if (current == null)
                return;

            current(value);
        }

        public void Dispose()
        {
            _isDisposed = true;
            _delegates = null;

            try
            {
                var disposeDelegates = _disposeAction;
                if (disposeDelegates == null)
                    return;

                _disposeAction = null;
                disposeDelegates();
            }
            finally
            {
                lock (_gateEvent)
                {
                    _delegates = null; // re-clean with sync
                }
            }
        }
        private void CheckDisposed()
        {
            if (_isDisposed)
            {
                ThrowDisposed();
            }
        }

        private void ThrowDisposed()
        {
            throw new ObjectDisposedException(this.GetType().Name);
        }
    }

EventSubject<TArgs> 特性

现在,让我们像我描述 .NET 事件一样,比较我的解决方案的各个方面。

使用 `EventSubject` 很简单,就像 .NET 中的任何其他对象一样。只需选择您想要的任何 `T` 类型并实例化它。

    private readonly EventSubject<int> _eventSubject = new EventSubject<int>();

我也创建了一个帮助器

    var eventSubject = EventSubject.Create<int>();

这是一个涵盖所有可能性的微妙方面,主要成就是增加了灵活性。您可以选择:

  1. 相同的 .NET 事件

在这种情况下,可以与常规代码集成。这是传统的使用方式,允许您使用弱事件技术(对于订阅者端)或您通常用于处理事件的其他类型的代码。

        private readonly EventSubject<int> _eventSubject = new EventSubject<int>();
        private bool _delegateCalled;
        
        [TestMethod]
        public void EventSubjectHandleTest()
        {
            _delegateCalled = false;
            _eventSubject.Handler += eventSubject_Handle;
            _eventSubject.OnNext(1);
            _eventSubject.Handler -= eventSubject_Handle;
            Assert.IsTrue(_delegateCalled);

            _delegateCalled = false;
            _eventSubject.OnNext(2);
            Assert.IsFalse(_delegateCalled);
        }

        private void eventSubject_Handle(int value)
        {
            _delegateCalled = true;
            Trace.WriteLine(string.Format("Doing something with '{0}' value.", value));
        }

关于转换为 EventHandler 模式,我将在文章的后面部分讨论这个主题。

  1. 由响应式扩展实现的推模型

这有一些优点,获取 `IObservable`(调用 `Observe()` 方法)后,您需要订阅一个观察者,使用 Rx 库有很多方法可以做到这一点(例如:使用 `Subscribe()` 扩展方法),之后您将开始监听 EventSubject 类推送的新值,这里重要的是,观察者的订阅会返回一个 `IDisposable`,您可以在需要处理订阅时调用 `Dispose()`,以防您想要或需要停止接收数据。

        [TestMethod]
        public void EventSubjectObsevableTest()
        {
            _delegateCalled = false;

            var disp =  _eventSubject.Observe().Subscribe(_ => _delegateCalled = true);
            _eventSubject.OnNext(1);
            disp.Dispose();
            Assert.IsTrue(_delegateCalled);

            _delegateCalled = false;
            _eventSubject.OnNext(2);
            Assert.IsFalse(_delegateCalled);
        }

此外,Rx 中还有其他几个选项,提供了更多便利。有些运算符可以自行停止订阅,这意味着如果您有中断条件,则无需担心处理订阅。

        [TestMethod]
        public void EventSubjectObservableUnsubscribeTestV1()
        {
            _delegateCalled = false;

            _eventSubject.Observe().Take(1)
                                   .Subscribe(_ => _delegateCalled = true);
            _eventSubject.OnNext(1);
            Assert.IsTrue(_delegateCalled);

            _delegateCalled = false;
            _eventSubject.OnNext(2);
            Assert.IsFalse(_delegateCalled);
        }

        [TestMethod]
        public void EventSubjectObservableUnsubscribeTestV2()
        {
            _delegateCalled = false;

            _eventSubject.Observe()
                        .TakeWhile(next => next < 2)
                        .Subscribe(_ => _delegateCalled = true);
            _eventSubject.OnNext(1);
            Assert.IsTrue(_delegateCalled);

            _delegateCalled = false;
            _eventSubject.OnNext(2);
            Assert.IsFalse(_delegateCalled);
        }

无论资源维护的选择如何,`EventSubject` 实例还实现了 `IDisposable` 接口以清理所有委托,从而确保订阅者(委托及其各自的对象)将由垃圾回收器回收。

       _eventSubject.Dispose();

为了处理可重复的监听器,在 `EventSubject` 中使用 .NET 事件(通过 `Handler` 访问器)时,我使用了之前解释过的相同技术。

需要强调的是,这不适用于 Rx 方式,使用责任完全集中在观察者身上。对于同一个 `IObservable` 处理多个 `IObserver`,推事件模型有其自己的行为,这取决于将使用哪个操作符,有一个概念区分热观测和冷观测。我不会深入探讨 Rx,但 `Observe()` 方法返回一个冷观测,它在其中订阅一个热观测(真实的事件),这意味着每次订阅一个观测时都会创建一个副作用订阅,副作用代码用于检查整个对象是否已提前处置。

下面的解决方案与类本身关系不大,更多的是关于 Rx 和 C#。

  1. 将事件直接传递给订阅者的方法是调用 `Observe()` 方法,它返回 `IObservable` 对象类型,并且您可以获得实例而无需封装在类中。
        public class Foo
        {
            private IObservable<System.Reactive.Unit> _observable;

            public Foo(IObservable<System.Reactive.Unit> observable)
            {
                this._observable = observable;
            }

            public void StartListen()
            {
                this._observable.Subscribe(_ => NextEvent());
            }

            private void NextEvent()
            {
                EventCount++;
            }

            public int EventCount { get; set; }

        }
        [TestMethod]
        public void EventSubjectPassJustAnEvent()
        {
            var ev = new EventSubject<System.Reactive.Unit>();
            var foo = new Foo(ev.Observe());
            foo.StartListen();
            ev.OnNext(System.Reactive.Unit.Default);
            Assert.IsTrue(foo.EventCount == 1);
            ev.Dispose();

            try
            {
                foo.StartListen();
            }
            catch (ObjectDisposedException ex)
            {
                Assert.IsTrue(ex.Message.Contains(typeof(EventSubject<System.Reactive.Unit>).Name));
            }
        }

经典 .NET 事件的另一种方法是获取可观察对象并调用 Observable.ToEvent 方法,该方法返回一个对象,它也是一个包装器,声明了要使用的事件,但这种方法不值得使用,因为它增加了代码和复杂性,最好还是使用 IEventDelegate 接口。

  1. 将事件传递给发布者的方法是将 `Publish(TArgs value)` 方法转换为 `Action`。在这种情况下,与解决方案无关,它只是 C#。
        public class Fire
        {
            private Action<System.Reactive.Unit> _raiseValue;
            public Fire(Action<System.Reactive.Unit> raiseValueDelegate)
            {
                this._raiseValue = raiseValueDelegate;
            }

            public void Shoot()
            {
                this._raiseValue(System.Reactive.Unit.Default);
            }
        }

        [TestMethod]
        public void EventSubjectPassToPublish()
        {
            Fire f;
            using (var ev = new EventSubject<System.Reactive.Unit>())
            {
                bool eventCalled = false;
                var disp = ev.Observe().Subscribe(_ => eventCalled = true);
                f = new Fire(ev.OnNext);
                f.Shoot();
                Assert.IsTrue(eventCalled);
                eventCalled = false;
                disp.Dispose();
                f.Shoot();
                Assert.IsFalse(eventCalled);
            }
            try
            {
                f.Shoot();
            }
            catch (ObjectDisposedException ex)
            {
                Assert.IsTrue(ex.Message.Contains(typeof(EventSubject<System.Reactive.Unit>).Name));
            }
        }

无论您想使用哪种情况,整个类都是一个对象,它可以像代码中的任何引用类型一样处理,为您提供更大的灵活性。此外,如果您想保护代码,可以使用解决方案中使用的接口类型(之前在类图图像中显示)。

对于订阅者:`IEventObservable` 和 `IEventDelegate`

对于发布者:`IEventPublisher`

对于两者:`IEventSubject`

如果你读过关于线程安全事件的文章,你会发现没有完美的解决方案。对于这个解决方案,我使用了另一种替代方案来处理这个场景,除了我需要执行重复委托比较的部分之外,整个类都使用了无锁数据结构,这足以供多个线程并发使用。如果你对这项技术感兴趣,你可以在这里查看更多信息。主要优点是尽可能快地调用事件,甚至在执行测试是否有任何委托要调用以及类之前是否已释放之前。

这里是响应式扩展发挥作用的主要部分。既然你熟悉 Rx,你只需调用 `Observe()` 方法,它会返回一个 `IObservable`,然后开始组合操作,就像 LINQ 处理集合一样,但这里是针对事件。此外,你还获得了一种更好的资源维护方式。

以下示例是 Netflix 的 Jafair Hussain 提出的学习 Rx 的五大要点。这是我做的,而不是他或他的团队。

  1. 映射
        [TestMethod]
        public void ObservableMap() // Using Select Operator 
        {
            var values = new List<string>();
            using (var ev = new EventSubject<Tuple<int, string>>())
            using (var sub = ev.Observe().Select(obj => obj.Item2)
                                        .Subscribe(values.Add))
            {
                ev.OnNext(new Tuple<int, string>(1, "First"));
                ev.OnNext(new Tuple<int, string>(2, "Second"));
                ev.OnNext(new Tuple<int, string>(3, "Third"));

                Assert.IsTrue(values.SequenceEqual(new[] { "First", "Second", "Third" }));
            }
        }
  1. Filter
        [TestMethod]
        public void ObservableFilter() // Using Where Operator
        {
            var values = new List<int>();
            using (var ev = new EventSubject<Tuple<int, string>>())
            using (var sub = ev.Observe().Where(obj => (obj.Item1 % 2) == 0)
                                        .Subscribe(a => values.Add(a.Item1)))
            {
                ev.OnNext(new Tuple<int, string>(1, "First"));
                ev.OnNext(new Tuple<int, string>(2, "Second"));
                ev.OnNext(new Tuple<int, string>(3, "Third"));
                ev.OnNext(new Tuple<int, string>(4, "Fourth"));

                Assert.IsTrue(values.SequenceEqual(new[] { 2, 4 }));
            }
        }
  1. 减少
        [TestMethod]
        public void ObservableReduce() // Using Sum Operator
        {
            var values = new List<int>();
            var ev = new EventSubject<Tuple<int, string>>();

            using (var sub = ev.Observe().Sum(a => a.Item1)
                                        .Subscribe(values.Add))
            {
                ev.OnNext(new Tuple<int, string>(1, "First"));
                ev.OnNext(new Tuple<int, string>(2, "Second"));
                ev.OnNext(new Tuple<int, string>(3, "Third"));
                ev.OnNext(new Tuple<int, string>(4, "Fourth"));

                ev.Dispose();

                Assert.IsTrue(values.SequenceEqual(new[] { 10 }));
            }
        }
  1. 合并
        [TestMethod]
        public void ObservableMerge() // Using Merge Operator
        {
            var values = new List<Tuple<int,String>>();
            using (var left = new EventSubject<Tuple<int, string>>())
            using (var right = new EventSubject<Tuple<int,string>>())
            using (var sub = left.Observe().Merge(right.Observe())
                                        .Subscribe(values.Add))
            {
                left.OnNext(new Tuple<int, string>(1, "First"));
                right.OnNext(new Tuple<int, string>(2, "Two"));
                left.OnNext(new Tuple<int, string>(3, "Third"));
                right.OnNext(new Tuple<int, string>(4, "Four"));

                Assert.IsTrue(values.SequenceEqual(new[] { Tuple.Create(1,"First"), Tuple.Create(2, "Two"), Tuple.Create(3, "Third"), Tuple.Create(4, "Four") }));
            }
        }
  1. Zip
        [TestMethod]
        public void ObservableZip() // Using Zip Operator
        {
            var values = new List<Tuple<int, String, String>>();
            using (var left = new EventSubject<Tuple<int, string>>())
            using (var right = new EventSubject<Tuple<int, string>>())
            using (var sub = left.Observe().Zip(right.Observe(), (a, b) => new { a, b })
                                        .Where(next => next.a.Item1 == next.b.Item1)
                                        .Select(next => Tuple.Create(next.a.Item1, next.a.Item2, next.b.Item2))
                                        .Subscribe(values.Add))
            {
                left.OnNext(new Tuple<int, string>(1, "First"));
                right.OnNext(new Tuple<int, string>(1, "One"));

                left.OnNext(new Tuple<int, string>(2, "Second"));
                left.OnNext(new Tuple<int, string>(3, "Third"));

                right.OnNext(new Tuple<int, string>(2, "Two"));
                right.OnNext(new Tuple<int, string>(3, "Three"));

                Assert.IsTrue(values.SequenceEqual(new[] { Tuple.Create(1, "First", "One"), Tuple.Create(2, "Second", "Two"), Tuple.Create(3, "Third", "Three") }));
            }
        }

为了获得与著名的 EventHandler 相同的数据,可以使用 EventPattern<T> 类,该类包含一个发送者和一个参数,作为 EventSubject 的 `TArgs` 类型。

为了与事件订阅者的 EventHandler 签名一起使用,会稍微复杂一些,你可以从可观察对象转换,或者创建 IEventSubject<TArgs> 的实现——我尝试了第二种选择,不幸的是,代码结果几乎是 EventSubject 实现的副本。

为了保持 `IDisposable` 实现的简单性,同步对象和无锁算法的安全,扩展 `IEventSubject` 接口的类都是密封的。

我没有运行性能测试,如果你与直接和干净的事件使用进行比较,那么所提议的解决方案可能会在性能上有所损失。我相信在这种情况下,收益大于损失,这是一种添加新代码层时总是会发生的事情,除非你每秒有数千个事件触发,并伴随着添加和删除回调,否则你应该没问题。

结论

如果考虑到解决所有技术问题和限制,使事件易于处理在实现上并非那么简单,但为了提高生产力,让这个解决方案来处理事件,你只需编写代码,让你的项目至少在使用上变得简单。

如今,对于所有事物都有大量的通用解决方案,但如果需要简洁性,就不能过于通用。正如我们所看到的,即使面对如此多的要点,有时也容易找到出路,有时则不然,需要做出选择,尽管我试图涵盖几乎所有可能性,但通常最好的做法是移除或搁置某个功能,只确保相同的代码以后不会再次遇到。

最后,重要的是强调 Reactive Extensions 库,我相信它将继续存在并随着时间的推移而不断发展壮大,它无疑是这个解决方案中的主要亮点。基于我解释的各种原因,为什么不将 Rx 和这个解决方案集成到您的所有项目中,并开始组合操作以享受处理事件的强大方式呢?

最终想法

在开始写这篇文章之前,我曾试图向自己证明,事件是解决各种问题的主要方案,主要原因在于 Rx 的使用。现在我更加开明了,作为一名后端开发者,鉴于其特性和限制,谨慎研究何时应用事件模型(无论是经典的 .NET 委托模型还是 Rx 的推送模型)是一个很棒的想法。

需要回答一些问题,比如:我需要在一个有多个数据消费者的场景中使用事件吗?仅仅为了将来可能有趣而创建一个事件是个好主意吗?反过来,今天不可能避免使用回调来处理各种库,而且应用程序中推送处理模型越来越多,为什么不直接组合操作呢?

© . All rights reserved.