发布于2026-07-23 阅读(0)
扫一扫,手机访问
在 .NET 生态中,ANTLR4 是一个相当好用的语法分析器生成工具,能帮你快速搭建编译器、解释器这类东西。这篇内容会从环境搭建、语法规则编写、生成分析器,到实际调用,完整走一遍基本流程。受篇幅限制,ANTLR4 的语法规则不会深入展开,更详细的说明可以去看官方文档或者相关资料。
例子都比较简单,也没有做严格测试,目的只是演示操作路径。实际开发中,有个不错的习惯——先去官方的 grammars-v4 仓库看看是不是已经有现成的 grammar 文件可以用:https://github.com/antlr/grammars-v4
代码示例已经传到了 GitHub:https://github.com/eventhorizon-cli/Antlr4Demo
ANTLR(全称 Another Tool for Language Recognition)是一个老牌的语法分析器生成器,属于编译技术里的前端工具。用它写编译器、解释器、翻译器都很顺手。
当前版本 ANTLR4 已经支持生成 Ja va、C#、Python、Ja vaScript 等多种语言的语法分析器。它的语法规则定义方式有点类似正则表达式,描述复杂语法结构时非常灵活。
它的工作流程大致分四步:
语法分析可以分成两个阶段:词法分析和语法分析。
1 + 2 拆成 1、+、2 三个 token举个例子,表达式 1 + 2 * 3 的 AST 长这样:
+ / \ 1 * / \ 2 3
下面用加减乘除四则运算来演示完整流程。先新建一个 C# 项目,然后添加包:
dotnet add package Antlr4.Runtime.Standard
新建一个 .g4 文件,比如 Arithmetic.g4,内容如下:
grammar Arithmetic; // grammar name 需要和文件名一致
// 语法规则
// op=('*'|'/') 表示 op 将 ‘*' 或者 ‘/' 标记为一个操作符号
// # MulDiv 将这个规则命名为 MulDiv,访问 AST 时会用到
expr:
expr op=('*'|'/') expr # MulDiv
| expr op=('+'|'-') expr # AddSub
| INT # Int
| '(' expr ')' # Parens
;
// 词法规则
INT : [0-9]+ ;
WS : [ \t\r\n]+ -> skip ; // 表示忽略空格

.g4 文件由两部分组成:词法规则(通常大写开头)和语法规则(通常小写开头)。词法规则定义 token,比如数字、运算符;语法规则定义语法结构,比如表达式、语句。
上面的例子定义了一个简单的四则运算语法,支持加减乘除和括号。整数 token 用 INT 表示(一个或多个数字)。expr 规则中用 | 分隔可选分支,乘除法写在加减法前面,这样乘除法优先级更高。每个分支可以用 # 命名,比如 # MulDiv,在 Visitor 访问时会用到这个名字。规则支持递归,比如乘法规则里又引用了 expr。
这个例子比较简单,所以词法和语法写在一个文件里。复杂项目可以把它们分开。
安装 ANTLR4 的插件(Rider 或 VS Code)后,可以检查语法规则是否正确。在 Rider 中选中 expr 规则,右键选择 Test Rule expr,输入测试表达式就能看到树形结构的结果。


ANTLR4 本身基于 Ja va,所以需要安装 Ja va 运行环境才能使用它的工具。有两种方式生成分析器,推荐使用 Antlr4BuildTasks 自动生成。
方式一:直接使用 ANTLR4 官方工具
从官网下载 jar 包(本文写作时最新版 4.13.2):
https://www.antlr.org/download/antlr-4.13.2-complete.jar
下载后放到 .g4 文件所在目录,然后运行:
ja va -jar antlr-4.13.2-complete.jar -Dlanguage=CSharp Arithmetic.g4
-Dlanguage=CSharp 表示生成 C# 语言的代码。执行后会生成 ArithmeticLexer.cs、ArithmeticParser.cs 等文件,然后就可以在项目里使用了。


方式二:借助 Antlr4BuildTasks 自动生成
手动方式比较繁琐,推荐使用 Antlr4BuildTasks 这个 MSBuild 任务,它可以自动下载 ANTLR4 工具、生成分析器代码,并添加到项目中。它也会尝试下载 Ja va 运行环境,如果构建出错,可以手动安装全局 Ja va。
项目文件(.csproj)配置如下:
Exe net9.0 enable enable
表示项目里所有 .g4 文件都会参与编译。构建之后,生成的代码会放在 obj 文件夹中,每次构建都会重新生成,不需要手动添加。

