【发布时间】:2019-03-18 11:40:48
【问题描述】:
考虑以下类:
class Program
{
static void Test()
{
TestDelegate<string, int>(s => s.Length);
TestExpressionTree<string, int>(s => s.Length);
}
static void TestDelegate<T1, T2>(Func<T1, T2> del) { /*...*/ }
static void TestExpressionTree<T1, T2>(Expression<Func<T1, T2>> exp) { /*...*/ }
}
这是编译器生成的(以稍微不太可读的方式):
class Program
{
static void Test()
{
// The delegate call:
TestDelegate(Cache.Func ?? (Cache.Func = Cache.Instance.FuncImpl));
// The expression call:
var paramExp = Expression.Parameter(typeof(string), "s");
var propExp = Expression.Property(paramExp, "Length");
var lambdaExp = Expression.Lambda<Func<string, int>>(propExp, paramExp);
TestExpressionTree(lambdaExp);
}
static void TestDelegate<T1, T2>(Func<T1, T2> del) { /*...*/ }
static void TestExpressionTree<T1, T2>(Expression<Func<T1, T2>> exp) { /*...*/ }
sealed class Cache
{
public static readonly Cache Instance = new Cache();
public static Func<string, int> Func;
internal int FuncImpl(string s) => s.Length;
}
}
这样,第一次调用传递的委托被初始化一次,并在多个Test调用中重复使用。
但是,第二次调用传递的表达式树没有被重用 - 每次Test 调用都会初始化一个新的 lambda 表达式。
如果它不捕获任何东西并且表达式树是不可变的,那么缓存表达式树会有什么问题?
编辑
我想我需要澄清一下为什么我认为表达式树适合缓存。
- 生成的表达式树在编译时是已知的(嗯,它是由编译器创建的)。
- 它们是不可变的。因此,与下面 X39 给出的数组示例不同,表达式树在初始化后无法修改,因此可以安全缓存。
- 在代码库中只能有这么多的表达式树 - 再次,我在谈论可以缓存的那些,即使用 lambda 表达式初始化的那些(不是手动创建的)捕获任何外部状态/变量。字符串文字的自动实习就是一个类似的例子。
- 它们是用来遍历的——它们可以被编译来创建一个委托,但这不是它们的主要功能。如果有人想要一个编译的委托,他们可以只接受一个(
Func<T>,而不是Expression<Func<T>>)。接受表达式树表示它将被用作数据结构。因此,“应该先编译它们”并不是反对缓存表达式树的明智论据。
我要问的是缓存这些表达式树的潜在缺点。 svick 提到的内存需求是一个更可能的例子。
【问题讨论】:
-
缓存也有它的成本(增加内存使用)。而且由于处理表达式树的代码可能会比这里的编译器生成的代码更昂贵(在时间和分配方面),我不清楚这种优化实际上是否是一种改进。
-
表达式不仅用于方法执行。表达式应先编译,然后才能作为函数运行。假设表达式只是某个类的另一个实例,那么它取决于如何处理它来兑现或不兑现
-
只有 C# 语言设计者才能回答 why 部分。我同意你的观点,也希望它们被缓存。但我想答案更加琐碎且非技术性 - 就像Eric Lippert 的this answer 的开头和相关的博客文章(太糟糕了,他不再为MS 工作了)。简而言之,实现功能的时间、资源、成本、优先级和收益之间的典型平衡:)
标签: c# lambda compilation delegates expression-trees