实现一个类似c#LINQ链式语法的观察者模式


前言

最近在学习 UniRx 的源码,其中链式语法的实现引起了我的兴趣,结合LINQ的实现原理,想要自己实现一个类似的事件机制。设计目标是一个支持链式语法的,可方便取消订阅的,在事件到达中途可使用任意操作符对事件参数进行转换的观察者模式。可基于此观察者模式设计事件系统。


一、UniRx与LINQ的关系

UniRx 是针对 Unity 引擎的响应式编程库,它基于 Reactive Extensions(Rx)开发而来,旨在简化异步和事件驱动编程。LINQ 是一种在 .NET 平台上的查询语言集成功能,允许开发者使用类似 SQL 的语法来查询各种数据源(如集合、数组、数据库等)。LINQ 提供大量操作符以及链式语法方便对数据源的操作,而UniRx 同样提供大量操作符(继承了绝大部分LINQ操作符)以及链式语法的特征。UniRx将事件源(数据源)抽象为在时间上离散分部的事件流。其主要由事件源、操作符、观察者三部分组成。

二、实现原理

UniRx 对链式语法支持的实现原理同LINQ类似,二者都是使用装饰器模式的层层嵌套来达到插入操作的目的。LINQ的实现原理可以参考这位大佬的文章C# LINQ查询,而操作符则是通过静态扩展的方式实现。

三、核心代码实现

前面说了UniRx 主要由事件源、操作符、观察者三部分组成。

1.接口部分

接口部分的设计,确定各角色职责:

  • 事件源:接收订阅,发送事件,取消订阅
  • 操作符:附加操作(提供链式支持),接收订阅(可将其理解为订阅者视角变换后的事件源),接收事件(可将其理解为事件源视角下的订阅者)。
  • 观察者:事件的订阅者,事件或数据接收的终端,接收事件。
 interface ISubject<T>
    {
        /// <summary>
        /// 发送事件
        /// </summary>
        /// <param name="para"></param>
        void SendEvent(T para);
        /// <summary>
        /// 订阅
        /// </summary>
        /// <param name="observer"></param>
        IDisposable Subscrib(IObserver<T> observer);
        /// <summary>
        /// 取消
        /// </summary>
        /// <param name="observer"></param>
        void Dispose(IObserver<T> observer);
    }
    interface IOperator<Ts, Tr> : IObserver<Ts>
    {
        /// <summary>
        /// 附加操作符或观察者到末尾
        /// </summary>
        /// <param name="op"></param>
        IDisposable Attach(IObserver<Tr> observer);
    }
    interface IObserver<T>
    {
        /// <summary>
        /// 接收事件
        /// </summary>
        /// <param name="para"></param>
        void OnEvent(T para);
    }

注意:操作符有两个泛型参数,是因为数据在中途可能会发生类型转换

2.Disposable

  class Disposable : IDisposable
    {
        Action func;
        public static readonly Disposable Default = new Disposable(null);
        public Disposable(Action func)
        {
            this.func = func;
        }

        public static IDisposable Creat(Action func)
        {
            return new Disposable(func);
        }

        public void Dispose()
        {
            func?.Invoke();
            //仅可销毁一次
            func = null;
        }
    }

这个类主要是用于创建事件的注销器返回给订阅者。

3.主题(事件源)

class Subject<T> : ISubject<T>
    {
        HashSet<IObserver<T>> mObservers = new HashSet<IObserver<T>>();

        public void Dispose(IObserver<T> op)
        {
            if (mObservers.Contains(op))
            {
                mObservers.Remove(op);
            }
        }

        public void SendEvent(T para)
        {
            foreach (var op in mObservers)
            {
                op.OnEvent(para);
            }
        }
        public IDisposable Subscrib(IObserver<T> observer)
        {
            mObservers.Add(observer);
            return Disposable.Creat(() => Dispose(observer));
        }
    }
    class Subject:Subject<Subject.Null?>
    {
        public struct Null
        {

        }
        public void SendEvent()
        {
            SendEvent(null);
        }
    }

这里有两个实现,一个用于发送带参数的事件,一个用于发送不带参的事件,至于要发送带多个参数的可以使用数组、结构体封装或元组等方式。

4.操作符

 abstract class OperatorBase<Ts, Tr> : IOperator<Ts, Tr>
    {

        public IDisposable Disposable { get; set; }
        protected IObserver<Tr> NextOp { get; set; }


        public abstract void OnEvent(Ts para);


        public IDisposable Attach(IObserver<Tr> observer)
        {
            NextOp = observer;
            return Disposable;
        }

        protected void MoveToNext(Tr para)
        {
            NextOp?.OnEvent(para);
        }
    }

    class Where<T> : OperatorBase<T, T>
    {
        Func<T, bool> predicate;

        public Where(Func<T, bool> predicate)
        {
            this.predicate = predicate;
        }

        public override void OnEvent(T para)
        {
            if (predicate(para))
            {
                MoveToNext(para);
            }
        }
    }
    class Select<Ts, Tr> : OperatorBase<Ts, Tr>
    {
        Func<Ts, Tr> transfer;

        public Select(Func<Ts, Tr> transfer)
        {
            this.transfer = transfer;
        }

        public override void OnEvent(Ts para)
        {
            var p = transfer(para);
            MoveToNext(p);
        }
    }

