LINQ Select Distinct с анонимными типами

150

Итак, у меня есть коллекция предметов. Точный тип не важен. Из него я хочу извлечь все уникальные пары пары определенных свойств, таким образом:

myObjectCollection.Select(item=>new
                                {
                                     Alpha = item.propOne,
                                     Bravo = item.propTwo
                                }
                 ).Distinct();

Итак, мой вопрос: будет ли Distinct в этом случае использовать объект по умолчанию, равный (что будет бесполезно для меня, поскольку каждый объект является новым), или можно сказать, что он должен делать разные равенства (в данном случае, равные значения Alpha и Bravo => равные случаи)? Есть ли способ достичь этого результата, если он этого не делает?

GWLlosa
источник
Это LINQ-to-Objects или LINQ-to-SQL? Если только объекты, вам, вероятно, не повезло. Однако, если L2S, то он может работать, так как DISTINCT будет передан в оператор SQL.
Джеймс Керран

Ответы:

188

Прочитайте отличный пост К. Скотта Аллена здесь:

И Равенство для Всех ... Анонимные Типы

Краткий ответ (и я цитирую):

Оказывается, компилятор C # переопределяет Equals и GetHashCode для анонимных типов. Реализация двух переопределенных методов использует все открытые свойства типа для вычисления хеш-кода объекта и проверки на равенство. Если два объекта одного и того же анонимного типа имеют одинаковые значения для своих свойств - объекты равны.

Поэтому совершенно безопасно использовать метод Distinct () в запросе, который возвращает анонимные типы.

Мэтт Гамильтон
источник
2
Я думаю, это верно только в том случае, если сами свойства являются типами значений или реализуют равенство значений - см. Мой ответ.
tvanfosson
Да, поскольку он использует GetHashCode для каждого свойства, он будет работать только в том случае, если каждое свойство имеет свою собственную уникальную реализацию этого. Я думаю, что большинство сценариев использования будут включать только простые типы в качестве свойств, поэтому в целом это безопасно.
Мэтт Гамильтон
4
Это означает, что равенство двух анонимных типов зависит от равенства членов, и это хорошо для меня, так как члены определены где-то, куда я могу добраться, и переопределить равенство, если нужно. Я просто не хотел создавать класс для этого просто чтобы переопределить равные.
GWLlosa
3
Возможно, стоит обратиться к MS с просьбой ввести синтаксис «ключа» в C #, который есть у VB (где вы можете указать определенные свойства анонимного типа как «первичный ключ» - см. Сообщение в блоге, на которое я ссылался).
Мэтт Гамильтон
1
Очень интересная статья. Спасибо!
Александр Прокофьев
14
public class DelegateComparer<T> : IEqualityComparer<T>
{
    private Func<T, T, bool> _equals;
    private Func<T, int> _hashCode;
    public DelegateComparer(Func<T, T, bool> equals, Func<T, int> hashCode)
    {
        _equals= equals;
        _hashCode = hashCode;
    }
    public bool Equals(T x, T y)
    {
        return _equals(x, y);
    }

    public int GetHashCode(T obj)
    {
        if(_hashCode!=null)
            return _hashCode(obj);
        return obj.GetHashCode();
    }       
}

public static class Extensions
{
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> items, 
        Func<T, T, bool> equals, Func<T,int> hashCode)
    {
        return items.Distinct(new DelegateComparer<T>(equals, hashCode));    
    }
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> items,
        Func<T, T, bool> equals)
    {
        return items.Distinct(new DelegateComparer<T>(equals,null));
    }
}

var uniqueItems=students.Select(s=> new {FirstName=s.FirstName, LastName=s.LastName})
            .Distinct((a,b) => a.FirstName==b.FirstName, c => c.FirstName.GetHashCode()).ToList();

Извините за испорченное форматирование ранее


источник
Это расширение не может обрабатывать тип objectи object. Если оба objectэто stringвсе еще возвращает повторяющиеся строки. Попробуйте FirstNameis typeof objectи назначьте там же string.
CallMeLaNN
Это отличный ответ для типизированных объектов, но не нужен для анонимных типов.
crokusek
5

Интересно, что это работает в C #, но не в VB

Возвращает 26 букв:

var MyBet = "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ";
MyBet.ToCharArray()
.Select(x => new {lower = x.ToString().ToLower(), upper = x.ToString().ToUpper()})
.Distinct()
.Dump();

Возвращает 52 ...

Dim MyBet = "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ"
MyBet.ToCharArray() _
.Select(Function(x) New With {.lower = x.ToString.ToLower(), .upper = x.ToString.ToUpper()}) _
.Distinct() _
.Dump()
GeorgeBarker
источник
11
Если вы добавите Keyключевое слово в анонимный тип, он .Distinct()будет работать как задумано (например New With { Key .lower = x.ToString.ToLower(), Key .upper = x.ToString.ToUpper()}).
Cᴏʀʏ
3
Кори прав. Правильный перевод кода C # new {A = b}есть New {Key .A = b}. Неключевые свойства в анонимных классах VB являются изменяемыми, поэтому они сравниваются по ссылке. В C # все свойства анонимных классов неизменны.
Хайнци
4

