Lv.的博客

c++ Reactive Extensions (Rx) 反应式扩展简介与安装

反应式扩展

Reactive Extensions (Rx) 是一个库,用于使用可观察序列和 LINQ 样式的查询运算符组成异步和基于事件的程序。

数据序列可以采用多种形式,例如来自文件或 Web 服务的数据流、Web 服务请求、系统通知或一系列事件(例如用户输入)。

Reactive Extensions 将所有这些数据序列表示为可观察序列。应用程序可以订阅这些可观察序列以在新数据到达时接收异步通知。Rx 库可用于 C++、.NET、Ruby、Python、Silverlight、Windows Phone 7 和 JavaScript 中的应用程序开发。有关这些不同平台的更多信息,请参阅 Rx 版本之间的差异主题。

拉取数据与推送数据

在交互式编程中,应用程序通过从代表源的序列中提取数据来主动轮询数据源以获取更多信息。迭代器允许我们通过返回当前属性来获取当前项,并确定是否还有更多项要迭代(通过调用一些 on_next 方法)。

应用程序在数据检索过程中处于活动状态,在方便时通过调用 on_next 来控制检索的速度。这种模式是同步的,这意味着应用程序可能在轮询数据源时被阻塞。这种拉动模式类似于访问您的图书馆并查看一本书。读完这本书后,您再次访问以查看另一本书。

另一方面,在反应式编程中,通过订阅数据流(在 Rx 中称为可观察序列)为应用程序提供更多信息,并且任何更新都从源传递给它。应用程序在数据检索过程中是被动的:除了订阅可观察源之外,它不会主动轮询源,而只是对推送给它的数据做出反应。当流没有更多数据可提供时,或者当它出错时,源将向订阅者发送通知。这样,应用程序就不会因为等待源更新而被阻塞。

这是 Reactive Extensions 采用的推送模式。这类似于加入一个读书俱乐部,您在其中注册您对特定类型的兴趣,并且符合您兴趣的书籍在出版时会自动发送给您。你不需要排队来获得你想要的东西。采用推送模式在许多情况下都有帮助,尤其是在 UI 繁重的环境中,在该环境中,当应用程序等待某些事件时,无法阻止 UI 线程。总之,通过使用 Rx,您可以使您的应用程序更具响应性。

Rx 实现的推送模型由 Rx.Observable/Observer 的可观察模式表示。Rx.Observable 将自动通知所有观察者任何状态变化。要通过订阅注册兴趣,您可以使用 Rx.Observable 的 subscribe 方法,该方法接受一个 Observer 并返回一个一次性的。这使您能够跟踪和处置订阅。此外,Rx 在可观察序列上的 LINQ 实现允许开发人员在基于推送的序列(例如事件、基于 APM(“AsyncResult”)计算、基于任务的计算和异步工作流)上组合复杂的事件处理查询。有关 Observable/Observer 类的更多信息,请参阅探索 Rx 中的主要类。有关在 Rx 中使用不同功能的教程,请参阅使用 Rx。

开始使用 Rx

本节一般性地描述了响应式扩展 (Rx) 是什么,以及它如何使创建异步应用程序的程序员受益。

在这个部分

1. 什么时候使用 Rx
2. 安装 Rx
3. Rx 版本之间的差异

相关章节

在 MSDN 开发人员中心使用 Rx
反应式扩展

你什么时候使用 Rx

本主题为当前使用 .NET 事件模型进行异步编程的用户描述了使用 Rx 的优势。

使用 Rx 的优势

无论您是在创作传统的桌面应用程序还是基于 Web 的应用程序,您都必须时不时地处理异步编程。桌面应用程序的 I/O 或 UI 线程可能需要很长时间才能完成并可能阻塞所有其他活动线程。但是,现代异步编程模型的用户必须手动管理异常和事件取消。要编写或过滤事件,他必须编写难以破译和维护的自定义代码。

此外,如果您的应用程序与多个数据源交互,管理所有这些交互的常规方法是实现单独的方法作为每个数据流的事件处理程序。例如,一旦用户键入字符,就会将 keydown 事件推送到您的 keydown 事件处理程序方法。在这个 keydown 事件处理程序中,您必须提供代码来对该事件做出反应,或者在所有不同的数据流之间进行协调,并将这些数据处理成可用的形式。

