简介
作为语言的一个标准实施模板的问题在 mql5.com 论坛中出现了很多次。随着我被 MQL5 开发人员拒绝,我使用自定义方法实施模板的兴趣逐渐变浓。本文介绍了我的研究结果。
C 和 C++ 的部分历史
从一开始起,C 语言就是为了提供执行系统任务的可能性而开发的。C 语言的创建者没有实施一个语言执行环境的抽象模型;他们只是实施了满足系统编程者需要的功能。首先,它们是直接处理应用程序的内存、控制的结构构建和模块管理的方法。
实际上,C 语言中再也没有包含别的事情了;所有其他事情都来自运行时库。这是为什么一些不友好的人有时将 C 语言称为结构化汇编程序的原因。但是无论他们说什么,此方法似乎非常成功。因此,C 语言的简单性和功能之比达到了新的水平。
这样,C 逐渐成为一种通用的系统编程语言。但是它并没有固守在这些限制里。在二十世纪八十年代后期,将 Fortran 从主导位置推开,C 成为全世界的程序员最喜欢的语言,并且在不同的应用程序中被广泛使用。在培养新一代程序员的大学中, Unix 的分配(C 语言亦是如此)对其流行做出了重要的贡献。
但是,如果一切都是如此光明,那么为什么其他语言仍然在使用中并且又是什么在支持它们的存在呢?C 语言的阿基里斯之踵对于在二十世纪九十年代出现的问题而言,它太低级了。此问题有两个方面。
一方面,该语言包含太多低级含义:首先,它处理内存和地址运算。因此,处理器位数的改变导致很多 C 应用程序出现问题。另一方面,在 C 中缺乏高级含义 - 数据和对象的抽象类型、多态现象和异常处理。因此,在 C 应用程序中,一项任务的实施技术通常比其实质性内容还重要。
解决这些缺点的最初尝试出现于二十世纪八十年代初期。在那个时候,AT&T 贝尔实验室的 Bjarne Stroustrup 开始开发 C 语言的扩展,称为“带有类的 C”。开发风格与 C 语言本身的创建精神一致 - 添加不同的功能,使某类人群的工作更加方便。
C++ 的主要创新是让使用新的数据类型成为可能的类的机制。程序员描述类对象的内部陈述和用于访问该陈述的一组函数-方法。创建 C++ 的主要目的之一是提高重复使用已经写好的代码的比例。
C++ 语言的创新不仅仅是包含类的引入。它还实施了结构化异常处理机制(缺少它会让容错应用程序的开发变得复杂)、模板机制和很多其他机制。因此,语言发展的主干线是通过引入新的高级构建,同时保持与 ANSI С 的完全兼容性来扩展其可能性。
作为宏替换机制的模板
为了理解如何在 MQL5 中实施一个模板,需要理解它们是如何在 C++ 中工作的。
让我们看一看定义。
MQL5 没有模板,但是这并不意味着不可能运用使用模板进行编程的风格。实际上,C++ 语言中的模板机制是一种深度嵌入语言的、完善的宏生成机制。换言之,当程序员使用模板时,编译器在调用相应函数的地方确定数据类型,而不是在声明函数的地方。
在其中引入 C++ 的模板减少了程序员编写代码的量。但是您不应忘记程序员在键盘上键入的代码与编译器创建的代码不相同。模板机制本身并不会导致程序大小的减小;它只是减少它们的源代码的大小。这是为什么使用模板所解决的主要问题是减少程序员键入的代码的原因。
因为机器代码是在编译期间生成的,普通的程序员看不到函数的代码是生成一次还是生成几次。在模板代码的编译期间,在使用模板的地方有多少种类型,函数代码就生成多少次。基本而言,模板会在编译阶段重写。
在 C++ 中引入模板的第二方面是内存分配。问题在于 C 语言中的内存是静态分配的。为了使此分配更加灵活,使用了一个为数组设置内存大小的模板。但是这方面已经被 MQL4 的开发人员以动态数组的形式实施了,并且它也在 MQL5 中以动态对象的形式实施。
因此,只剩下类型替换的问题没有解决。MQL5 的开发人员拒绝解决这一问题,称使用模板替换机制会让编译器崩溃,进而导致反编译的出现。
当然,他们更清楚。现在我们只有一种选择 - 以自定义的方式实施这一范例。
首先,让我声明一下,我们并不是要更改编译器或更改语言的标准。我建议更改模板本身的方法。如果我们不在编译阶段创建模板,这并不意味着不允许我们编写机器代码。我建议将模板的使用从二进制代码的生成部分移到编写文本代码的部分。让我们将这种方法称为“伪模板”。
伪模板
与 C++ 模板相比,伪模板有其优点和缺点。缺点包括额外处理文件的移动。优点包括与标准语言确定的可能性相比,其可能性更加灵活。让我们付诸行动。
要使用伪模板,我们需要一个模拟预处理器。为此,我们将使用“模板”脚本。以下是对此脚本的一般要求:它必须读取指定文件(保持数据结构)、查找一个模板并用指定类型替换。
在这里我需要声明一下。因为我们将代替模板使用重写机制,要重写的类型有多少,代码就要重写多少次。换言之,将在指定用于分析的整个代码中进行替换。之后,脚本将重写代码几次,每次产生一个新的替换。因此,我们能够实现“用机器执行手工劳动”这一口号。
开发脚本代码
让我们确定需要的输入变量:
- 要处理的文件的名称。
- 用于存储要重写的数据类型的变量。
- 将用于代替实际数据类型的模板的名称。
input string folder="Example templat";//name of file for processing input string type="long;double;datetime;string" ;//names of custom types, separator ";" string TEMPLAT="_XXX_";// template name
为了让脚本只增加代码的一部分,要设置标记的名称。开始标记用于指出要处理的部分的起点,而结束标记用于指出其终点。
使用脚本时,我遇到读取标记的问题。
我在分析期间发现,在 MetaEditor 中对文档进行格式化时,注释行常常被添加空格或制表符(视情形而定)。通过在确定标记时删除有意义的符号前后的空格解决了这一问题。在脚本中自动实现了此功能,但是需要指出一点。
标记名称不得以空格开头或结尾。
结束标记不是必须的;如果不存在,则向下处理代码直到文件结束。但是必须要有一个开始标记。因为标记的名称是不变的,我使用 #define 预处理器指令代替变量。
#define startread "//start point" #define endread "//end point"
为了构建一个类型数组,我创建了函数 void ParserInputType(int i,string &type_d[],string text),该函数使用 'type' 变量向 type_dates[] 数组填充值。
一旦脚本收到文件和标记的名称,它就开始读取文件。为了保存文档的格式,脚本逐行读取信息,将找到的行保存在数组中。
当然,您可以将所有行都保存在一个变量中;但是在这种情况下,您将丢失连字符并且文本将变为没有穷尽的一行。这是为什么读取文件的函数使用在每次反复获取新的字符串时更改其大小的字符串数组的原因。
//+------------------------------------------------------------------+ //| downloading file | //+------------------------------------------------------------------+ void ReadFile() { string subfolder="Templates"; int han=FileOpen(subfolder+"\\"+folder+".mqh",FILE_READ|FILE_SHARE_READ|FILE_TXT|FILE_ANSI,"\r"); if(han!=INVALID_HANDLE) { string temp=""; //--- scrolling file to the starting point do {temp=FileReadString(han);StringTrimLeft(temp);StringTrimRight(temp);} while(startread!=temp); string text=""; int size; //--- reading the file to the array until a break point or the end of the file while(!FileIsEnding(han)) { temp=text=FileReadString(han); // deleting symbols of tabulation to check the end StringTrimLeft(temp);StringTrimRight(temp); if(endread==temp)break; // flushing data to the array if(text!="") { size=ArraySize(fdates); ArrayResize(fdates,size+1); fdates[size]=text; } } FileClose(han); } else { Print("File open failed"+subfolder+"\\"+folder+".mqh, error",GetLastError()); flagnew=true; } }
为方便起见,文件以 FILE_SHARE_READ 模式打开。这样提供了开始脚本而不关闭所编辑文件的可能。文件扩展名被指定为 'mqh'。因此,脚本直接读取存储在包含文件中的代码的文本。问题在于扩展名为 'mqh' 的文件实际上是一个文本文件;您可以简单地通过将其重命名为 'txt'文件,然后使用任何文本编辑器打开 'mqh' 文件来确认这一点。
在读取结束后,数组的长度等于开始标记和结束标记之间的行数。
现在,让我们进行信息的分析。分析和替换信息的函数是从写到文件的函数 void WriteFile(int count) 调用的。在函数内给出了注释。
void WriteFile(int count) { ... if(han!=INVALID_HANDLE) { if(flagnew)// if the file cannot be read { ... } else {// if the file exists ArrayResize(tempfdates,count); int count_type=ArraySize(type_dates); //--- the cycle rewrites the contents of the file for each type of the type_dates template for(int j=0;j<count_type;j++) { for(int i=0;i<count;i++) // copy data into the temporary array tempfdates[i]=fdates[i]; for(int i=0;i<count;i++) // replace templates with types Replace(tempfdates,i,j); for(int i=0;i<count;i++) FileWrite(han,tempfdates[i]); // flushing array in the file } } ... }
因为数据是在原位被替换并且数组是在转换之后更改的,我们将处理副本。在这里,我们设置用于临时存储数据的 tempfdates[] 数组的大小,并依据 fdates[] 样本填充。
然后,使用 Replace() 函数执行模板替换。函数的参数如下:要处理的数组(在其中执行模板替换)、(要移到数组内的)行的计数器 i 和(要遍历类型数组)的类型计数器 j 。
因为我们有两个嵌套循环,指定多少个类型,就会添加多少次源代码。
//+------------------------------------------------------------------+ //| replacing templates with types | //+------------------------------------------------------------------+ void Replace(string &temp_m[],int i,int j) { if(i>=ArraySize(temp_m))return; if(j<ArraySize(type_dates)) StringReplac(temp_m[i],TEMPLAT,type_dates[j]);// replacing templat with types }
Replace() 函数包含检查(以避免调用不存在的数组索引),并且调用嵌套函数 StringReplac()。为什么函数的名称与标准函数 StringReplace 类似是有原因的,它们也有相同数量的参数。
因此,通过添加一个字母 "e",我们能够更改替换的整个逻辑。标准函数采用 'find' 样本的值,并用指定字符串 'replacement' 代替它。并且我的函数不仅仅是替换,还分析在 'find' 前面是否有符号(即检查 'find' 是否为一个单词的一部分);如果有,则用大写的 'replacement' 替换 'find',否则按原样执行替换。因此,除了设置类型以外,您还可以在重写后的数据的名称中使用它们。
创新
现在,让我谈一谈在使用时添加的创新。我已经提过在使用脚本时存在读取标记的问题。
通过 void ReadFile() 函数内的以下代码解决了问题:
string temp=""; //--- scrolling the file to the start point do {temp=FileReadString(han);StringTrimLeft(temp);StringTrimRight(temp);} while(startread!=temp);
循环本身是用以前的版本实施的,但是使用 StringTrimLeft() 和 StringTrimRight() 函数除去制表符仅出现在增强版本中。
此外,创新包括从输出文件的名称删除 "templat" 扩展名,这样就为使用准备好了输出文件。通过使用从指定字符串删除指定样本的函数来实施。
删除函数的代码:
//+------------------------------------------------------------------+ //| Deleting the 'find' template from the 'text' string | //+------------------------------------------------------------------+ string StringDel(string text,const string find) { string str=text; StringReplace(str,find,""); return(str); }
删除文件名的代码位于函数 void WriteFile(int count) 中:
string newfolder; if(flagnew)newfolder=folder;// if it is the first start, create an empty file of pre-template else newfolder=StringDel(folder," templat");// or create the output file according to the template
此外,引入了预模板的准备模式。如果在 Files/Templates 目录中不存在需要的文件,则将按预模板文件生成。
例如:
//#define _XXX_ long //this is the start point _XXX_ //this is the end point
创建该行的代码位于 void WriteFile(int count) 函数中:
if(flagnew)// if the file couldn't be read {// fill the template file with the pre-template FileWrite(han,"#define "+TEMPLAT+" "+type_dates[0]); FileWrite(han," "); FileWrite(han,startread); FileWrite(han," "+TEMPLAT); FileWrite(han,endread); Print("Creating pre-template "+subfolder+"\\"+folder+".mqh"); }
代码的执行受到全局变量 flagnew 的保护,在读取文件出错时,该变量的值为 'true'。
在使用脚本时,我添加了一个额外的模板。连接第二个模板的过程是一样的。需要更改的函数更靠近 OnStart() 函数以便连接额外的模板。这样就走出了一条用于连接新模板的路径。因此,我们能够连接我们需要的任意数量的模板。现在,让我们检查操作。
检查操作
首先,让我们从指定所有必需的参数启动脚本。在出现的窗口中,指定 "Example templat"(示例模板)文件名。
使用 ';' 分隔符填写自定义数据类型字段。
一旦按下 "OK"(确定)按钮,则创建 Templates 目录,该目录包含预模板文件 "Example templat.mqh"。
此事件显示在日志中,消息如下所示:
让我们更改预模板并重新启动脚本。这一次,文件已经存在于 Templates 目录中(目录本身也已经存在),这是为什么有关在打开文件时出错的消息不再显示的原因。将依据指定模板执行替换:
//this_is_the_start_point _XXX_ Value_XXX_; //this_is_the_end_point
再次打开创建的文件 "Example.mqh"。
long ValueLONG; double ValueDOUBLE; datetime ValueDATETIME; string ValueSTRING;
如您所见,依据我们作为参数传递的类型的数量,一行变为 4 行。现在,在模板文件中写入以下代码行:
//this_is_the_start_point _XXX_ Value_XXX_; _XXX_ Type_XXX_; //this_is_the_end_point
结果证明脚本逻辑以一种清晰的方式运行。
首先,一个数据类型重写整个代码,接着执行对另一类型的处理。直到所有类型处理完毕为止。
long ValueLONG; long TypeLONG; double ValueDOUBLE; double TypeDOUBLE; datetime ValueDATETIME; datetime TypeDATETIME; string ValueSTRING; string TypeSTRING;
现在,于示例文本内包含第二个模板。
//this_is_the_start_point _XXX_ Value_XXX_(_xxx_ ind){return((_XXX_)ind);}; _XXX_ Type_XXX_(_xxx_ ind){return((_XXX_)ind);}; //this_is_the_end_button
结果:
long ValueLONG(int ind){return((long)ind);}; long TypeLONG(int ind){return((long)ind);}; double ValueDOUBLE(float ind){return((double)ind);}; double TypeDOUBLE(float ind){return((double)ind);}; datetime ValueDATETIME(int ind){return((datetime)ind);}; datetime TypeDATETIME(int ind){return((datetime)ind);}; string ValueSTRING(string ind){return((string)ind);}; string TypeSTRING(string ind){return((string)ind);};
在最后一个例子中,我有意在最后一行后边输入一个空格。该空行说明脚本结束处理一个类型并开始处理另一类型的地方。至于第二个模板,我们可以注意到对类型的处理与第一个模板类似。如果找不到第一个模板的某个类型的对应类型,则不显示任何代码。
现在,我想澄清调试代码的问题。对于调试而言,给出的示例非常简单。在编程期间,您可能需要调试很大一部分的代码,并且在完成时增加代码。为此,在预模板中有一个保留的注释行:"//#define _XXX_ long"。
如果删除注释,我们的模板将变为真实类型。换言之,我们将告诉编译器如何解释模板。
很不幸,我们不能用此方式调试所有类型。但是我们可以调试一种类型,然后在 'define' 中更改模板类型,这样我们就能一个接一个地调试所有类型。当然,为了调试,我们需要将文件移到调用文件的目录或移到 Include 目录中。这是我在先前讨论伪模板的缺点时提到的调试的不便之处。
总结
最后,我想说,尽管使用伪模板的想法很有趣并且非常高效,它也仅是刚刚开始实施的想法。尽管上述代码有作用并且为我节省了很多编写代码的时间,但是仍然有很多问题没有得到解决。首先是开发标准的问题。
我的脚本实施模板的块替换。但是这种方法并不是强制的。您可以创建对某些规则进行解释的更加复杂的分析程序。但这里是开始之处。欢迎大家热烈讨论。思想在碰撞中向前发展。祝您好运!