Reactive Cocoa Tutorial

YOLO26算法大门区域检测+训练好的模型+285张数据集+pyqt可视化界面.zip 附训练好的 YOLO 权重与 PyQt 可视化检测界面,页面底部含可视化效果预览,毕设/课设开箱即用。 【数据集概况】 · 检测类别(中文):[大门(gates)] 【训练曲线与评估图】 【模型训练配置】 参数 | 值 模型 | yolo26n 训练轮数 | 100 epochs 输入尺寸 | 640x640 批次大小 | 24 优化器 | auto 初始学习率 | 0.01 训练设备 【关键指标汇总】 训练了 64 个 epoch,最终轮指标: 指标 | 数值 mAP50 | **0.6364** mAP50-95 | 0.4539 Precision | 0.7309 Recall | 0.6111 train/box_loss | 0.6662 train/cls_loss | 0.5916 val/box_loss | 1.3351 val/cls_loss | 1.5479 【训练过程分析】 训练过程可分成三个主要阶段:前期、中期和后期。刚开始时,模型的损失函数确实有了显著下降,box_loss 从0.6662开始,经过几轮的迭代,迅速降至0.5左右。但到了中期,loss 的下降速度明显放缓,甚至有停滞不前的趋势。这一现象在val/box_loss的表现上尤其明显,从1.3351缓慢上升至1.4左右,直到训练到64轮时才略有下降。在这一阶段,mAP50从0.6364仅轻微上升到0.64,提升有限。这期间,模型似乎陷入了平台期,进展缓慢,让人感到有些煎熬。从第30轮到第64轮,我频繁查看曲线图,希望能捕捉到一点改善的迹象,但都未能如愿。这个阶段也让我对模型的潜力产生了怀疑,担心训练过程可能需要更长时间才能看到显著改善。 进入后期阶段,尽管损失函数略有下降,但mAP50的提升并不明显,这让人感到非常... 使用场景:毕业设计、课程设计、深度学习入门、工业场景落地验证。 立即下载

转自sunnyxx的博客 


简而言之,Reactive Cocoa(RAC)就是一个函数响应式编程思想在Cocoa下的实现。
 
说说在RAC框架下做了一个项目的赶脚吧:
 
挺新鲜挺有意思,开发人员水平很高,框架封装性和实用性一流,看了看人家对宏的使用发现原来用的纯小儿科,对编译器的控制,block的使用也很值得的学习。
 
编程思想上的一些改变。原创的一个可能也不大恰当的比喻:原来的编程思想像是“走迷宫”,RAC的编程思想是“建迷宫”。意思是,之前的编程思路是命令式,大概都是“程序启动时执行xxxx,在用户点击后的回调函数执行xxx,收到一个Notification后执行xxx”等等,如同走迷宫一样,走出迷宫需要在不同时间段记住不同状态根据不同情况而做出一系列反应,继而走出迷宫;相比下,RAC的思想是建立联系,像钟表中的齿轮组一样,一个扣着一个,从转动发条到指针走动,一个齿轮一个齿轮的传导(Reactive),复杂但完整而自然。如同迷宫的建造者,在设计时早已决定了哪里是通路,哪里是死路或是哪个路口指向了出口,当一个挑战者(Event)走入迷宫时(Signal),他一定会在设置好的迷宫中的某个路线行走(传递),继而走到终点(Completion)或困死在里面(Error)。
 
写出代码结构明显不一样。由于RAC将Cocoa中KVO、UIKit Event、delegate、selector等都增加了RAC支持,所以都不用去做很多跨函数的事,比如KVO个对象然后在回调里面xxx,从storyboard里面连个UIButton的IBAction出来xxx,或是设个UITextField的delegate出来去取输入的文本xxx。但在RAC下就像上面比喻的建迷宫,把这些大都放在“-viewDidLoad:”就可以了,当然像UITableView的delegate和data source这么大规模的代理模式就还是老老实实写吧。
 
举个例子:
 
我就想干这么个事:“一个label一个text field,下面输啥上面显示啥”
 
老写法大概做法是这个vc实现UITextFieldDelegate协议,把这个text field的delegate设到vc上面,然后在要改变text的那个delegate方法里面取当前text field的text值,再赋给label上;
 
使用RAC的话就一句话(当然得把这俩控件都IBOutlet出来):
 
  1. RAC(self.outputLabel, text) = self.inputTextField.rac_textSignal; 
看着就挺爽。复杂的例子先不举了。
 
总之吧,等今后维护RAC的开发者和使用者把更多的Cocoa的东西归入RAC的框架中,这个框架基本上都可以凌驾于Cocoa这个框架了,意思是甚至用不着知道那些delegate啊KVO啊苹果告诉你是咋用的,用RAC封装的就行了。RAC对于值的显示大都是和property“绑定”的关系,像使用storyboard构建页面时,对于有响应的控件基本都得IBOutlet出来作为一个property,而不是像原来一样连个IBAction出来或者连个delegate出来。对于视图层到model层之间的绑定就显得有些生硬了,相当于视图直接耦合了model,于是应运而生M-V-VM结构,说白了就是在View和Model之间增加了一个ViewModel来解耦,这样View里面要做的基本就是绑定VM以及一些纯视图的操作(比如用什么动画效果展示一个数据);VM里面是和View相关的数据部分的储存和操作,比如说一个UITableView的data source,一个对email输入合法性的验证方法,当然还有的是对真正Model层的调用和结果的刷新,由于View已经和VM绑定,这样VM在刷新的时候只刷新自己的属性就得了。
 
其实重要的还是写代码思维方式的变化,如果全工程都使用RAC来实现,对于同一个业务逻辑终于可以在同一块代码里完成了,将UI事件,逻辑处理,文件或数据库操作,异步网络请求,UI结果显示,这一大套统统用函数式编程的思路嵌套起来,进入页面时搭建好这所有的关系,用户点击后妥妥的等着这一套联系一个个的按期望的逻辑和次序触发,最后显示给用户。感觉就像是搭好了一个精致的游乐场,然后不紧不慢地打开大门:@”Come on 熊孩子们!”
 
PS:写这个blog的时候用的RAC版本是2.2.3,现在有更高的版本,所以还会有很多变化。
 
