为什么有人想写另一种编程语言呢?为什么要用C#呢?

人们常常想当然地认为,你需要一个计算机科学的高级学位—或者需要大量的固执—来编写一个编译器。无论哪种情况,你都会因此而有不少不眠之夜和破碎的关系。这篇文章告诉你如何避免这一切。

以下是编写自己的语言的几个优点。

与其他大多数语言不同,它非常容易修改功能,因为所有的东西都在一个易于理解的标准C#代码中,有一个清晰的界面供添加功能。任何额外的功能都可以用几行代码添加到这种语言中去
这种语言的所有关键字(if、else、while、function等)都可以很容易地被任何非英语的关键字所取代(而且它们不一定是ASCII码,与大多数其他语言相反)。替换关键词只需要改变配置即可。
这种语言既可以作为脚本语言,也可以作为shell程序,就像Unix上的Bash或Windows上的PowerShell(但你要让它比PowerShell更方便使用)。
甚至Python也没有前缀和后缀运算符++和-的杀手锏,”你不需要它们”。有了你自己的语言,你可以自己决定你需要什么。而我将告诉你如何做。
任何自定义的解析都可以在飞行中实现。对解析的完全控制意味着更少的时间去寻找如何使用一个外部包或一个regex库。
你根本就不会使用任何正则表达式! 我相信这是少数人的主要障碍,他们迫切需要解析一个表达式,但又厌恶正则规则所带来的痛苦和羞辱。

这篇文章是基于我在MSDN杂志上发表的两篇文章(见边栏的参考文献)。在第一篇文章中,我描述了解析数学表达式的Split-and-Merge算法,在第二篇文章中,我描述了如何在该算法的基础上编写一种脚本语言。我把这种语言称为CSCS(C#中的自定义脚本)。为了保持一致性,我也将在这篇文章中描述的语言称为CSCS。

在我的第二篇MSDN文章中描述的CSCS语言还不是很成熟。特别是在文章的结尾处,有一节提到了一些通常存在于脚本语言中的重要功能,而CSCS仍然没有。在这篇CODE杂志的文章中,我将对CSCS语言进行概括,并展示如何实现这些缺失的大部分功能以及其他一些功能。

解析语言声明的拆分与合并算法

在这里,我将对分割与合并算法进行概括,不仅可以解析数学表达式,还可以解析任何CSCS语言语句。所有CSCS语句之间必须有一个分隔符。我在Constants.cs文件中把它定义为Constants.END_STATEMENT = ‘;’ 常数。

分割和合并算法包括两个步骤。首先,你将字符串分割成令牌的列表。每个标记由一个数字或一个字符串和一个可以应用于它的动作组成。

对于字符串,动作只能是加号+(字符串连接)或布尔比较,如==、<、>=等,给出一个布尔值作为结果。对于数字,还有一些其他可能的操作,如-、、/、^和%。前缀和后缀运算符++和—以及赋值运算符+=、-=、=等,被视为数字的特殊动作。对于字符串,我只实现了+=赋值运算符,因为我找不到对字符串使用其他赋值运算符的理由。

代号的分离标准是一个动作、括号内的表达式或任何特殊的函数,之前已经在分析器中注册。如果是小括号中的表达式或函数,你将递归地把整个算法应用于小括号中的表达式或带有参数的函数。在第一步结束时,你会有一个单元格的列表,每个单元格由一个动作和一个数字或一个字符串组成。这个动作会应用到下一个单元格。最后一个单元格总是有一个空动作。空动作的优先级是最低的。

第二步是合并第一步中创建的列表中的元素。两个单元格的合并包括将左边的单元格的动作应用于左边和右边的单元格的数字或字符串。只有当左边单元格的动作的优先级大于或等于右边单元格的动作的优先级时,才能进行两个单元格的合并。否则,你首先将右边的单元格与它右边的单元格合并,以此类推,递归地合并,直到你到达列表的末端。

行动的优先级显示在清单1中。如果它们对你的使用没有意义,你可以轻易地改变它们。

清单1:行动的优先次序

  1. private static bool CanMergeCells(Variable leftCell,
  2. Variable rightCell) {
  3. return GetPriority(leftCell.Action) >=
  4. GetPriority(rightCell.Action);
  5. }
  6. private static int GetPriority(string action) {
  7. switch (action)
  8. { case "++":
  9. case "--": return 10;
  10. case "^" : return 9;
  11. case "%" :
  12. case "*" :
  13. case "/" : return 8;
  14. case "+" :
  15. case "-" : return 7;
  16. case "<" :
  17. case ">" :
  18. case ">=":
  19. case "<=": return 6;
  20. case "==":
  21. case "!=": return 5;
  22. case "&&": return 4;
  23. case "||": return 3;
  24. case "+=":
  25. case "=" : return 2;
  26. }
  27. return 0;
  28. }

分割和合并算法的例子

让我们看看如何评估以下表达式:x == “a” || x == “b”。

首先,x必须被解析器注册为一个函数(所有的CSCS变量都被注册并作为函数对待)。因此,当解析器提取标记x时,它认识到它是一个函数,并将其替换为实际的x值,例如,c。

在第一步之后,你会有以下由字符串和动作组成的单元格。(“c”, ==), (“a”, ||), (“c”, ==), (“b”, “) “)。符号”)”表示一个空动作。最后一个单元格总是有一个空动作。

