C# lambda 表达式常量与字符串

C# lambda expression constant vs string

有人可以解释为什么如果我 运行 这个表达式:

const string testValue = "ABC"; 
return NameDbContext.MasterNames
    .Where(m => m.Names.Any(n => n.LastName == testValue))
    .ToList();

我得到了预期的结果,但是如果我 运行 与 testValue 相同作为变量,它会失败:

string testValue = "ABC"; 
return NameDbContext.MasterNames
    .Where(m => m.Names.Any(n => n.LastName == testValue))
    .ToList();

这似乎只发生在 stringint 的类似代码在 testValue 作为变量或常量时都能正常工作。

我怀疑这是由于字符串的对象性质造成的。如果是这种情况,我怎么能用变量调用这个表达式(我不知道 testValue 在编译时的值)。

谢谢。

编辑:

此查询 运行 针对大型 Oracle 数据 table(> 600 万行)。当使用常量时,它 return 会立即得到正确的结果集。当 运行 变量时,似乎 where 的应用效率很低(return 需要一分钟多的时间)。

EDIT2:

我看到的数据库中的跟踪查询:

使用常量调用时:

SELECT *
  FROM (SELECT   "Filter2"."MALPHA_KEY" AS "MALPHA_KEY"
      FROM (SELECT "Extent1"."MALPHA_KEY" AS "MALPHA_KEY",
          ROW_NUMBER () OVER (ORDER BY "Extent1"."MALPHA_KEY" ASC)
                                                              AS "row_number"
                    FROM "RMS"."ALPHA_MASTER_NAME" "Extent1"
                   WHERE (EXISTS (
                             SELECT 1 AS "C1"
                               FROM "RMS"."ALPHA" "Extent2"
                              WHERE (    ("Extent1"."MALPHA_KEY" =
                                                        "Extent2"."MALPHA_KEY"
                                         )
                                     AND ('ABC' = "Extent2"."LAST_NAME")
                                    ))
                         )) "Filter2"
           WHERE ("Filter2"."row_number" > 0)
        ORDER BY "Filter2"."MALPHA_KEY" ASC)
 WHERE (ROWNUM <= (50))

使用变量调用时:

SELECT *
  FROM (SELECT   "Project2"."MALPHA_KEY" AS "MALPHA_KEY"
            FROM (SELECT "Project2"."MALPHA_KEY" AS "MALPHA_KEY",
                         ROW_NUMBER () OVER (ORDER BY "Project2"."MALPHA_KEY" ASC)
                                                              AS "row_number"
                    FROM (SELECT "Extent1"."MALPHA_KEY" AS "MALPHA_KEY"
                            FROM "RMS"."ALPHA_MASTER_NAME" "Extent1"
                           WHERE (EXISTS (
                                     SELECT 1 AS "C1"
                                       FROM "RMS"."ALPHA" "Extent2"
                                      WHERE (    ("Extent1"."MALPHA_KEY" =
                                                        "Extent2"."MALPHA_KEY"
                                                 )
                                             AND (   ("Extent2"."LAST_NAME" =
                                                                   :p__linq__0
                                                     )
                                                  OR (    ("Extent2"."LAST_NAME" IS NULL
                                                          )
                                                      AND (:p__linq__0 IS NULL
                                                          )
                                                     )
                                                 )
                                            ))
                                 )) "Project2") "Project2"
           WHERE ("Project2"."row_number" > 0)
        ORDER BY "Project2"."MALPHA_KEY" ASC)
 WHERE (ROWNUM <= (50))

注意 where 语句中的区别(除了使用变量)它测试 NULL 相等性

    AND (   ("Extent2"."LAST_NAME" = :p__linq__0
        )
   OR (    ("Extent2"."LAST_NAME" IS NULL )
   AND (:p__linq__0 IS NULL )  )  )

NULL 测试导致完整 table 扫描...

