M姐今天推荐一篇关于C#不错的文章,内容比较丰富,讲述的也十分详细,拆分成两部分推送给大家。要好好学习哦。
=============
在本文中,我将介绍如何使用 C#(无需使用任何外部库)构建自定义脚本语言。我在 MSDN 杂志 2015 年 10 月刊 (msdn.com/magazine/mt573716) 中介绍过,脚本语言根据“拆分与合并”算法在 C# 中分析数学表达式。
通过使用自定义函数,我可以扩展“拆分与合并”算法,以便同时分析数学表达式和可自定义脚本语言。可以将“标准”语言控制流语句(if、else、while、continue、break 等)添加为自定义函数,就像添加其他典型的脚本语言功能(操作系统命令、字符串操作、搜索文件等)一样。
我将把我的语言称为“C# 中的可定义脚本”或 CSCS。我为什么想要创建另一种脚本语言呢? 因为这是一种可自定义的简易语言。添加新的函数或添加提取任意多个参数的新控制流语句只需要添加几行代码即可。此外,可以在任意非英语方案中使用函数名称和控制流语句,只需更改一些配置即可,我也将在本文中予以介绍。通过查看 CSCS 语言的实现方式,您将能够创建您自己的自定义脚本语言。
实现非常基本的脚本语言相当容易,但实现五星级水平的语言则是既残酷又艰难。在本文中,我将限制 CSCS 的作用域,以便您有个大致的了解:
CSCS 语言包含 if、else if、else、while、continue 和 break 控制流语句。此外,还支持嵌套语句。您将学习如何快速添加其他控制语句。
没有布尔值。您需要编写“if (a == 1)”,而不是“if (a)”。
不支持逻辑运算符。您需要将嵌套的 if 编写为“if (a == 1) { if (b == 2) { … } }”,而不是“if (a ==1 and b == 2)”。
CSCS 不支持函数和方法,但您可以在 C# 中编写函数和方法,并使用“拆分与合并”分析程序注册它们,从而将它们与 CSCS 结合使用。
只支持“//”-style 注释。
支持变量和单维数组(全都在全局级别定义)。变量可以包含数字、字符串或其他变量的元组(作为列表实现)。不支持多维数组。
图 1 显示了用 CSCS 编写的“Hello, World!”程序。由于错误键入“print”,因此这个程序在最后显示错误: “无法分析令牌 [pint]”。 请注意,所有之前的语句均已成功执行;也就是说,CSCS 是解释器。
图 1:用 CSCS 编写的“Hello, World!”
我已对“拆分与合并”算法的“拆分”部分进行了两处更改(“合并”部分保持不变)。
第一处更改是,表达式分析结果现在可以是数字、字符串或值元组(每个都可以是字符串或数字),而不是只能为数字。我创建了以下 Parser.Result 类来存储应用“拆分与合并”算法的结果:
public class Result { public Result(double dRes = Double.NaN, string sRes = null, List<Result> tRes = null) { Value = dResult; String = sResult; Tuple = tResult; } public double Value { get; set; } public string String { get; set; } public List<Result> Tuple { get; set; } }
第二处更改是,现在不仅要等到找到停止分析字符 ) 或 \n 才能执行“拆分”部分,而且还要等到找到所传递的停止分析字符数组中的任意字符才能执行“拆分”部分。此更改非常有必要,例如,在分析 If 语句的第一个自变量时,其中分隔符可以是任意 <、> 或 = 字符。
您可以在随附的源代码下载中查看修改后的“拆分与合并”算法。
负责解释 CSCS 代码的类称为“解释器”。它是作为单一实例(即,只能有一个类实例的类定义)实现。在它的 Init 方法中,分析程序(见刚才提到的原文)是通过解释器使用的所有函数进行初始化:
public void Init() { ParserFunction.AddFunction(Constants.IF, new IfStatement(this)); ParserFunction.AddFunction(Constants.WHILE, new WhileStatement(this)); ParserFunction.AddFunction(Constants.CONTINUE, new ContinueStatement()); ParserFunction.AddFunction(Constants.BREAK, new BreakStatement()); ParserFunction.AddFunction(Constants.SET, new SetVarFunction()); ... }
CSCS 中使用的实际名称在 Constants.cs 文件中进行了定义:
...public const string IF = "if";public const string ELSE = "else";public const string ELSE_IF = "elif";public const string WHILE = "while";public const string CONTINUE = "continue";public const string BREAK = "break";public const string SET = "set";
使用分析程序注册的所有函数都必须作为派生自 ParserFunction 类的类进行实现,且必须替代其 Evaluate 方法。
解释器在开始处理脚本时做的第一件事就是,通过删除所有空格(除非空格包含在字符串里面)和注释来简化脚本。因此,空格或新行不能用作运算符分隔符。运算符分隔符和注释字符串也在 Constants.cs 文件中进行了定义:
public const char END_STATEMENT = ';';public const string COMMENT = "//";
CSCS 支持数字(类型是 Double)、字符串或元组(变量数组,作为 C# 列表实现)。元组的每个元素可以是字符串或数字,但不能是其他元组。因此,不支持多维数组。若要定义变量,请使用 CSCS 函数“set”。C# 类 SetVarFunction 可实现设置变量值的功能,如图 2 所示。
图 2:实现 Set 变量函数
class SetVarFunction : ParserFunction { protected override Parser.Result Evaluate(string data, ref int from) { string varName = Utils.GetToken(data, ref from, Constants.NEXT_ARG_ARRAY); if (from >= data.Length) { throw new ArgumentException("Couldn't set variable before end of line"); } Parser.Result varValue = Utils.GetItem(data, ref from); // Check if the variable to be set has the form of x(i), // meaning that this is an array element. int arrayIndex = Utils.ExtractArrayElement(ref varName); if (arrayIndex >= 0) { bool exists = ParserFunction.FunctionExists(varName); Parser.Result currentValue = exists ? ParserFunction.GetFunction(varName).GetValue(data, ref from) : new Parser.Result(); List<Parser.Result> tuple = currentValue.Tuple == null ? new List<Parser.Result>() : currentValue.Tuple; if (tuple.Count > arrayIndex) { tuple[arrayIndex] = varValue; } else { for (int i = tuple.Count; i < arrayIndex; i++) { tuple.Add(new Parser.Result(Double.NaN, string.Empty)); } tuple.Add(varValue); } varValue = new Parser.Result(Double.NaN, null, tuple); } ParserFunction.AddFunction(varName, new GetVarFunction(varName, varValue)); return new Parser.Result(Double.NaN, varName); } }
下面的几个示例展示了如何在 CSCS 中定义变量:
set(a, "2 + 3"); // a will be equal to the string "2 + 3"set(b, 2 + 3); // b will be equal to the number 5set(c(2), "xyz"); // c will be initialized as a tuple of size 3 with c(0) = c(1) = ""
请注意,无需对数组进行特殊的声明:只需定义带索引的变量,即可初始化尚未初始化的数组,并根据需要向其中添加空元素。在上一个示例中,添加的是元素 c(0) 和 c(1),两个元素均初始化为空字符串。我认为这就免去了大多数脚本语言在先声明数组时必须采取的不必要步骤。
所有 CSCS 变量和数组都是使用 CSCS 函数(如 set 或 append)创建。它们全都使用全局作用域进行定义,稍后可以通过调用变量名称或带索引的变量进行使用。在 C# 中,这是在 GetVarFunction 中实现,如图 3 所示。
图 3:实现 Get 变量函数
class GetVarFunction : ParserFunction { internal GetVarFunction(Parser.Result value) { m_value = value; } protected override Parser.Result Evaluate(string data, ref int from) { // First check if this element is part of an array: if (from < data.Length && data[from - 1] == Constants.START_ARG) { // There is an index given - it may be for an element of the tuple. if (m_value.Tuple == null || m_value.Tuple.Count == 0) { throw new ArgumentException("No tuple exists for the index"); } Parser.Result index = Utils.GetItem(data, ref from, true /* expectInt */); if (index.Value < 0 || index.Value >= m_value.Tuple.Count) { throw new ArgumentException("Incorrect index [" + index.Value + "] for tuple of size " + m_value.Tuple.Count); } return m_value.Tuple[(int)index.Value]; } // This is the case for a simple variable, not an array: return m_value; } private Parser.Result m_value; }
只有 set 变量函数才必须使用分析程序进行注册:
ParserFunction.AddFunction(Constants.SET, new SetVarFunction());
get 变量函数是在 set 变量函数 C# 代码内进行注册(见图 2 中倒数第二行语句):
ParserFunction.AddFunction(varName, new GetVarFunction(varName, varValue));
下面是一些在 CSCS 中获取变量的示例:
append(a, "+ 5"); // a will be equal to the string "2 + 3 + 5"set(b, b * 2); // b will be equal to the number 10 (if it was 5 before)
If、Else If、Else 控制流语句也是作为分析程序函数进行内部实现。它们是使用分析程序进行注册,就像其他任何函数一样:
ParserFunction.AddFunction(Constants.IF, new IfStatement(this));
只有 IF 关键字才必须使用分析程序进行注册。ELSE_IF 和 ELSE 语句将在 IfStatement 实现内部进行处理:
class IfStatement : ParserFunction { protected override Parser.Result Evaluate(string data, ref int from) { m_interpreter.CurrentChar = from; Parser.Result result = m_interpreter.ProcessIf(); return result; } private Interpreter m_interpreter; }
If 语句的真正实现发生在 Interpreter 类中,如图 4 所示。
图 4:实现 If 语句
internal Parser.Result ProcessIf() { int startIfCondition = m_currentChar; Parser.Result result = null; Parser.Result arg1 = GetNextIfToken(); string comparison = Utils.GetComparison(m_data, ref m_currentChar); Parser.Result arg2 = GetNextIfToken(); bool isTrue = EvalCondition(arg1, comparison, arg2); if (isTrue) { result = ProcessBlock(); if (result is Continue || result is Break) { // Got here from the middle of the if-block. Skip it. m_currentChar = startIfCondition; SkipBlock(); } SkipRestBlocks(); return result; } // We are in Else. Skip everything in the If statement. SkipBlock(); int endOfToken = m_currentChar; string nextToken = Utils.GetNextToken(m_data, ref endOfToken); if (ELSE_IF_LIST.Contains(nextToken)) { m_currentChar = endOfToken + 1; result = ProcessIf(); } else if (ELSE_LIST.Contains(nextToken)) { m_currentChar = endOfToken + 1; result = ProcessBlock(); } return result != null ? result : new Parser.Result(); }
明确规定,If 条件必须采用“自变量 1, 比较符号, 自变量 2”形式:
Parser.Result arg1 = GetNextIfToken();string comparison = Utils.GetComparison(m_data, ref m_currentChar); Parser.Result arg2 = GetNextIfToken();bool isTrue = EvalCondition(arg1, comparison, arg2);
可以在其中添加可选的 AND、OR 或 NOT 语句。
EvalCondition 函数仅根据比较符号比较令牌:
internal bool EvalCondition(Parser.Result arg1, string comparison, Parser.Result arg2) { bool compare = arg1.String != null ? CompareStrings(arg1.String, comparison, arg2.String) : CompareNumbers(arg1.Value, comparison, arg2.Value); return compare; }
下面展示了如何实现数字比较:
internal bool CompareNumbers(double num1, string comparison, double num2) { switch (comparison) { case "==": return num1 == num2; case "<>": return num1 != num2; case "<=": return num1 <= num2; case ">=": return num1 >= num2; case "<" : return num1 < num2; case ">" : return num1 > num2; default: throw new ArgumentException("Unknown comparison: " + comparison); } }
字符串比较是类似的,包含在随附的代码下载中(作为 GetNextIfToken 函数的直接实现)。
如果 if、else if 或 else 条件为 true,则代码块中的所有语句都会得到处理。这是在图 5 中 ProcessBlock 方法内实现。如果条件不为 true,则所有语句都会被跳过。这是在 SkipBlock 方法内实现(见随附的源代码)。
图 5:实现 ProcessBlock 方法
internal Parser.Result ProcessBlock() { int blockStart = m_currentChar; Parser.Result result = null; while(true) { int endGroupRead = Utils.GoToNextStatement(m_data, ref m_currentChar); if (endGroupRead > 0) { return result != null ? result : new Parser.Result(); } if (m_currentChar >= m_data.Length) { throw new ArgumentException("Couldn't process block [" + m_data.Substring(blockStart) + "]"); } result = Parser.LoadAndCalculate(m_data, ref m_currentChar, Constants.END_PARSE_ARRAY); if (result is Continue || result is Break) { return result; } } }
请注意“Continue”和“Break”语句在 while 循环中的用法。这些语句也是作为函数实现。下面展示了 Continue 用法:
class Continue : Parser.Result { }class ContinueStatement : ParserFunction { protected override Parser.Result Evaluate(string data, ref int from) { return new Continue(); } }
Break 语句的实现类似。它们均使用分析程序进行注册,就像其他任何函数一样:
ParserFunction.AddFunction(Constants.CONTINUE, new ContinueStatement()); ParserFunction.AddFunction(Constants.BREAK, new BreakStatement());
您可以使用 Break 函数退出嵌套的 If 代码块或 while 循环。
本文内容较多,有兴趣继续学习的,可以看今天另一条推送内容。
版权声明:本站内容全部来自于腾讯微信公众号,属第三方自助推荐收录。《C#中的可自定义脚本(上)》的版权归原作者「微软中国MSDN」所有,文章言论观点不代表Lambda在线的观点, Lambda在线不承担任何法律责任。如需删除可联系QQ:516101458
文章来源: 阅读原文