使用 Rx,您可以表示多个异步数据流(来自不同来源,例如股票报价、推文、计算机事件、Web 服务请求等),并使用 Observer 类订阅事件流。Observable 类维护一个依赖 Observer 线程的列表,并自动通知它们任何状态更改。您可以使用由 Rx.Observable 类型实现的标准 LINQ 查询运算符来查询可观察序列。因此,您可以使用这些 LINQ 运算符轻松过滤、聚合和组合多个事件。取消和异常也可以通过使用 Rx 提供的扩展方法来优雅地处理。

下面的例子展示了在 C++ 中实现一个 observable 是多么容易。

    auto ints = rxcpp::observable<>::create(
        [](rxcpp::subscriber<int> s){
            s.on_next(1);
            s.on_next(2);
            s.on_completed();
    });

    ints.
        subscribe(
            [](int v){printf("OnNext: %d\n", v);},
            [](){printf("OnCompleted\n");});

 

您还可以使用调度程序来控制订阅何时开始,以及何时将通知推送给订阅者。有关这方面的更多信息,请参阅使用调度程序进行并发控制。

过滤

C++ 事件模型的一个缺点是,每次引发事件时总是调用事件处理程序,并且事件的到达与源发出的完全相同。要在调用处理程序之前过滤掉您不感兴趣的事件或转换数据,您必须向处理程序添加自定义过滤器逻辑。

以检测鼠标按下的应用程序为例。在当前的事件编程模型中,应用程序可以通过显示消息来响应引发的事件。在 Rx 中,此类鼠标按下事件被视为有关点击的信息流。每当您单击鼠标时,有关此单击的信息(例如,光标位置)都会出现在一个流中,准备好进行处理。在这个范例中,事件(或事件流)与列表或其他集合非常相似。这意味着我们可以使用处理集合的技术来处理事件。例如,您可以过滤掉出现在特定区域之外的那些点击,并且仅在用户在区域内点击时显示消息。或者你可以等待一个特定的时间段,并告知用户在此期间的“有效”点击次数。相似地,您可以捕获股票报价流,并仅响应在特定时间窗口内特定范围内发生变化的报价。所有这些都可以通过使用 Rx 提供的 LINQ 查询样式的运算符轻松完成。

通过这种方式,函数可以获取一个事件,对其进行处理,然后将处理后的流传递给应用程序。这为您提供了当前编程模型中不可用的灵活性。此外,由于 Rx 在后台执行所有管道工作以过滤、同步和转换数据,因此您的处理程序可以对其接收的数据做出反应并对其进行处理。这会产生更简洁的代码,更易于阅读和维护。有关过滤的更多信息,请参阅使用 LINQ 运算符查询可观察集合。

操纵事件

Rx 将事件表示为对象的集合:例如,OnMouseMove 事件包含 Point 值的集合。由于可观察对象的一流对象性质,它们可以作为函数参数和返回值传递,或存储在变量中。

安装 Rx

本主题介绍您可以在何处下载反应式扩展 (Rx) SDK。

下载 Rx

Reactive Extensions 可用于不同的平台,例如 C++、Javascript、.NET Framework 3.5、4.0、4.5、Silverlight 3 和 4,以及 Windows Phone 7 和 8。您可以下载这些库,并在以下位置了解它们的先决条件Rx MSDN 开发人员中心

Rx 版本之间的差异

以下主题描述了您可以使用响应式扩展开发解决方案的各种平台。

要获取最新版本的 Rx,以及了解其先决条件,请访问Rx MSDN 开发人员中心

C++

C++ 响应式扩展 (RxCpp) 是一个库,用于在 C++ 中使用可观察序列和 LINQ 样式的查询运算符来编写异步和基于事件的程序。

Javascript

Rx for Javascript (RxJS) 允许您在 JavaScript 中使用 LINQ 运算符。它提供了从现有 DOM、XmlHttpRequest (AJAX) 和 jQuery 事件到基于推送的可观察集合的易于使用的转换,允许用户将 Rx 无缝集成到他们现有的基于 JavaScript 的网站中。

RxJS 为客户端脚本带来了类似的功能,并与 jQuery 事件(Rx.Observable.FromJQueryEvent)集成。它还支持脚本#。

Ruby

Rx for Ruby (Rx.rb) 允许您使用 Linq 运算符在 Ruby 中创建基于推送的可观察集合。