第二步是将所有单元格从左到右逐一合并。因为==的优先级比||的优先级高,所以前两个单元格可以被合并。左边单元格的动作,==,必须被应用,产生的结果是。

  1. Merge(("c", ==), ("a", ||)) =
  2. ("c" == "a", ||) = (0, "||").

你不能将(0, ||)单元格与下一个单元格(“c”, ==)合并,因为根据清单1,||动作的优先级比==的优先级低。所以我们必须首先将(”c”,==)与下一个单元格(”b”,)合并。) 这种合并是可能的,与前面的合并类似。Merge((“c”, ==), (“b”, “)” ) = (0, “)”)。

最后你必须合并两个结果的单元格。

  1. Merge ((0, ||), (0, ")")) =
  2. (0 || 0, )) = (0, ")")

表达式的结果是0(当x=”c “时)。

请看附带的源代码下载中的Split-and-Merge算法的完整实现(在CODE杂志的网站上),在Parser.cs文件中。请看图1中包含解析CSCS语言时使用的所有类的UML图。

image1.png
图1:分析器的UML类图

使用上述带有递归功能的算法,可以解析任何复合表达式。下面是一个CSCS代码的例子。

  1. x = sin(pi*2);
  2. if (x < 0 && log(x + 3*10^2) < 6*exp(x) || x < 1 - pi) {
  3. print("in if, x=", x);
  4. } else {
  5. print("in else, x=", x);
  6. }

上面的CSCS代码片段使用了几个函数:sin, exp, log, 和print。解析器是如何将它们映射到函数中的?

用C#编写自定义函数以用于CSCS代码中

让我们看一个实现Round()函数的例子。首先,你在Constants.cs文件中定义它的名字,如下。

  1. public const string ROUND = "round";

接下来,你向解析器注册该函数的实现。

  1. ParserFunction.AddGlobal(Constants.ROUND,
  2. new RoundFunction());

为了使用配置文件中所有可用的翻译,你还必须在解释器中注册函数名称,这样它就知道它需要向分析器注册所有可能的翻译。

  1. AddTranslation(languageSection, Constants.ROUND);

基本上就是这样,解析器会做剩下的事情。一旦解析器得到Constants.ROUND标记(或来自配置文件的任何翻译),就会调用Round()函数的实现。所有函数的实现都必须派生自ParserFunction类。

  1. class RoundFunction : ParserFunction
  2. {
  3. protected override Variable Evaluate(
  4. string data, ref int from) {
  5. Variable arg = Parser.LoadAndCalculate(
  6. data, ref from, Constants.END_ARG_ARRAY);
  7. arg.Value = Math.Round(arg.Value);
  8. return arg;
  9. }
  10. }

Parser.LoadAndCalculate()是解析器的主要入口点,它完成了解析和计算表达式并返回结果的所有工作。其余函数的实现看起来与 Round() 函数的实现非常相似。

例子:客户端和服务器功能

使用函数,你可以实现任何可以在CSCS语言中使用的东西—只要它可以在C#中实现,就是这样。让我们看一个进程间通信的例子:CSCS中的回声服务器,通过套接字实现。

在Constants.cs中定义服务器和客户端的CSCS函数名称。

  1. public const string CONNECTSRV = "connectsrv";
  2. public const string STARTSRV = "startsrv";

然后你在分析器上注册这些函数。

  1. ParserFunction.AddGlobal(Constants.CONNECTSRV,
  2. new ClientSocket(this));
  3. ParserFunction.AddGlobal(Constants.STARTSRV,
  4. new ServerSocket(this));

请看清单2中ServerSocket的实现。ClientSocket的实现也是类似的。

清单2:回声服务的实现

  1. class ServerSocket : ParserFunction
  2. {
  3. internal ServerSocket(Interpreter interpreter)
  4. m_interpreter = interpreter;
  5. protected override Variable Evaluate(string data,
  6. ref int from)
  7. {
  8. Variable portRes = Utils.GetItem (data, ref from);
  9. Utils.CheckPosInt(portRes);
  10. int port = (int)portRes.Value;
  11. try {
  12. IPHostEntry ipHostInfo = Dns.GetHostEntry(
  13. Dns.GetHostName());
  14. IPAddress ipAddress = ipHostInfo.AddressList[0];
  15. IPEndPoint localEndPoint = new IPEndPoint(ipAddress, port);
  16. Socket listener = new Socket(AddressFamily.InterNetwork,
  17. SocketType.Stream, ProtocolType.Tcp);
  18. listener.Bind (localEndPoint);
  19. listener.Listen(10);
  20. Socket handler = null;
  21. while (true) {
  22. m_interpreter.AppendOutput("Waiting for connections on " +
  23. port + " ...");
  24. handler = listener.Accept();
  25. // Data buffer for incoming data.
  26. byte[] bytes = new byte[1024];
  27. int bytesRec = handler.Receive(bytes);
  28. string received = Encoding.UTF8.GetString(bytes, 0,
  29. bytesRec);
  30. m_interpreter.AppendOutput("Received from " +
  31. handler.RemoteEndPoint.ToString() +
  32. ": [" + received + "]");
  33. byte[] msg = Encoding.UTF8.GetBytes(received);
  34. handler.Send(msg);
  35. if (received.Contains ("<EOF>")) {
  36. break;
  37. }
  38. }
  39. if (handler != null) {
  40. handler.Shutdown (SocketShutdown.Both);
  41. handler.Close ();
  42. }
  43. } catch (Exception exc) {
  44. throw new ArgumentException ("Couldn't start server: (" +
  45. exc.Message + ")");
  46. }
  47. return Variable.EmptyInstance;
  48. }
  49. private Interpreter m_interpreter;
  50. }

