原文:Notes on Reactive Programming Part II: Writing Some Code
译者:杰微刊兼职翻译刘晓鹏
在这篇文章中,我们继续响应 式编程(Reactive Programming)这个系列,我们主要通过实际的代码实例来解释某些概念。最终的结果应该能够让你更好的了解到Reactive的不同之处。这里的 例子非常抽象,但是这样可以给你一个思考API和编程风格的方式,并开始感觉到它的不同之处。我们将看到Reactive中的诸多元素,并学习如何控制数 据流,如果有必要的话,将会通过后台线程中进行处理。
建立项目
我们将使用Reactor库来说解释我们的观点。该代码可以很容易地用其他工具编写。如果你想玩转这些代码,弄清楚它的工作原理而不仅仅是复制粘贴,在GitHub有可运行的示例可以参考。
开始可以在 https://start.spring.io中建立一个空白项目,并通过Maven添加对Reactor Core的依赖。
1
2
3
4
5
|
<dependency> <groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
<version>3.0.0.BUILD-SNAPSHOT</version>
</dependency>
|
使用Gradle非常相似:
1
|
compile 'io.projectreactor:reactor-core:3.0.0.BUILD-SNAPSHOT'
|
现在我们开始编写一些代码。
什么使它正常运转?
Reactive的基本模块是 由一系列事件组成的,有两个主要的参与者,一个发布者和一个订阅者。它也可以称其为一个序列“流”,因为它就是这样一个东西。如果我们需要,我们会用一个 小写的s来代表“流”,因为java 8中有一个java.util.Stream,这两者是不同的,所以不要把这两者混淆了。我们主要讨论发布者和订阅者(也就是Reactive Streams)。
我们在示例中使用了 Reactor库,所以我们将一直使用其中的标示符,发布者叫做Flux(它实现了Reactive Streams的Publisher接口)。RxJava库非常类似,包含了很多并行特性,不过在RxJava中,我们将会讨论Observable ,但代码会很相似。(Reactor 2.0将其称之为Stream,这与我们需要讨论的Java 8中Stream容易混淆,所以我们只使用Reactor2.5中的新代码)。
生成器
一个Flux是一个发布者,是一组特定的POJO类型事件的集合,所以它支持泛型,例如Flux<T> 是一个T类型的发布者。 Flux 提供一些静态方便的方法来从多个数据源创建自身的实例。例如,从一个数组创建一个Flux:
我们只要生成了一个Flux,现在就可以用它来完成一些功能。事实上,你只能做两件事情:运算(转换,或者与其他序列相结合),订阅(这是一个发布者)。
单值序列
你经常会遇到一种序列,你知道 它包含只有1或0个元素,例如,一个根据标识符查找实体的存储方法。Reactor有一个Mono类型,表示一个单值或空的Flux。Mono的API 与Flux非常类似,但是更为集中,因为并不是所有的操作对单值序列都是有意义的。RxJava也有一个类似的类(版本1.x)叫做Single,还有另 一个叫Completable的空序列。Reactor用Mono<Void>表示空序列。
操作数
Flux有很多方法,几乎所有的方法都是做运算操作的。在这里我们不会看到这些方法,因为有更好的地方去查看(例如javadocs)。我们只需要知道通过一个操作能获取到什么,它可以为你做什么。
例如,查询一个Flux的内部事件并记录到标准输出,你可以调用.log()方法,或者你可以用map()来进行转换:
1
2
3
4
5
|
Flux<String> flux = Flux.just( "red" , "white" , "blue" );
Flux<String> upper = flux .log()
.map(value -> value.toUpperCase());
|
在这段代码中,我们改变了输入的字符串,将他们转换为大写。不过到目前为止,这些都是微不足道的。
这个小示例中有趣的是——令人震惊,即使你没有使用过它——没有数据处理。因为从字面上来看根本就没有任何记录,什么都没有发生(试一下你就知道)。
调用Flux上的一个操作来建立一个后续的执行计划。它完全是声明式的,这就是为什么人们称其为“functional”的原因。操作的实现逻辑只有在数据开始流动时才执行,也就是说只有当有人订阅Flux(或等价的Publisher)的时候才会执行。
已存在的所有Reactive库中都存在相同的声明式、函数式处理数据集的方式,java 8的 Streams也有类似功能。考虑一下类似的代码,使用相同内容的Stream来代替Flux:
1
2
3
4
5
|
Stream<String> stream = Streams.of( "red" , "white" , "blue" );
Stream<String> upper = stream.map(value -> { System.out.println(value);
return value.toUpperCase();
}); |
根据我们对Flux所做的观 察可以知道:没有数据处理,它只是一个执行计划。但是,Flux和Stream之间有很重要的差别,这种差别使得Stream API不适合于Reactive的使用场景。Flux还有有多操作数,其中大部分只是为了方便,其真正的区别是在消费数据时如何查找下一部分我们需要的数 据。
提示:Sebastien Deleuz的Reactive Types是一篇非常有用的博客,他通过他们定义的类型,描述了流(Stream)与响应式(Reactive)API之间的差异,以及如何使用它们。 Flux和Stream之间更多细节上差异的在这篇文章中都有高亮显示。
订阅者
为了获取数据流,你必须通过 Flux的某个subscribe()方法来进行订阅。只有这些方法能使数据流动 。背后的过程是通过在序列上声明操作链(如果有的话),并请求发布者开 始创建数据来实现的。在我们一直在使用的示例中,这意味着底层的字符串集合是在进行迭代的。在更复杂的示例中可能会触发从文件系统中读取一个文件,或者从 一个数据库中拉取,或者调用HTTP服务。
下面是调用subscribe()的实际示例:
1
2
3
4
|
Flux.just( "red" , "white" , "blue" )
.log()
.map(value -> value.toUpperCase())
.subscribe(); |
输出结果为:
09:17:59.665[main] INFO reactor.core.publisher.FluxLog- onSubscribe(reactor.core.publisher.FluxIterable$IterableSubscription@3ffc5af1) 09:17:59.666[main] INFO reactor.core.publisher.FluxLog- request(unbounded) 09:17:59.666[main] INFO reactor.core.publisher.FluxLog- onNext(red) 09:17:59.667[main] INFO reactor.core.publisher.FluxLog- onNext(white) 09:17:59.667[main] INFO reactor.core.publisher.FluxLog- onNext(blue) 09:17:59.667[main] INFO reactor.core.publisher.FluxLog- onComplete() |
这样我们可以看到无参数方法 subscribe()的效果,即请求发送者发送所有数据?-?只有一request()方法记录,并且它是“无界”的。我们也可以看到每一项的回调:发 布后(onNext()),序列结束(onComplete()),以及原始的订阅(onSubscribe())。如果你需要,你可以通过Flux的 doOn*() 方法来监听这些事情。这些都是自身的操作,不需要订阅,所以,它们不会引起任何数据流动。
subscribe() 方法是被重载的,通过其他一些你给定的选项可以用来控制不同的行为。一个重要且方便的subscribe()的重载方式是将回调函数作为其参数。第一个参 数是一个Consumer,它可以为每一项提供回调方法,你也可以选择添加一个Consumer来处理异常,或者在序列完成后执行一个Runnable。 例如,为每一项提供回调:
1
2
3
4
|
Flux.just( "red" , "white" , "blue" )
.log() .map(value -> value.toUpperCase()) .subscribe(System.out::println); |
输出结果如下:
09:56:12.680[main] INFO reactor.core.publisher.FluxLog- onSubscribe(reactor.core.publisher.FluxArray$ArraySubscription@59f99ea) 09:56:12.682[main] INFO reactor.core.publisher.FluxLog- request(unbounded) 09:56:12.682[main] INFO reactor.core.publisher.FluxLog- onNext(red) RED 09:56:12.682[main] INFO reactor.core.publisher.FluxLog- onNext(white) WHITE 09:56:12.682[main] INFO reactor.core.publisher.FluxLog- onNext(blue) BLUE 09:56:12.682[main] INFO reactor.core.publisher.FluxLog- onComplete() |
我们可以用各种方式来控制数据流,使它们“有界”。通过原生的API进行控制时,可以从一个 Subscriber中获取Subscription。这相当于简短方法subscribe()的冗长形式。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
.subscribe(newSubscriber<String>(){ @Override publicvoid onSubscribe(Subscription s){ s.request(Long.MAX_VALUE);
} @Override publicvoid onNext(String t){ System.out.println(t); } @Override publicvoid onError(Throwable t){ } @Override publicvoid onComplete(){ } }) |
如果要控制流,例如,在一次最多消耗2项,你可以更智能地使用Subscription:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
.subscribe(newSubscriber<String>(){ privatelong count =0; privateSubscription subscription; @Override publicvoid onSubscribe(Subscription subscription){ this .subscription = subscription;
subscription.request(2);
} @Override publicvoid onNext(String t){ count++;
if (count>=2){
count =0;
subscription.request(2);
} } ... |
该Subscriber即为一次“批处理”两项。这是一个非常常见的情形,所以你可能会考虑提取到一个方便的类,使代码更加具备可读性。输出结果如下:
09:47:13.562[main] INFO reactor.core.publisher.FluxLog- onSubscribe(reactor.core.publisher.FluxArray$ArraySubscription@61832929) 09:47:13.564[main] INFO reactor.core.publisher.FluxLog- request(2) 09:47:13.564[main] INFO reactor.core.publisher.FluxLog- onNext(red) 09:47:13.565[main] INFO reactor.core.publisher.FluxLog- onNext(white) 09:47:13.565[main] INFO reactor.core.publisher.FluxLog- request(2) 09:47:13.565[main] INFO reactor.core.publisher.FluxLog- onNext(blue) 09:47:13.565[main] INFO reactor.core.publisher.FluxLog- onComplete() |
事实上,订阅者的批量处理是非常常见的一种情况,所以在Flux里已经有了方便可用方法。上面的批量处理的示例也可以这样来实现:
1
2
3
4
|
Flux.just( "red" , "white" , "blue" )
.log() .map(value -> value.toUpperCase()) .subscribe( null ,2);
|
(注意:subscribe()的调用需要有一个请求限制)。输出结果如下:
10:25:43.739[main] INFO reactor.core.publisher.FluxLog- onSubscribe(reactor.core.publisher.FluxArray$ArraySubscription@4667ae56) 10:25:43.740[main] INFO reactor.core.publisher.FluxLog- request(2) 10:25:43.740[main] INFO reactor.core.publisher.FluxLog- onNext(red) 10:25:43.741[main] INFO reactor.core.publisher.FluxLog- onNext(white) 10:25:43.741[main] INFO reactor.core.publisher.FluxLog- request(2) 10:25:43.741[main] INFO reactor.core.publisher.FluxLog- onNext(blue) 10:25:43.741[main] INFO reactor.core.publisher.FluxLog- onComplete() |
提示:一个处理序列的库(例如 Sping Reactive Web)能够处理订阅。一种好的方式是把这些问题沉淀到栈中,因为它可以让你的代码从业务逻辑中分离出来,使其更具可读性,更容易测试和维护。所以,通常 来说,如果能避免订阅一个序列是一件好事,或者至少把该代码放入到处理层,与业务逻辑分离开来。
线程、调度和后台处理
以上所有的记录都有一个有趣的 特点,那就是它们都是在主线程上执行,即主线程是subscribe()的调用者。这强调了一个重要的观点:Reactor能大量减少线程,因为这样能让 你获取最好的性能。如果在过去的5年中你一直争论线程和线程池与异步执行的问题,这可能是一个令人惊讶的声明,因为这是试图从你的服务中挤出更多的“果 汁”。不过这是真的:没有任何线程的切换,即使优化后的JVM对线程的处理非常有效,但是在单线程做计算还是会更快。Reactor已经处理了所有异步处 理的关键因素,它假设你是知道自己在做什么。
Flux提供了一些配置方法控制线程边界。例如,你可以通过Flux.subscribeOn()来配置订阅在一个后台线程中执行:
1
2
3
4
5
|
Flux.just( "red" , "white" , "blue" )
.log() .map(String::toUpperCase) .subscribeOn(Schedulers.parallel()) .subscribe( null ,2)
|
输出结果如下:
13:43:41.279[parallel-1-1] INFO reactor.core.publisher.FluxLog- onSubscribe(reactor.core.publisher.FluxArray$ArraySubscription@58663fc3) 13:43:41.280[parallel-1-1] INFO reactor.core.publisher.FluxLog- request(2) 13:43:41.281[parallel-1-1] INFO reactor.core.publisher.FluxLog- onNext(red) 13:43:41.281[parallel-1-1] INFO reactor.core.publisher.FluxLog- onNext(white) 13:43:41.281[parallel-1-1] INFO reactor.core.publisher.FluxLog- request(2) 13:43:41.281[parallel-1-1] INFO reactor.core.publisher.FluxLog- onNext(blue) 13:43:41.281[parallel-1-1] INFO reactor.core.publisher.FluxLog- onComplete() |
提示:如果你自己编写这段代码,或者复制粘贴,请记住要在jvm退出之前等待处理停止。
请注意,订阅和所有的处理都发 生在后台一个叫“parallel-1-1”线程中——这是因为我们要求订阅Flux的订阅者在后台运行。如果项目是CPU密集的型话,这将是很好的(但 是实际上,在后台线程中执行是没有意义的,因为你需要关注上下文的切换,但是这样就无法更快的获取结果)。您可能还希望能够执行I / O密集型任务,这有可能会阻塞。在这种情况下,您希望尽快完成任务,而不需要阻塞调用方。一个线程池是你的最好朋友,你可以通过 Schedulers.parallel()获取线程池。要将任务的每一项分布到单独的线程(最多到线程池的限制)中执行,我们需要将它们分成独立的发布 者,并通过一个后台进程来询问每个发布者的结果。我们可以通过一个叫做flatmap()的方法来实现,将每一项映射到一个发布者(可能是不同类型的), 然后返回到一个新类型的序列:
1
2
3
4
5
6
7
8
9
|
Flux.just( "red" , "white" , "blue" )
.log() .flatMap(value -> Mono.just(value.toUpperCase()) .subscribeOn(Schedulers.parallel()), 2) .subscribe(value ->{ log.info( "Consumed: " + value);
}) |
注意,这里使用 flatMap()将项分解到一个“子”发布者中,在“子”发布者中我们可以控制订阅的每一项而不是整个序列。Reactor内建的默认行为是尽可能长的 维持在一个线程中,如果我们想要在后台线程中处理特殊的项或者对项进行分组,我们就需要显示的声明。事实上,这是一项公认的强制进行并行处理的技巧之一 (更多细节问题见Reactive Gems)。
输出结果如下:
1
2
3
4
5
6
7
8
9
10
|
15:24:36.596[main] INFO reactor.core.publisher.FluxLog- onSubscribe(reactor.core.publisher.FluxIterable$IterableSubscription@6f1fba17) 15:24:36.610[main] INFO reactor.core.publisher.FluxLog- request(2) 15:24:36.610[main] INFO reactor.core.publisher.FluxLog- onNext(red) 15:24:36.613[main] INFO reactor.core.publisher.FluxLog- onNext(white) 15:24:36.613[parallel-1-1] INFO com.example.FluxFeaturesTests-Consumed: RED 15:24:36.613[parallel-1-1] INFO reactor.core.publisher.FluxLog- request(1) 15:24:36.613[parallel-1-1] INFO reactor.core.publisher.FluxLog- onNext(blue) 15:24:36.613[parallel-1-1] INFO reactor.core.publisher.FluxLog- onComplete() 15:24:36.614[parallel-3-1] INFO com.example.FluxFeaturesTests-Consumed: BLUE 15:24:36.617[parallel-2-1] INFO com.example.FluxFeaturesTests-Consumed: WHITE |
注意,现在有多个线程来消费 数据项,flatmap()是隐式的进行并发处理,只要有可用的项,就要确保任何给定的时间内都有2个数据项在处理。我们可以看到许多 request(1),这是因为系统试图在管道中保持2个数据项,通常它们不会在同一时间点上完成。事实上,Reactor的尝试是非常聪明的,它会从上 游发布者中预取数据项,以便消除订阅者的等待时间(我们在这没有看到,是因为这里的数据项太少?-?我们只处理3项)。
提示:(“red”、“white”、“blue”)三项可能太少,很难证明后台有多个线程在执行,所以生成更多数据可能会更合适。例如,你可以用一个随机数生成器来实现。
Flux也有一个同样的publishOn()方法,但是它是通过监听器(如onNext()或消费者(consumer)回调),而不是订阅者自身来实现的:
1
2
3
4
5
6
7
8
|
Flux.just( "red" , "white" , "blue" )
.log() .map(String::toUpperCase) .subscribeOn(Schedulers.newParallel( "sub" ))
.publishOn(Schedulers.newParallel( "pub" ),2)
.subscribe(value ->{ log.info( "Consumed: " + value);
}); |
输出结果如下:
15:12:09.750[sub-1-1] INFO reactor.core.publisher.FluxLog- onSubscribe(reactor.core.publisher.FluxIterable$IterableSubscription@172ed57) 15:12:09.758[sub-1-1] INFO reactor.core.publisher.FluxLog- request(2) 15:12:09.759[sub-1-1] INFO reactor.core.publisher.FluxLog- onNext(red) 15:12:09.759[sub-1-1] INFO reactor.core.publisher.FluxLog- onNext(white) 15:12:09.770[pub-1-1] INFO com.example.FluxFeaturesTests-Consumed: RED 15:12:09.771[pub-1-1] INFO com.example.FluxFeaturesTests-Consumed: WHITE 15:12:09.777[sub-1-1] INFO reactor.core.publisher.FluxLog- request(2) 15:12:09.777[sub-1-1] INFO reactor.core.publisher.FluxLog- onNext(blue) 15:12:09.777[sub-1-1] INFO reactor.core.publisher.FluxLog- onComplete() 15:12:09.783[pub-1-1] INFO com.example.FluxFeaturesTests-Consumed: BLUE |
请注意,消费回调(记为 “Consumed:…”)是在发布者线程pub-1-1上的。如果你调用subscribeOn(),你可以看在pub-1-1线程上看到所有的处理数 据的第二个chunk。这再次反映了Reactor的线程节约——如果没有显式请求线程切换,它下一个请求便停留在相同的线程上。
注意:
我们改变了这个示例中的代码,将subscribe(null, 2)改为一个包含prefetch=2的publishon()方法。在这种情况下,subscribe()中隐式获取数据的尺寸将会被忽略。
提取器:黑暗面的订阅者
还有另一种方式订阅一个序列, 即Mono.block()或Mono.toFuture()或Flux.toStream()(这些都是“提取”的方法?-?这些方法获取到的 Reactive类型缺乏灵活性、阻碍抽象)。Flux也有转换器collectlist()和collectmap(),可以将Flux 转换为 Mono。实际上它们不订阅序列,但是它们可以对独立的数据项以任何方式来实现你通过订阅完成的功能。
警告
一个很好的经验法则是"永远不要调用extractor"。当然,也有一些例外(否则方法将不会存在)。一个值得注意的例外是测试中,因为它可以阻塞计算结果,这是非常有用的。
如果你需要适应旧的API, 例如Spring MVC,这些方法提供一个从Reactive到阻塞“逃生舱”。当你调用Mono.block()你就放弃Reactive Streams的所有好处。这就是Reactive Streams和java8 Streams?的本质区别——java原生的Stream只具有“全有或全无”的订阅模式,即相当于Mono.block()。当 然,subscribe()可以阻塞调用线程,所以它像转换器方法一样危险,但是你具有更好的控制——你可以通过使用subscribeOn()来代替, 你也可以通过一段时间的后台压力测试来决定是否继续使用。
结论
在这篇文章中,我们介绍了 Reactive Streams和Reactor API的基本知识。如果你需要知道更多细节,有很多地方可以查看,但是不能代替手动编码,所以,可以使用GitHub中的代码(本文中的测试项目叫做 “flux”),或前往 Lite RX Hands On专题讨论会。到目前为止,这真的只是略读,我们还没有学到太多,没有证明通过非Reactive工具不能以一种更明显的方式来完成的功能。本系列的下 一篇文章将更深入地了解Reactive模型中的阻塞,调度和异步处理,并演示在什么情况下使用可以能获得真正的好处。
http://www.jointforce.com/jfperiodical/article/2866
相关推荐
通过CSS,你可以控制字体、颜色、布局、响应式设计等,实现美观的界面。CSS有多种选择器,如类选择器、ID选择器和伪类,以及盒模型、浮动和定位等布局技术。CSS3带来了更多高级功能,如动画、过渡和新的选择器。 4....
《VB.NET高级编程技巧笔记及示例代码》是针对熟练掌握基础VB.NET语法的开发者们的一份宝贵资源,旨在深入探讨和提升编程技能。这份资料包含了一系列精心挑选的主题和实战代码,帮助开发者们掌握VB.NET的高级特性,...
【标题】"01天气网笔记+代码.zip" 涉及的知识点主要围绕着天气网站的开发,可能包括前端界面设计、后端数据获取与处理以及编程代码实践。这个压缩包包含的资源可能帮助我们深入理解如何创建一个天气查询应用。 ...
8. **异步编程**:掌握异步编程的基础,包括async/await关键字,以提高应用程序的响应性和性能。 9. **Windows Forms或WPF**:可能涉及创建桌面应用程序的基础,如UI设计、控件使用、事件处理等,帮助学习者实现...
这款应用的核心目标是帮助用户轻松地编写和保存他们的笔记。本项目使用了Express.js,这是一个基于Node.js的轻量级Web应用程序框架,特别适合构建API和服务。 Express.js允许开发者快速地设置路由,处理HTTP请求...
笔记 所有笔记可以通过wiki查看: 更新 增加深入剖析 JavaScriptCore: 增加读 SnapKit 和 Masonry 自动布局框架源码: 增加Swift 项目中涉及到 JSONDecoder,...增加iOS函数响应式编程以及ReactiveCocoa的使用: 增
- 移动端适配:响应式设计,适应不同设备浏览。 这15章的学习笔记详尽地介绍了ASP网络编程技术,并结合网上商城实例,让读者能够理论联系实际,逐步掌握开发动态网站的能力。通过学习,不仅可以提升编程技能,还能...
2. **语法高亮**:例如,对于编程相关的笔记,可以使用插件实现代码语法高亮,使得代码片段更易阅读和理解。 3. **Markdown支持**:如 dein.vim 或 Vim-Plug 等插件管理工具可以安装像Markdown Preview Plus这样的...
这些框架提供了一种更高效的方式来构建交互式和响应式的Web应用。 8. **云部署**:考虑到是“云笔记”,应用可能部署在Azure、AWS或Google Cloud等云平台上。了解如何配置和管理云服务,如虚拟机、负载均衡和自动...
- **界面设计**:采用响应式设计,确保不同设备上的良好用户体验。 - **安全性设计**:实现用户认证与授权机制,保护敏感信息的安全。 #### 2.3 实现细节 - **前后端分离**:前端负责展示逻辑,后端提供API接口,...
7. **响应式设计**:为了适应不同设备的屏幕尺寸,广告条可能需要具备响应式布局。源代码可能会包含调整大小和定位的算法。 8. **发布和嵌入**:最后,源代码还会包含如何将完成的Flash Banner导出为SWF格式,并在...
6. **响应式编程**:Spring 5引入了对Reactive Streams的支持,允许开发者创建非阻塞、事件驱动的应用,提升了系统的并发性能。 7. **Spring Data**:Spring Data项目简化了数据库访问,支持多种持久化技术,如JPA...
6. **函数式编程**:Python支持函数式编程特性,如高阶函数、map、filter、reduce等,了解这些可以写出更简洁的代码。 7. **模块化编程**:使用import导入模块,以及如何创建和使用自定义模块,有助于代码的组织和...
6. **响应式设计**:FireMonkey 支持响应式布局,使得应用程序可以在不同屏幕尺寸和分辨率的设备上自适应显示。 7. **性能优化**:FireMonkey 直接生成原生代码,避免了中间层解释,提高了运行时性能,尤其是对于...
根据提供的文件信息,可以看出这是一份关于Python编程的学习笔记,主要涵盖了Python的基础知识、特性、用法以及一些高级主题如异常处理、文件操作等。下面将对这些知识点进行详细的整理和解释。 ### 1. Python基础 ...
- Bootstrap或Vue.js:可能用于美化页面和增强用户体验,实现响应式布局。 6. **文件上传与下载**: - 项目可能涉及菜品图片的上传,这需要用到文件处理API,以及理解HTTP协议中的文件上传规范。 7. **安全机制*...
7. **Python的交互式编程**: Python的REPL(Read-Eval-Print Loop)使得开发者可以在命令行直接测试代码片段,快速验证思路。 8. **单元测试**: Python中使用unittest库进行单元测试,编写测试用例确保代码功能...
**服务** (Services) 是一种请求/响应式的通信机制,而**参数** (Parameters) 是用于配置节点的值。 ##### 7.1 ROS Services 服务允许两个节点之间进行双向通信。 ##### 7.2 用rosservice命令 - **`rosservice ...
7. **函数式编程**:介绍Python中的高阶函数,如map、filter、reduce,以及lambda表达式和闭包。 8. **正则表达式**:学习使用re模块进行字符串匹配和搜索,处理复杂的文本数据。 9. **网络编程**:了解如何使用...
【jQuery响应式宽屏幻灯片无缝切换代码】 在网页设计中,动态的幻灯片效果经常被用来吸引用户注意力并展示重要的信息或内容。jQuery,一个广泛使用的JavaScript库,因其简洁的API和丰富的功能而成为了实现这类效果...