.net中的expression表达式有啥用?

作者:林明基
链接:https://www.zhihu.com/question/392350352/answer/1200192977
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

很多高级特性只有你在需要用到它的场景下才能比较容易理解。在这里我尝试为你假设这样的场景:

你需要设计一个json序列化库,可以把c#对象序列化json字符串,同时可指定哪些属性不参与序列化。

如果你用过.net上流行的库比如说,你就会参考它在需要忽略的字段上使用特性 [JsonIgnore] 。但如果你无法修改这个需要被序列化的类,怎么办?你可能会设计一个 Ignore(string propName) 方法让调用者使用代码指定需要被忽略的属性。

这里我们深入聊一下这个Ignore方法,当然让调用者传入一个字符串当然已经满足需求了。但是我们可以做得更好吗?

考虑这样一种情况,调用方使用 Ignore("Password"); 让密码属性不参与序列化,但是过了一段时间 Password 重构为 PasswordHash,如果没有完善的单元测试,可能没有人会发现这个遗漏。那么调用者使用了nameof关键字可以完美规避重构带来的问题了吗?请看下面的代码:

//原来是序列化User类型,后期改为序列化UserExt类型
var userJson = new MyJson<UserExt>();
//即便使用nameof还是无法完全避免重构时候的遗漏
userJson.Ignore(nameof(User.Password));
var result = userJson.ToString(user);

这样即便使用nameof也还是不够好。这时有了 Expression “代码即数据”的特性,我们可以构建这样一个方法:Ignore(Expression<Func<T,object>> e),调用的代码如下:

var userJson = new MyJson<UserExt>();
//忽略一个属性
userJson.Ignore(s => s.Password);
//忽略多个属性
userJson.Ignore(s => new { s.Password, s.Secret });

除了代码量变得更小,代码的可读性提高之外;可以有效的避免上面提到重构陷阱,还可以利用IDE的代码提示减小调用者的心智负担。一举多得岂不美哉?

这样只需要在 Ignore 内部解析 e 参数就可以得到调用者需要忽略序列化的属性。那么我们是怎么获得调用者输入的属性本身,而不是这个属性值呢?我们需要分析这个 Expression<Func<T,object>> 类型,它看起来是接受一个 s => s.Password 这样的委托:

userJson.Ignore(s => s.Password);

但是实际上编译器会为你生成类似这样的代码:

ParameterExpression parameterExpression = Expression.Parameter(typeof(A), "s");
userJson.Ignore(Expression.Lambda<Func<A, object>>(Expression.Convert(Expression.Property(parameterExpression, methodof(A.get_P1())), typeof(object)), new ParameterExpression[]
{
parameterExpression
}));

你可以看到实际上传入的是一个描述 s => s.Password 这段代码的结构化数据,你可以利用系统提供的各种方法读取这结构。如果你觉得这例子不太合理,那么我们回忆一下 Entity Framework 是如何将你的 C# 代码转换成 SQL语句。

//使用 Entity framework 时常见的代码
var users = dbContext.Users.Where(w => w.CreateTime < DateTime.Today && w.Enabled);
//实际上生成执行的 SQL 类似于
//SELECT * FROM users WHERE CreateTime < TODAY() AND Enabled = True

如上,你传给 Where 方法的并不是一个实际的委托,而是描述这个委托的结构化数据。利用这些结构化数据和一些编译的知识就可以把上面的委托翻译成 SQL 代码了。甚至可以根据你数据库类型生成针对不同数据库类型的 SQL 语句,做到了更高程度的抽象。

最后,假设 C# 并不存在 Expression 特性,你可以给 Ignore 传递字符串、可以给 Where 传递 SQL 字符串都可以满足需求,毕竟都是图灵完备的语言;但是也同样的,失去了编译器提供的静态类型检查的优势、失去了IDE提供的代码提示从而减小心智负担的优势,失去进一步抽象化程序的办法。

Expression 特性是在 2007 年 C# 3.0 中增加的,可能因为孤陋寡闻我至今没有在其它语言中见到类似的特性。所以习惯了 Expression 的存在再去使用其它语言完成类似的需求时往往让人感觉货比货得扔。 [doge]

posted on 2025-04-14 01:05  漫思  阅读(45)  评论(0)    收藏  举报

导航