使用linq to entities和自定义IEqualityComparer计算不同项数目

4

我的自定义比较器似乎不起作用。我想要计算不同对象的数量,但每次都得到1的结果。即使查看数据库本身清楚地显示查询具有不同“TimeOfAction”值的实例超过1个。

 class TimeComparer : IEqualityComparer<Action>
{
    public bool Equals(Action a, Action b)
    {
        if (a.TimeOfAction == b.TimeOfAction)
            return true;
        else
            return false;
    }

    public int GetHashCode(Action obj)
    {
        return obj.ToString().ToLower().GetHashCode();
    }
}

我觉得问题可能出在 GetHashCode 方法上,因为我对它的工作原理不是很熟悉。以下是 LINQ 查询代码。我将其转换为 AsEnumerable,因为 Linq to Entities 不支持 distinct 方法。

DBEntities db = new DBEntities();

        IEnumerable<Action> query = 
                    from action in db.Action.AsEnumerable()
                    where action.TimeOfAction > new DateTime(2010, 11, 1, 0, 0, 0)
                    where action.TimeOfAction < new DateTime(2011, 2, 7, 0, 0, 0)
                    where action.EntityName == "seant"
                    select action;

var count = query.   
            Distinct(new TimeComparer()).Count();
2个回答

6
你的Equals和GetHashCode方法采用了完全不同的方法。特别是,相等的对象可能具有不同的哈希码,假设Action.ToString使用的字段不同于TimeOfAction。它们必须对齐,否则你将无法获得合理的结果。不同的对象拥有相同的哈希码是可以的(尽管这会影响性能),但相等的对象必须给出相同的哈希码。
请注意,使用自定义比较器将强制在进程内而不是数据库中执行Distinct操作。这可能不是问题,你只需要理解它。编辑:我没有注意到Queryable.Distinct有一个重载,它确实采用IEqualityComparer。我的猜测是,这是为了提供自定义字符串比较器和其他一些知名的比较器,而不仅仅是任意代码。如果它起作用,它将在本地完成。我不会感到惊讶,如果它爆炸了。
编辑:正如Marc所说,你可以使用Select(x => x.TimeOfAction).Distinct().Count()在数据库中完成这个操作。你还需要删除对AsEnumerable的调用。我的猜测是,这是因为其他东西没有工作。你可以尝试这个:
DBEntities db = new DBEntities();
IQueryable<DateTime> query = 
            from action in db.Action
            where action.TimeOfAction > new DateTime(2010, 11, 1, 0, 0, 0)
            where action.TimeOfAction < new DateTime(2011, 2, 7, 0, 0, 0)
            where action.EntityName == "seant"
            select action.TimeOfAction;
var count = query.Distinct().Count();

当然,如果您需要用query进行其他操作,您也需要保留原始版本:
DBEntities db = new DBEntities();
IQueryable<Action> query = 
            from action in db.Action
            where action.TimeOfAction > new DateTime(2010, 11, 1, 0, 0, 0)
            where action.TimeOfAction < new DateTime(2011, 2, 7, 0, 0, 0)
            where action.EntityName == "seant"
            select action;

var count = query.Select(x => x.TimeOfAction).Distinct().Count();
// Use query here as well to get at full action details

请注意,再次使用查询将产生第二个数据库查询。如果需要计数与第二个查询一致,请查看事务的运作方式... 或从数据库中提取所有详细信息(使用ToList调用),然后在进程内执行Distinct部分。
回到自定义相等比较器...
假设TimeOfAction是一个DateTime或其他具有合理哈希码的类型,则可以更改您的类:
class TimeComparer : IEqualityComparer<Action>
{
    public bool Equals(Action a, Action b)
    {
        return a.TimeOfAction == b.TimeOfAction;
    }

    public int GetHashCode(Action obj)
    {
        return obj.TimeOfAction.GetHashCode();
    }
}

请注意,我还简化了您的Equals方法 - 每当您发现自己有以下情况时:
if (condition)
{
    return true;
}
else
{
    return false;
}

您可以简化为:
return condition;

很详细(但仍然使用IEnumerable<>吗?)。 - Marc Gravell

4
首先,请注意这不会在数据库服务器上运行,因此与EF无关。
怀疑这部分是因为您的GetHashCodeEquals不一致;您最好有类似以下的代码:
public int GetHashCode(Action obj)
{
    return obj.TimeOfAction.GetHashCode();
}

因为这是您的Equals关心的内容。

另外,请注意,整个查询可以被重新编写(如果您使用IQueryable<Action> query(并删除AsEnumerable()),而不是IEnumerable<Action> query),则可能在数据库服务器上运行:

var count = query.Select(x => x.TimeOfAction).Distinct().Count();

+1 对于最后一点,这正是我要添加到我的答案中的。我会将我的答案保留一段时间,因为我认为简化Equals方法的观点值得OP看到。不过,如果您愿意在您的答案中包含它,我很高兴,然后我会删除我的答案 :) - Jon Skeet
请注意调用AsEnumerable。他也需要将其删除 :) - Jon Skeet
@Jon - 实际上,我认为OP指的是隐式转换,但无论如何我已经添加了它。 - Marc Gravell
@Marc:我在我的回答中已经详细讨论了这个问题,特别是关于多个数据库查询等方面。 - Jon Skeet
如果我将其保留为IQueryable(通常的方式),则所有操作都在数据库服务器上执行,这就是为什么不先转换为IEnumerable而使用Distinct(IEqualityComparer)方法会失败的原因?我进行了建议的更改,它起作用了,谢谢。只是想知道我是否仍然可以使用这些IEqualityComparer类,还是应该放弃它们? - Sean
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接