在我目前正在开发的项目中,我们有许多静态表达式,当我们在它们上面调用Invoke方法并传递我们的lambda表达式时,我们必须在本地范围内带一个变量。参数。
今天,我们声明了一个静态方法,其参数正是查询所期望的类型。所以,我的同事和我正在搞乱,看看我们是否可以在查询的Select语句中使用此方法来执行项目,而不是在整个对象上调用它,而不将其带入本地范围。
它有效!但我们不明白为什么。
想象一下像这样的代码
// old way
public static class ManyExpressions {
public static Expression<Func<SomeDataType, bool> UsefulExpression {
get {
// TODO implement more believable lies and logic here
return (sdt) => sdt.someCondition == true && false || true;
}
}
}
public class ARealController : BaseController {
/* many declarations of important things */
public ARealClass( /* many ninjected in things */) {
/* many assignments */
}
public JsonNet<ImportantDataResult> getSomeInfo(/* many useful parameter */) {
var usefulExpression = ManyExpressions.UsefulExpression;
// the db context is all taken care of in BaseController
var result = db.SomeDataType
.Where(sdt => usefulExpression.Invoke(sdt))
.Select(sdt => new { /* grab important things*/ })
.ToList();
return JsonNet(result);
}
}
然后你就可以做到这一点!
// new way
public class SomeModelClass {
/* many properties, no constructor, and very few useful methods */
// TODO come up with better fake names
public static SomeModelClass FromDbEntity(DbEntity dbEntity) {
return new SomeModelClass { /* init all properties here*/ };
}
}
public class ARealController : BaseController {
/* many declarations of important things */
public ARealClass( /* many ninjected in things */) {
/* many assignments */
}
public JsonNet<SomeModelClass> getSomeInfo(/* many useful parameter */) {
// the db context is all taken care of in BaseController
var result = db.SomeDataType
.Select(SomeModelClass.FromDbEntity) // TODO; explain this magic
.ToList();
return JsonNet(result);
}
}
因此,当ReSharper提示我这样做时(通常不会这样,因为匹配代理所期望的类型的条件通常不会满足),它会说转换为方法组。我有点模糊地理解一个方法组是一组方法,并且C#编译器可以负责将方法组转换为LINQ提供程序的显式类型和适当的重载,但不是......但是我是模糊了为什么这完全有效。
这里发生了什么?
答案 0 :(得分:18)
当你不理解某事时,问一个问题真的很棒,但问题是很难知道某人不理解哪一点。我希望我在这里提供帮助,而不是告诉你一堆你知道的东西,而不是实际回答你的问题。
让我们回到Linq之前的日子,在表达之前,在lambda之前,甚至在匿名代表之前。
在.NET 1.0中,我们没有任何这些。我们甚至没有仿制药。我们确实有代表。委托与函数指针相关(如果您了解C,C ++或具有此类的语言)或函数作为参数/变量(如果您了解Javascript或具有此类的语言)。
我们可以定义一个委托:
public delegate int MyDelegate(double someValue, double someOtherValue);
然后将其用作字段,属性,变量,方法参数的类型或作为事件的基础。
但当时实际为代表提供值的唯一方法是引用实际方法。
public int CompareDoubles(double x, double y)
{
if (x < y) return -1;
return y < x ? 1 : 0;
}
MyDelegate dele = CompareDoubles;
我们可以使用dele.Invoke(1.0, 2.0)
或简写dele(1.0, 2.0)
来调用它。
现在,因为我们在.NET中有重载,所以我们可以有多个CompareDoubles
引用的东西。这不是一个问题,因为如果我们也有例如public int CompareDoubles(double x, double y, double z){…}
编译器可能知道您只能将另一个CompareDoubles
分配给dele
,因此它是明确的。尽管如此,在上下文中CompareDoubles
表示一个方法,它接受两个double
参数并返回int
,在该上下文之外CompareDoubles
表示所有方法的组名。
因此,我们称之为方法组。
现在,使用.NET 2.0我们得到了泛型,这对代表很有用,同时在C#2中我们得到了匿名方法,这也很有用。从2.0开始,我们现在可以做到:
MyDelegate dele = delegate (double x, double y)
{
if (x < y) return -1;
return y < x ? 1 : 0;
};
这部分只是来自C#2的语法糖,在幕后仍有一种方法,尽管它有一个不可言喻的名字&#34; (作为.NET名称有效但作为C#名称无效的名称,因此C#名称不能与之冲突)。如果通常情况下,创建方法只是为了让它们与特定的代表一起使用,那么它很方便。
向前推进一点,在.NET 3.5中,Func
和Action
代表具有协方差和逆向性(非常适合代表)(非常适合根据类型重用相同的名称,而不是一堆不同的代表,通常非常相似),随之而来的是C#3,它有lambda表达式。
现在,这些在一次使用中有点像匿名方法,但在另一种用途中却没有。
这就是我们无法做到的原因:
var func = (int i) => i * 2;
var
解决了它分配给它的意义,但是lamdas从他们被分配到的内容中找出它们的含义,所以这是不明确的。
这可能意味着:
Func<int, int> func = i => i * 2;
在哪种情况下,它的缩写为:
Func<int, int> func = delegate(int i){return i * 2;};
反过来又是简写:
int <>SomeNameImpossibleInC# (int i)
{
return i * 2;
}
Func<int, int> func = <>SomeNameImpossibleInC#;
但它也可以用作:
Expression<Func<int, int>> func = i => i * 2;
这是简写:
Expression<Func<int, int>> func = Expression.Lambda<Func<int, int>>(
Expression.Multiply(
param,
Expression.Constant(2)
),
param
);
我们还使用.NET 3.5使Linq大量使用这两种方法。实际上,表达式被认为是Linq的一部分,并且位于System.Linq.Expressions
命名空间中。请注意,我们在这里得到的对象是我们想要做的事情的描述(取参数,乘以2,给我们结果)而不是如何做。
现在,Linq以两种主要方式运作。在IQueryable
和IQueryable<T>
以及IEnumerable
和IEnumerable<T>
上。前者定义了在&#34;提供商&#34;上使用的操作。正是&#34;提供商做了什么&#34;由该提供者决定,后者在内存中的值序列中定义相同的操作。
我们可以从一个移动到另一个。我们可以使用IEnumerable<T>
将IQueryable<T>
转换为AsQueryable
,这将为我们提供可枚举的包装器,我们可以将IQueryable<T>
转换为IEnumerable<T>
将其视为一个,因为IQueryable<T>
来自IEnumerable<T>
。
可枚举的表单使用委托。 Select
如何工作的简化版本(这个版本遗漏了许多优化,我跳过错误检查并在间接中确保立即进行错误检查)将是:
public static IEnumerable<TResult> Select(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
foreach(TSource item in source) yield return selector(item);
}
另一方面,可查询版本的工作原理是从Expression<TSource, TResult>
获取表达式树,使其成为包含对Select
的调用和源查询的表达式的一部分,并返回一个对象包装那个表达。换句话说,对可查询的Select
的调用会返回一个对象,该对象代表对可查询的Select
的调用!
正是这样做取决于提供者。数据库提供程序将它们转换为SQL,枚举在表达式上调用Compile()
以创建委托,然后我们返回上面的Select
的第一个版本,依此类推。
但是历史考虑过,让我们再次回顾历史。 lambda可以表示表达式或委托(如果是表达式,我们可以Compile()
来获得相同的委托)。委托是一种通过变量指向方法的方法,而方法是方法组的一部分。所有这些都建立在第一个版本的技术之上,只能通过创建方法然后传递它来调用。
现在,假设我们有一个采用单个参数并且有结果的方法。
public string IntString(int num) { return num.ToString(); }
现在假设我们在lambda选择器中引用它:
Enumerable.Range(0, 10).Select(i => IntString(i));
我们有一个lambda为委托创建一个匿名方法,而匿名方法又调用一个具有相同参数和返回类型的方法。在某种程度上,如果我们有:
public string MyAnonymousMethod(int i){return IntString(i);}
MyAnonymousMethod
在这里有点无意义;所有这一切都是调用IntString(i)
并返回结果,所以为什么不首先调用IntString
并切断该方法:
Enumerable.Range(0, 10).Select(IntString);
通过获取基于lambda的委托并将其转换为方法组,我们已经删除了一个不必要的(虽然请参阅下面关于委托缓存的说明)间接级别。因此,ReSharper的建议&#34;转换为方法组&#34;或者它的措辞(我不会自己使用ReSharper)。
这里有一点需要注意。 IQueryable<T>
选择只接受表达式,因此提供程序可以尝试解决如何将其转换为执行操作的方式(例如,针对数据库的SQL)。 IEnumerable<T>
选择只接受委托,以便可以在.NET应用程序本身中执行。我们可以使用Compile()
从前者转到后者(当可查询实际上是一个包装可枚举的时候),但是我们不能从后者转到前者:我们没有办法接受一名代表并将其转变为一种表达方式,这意味着除了&#34;以外的任何其他方式称这个代表&#34;这不是可以变成SQL的东西。
现在当我们使用像i => i * 2
这样的lambda表达式时,当与IQueryable<T>
一起使用时,它将是一个表达式,当与IEnumerable<T>
一起使用时,由于重载决策规则支持带有可查询的表达式,它将是一个委托(作为它可以处理两者的类型,但表达式表单适用于最派生类型)。如果我们明确地给它一个委托,无论是因为我们在某个地方键入Func<>
,还是来自一个方法组,那么表达式的重载是不可用的,并且使用了代理。这意味着它不会被传递到数据库,而是直到那一点的linq表达式成为数据库部分&#34;它被调用,其余的工作在内存中完成。
95%的时间最好避免。所以95%的时间如果你得到建议&#34;转换为方法组&#34;使用数据库支持的查询,你应该思考&#34;呃哦!那实际上是一名代表。为什么是代表?我可以把它改成表达吗?&#34;。如果我只是传递方法名称&#34;,那么只有剩下的5%的时间你会想到&#34;它会稍微缩短一点。 (另外,使用方法组而不是委托来防止编译器可以执行的委托缓存,因此可能效率较低)。
在那里,我希望我能够涵盖你在所有这些过程中无法理解的内容,或者至少在这里你可以指出并说出那里的那一点,那是我不会理解的#34;
答案 1 :(得分:1)
我不想让你失望,但根本就没有魔法。我建议你对这个&#34;新方式&#34;非常小心。
始终通过将其悬停在VS中来检查功能的结果。请记住IQueryable<T>
&#34;继承&#34; IEnumerable<T>
以及Queryable
包含与Enumerable
具有相同名称的扩展方法,唯一的区别是前者与Expression<Func<...>>
一起使用,而后者只与Func<..>
一起使用1}}。
因此,只要您使用Func
或method group
而不是IQueryable<T>
,编译器就会选择Enumerable
重载,从而默默地从LINQ to Entities
切换到{{1}上下文。但两者之间存在巨大差异 - 前者在数据库中执行,而后者在内存中执行。
关键是要在LINQ to Objects
上下文中保持尽可能长的时间,所以&#34;旧方式&#34;应该是首选。例如。来自你的例子
IQueryable<T>
或
.Where(sdt => sdt.someCondition == true && false || true)
或
.Where(ManyExpressions.UsefulExpression)
但不是
.Where(usefulExpression)
永远不会
.Where(sdt => usefulExpression.Invoke(sdt))
答案 2 :(得分:0)
Select(SomeModelClass.FromDbEntity)
这使用Enumerable.Select
,这不是你想要的。这转换为&#34; queryable-LINQ&#34;进入LINQ对象。这意味着数据库无法执行此代码。
.Where(sdt => usefulExpression.Invoke(sdt))
在这里,我假设您的意思是.Where(usefulExpression)
。这会将表达式传递到查询底层的表达式树中。 LINQ提供程序可以翻译此表达式。
当您执行此类实验时,请使用SQL事件探查器查看SQL通过网络传输的内容。确保查询的所有相关部分都是可翻译的。
答案 3 :(得分:0)
这个解决方案为我抛出了一些危险信号。其中的关键是:
var result = db.SomeDataType
.Select(SomeModelClass.FromDbEntity) // TODO; explain this magic
.ToList(); // <<!!!!!!!!!!!!!
每当您处理实体框架时,您都可以阅读&#34; ToList()&#34; as&#34;将整个内容复制到内存中。&#34;所以&#34; ToList()&#34;应该只在最后一秒完成。
考虑一下:在处理EF时,你可以传递很多有用的对象:
var query = context.Where(o => o.Customer.Name == "John")
.Where(o => o.TxNumber > 100000)
.OrderBy(o => o.TxDate);
//I've pulled NO data so far! "var query" is just an object I can pass around
//and even add on to! For example, I can now do this:
query = query.ThenBy(o => o.Items.Description); //and now I've appended that to my query
真正的魔力是那些lambdas也可以被抛入变量中。这是我在我的一个项目中使用的方法:
/// <summary>
/// Generates the Lambda "TIn => TIn.memberName [comparison] value"
/// </summary>
static Expression<Func<TIn, bool>> MakeSimplePredicate<TIn>(string memberName, ExpressionType comparison, object value)
{
var parameter = Expression.Parameter(typeof(TIn), "t");
Expression left = Expression.PropertyOrField(parameter, memberName);
return (Expression<Func<TIn, bool>>)Expression.Lambda(Expression.MakeBinary(comparison, left, Expression.Constant(value)), parameter);
}
使用此代码,您可以编写如下内容:
public GetQuery(string field, string value)
{
var query = context.Orders;
var condition = MakeSimplePredicate<Order>(field, ExpressionType.Equal, value);
return query.Where(condition);
}
最好的事情是,目前还没有数据通话。您可以根据需要继续添加条件。当您准备好获取数据时,只需遍历它或调用ToList()。
享受!
哦,如果您希望看到更完善的解决方案,请查看此信息,尽管来自不同的背景。 My Post on Linq Expression Trees