图2显示了Mac上一个客户端和一个服务器的运行实例。

image2.png
图2:在Mac上运行一个客户端和一个服务器

任何你想在CSCS中使用的函数都可以在C#中实现。但是你能在脚本语言中,在CSCS本身实现一个函数吗?

在CSCS中编写自定义函数

你用Constants.cs文件中的自定义函数定义来定义自定义函数。

  1. public const string FUNCTION = "function";

为了告诉解析器在看到函数关键字时立即执行特殊代码,你需要向处理程序注册函数处理程序。解释器类就是这样做的。

  1. ParserFunction.AddGlobal(Constants.FUNCTION,
  2. new FunctionCreator(this));

你可以在配置文件中提供对任何语言的翻译,这同样适用于所有其他功能。请看附带的源代码下载中的项目配置文件(在CODE杂志的网站上)。你会在那里找到西班牙语的关键词funci?n。为了使用所有可用的翻译,你还必须在解释器中注册它们。

  1. AddTranslation(languageSection, Constants.FUNCTION);

请看清单3中的Function Creator的实现。

清单3:函数创造者类的实现

  1. class FunctionCreator : ParserFunction
  2. {
  3. internal FunctionCreator(Interpreter interpreter)
  4. {
  5. m_interpreter = interpreter;
  6. }
  7. protected override Variable Evaluate(
  8. string data, ref int from)
  9. {
  10. string funcName = Utils.GetToken(data, ref from,
  11. Constants.TOKEN_SEPARATION);
  12. m_interpreter.AppendOutput("Registering function [" +
  13. funcName + "] ...");
  14. string[] args = Utils.GetFunctionSignature(data, ref from);
  15. if (args.Length == 1 && string.IsNullOrWhiteSpace(args[0]))
  16. {
  17. args = new string[0];
  18. }
  19. Utils.MoveForwardIf(data, ref from,
  20. Constants.START_GROUP, Constants.SPACE);
  21. string body = Utils.GetBodyBetween(data, ref from,
  22. Constants.START_GROUP, Constants.END_GROUP);
  23. CustomFunction customFunc = new CustomFunction(funcName,
  24. body, args);
  25. ParserFunction.AddGlobal(funcName, customFunc);
  26. return new Variable(funcName);
  27. }
  28. private Interpreter m_interpreter;
  29. }

它创建了另一个函数并将其注册到解析器中。

  1. CustomFunction customFunc = new CustomFunction(
  2. funcName, body, args)。
  3. ParserFunction.AddGlobalfuncName, customFunc)。

要注册的自定义函数的名称是funcName。解析器希望带有函数名称的标记是函数标记后的下一个。逗号将令牌分开。

你在CSCS代码中实现的所有函数都对应于C# CustomFunction类的不同实例。

在解析过程中,只要解析器遇到funcName标记,它就会调用它的处理程序,即CustomFunction,所有的动作都在这里发生。你可以在清单4中看到CustomFunction的实现。

清单4:自定义函数类的实现

  1. class CustomFunction : ParserFunction
  2. {
  3. internal CustomFunction(string funcName,
  4. string body, string[] args)
  5. {
  6. m_name = funcName;
  7. m_body = body;
  8. m_args = args;
  9. }
  10. protected override Variable Evaluate(string data,
  11. ref int from)
  12. {
  13. bool isList;
  14. List<Variable> functionArgs = Utils.GetArgs(data,
  15. ref from, Constants.START_ARG, Constants.END_ARG,
  16. out isList);
  17. Utils.MoveBackIf(data, ref from, Constants.START_GROUP);
  18. if (functionArgs.Count != m_args.Length) {
  19. throw new ArgumentException("Function [" + m_name +
  20. "] arguments mismatch: " + m_args.Length + " declared, " +
  21. functionArgs.Count + " supplied");
  22. }
  23. // 1. Add passed arguments as local variables to the Parser.
  24. StackLevel stackLevel = new StackLevel(m_name);
  25. for (int i = 0; i < m_args.Length; i++) {
  26. stackLevel.Variables[m_args[i]] = new GetVarFunction(
  27. functionArgs[i]);
  28. }
  29. ParserFunction.AddLocalVariables(stackLevel);
  30. // 2. Execute the body of the function.
  31. int temp = 0;
  32. Variable result = null;
  33. while (temp < m_body.Length - 1)
  34. {
  35. result = Parser.LoadAndCalculate(m_body, ref temp,
  36. Constants.END_PARSE_ARRAY);
  37. Utils.GoToNextStatement(m_body, ref temp);
  38. }
  39. ParserFunction.PopLocalVariables();
  40. return result;
  41. }
  42. private stringm_body;
  43. private string[] m_args;
  44. }

自定义函数做两件事。首先,它提取函数参数并将它们作为局部变量添加到解析器中(一旦函数执行完毕或抛出异常,它们就会从解析器中删除)。

第二,评估函数的主体,如果主体包含对其他函数的调用,则使用主解析器的入口点LoadAndCalculate()方法,或者对其本身的调用,对CustomFunction的调用可以是递归的。让我们通过阶乘的例子来看看这个问题。

例子:阶乘

阶乘符号是n!,它的定义如下。0! = 1, n! = 1 2 3 n.

在这个符号中,n必须是一个非负的整数。因此,它可以被递归定义为:n!=1 2 3 (n - 1) n = (n - 1)! n.