神奇的Macros
 
先说说RAC中必须要知道的宏:
 
  1. RAC(TARGET, [KEYPATH, [NIL_VALUE]]) 
使用:
 
  1. RAC(self.outputLabel, text) = self.inputTextField.rac_textSignal; 
  2.  
  3. RAC(self.outputLabel, text, @"收到nil时就显示我") = self.inputTextField.rac_textSignal; 
这个宏是最常用的,RAC()总是出现在等号左边,等号右边是一个RACSignal,表示的意义是将一个对象的一个属性和一个signal绑定,signal每产生一个value(id类型),都会自动执行:
 
  1. [TARGET setValue:value ?: NIL_VALUE forKeyPath:KEYPATH]; 
数字值会升级为NSNumber *,当setValue:forKeyPath时会自动降级成基本类型(int, float ,BOOL等),所以RAC绑定一个基本类型的值是没有问题的
 
  1. · RACObserve(TARGET, KEYPATH) 
作用是观察TARGET的KEYPATH属性,相当于KVO,产生一个RACSignal
 
最常用的使用,和RAC宏绑定属性:
 
  1. RAC(self.outputLabel, text) = RACObserve(self.model, name); 
上面的代码将label的输出和model的name属性绑定,实现联动,name但凡有变化都会使得label输出
 
  1. @weakify(Obj); 
  2. @strongify(Obj); 
这对宏在 RACEXTScope.h 中定义,RACFramework好像没有默认引入,需要单独import
 
他们的作用主要是在block内部管理对self的引用:
 
  1. @weakify(self); // 定义了一个__weak的self_weak_变量 
  2. [RACObserve(self, name) subscribeNext:^(NSString *name) { 
  3.     @strongify(self); // 局域定义了一个__strong的self指针指向self_weak 
  4.     self.outputLabel.text = name; 
  5. }]; 
这个宏为什么这么吊,前面加@,其实就是一个啥都没干的@autoreleasepool {}前面的那个@,为了显眼罢了。
 
这两个宏一定成对出现,先weak再strong
 
除了RAC中常用宏的使用,有一些宏的实现方法也很值得观摩。
 
举个高级点的栗子:
 
要干的一件事,计算一个可变参数列表的长度。
 
第一反应就是用参数列表的api,va_start va_arg va_end遍历一遍计算个和,但仔细想想,对于可变参数这个事,在编译前其实就已经确定了,代码里括号里有多少个参数一目了然。
 
RAC中Racmetamarcos.h中就有一系列宏来完成这件事,硬是在预处理之后就拿到了可变参数个数:
 
  1. #define metamacro_argcount(...) \ 
  2.     metamacro_at(20, __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1) 
这个宏由几个工具宏一层层展开,现在模拟一下展开过程:
 
假如我们要计算的如下:
 
  1. int count = metamacro_argcount(a, b, c); 
于是乎第一层展开后:
 
  1. int count = metamacro_at(20, a, b, c, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1) 
再看metamacro_at的定义:
 
  1. #define metamacro_at(N, ...) metamacro_concat(metamacro_at, N)(__VA_ARGS__) 
  2. // 下面是metamacro_concat做的事(简写一层) 
  3. #define metamacro_concat_(A, B) A ## B 
于是乎第二层展开后:
 
  1. int count = metamacro_at20(a, b, c, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1); 
再看metamacro_at20这个宏干的事儿:
 
  1. #define metamacro_at20(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, ...) metamacro_head(__VA_ARGS__) 
于是乎第三层展开后,相当于截断了前20个参数,留下剩下几个:
 
  1. int count = metamacro_head(3, 2, 1); 
这个metamacro_head:
 
  1. #define metamacro_head(...) metamacro_head_(__VA_ARGS__, 0) 
  2. #define metamacro_head_(FIRST, ...) FIRST 
后面加个0,然后取参数列表第一个,于是乎:
 
  1. int count = 3; 
大功告成。
 
反正我看完之后感觉挺震惊,宏还能这么用,这样带来的好处不止是将计算在预处理时搞定,不拖延到运行时恶心cpu;但更重要的是编译检查。比如某些可变参数的实现要求可以填2个参数,可以填3个参数,其他的都不行,这样,也只有这样的宏的实现,才能在编译前就确定了错误。
 
除了上面,还有一个神奇的宏的使用:
 
当使用诸如RAC(self, outputLabel)或RACObserve(self, name)时,发现写完逗号之后,输入第二个property的时候会出现完全正确的代码提示!这相当神奇。
探究一下,关键的关键是如下一个宏:
 
  1. #define keypath(...) \ 
  2.     metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__)) 
这个metamacro_argcount上面说过,是计算可变参数个数,所以metamacro_if_eq的作用就是判断参数个数,如果个数是1就执行后面的keypath1,若不是1就执行keypath2。
 
