首页 > 编程语言 >实现一个类似c#LINQ链式语法的观察者模式

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

时间:2024-07-26 14:53:40浏览次数:18  
标签:SendEvent c# LINQ 操作符 func 链式 subject public op

文章目录


前言

最近在学习 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

运行如下:

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

总结

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

标签:SendEvent,c#,LINQ,操作符,func,链式,subject,public,op
From: https://blog.csdn.net/wts327/article/details/140713184

相关文章

  • 白鲸开源CEO郭炜荣获「2024中国数智化转型升级先锋人物」称号
    2024年7月24日,由数据猿主办,IDC协办,新华社中国经济信息社、上海大数据联盟、上海市数商协会、上海超级计算中心作为支持单位,举办“数智新质·力拓未来2024企业数智化转型升级发展论坛——暨AI大模型趋势论坛”数据猿“年中·特别策划季——数智化转型升级”主题策划活动。在这场......
  • Logtrick
    logtrick的用法与实战logtrick是我从灵神视频中学习到的,此文章介绍logtrick用法与实践,以及灵神视频中未提到的,我本人总结出来的小技巧用法logtrick通常用于求子数组(gcd,lcm,&,|)后的max或者min或者计数问题子数组问题logtrick主要是解决子数组问题,所以在此我们先引出子......
  • X-Frame-Options may only be set via an HTTP header sent along with a documen
    X-Frame-OptionsmayonlybesetviaanHTTPheadersentalongwithadocumen_百度搜索(baidu.com)X-Frame-Options-盼星星盼太阳-博客园(cnblogs.com)vue项目中iframe嵌套其他项目,iframe父子页面传值-盼星星盼太阳-博客园(cnblogs.com)......
  • EasyExcel复杂导出 一对多
     将数据一条一条查出来千万不要用一对多查询最后用方法进行合并publicclassExcelFileCellMergeStrategyimplementsCellWriteHandler{/***合并列的范围索引*/privateint[]mergeColumnIndex;/***合并起始行索引*/privateintmer......
  • getBoundingClientRect 和 IntersectionObserver 的区别和用法
    目录getBoundingClientRectIntersectionObservergetBoundingClientRectgetBoundingClientRect是一个DOMAPI方法,用于获取指定元素相对于视口的位置和尺寸信息。它返回一个DOMRect对象,包含了元素的左上角和右下角相对于视口的坐标。“图片懒加载”,这个词语想必大家再熟悉不......
  • 可以捕捉高动态范围成像的的AR0521SR2C09SURA0-DP2、AR0522SRSM09SURA0-DP2、AR0821CS
    AR0521SR2C09SURA0-DP2、AR0522SRSM09SURA0-DP2、AR0821CSSC18SMEA0-DPBR图像传感器——明佳达1、AR0521SR2C09SURA0-DP2是一款1/2.5英寸CMOS数字图像传感器,带有2592(H)×1944(V)有效像素阵列。它能在线性或高动态范围模式下捕捉图像,且带有卷帘快门读取,其中包含了复杂......
  • 将 google 驱动器连接到 google colab
    如何修复无法通过此代码连接colab来获取错误:fromgoogle.colabimportdrivedrive.mount('/content/fooddirectory')并且此代码fromgoogle.colabimportdrivedrive.mount('/content/drive')尝试了两个代码,但它们不起作用,并出现与之前相同的错误并尝试通过驱......
  • 解决cv2.VideoCapture无法打开摄像头
    上手YOLOV8,训练完了,生成了权重pt文件,用cv2调用摄像头,失败,报错[ERROR:[email protected]]globalobsensor_uvc_stream_channel.cpp:159cv::obsensor::getStreamChannelGroupCameraindexoutofrange 搜索了下,说是Videocapture方法得传参,加上,不报错了,但是警告,没法用[WARN:[email protected]]......
  • 记一种Oracle中行转列PIVOT函数的替换方案
    在实际工作中,开发可能会碰到数据需要进行行转列的查询,第一个想到的就是用Oracle的内置函数PIVOT,但PL可能会说,这种查询的性能可能会不太好,项目上要求不使用这个函数,那么有什么方法实现这种查询呢?方案:使用同一张表的row_id来进行关联查询,因为Oracle中表数据的row_id是唯......
  • Ryujinx(Switch模拟器) v1.1.1361 汉化版
    Ryujinx是一款免费、开源的NintendoSwitch模拟器,它可以在电脑上模拟NintendoSwitch游戏机的运行环境,让玩家们能够在PC上畅玩Switch游戏。Ryujinx支持大部分NintendoSwitch游戏,包括TheLegendofZelda:BreathoftheWild、SuperMarioOdyssey等知名游戏,而且还......