在CSCS中,代码是这样的。

  1. function factorial(n) {
  2. if (!isInteger(n)) {
  3. exc = "Factorial is for integers only (n="+n+")";
  4. throw (exc);
  5. }
  6. if (n < 0) {
  7. exc = "Negative number (n="+n+") for factorial";
  8. throw (exc);
  9. }
  10. if (n <= 1) {
  11. return 1;
  12. }
  13. return n * factorial(n - 1);
  14. }

上面的阶乘函数使用了一个辅助的isInteger()函数。

  1. function isInteger(candidate) {
  2. return candidate == round(candidate);
  3. }

isInteger()函数调用了另一个round()函数。round()函数的实现并不在CSCS中,而是在你在上一节看到的C#代码中已经存在。

用不同的参数执行阶乘函数,会有以下输出。

  1. .../Documents/cscs/cscs/bin/Debug>> a = factorial(-1)
  2. Negative number (n=-1) for factorial
  3. .../Documents/cscs/cscs/bin/Debug>> a = factorial(1.5)
  4. Factorial is for integers only (n=1.5)
  5. .../Documents/cscs/cscs/bin/Debug>> a = factorial(6)
  6. 720

阶乘代码中包含一些 throw() 语句。这表明应该有一些东西能够捕获它们。

抛出、尝试和捕获控制流语句

try()和throw()控制流语句可以用函数的方式实现,就像你在上面看到的Round()函数的实现一样。

这两个函数也必须先在解析器中注册。

  1. public const string TRY = "try";
  2. public const string THROW = "throw";

throw()函数的实现如下。

  1. class ThrowFunction : ParserFunction
  2. {
  3. protected override Variable Evaluate(
  4. string data, ref int from)
  5. {
  6. // 1. Extract what to throw.
  7. Variable arg = Utils.GetItem(data, ref from);
  8. // 2. Convert it to a string.
  9. string result = arg.AsString();
  10. // 3. Throw it!
  11. throw new ArgumentException(result);
  12. }
  13. }

try函数需要更多的工作,所以把所有的工作委托给解释器更容易,解释器可以告诉解析器要做什么。

  1. class TryBlock : ParserFunction
  2. {
  3. internal TryBlock(Interpreter interpreter)
  4. {
  5. m_interpreter = interpreter;
  6. }
  7. protected override Variable Evaluate(
  8. string data, ref int from)
  9. {
  10. return m_interpreter.ProcessTry(data, ref from);
  11. }
  12. private Interpreter m_interpreter;
  13. }

在Interpreter.ProcessTry()的实现中,首先你应该注意你开始处理的地方(所以以后你可以返回跳过整个try-catch块)。然后,你对try块进行处理,如果抛出了异常,你就捕捉它。在解析器代码中,你只抛出ArgumentException异常。

  1. int startTryCondition = from - 1;
  2. int currentStackLevel =
  3. ParserFunction.GetCurrentStackLevel();
  4. Exception exception = null;
  5. Variable result = null;
  6. try {
  7. result = ProcessBlock(data, ref from);
  8. }
  9. catch(ArgumentException exc) {
  10. exception = exc;
  11. }

如果有一个异常,或者有一个catch或break语句,你需要跳过整个catch块。为此,请回到尝试块的开头,然后跳过它。

  1. if (exception != null ||
  2. result.Type == Variable.VarType.BREAK ||
  3. result.Type == Variable.VarType.CONTINUE)
  4. {
  5. from = startTryCondition;
  6. SkipBlock(data, ref from);
  7. }

在try块之后,你希望有一个catch标记和要捕获的异常名称,不管这个异常是否被抛出。

  1. string catchToken = Utils.GetNextToken(data, ref from);
  2. from++; // skip opening parenthesis
  3. // The next token after the try must be a catch.
  4. if (!Constants.CATCH_LIST.Contains(catchToken))
  5. {
  6. throw new ArgumentException(
  7. "Expecting a 'catch()' but got [" +
  8. catchToken + "]");
  9. }
  10. string exceptionName = Utils.GetNextToken(data, ref from);
  11. from++; // skip closing parenthesis

为什么要用CATCH_LIST来查看是否有catch这个关键词,而不是只用Constants.CATCH = “catch”?因为CATCH_LIST包含了catch关键字在不同语言中的所有可能的翻译。你在配置文件中提供了它们。例如,你可以在西班牙语中使用atrapar,或者在德语中使用fangen。

在发生异常的情况下,你必须处理catch块。你首先创建一个异常堆栈(从什么地方调用了什么),然后将这些信息添加到异常变量中,可以在捕获表达的CSCS代码中使用。

  1. if (exception != null) {
  2. string excStack = CreateExceptionStack(
  3. currentStackLevel);
  4. ParserFunction.InvalidateStacksAfterLevel(
  5. currentStackLevel);
  6. GetVarFunction excFunc = new GetVarFunction(
  7. new Variable(Double.NaN,
  8. exception.Message + excStack));
  9. ParserFunction.AddGlobalOrLocalVariable(
  10. exceptionName, excFunc);
  11. result = ProcessBlock(data, ref from);
  12. ParserFunction.PopLocalVariable(
  13. exceptionName);
  14. }

如果没有异常,就跳过catch块。

  1. else {
  2. SkipBlock(data, ref from)
  3. }

