查询中 GroupBy 方法的自定义比较

Custom compare for GroupBy method in a query

我想将具有特定值的对象(在本例中 (int?)null)放在不同的组中。

所以这个:

Id  NullableInt
A      1
B      2
C      1
D     null
E     null
F      1
G     null

应该这样结束:

Key   Ids
1      A, C, F
2      B
null   D
null   E
null   G

我正在使用 .GroupBy 和自定义比较器来尝试实现这一点。

问题是获取错误

LINQ to Entities does not recognize the method, and this method cannot be translated into a store expression

当我单独测试 LINQ 表达式时它可以工作,所以我假设它在 Entity Framework 中不受支持或工作方式不同,但我找不到任何关于它的信息。

我的代码(简化版):

var result = db.Table
    ...
    .GroupBy(
        t => t.NullableInt,
        new NullNotEqualComprare())
    ...
    .ToList();

显然我想在数据库本身做尽可能多的事情。

比较器代码:

    private class NullNotEqualComparer : IEqualityComparer<int?>
    {
        public bool Equals(int? x, int? y)
        {
            if (x == null || y == null)
            {
                return false;
            }

            return x.Value == y.Value;
        }

        public int GetHashCode(int? obj)
        {
            return obj.GetHashCode();
        }
    }

我是不是做错了什么,如果不支持,我该如何解决这个问题?

  1. 根据 MSDN https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/ef/language-reference/supported-and-unsupported-linq-methods-linq-to-entities ,linq-to-entities GroupBy 不支持比较器。

  2. 根据您的要求,您可以尝试2个功能。第一个块过滤所有空键,然后第二个块按非空键分组。像这样

     var nullKeyList = db.Table.Where(x => x.NullableInt == null).ToList();
    
     var valueKeyGroup = db.Table.Where(x => x.NullableInt != null)
                        .GroupBy(t => t.NullableInt).ToList();
    

哎呀,你忘了准确写下你组的要求了。我无法从你的比较器中扣除它,因为比较器不正确。

根据您的比较器,如果 x 等于 null,则 x 不等于 x

IEqualityComparer<int?> comparer = new NullNotEqualComparer();
int? x = null;
bool b = comparer(x, x);

因此 x 不会与 x 在同一组中。

后面再说比较器,先解释一下异常

IQueryable 不能使用 IEqualityComparer

一个IQueryable<...>有一个Expression和一个Provider。 Expression 以通用形式表示必须执行的查询;提供者知道哪个进程将执行查询(通常是数据库管理系统)以及使用哪种语言与该进程通信(通常 SQL)。

只要连接 return IQueryable<...> 的 LINQ 语句,只有 Expression 会发生变化。未联系数据库,未执行查询。只有当您通过调用 GetEnumerator() 开始枚举时(直接调用,或在另一个函数中深入调用,如 ToList()foreach),表达式才会发送给 Provider,Provider 将尝试将表达式翻译成 SQL,然后执行查询。 returned 数据显示为 IEnumerator<...>,您可以使用它逐一访问 returned 项目。

问题是,提供商不知道您的 NullNotEqualComparer,因此无法将其翻译成 SQL。事实上,有几种 LINQ 方法不受 LINQ-to-Entities 支持。请参阅 [支持和不支持的方法 (LINQ to Entities)] 1

因此您必须尝试将比较放在 GroupBy 的 keySelector 中。

间奏曲:你的 NullNotEqualComparer

你的相等比较器不是一个好的比较器。不满足x等于x:

的要求
IEqualityComparer<int?> comparer = new NullNotEqualComparer();
int? x = null;
bool b = comparer.Equals(x, x);

int? y = x;
bool c = comparer.Equals(x, y);

int? z = null;
bool d = comparer.Equals(x, z);

你期待什么,结果如何?

几乎总是,正确的相等比较器以相同的四行开始:

public bool equals(MyClass x, MyClass y)
{
    if (x == null) return y == null; // true if both null, false if x null, y not null
    if (y == null) return false;     // false, because x != null and y == null

    // the following two lines are just for efficiency:
    if (object.ReferenceEquals(x, y) return true;
    if (x.GetType() != y.GetType()) return false;

    // here starts the real comparison:
    ...
}

在极少数情况下,您希望不同类型成为相同的对象。在那种情况下,您将不会检查类型。

GetHashCode 用于快速检查两个对象是否不同。如果你要比较一千个对象的相等性,你可以很容易地发现其中 990 个是不同的,那么你只需要完全检查最后 10 个元素。

想想一个有 20 个属性的 class。要完全相等,您需要检查所有 20 个属性。如果您明智地选择 GetHashCode,则可能没有必要检查所有 20 个属性。

例如,如果您想查找住在同一地址的所有人,则必须检查国家/地区、城市、邮编、街道、门牌号……

从输入序列中消除大多数人的一种快速方法是仅检查邮政编码:如果两个人的邮政编码不同,他们将不会住在同一地址。

因此,对您的 GetHashCode 的唯一要求是:如果 Equals(x, y),则 GetHashCode(x) == GetHashCode(y)。请注意:不是相反:可能有不同的 x 和 y,它们具有相同的 HashCode。这个很容易看出:GetHashCodereturn是一个Int32,所以肯定有几个Int64对象共享他们的HashCode。

EqualityComparer<int?> comparer = new NullNotEqualComparer();
int? x = null;
int y = comparer.GetHashcode(x);   // <== Exception!

回到你的问题

在我看来,您创建了这个相等比较器,因为您希望为 table 中的所有项目创建一个单独的组,这些项目 t.NullableInt 的值等于 null。

Id  NullableInt
A      1
B      2
C      1
D     null
E     null
F      1
G     null

你想要三组:

  • 键 1,Id 为 A、C、F 的元素
  • 键 2,ID 为 B 的元素
  • 键为空,Id 为 D、E、G 的元素

如果这是你想要的,你可以使用 default comparer for class Nullable<T>:

  • x 和 y 的 HasValue 均为假:return真
  • x 和 y 的 HasValue 均为真:return x.Value == y.Value
  • 在所有其他情况下:return 错误。

假设您有一种方法可以将 dt.Table 的行转换为 IQueryable:

IQueryable<MyClass> tableRows = db.Table.ToMyClass();
var result = tableRows.GroupBy(row => row.NullableInt);

最后我进行了一些破解,但它非常简单并且效果很好。我想我会把它写在这里,因为它可能对其他人有用。

在我的问题中,NullableInt 实际上是另一个 table 的 ID,所以我知道它总是大于零。

因此我可以这样做:

var result = db.Table
    ...
    .GroupBy(t => t.NullableInt ?? int.MinValue + t.Id)
    ...
    .ToList();