Я выполнил небольшой тест и обнаружил, что если свойства являются типами значений, похоже, все работает нормально. Если они не являются типами значений, тогда типу необходимо предоставить собственные реализации Equals и GetHashCode, чтобы он работал. Я думаю, что струны будут работать.

tvanfosson
источник
2

Вы можете создать свой собственный метод Distinct Extension, который принимает лямбда-выражения. Вот пример

Создайте класс, производный от интерфейса IEqualityComparer

public class DelegateComparer<T> : IEqualityComparer<T>
{
    private Func<T, T, bool> _equals;
    private Func<T, int> _hashCode;
    public DelegateComparer(Func<T, T, bool> equals, Func<T, int> hashCode)
    {
        _equals= equals;
        _hashCode = hashCode;
    }
    public bool Equals(T x, T y)
    {
        return _equals(x, y);
    }

    public int GetHashCode(T obj)
    {
        if(_hashCode!=null)
            return _hashCode(obj);
        return obj.GetHashCode();
    }       
}

Затем создайте свой метод Distinct Extension

public static class Extensions
{
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> items, 
        Func<T, T, bool> equals, Func<T,int> hashCode)
    {
        return items.Distinct(new DelegateComparer<T>(equals, hashCode));    
    }
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> items,
        Func<T, T, bool> equals)
    {
        return items.Distinct(new DelegateComparer<T>(equals,null));
    }
}

и вы можете использовать этот метод, чтобы найти отдельные предметы

var uniqueItems=students.Select(s=> new {FirstName=s.FirstName, LastName=s.LastName})
            .Distinct((a,b) => a.FirstName==b.FirstName, c => c.FirstName.GetHashCode()).ToList();
Buildstarted
источник
Это расширение не может обрабатывать тип objectи object. Если оба objectэто stringвсе еще возвращает повторяющиеся строки. Попробуйте FirstNameis typeof objectи назначьте там же string.
CallMeLaNN
0

Если Alphaи Bravoоба наследуют от общего класса, вы сможете диктовать проверку на равенство в родительском классе путем реализации IEquatable<T>.

Например:

public class CommonClass : IEquatable<CommonClass>
{
    // needed for Distinct()
    public override int GetHashCode() 
    {
        return base.GetHashCode();
    }

    public bool Equals(CommonClass other)
    {
        if (other == null) return false;
        return [equality test];
    }
}
орлан-белохвост
источник
поэтому, если вы используете в качестве свойств свои классы анонимных типов, которые реализуют IEquatable <T>, Equals вызывается вместо поведения по умолчанию (проверка всех открытых свойств с помощью отражения?)
D_Guidi
0

Привет, у меня та же проблема, и я нашел решение. Вы должны реализовать интерфейс IEquatable или просто переопределить методы (Equals & GetHashCode). Но это не уловка, уловка в методе GetHashCode. Вы не должны возвращать хеш-код объекта вашего класса, но вы должны возвращать хэш свойства, которое вы хотите сравнить таким образом.

public override bool Equals(object obj)
    {
        Person p = obj as Person;
        if ( obj == null )
            return false;
        if ( object.ReferenceEquals( p , this ) )
            return true;
        if ( p.Age == this.Age && p.Name == this.Name && p.IsEgyptian == this.IsEgyptian )
            return true;
        return false;
        //return base.Equals( obj );
    }
    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }

Как вы видите, у меня есть класс с именем person, который получил 3 свойства (Name, Age, IsEgyptian «Потому что я есть»). В GetHashCode я вернул хэш свойства Name, а не объекта Person.

Попробуйте, и это будет работать ISA. Спасибо, Модер Садик

Модер Садик
источник
1
GetHashCode должен использовать все те же поля и свойства, которые используются для сравнения на равенство, а не только одно из них. то естьpublic override int GetHashCode() { return this.Name.GetHashCode() ^ this.Age.GetHashCode() ^ this.IsEgyptian.GetHashCode(); }
JG в SD
Для получения информации о создании хорошего алгоритма хеширования: stackoverflow.com/questions/263400/…
JG в SD
0

Чтобы это работало в VB.NET, вам нужно указать Keyключевое слово перед каждым свойством в анонимном типе, вот так:

myObjectCollection.Select(Function(item) New With
{
    Key .Alpha = item.propOne,
    Key .Bravo = item.propTwo
}).Distinct()

Я боролся с этим, я думал, что VB.NET не поддерживает этот тип функций, но на самом деле это так.

Alisson
источник