Python

RX for Python (Rx.py) 允许您在 Python 中使用 Linq 运算符。Rx.py 允许您实现基于推送的可观察集合,允许用户将 Rx 无缝集成到他们现有的 Python 应用程序中。

.NET 框架

核心 Rx 接口,IObservable和 IObserver,作为 .NET Framework 4 的一部分发布。如果您在 .NET Framework 3.5 SP1 上运行,或者如果您想利用以 Observable 类型实现的 LINQ 运算符以及许多其他功能(例如调度程序),您可以下载Rx MSDN 开发人员中心中的 Rx 仅标头库

Silverlight 

Silverlight 不允许您进行跨线程调用,因此您不能使用后台线程来更新 UI。您可以使用 Rx 仅标头库提供的工厂 Observable.Start 方法来异步调用操作,而不是使用 Dispatcher.BeginInvoke 调用来编写详细代码以在主 UI 线程上显式执行代码。引擎盖下的 Rx 透明地处理交叉线程。

您还可以使用接收调度程序的各种 Observable 运算符重载,并指定要使用的 System.Reactive.Concurrency.DispatcherScheduler。

Windows Phon

Windows Phone 7 附带了一个嵌入到设备 ROM 中的响应式扩展版本。有关详细信息,请参阅适用于 Windows Phone 的 .NET 反应式扩展概述此版本的响应式扩展的文档可以在Microsoft.Phone.Reactive 命名空间的 Windows Phone API 库中找到。

Rx MSDN 开发人员中心还包含更新版本的 Rx for WP7,它在 System.Reactive.Linq 命名空间中有新定义请注意,新的 API 不会与手机内​​置的库发生冲突(它们也不会替换 ROM 中的版本)。有关这 2 个版本的差异的更多信息,请参阅此Rx 团队博客文章

Rx 可用于 Windows Phone 8 和 Windows Phone 7。使用 Nuget 可以使用 .NET 可移植库,该库可用于开发适用于 Windows Phone、Windows 应用商店应用程序以及经典 Windows 桌面或服务器应用程序的库。

使用接收

本节包括解释如何使用 Rx 创建和订阅序列、桥接现有事件和现有异步模式以及使用调度程序的主题。它还描述了更高级的任务,例如实现您自己的运算符。

在这个部分

1. 探索 Rx 中的主要接口
2. 创建和查询事件流
3. 主题
6. 为 IObservable 实现自己的操作符
7. 使用 Observable 提供程序

探索 Rx 中的主要接口

本主题描述了用于表示可观察序列并订阅它们的主要反应扩展 (Rx) 接口。

可观察/观察者

Rx 将异步和基于事件的数据源公开为基于推送的、可观察的序列。这个 Observable 类代表了一个可以被观察到的数据源,这意味着它可以向任何感兴趣的人发送数据。它维护一个代表这些感兴趣的侦听器的依赖 Observer 实现的列表,并自动通知它们任何状态更改。

正如什么是 Rx 中所述,推送模型的另一半由 Observer 类表示,该类表示通过订阅注册兴趣的观察者。随后将项目从它订阅的可观察序列传递给观察者。

为了从 observable 集合接收通知,您使用 Observable 的 subscribe 方法将 Observer 对象传递给它作为对这个观察者的回报,subscribe 方法返回一个一次性对象,该对象充当订阅的句柄。这允许您在完成后清理订阅。对该对象调用 dispose 会将观察者与源分离,以便不再传递通知。正如你可以推断的,在 Rx 中你不需要显式地取消订阅一个事件。

观察者支持三个发布事件,由接口的方法反映。当可观察数据源有可用数据时,可以调用 OnNext 零次或多次。例如,用于鼠标移动事件的可观察数据源可以在每次鼠标移动时发送一个 Point 对象。其他两种方法用于指示完成或错误。

下面列出了 Observable/Observer 定义。

namespace rxcpp {
    template <class T>
    struct subscriber
    {
        // observer<T>
        void on_next(T);
        void on_error(std::exception_ptr);
        void on_completed();

        // composite_subscription
        bool is_subscribed();
        void unsubscribe();
    };

    template <class T>
    struct observable
    {
        composite_subscription subscribe(subscriber<T> out);
    };
}

 

请注意,OnError 事件返回 exception_ptr 类型。上面的示例显示将错误传递给处理函数。