操作符主要封装注销器与链接的下一个操作符或观察者。这里仅实现两个最常用也是我认为最有用的两个操作符,其他的操作符可自行扩充。

5.观察者

/// <summary>
    /// 观察者
    /// </summary>
    /// <typeparam name="T"></typeparam>
    class Observer<T> : IObserver<T>
    {
        Action<T> func;

        public Observer(Action<T> func)
        {
            this.func = func;
        }

        public void OnEvent(T para)
        {
            func?.Invoke(para);
        }
    }

6.操作符扩展

操作符扩展用于支持链式语法。

static class SimpleRxExtension
    {
        public static IDisposable Subscrib<Ts, Tr>(
            this IOperator<Ts, Tr> @operator,
            Action<Tr> func)
        {
            var observer = new Observer<Tr>(func);
            return @operator.Attach(observer);
        }
        public static IOperator<Tr, Tr> Where<Ts, Tr>(
            this IOperator<Ts, Tr> @operator,
            Func<Tr, bool> func)
        {
            var op = new Where<Tr>(func);
            op.Disposable = @operator.Attach(op);
            return op;
        }
        public static IOperator<Tr, Trr> Select<Ts, Tr, Trr>(
            this IOperator<Ts, Tr> @operator,
            Func<Tr, Trr> func)
        {
            var op = new Select<Tr, Trr>(func);
            op.Disposable = @operator.Attach(op);
            return op;
        }

        public static IDisposable Subscrib<T>(this ISubject<T> subject, Action<T> func)
        {
            var observer = new Observer<T>(func);
            return subject.Subscrib(observer);
        }
        public static IOperator<T, T> Where<T>(
            this ISubject<T> subject,
            Func<T, bool> func)
        {
            var op = new Where<T>(func);
            op.Disposable = subject.Subscrib(op);
            return op;
        }
        public static IOperator<Ts, Tr> Select<Ts, Tr>(
           this ISubject<Ts> subject,
           Func<Ts, Tr> transfer)
        {
            var op = new Select<Ts, Tr>(transfer);
            op.Disposable = subject.Subscrib(op);
            return op;
        }
    }

使用示例

1.带参数主题

  #region 带参数主题
            Subject<int> subject = new Subject<int>();
            IDisposable unbinder1 = subject
                .Where(x => x > 10)
                .Select(x => (org: x, now: x + 2))
                .Select(x => x.ToString())
                .Subscrib(tuple => Console.WriteLine($"订阅者1接收结果{tuple}"));
            IDisposable unbinder2 = subject
                .Where(x => x < 10)
                .Select(x => x * 2.0f)
                .Where(x => x > 5f)
                .Subscrib(x => Console.WriteLine($"订阅者2接收结果{x}"));

            subject.SendEvent(20);
            subject.SendEvent(15);
            subject.SendEvent(1);
            subject.SendEvent(5);
            Console.WriteLine("-------------------------------");
            unbinder1.Dispose();
            Console.WriteLine("订阅者1取消订阅");
            subject.SendEvent(20);
            subject.SendEvent(15);
            subject.SendEvent(1);
            subject.SendEvent(5);
            Console.WriteLine("--------------------------------");
            unbinder1.Dispose();
            unbinder2.Dispose();
            Console.WriteLine("订阅者2取消订阅");
            subject.SendEvent(20);
            subject.SendEvent(15);
            subject.SendEvent(1);
            subject.SendEvent(5);
            #endregion

运行结果如下:

带参数结果

2.不带参数主题

 #region 不带参主题
            Random random = new Random();
            Subject SubjectNoPara = new Subject();

            var unbinder1 = SubjectNoPara
                .Subscrib(_ => Console.WriteLine("订阅者1接收到事件"));

            var unbinder2 = SubjectNoPara
                .Select(_ => 6)
                .Where(x => x > 5)
                .Subscrib(x => Console.WriteLine($"订阅者2接收事件,参数{x}"));
            SubjectNoPara.SendEvent();
            Console.WriteLine("-------------------------------");
            unbinder1.Dispose();
            Console.WriteLine("订阅者1取消订阅");
            SubjectNoPara.SendEvent();
            Console.WriteLine("-------------------------------");
            unbinder2.Dispose();
            Console.WriteLine("订阅者2取消订阅");
            #endregion

运行如下:

不带参结果
可以看到通过操作符甚至可以将一个不带参的事件源创造出参数,从而使得订阅相同事件的观察者接收到完全不同的参数。

总结

通过使用操作符,可以在事件在真正被接收前插入任意操作,从而可以实现更灵活的控制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值