Linq/Enumerable Any Vs содержит

Я решил проблему, которая у меня была, но хотя я узнал, как что-то работает (или не работает), я не понимаю, почему.

Поскольку я из тех людей, которым нравится знать «почему», я надеюсь, что кто-то может объяснить:

У меня есть список элементов и связанных с ними комментариев, и я хотел различать комментарии администратора и комментарии пользователей, поэтому я попробовал следующий код:

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
}

Пошаговое выполнение кода показало, что, несмотря на правильный объект Role, он не распознал роль в commentUser.Roles.

Код, который в итоге сработал:

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

Я доволен этим, потому что это меньше кода и, на мой взгляд, чище, но я не понимаю, почему не работает contains.

Надеясь, что кто-то может прояснить это для меня.


person richardterris    schedule 05.08.2013    source источник


Ответы (4)


Это вероятно потому, что вы не переопределили сравнения на равенство (Equals, GetHashCode, operator==) в своем классе Role. Поэтому он выполнял сравнение ссылок, что на самом деле не лучшая идея, как будто это не один и тот же объект, это заставляет его думать, что это разные объекты. Вам необходимо переопределить эти операторы равенства, чтобы обеспечить равенство значений.

person It'sNotALie.    schedule 05.08.2013
comment
Я не понимаю - разве User.Roles не список объектов Role? - person richardterris; 05.08.2013
comment
Поскольку он не использовал == между объектом Role, не важно, перегружает ли он operator ==, но вы правы в переопределении Equals и GetHashCode. - person Jeppe Stig Nielsen; 05.08.2013
comment
@richardterris Все зависит от того, как контекст вашего объекта обрабатывает второй запрос того же объекта. Похоже, поведение заключается в создании нового объекта, а не в возврате уже запрошенного объекта. Это означает, что хотя все значения свойств получаются одинаковыми, на самом деле они являются разными объектами, и без переопределения равенства значение возвращает false. - person Bob Vale; 05.08.2013
comment
@JeppeStigNielsen Просто для последовательности: я знаю, что в этом примере ему это не нужно. - person It'sNotALie.; 05.08.2013

Вы должны переопределить Equals (а затем всегда также GetHashCode), если хотите использовать Contains. В противном случае 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;
    } 
}

Другой вариант — реализовать собственный IEqualityComparer<Role> для перегрузки Enumerable.Contains:

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))
{
    // ...
}
person Tim Schmelter    schedule 05.08.2013
comment
Конечно, если Role — это класс, отличный от sealed, некоторые люди предпочитают использовать return GetType() == r2.GetType() && RoleID == r2.RoleID;. В противном случае будет трудно (даже для осторожных программистов) вывести из Role и получить свои Equals правильные. - person Jeppe Stig Nielsen; 05.08.2013
comment
@JeppeStigNielsen: я уже добавил еще один вариант, он может использовать IEqualityComparer<Role>. Таким образом, ему не нужно было модифицировать сам Role. - person Tim Schmelter; 05.08.2013
comment
Мне нравится, что. Мой комментарий относится только к вашему первому варианту, изменяющему сам тип Role, и, поскольку вы не удалили этот вариант из своего ответа, мне все еще хочется прокомментировать это. - person Jeppe Stig Nielsen; 05.08.2013
comment
Я полагаю, вопрос в том, лучше ли это, чем просто использовать .Any для проверки имени? - person richardterris; 05.08.2013
comment
@richardterris: Не обязательно, все дело в удобочитаемости: если вам часто нужен этот класс и вам нужно часто сравнивать роли друг с другом (например, в Enumerable.Contains), лучше переопределить Equals и GetHashCode. В любом случае это лучшая практика, если вы можете изменить класс. Если вы не можете, вы все равно можете использовать пользовательский IEqualityComparer, как показано выше. Затем вы даже можете использовать компаратор в других методах linq, таких как GroupBy, Intersect, Distinct, Join и т. д. Все эти методы имеют перегруженную версию для IEqualityComparer<T>, если вы не можете переопределить Equals+GetHashCode. - person Tim Schmelter; 05.08.2013
comment
Да хороший момент! В моей ситуации это единственное место, где я должен это сделать, и поэтому .Any лучше всего. Я думаю, что если бы мне приходилось делать много этого, я бы изменил класс Role. - person richardterris; 05.08.2013
comment
В других местах, где я проверяю роли, я бы проверял текущего вошедшего в систему пользователя, поэтому я могу использовать User.IsInRole, но здесь я проверяю пользователя, опубликовавшего комментарий. Спасибо еще раз - person richardterris; 05.08.2013

При использовании метода Contains вы проверяете, содержит ли массив Roles пользовательского объекта объект, который вы предварительно извлекли из базы данных. Хотя массив содержит объект для роли «admin», он не содержит точного объекта, который вы извлекли ранее.

При использовании Any-метода вы проверяете, есть ли какая-либо роль с именем "admin" - и это дает ожидаемый результат.

Чтобы получить тот же результат с помощью метода Contains, реализуйте интерфейс IEquatable<Role> в классе ролей и сравните имя, чтобы проверить, действительно ли два экземпляра имеют одно и то же значение.

person Spontifixus    schedule 05.08.2013

Это будет ваше сравнение равенства для роли.

Объект в commentUserRole отличается от того, который вы ищете commentUser.Roles.

Ваш контекстный объект создаст новый объект, когда вы выберете из него и заполните свое свойство Roles коллекцией новых ролей. Если ваш контекст не отслеживает объекты, чтобы вернуть тот же объект, когда запрашивается вторая копия, это будет другой объект, даже если все свойства могут быть одинаковыми. Следовательно, отказ Содержит

Ваше предложение Any явно проверяет свойство Name, поэтому оно работает

Попробуйте реализовать роль IEquatable<Role>

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

Хотя MSDN показывает, что это нужно только для List<T>, на самом деле вам может понадобиться переопределите Equals и GetHashCode, чтобы это работало

в таком случае:

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();
  }
}
person Bob Vale    schedule 05.08.2013
comment
Спасибо за помощь - я прокомментировал комментарий 'It'sNotALie' - но я думаю, что вы оба говорите примерно одно и то же. - person richardterris; 05.08.2013