【Kotlin】内联函数
文章目录
- 内联函数
- noinline: 避免参数被内联
- 非局部返回
- 使用标签实现Lambda非局部返回
- 为什么要设计noinline
- crossinline
- 具体化参数类型
Kotlin中的内联函数之所以被设计出来,主要是为了优化Kotlin支持Lambda表达式之后所带来的开销。然而,在Java中我们似乎并不需要特别关注这个问题,因为在Java 7之后,JVM引入了一种叫做
invokedynamic的技术,它会自动帮助我们做Lambda优化。但是为什么Kotlin要引入内联函数这种手动的语法呢? 这主要还是因为Kotlin要兼容Java 6。
在Kotlin中每声明一个Lambda表达式,就会在字节码中产生一个匿名类(也就是说我们一直使用的Lambda表达式在底层被转换成了匿名类的实现方式)。该匿名类包含了一个
invoke方法,作为Lambda的调用方法,每次调用的时候,还会创建一个新的匿名类对象。可想而知,Lambda语法虽然简洁,但是额外增加的开销也不少。并且,如果Lambda捕捉了某个变量,那么每次调用的时候都会创建一个新的对象,这样导致效率较低。尤其对Kotlin这门语言来说,它当今优先要实现的目标,就是在Android这个平台上提供良好的语言特性支持。Kotlin要在Android中引入Lambda语法,必须采用某种方法来优化Lambda带来的额外开销,也就是内联函数。
内联函数
Kotlin拥抱了内联函数,在C++、C#等语言中也支持这种特性。简单的来说,我们可以用inline关键字来修饰函数,这些函数就称为了内联函数。他们的函数体在编译期被嵌入每一个被调用的地方,以减少额外生成的匿名类数,以及函数执行的时间开销。所以内联函数的工作原理并不复杂,就是Kotlin编译器会将内敛函数中的代码在编译的时候自动替换到调用它的地方,这样也就不存在运行时的开销了。 。
看看Kotlin的内联函数是具体如何操作的:
fun main(args: Array<String>) {foo {println("dive into Kotlin...")}
}fun foo(block: () -> Unit) {println("before block")block()println("end block")
}
首先,我们声明了一个高阶函数foo,可以接受一个类型为() -> Unit的Lambda,然后在main函数中调用它。以下是通过字节码反编译的相关Java代码:
public static final void main(@NotNull String[] args) {Intrinsics.checkParameterIsNotNull(args, "args");foo((Function0)null.INSTANCE);
}public static final void foo(@NotNull Function0 block) {Intrinsics.checkParameterIsNotNull(block, "block");String var1 = "before block";System.out.println(var1);block.invoke();var1 = "end block";System.out.println(var1);
}
据我们所知,调用foo就会产生一个Function()类型的block类,然后通过invovke方法来执行,这会增加额外的生成类和调用开销。现在,我们给foo函数加上inline修饰符,如下:
inline fun foo(block: () -> Unit) {println("before block")block()println("end block")
}
再来看看相应的Java代码:
public static final void main(@NotNull String[] args) {Intrinsics.checkParameterIsNotNull(args, "args");String va1 = "before block";System.out.println(var1);// block函数体在这里开始粘贴String var2 = "dive into Kotlin...";System.out.println(var2);// block函数体在这里结束粘贴var1 = "end block";System.out.println(var1);
}public static final void foo(@NotNull Function0 block) {Intrinsics.checkParameterIsNotNull(block, "block");String var2 = "before block";System.out.println(var2);block.invoke();var2 = "end block";System.out.println(var2);
}
foo函数体代码及被调用的Lambda代码都粘贴到了相应调用的位置。试想下,如果这是一个工程中公共的方法,或者被嵌套在一个循环调用的逻辑体中,这个方法势必会被调用很多次。通过inline的语法,我们可以彻底消除这种额外调用,从而节省了开销。
内联函数典型的一个应用场景就是Kotlin的集合类。如果你看过Kotlin的集合类API文档或者源码实现就会发现,集合函数式API,如map、filter都被定义成内联函数,如:
inline fun <T, R> Array<out T>.map {transform: (T) -> R
}: List<R>inline fun <T> Array<out T>.filter {predicate: (T) -> Boolean
}: List<T>
这个很容易理解,由于这些方法都接收Lambda作为参数,同时都需要对集合元素进行遍历操作,所以把相应的实现进行内联无疑是非常适合的。
但是内联函数不是万能的,以下情况我们应避免使用内联函数:
- 由于JVM对普通的函数已经能够根据实际情况智能地判断是否进行内联优化,所以我们并不需要对其使用Kotlin的inline语法,那只会让字节码变得更加复杂。
- 尽量避免对具有大量函数体的函数进行内联,这样会导致过多的字节码数量。
- 一旦一个函数被定义为内联函数,便不能获取闭包类的私有成员,除非你把他们声明为internal。
noinline: 避免参数被内联
通过上面的例子我们已经知道,如果在一个函数的开头加上inline修饰符,那么它的函数体及Lambda参数都会被内联。然而现实中的情况比较复杂,有一种可能是函数需要接受多个参数,但我们只想对其中部分Lambda参数内联,其他的则不内联,这个又该如何处理?
解决这个问题也很简单,Kotlin在引入inline的同时,也新增了noinline关键字,我们可以把它加在不想要被内联的参数开头,该参数便不会具有内联的效果:
fun main(args: Array<String>) {foo ( {println("I am inlined...") }, {println("I am not inlined...")})
}inline fun foo(block1: () -> Unit, noinline block2: () -> Unit) {println("before block")block1()block2()println("end block")
}
同样的方法,再来看看反编译的Java版本:
public static final void main(@NotNull String[] args) {Intrinsics.checkParameterIsNotNull(args, "args");Function0 block2$iv = (Function0)null.INSTANCE;String var2 = "before block";System.out.println(var2);// block1 被内联了String var3 = "I am inlined...";System.out.println(var3);// block2 还是原样block2$iv.invoke();System.out.println(var2);
}
public static final void foo(@NotNull Function0 block1, @NotNull Function0 block2) {Intrinsics.checkParameterIsNotNull(block1, "block1");Intrinsics.checkParameterIsNotNull(block2, "block2");String var3 = "before block";System.out.println(var3);block1.invoke();block2.invoke();var3 = "end block";System.out.println(var3);
}
可以看出,foo函数的block2参数在带上noinline之后,反编译后的Java代码中并没有将其函数体代码在调用处进行替换。
非局部返回
Kotlin中的内联函数除了优化Lambda开销之外,还带来了其他方面的特效,典型的就是非局部返回和具体化参数类型。我们先来看下Kotlin如何支持非局部返回。
以下是我们常见的局部返回的例子:
fun main(args: Array<String>) {foo()
}
fun localReturn() {return
}
fun foo() {println("before local return")localReturn()println("after local return")return
}
// 运行结果
before local return
after local return
正如我们所熟知的,localReturn执行后,其函数体中的return只会在该函数的局部生效,所以localReturn()之后的println函数依旧生效。我们再把这个函数换成Lambda表达式的版本:
fun main(args: Array<String>) {foo { return }
}
fun foo(returning: () -> Unit) {println("before local return")returning()println("after local return")return
}
// 运行结果
Error:(2, 11)Kotlin: 'return' is not allowed here
这时,编译器报错了,就是说在Kotlin中,正常情况下Lambda表达式不允许存在return关键字。这时候,内联函数又可以排上用场了。我们把foo进行内联后再试试看:
fun main(args: Array<String>) {foo { return }
}
inline fun foo(returning: () -> Unit) {println("before local return")returning()println("after local return")return
}
// 运行结果
before local return
编译顺利通过了,但结果与我们的局部返回效果不同,Lambda的return执行后直接让foo函数退出了执行。如果你仔细考虑一下,可能很快就想出了原因。因为内联函数foo的函数体及参数Lambda会直接替代具体的调用。所以实际产生的代码中,retrurn相当于是直接暴露在main函数中,所以returning()之后的代码自然不会执行,这个就是所谓的非局部返回。
使用标签实现Lambda非局部返回
另外一种等效的方式,是通过标签利用@符号来实现Lambda非局部返回。同样以上的例子,我们可以在不声明inline修饰符的情况下,这么做来实现相同的效果:
fun main(args: Array<String>) {foo { return@foo }
}
fun foo(returning: () -> Unit) {println("before local return")returning()println("after local return")return
}
// 运行结果
before local return
非局部返回尤其在循环控制中显得特别有用,比如Kotlin的forEach接口,它接收的就是一个Lambda参数,由于它也是一个内联函数,所以我们可以直接在它调用的Lambda中执行return退出上一层的程序。
fun hasZeros(list: List<Int>): Boolean {list.forEach {if (it == 0) return true // 直接返回foo函数结果}return false
}
为什么要设计noinline
这里我已经蒙了,前面已经说了内联函数的好处,那为什么Kotlin还要提供一个noinline关键字来排除内联功能呢?
这是因为内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数属性。
非内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性。
另外,内联函数和非内联函数还有一个重要的区别,那就是内联函数所引用的Lambda表达式中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回。为了说明这个问题,我们来看下面的例子:
fun printString(str: String, block: (String) -> Unit) {println("printString begin")block(str)println("printString end")
}fun main() {println("main start")val str = ""printString(str) { s ->println("lambda start")if (s.isEmpty()) return@printStringprintln(s)println("lambda end")}println("main end")
}
这里定义了一个叫作printString()的高阶函数,用于在Lambda表达式中打印传入的字符串参数。但是如果字符串参数为空,那么就不进行打印。注意,Lambda表达式中是不允许直接使用return关键字的,这里使用了return@printString的写法,表示进行局部返回,并且不再执行Lambda表达式的剩余部分代码。现在我们就刚好传入一个空的字符串参数,运行程序,打印结果如下:
main start
printString begin
lambda start
printString end
main end
可以看到,除了Lambda表达式中return@printString语句之后的代码没有打印,其他的日志是正常打印的,说明return@printString确实只能进行局部返回。但是如果我们将printString()函数声明成一个内联函数,那么情况就不一样了,如下所示:
inline fun printString(str: String, block: (String) -> Unit) {println("printString begin")block(str)println("printString end")
}fun main() {println("main start")val str = ""printString(str) { s ->println("lambda start")if (s.isEmpty()) returnprintln(s)println("lambda end")}println("main end")
}
现在printString()函数变成了内联函数,我们就可以在Lambda表达式中使用return关键字了。此时的return代表的是返回外层的调用函数,也就是main()函数,如果想不通为什么的话,可以回顾一下在上一小节中学习的内联函数的代码替换过程。现在重新运行一下程序,打印结果如下:
main start
printString begin
lambda start
可以看到,不管是main()函数还是printString()函数,确实都在return关键字之后停止执行了,和我们所预期的结果一致。
将高阶函数声明成内联函数是一种良好的编程习惯,事实上,绝大多数高阶函数是可以直接声明成内联函数的,但是也有少部分例外的情况。观察下面的代码示例:
inline fun runRunnable(block: () -> Unit) {val runnable = Runnable {block()}runnable.run()
}
这段代码在没有加上inline关键字声明的时候绝对是可以正常工作的,但是在加上inline关键字之后就会提示如下:

这个错误出现的原因解释起来可能会稍微有点复杂。首先,在runRunnable()函数中,我们创建了一个Runnable对象,并在Runnable的Lambda表达式中调用了传入的函数类型参数。而Lambda表达式在编译的时候会被转换成匿名类的实现方式,也就是说,上述代码实际上是在匿名类中调用了传入的函数类型参数。
而内联函数所引用的Lambda表达式允许使用return关键字进行函数返回,但是由于我们是在匿名类中调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中的函数调用进行返回,因此这里就提示了上述错误。
也就是说,如果我们在高阶函数中创建了另外的Lambda或者匿名类的实现,并且在这些实现中调用函数类型参数,此时再将高阶函数声明成内联函数,就一定会提示错误。
那么是不是在这种情况下就真的无法使用内联函数了呢?也不是,比如借助crossinline关键字就可以很好地解决这个问题:
inline fun runRunnable(crossinline block: () -> Unit) {val runnable = Runnable {block()}runnable.run()
}
可以看到,这里在函数类型参数的前面加上了crossinline的声明,代码就可以正常编译通过了。
那么这个crossinline关键字又是什么呢?前面我们已经分析过,之所以会提示上面所示的错误,就是因为内联函数的Lambda表达式中允许使用return关键字,和高阶函数的匿名类实现中不允许使用return关键字之间造成了冲突。而crossinline关键字就像一个契约,它用于保证在内联函数的Lambda表达式中一定不会使用return关键字,这样冲突就不存在了,问题也就巧妙地解决了。
声明了crossinline之后,我们就无法在调用runRunnable函数时的Lambda表达式中使用return关键字进行函数返回了,但是仍然可以使用return@runRunnable的写法进行局部返回。总体来说,除了在return关键字的使用上有所区别之外,crossinline保留了内联函数的其他所有特性。
crossinline
值得注意的是,非局部返回虽然在某些场合下非常有用,但可能也存在危险。因为有时候,我们内联的函数所接收的Lambda参数常常来自于上下文其他地方。为了避免带有return的Lambda参数产生破坏,我们还可以使用crossinline关键字来修饰该参数,从而杜绝此类问题的发生。就像这样子:
fun main(args: Array<String>) {foo { return }
}
inline fun foo(crossinline returning: () -> Unit) {println("before local return")returning()println("after local return")return
}
// 运行结果
Error: (2, 11) Kotlin: 'return' is not allowed here
具体化参数类型
除了非局部返回之外,内联函数还可以帮助Kotlin实现具体化参数类型。Kotlin与Java一样,由于运行时的类型擦除,我们并不能直接获取一个参数的类型。然而,由于内联函数会直接在字节码中生成相应的函数体实现,这种情况下我们反而可以获得参数的具体类型。我们可以用reified修饰符来实现这一效果。
fun main(args: Array<String>) {getType<Int>()
}
inline fun <reified T> getType() {print(T::class)
}
// 运行结果
class kotlin.Int
这个特性在Android开发中也格外有用。比如在Java中,当我们要调用startActivity时,通常需要把具体的目标视图类作为一个参数。然而,在Kotlin中,我们可以用reified来进行简化:
inline fun <refied T : Activity> Activity.startActivity() {startActivity(Intent(this, T::class.java))
}
这样,我们进行视图导航就非常容易了,如:
startActivity<DetailActivity>()
相关文章:
【Kotlin】内联函数
文章目录 内联函数noinline: 避免参数被内联非局部返回使用标签实现Lambda非局部返回为什么要设计noinline crossinline具体化参数类型 Kotlin中的内联函数之所以被设计出来,主要是为了优化Kotlin支持Lambda表达式之后所带来的开销。然而,在Java中我们似…...
Unity技美35——再URP管线环境下,配置post后期效果插件(post processing)
前两年在我的unity文章第10篇写过,后效滤镜的使用,那时候大部分项目用的还是unity的基础管线,stander管线。 但是现在随着unity的发展,大部分项目都用了URO管线,甚至很多PC端用的都是高效果的HDRP管线,这就…...
Redis:持久化RDB和AOF
目录 概述RDB持久化流程指定备份文件的名称指定备份文件存放的目录触发RDB备份redis.conf 其他一些配置rdb的备份和恢复优缺点停止RDB AOF持久化流程AOF启动/修复/恢复AOF同步频率设置rewrite压缩原理触发机制重写流程no-appendfsync-on-rewrite 优缺点 如何选择 概述 Redis是…...
基于python协同过滤推荐算法的音乐推荐与管理系统
欢迎大家点赞、收藏、关注、评论啦 ,由于篇幅有限,只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 基于Python的协同过滤推荐算法的音乐推荐与管理系统是一个集成了音乐推荐和管理的系统,它使用协同过滤算…...
【极客技术】真假GPT-4?微调 Llama 2 以替代 GPT-3.5/4 已然可行!
近日小编在使用最新版GPT-4-Turbo模型(主要特点是支持128k输入和知识库截止日期是2023年4月)时,发现不同商家提供的模型回复出现不一致的情况,尤其是模型均承认自己知识库达到2023年4月,但当我们细问时,Fak…...
STK Components 二次开发-创建地面站
1.地面站只需要知道地面站的经纬高。 // Define the location of the facility using cartographic coordinates.var location new Cartographic(Trig.DegreesToRadians(-75.596766667), Trig.DegreesToRadians(40.0388333333), 0.0); 2.创建地面站 创建方式和卫星一样生成对…...
数据结构与算法(三)贪心算法(Java)
目录 一、简介1.1 定义1.2 基本步骤1.3 优缺点 二、经典示例2.1 选择排序2.2 背包问题 三、经典反例:找零钱3.1 题目3.2 解答3.3 记忆化搜索实现3.4 动态规划实现 一、简介 1.1 定义 贪心算法(Greedy Algorithm),又名贪婪法&…...
057-第三代软件开发-文件监视器
第三代软件开发-文件监视器 文章目录 第三代软件开发-文件监视器项目介绍文件监视器实现原理关于 QFileSystemWatcher实现代码 关键字: Qt、 Qml、 关键字3、 关键字4、 关键字5 项目介绍 欢迎来到我们的 QML & C 项目!这个项目结合了 QML&…...
二十七、微服务案例
目录 一、实现输入搜索功能 1、下载代码,在idea上打开 2、新建RequestParams类,用于接收解析请求 3、在启动类中加入客户端地址Bean,以便实现服务 4、编写搜索方法 5、新建返回分页结果类 6、实现搜索方法 7、编写控制类,…...
(C++)string类的模拟实现
愿所有美好如期而遇 前言 我们模拟实现string类不是为了去实现他,而是为了了解他内部成员函数的一些运行原理和时间复杂度,在将来我们使用时能够合理地去使用他们。 为了避免我们模拟实现的string类与全局上的string类冲突(string类也在std命名空间中)&…...
处理数据中的缺失值--删除缺少值的行
两个最主要的处理缺失值的方法是: ❏ 删除缺少值的行; ❏ 填充缺失值; 我们首先将serum_insulin的中的字段值0替换为None,可以看到缺失值的数量为374个; print(pima[serum_insulin].isnull().sum()) pima[serum_insu…...
Kotlin学习——kt里的集合,Map的各种方法之String篇
Kotlin 是一门现代但已成熟的编程语言,旨在让开发人员更幸福快乐。 它简洁、安全、可与 Java 及其他语言互操作,并提供了多种方式在多个平台间复用代码,以实现高效编程。 https://play.kotlinlang.org/byExample/01_introduction/02_Functio…...
MIT 6.824 -- MapReduce Lab
MIT 6.824 -- MapReduce Lab 环境准备实验背景实验要求测试说明流程说明 实验实现GoLand 配置代码实现对象介绍协调器启动工作线程启动Map阶段分配任务执行任务 Reduce 阶段分配任务执行任务 终止阶段 崩溃恢复 注意事项并发安全文件转换golang 知识点 测试 环境准备 从官方gi…...
创新研报|顺应全球数字化,能源企业以“双碳”为目标的转型迫在眉睫
能源行业现状及痛点分析 挑战一:数字感知能力较弱 挑战二:与业务的融合度低 挑战三:决策响应速度滞后 挑战四:价值创造有待提升 挑战五:安全风险如影随形 能源数字化转型定义及架构 能源行业数字化转型体系大体…...
Blender 连续 5 天遭受大规模 DDoS 攻击
Blender 发布公告指出,在2023年11月18日至23日期间,blender.org 网站遭受了持续的分布式拒绝服务(DDoS)攻击,攻击者通过不断发送请求导致服务器超载,使网站运营严重中断。此次攻击涉及数百个 IP 地址的僵尸…...
Python 获取本地和广域网 IP
Python 获取本地IP ,使用第三方库,比如 netifaces import netifaces as nidef get_ip_address():try:# 获取默认网络接口(通常是 eth0 或 en0)default_interface ni.gateways()[default][ni.AF_INET][1]# 获取指定网络接口的IP地…...
静态路由配置过程
静态路由 静态路由简介 路由器在转发数据时,要先在路由表(Routing Table)中在找相应的路由,才能知道数据包应该从哪个端口转发出去。路由器建立路由表基本上有以下三种途径。 (1)直连路由:路由…...
基于OGG实现MySQL实时同步
📢📢📢📣📣📣 哈喽!大家好,我是【IT邦德】,江湖人称jeames007,10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】!😜&am…...
【计算机网络笔记】多路访问控制(MAC)协议——轮转访问MAC协议
系列文章目录 什么是计算机网络? 什么是网络协议? 计算机网络的结构 数据交换之电路交换 数据交换之报文交换和分组交换 分组交换 vs 电路交换 计算机网络性能(1)——速率、带宽、延迟 计算机网络性能(2)…...
什么是好的FPGA编码风格?(3)--尽量不要使用锁存器Latch
前言 在FPGA设计中,几乎没人会主动使用锁存器Latch,但有时候不知不觉中你的设计莫名其妙地就生成了一堆Latch,而这些Latch可能会给你带来巨大的麻烦。 什么是锁存器Latch? Latch,锁存器,一种可以存储电路…...
线程与协程
1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指:像函数调用/返回一样轻量地完成任务切换。 举例说明: 当你在程序中写一个函数调用: funcA() 然后 funcA 执行完后返回&…...
汽车生产虚拟实训中的技能提升与生产优化
在制造业蓬勃发展的大背景下,虚拟教学实训宛如一颗璀璨的新星,正发挥着不可或缺且日益凸显的关键作用,源源不断地为企业的稳健前行与创新发展注入磅礴强大的动力。就以汽车制造企业这一极具代表性的行业主体为例,汽车生产线上各类…...
Qwen3-Embedding-0.6B深度解析:多语言语义检索的轻量级利器
第一章 引言:语义表示的新时代挑战与Qwen3的破局之路 1.1 文本嵌入的核心价值与技术演进 在人工智能领域,文本嵌入技术如同连接自然语言与机器理解的“神经突触”——它将人类语言转化为计算机可计算的语义向量,支撑着搜索引擎、推荐系统、…...
Python爬虫(二):爬虫完整流程
爬虫完整流程详解(7大核心步骤实战技巧) 一、爬虫完整工作流程 以下是爬虫开发的完整流程,我将结合具体技术点和实战经验展开说明: 1. 目标分析与前期准备 网站技术分析: 使用浏览器开发者工具(F12&…...
高危文件识别的常用算法:原理、应用与企业场景
高危文件识别的常用算法:原理、应用与企业场景 高危文件识别旨在检测可能导致安全威胁的文件,如包含恶意代码、敏感数据或欺诈内容的文档,在企业协同办公环境中(如Teams、Google Workspace)尤为重要。结合大模型技术&…...
Unit 1 深度强化学习简介
Deep RL Course ——Unit 1 Introduction 从理论和实践层面深入学习深度强化学习。学会使用知名的深度强化学习库,例如 Stable Baselines3、RL Baselines3 Zoo、Sample Factory 和 CleanRL。在独特的环境中训练智能体,比如 SnowballFight、Huggy the Do…...
免费PDF转图片工具
免费PDF转图片工具 一款简单易用的PDF转图片工具,可以将PDF文件快速转换为高质量PNG图片。无需安装复杂的软件,也不需要在线上传文件,保护您的隐私。 工具截图 主要特点 🚀 快速转换:本地转换,无需等待上…...
CSS3相关知识点
CSS3相关知识点 CSS3私有前缀私有前缀私有前缀存在的意义常见浏览器的私有前缀 CSS3基本语法CSS3 新增长度单位CSS3 新增颜色设置方式CSS3 新增选择器CSS3 新增盒模型相关属性box-sizing 怪异盒模型resize调整盒子大小box-shadow 盒子阴影opacity 不透明度 CSS3 新增背景属性ba…...
python打卡day49@浙大疏锦行
知识点回顾: 通道注意力模块复习空间注意力模块CBAM的定义 作业:尝试对今天的模型检查参数数目,并用tensorboard查看训练过程 一、通道注意力模块复习 & CBAM实现 import torch import torch.nn as nnclass CBAM(nn.Module):def __init__…...
[QMT量化交易小白入门]-六十二、ETF轮动中简单的评分算法如何获取历史年化收益32.7%
本专栏主要是介绍QMT的基础用法,常见函数,写策略的方法,也会分享一些量化交易的思路,大概会写100篇左右。 QMT的相关资料较少,在使用过程中不断的摸索,遇到了一些问题,记录下来和大家一起沟通,共同进步。 文章目录 相关阅读1. 策略概述2. 趋势评分模块3 代码解析4 木头…...