所以重点说一下keypath2:
 
  1. #define keypath2(OBJ, PATH) \ 
  2.     (((void)(NO && ((void)OBJ.PATH, NO)), # PATH)) 
乍一看真挺懵,先化简,由于Objc里面keypath是诸如”outputLabel.text”的字符串,所以这个宏的返回值应该是个字符串,可以简化成:
 
  1. #define keypath2(OBJ, PATH) (???????, # PATH) 
先不管”??????”是啥,这里不得不说C语言中一个不大常见的语法(第一个忽略):
 
  1. int a = 0, b = 0; 
  2. a = 1, b = 2; 
  3. int c = (a, b); 
这些都是逗号表达式的合理用法,第三个最不常用了,c将被b赋值,而a是一个未使用的值,编译器会给出warning。
 
去除warning的方法很简单,强转成void就行了:
 
  1. int c = ((void)a, b); 
再看上面简化的keypath2宏,返回的就是PATH的字符串字面值了(单#号会将传入值转成字面字符串)
 
  1. (((void)(NO && ((void)OBJ.PATH, NO)), # PATH)) 
对传入的第一个参数OBJ和第二个正要输入的PATH做了点操作,这也正是为什么输入第二个参数时编辑器会给出正确的代码提示。强转void就像上面说的去除了warning。
 
但至于为什么加入与NO做&&,我不太能理解,我测试时其实没有时已经完成了功能,可能是作者为了屏蔽某些隐藏的问题吧。
 
这个宏的巧妙的地方就在于使得编译器以为我们要输入“点”出来的属性,保证了输入值的合法性(输了不存在的property直接报错的),同时利用了逗号表达式取逗号最后值的语法返回了正确的keypath。
 
总之
RAC对宏的使用达到了很高的水平,还有诸如RACTuplePack,RACTupleUnpack的宏就不细说了,值得研究。
 
PS:上面介绍的metamacro和@strongify等宏确切来说来自RAC依赖的extobjc,作者是Justin Spahr-Summers,正是RAC作者之一。
 
百变RACStream
 
在RAC下开发干的最多的事就是建立RACSignal和subscribe RACSignal了,它是RAC的核心所在。本篇介绍了RAC的运作原理和设计思路,从函数式编程形成的RACStream继而介绍它的子类 - RAC最核心的部分RACSignal。
 
函数式编程
我们知道Reactive Cocoa是函数式编程(Functional Programing)(FP)思想的实现。FP有一套成熟的理论,这里只讲讲我个人理解吧。
 
我觉得FP就是“像计算函数表达式一样来解决一个问题”,举个栗子,中学题:
 
  1. 已知:f(x) = 2sin(x + π/2), 求 f(π/2)的值。 
其中x是这个函数的输入,f(x)为计算的输出结果,求f(π/2)时给定了x自然能计算出个结果来(说实话我真忘了咋算了)
 
当然,仔细看这个函数,其实是可以分解成几个小函数的:
 
  1. f1(x) = x + π/2 
  2. f2(x) = sin(x) 
  3. f3(x) = 2x 
而原来的f(x)可以被小函数组合:
 
  1. f(x) = f3(f2(f1(x))) 
所以不难得出这么个推论:要是我手上有足够的基本函数,我就能用上面的组合的方法组合出任意一个复杂的函数了。再想想事实上这些年来学数学的过程不就是在一个个积累基本函数的过程嘛,从基本运算,到三角函数,到乘方开方,再到微积分。基本函数越来越多,能解决的数学问题也越来越复杂。
 
再来看一个函数是怎么构成的,FP理论里叫monads,十分抽象,没读懂,但能理解出来:一个函数只要有一个对于输入值的运算方法和一个返回值,就够了。也容易理解,给它一个输入,干点事情,给出一个输出,就行了,当然现实情况要复杂得多(比如说输出值本身就是个函数?)有些函数是有输入的条件的,比如原来数学解个函数时候经常跟个作用域或者限制条件,比如f(x) = 10 / x , (x不为0),要是传个0这个函数就认为计算错误。
 
对于像上面栗子的函数,每个函数都能接收上一个函数输出的结果,作为自己的输入,这样才能嵌套生成最终结果,同时,计算的顺序也是一定从里向外,所以换个写法可以写成:
 
  1. start ---x--> f1(x) --(temp value1)--> f2(temp value1) --(temp value2)--> f3(temp value2) ---> result 
于是乎嵌套就被表示成了序列,来个高大上的名字怎么样,就叫流(Stream)
 
RACStream
这就是RACStream所表示的含义。
 
按照上面说的,其实RACStream的名字有点点歧义,对于一个RACStream对象,它在意义上等同于上面的f1(x),f2(x),f3(x),而不是那一大串整体,表示整体的应该是最外层的和f(x)对应的那个对象,叫个RACStreamComponent比较好?理解时候得注意下。
 
所以作为一个基本函数的RACStream应该至少应该有:
 
1、怎么传入值
2、怎么返回值
3、怎么与其他函数组合
4、怎么实现函数的作用域(监测输入值来做处理)
5、这函数叫啥- -
 
得益于在Objc下实现,所以输入输出的“值”都用个id类型就行了,遇到多个值的组合就用RACTurple(可以把多个值压包和解包,类比WINRAR),1和2解决
 
RACStream从实例变量来看只有一个name,当然它也只应该有个name - -,5解决
 
里面重点问题就是上面的3和4了。由于函数组合之后仍然是个函数,所以也很容易理解两个Stream对象的组合其实就是生成一个新的Stream对象,它返回了分别由两个子Stream先后运算产生的最终结果
 
观摩一下RACStream定义的基本方法:
 
  1. + (instancetype)empty; 
  2. + (instancetype)return:(id)value; 
  3. - (instancetype)bind:(RACStreamBindBlock (^)(void))block; // for 4 
  4. - (instancetype)concat:(RACStream *)stream; // for 3 
  5. - (instancetype)zipWith:(RACStream *)stream; // for 3 
RACStream作为一个描述抽象的父类,这几个基本方法并没有实现,是由具体子类来实现,RACStream的两个子类分别是RACSignal和RACSequence
 
+empty 是一个不返回值,立刻结束(Completed)的函数,意思是执行它之后除了立刻结束啥都不会发生,可以理解为RAC里面的nil。
+return: 是一个直接返回给定值,然后立刻结束的函数,比如 f(x) = 213
-bind:是一个非常重要的函数,在Rac Doc中被描述为‘basic primitives, particularly’,它是RACStream监测“值”和控制“运行状态”的基本方法,个人认为看注释文档不能理解它是干嘛的,而且bind英语“捆绑,绑定,强迫,约束”这几个意思也感觉对不上,我觉得叫“绑架”倒是更贴切一点。在-bind:之后,之前的RACStream就处于被“绑架”的状态,被绑架的RACStream每产生一个值,都要经过“绑架者”来决定:
 
1、是否使这个RACStream结束(被绑架者是否还能继续活着)
 
2、用什么新的RACStream来替换被绑架的RACStream,传出的结果也成了新RACStream产生的值(绑匪可以选择再抓一个人质放之前那个前面)
 
举个具体栗子,RACStream的 - take:方法,这个方法使一个RACStream只取前N次的值(有缩减):
 
  1. - (instancetype)take:(NSUInteger)count { 
  2.     Class class = self.class; 
  3.      
  4.     return [[self bind:^{ // self被绑架 
  5.         __block NSUInteger taken = 0; 
  6.  
  7.         return ^ id (id value, BOOL *stop) { // 这个block在被绑架的self每输出一个值得时候触发 
  8.             RACStream *result = class.empty; 
  9.  
  10.             if (taken < count) result = [class return:value]; // 未达到N次时将原值原原本本的传递出去 
  11.             if (++taken >= count) *stop = YES; // 达到第N次值后干掉了被绑架的self 
  12.  
  13.             return result; // 将被绑架的self替换为result 
  14.         }; 
  15.     }]]; 
  16. } 
-concat: 和 -zipWith: 就是将两个RACStream连接起来的基本方法了:
 
[A concat:B]中A和B像皇上和太子的关系,A是皇上,B是太子。皇上健在的时候统治天下发号施令(value),太子就候着,不发号施令(value),当皇上挂了(completed),太子登基当皇上,此时发出的号令(value)是太子的。
[C zipWith:D]可以比喻成一对平等恩爱的夫妻,两个人是“绑在一起“的关系来组成一个家庭,决定一件事(value)时必须两个人都提出意见(当且仅当C和D同时都产生了值的时候,一个value才被输出,CD只有其中一个有值时会挂起等待另一个的值,所以输出都是一对值(RACTuple)),当夫妻只要一个人先挂了(completed)这个家庭(组合起来的RACStream)就宣布解散(也就是无法凑成一对输出时就终止)
 
除了上面几个基本方法,RACStream还有不少的Operation方法,这些操作方法的实现大都是组合基本的方法来达到特定的目的,虽然是RACStream这个基类实现的,但我觉得还是放在后面介绍RACSignal的时候作为它的使用方法来说比较合适,毕竟绝大多数编程的对象的都是RACStream的两个子类,后面再展开介绍好了。
 
RACSignal的巧克力工厂
 
上面介绍了函数式编程和RACStream,使得函数得以串联起来,而它的具体子类,也是RAC编程中最重要的部分,RACSignal就是使得算式得以逐步运算并使其有意义的关键所在,本节主要介绍RACSignal的机理,具体的使用放到接下来的几节。
 
巧克力工厂的运作模式
RACStream实现了一个嵌套函数的结构,如f(x) = f1(f2(f3(x))),但好像是考试卷子上的一道题,没有人去做它,没得出个结果的话这道题是没有意义的。
 
OK,现在起将这个事儿都比喻成一个巧克力工厂,f(x)的结果是一块巧克力,f1,f2,f3代表巧克力生产的几个步骤,如果这个工厂不开工,它是没有意义的。
 
再说RACSignal,引用RAC doc的描述: “A signal, represented by the RACSignal class, is a push-driven stream.”
 
我觉得这个push-driven要想解释清楚,需要和RACSequence的pull-driven放在一起来看。在巧克力工厂,push-driven是“生产一个吃一个”,而pull-driven是“吃完一个才生产下一个”,对于工厂来说前者是主动模式:生产了巧克力就“push”给各个供销商,后者是被动模式:各个供销商过来“pull”产品时才给你现做巧克力。
 
Status
 
所以,对于RACSigna的push-driven的生产模式,首先,当工厂发现没有供销商签合同准备要巧克力的时候,工厂当然没有必要开动生产;只要当有一个以上准备收货的经销商时,工厂才开动生产。这就是RACSignal的休眠(cold)和激活(hot)状态,也就是所谓的冷信号和热信号。一般情况下,一个RACSignal创建之后都处于cold状态,有人去subscribe才被激活。
 
Event
RACSignal能产生且只能产生三种事件:next、completed,error。
 
next表示这个Signal产生了一个值(成功生产了一块巧克力)
 
completed表示Signal结束,结束信号只标志成功结束,不带值(一个批次的订单完成了)
 
error表示Signal中出现错误,立刻结束(一个机器坏了,生产线立刻停止运转)
 
工厂厂长存了所有供销商的QQ,每当发生上面三件事情的一件时,都用QQ挨个儿发消息告诉他们,于是供销商就能根据生产状态决定要做点什么。当订单完成或者失败后,厂长就会把这个供销商的QQ删了,以后发消息的时候也就没必要通知他了。
 
Side Effects
RACSignal在被subscribe的时候可能会产生副作用,先举个官方的栗子:
 
  1. __block int aNumber = 0; 
  2.  
  3. // Signal that will have the side effect of incrementing `aNumber` block 
  4. // variable for each subscription before sending it. 
  5. RACSignal *aSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) { 
  6.     aNumber++; 
  7.     [subscriber sendNext:@(aNumber)]; 
  8.     [subscriber sendCompleted]; 
  9.     return nil; 
  10. }]; 
  11.  
  12. // This will print "subscriber one: 1" 
  13. [aSignal subscribeNext:^(id x) { 
  14.     NSLog(@"subscriber one: %@", x); 
  15. }]; 
  16.  
  17. // This will print "subscriber two: 2" 
  18. [aSignal subscribeNext:^(id x) { 
  19.     NSLog(@"subscriber two: %@", x); 
  20. }]; 
上面的signal在作用域外部引用了一个int变量,同时在signal的运算过程中作为next事件的值返回,这就造成了所谓的副作用,因为第二个订阅者的订阅而影响了输出值。
 
 
我的理解来看,这个事儿做的就不太地道,一个正经的函数式编程中的函数是不应该因为进行了运算而导致后面运算的值不统一的。但对于实际应用的情况来看也到无可厚非,比如用户点击了“登录”按钮,编程时把登录这个业务写为一个login的RACSignal,当然,第一次调用登录和再点一次第二次调用登录的结果肯定不一样了。所以说RAC式编程减少了大部分对临时状态值的定义,但不是全部哦。
 
怎么办呢?我觉得最好的办法就是“约定”,RAC design guide里面介绍了对于一个signal的命名法则:“Hot signals without side effects 最好使用property,如“textChanged”,不太理解什么情况用到这个,权当做一个静态的属性来看就行。
Cold signals without side effects 使用名词类型的方法名,如“-currentText”,“currentModels”,同时表明了返回值是个啥(这个尤其得注意,RACSignal的next值是id类型,所以全得是靠约定才知道具体返回类型)
Signals with side effects 这种就是像login一样有副作用的了,推荐使用动词类型的方法名,用对动词基本就能知道是不是有副作用了,比如“-loginSignal”和“-saveToFile”大概就知道前面一个很可能有副作用,后面一个多存几次文件应该没副作用。”
 
当然,也可以multicast一个event,使得某些特殊的情况来共享一个副作用,后面再具体讲,先一个官方的简单的栗子:
 
  1. // This signal starts a new request on each subscription. 
  2. RACSignal *networkRequest = [RACSignal createSignal:^(id<RACSubscriber> subscriber) { 
  3.     AFHTTPRequestOperation *operation = [client 
  4.         HTTPRequestOperationWithRequest:request 
  5.         success:^(AFHTTPRequestOperation *operation, id response) { 
  6.             [subscriber sendNext:response]; 
  7.             [subscriber sendCompleted]; 
  8.         } 
  9.         failure:^(AFHTTPRequestOperation *operation, NSError *error) { 
  10.             [subscriber sendError:error]; 
  11.         }]; 
  12.  
  13.     [client enqueueHTTPRequestOperation:operation]; 
  14.     return [RACDisposable disposableWithBlock:^{ 
  15.         [operation cancel]; 
  16.     }]; 
  17. }]; 
  18.  
  19. // Starts a single request, no matter how many subscriptions `connection.signal` 
  20. // gets. This is equivalent to the -replay operator, or similar to 
  21. // +startEagerlyWithScheduler:block:. 
  22. RACMulticastConnection *connection = [networkRequest multicast:[RACReplaySubject subject]]; 
  23. [connection connect]; 
  24.  
  25. [connection.signal subscribeNext:^(id response) { 
  26.     NSLog(@"subscriber one: %@", response); 
  27. }]; 
  28.  
  29. [connection.signal subscribeNext:^(id response) { 
  30.     NSLog(@"subscriber two: %@", response); 
  31. }]; 
当地一个订阅者subscribeNext的时候触发了AFNetworkingOperation的创建和执行,开始网络请求,此时又来了个订阅者订阅这个Signal,按理说这个网络请求会被“副作用”,重新发一遍,但做了上面的处理之后,这两个订阅者接收到了同样的一个请求的内容。
 
RACScheduler - 生产线
RACScheduler是RAC里面对线程的简单封装,事件可以在指定的scheduler上分发和执行,不特殊指定的话,事件的分发和执行都在一个默认的后台线程里面做,大多数情况也就不用动了,有一些特殊的signal必须在主线程调用,使用-deliverOn:可以切换调用的线程。
 
但值得特殊了解的事实是:However, RAC guarantees that no two signal events will ever arrive concurrently. While an event is being processed, no other events will be delivered. The senders of any other events will be forced to wait until the current event has been handled.
 
意思是订阅者执行时的block一定非并发执行,也就是说不会执行到一半被另一个线程进入,也意味着写subscribeXXX block的时候没必要做加锁处理了。
 
巧克力的生产工艺
RACSignal的厂子建好了,运行的模式也都想好了,剩下的就是巧克力的加工工艺了。
 
有了RACStream的嵌套和组装的基础,RACSignal得以使用组件化的工艺来一步步的加工巧克力,从可可,牛奶,糖等原料,混合到这种巧克力适用的液态巧克力,过滤,提纯,冷却,夹心,压模,再到包装,一个巧克力就产出了。对于不同种类的巧克力,比如酒心巧克力,也不过是把其中的某个组件替换成注入酒心罢了。
 
RACSignal的生产组件,也就是它的各式各样的operation,一个具体业务逻辑的实现,其实也就是选择合适operation按合适的顺序组合起来。
 
还举那个用户在textFiled输入并显示到上面的label中的栗子:
 
  1. RAC(self.outputLabel, text) = self.inputTextField.rac_textSignal; 
现在需求变成“用户输入3个字母以上才输出到label,当不足3个时显示提示”,OK,好办:
 
  1. RAC(self.outputLabel, text) = [[self.inputTextField.rac_textSignal 
  2.     startWith:@"key is >3"] /* startWith 一开始返回的初始值 */ 
  3.     filter:^BOOL(NSString *value) { 
  4.         return value.length > 3; /* filter使满足条件的值才能传出 */ 
  5. }]; 
需求又增加成“当输入sunny时显示输入正确”
 
  1. RAC(self.outputLabel, text) = [[self.inputTextField.rac_textSignal 
  2.     startWith:@"key is >3"] // startWith 一开始返回的初始值 
  3.     filter:^BOOL(NSString *value) { // filter使满足条件的值才能传出 
  4.         return value.length > 3; 
  5.     }] 
  6.     map:(NSString *value) { // map将一个值转化为另一个值输出 
  7.         return [value isEqualToString:@"sunny"] ? @"bingo!" : value; 
  8.     }]; 
可以看出,基本上一个业务逻辑经过分析后可以拆解成一个个小RACSignal的组合,也就像生产巧克力的一道道工艺了。上面的栗子慢慢感觉就像了一个简陋的输答案的框了。
 
只取所需的Filters
 
RAC中的Filters
 
画个范围
一个Signal源可以产生一系列next值,但并非所有值都是需要的,具体的Subscriber可以选择在原有Signal上套用Filter操作来过滤掉不需要的值。
我的定义:RAC中如果一个Operation将处理后的值集合是处理前值集合的子集,我们就可以把它归为Filter类型。
 
当然通过之前介绍的基础操作完全可以自己拼出个想要的filter来,RAC为了方便使用已经实现了几个常用的filter,经过总结,这些filter大概可以分成两类:next值过滤类型和起止点过滤类型
 
值过滤类型Filters
 
- filter: (BOOL (^)(id value))
RAC中的filter同名方法- filter:(BOOL (^)(id value)),简单明了,将一个value用block做test,返回YES的才会通过,它的内部实现使用了- flattenMap:,将原来的Signal经过过滤转化成只返回过滤值的Signal,用法也不难理解:
 
  1. [[self.inputTextField.rac_textSignal filter:^BOOL(NSString *value) { 
  2.     return [value hasPrefix:@"sunny"]; 
  3. }] subscribeNext:^(NSString *value) { 
  4.     NSLog(@"This value has prefix `sunny` : %@", value); 
  5. }]; 
此外,还有几个这个方法的衍生方法:
 
- ignore: (id)
忽略给定的值,注意,这里忽略的既可以是地址相同的对象,也可以是- isEqual:结果相同的值,也就是说自己写的Model对象可以通过重写- isEqual:方法来使- ignore:生效。常用的值的判断没有问题,如下:
 
  1. [[self.inputTextField.rac_textSignal ignore:@"sunny"] subscribeNext:^(NSString *value) { 
  2.     NSLog(@"`sunny` could never appear : %@", value); 
  3. }]; 
 
- ignoreValues
这个比较极端,忽略所有值,只关心Signal结束,也就是只取Comletion和Error两个消息,中间所有值都丢弃。
注意,这个操作应该出现在Signal有终止条件的的情况下,如rac_textSignal这样除dealloc外没有终止条件的Signal上就不太可能用到。
 
- distinctUntilChanged
也是一个相当常用的Filter(但它不是- filter:的衍生方法),它将这一次的值与上一次做比较,当相同时(也包括- isEqual:)被忽略掉。
比如UI上一个Label绑定了一个值,根据值更新显示的内容:
 
  1. RAC(self.label, text) = [RACObserve(self.user, username) distinctUntilChanged]; 
  2. self.user.username = @"sunnyxx"; // 1st 
  3. self.user.username = @"sunnyxx"; // 2nd 
  4. self.user.username = @"sunnyxx"; // 3rd 
如果不增加distinctUntilChanged的话对于连续的相同的输入值就会有不必要的处理,这个栗子只是简单的UI刷新,但遇到如写数据库,发网络请求的情况时,代价就不能购忽略了。
 
所以,对于相同值可以忽略的情况,果断加上它吧。
 
起止点过滤类型
 
除了被动的当next值来的时候做判断,也可以主动的提前选择开始和结束条件,分为两种类型:
take型(取)和skip型(跳)
 
- take: (NSUInteger)
从开始一共取N次的next值,不包括Competion和Error,如:
 
  1. [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { 
  2.     [subscriber sendNext:@"1"]; 
  3.     [subscriber sendNext:@"2"]; 
  4.     [subscriber sendNext:@"3"]; 
  5.     [subscriber sendCompleted]; 
  6.     return nil; 
  7. }] take:2] subscribeNext:^(id x) { 
  8.     NSLog(@"only 1 and 2 will be print: %@", x); 
  9. }]; 
 
