英文原文:
https://www.jacksondunstan.com/articles/3199
LINQ 表达式与 LINQ 查询不同。它们更像是 C# 本身语法的反射。这是该语言的一个引人入胜且功能强大的领域,我将在今天的文章中对其进行一些探索。
我们今天要讲的核心类是 System.Linq.Expressions.Expression。它是一大堆 SomethingExpression 类的基类,Something 可以代表你可以编写的大量代码。
你看,表达式代表代码,就像类型代表一个类一样。它不是对该代码的评估,而是对它的反映。这意味着你用一个Expression来检查你写的其他代码。
“这可能有什么用?”,你可能会问。好吧,一旦你有了工具,你就会开始想出更好的解决方案来解决各种问题。
想到的一个简单问题是需要函数的用户传入带有字段名称的字符串参数的问题。稍后您可能会使用反射来使用字符串名称查找该字段。但这是有问题的,因为编译器没有验证字符串。因此,如果您打错字或更改字段名称,那么您的代码将在运行时中断,这不太理想。
进入LINQ表达式。你可以用它们来把错误从运行时移到编译时。你只需要想出一种方法来表达C#中的字段名。有很多方法可以做到这一点,但事实证明,一个方便的方法是使用一个lambda:
// Class to get field names from
class Person
{
public string First;
public string Last;
public int Age;
}
// Lambda expressing the field name: First
p => p.First
一个很大的优点是,你不需要一个实际的Person实例来用lambda表达其字段名。它就像一个没有被调用的函数。这有点像你可以使用typeof(Person)获得一个类的类型,而不需要实际实例化一个类。
现在我们如何使用LINQ表达式来获取字段名呢?这其实很简单 你只需用Expression< T >来包装你想要的东西。在本例中,我们有一个lambda,它接收一个Person并返回一个字符串,也就是所谓的Func<Person,string>。这意味着我们需要一个Expression<Func<Person,string>>:
Expression<Func<Person,string>> expression = p => p.First;
现在我们有了一个表达式,我们可以检查它。由于这是一个lambda的表达式,我们实际上有一个LambdaExpression。这暴露了lambda的两个部分。参数和主体。主体是我们放置字段名的地方,所以我们来抓取它。
Expression<Func<Person,string>> expression = p => p.First;
Expression body = expression.Body;
现在lambda的主体是什么类型的表达式?嗯,它指的是类中的一个成员。First。这使得它成为一个MemberExpression,所以让我们转换为:
Expression<Func<Person,string>> expression = p => p.First;
MemberExpression body = (MemberExpression)expression.Body;
MemberExpression有一个Member属性,包含被访问成员的信息。这是一个MemberInfo,就像你从typeof(Person).GetMember(“First”)得到的那样。所以让我们来获取这个属性:
Expression<Func<Person,string>> expression = p => p.First;
MemberExpression body = (MemberExpression)expression.Body;
MemberInfo info = body.Member;
最后,让我们从 MemberInfo 中获取成员的字符串名称:
Expression<Func<Person,string>> expression = p => p.First;
MemberExpression body = (MemberExpression)expression.Body;
MemberInfo info = body.Member;
string fieldName = info.Name;
这显示了一种类型安全的方法来获取字段名。没有更多的错别字! 再也不会在字段名改变时忘记更新字符串了! 但是每次你想要一个字段名的时候,都要打出所有的代码,这实在是太乏味了,所以让我们做一个辅助函数:
public static string GetFieldName<T,K>(Expression<Func<T,K>> expression)
{
return ((MemberExpression)expression.Body).Member.Name;
}
这个辅助函数使用类型实参(又名泛型)来允许使用任何类 T 和任何字段类型 K。使用它很容易:
var fieldName = GetFieldName<Person,string>(p => p.First);
Debug.Log(fieldName); // prints: First
到目前为止,我们只是触及了 LINQ 表达式的皮毛。我们只检查了一个非常简单的 lambda,但它们暴露的远不止这些。表达式形成一个子表达式树,您可以出于任何目的深入研究这些子表达式。例如,考虑这个 lambda:
p => p.First == "Jackson" && p.Last == "Dunstan" && p.Age < 100
你得到的表达式如下所示:
LambdaExpression
{
Body = BinaryExpression
{
NodeType = AndAlso,
Left = BinaryExpression
{
NodeType = AndAlso,
Left = BinaryExpression
{
NodeType = Equal,
Left = MemberExpression
{
Member.Name = "First"
},
Right = ConstantExpression
{
Value = "Jackson"
}
},
Right = BinaryExpression
{
NodeType = Equal,
Left = MemberExpression
{
Member.Name = "Last"
},
Right = ConstantExpression
{
Value = "Dunstan"
}
}
},
Right = BinaryExpression
{
NodeType = LessThan,
Left = MemberExpression
{
Member.Name = Age
},
Right = ConstantExpression
{
Value = 100
}
}
}
关于如何使用LINQ查询来获取字段名和评估像这样的表达式的真正好例子,请看开源的单文件数据库系统LiteDB。字段使用像GetFieldName这样的LINQ表达式进行索引,而数据库则使用像上面的树状表达式进行查询。与SQL查询不同的是,这都是类型安全的,并由编译器检查!
最后,我应该注意一下IL2CPP的兼容性。在我所做的测试中,LINQ表达式在启用IL2CPP的iOS上似乎得到了很好的支持。不过,今天我还没有讨论LINQ查询的另一面。这一方面是建立你自己的表达式实例,不是从代码中而是动态的。例如,你可以把LambdaExpression编译成Delegate,然后用DynamicInvoke()来执行它。像这样的动态代码生成并不能与iOS上的IL2CPP一起工作。然而,只要你在某种 "只读 "模式下使用LINQ表达式,你就应该没事。真的,这就像使用System.Reflection而避免使用System.Reflection.Emit一样。
这就是今天 LINQ 表达式的全部内容。如果您将它们用于任何用途,请在评论中发布您对它们的体验!