- 精华
- 0
- 在线时间
- 6 小时
- UID
- 166427
- 积分
- 72
- 帖子
- 2
- 阅读权限
- 30
- 注册时间
- 2012-5-14
- 最后登录
- 2014-6-11
- 精华
- 0
- UID
- 166427
- 积分
- 72
- 帖子
- 2
- 主题
- 1
- 阅读权限
- 30
- 注册时间
- 2012-5-14
- 最后登录
- 2014-6-11
|
TradeBlazer中有一个编译警告W0201,它的含义是:“FOR,WHILE,IF,ELSE中包含序列函数,可能存在潜在的逻辑错误,请确认代码无误”。
但对这个解释,很多人并不清楚是什么意思。并且我认为,官方文档在这个问题上的解释存在一点误导性,于是拿出来说一说。
所谓“序列函数”,就是带序列类型参数的函数,也就是在Params字段包含了NumericSeries,或者BoolSeries,或者StringSeries类型的参数的函数。
为什么要避免在FOR,WHILE,IF,ELSE中,调用序列函数呢?
网上的一些资料解释说:序列函数需要每个Bar都被调用,如果有些Bar没有调用序列函数,序列函数中的序列数据则是上一个Bar的值。
我来解释一下这句话的意思:
假设主程序中有如下代码:
// 代码段一
Vars
NumericSeries numSeries;
Numeric nVal;
Begin
numSeries = CurrentBar;
If (CurrentBar % 3 == 0 )
nVal = Average(numSeries, 3);
Else If (CurrentBar % 3 == 1 )
nVal = Average(numSeries, 3);
Else
nVal = Average(numSeries, 3);
FileAppend(“LogFile”, Text(nVal));
End
这段代码的含义很简单,无论当前Bar的序号对3的余数是0、1、还是2(当然只有这三种情况),都把numSeries的最后三个元素的平均数打印出来。而numSeries 实际上就是CurrentBar的序号序列:0、1、2、3、4……
这段代码在编译中,会出现本文开始时候提到的警告提示,因为我们在If体中引用了序列函数Average,并提示我们“可能存在潜在的逻辑错误”。
可是,这段代码真的有逻辑错误吗?从逻辑上来讲,如果一个条件成立就执行A,否则也执行A,那就是说,无论如何都要执行A,那么这个逻辑和直接执行A就是等价的。所以上述代码等价于如下代码:
// 代码段二
Vars
NumericSeries numSeries;
Numeric nVal;
Begin
numSeries = CurrentBar;
nVal = Average(numSeries, 3);
FileAppend(“LogFile”, Text(nVal));
End
OK,这段代码的编译没有警告出现,而且“逻辑上和代码段一是等价的”。但是,如果你真的这样认为,那就上当了。我们先看看代码段二的输出:
0: 0
1: 0.333333
2: 1
3: 2
4: 3
5: 4
6: 5
7: 6
……
……
序号是2及以后的Bar的输出很好理解,因为从CurrentBar开始向前回溯,连续三个数的平均数是 CurrentBar - 1,这一点毫无疑问。问题是最前面的两项。当CurrentBar是0的时候,Average函数的输出是0,而当CurrentBar为1的时候,Average的输出是1/3。也许你会认为,Average很聪明,当CurrentBar是0和1的时候,向前回溯3个元素是无法做到的,所以它就只“尽力”回溯到可以回溯的地方。在CurrentBar为0的时候,它无法向前回溯,所以计算平均值就是 0 / 3 = 0;在CurrentBar为1的时候,它只向前回溯1个元素,所以计算平均值就是(0+1) / 3 = 0.333333
这一切看起来很合理,但如果我们稍微改动一下程序,你马上就发现不是那么回事了。
// 代码段三
Vars
NumericSeries numSeries;
Numeric nVal;
Begin
numSeries = CurrentBar + 1;
nVal = Average(numSeries, 3);
FileAppend(“LogFile”, Text(nVal));
End
对代码段三来说,如果按照之前的推理,前两个平均数输出应该是1 / 3,和 (1 + 2)/ 3 = 1,但事实上它的输出是:
0: 1
1: 1.333333
2: 2
3: 3
4: 4
5: 5
6: 6
7: 7
……
……
之所以会出现这种情况,是因为出现了所谓的回溯越界。当CurrentBar为0时,引用numSeries[1]就会引发越界。据说在老版本的TB中,越界会返回一个无效值,但现在的TB会返回最近的一个有效元素的值。对于numSeries[1]来说,TB会返回numSeries(也就是numSeries[0])的值作为结果。同样的,此时对于numSeries[2], numSeries[3], …… numSeries[N]来说,它们的值都是一样的,都是numSeries的值。
所以,对代码三来说,CurrentBar为0时,Average的计算并不是(0+0+1)/3,而是(1+1+1)/3,当CurrentBar为1的时候,Average的计算是 (1+1+2)/ 3,于是出现了上面的结果。
好,之所以说了这么一大堆关于回溯越界的事情,是因为这样可以更好地了解TB的传参机制。现在回到前面,继续之前的分析。如果说,代码段二和代码段一是等价的,那么它们的输出应该完全一致。那事实上是不是这样呢?我们再来看下代码段一的输出。为了方便比较,我们把代码一和二的两组输出放在一起观察:
代码段一的输出:
0: 0
1: 1
2: 2
3: 3
4: 4
5: 5
6: 6
7: 7
……
……
代码段二的输出:
0: 0
1: 0.333333
2: 1
3: 2
4: 3
5: 4
6: 5
7: 6
……
……
看,貌似逻辑相同的两段代码,输出的结果却不同。我认为这是TB设计上的缺陷。但关键是,我们得知道问题是怎么发生的。
我们假设,对代码段一来说,当前的CurrentBar是2,这时候numSeries已经保存了3个元素,从左到右(从旧到新)分别是0、1、2。此时,CurrentBar % 3 的结果是2,所以代码进入了If-Else结构的最后一个分支,开始调用Average(numSeries, 3)来计算三者的平均值。按照我们的设想,Average的计算过程应该是(0 + 1 + 2)/ 3 = 1,可查看代码一的结果,却发现,输出居然是2 !!
其实,问题就出在序列函数的传参机制上。
当程序在If-Else结构的最后一个分支调用Average的时候,代码并没有原原本本地把参数numSeries传给它。C++或者Java的程序员,已经习惯了关于对象的引用传参方式,大多会想当然地认为Average接收到的就是0、1、2序列,可事实上却不是。
出于某种原因,TB除了三种基本类型以外,并没有为序列变量设计引用方式的传参。当我们调用Average(numSeries, 3)的时候,Average函数在作用域内部构造一个NumericSeries内部变量用于接收参数,可是基于效率的考虑,TB自然不会把整个序列变量全盘拷贝,它采取了最直接的方式,把numSeries[0],赋给了Average的内部变量。注意,这是处于If-Else结构的最后分支上的Average函数第一次被调用,前面两个Bar(CurrentBar = 0 和 CurrentBar = 1)并没有给这个内部变量赋予任何东西。于是,对于CurrentBar = 2那一刻来说,Average的内部序列变量中保存的值序列是:N/A, N/A, 2
此时,由于前两个值是无效的,对内部序列的回溯产生了越界,于是按照TB的机制,越界回溯返回了最后一个有效元素的值,也就是2,因此在这一刻,Average的实际计算方式是:(2 + 2 + 2)/3 = 2
看到了吧,结果就是这么发生的。
事实上,由于代码段一中的If-Else模块共有三个分支,在每个分支上的Average都有一个自己的内部变量,用于接收参数传递,所以,不管代码在那个分支上执行,Average得到的都不是一个完整的numSeries序列。更要命的是,它们都只按照自己内部的序列变量来计算均值,于是导致了最终的结果。
为什么要花这么大的篇幅来讲这个事情呢?
因为我觉得TB在W0201警告中的说明太含糊了,甚至不完全准确。它只考虑到在FOR,WHILE,IF,ELSE这些判断类条件中,序列变量的传参问题可能会带来序列函数内部变量的不连续性,但事实上情况比这个要复杂。
看下面这个代码:
Vars
NumericSeries numSeries;
Numeric nVal;
Begin
numSeries = 1;
nVal = Average(numSeries, 3);
FileAppend(“LogFile”, Text(nVal));
numSeries = 2;
nVal = Average(numSeries, 3);
FileAppend(“LogFile”, Text(nVal));
numSeries = 3;
nVal = Average(numSeries, 3);
FileAppend(“LogFile”, Text(nVal));
End
你认为它的输出会是什么?
在每个Bar里面,numSeries会被重复赋值,最后一次赋值总是3。当然,我们有理由相信,当CurrentBar=2到来时,numSeries里面的元素应该是numSeries[2]=3,numSeries[1]=3,此时,执行第一个求平均,numSeries[0]被赋予1,那么Average计算的应该是(3 + 3 + 1) / 3。可事实上是不是这样呢?来看一下真正的输出吧:
0: 1
0: 2
0: 3
1: 1
1: 2
1: 3
2: 1
2: 2
2: 3
3: 1
3: 2
3: 3
……
……
具体原因参照之前的分析就好了。我们可以看到,即便不是在FOR,WHILE,IF,ELSE这些句块当中,我们在主程序中多次给序列变量赋值,并且用不同的值调用序列函数,也会得到不同的结果。这是由序列函数的传参机制决定的,跟在哪个句块当中出现,没有关系。
当然,听TB的警告,不要在FOR,WHILE,IF,ELSE中包含序列函数是对的,另外,看过本文,你也可以知道如何分析 “可能存在潜在的逻辑错误”了。
|
-
总评分: 威望 + 69
查看全部评分
|