让我们用上面看到的阶乘函数来尝试抛出和捕获异常的操作。你使用下面的CSCS代码,它有一些人为创建的执行堆栈来抛出异常。

  1. function trySuite(n) {
  2. print("Trying to calculate the",
  3. "negative factorial?");
  4. result = tryNegative(n);
  5. return result;
  6. }
  7. function tryNegative(n) {
  8. return factorial(-1 * n);
  9. }
  10. try {
  11. f = tryNegative(5);
  12. print("factorial(", n, ")=", f);
  13. } catch(exc) {
  14. print ("Caught Exception: ", exc);
  15. }

运行后,你会得到以下异常信息。

  1. Trying to calculate negative factorial...
  2. Caught Exception: Negative number (n=-5)
  3. for factorial at
  4. factorial()
  5. tryNegative()
  6. trySuite()

当然,这只是一个赤裸裸的异常处理,所以你可能想添加一些更高级的东西,比如在函数的哪一行抛出异常,函数参数,等等。

你是如何跟踪执行栈的?就是说,被调用的函数的情况?在 ParserFunctions 中,你定义了以下静态变量。

  1. public class StackLevel
  2. {
  3. public StackLevel(string name = null) {
  4. Name = name;
  5. Variables = new Dictionary<string,
  6. ParserFunction> ();
  7. }
  8. public string Name { get; set; }
  9. public Dictionary<string, ParserFunction> Variables
  10. { get; set; }
  11. }
  12. private static Stack<StackLevel> s_locals =
  13. new Stack<StackLevel>();

每个StackLevel由正在执行的函数的所有局部变量(包括传入的参数)和函数名称组成。这是你在异常堆栈中看到的名字。