您可以将可观察序列(例如鼠标悬停事件序列)视为普通集合。因此,您可以在集合上编写 LINQ 查询来执行过滤、分组、组合等操作。为了使可观察序列更有用,Rx 仅标头库提供了许多工厂 LINQ 运算符,因此您不需要靠自己实现任何这些。这将在使用 LINQ 运算符查询 Observable 集合主题中进行介绍。

也可以看看

创建和订阅简单的 Observable 序列 使用 LINQ 运算符查询 Observable 集合

创建和查询 Observable 序列

本节介绍如何创建和订阅可观察序列,将现有 C++ 事件转换为序列并查询它。

在这个部分

创建和订阅简单的 Observable 序列
使用 LINQ 运算符查询 Observable 集合

创建和订阅简单的可观察序列

您无需手动实现 Observable 接口即可创建可观察序列。同样,您也不需要实现 Observer 来订阅序列。通过安装 Reactive Extension 仅标头库,您可以利用 Observable 类型,该类型为您提供许多 LINQ 运算符来创建包含零个、一个或多个元素的简单序列。此外,Rx 提供了 Subscribe 方法,这些方法在委托方面采用 OnNext、OnError 和 OnCompleted 处理程序的各种组合。

创建和订阅一个简单的序列

以下示例使用 Observable 类型的范围运算符创建一个简单的可观察数字集合。观察者使用 Observable 类的 Subscribe 方法订阅此集合,并提供作为处理 OnNext、OnCompleted 和 OnError 的委托的操作。

范围运算符有几个重载。在我们的示例中,它创建了一个以 x 开头的整数序列,然后生成 y 个序列号。

一旦订阅发生,值就会发送给观察者。OnNext 委托然后打印出这些值。

    auto values1 = rxcpp::observable<>::range(1, 5);
    values1.
        subscribe(
            [](int v){printf("OnNext: %d\n", v);},
            [](){printf("OnCompleted\n");});

 

当观察者订阅可观察序列时,调用 subscribe 方法的线程可能与序列运行直到完成的线程不同。因此,订阅调用是异步的,因为在序列观察完成之前调用者不会被阻塞。这将在使用调度程序主题中详细介绍。

请注意,subscribe 方法返回一个 Disposable,因此您可以取消订阅一个序列并轻松地处理它。当您在可观察序列上调用 Dispose 方法时,观察者将停止侦听可观察数据。通常,您不需要显式调用 Dispose,除非您需要提前取消订阅,或者当源 observable 序列的生命周期比观察者更长时。Rx 中的订阅是为不使用终结器的即发即弃场景而设计的。当垃圾收集器收集 Disposable 实例时,Rx 不会自动释放订阅。但是,请注意 Observable 操作符的默认行为是尽快处理订阅(即,当发布 OnCompleted 或 OnError 消息时)。

除了从头开始创建可观察序列之外,您还可以将现有的枚举器、C++ 事件和异步模式转换为可观察序列。本节中的其他主题将向您展示如何执行此操作。

请注意,本主题仅向您展示了一些可以从头开始创建可观察序列的运算符。要了解有关其他 LINQ 运算符的更多信息,请参阅使用 LINQ 运算符查询可观察集合。

将可枚举集合转换为可观察序列

使用 Iterate 运算符,您可以将数组集合转换为可观察序列并订阅它。

    std::array< int, 3 > a={{1, 2, 3}};
    auto values1 = rxcpp::observable<>::iterate(a);
    values1.
        subscribe(
            [](int v){printf("OnNext: %d\n", v);},
            [](){printf("OnCompleted\n");});

 

 

也可以看看

使用 LINQ 运算符查询 Observable 集合

使用 LINQ 运算符查询可观察序列

我们已经将现有的 C++ 事件转换为可观察的序列来订阅它们。在本主题中,我们将观察可观察序列的一流性质作为可观察对象,其中通用 LINQ 运算符由 Rx 仅标头库提供以操作这些对象。大多数运算符采用可观察序列并对其执行一些逻辑并输出另一个可观察序列。此外,正如您从我们的代码示例中看到的那样,您甚至可以在源序列上链接多个运算符,以根据您的确切要求调整生成的序列。

使用不同的运算符