访问 AST 的方式有两种:Visitor 和 Listener。ANTLR4 会为每个 .g4 文件生成对应的 BaseVisitor 和 BaseListener,咱们只需要继承并重写相关方法即可。
继续用四则运算的例子。ANTLR4 为我们生成了一个 ArithmeticBaseVisitor 泛型基类,对应 .g4 中命名的规则(MulDiv、AddSub、Int、Parens),基类中有 VisitMulDiv、VisitAddSub 等方法。我们写一个自定义 Visitor 重写它们:
public class ArithmeticVisitor : ArithmeticBaseVisitor{ // 解析乘除法 public override int VisitMulDiv(ArithmeticParser.MulDivContext context) { int left = Visit(context.expr(0)); int right = Visit(context.expr(1)); return context.op.Text switch { "*" => left * right, "/" => left / right, _ => throw new NotSupportedException($"Operator {context.op.Text} is not supported.") }; } // 解析加减法 public override int VisitAddSub(ArithmeticParser.AddSubContext context) { int left = Visit(context.expr(0)); int right = Visit(context.expr(1)); return context.op.Text switch { "+" => left + right, "-" => left - right, _ => throw new NotSupportedException($"Operator {context.op.Text} is not supported.") }; } // 去掉括号,访问括号内的表达式 public override int VisitParens(ArithmeticParser.ParensContext context) => Visit(context.expr()); // 解析整数 public override int VisitInt(ArithmeticParser.IntContext context) => int.Parse(context.INT().GetText()); }
然后写一个 Evaluate 方法,把字符串表达式传进去:
Console.WriteLine(Evaluate("1 + 2 * 3")); // 7
Console.WriteLine(Evaluate("(1 + 2) * 3")); // 9
int Evaluate(string expression)
{
var lexer = new ArithmeticLexer(new AntlrInputStream(expression));
var tokens = new CommonTokenStream(lexer);
var parser = new ArithmeticParser(tokens);
var visitor = new ArithmeticVisitor();
return visitor.Visit(parser.expr());
}
Listener 模式通过 EnterXXX 和 ExitXXX 方法触发,我们可以在 ExitXXX 里把中间结果压入栈,最后弹出结果。
public class ArithmeticListener : ArithmeticBaseListener
{
private readonly Stack _stack = new();
public int Result => _stack.Pop();
public override void ExitMulDiv(ArithmeticParser.MulDivContext context)
{
int right = _stack.Pop();
int left = _stack.Pop();
int result = context.op.Text switch
{
"*" => left * right,
"/" => left / right,
_ => throw new NotSupportedException($"Operator {context.op.Text} is not supported.")
};
_stack.Push(result);
}
public override void ExitAddSub(ArithmeticParser.AddSubContext context)
{
int right = _stack.Pop();
int left = _stack.Pop();
int result = context.op.Text switch
{
"+" => left + right,
"-" => left - right,
_ => throw new NotSupportedException($"Operator {context.op.Text} is not supported.")
};
_stack.Push(result);
}
public override void ExitInt(ArithmeticParser.IntContext context)
{
int value = int.Parse(context.INT().GetText());
_stack.Push(value);
}
}
使用方式类似:
Console.WriteLine(Evaluate("1 + 2 * 3")); // 7
Console.WriteLine(Evaluate("(1 + 2) * 3")); // 9
int Evaluate(string expression)
{
var lexer = new ArithmeticLexer(new AntlrInputStream(expression));
var tokens = new CommonTokenStream(lexer);
var parser = new ArithmeticParser(tokens);
var listener = new ArithmeticListener();
parser.AddParseListener(listener);
parser.expr();
return listener.Result;
}
上面的例子直接在遍历时计算了值,对于简单场景够用了。但如果处理逻辑比较复杂,更好的做法是把 ANTLR4 生成的原始 AST 转换成一个自定义的 AST 结构,把解析逻辑和执行逻辑拆开,代码会更清晰,扩展起来也更方便。
下面看一个更接近实际的需求:指定一个文件夹,用 SQL 语句查询文件夹下的 CSV 文件,支持过滤、排序等。为了简化,只支持 SELECT、FROM、WHERE、ORDER BY,数据类型全当字符串处理,过滤支持 =、!=、LIKE,条件只能用 AND 连接,排序支持 ASC 和 DESC。
这次把词法和语法规则分开成两个文件:SqlLexer.g4 和 SqlParser.g4。
SqlLexer.g4
lexer grammar SqlLexer;
options { caseInsensitive = true; }
SELECT : 'SELECT' ;
FROM : 'FROM' ;
WHERE : 'WHERE' ;
ORDER : 'ORDER' ;
BY : 'BY' ;
ASC : 'ASC' ;
DESC : 'DESC' ;
AND : 'AND' ;
OR : 'OR' ;
COMMA : ',' ;
STAR : '*' ;
EQ : '=' ;
NEQ : '!=' ;
LIKE : 'LIKE' ;
STRING_LITERAL : '\'' ( ~('\'' | '\\') | '\\' . )* '\'' ;
IDENTIFIER : [a-z_][a-z0-9_]* ;
WS : [ \t\r\n]+ -> skip ;
SqlParser.g4
parser grammar SqlParser;
options { tokenVocab=SqlLexer; }
query : SELECT selectList FROM tableName (WHERE whereClause)? (ORDER BY orderByClause)? ;
selectList : columnName (COMMA columnName)* | STAR ;
columnName: IDENTIFIER ;
tableName: IDENTIFIER ;
whereClause : whereCondition (AND whereCondition)* ;
whereCondition
: columnName op=(EQ | NEQ) STRING_LITERAL
| columnName op=LIKE STRING_LITERAL
;
orderByClause : orderByCondition (COMMA orderByCondition)* ;
orderByCondition : columnName (ASC | DESC)? ;
写完后可以在 Rider 里用插件测试 query 规则,验证语法正确性。

接下来定义一组类型来表示 SQL 的自定义 AST:
public abstract class Expression;
public class QueryExpression : Expression
{
public required string TableName { get; init; }
public required bool SelectAll { get; init; }
public required IEnumerable SelectList { get; init; }
public required IEnumerable WhereConditions { get; init; }
public required IEnumerable OrderByConditions { get; init; }
}
public class WhereCondition : Expression
{
public required string ColumnName { get; init; }
public WhereConditionOperator Operator { get; init; }
public required string Value { get; init; }
}
public enum WhereConditionOperator
{
Equal, NotEqual, StartsWith, EndsWith, Contains
}
public class OrderByCondition : Expression
{
public required string ColumnName { get; init; }
public bool IsDescending { get; init; }
}
写一个 SqlAstBuilder 类(继承 SqlParserBaseVisitor)把原始 AST 转换成自定义结构:
public class SqlAstBuilder : SqlParserBaseVisitor{ public override QueryExpression VisitQuery(SqlParser.QueryContext context) { var selectList = context.selectList(); bool selectAll = selectList?.STAR() != null; var columns = selectList?.columnName() .Select(c => c.GetText()) .ToList() ?? []; var tableName = context.tableName().GetText(); var whereConditions = context.whereClause() ?.whereCondition() .Select(c => { var stringValue = c.STRING_LITERAL().GetText().Trim('\''); var opText = c.op.Text.ToUpperInvariant(); var op = WhereConditionOperator.Equal; if (opText == "=") op = WhereConditionOperator.Equal; else if (opText == "!=") op = WhereConditionOperator.NotEqual; else if (opText == "LIKE") { if (stringValue.StartsWith("%") && stringValue.EndsWith("%")) { op = WhereConditionOperator.Contains; stringValue = stringValue.Substring(1, stringValue.Length - 2); } else if (stringValue.StartsWith("%")) { op = WhereConditionOperator.EndsWith; stringValue = stringValue.Substring(1); } else if (stringValue.EndsWith("%")) { op = WhereConditionOperator.StartsWith; stringValue = stringValue.Substring(0, stringValue.Length - 1); } } else throw new NotSupportedException($"Operator {c.op.Text} is not supported."); return new WhereCondition { ColumnName = c.columnName().GetText(), Operator = op, Value = stringValue }; }) .ToList() ?? []; var orderByConditions = context.orderByClause() ?.orderByCondition() .Select(c => new OrderByCondition { ColumnName = c.columnName().GetText(), IsDescending = c.DESC() != null }) .ToList() ?? []; return new QueryExpression { SelectAll = selectAll, SelectList = columns, TableName = tableName, WhereConditions = whereConditions, OrderByConditions = orderByConditions }; } }
最后实现 SqlToCsvEngine 类,负责解析 SQL、读取 CSV、过滤排序并返回结果:
public class SqlToCsvEngine(DirectoryInfo csvDirectory)
{
public IEnumerable> ExecuteQuery(string query)
{
var lexer = new SqlLexer(new AntlrInputStream(query));
var tokens = new CommonTokenStream(lexer);
var parser = new SqlParser(tokens);
var astBuilder = new SqlAstBuilder();
var expression = astBuilder.Visit(parser.query());
var csvData = ReadCsv(expression.TableName);
// 过滤
var filteredData = csvData.Where(row =>
{
bool isMatch = true;
foreach (var condition in expression.WhereConditions)
{
if (row.TryGetValue(condition.ColumnName, out var value))
{
isMatch = condition.Operator switch
{
WhereConditionOperator.Equal => value == condition.Value,
WhereConditionOperator.NotEqual => value != condition.Value,
WhereConditionOperator.StartsWith => value.StartsWith(condition.Value),
WhereConditionOperator.EndsWith => value.EndsWith(condition.Value),
WhereConditionOperator.Contains => value.Contains(condition.Value),
_ => throw new ArgumentOutOfRangeException()
};
}
else throw new InvalidOperationException($"Column {condition.ColumnName} does not exist in CSV file.");
}
return isMatch;
});
// 排序
foreach (var orderByCondition in expression.OrderByConditions)
{
Func>, Func, string>,
IEnumerable>> orderByFunc;
if (filteredData is IOrderedEnumerable> orderedData)
{
orderByFunc = orderByCondition.IsDescending
? (_, keySelector) => orderedData.ThenByDescending(keySelector)
: (_, keySelector) => orderedData.ThenBy(keySelector);
}
else
{
orderByFunc = orderByCondition.IsDescending
? Enumerable.OrderByDescending
: Enumerable.OrderBy;
}
filteredData = orderByFunc(filteredData, row =>
{
if (row.TryGetValue(orderByCondition.ColumnName, out var value)) return value;
throw new InvalidOperationException($"Order by column {orderByCondition.ColumnName} does not exist in CSV file.");
});
}
// 投影
if (expression.SelectAll) return filteredData;
var selectedData = filteredData.Select(row =>
{
var selectedRow = new Dictionary();
foreach (var columnName in expression.SelectList)
{
if (row.TryGetValue(columnName, out var value)) selectedRow[columnName] = value;
else throw new InvalidOperationException($"Column {columnName} does not exist in CSV file.");
}
return selectedRow;
});
return selectedData;
}
private IEnumerable> ReadCsv(string tableName)
{
var csvFile = new FileInfo(Path.Combine(csvDirectory.FullName, $"{tableName}.csv"));
if (!csvFile.Exists) throw new FileNotFoundException($"CSV file {csvFile.FullName} does not exist.");
using var reader = new StreamReader(csvFile.FullName);
var headerLine = reader.ReadLine();
if (headerLine == null) throw new InvalidOperationException($"CSV file {csvFile.FullName} is empty.");
var headers = headerLine.Split(',');
while (!reader.EndOfStream)
{
var line = reader.ReadLine();
if (line == null) continue;
var values = line.Split(',');
yield return headers.Zip(values).ToDictionary(x => x.First, x => x.Second);
}
}
}
测试用的 CSV 文件内容:
Name,City,Occupation,Company Alice,New York,Engineer,TechCorp Bob,Los Angeles,Designer,Creative Inc Ben,Atlanta,Writer,Publishing House Charlie,Chicago,Manager,Finance Group Da vid,Houston,Teacher,School District Eve,Miami,Student,University Frank,Seattle,Chef,Restaurant Co Grace,San Francisco,Doctor,HealthCare Hannah,Boston,Lawyer,Legal Partners Ian,Denver,Architect,BuildIt
执行查询:
var directory = new DirectoryInfo("/Users/hkh/Desktop/test");
var engine = new SqlToCsvEngine(directory);
var sql = """
SELECT Name, City, Occupation, Company
FROM Employee
WHERE City != 'Miami'
AND Occupation LIKE '%er'
ORDER BY Name ASC, Company DESC
""";
var result = engine.ExecuteQuery(sql);
// 打印头部
foreach (var column in result.First().Keys)
{
Console.Write($"{column}\t");
}
foreach (var row in result)
{
Console.WriteLine();
foreach (var (_, value) in row)
{
Console.Write($"{value}\t");
}
}
输出结果:
Name City Occupation Company
Alice New York Engineer TechCorp
Ben Atlanta Writer Publishing House
Bob Los Angeles Designer Creative Inc
Charlie Chicago Manager Finance Group
Da vid Houston Teacher School District
Hannah Boston Lawyer Legal Partners
https://github.com/antlr/grammars-v4
https://wizardforcel.gitbooks.io/antlr4-short-course/content/basic-concept.html
https://github.com/antlr/antlr4/blob/master/doc/csharp-target.md
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8