在讲述如何解析转换成员访问表达式之前,先来讲一些预备知识。
一个标准的lambda表达式应该是
( 参数列表 ) => 表达式或表达式块
其中参数列表和方法的参数列表类似,不过lambda表达式更灵活,允许用的时候不用显示声明参数的类型甚至在一个参数的时候括号也可以不用加,也就是(int a) => ... 和 (a)=> ... 以及 a=> ... 都是允许的。
我们之前说了不少表达式的解析,都是针对lambda表达式的右半边进行的。对于左半边的参数列表的解析是非常简单的,不过最大的问题在于解析之后如何把参数应用到表达式中。从语言特性上来说,C#里所有变量是不会影响在它范围之外的区域的,它只对在它范围包括所有子语句体的范围产生影响,所以我们如果在解析的时候都能得到当前及父语句块内的参数就可以了,不需要考虑子块产生新变量对父块的影响:
int a = 2;
Func<int, Func<int>> f1 = (b) => () => b + a;
Func<Func<int, int>> f2 = () => b => b + a;
f1和f2是并列关系在块中,所以f1的新变量参数b不会影响f2同样生成变量b。但如果在f1,f2外已经有一个变量b的话,就会导致命名冲突。
有了这个结论,我们可以轻松使用一种简单方法来实现lambda参数的传递:
private Stack<IEnumerable<ParameterExpression>> _parameters = new Stack<IEnumerable<ParameterExpression>>();
这里使用了Stack结构来存放参数列表,完整的解析代码如下:
private Expression ProcessLambdaExpression(ParseTreeNode expNode) { List<ParameterExpression> arglist = new List<ParameterExpression>(); var args = expNode.GetChild("lambda_function_signature").GetDescendant("anonymous_function_parameter_list_opt"); if (args == null) { var child = expNode.GetChild("lambda_function_signature"); arglist.Add(Expression.Parameter( child.GetClrType() ?? _paramTypes.FirstOrDefault() ?? typeof(int), child.GetChild("Identifier").GetValue())); } else { for (int i = 0; i < args.ChildNodes.Count; ++i) { var child = args.ChildNodes[i]; arglist.Add(Expression.Parameter( child.GetClrType() ?? _paramTypes.Skip(i).FirstOrDefault() ?? typeof(int), child.GetChild("Identifier").GetValue())); } } EnterScope(arglist.ToArray()); var expression = expNode.GetChild("anonymous_function_body"); var body = ProcessExpression(expression); LeaveScope(); return Expression.Lambda(body, arglist); }
解析的时候虽然分2种无惨和有参的情况,但具体解析逻辑是一致的。由于我们解析的只是一串代表lambda表达式的字符串,如果参数没自带类型(大多数情况下没人写lambda会给参数加类型声明)没法根据上下文自动识别出,所以还需要一种机制能帮我们来识别出参数类型。
private List<Type> _paramTypes = new List<Type>();
public Expression<TDelegate> Parse<TDelegate>(string expression)
{
_paramTypes.Clear();
var genericParameters = typeof(TDelegate).GetGenericTypeDefinition().GetGenericArguments();
var realArguments = typeof(TDelegate).GetGenericArguments();
_paramTypes.AddRange(
realArguments.Where((a, i) =>
genericParameters.First(p => p.GenericParameterPosition == i).
GenericParameterAttributes.HasFlag(GenericParameterAttributes.Contravariant)));
var exp = Parse(expression);
var genericlambda = exp as Expression<TDelegate>;
if (genericlambda != null)
return genericlambda;
var lambda = exp as LambdaExpression;
if (lambda != null)
{
return Expression.Lambda<TDelegate>(lambda.Body, lambda.Parameters);
}
return Expression.Lambda<TDelegate>(exp);
}
现在用泛型的解析方法通过分析泛型参数自动识别出参数类型放入_paramTypes里。
正常分析出完成参数后,还需要把参数传递给整个表达式,就是通过前面说的堆在_parameters来实现:
private void EnterScope(ParameterExpression[] parameters) { if (_parameters.Count > 0) _parameters.Push(_parameters.Peek().Union(parameters)); else _parameters.Push(parameters); } private void LeaveScope() { _parameters.Pop(); }
每次进入一个lambda的时候都会调用EnterScope把当前参数和前一个解析栈的参数放入,这样就保证每次进入一个表达式_parameters的栈顶都包含了当前及父辈们所有的参数。
到此为止,我们顺利的解决了表达式的参数传递问题,接下来说一下如何解决闭包类型变量的传递问题。
先看一个最简单的lambda:
int a = 2;
Func<int> f = () => a;
这个a是到了表达式范围外的变量,它没有定义在表达式内,所以不管我们解析的多完美对于这个a是没法解析的,而这个a就是所谓的闭包(clousre)了。观测C#编译器的做法是先在编译时插入一个类名后缀为DisplayClass的类,这个类的成员就是int a,类似于:
class <>c__DisplayClass { public int a { get; set; } }
然后把表达式所有对a的引用都改为对<>c__DisplayClass1.a的引用(要说为什么会用<>来做名字,可能是因为C#里无法使用<>作为类型名不会和用户定义的产生冲突,当然如果用户emit这种名字还是会冲突的)这样等于是把局部变量转换为一个可访问的内部变量,我们可以简单的模拟这种行为而不必要用如此大费周章:
private Dictionary<string, object> _knownVariables = new Dictionary<string, object>(); public ExpressionParser With(Expression<Func<object>> variable) { var name = variable.Body.ToReadableString(); var @var = variable.Compile()(); return With(name, @var); } public ExpressionParser With(string name, object variable) { var parser = new ExpressionParser(); parser._knownVariables.Add(name, variable); knownTypes.TryAdd(variable.GetType().Name, variable.GetType()); return parser; }
通过简单的
parser.With(()=>a).Parse("()=>a")
就完成了,甚至还能改名:
parser.With("b", a).Parse("()=>b")
其实就是个注册操作,这可以说是解析只有表达式而得不到其他相关上下文时的无奈之举了。