Linq/Enumerable Any和Contains的区别

3

我解决了一个问题,但尽管我已经知道了某个东西的工作原理(或不工作),但我对其原因并不清楚。

由于我是那种喜欢知道“为什么”的人,所以希望有人能解释一下:

我有一些项目和相关评论的列表,我想区分管理员评论和用户评论,所以我尝试了以下代码:

User commentUser = userRepository.GetUserById(comment.userId);
Role commentUserRole = context.Roles.Single(x=>x.Name == "admin");
if(commentUser.Roles.Contains(commentUserRole)
 {
   //do stuff
 }
else
{
 // do other stuff
}

逐步执行代码后发现,虽然它有正确的角色对象,但在commentUser.Roles中无法识别该角色

最终有效的代码如下:

if(commentUser.Roles.Any(x=>x.Name == "admin"))
{
  //do stuff
}

我很满意这个方法,因为代码量少且更加简洁易懂,但是我不明白为什么"contains"没有起作用。希望有人能帮我解决这个问题。
4个回答

9

这可能是因为您没有覆盖 Role 类上的相等比较方法(EqualsGetHashCodeoperator==)。因此,它进行了引用比较,这并不是最好的想法,因为如果它们不是同一个对象,它会认为它们是不同的。您需要覆盖这些相等运算符以提供值相等性。


我不明白 - User.Roles 不是一个 Role 对象的列表吗? - richardterris
2
由于他在Role对象之间没有使用==,所以重载operator ==并不重要,但你关于重写EqualsGetHashCode是正确的。 - Jeppe Stig Nielsen
2
@richardterris 这完全取决于您的对象上下文如何处理对同一对象的第二个请求。看起来行为是创建一个新对象而不是返回已请求的对象。这意味着虽然所有属性值都相同,但它们实际上是不同的对象,如果没有等式重写则该值返回false。 - Bob Vale
@JeppeStigNielsen 为了保持一致性:我知道在这个例子中他不需要它。 - It'sNotALie.

4

如果想使用 Contains,您必须覆盖 Equals(并且通常还需要同时覆盖GetHashCode)。否则,Equals 将仅比较引用。

例如:

public class Role
{
    public string RoleName{ get; set; }
    public int RoleID{ get; set; }
    // ...

    public override bool Equals(object obj)
    {
        Role r2 = obj as Role;
        if (r2 == null) return false;
        return RoleID == r2.RoleID;
    }
    public override int GetHashCode()
    {
        return RoleID;
    }
    public override string ToString()
    {
        return RoleName;
    } 
}

另一种选择是为 Enumerable.Contains 的重载实现自定义的 IEqualityComparer<Role>:

public class RoleComparer : IEqualityComparer<Role>
{
    public bool Equals(Role x, Role y)
    {
        return x.RoleID.Equals(y.RoleID);
    }

    public int GetHashCode(Role obj)
    {
        return obj.RoleID;
    }
}

使用方法如下:

var comparer = new RoleComparer();
User commentUser = userRepository.GetUserById(comment.userId);
Role commentUserRole = context.Roles.Single(x=>x.Name == "admin");
if(commentUser.Roles.Contains(commentUserRole, comparer))
{
    // ...
}

当然,如果Role是一个未封闭的类,有些人更喜欢使用return GetType() == r2.GetType() && RoleID == r2.RoleID;。否则,即使对于小心的编码人员来说,从Role派生并正确地获取它们的Equals也会很困难。 - Jeppe Stig Nielsen
我喜欢那个。我的评论仅与您的第一个“选项”相关,即修改Role类型本身,由于您尚未从答案中删除该选项,因此我仍然想评论一下。 - Jeppe Stig Nielsen
1
@richardterris:不一定,这取决于可读性:如果您经常需要使用此类,并且需要经常将角色与其他角色进行比较(例如在Enumerable.Contains中),最好重写EqualsGetHashCode。无论如何,如果您可以修改类,则最好采用最佳实践。如果您不能修改类,则仍然可以像上面所示那样使用自定义的IEqualityComparer。然后,您甚至可以在其他Linq方法中使用比较器,例如GroupByIntersectDistinctJoin等。如果您无法重写Equals+GetHashCode,所有这些方法都有一个重载版本,用于IEqualityComparer<T> - Tim Schmelter
是的,好观点!在我的情况下,这是我唯一需要这样做的地方,因此使用“Any”最好。我认为如果我需要经常这样做,那么我会更改“Role”类。 - richardterris
在我检查角色的其他地方,我会检查当前登录的用户,这样我就可以使用User.IsInRole,但是在这里我要检查发表评论的用户。再次感谢。 - richardterris
显示剩余2条评论

1
这将是您对角色进行的等值比较。
commentUserRole对象中,其实际对象与您寻找的commentUser.Roles不同。
当您从上下文对象中选择并使用新的Roles集合填充您的Roles属性时,它将创建一个新的对象。如果您的上下文对象没有跟踪对象以在请求第二个副本时返回相同的对象,则即使所有属性都相同,它也将是不同的对象。因此,Contains操作失败了。
您的Any子句明确检查了Name属性,这就是它起作用的原因。
尝试让Role实现IEquatable<Role>
public class Role : IEquatable<Role> {
  public bool Equals(Role compare) {
    return compare != null && this.Name == compare.Name;
  }
}

虽然MSDN显示您只需要对List<T>执行此操作,但实际上您可能需要重写EqualsGetHashCode以使其正常工作

在这种情况下:

public class Role : IEquatable<Role> {
  public bool Equals(Role compare) {
    return compare != null && this.Name == compare.Name;
  }

  public override bool Equals(object compare) {
     return this.Equals(compare as Role); // this will call the above equals method
  }

  public override int GetHashCode() {
     return this.Name == null ? 0 : this.Name.GetHashCode();
  }
}

谢谢帮忙 - 我已经对'It'sNotALie'的评论进行了回复 - 不过我认为你们两个的意思基本相同。 - richardterris

1
当使用Contains方法时,您需要检查用户对象的Roles数组是否包含先前从数据库中检索到的对象。尽管该数组包含角色“admin”的对象,但它不包含您之前获取的确切对象。
当使用Any方法时,您需要检查是否存在任何具有名称“admin”的角色 - 这将提供预期结果。
要使用Contains方法获得相同的结果,请在角色类上实现IEquatable<Role>接口,并比较名称以检查两个实例是否实际具有相同的值。

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