lambda表示式的解析(一) 序
當你要把一個表示式字串轉換成一個lambda expression,本篇也許能幫到你。
把一個字串解析成lambda要做的工作其實就2個,第一步把字串通過語法解析轉換一顆語法樹,第二步就是遍歷這棵樹把它轉換成.lambda expression。做一個C#語法解析器是件麻煩事,好在有一些現成的解析器可以使用,這裡選用Irony來完成這個工作。
Irony的包裡含有一個Sample的C# grammar,這個grammar除了不支援linq外已經非常完整了,接下來只要稍微修改下就能用來解析C#的表示式。
sample裡預設的C# grammar對錶達式支援不完善的地方有:
1.不能把強型別轉換解析成恰當的節點。
2.不能很好區分成員訪問操作,a.b, a.b(), a.b[] 這種成員訪問操作的解析結果沒有有針對性的區分,加大了語法樹的轉換難度。
3.對匿名物件支援不夠,也就是new {}, new[] {}這種操作支援不夠。
4.不支援linq。
只要修改掉以上缺陷,我們就能得到一個相對完美的支援linq的C# 表示式解析器了,由於我沒有剔除掉namespace,class,struct等定義,所以修改後的grammar雖然是針對expression用的但是檔案還是比較大的,你可以到這裡下載CSharpExpressionGrammar.cs,因為本篇不是講如何用Irony的,所以想了解具體怎麼改的話請自行用winmerge之類的工具比較下修改過的grammar和Irony提供的grammar之間的差異。
有一點提一下的是,改過的grammar有一行
this.Root = expression;
這表示這個grammar只支援表示式解析,無法解析namespace,class之類的結構定義語法。
現在我們有了個可以用的grammar,接下來的工作就是使用它進行解析轉換了。
首先看下這個解析類
Parse方法很簡單,輸入一個表示式字串,然後返回一個Expression物件。具體實現就是先用Irony根據修改後的grammar建立解析器,解析器根據表示式字串解析成Irony自己的結構ParseTree,請先忽略_parameters.Clear(),重點是:public class ExpressionParser { public Expression Parse(string expression) { var parser = new Parser(new CSharpExpressionGrammar()); var tree = parser.Parse(expression); if (tree.Status == ParseTreeStatus.Error) throw new Exception("Parse Error"); _parameters.Clear(); return ProcessExpression(tree.Root); } }
return ProcessExpression(tree.Root);
語法樹解析本質上就是個遞迴操作,ProcessExpression和很多Visitor.Visit做的工作很接近,只不過沒取同樣的名字罷了。
private Expression ProcessExpression(ParseTreeNode expNode)
{
switch (expNode.GetName())
{
default:
throw new Exception(expNode.GetName());
case "anonymous_function_body":
case "parenthesized_expression":
return ProcessExpression(expNode.FirstChild);
case "lambda_expression":
return ProcessLambdaExpression(expNode);
case "typecast_expression":
return ProcessConvertExpression(expNode);
case "primary_expression":
return ProcessUnaryExpression(expNode);
case "bin_op_expression":
return ProcessBinaryExpression(expNode);
case "conditional_expression":
return ProcessConditionalExpression(expNode);
case "member_access":
return ProcessMemberAccessExpression(expNode);
case "object_creation_expression":
return ProcessNewExpression(expNode);
case "anonymous_type_creation_expression":
return ProcessNewAnonymousExpression(expNode);
case "literal":
return ProcessConstantExpression(expNode);
case "query_expression":
return ProcessLINQ(expNode);
}
return Expression.Empty();
}
ProcessExpression解析樹的每個節點,根據節點名字再進行進一步的解析工作,順便說一下通過Irony自帶的GrammarExplorer這個工具可以幫你看到它解析後的完整語法樹,我這些手工解析程式碼就是根據這樣的觀察得來的。
為了很方便的解析ParseTree,我為ParseTreeNode加了一些擴充套件方法,把編碼複雜度降低了不少,當然如果花點時間給ParseTree專門做一套支援xpath的查詢方法,那程式碼寫起來就更舒服了。
internal static class ParseTreeNodeExtensions
{
public static ParseTreeNode FirstOrDefault(this ParseTreeNode node, Func<ParseTreeNode, bool> predicate)
{
if (predicate(node)) return node;
foreach (var n in node.ChildNodes)
{
var found = n.FirstOrDefault(predicate);
if (found != null) return found;
}
return null;
}
public static ParseTreeNode GetChild(this ParseTreeNode node, string childName)
{
return node.ChildNodes.Find(p => p.GetName() == childName);
}
public static ParseTreeNode GetDescendant(this ParseTreeNode node, string childName)
{
return node.FirstOrDefault(p => p.GetName() == childName);
}
public static string GetName(this ParseTreeNode node)
{
return node.Term.Name;
}
public static string GetValue(this ParseTreeNode node)
{
return node.Token.Text;
}
public static object GetObject(this ParseTreeNode node)
{
return node.Token.Value;
}
public static bool HasChild(this ParseTreeNode node, string childName)
{
return node.GetChild(childName) != null;
}
public static Type GetClrType(this ParseTreeNode node)
{
if (node.HasChild("type_ref"))
{
var isNullable = node.GetDescendant("qmark_opt").FindTokenAndGetText() == "?";
var typeName = node.FindTokenAndGetText();
var type = ExpressionParser.GetType(typeName);
if (isNullable)
return typeof(Nullable<>).MakeGenericType(type);
return type;
}
return null;
}
}
到了這裡大致上我們解析表示式的程式碼邏輯整體框架已經完成了,接下來就是針對特定的操作生成表示式了。完整的ExpressionParser.cs程式碼也可以到http://code.google.com/p/tinynetevent/downloads/list去下載ExpressionParser.zip得到。要提前說明下的是ExpressionToCodeLib這個第三方庫是把表示式轉換成易讀的字串,最新版可在http://code.google.com/p/expressiontocode/得到;LINQ的解析完全是手工硬程式碼結構上沒有做任何優化,而且測試也不完全。
可以這麼用:
int i = 10;
int j = 3;
var lambda = parser.Parse<Func<int, int, int>>("(i, j) => i + j");
var method = lambda.Compile();
var result = method(i, j);
也可以這麼用:
int i = 10;
int j = 3;
var lambda = parser.With(()=>i).With(()=>j).Parse<Func<int>>("() => i + j");
var method = lambda.Compile();
var result = method();
LINQ:
parser.Parse<Func<IQueryable<dynamic>, IQueryable<dynamic>, dynamic>>("(A, B) => from a in A from b in B select new { a, b }")
接下來幾篇,會專門針對每個操作講解下如何生成對應的表示式及一些生成表示式時候的技巧和要點。