每次你开始执行一个新的函数(不管它是在C#代码中还是在CSCS代码中定义的),一个新的StackLevel被添加到s_locals栈中。每当你完成一个函数的执行时,你就从s_locals数据结构中弹出一个StackLevel。

在例子中,你看到了一些用CSCS实现的函数。所有的脚本都必须在同一个文件中吗?你可以包括包含CSCS代码的其他文件吗?

包括含有CSCS代码的其他文件

要包括另一个包含CSCS脚本的模块,你要使用与所有其他函数相同的函数方法,就像你使用Round()或try/throw控制语句一样。include关键字也在Constants.cs中定义。

  1. public const string INCLUDE = "include";

该函数的实现是在IncludeFile类中,该类衍生于ParserFunction类。

  1. ParserFunction.AddGlobal(Constants.INCLUDE,
  2. new IncludeFile());

在CSCS代码中,包括另一个文件看起来像这样。

  1. include("filename.cscs");

一旦解析器得到 INCLUDE 标记(或其翻译之一),就会触发 IncludeFile.Evaluate() 的执行。这个函数必须首先从要包含的文件中提取实际的脚本。

  1. class IncludeFile : ParserFunction
  2. {
  3. protected override Variable Evaluate(
  4. string data, ref int from)
  5. {
  6. string filename = Utils.ResultToString(
  7. Utils.GetItem(data, ref from));
  8. string[] lines = Utils.GetFileLines(filename);
  9. string includeFile = string.Join(
  10. Environment.NewLine, lines);
  11. string includeScript =
  12. Utils.ConvertToScript(includeFile);

然后你用解析器的主方法LoadAndCalculate()来处理整个脚本。注意,在最后,你会返回一个空的结果,因为完成后没有什么可以返回。

  1. int filePtr = 0;
  2. while (filePtr < includeScript.Length)
  3. {
  4. Parser.LoadAndCalculate(includeScript,
  5. ref filePtr, Constants.END_LINE_ARRAY);
  6. Utils.GoToNextStatement(includeScript,
  7. ref filePtr);
  8. }
  9. return Variable.EmptyInstance;
  10. }
  11. }

在Include语句完成后,所有从包含的文件中添加的全局函数都留在解析器中。

你可以用实现包括文件的方法来实现if、when、for和其他控制流语句。我没有实现for循环,因为它的功能可以用while循环轻松实现。下面是在CSCS代码中这样一个替换for循环的例子。

  1. i = 0;while (i++ < 10) {
  2. if (i % 2 == 0) {
  3. print (i, " is even.");
  4. } else {
  5. print (i, " is odd.");
  6. }
  7. }

试着猜一猜:在上面的CSCS代码中,有多少个标记是作为函数(即从ParserFunction类派生出来的类)实现的?有四个:while()、if()、print()和`++``。Else本身不是一个函数,它和if一起处理(同样,catch也不是一个单独的函数,而是和try一起处理)。

while()语句中的i++标记是怎么回事?它是如何实现的?

实现++和-前缀和后缀以及复合赋值运算符

你可以对赋值使用与包含文件相同的方法,例如,将其实现为一个函数set()。这就是我在MSDN杂志中描述的第一版语言中实现赋值的方法(链接见侧边栏)。

赋值a = 5等同于set(a, 5),前缀运算符++i等同于set(i, i + 1)。后缀运算符i++更长一些:i++在CSCS中相当于set(i, i + 1) - 1。

显然,一个拥有如此尴尬的赋值运算符的语言不可能成为编程语言的英超联赛的一部分。你需要一种不同的方法来实现适当的赋值操作。

我决定采取以下方法。声明动作函数,所有的动作函数都派生自抽象的ActionFunction类(该类派生自ParserFunction类)。只要解析器得到以下任何一个动作标记,就会触发一个动作函数。++, —, +=, -=, *=, 等等。如果是++和—,你首先需要找到它是前缀还是后缀运算符—解析器会知道这一点。如果是前缀,它将在动作前有一个未处理的标记。

所有的动作都必须先在分析器上注册。

  1. ParserFunction.AddAction(Constants.ASSIGNMENT,
  2. new AssignFunction());
  3. ParserFunction.AddAction(Constants.INCREMENT,
  4. new IncrementDecrementFunction());
  5. ParserFunction.AddAction(Constants.DECREMENT,
  6. new IncrementDecrementFunction());

请看清单5中IncrementDecrementFunction()的实现。其他动作函数的实现是类似的。正如你所看到的,解析器从上下文中知道它是用前缀还是后缀运算符工作的,以及它是由于 — 还是 ++ 动作而被触发的。请注意,在最后,该函数要么返回当前的变量值(如果是前缀),要么返回前一个值(如果是后缀)。

清单5:++和-运算符的实现

  1. protected override Variable Evaluate(string data,
  2. ref int from)
  3. {
  4. bool prefix = string.IsNullOrWhiteSpace(m_name);
  5. if (prefix)
  6. {// If it's a prefix we do not have variable name yet.
  7. m_name = Utils.GetToken(data, ref from,
  8. Constants.TOKEN_SEPARATION);
  9. }
  10. // Value to be added to the variable:
  11. int valueDelta = m_action == Constants.INCREMENT ? 1 : -1;
  12. int returnDelta = prefix ? valueDelta : 0;
  13. // Check if the variable to be set has the form of x(0),
  14. // meaning that this is an array element.
  15. double newValue = 0;
  16. int arrayIndex = Utils.ExtractArrayElement(ref m_name);
  17. bool exists = ParserFunction.FunctionExists(m_name);
  18. if (!exists)
  19. {
  20. throw new ArgumentException("Variable [" + m_name +
  21. "] doesn't exist");
  22. }
  23. Variable currentValue = ParserFunction.GetFunction(m_name)
  24. GetValue(data, ref from);
  25. if (arrayIndex >= 0)
  26. {// A variable with an index (array element).
  27. if (currentValue.Tuple == null)
  28. {
  29. throw new ArgumentException("Tuple [" + m_name +
  30. "] doesn't exist");
  31. }
  32. if (currentValue.Tuple.Count <= arrayIndex)
  33. {
  34. throw new ArgumentException("Tuple [" + m_name +
  35. "] has only " + currentValue.Tuple.Count + " elements");
  36. }
  37. newValue = currentValue.Tuple[arrayIndex].Value + returnDelta;
  38. currentValue.Tuple[arrayIndex].Value += valueDelta;
  39. }
  40. else // A normal variable.
  41. {
  42. newValue = currentValue.Value + returnDelta;
  43. currentValue.Value += valueDelta;
  44. }
  45. Variable varValue = new Variable(newValue);
  46. ParserFunction.AddGlobalOrLocalVariable(m_name,
  47. new GetVarFunction(currentValue));
  48. return varValue;
  49. }

通过这种方法,你可以在CSCS中玩转assignments,如下所示。

  1. a = 1;
  2. b = a++ - a--; // b = -1, a = 1
  3. c = a = (b += 1); // a = b = c = 0
  4. a -= ++c; // c = 1, a = -1
  5. c = --a - ++a; // a = -1, c = -1

清单5中有一个关于数组的部分。我还没有谈及它们。赋值是如何与数组一起工作的?

Arrays

数组的声明与CSCS中变量的声明不同。要声明一个数组并用数据初始化它,你可以使用相同的语句。作为一个例子,下面是CSCS的代码。

  1. a = 20;
  2. arr = {++a-a--, ++a*exp(0)/a--,
  3. -2*(--a - ++a), ++a};i = 0;
  4. while(i < size(arr)) {
  5. print("a[", i, "]=", arr[i],
  6. ", expecting ", i);
  7. i++;
  8. }

数组中的元素数没有明确声明,因为它可以从赋值中推导出来。

函数size()被实现为一个典型的CSCS函数,返回数组中的元素数。但是如果传递的参数不是一个数组,它就会返回其中的字符数。

在内部,数组被实现为一个C#列表,所以你可以在运行中向其添加元素。

你可以通过使用方括号来访问数组的元素,或者修改它们。如果你访问一个数组的元素,而这个元素还没有被初始化,解析器就会抛出一个异常。然而,有可能只给一个数组中的一个元素赋值,即使使用的索引大于数组中的元素数。在这种情况下,数组中不存在的元素被初始化为空值。即使这是该数组的第一次赋值,也会发生这种情况。对于这种特殊的捷径数组赋值,CSCS函数set()被使用。

  1. i = 10;while(--i > 0) {
  2. newarray[i] = 2*i;
  3. }
  4. print("newarray[9]=", newarray[9]); // 18
  5. print("size(newarray)=", size(newarray)); // 10

在附带的源代码下载中查看阵列的实现(可通过CODE杂志网站获得)。

在各种操作系统上进行编译

人们普遍认为C#只适用于Windows,这是一种误解。人们可能听说过一些将其移植到其他操作系统的尝试,但大多数人认为这些尝试仍处于某种工作的进展中。

在使用Xamarin Studio for Mac一段时间后,我发现它并不是一个正在进行的工作,而是一个在Mac上构建和运行C#应用程序的非常强大的工具。而且它是免费的! 底层的免费和开源的Mono项目目前支持.NET 4.5与C# 5.0。最近,微软宣布计划收购Xamarin,所以支持应该会继续下去,希望Mac版Xamarin Studio能保持免费(你也可以用C#与Xamarin进行iOS和Android编程,但这已经不是免费的了)。

Xamarin不支持任何Windows Forms,但与核心C#语言有关的是,在从Windows移植我的Visual Studio项目时,我不需要改变任何一行的代码。我必须要改变的是配置文件。对于Windows来说,配置看起来像这样。

  1. <configuration>
  2. <configSections>
  3. <section name="Languages" type=
  4. "System.Configuration.NameValueSectionHandler"/>
  5. <section name="Synonyms" type=
  6. "System.Configuration.NameValueSectionHandler"/>

不幸的是,这在Xamarin中是行不通的。在那里你必须在类型中加入System。

  1. <configuration>
  2. <configSections>
  3. <section name="Languages" type=
  4. "System.Configuration.
  5. NameValueSectionHandler,System"/>
  6. <section name="Synonyms" type=
  7. "System.Configuration.
  8. NameValueSectionHandler,System"/>

这种配置在Visual Studio中无法使用,所以你必须在Visual Studio中使用与Xamarin不同的配置文件。

如果你想在Windows和Mac OS上使用你的语言时有不同的代码,还有一种方法可以使用C#宏。如果你用文件系统工作,这尤其有意义。

在Windows和Mac OS上实现目录列表

Xamarin Studio使用Mono框架,如果你想知道你是否在使用Mono,要使用的宏是#ifdef MonoCS。你可以在这里看到它在工作。

  1. public static string GetPathDetails(FileSystemInfo fs,
  2. string name)
  3. {
  4. string pathname = fs.FullName;
  5. bool isDir = (fs.Attributes &
  6. FileAttributes.Directory) != 0;
  7. #if __MonoCS__
  8. Mono.Unix.UnixFileSystemInfo info;
  9. if (isDir) {
  10. info = new Mono.Unix.UnixDirectoryInfo(pathname);
  11. } else {
  12. info = new Mono.Unix.UnixFileInfo(pathname);
  13. }

在上面的代码片段中,你看到了Unix特定的代码,以获得目录或文件数据结构。使用这个结构,很容易找到用户/组/其他的典型Unix权限,这在Windows上是没有意义的。

  1. char ur = (info.FileAccessPermissions & Mono.Unix.
  2. FileAccessPermissions.UserRead) != 0 ? 'r' : '-';
  3. char uw = (info.FileAccessPermissions & Mono.Unix.
  4. FileAccessPermissions.UserWrite) != 0 ? 'w' : '-';
  5. char ux = (info.FileAccessPermissions & Mono.Unix.
  6. FileAccessPermissions.UserExecute) != 0 ? 'x' : '-';
  7. char gr = (info.FileAccessPermissions & Mono.Unix.
  8. FileAccessPermissions.GroupRead) != 0 ? 'r' : '-';
  9. ...
  10. string permissions = string.Format(
  11. "{0}{1}{2}{3}{4}{5}{6}{7}{8}",
  12. ur, uw, ux, gr, gw, gx, or, ow, ox);

看看清单6,看看GetPathDetails()函数的完整实现。

清单6:在Unix和Windows上获取目录或文件信息

  1. public static string GetPathDetails(FileSystemInfo fs, string name)
  2. {
  3. string pathname = fs.FullName;
  4. bool isDir = (fs.Attributes & FileAttributes.Directory) != 0;
  5. char d = isDir ? 'd' : '-';
  6. string last = fs.LastAccessTime.ToString("MMM dd yyyy HH:mm");
  7. #if __MonoCS__
  8. Mono.Unix.UnixFileSystemInfo info;
  9. if (isDir) {
  10. info = new Mono.Unix.UnixDirectoryInfo(pathname);
  11. } else {
  12. info = new Mono.Unix.UnixFileInfo(pathname);
  13. }
  14. char ur = (info.FileAccessPermissions &
  15. Mono.Unix.FileAccessPermissions.UserRead) != 0 ? 'r' : '-';
  16. char uw = (info.FileAccessPermissions &
  17. Mono.Unix.FileAccessPermissions.UserWrite) != 0 ? 'w' : '-';
  18. char ux = (info.FileAccessPermissions &
  19. Mono.Unix.FileAccessPermissions.UserExecute) != 0 ? 'x' : '-';
  20. char gr = (info.FileAccessPermissions &
  21. Mono.Unix.FileAccessPermissions.GroupRead) != 0 ? 'r' : '-';
  22. char gw = (info.FileAccessPermissions &
  23. Mono.Unix.FileAccessPermissions.GroupWrite) != 0 ? 'w' : '-';
  24. char gx = (info.FileAccessPermissions &
  25. Mono.Unix.FileAccessPermissions.GroupExecute) != 0 ? 'x' : '-';
  26. char or = (info.FileAccessPermissions &
  27. Mono.Unix.FileAccessPermissions.OtherRead) != 0 ? 'r' : '-';
  28. char ow = (info.FileAccessPermissions &
  29. Mono.Unix.FileAccessPermissions.OtherWrite) != 0 ? 'w' : '-';
  30. char ox = (info.FileAccessPermissions &
  31. Mono.Unix.FileAccessPermissions.OtherExecute) != 0 ? 'x' : '-';
  32. string permissions = string.Format("{0}{1}{2}{3}{4}{5}{6}{7}{8}",
  33. ur, uw, ux, gr, gw, gx, or, ow, ox);
  34. string user = info.OwnerUser.UserName;
  35. string group = info.OwnerGroup.GroupName;
  36. string links = info.LinkCount.ToString();
  37. long size = info.Length;
  38. if (info.IsSymbolicLink) {
  39. d = 's';
  40. }
  41. #else
  42. string user = string.Empty;
  43. string group = string.Empty;
  44. string links = null;
  45. string permissions = "rwx";
  46. long size = 0;
  47. if (isDir)
  48. {
  49. user = Directory.GetAccessControl(fs.FullName).GetOwner(
  50. typeof(System.Security.Principal.NTAccount)).ToString();
  51. DirectoryInfo di = fs as DirectoryInfo;
  52. size = di.GetFileSystemInfos().Length;
  53. }
  54. else {
  55. user = File.GetAccessControl(fs.FullName).GetOwner(
  56. typeof(System.Security.Principal.NTAccount)).ToString();
  57. FileInfo fi = fs as FileInfo;
  58. size = fi.Length;
  59. string[] execs = new string[] { "exe", "bat", "msi"};
  60. char x = execs.Contains(fi.Extension.ToLower()) ? 'x' : '-';
  61. char w = !fi.IsReadOnly ? 'w' : '-';
  62. permissions = string.Format("r{0}{1}", w, x);
  63. }
  64. #endif
  65. string infoStr = string.Format(
  66. "{0}{1} {2,4} {3,8} {4,8} {5,9} {6,23} {7}",
  67. d, permissions, links, user, group, size, last, name);
  68. return infoStr;
  69. }

图3显示在Mac上运行ls命令,图4显示在PC上运行dir命令。

image3.png
图3:在Mac上运行CSCS ls命令

图4:在PC上运行CSCS dir命令

现在你可能想到的问题是。”我如何配置使用Mac上的ls命令和PC上的dir命令来实现同一个CSCS功能?”

不同语言的关键词

如果你想让一个关键词在任何其他语言中使用,你必须添加一个代码,这样就可以从配置文件中读取可能的翻译。例如,我为一个显示目录内容的函数定义了关键词,如下所示。

  1. public const string DIR = "dir";

现在,如果我想要这个关键词的可能翻译,我就在解释器的初始化代码中添加。

  1. AddTranslation(languageSection, Constants.DIR);

因为ls并不是真的从dir翻译成外语,所以我增加了同义词配置部分,以平滑Windows和Mac概念之间的差异,使它们看起来不那么陌生。

  1. <Languages> <add key="languages" value=
  2. "Synonyms,Spanish,German,Russian" />
  3. </Languages>
  4. <Synonyms>
  5. <add key="del" value ="rm" />
  6. <add key="move" value ="mv" />
  7. <add key="copy" value ="cp" />
  8. <add key="dir" value ="ls" />
  9. <add key="read" value ="scan" />
  10. <add key="writenl" value ="print" />
  11. </Synonyms>

使用相同的配置文件,你可以为CSCS关键字添加任何语言的翻译。下面是一个有效的CSCS代码,使用德语关键词检查一个数字是奇数还是偶数。

  1. ich = 0;
  2. solange (ich++ < 10) {
  3. falls (ich % 2 == 0) {
  4. drucken (ich, " Gerade Zahl");
  5. } sonst {
  6. drucken (ich, " Ungerade Zahl");
  7. }
  8. }

结束语

使用本文介绍的技术和查阅附带的源代码下载,你可以使用你自己的关键字和函数开发你自己的完全定制的语言。由此产生的语言将在运行时直接被逐个语句解释。

向语言添加新功能的直接方法如下。

  • 想一想要用英文实现的函数的英文关键词(主要名称)。 ```csharp public const string ROUND = “round”;
  1. - 将这个关键字映射到一个C#类,并将它们都注册到解析器中。
  2. ```csharp
  3. ParserFunction.AddGlobal(Constants.ROUND,
  4. new RoundFunction());
  • 使之能够从配置文件中读取该关键词的任何语言的翻译。 ```csharp AddTranslation(languageSection, Constants.ROUND);

```

  • 实现上面用解析器注册的类。该类必须派生自ParserFunction类,你必须覆盖Evaluate()方法。

就是这样:使用上述技术,你不仅可以实现典型的函数,如round()、sin()、abs()、sqrt()等,还可以实现大多数控制流语句,如if()、while()、break、return、continue、throw()、include()等等。所有在CSCS代码中声明的变量都以同样的方式实现:你把它们作为函数在解析器中注册。

你可以把这种语言作为一种shell语言来执行不同的文件或操作系统命令(查找文件、列出目录或运行中的进程、从命令行中杀死或启动一个新的进程,等等)。或者你可以把它作为一种脚本语言,编写任何任务并把它们添加到脚本中去执行。基本上,任何任务都可以在CSCS中实现,只要有可能在C#中实现它。

有一些东西还远未完善,可以添加到CSCS语言中。例如,调试的可能性几乎为零。异常处理是非常基本的,所以增加异常堆栈的可能性以显示异常发生的代码行将是非常有趣的。另外,目前CSCS中只支持列表数据结构(我称之为元组)。增加使用其他数据结构的可能性,例如字典,也将是一个有趣的练习:让我知道你想出了什么办法

参考文献

Customizable Scripting in C#: https://msdn.microsoft.com/en-us/magazine/mt632273.aspx
A Split-and-Merge Expression Parser in C#: https://msdn.microsoft.com/en-us/magazine/mt573716.aspx
Interpreters: https://en.wikipedia.org/wiki/Interpreter_
(computing) Xamarin Studio for Mac: https://xamarin.com/studio

原文章地址:https://www.codemag.com/article/1607081