- takeLast: (NSUInteger)
取最后N次的next值,注意,由于一开始不能知道这个Signal将有多少个next值,所以RAC实现它的方法是将所有next值都存起来,然后原Signal完成时再将后N个依次发送给接收者,但Error发生时依然是立刻发送的。
 
- takeUntil:(RACSignal *)
当给定的signal完成前一直取值。最简单的栗子就是UITextField的rac_textSignal的实现(删减版本):
 
  1. - (RACSignal *)rac_textSignal { 
  2.     @weakify(self); 
  3.     return [[[[[RACSignal 
  4.         concat:[self rac_signalForControlEvents:UIControlEventEditingChanged]] 
  5.         map:^(UITextField *x) { 
  6.             return x.text; 
  7.         }] 
  8.         takeUntil:self.rac_willDeallocSignal] // bingo! 
  9. } 
也就是这个Signal一直到textField执行dealloc时才停止
 
- takeUntilBlock:(BOOL (^)(id x))
对于每个next值,运行block,当block返回YES时停止取值,如:
 
  1. [[self.inputTextField.rac_textSignal takeUntilBlock:^BOOL(NSString *value) { 
  2.     return [value isEqualToString:@"stop"]; 
  3. }] subscribeNext:^(NSString *value) { 
  4.     NSLog(@"current value is not `stop`: %@", value); 
  5. }]; 
 
- takeWhileBlock:(BOOL (^)(id x))
上面的反向逻辑,对于每个next值,block返回 YES时才取值
 
- skip:(NSUInteger)
从开始跳过N次的next值,简单的栗子:
 
  1. [[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { 
  2.     [subscriber sendNext:@"1"]; 
  3.     [subscriber sendNext:@"2"]; 
  4.     [subscriber sendNext:@"3"]; 
  5.     [subscriber sendCompleted]; 
  6.     return nil; 
  7. }] skip:1] subscribeNext:^(id x) { 
  8.     NSLog(@"only 2 and 3 will be print: %@", x); 
  9. }]; 
 
- skipUntilBlock:(BOOL (^)(id x))
和- takeUntilBlock:同理,一直跳,直到block为YES
 
- skipWhileBlock:(BOOL (^)(id x))
和- takeWhileBlock:同理,一直跳,直到block为NO
 
总结
 
本章介绍了RAC中Filter类型的Operation,总结一下:
 
适用场景:需要一个next值集合的子集时
Filter类型:值过滤型和起止点过滤型
值过滤型常用方法: -filter:,-ignore:,-distinctUnitlChanged
起止点过滤型常用方法:take系列和skip系列
 
References
 

状态估计【卡尔曼滤波kalman】UKF、EKF、粒子滤波PF学习与研究(Matlab代码实现) 内容概要:本文围绕非线性状态估计中的三种经典滤波算法——无迹卡尔曼滤波(UKF)、扩展卡尔曼滤波(EKF)和粒子滤波(PF)展开系统性学习与研究,重点在于通过Matlab编程实现这些算法的核心流程,并将其应用于实际系统的状态估计问题中。文档不仅详细阐述了各算法的数学原理、适用条件与性能差异,还提供了完整的仿真代码与案例分析,帮助读者深入理解其在非线性、非高斯环境下的表现特性。同时,资源拓展至EKF与神经网络融合训练、多源数据融合等高级应用场景,展现了其在智能控制、无人机导航、电力系统状态估计、目标跟踪等领域的广泛应用价值。; 适合人群:具备一定信号处理、控制理论或状态估计基础的研究生、科研人员及工程技术人员,尤其适用于从事自动化、机器人、导航、电力电子与智能系统等相关方向的研究与开发工作者;; 使用场景及目标:① 掌握UKF、EKF与PF在非线性系统中的建模思路与Matlab实现方法,理解其优缺点与适用边界;② 提升科研仿真能力,能够独立构建状态估计模型并进行算法对比验证;③ 将所学方法应用于如电池SOC估算、无人机姿态估计、传感器融合、故障诊断等实际工程问题中;; 阅读建议:建议结合Matlab动手实践,逐行解读并调试所提供的代码,深入理解算法实现细节;同时可延伸学习文中提及的数据融合、神经网络与滤波器结合等进阶主题,以构建完整的状态估计技术知识体系。 立即下载

相关推荐

Plecs全桥LLC谐振变换器仿真模型

内容概要:本文档系统性地整合了电力电子与控制系统仿真领域的丰富资源,聚焦于基于Plecs和Simulink平台的高频软开关变换器建模与仿真,涵盖全桥LLC、双向CLLC、DAB、PFC、Vienna整流器、Buck-Boost等多种典型拓扑结构。资源内容不仅包括闭环控制(如变频控制、移相混合控制、双闭环、恒压恒流双竞争控制)的仿真模型,还配套提供详细的参数设计报告、Mathcad计算书、PPT说明文档及参考文献,构建了从理论建模到工程实现的完整技术链条。此外,文档拓展至状态估计(如卡尔曼滤波、EKF、UKF、PF)与智能算法(如BP神经网络、模型预测控制)在电力系统中的融合应用,为先进控制策略的研究与复现提供了坚实支撑。; 适合人群:面向具备电力电子、自动控制或电气工程等相关专业背景,熟练掌握MATLAB/Simulink/Plecs等仿真工具,从事高频开关电源、新能源变换系统、储能系统或车载电源研发的研究生、高校科研人员及企业工程技术人员。; 使用场景及目标:① 学习并掌握全桥LLC等谐振变换器的核心工作原理与闭环控制系统设计方法,特别是变频与移相控制策略的实现细节;② 利用提供的仿真模型与Mathcad计算工具,快速完成实际项目中的参数设计、性能验证与优化迭代;③ 结合状态估计算法与神经网络等智能方法,开展电力电子系统状态观测、故障诊断或控制策略创新等前沿科研课题的复现与探索。; 阅读建议:建议严格按照资源目录结构循序渐进地查阅,优先下载并运行核心仿真模型,结合对应的设计报告与计算书深入理解其建模逻辑与参数推导过程,重点关注闭环控制架构、软启动策略及中点电位平衡等关键技术环节,并可进一步将状态估计与智能算法模块融入现有模型,探索其在系统优化中的应用潜力。

ReactiveCocoa源码解析

(一)神奇的Macros  先说说RAC中必须要知道的宏 RAC(TARGET, [KEYPATH, [NIL_VALUE]])    使用: RAC(self.outputLabel, text) = self.inputTextField.rac_textSignal; RAC(self.outputLabel, text, @"收到nil时就显示我") = self.i

Secret Island 1144

ROS 2 + Astra Pro深度相机实战:5分钟搞定远程可视化与距离检测(附避坑指南)

本文提供了一份详尽的ROS 2与Astra Pro深度相机集成实战指南。通过5分钟快速部署,实现远程可视化与实时目标距离检测。文章涵盖驱动安装、SSH+X11远程调试、核心测距节点编写,并附有详细的避坑指南,帮助开发者高效构建机器人视觉感知系统。

weixin_29232507的博客 278

iOS中的预编译指令的初步探究

目录   文件包含    #include    #include_next    #import   宏定义    #define     #undef   条件编译    #if #else #endif    #if define #ifdef #ifndef #elif   错误、警告处理    #error    #warning   编译器控制    #pragma ...

weixin_30340819的博客 529

全志Longan和Tina系统T113-S3-(Tina Linux 系统介绍)

Tina Linux是全志科技基于Linux内核开发的针对智能硬件类产品的嵌入式软件系统。Tina Linux基于openwrt-14.07 版本的软件开发包,包含了 Linux 系统开发用到的内核源码、驱动、工具、系统中间件与应用程序包。Tina系统软件架构如图所示。从下至上分别为Kernel && Driver、Libraries、System Services、Applications 四层。

szembed的博客 6332

XC7Z010-1CLG400C开发板ADC引脚配置全攻略(附完整引脚列表)

本文详细解析了XC7Z010-1CLG400C开发板的ADC引脚配置方法,涵盖XADC模块基础、CLG400封装的引脚布局、硬件设计规范及Vivado工程配置实战。特别提供了完整的引脚列表和性能优化技巧,帮助开发者高效实现模拟信号采集,适用于Zynq-7000系列开发。

weixin_29188043的博客 466

Camera tuning flow相机调试流程

相机调试流程分为三大模块:ProcessRaw处理原始数据(黑电平校正、阴影校正),3A实现自动控制(曝光/对焦/白平衡),ISP进行图像精加工(伽马校正、锐化降噪、色彩处理)。三个阶段环环相扣,前阶段为后阶段提供基础,最终输出画质优良且风格化的图像。

weixin_64706696的博客 1229

宏定义的黑魔法 - 宏菜鸟起飞手册

转自OneV's Den的博客 宏定义在C系开发中可以说占有举足轻重的作用。底层框架自不必说,为了编译优化和方便,以及跨平台能力,宏被大量使用,可以说底层开发离开define将寸步难行。而在更高层级进行开发时,我们会将更多的重心放在业务逻辑上,似乎对宏的使用和依赖并不多。但是使用宏定义的好处是不言自明的,在节省工作量的同时,代码可读性大大增加。如果想成为一个能写出漂亮优雅

oChunCui1的专栏 2182

在Redroid容器中构建Frida RPC服务:自动化生成淘宝X-Sign签名参数

本文详细介绍了在Redroid容器中构建Frida RPC服务,实现自动化生成淘宝X-Sign等签名参数的完整方案。通过逆向分析定位关键方法,定制Redroid系统使Frida-Server常驻运行,并编写Frida脚本将签名方法暴露为RPC接口,最终封装为可远程调用的微服务,解决了签名生成的可移植性与工程化难题。

ice55的博客 643

改进后的yolov8导出进行部署到rk3588上时,导出为onnx格式报错aten::fft_fft2算子不支持导出...如何解决?

🏆本文收录于《全栈Bug调优(实战版)》专栏,主要记录项目实战过程中所遇到的Bug或因后果及提供真实有效的解决方案,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!! 备注:部分问题/疑难杂症搜集于互联网。

**My Coding Family** 1797

具有提示功能的KeyPath宏

宏的定义: #define KeyPath(obj, keyPath) @(((void)obj.keyPath, #keyPath)) 示例代码: #import "ViewController.h" #define KeyPath(obj, keyPath) @(((void)obj.keyPath, #keyPath)) @implementatio...

a3031615的博客 278

【温室气体数据集】全球总碳柱观测网络 TCCON

【温室气体数据集】全球总碳柱观测网络 TCCON

WW、forever的博客 437

Reactive Cocoa Tutorial = 只取所需的Filters

原文链接:http://www.cocoachina.com/industry/20140630/8985.html 概览   简而言之,Reactive Cocoa(RAC)就是一个函数响应式编程思想在Cocoa下的实现。   说说在RAC框架下做了一个项目的赶脚吧:   挺新鲜挺有意思,开发人员水平很高,框架封装性和实用性一流,看了看人家对

yuping2901的专栏 405

使用安卓的IMU数据进行 行人航位推算(PDR)推算步长 附python 代码

加速度计数据、陀螺仪数据、重力传感器数据、姿态仰角数据。数据分析与定位轨迹生成主要使用Python离线完成。示意图。

三维点云技术探索 1913

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

Reactive Cocoa Tutorial [4] = 只取所需的Filters

本文原文发表自我的【自建博客】,cnblogs同步发表,格式未经调整,内容以原博客为准 注:由于拖延症和最近因项目暂缓了RAC的研究,Reactive Cocoa Tutorial系列文章更新很慢,最近醒悟,开始慢慢补上剩下的部分,请见谅。 我是前言 这是Reactive Cocoa Tutorial系列其中的一篇,上一篇简单介绍了RAC中最重要的RACSignal,下面几篇文章...

weixin_30779691的博客 120

efm32 入门.pdf

本PDF共计22页,介绍的是操作驱动API,下载与否自己斟酌。

Reactive Cocoa Tutorial [0] = "Overview";

  关于这系列(如果真能写下去的话):说是教程有点狂,边学边总结,更像个笔记吧,等完全用透之后再写就会忘了一开始学习过程中遇到的问题了,Reactive Cocoa(RAC)现在资料真心少,中文英文加起来没几篇,还都是转来转去的。这是个好东西,相信以后用的人会变多,转了请留该文原地址哦~ by sunny   废话少说 --> RAC   是什么?怎么来的?干啥用的? 怎么用的? 可以...

weixin_30376453的博客 128

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

Reactive Cocoa Tutorial [2] = "百变RACStream";

Overview   在RAC下开发干的最多的事就是建立RACSignal和subscribe RACSignal了,它是RAC的核心所在。本篇介绍了RAC的运作原理和设计思路,从函数式编程形成的RACStream继而介绍它的子类 - RAC最核心的部分RACSignal。 · 函数式编程   我们知道Reactive Cocoa是函数式编程(Functional Programin

WO的专栏 549

[转] Reactive Cocoa Tutorial [1] = "神奇的Macros";

  写在前面:     详细解释了RAC中几个狠NX的宏的设计。表示很很很心动!! 特转载学习之。  感谢 sunny 的分析和讲解。  Reactive Cocoa Tutorial系列,转载请注明该文源地址-- by sunny   先说说RAC中必须要知道的宏:   ·RAC(TARGET, [KEYPATH, [NIL_VALUE]]) RAC(s...

weixin_33877885的博客 96

进击的UI-------------------RAC

http://nshipster.cn/reactivecocoa/ RAC 具有函数式编程和响应式编程的特性。它主要吸取了 .Net 的 Reactive Extensions 的设计和实现。 一些学习资源 博客 & 教程 http://spin.atomicobject.com/2014/02/03/objective-c-delegate-pattern/ ...

dongchuangzhi1998的博客 108

c++ 设计模式1:单件模式

单件模式应该是所有设计模式中最简单的一个了,但是如果使用不当会出现很多问题,下面我们就分析一下单件模式 作用:保证一个class只有一个实体(instance),并为它提供一个全局唯一的访问点  适用性: 1、对于一个类(对象),如果它比较大(占用的系统资源非常多),而且这些资源可以被全局共享,则可以设计为singleton模式。 2、对于一个类,需要对实例进行计数。可以在In

Bryan_Zhang的专栏 315

分巧克力c语言算法设计思路,[蓝桥杯][2017年第八届真题]分巧克力 (C++代码)...

解题思路:【思路一】枚举每个小巧克力的边长,并把该边长下共有几个小巧克力映射到该边长,用数组实现。然后二分查找小巧克力的个数,再根据映射得到边长。 !超时!枚举每个边长并计算该边长下小巧克力的数目,数据太大了。男票提供了思路二!【思路二】二分查找符合条件的边长,条件用ok()函数来判断。注意事项:参考代码:【思路一代码】#include#include#includeusingnam...

weixin_39761573的博客 778

reactiveCocoa实践四(过滤)

一 满足条件才能拿到值 - (void)filter {     // 只有当我们文本框的内容长度大于5,才想要获取文本框的内容     [[_textField.rac_textSignal filter:^BOOL(id value) {         // value:源信号的内容         return  [value length] > 5;

qq_30997845的博客 401

iOS开发笔记之六十九——FRP与RAC介绍(二)

******阅读完此文,大概需要20分钟****** 本篇章主要以介绍RAC的常见操作、常见使用场景、双向绑定、RACCommd等相关内容。 一、RACSignal信号的过滤、筛选、合并等常见操作 示例代码如下: - (void)coldSignalTest { //信号的创建 RACSignal *signal = [RACSignal createSignal:^RA...

iOS开发笔记 1万+
上一篇: iOS 后台运行NSTimer
下一篇: NSdictionary filter Null
ios_developer
博客等级 码龄14年 1粉丝 4原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值