表达式树报错:引用未定义变量,求动态JSON序列化代码修复方案
我最近在通过表达式树动态生成代码,实现模型对象的高效手动JSON序列化——目标是模型变更时不用手动更新序列化代码,但运行时却抛出了这个异常:
System.InvalidOperationException: 'variable 'sw' of type 'System.IO.StringWriter' referenced from scope '', but it is not defined'
是不是必须先声明变量再赋值?先看看我最初的实现代码:
public static Func<T, string> ConstructJsonParserFunction<T>() { List<Expression> methodBodyExpressions = new List<Expression>(); ParameterExpression methodParameter = Expression.Parameter(typeof(T), "entity"); ParameterExpression stringWriterExpression = Expression.Variable(typeof(StringWriter), "sw"); ParameterExpression jsonTextWriterExpression = Expression.Variable(typeof(JsonTextWriter), "writer"); ConstructorInfo jsonTextWriterConstructor = typeof(JsonTextWriter).GetConstructor(new Type[] { typeof(TextWriter) }); MethodInfo jsonTextWriterMethod_WriteStartObject = typeof(JsonTextWriter).GetMethod("WriteStartObject"); MethodInfo jsonTextWriterMethod_WritePropertyName = typeof(JsonTextWriter).GetMethods() .Where(mi => mi.Name == "WritePropertyName" && mi.GetParameters().Length == 1 && mi.GetParameters()[0].Name == "name") .First(); Dictionary<Type, MethodInfo> jsonTextWriterMethods_WriteValue = new Dictionary<Type, MethodInfo>(); foreach (MethodInfo method in typeof(JsonTextWriter).GetMethods().Where(mi => mi.Name == "WriteValue" && mi.GetParameters().Length == 1)) { jsonTextWriterMethods_WriteValue[method.GetParameters()[0].ParameterType] = method; } MethodInfo jsonTextWriterMethod_WriteEndObject = typeof(JsonTextWriter).GetMethod("WriteEndObject"); MethodInfo stringWriterMethod_ToString = typeof(StringWriter).GetMethods() .Where(mi => mi.Name == "ToString" && mi.GetParameters().Length == 0) .First(); methodBodyExpressions.Add(stringWriterExpression); methodBodyExpressions.Add(jsonTextWriterExpression); methodBodyExpressions.Add(Expression.Assign(stringWriterExpression, Expression.New(typeof(StringWriter)))); methodBodyExpressions.Add(Expression.Assign(jsonTextWriterExpression, Expression.New(jsonTextWriterConstructor, stringWriterExpression))); methodBodyExpressions.Add(Expression.Call(jsonTextWriterExpression, jsonTextWriterMethod_WriteStartObject)); foreach (PropertyInfo property in typeof(T).GetProperties().Where(p => Attribute.IsDefined(p, typeof(JsonPropertyAttribute)))) { methodBodyExpressions.Add(Expression.Call(jsonTextWriterExpression, jsonTextWriterMethod_WritePropertyName, Expression.Constant(property.Name))); methodBodyExpressions.Add(Expression.Call(jsonTextWriterExpression, jsonTextWriterMethods_WriteValue[property.PropertyType], Expression.Property(methodParameter, property))); } methodBodyExpressions.Add(Expression.Call(jsonTextWriterExpression, jsonTextWriterMethod_WriteEndObject)); methodBodyExpressions.Add(Expression.Call(stringWriterExpression, stringWriterMethod_ToString)); BlockExpression block = Expression.Block(methodBodyExpressions); return Expression.Lambda<Func<T, string>>(block, methodParameter).Compile(); }
当时的调试视图显示Block是这样的:
.Block() {
$sw;
$writer;
$sw = .New System.IO.StringWriter();
$writer = .New Newtonsoft.Json.JsonTextWriter($sw);
.Call $writer.WriteStartObject();
.Call $writer.WritePropertyName("CompanyId");
.Call $writer.WriteValue($entity.CompanyId);
.Call $writer.WritePropertyName("Name");
.Call $writer.WriteValue($entity.Name);
.Call $writer.WriteEndObject();
.Call $sw.ToString()
}
问题根源
仔细看就会发现,我虽然创建了sw和writer这两个变量,但BlockExpression根本不知道它们是当前作用域的局部变量——原代码只是把变量表达式直接丢进了方法体列表,没有在Block中显式声明这些变量,导致表达式树解析时认为变量未定义。
解决方案
要解决这个问题,必须满足两个关键条件:
- 用
Expression.Variable(type, "debugVarName")创建局部变量的ParameterExpression(这一步我已经做了); - 创建
BlockExpression时,要把局部变量集合和主体表达式列表分开传递,让Block明确知道当前作用域内的变量。
同时还要删掉原代码中多余的两行——把变量本身加入方法体的操作完全没必要,因为Block会负责声明变量:
// 删掉这两行 methodBodyExpressions.Add(stringWriterExpression); methodBodyExpressions.Add(jsonTextWriterExpression);
然后修改BlockExpression的创建代码,改成传递变量集合:
BlockExpression block = Expression.Block( new ParameterExpression[] { stringWriterExpression, jsonTextWriterExpression }, methodBodyExpressions );
修改后的完整代码如下:
public static Func<T, string> ConstructJsonParserFunction<T>() { List<Expression> methodBodyExpressions = new List<Expression>(); ParameterExpression methodParameter = Expression.Parameter(typeof(T), "entity"); ParameterExpression stringWriterExpression = Expression.Variable(typeof(StringWriter), "sw"); ParameterExpression jsonTextWriterExpression = Expression.Variable(typeof(JsonTextWriter), "writer"); ConstructorInfo jsonTextWriterConstructor = typeof(JsonTextWriter).GetConstructor(new Type[] { typeof(TextWriter) }); MethodInfo jsonTextWriterMethod_WriteStartObject = typeof(JsonTextWriter).GetMethod("WriteStartObject"); MethodInfo jsonTextWriterMethod_WritePropertyName = typeof(JsonTextWriter).GetMethods() .Where(mi => mi.Name == "WritePropertyName" && mi.GetParameters().Length == 1 && mi.GetParameters()[0].Name == "name") .First(); Dictionary<Type, MethodInfo> jsonTextWriterMethods_WriteValue = new Dictionary<Type, MethodInfo>(); foreach (MethodInfo method in typeof(JsonTextWriter).GetMethods().Where(mi => mi.Name == "WriteValue" && mi.GetParameters().Length == 1)) { jsonTextWriterMethods_WriteValue[method.GetParameters()[0].ParameterType] = method; } MethodInfo jsonTextWriterMethod_WriteEndObject = typeof(JsonTextWriter).GetMethod("WriteEndObject"); MethodInfo stringWriterMethod_ToString = typeof(StringWriter).GetMethods() .Where(mi => mi.Name == "ToString" && mi.GetParameters().Length == 0) .First(); // 移除了多余的变量添加操作 methodBodyExpressions.Add(Expression.Assign(stringWriterExpression, Expression.New(typeof(StringWriter)))); methodBodyExpressions.Add(Expression.Assign(jsonTextWriterExpression, Expression.New(jsonTextWriterConstructor, stringWriterExpression))); methodBodyExpressions.Add(Expression.Call(jsonTextWriterExpression, jsonTextWriterMethod_WriteStartObject)); foreach (PropertyInfo property in typeof(T).GetProperties().Where(p => Attribute.IsDefined(p, typeof(JsonPropertyAttribute)))) { methodBodyExpressions.Add(Expression.Call(jsonTextWriterExpression, jsonTextWriterMethod_WritePropertyName, Expression.Constant(property.Name))); methodBodyExpressions.Add(Expression.Call(jsonTextWriterExpression, jsonTextWriterMethods_WriteValue[property.PropertyType], Expression.Property(methodParameter, property))); } methodBodyExpressions.Add(Expression.Call(jsonTextWriterExpression, jsonTextWriterMethod_WriteEndObject)); methodBodyExpressions.Add(Expression.Call(stringWriterExpression, stringWriterMethod_ToString)); // 显式传递局部变量给BlockExpression BlockExpression block = Expression.Block( new ParameterExpression[] { stringWriterExpression, jsonTextWriterExpression }, methodBodyExpressions ); return Expression.Lambda<Func<T, string>>(block, methodParameter).Compile(); }
修改后的调试视图会变成这样,变量已经被正确声明在Block作用域内:
.Block(System.IO.StringWriter $sw, Newtonsoft.Json.JsonTextWriter $writer) {
$sw = .New System.IO.StringWriter();
$writer = .New Newtonsoft.Json.JsonTextWriter($sw);
.Call $writer.WriteStartObject();
.Call $writer.WritePropertyName("CompanyId");
.Call $writer.WriteValue($entity.CompanyId);
.Call $writer.WritePropertyName("Name");
.Call $writer.WriteValue($entity.Name);
.Call $writer.WriteEndObject();
.Call $sw.ToString()
}
这样就能解决变量未定义的异常,让表达式树正确识别并使用局部变量了。
内容的提问来源于stack exchange,提问作者Brad Sherard