我们已经在前面的主题中使用了 Create 和 Generate 运算符来创建和返回简单序列。在本主题中,我们将使用 Observable 类型的其他 LINQ 运算符,以便您可以过滤、分组和转换数据。此类运算符将可观察序列作为输入,并产生可观察序列作为输出。

组合不同的序列

在本节中,我们将研究一些将各种可观察序列组合成单个可观察序列的运算符。请注意,当我们组合序列时,数据不会被转换。

在下面的示例中,我们使用 concat 运算符将两个序列组合成一个序列并订阅它。

    auto values = rxcpp::observable<>::range(1); // infinite (until overflow) stream of integers

    auto s1 = values.
        take(3).
        map([](int prime) { return std::make_tuple("1:", prime);});

    auto s2 = values.
        take(3).
        map([](int prime) { return std::make_tuple("2:", prime);});

    s1.
        concat(s2).
        subscribe(rxcpp::util::apply_to(
            [](const char* s, int p) {
                printf("%s %d\n", s, p);
            }));

 

请注意,结果序列是

1:1
1:2
1:3
2:1
2:2
2:3

这是因为当您使用 concat 运算符时,在第一个序列 (source1) 完成推送其所有值之后,第二个序列 (source2) 才会处于活动状态。只有在 source1 完成后,source2 才会开始将值推送到结果序列。然后,订阅者将从结果序列中获取所有值。

将此与合并运算符进行比较。如果你运行下面的示例代码,你会得到

1:1
2:1
1:2
2:2
1:3
2:3

这是因为这两个序列同时处于活动状态,并且值在源中出现时被推出。结果序列仅在最后一个源序列完成推送值时完成。

请注意,要让 Merge 工作,所有源 observable 序列都需要属于同一类型的 Observable。结果序列将是 Observable 类型。如果 source1 在序列中间产生 OnError,则生成的序列将立即完成。

    auto values = rxcpp::observable<>::range(1); // infinite (until overflow) stream of integers

    auto s1 = values.
        map([](int prime) { return std::make_tuple("1:", prime);});

    auto s2 = values.
        map([](int prime) { return std::make_tuple("2:", prime);});

    s1.
        merge(s2).
        take(6).
        as_blocking().
        subscribe(rxcpp::util::apply_to(
            [](const char* s, int p) {
                printf("%s %d\n", s, p);
            }));

 

主题

本节描述了响应式扩展实现的主题类型。它还描述了用于不同目的的 Subject 的各种实现。

在这个部分

1. 使用主题

使用主题

Subject 类型同时实现了 Observable 和 Observer,从某种意义上说,它既是观察者又是可观察对象。您可以使用主题订阅所有观察者,然后将主题订阅到后端数据源。这样,主体可以充当一组订阅者和源的代理您可以使用主题来实现具有缓存、缓冲和时移的自定义 observable。此外,您可以使用主题向多个订阅者广播数据。

默认情况下,主题不会跨线程执行任何同步。他们不采用调度程序,而是假设所有序列化和语法正确性都由主题的调用者处理。主题只是向订阅者线程安全列表中的所有订阅观察者广播。这样做的好处是可以减少开销并提高性能。但是,如果您想使用调度程序同步对观察者的传出调用,则可以使用 Synchronize 方法来执行此操作。

不同类型的主题

Rx 库中的 Subject 类型是 Subject 接口的基本实现(您也可以实现 Subject 接口来创建自己的主题类型)。还有其他提供不同功能的 Subject 实现。所有这些类型都存储一些(或全部)通过 OnNext 推送给它们的值,并将其广播回其观察者。这意味着如果您多次订阅其中任何一项(即订阅 -> 取消订阅 -> 再次订阅),您将再次看到至少一个相同的值。

调度和并发

本节介绍如何使用调度程序来控制何时开始序列或订阅事件。

调度程序类型

Rx 提供的各种调度器类型有:

ImmediateScheduler:默认调度程序,在收到通知时推送通知。

EventLoopScheduler:在为 Rx 序列创建单独的线程时使用。

使用调度程序

调度程序控制订阅何时开始以及何时发布通知。它由三个组件组成。首先是数据结构。当您安排要完成的任务时,它们会被放入调度程序以根据优先级或其他标准进行排队。它还提供了一个执行上下文,表示任务在哪里执行(例如,在线程池、当前线程或另一个应用程序域中)。最后,它有一个为自己提供时间概念的时钟(通过访问调度程序的 Now 属性)。在特定调度程序上调度的任务将仅遵守该时钟指示的时间。