创建 linq 查询时,实际上是在构建表达式树。在您的示例中,您有两个表达式树来构建查询:

Expression<Func<Name, bool>> exp1 = name => name.LastName == testValue;
Expression<Func<MasterName, bool>> exp2 = masterName => masterName.Names.Any(exp1);
var result = NameDbContext.MasterNames.Where(exp2).ToList();

来自这个回答Local variable and expression trees

Capturing a local variable is actually performed by "hoisting" the local variable into an instance variable of a compiler-generated class. The C# compiler creates a new instance of the extra class at the appropriate time, and changes any access to the local variable into an access of the instance variable in the relevant instance.

So the expression tree then needs to be a field access within the instance - and the instance itself is provided via a ConstantExpression.

The simplest approach for working how to create expression trees is usually to create something similar in a lambda expression, then look at the generated code in Reflector, turning the optimization level down so that Reflector doesn't convert it back to lambda expressions.

如果我定义一个局部变量 string testValue = "ABC"; 调试视图将输出:

.Lambda #Lambda1<System.Func`2[ConsoleApp.Program+Name,System.Boolean]>(ConsoleApp.Program+Name $name)
{
    $name.LastName == .Constant<ConsoleApp.Program+<>c__DisplayClass0_0>(ConsoleApp.Program+<>c__DisplayClass0_0).testValue
}

现在,如果我定义一个常量 const string testValue = "ABC";,调试视图将输出:

.Lambda #Lambda1<System.Func`2[ConsoleApp.Program+Name,System.Boolean]>(ConsoleApp.Program+Name $name)
{
    $name.LastName == "ABC"
}

解决此问题的一种方法是创建一个简单的 ExpressionVisitor,使用部分应用程序将现有表达式的参数重写为常量值。

例如,我创建表达式然后将一个值(仅在运行时已知)应用于它们:

 Expression<Func<int, int, bool>> expr = (a, b) => a < b;
 var applied = expr.Apply(input.FirstMonth);

这是我使用的(许多)Apply 方法之一(每个方法都有不同数量的参数):

/// <summary>
/// Partially apply a value to an expression
/// </summary>
public static Expression<Func<U, bool>> Apply<T, U>(this Expression<Func<T, U, bool>> input,
    T value)
{
   var swap = new ExpressionSubstitute(input.Parameters[0],
       Expression.Constant(value));
   var lambda = Expression.Lambda<Func<U, bool>>(
       swap.Visit(input.Body), 
       input.Parameters[1]);
   return lambda;
}


class ExpressionSubstitute : System.Linq.Expressions.ExpressionVisitor
{
    private readonly Expression from, to;
    public ExpressionSubstitute(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }

    public override Expression Visit(Expression node)
    {
        if (node == from) return to;
        return base.Visit(node);
    }
}

理论#1

如果您测试了生成的查询并确定它实际上是导致完整 table 扫描的参数 null 检查,那么修复非常简单:

NameDbContext.Configuration.UseDatabaseNullSemantics = true;

这将导致简化的 WHERE 子句:

WHERE "Extent2"."LAST_NAME" = :p__linq__0

显然,您需要考虑这对使用 NameDbContext.

的其他查询的影响

或者,您可以使用@IanMercer 的非常有趣的解决方案并执行表达式树节点替换以获得所需的 WHERE 子句。我希望最终结果是相似的,尽管我不确定 Oracle 是否足够聪明,可以在没有显式参数化的情况下生成可重用的查询计划,这可能会导致一些重新编译开销。

理论#2

根据个人经验(尽管使用 SQL 服务器,但由于一般概念相同,我假设这适用于您的情况)绕过索引可能还有另一个原因,那就是类型LAST_NAME 列与 :p__linq__0 参数不匹配。在我的场景中,数据库中的列是非 unicode,但 EF 生成的参数是 unicode(分别为 varcharnvarchar - unicode 是 EF 的默认值),因此无法进行索引搜索。