C# 中的宏预处理器






4.48/5 (13投票s)
2005年4月7日
3分钟阅读

112227

1265
这个库提供了与 C/C++ 预处理器相同的宏替换功能。
引言
C 预处理器是一个有用的工具,允许程序员绕过 C 的限制。它被滥用来做聪明且难以维护的事情,因此 C++ 程序员现在避免使用它,事实上,它已被排除在 C# 等更现代的语言之外。 如果你真的想这样做,你可以放入一个单独的预处理步骤(使用像 CPP 这样的独立预处理器),但那些必须稍后阅读代码的人可能不会感谢你。
我将在此处展示的类执行 C 预处理器的词法替换部分。 您可以添加像 C 宏一样运行的宏,这些宏可以执行诸如“字符串化”和令牌粘贴之类的有用操作。 然后,调用 Substitute()
方法将在扩展所有宏后返回你的字符串。 它并非旨在取代独立的 C 预处理器(这已被重复完成),但有时能够进行复杂的宏替换是很方便的。 我编写这个类作为解释器项目的一部分,它使输入交互式会话更容易。 这也是以交互方式探索 C 宏替换的一种很酷的方式,并展示了 .NET 正则表达式如何使复杂的文本替换更容易。 完整的实现只有 130 行 C# 代码,包括注释!
背景
System.Text.RegularExpressions.Regexp
是一个很棒的类,每个人都应该熟悉它。 在文本中进行智能的、可能与上下文相关的替换时,它非常有用。 想法是找到匹配项,然后根据匹配的值,将匹配项替换为其他文本。 例如,宏的形式参数是在定义它时使用的虚拟变量; 预处理器将找到实际参数,并且必须将它们替换到宏文本中。
这是一个来自 MacroSubstitutor
的例子,我们必须用实际参数替换宏的形式参数。 该代码已略有简化(忽略“#”),以更清楚地显示匹配/替换循环。
public class MacroEntry {
public string Subst;
public string[] Parms;
}
...
static Regex iden = new Regex(@"[a-zA-Z_]\w*");
public string ReplaceParms(MacroEntry me, string[] actual_parms) {
Match m;
int istart = 0;
string subst = me.Subst;
while ((m = iden.Match(subst,istart)) != Match.Empty) {
int idx = Array.IndexOf(me.Parms,m.Value);
int len = m.Length;
if (idx != -1) {
string actual = actual_parms[idx];
subst = iden.Replace(subst,actual,1,istart);
len = actual.Length;
}
istart = m.Index + len;
}
return subst;
}
请注意此处使用的 Regex.Match
和 Regex.Replace
的重载形式。 它们允许你指定匹配和替换开始的起始位置以及实际进行的替换次数(默认情况下,它执行全局替换,这对于上下文相关的替换来说是不好的)。 显然,它可以更快,但对于我需要它做的事情来说已经足够快了,而且(最重要的是)它是简洁且可读的代码。
棘手的方法是 MacroSubstitutor.Substitute
,因为我们必须小心提取传递给宏的参数。 起初,这似乎很容易; 抓住直到 ')' 的字符串,并使用 ',' 分割。 但是考虑 FOR(i,f(x,g(y)))
; 必须仔细计算括号级别,并且只提取级别 1 的参数。 并非所有事情都可以用正则表达式完成,有时循环更明显。
int idx = 0, isi = i;
while (parenDepth > 0) {
if (parenDepth == 1 && (str[i] == ',' || str[i] == ')')) {
actuals[idx] = str.Substring(isi,i - isi);
idx++;
isi = i+1; // past ',' or ')'
}
if (str[i] == '(') parenDepth++; else
if (str[i] == ')') parenDepth--;
i++;
}
有趣的一点是,替换的结果本身会被检查是否有新的宏要替换。 我们从新替换的文本开始我们的下一次匹配。 它的工作方式如下
FOR(i,N) // original string
for(int i = 0; i < (N); i++) // 'FOR' is substituted
for(int i = 0; i < (20); i++) // 'N' is substituted
使用代码
该类使用起来很简单
static void Test() {
MacroSubstitutor ms = new MacroSubstitutor();
ms.AddMacro("FOR","for(int i = 0; i < (n); i++)",new string[]{"i"});
ms.AddMacro("N","20",null);
Console.WriteLine(ms.Substitute("FOR(k,N)"));
}
还有一种更简单的方法,你可以让该类处理宏定义。 这些看起来就像 C 预处理器的 #define
一样,只是我更喜欢 #def
,因为它更容易快速输入。 这是简单交互式预处理器的完整源代码。
class MacroTest {
static string Read() {
Console.Write(">> ");
return Console.In.ReadLine();
}
static void Main() {
MacroSubstitutor ms = new MacroSubstitutor();
string line;
while ((line = Read()) != null)
Console.WriteLine(ms.ProcessLine(line));
}
}
这是一个交互式会话的示例
>> #def N 20
>> #def write Console.WriteLine
>> #def FOR(i,n) for(int i = 0; i < (n); i++)
>> FOR(k,N) write(k);
for(int k = 0; k < (20); k++) Console.WriteLine(k);
>> #def quote(x) #x
>> #def cat(x,y) x##y
>> quote(cat(dog,mouse))
"dogmouse"
有时,你需要自定义查找。 这里的例子是我们希望在替换时用它们的值替换 DATE
和 USER
。 MacroSubstitutor
提供了一个可以被覆盖的 CustomReplacement
方法; 如果符号的宏替换是一个特殊值 MacroEntry.Custom
,它将被调用
class MyMacroSubstitutor : MacroSubstitutor {
public override string CustomReplacement(string s) {
switch(s) {
case "DATE": return DateTime.Now.ToString();
case "USER": return Environment.GetEnvironmentVariable("USERNAME");
}
return "";
}
}
static MacroSubstitutor ms = new MyMacroSubstitutor();
...
ms.AddMacro("DATE",MacroEntry.Custom,null);
ms.AddMacro("USER",MacroEntry.Custom,null);
或者,我们可以在这里使用委托,但一个好的老式虚方法可以很好地完成工作。 当 C# 2.0 广泛可用时,能够使用匿名委托函数编写代码将是一种乐趣
ms.AddMacro("DATE",delegate(string s) {
return DateTime.Now.ToString();
});