使用调度程序

调度程序控制订阅何时开始以及何时发布通知。它由三个组件组成。首先是数据结构。当您安排要完成的任务时,它们会被放入调度程序以根据优先级或其他标准进行排队。它还提供了一个执行上下文,表示任务在哪里执行(例如,在线程池、当前线程或另一个应用程序域中)。最后,它有一个为自己提供时间概念的时钟(通过访问调度程序的 Now 属性)。在特定调度程序上调度的任务将仅遵守该时钟指示的时间。

使用调度程序

您可能已经在您的 Rx 代码中使用了调度程序,而没有明确说明要使用的调度程序的类型这是因为所有处理并发的 Observable 操作符都有多个重载。如果你不使用以调度器为参数的重载,Rx 将使用最小并发原则选择一个默认调度器。这意味着选择引入满足操作员需求的最少并发量的调度程序。例如,对于返回带有有限且少量消息的 observable 的操作符,Rx 调用 ImmediateScheduler。对于返回可能大量或无限数量的消息的运算符,调用 CurrentThread

在下面的示例中,源 observable 序列每个都使用 EventLoopScheduler 在它们自己的线程中运行

  auto threads = rxcpp::observe_on_event_loop();

    auto values = rxcpp::observable<>::range(1); // infinite (until overflow) stream of integers

    auto s1 = values.
        subscribe_on(threads).
        map([](int prime) { std::this_thread::yield(); return std::make_tuple("1:", prime);});

    auto s2 = values.
        subscribe_on(threads).
        map([](int prime) { std::this_thread::yield(); return std::make_tuple("2:", prime);});

    s1.
        merge(s2).
        take(6).
        observe_on(threads).
        as_blocking().
        subscribe(rxcpp::util::apply_to(
            [](const char* s, int p) {
                printf("%s %d\n", s, p);
            }));

 

这将很快在观察者上排队。此代码使用 observe_on 运算符,它允许您指定要用于向观察者发送推送通知 (OnNext) 的上下文默认情况下,observe_on 运算符确保 OnNext 将在当前线程上被尽可能多地调用。您可以使用它的重载并将 OnNext 输出重定向到不同的上下文。此外,您可以使用 subscribe_on 运算符返回一个代理 observable,它将操作委托给特定的调度程序。例如,对于 UI 密集型应用程序,您可以通过使用 subscribe_on 并将 Concurrency.EventLoopScheduler 传递给它,将所有后台操作委托给在后台运行的调度程序上执行。

您还应该注意,通过使用 observe_on 运算符,将为通过原始 observable 序列的每条消息安排一个操作。这可能会改变时序信息并给系统带来额外的压力。如果您有一个查询组成了在许多不同的执行上下文上运行的各种可观察序列,并且您正在查询中进行过滤,那么最好在查询中稍后放置 observable_on这是因为查询可能会过滤掉大量消息,并且将 observe_on 运算符放在查询的前面将对无论如何都会被过滤掉的消息做额外的工作。在查询结束时调用 observe_on 操作符将产生最小的性能影响。

为 Observable 实现自己的操作符

您可以通过为 LINQ 库未提供的操作添加新运算符来扩展 Rx,或者通过创建自己的标准查询运算符实现来提高可读性和性能。当您想要使用内存中的对象进行操作并且预期的自定义不需要查询的全面视图时,编写标准 LINQ 运算符的自定义版本很有用。

创建新的运算符

LINQ 提供了一整套操作符,涵盖了对一组实体的大部分可能操作。但是,您可能需要一个运算符来为您的查询添加特定的语义——特别是如果您可以在代码中多次重复使用相同的运算符。

通过在构建新的 LINQ 运算符时重用现有的 LINQ 运算符,您可以利用 Rx 库中实现的现有性能或异常处理功能。

编写自定义运算符时,最好不要留下任何未使用的一次性用品;否则,您可能会发现资源实际上可能会泄漏,并且取消可能无法正常工作。

自定义现有运算符

向 LINQ 添加新运算符是扩展其功能的一种方式。但是,您还可以通过将现有运算符包装成更专业和更有意义的运算符来提高代码的可读性。

posted @ 2022-05-28 12:04  Avatarx  阅读(829)  评论(0编辑  收藏  举报