Selecione LINQ distinto com tipos anônimos

150

Então, eu tenho uma coleção de objetos. O tipo exato não é importante. A partir dele, quero extrair todos os pares únicos de um par de propriedades particulares, assim:

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

Portanto, minha pergunta é: Neste caso, Distinct usará o objeto padrão igual a (que será inútil para mim, já que cada objeto é novo) ou pode ser dito para fazer iguais diferentes (nesse caso, valores iguais de Alpha e Bravo => instâncias iguais)? Existe alguma maneira de alcançar esse resultado, se isso não acontecer?

GWLlosa
fonte
Isso é LINQ-to-Objects ou LINQ-to-SQL? Se apenas objetos, você provavelmente está sem sorte. No entanto, se L2S, pode funcionar, pois o DISTINCT seria passado para a instrução SQL.
James Curran

Respostas:

188

Leia o excelente post de K. Scott Allen aqui:

E igualdade para todos ... Tipos anônimos

A resposta curta (e cito):

Acontece que o compilador C # substitui Equals e GetHashCode para tipos anônimos. A implementação dos dois métodos substituídos usa todas as propriedades públicas no tipo para calcular o código hash de um objeto e testar a igualdade. Se dois objetos do mesmo tipo anônimo tiverem os mesmos valores para suas propriedades - os objetos serão iguais.

Portanto, é totalmente seguro usar o método Distinct () em uma consulta que retorna tipos anônimos.

Matt Hamilton
fonte
2
Acho que isso é verdade apenas se as propriedades em si são tipos de valor ou implementam igualdade de valor - veja minha resposta.
23410 tvanfosson
Sim, como ele usa GetHashCode em cada propriedade, ele só funcionaria se cada propriedade tivesse sua própria implementação exclusiva. Eu acho que a maioria dos casos de uso envolveria apenas tipos simples como propriedades, portanto é geralmente seguro.
Matt Hamilton
4
Isso acaba significando que a igualdade de dois tipos anônimos depende da igualdade dos membros, o que é bom para mim, uma vez que os membros são definidos em algum lugar em que eu possa chegar e substituir a igualdade se for necessário. Eu só não queria criar uma classe para isso apenas para substituir iguais.
24410 GWLlosa
3
Pode valer a pena solicitar à Microsoft que introduza a sintaxe "chave" no C # que o VB possui (onde você pode especificar certas propriedades de um tipo anônimo como a 'chave primária' - consulte a postagem do blog ao qual vinculei).
Matt Hamilton
1
Artigo muito interessante. Obrigado!
Alexander Prokofyev
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();

Desculpe pela formatação desarrumada anteriormente


fonte
Esta extensão não pode manipular do tipo objecte object. Se a ambos objecté stringainda retornar as linhas duplicadas. Experimente o tipo FirstNametypeof objecte atribua com o mesmo stringlá.
CallMeLaNN
Essa é uma ótima resposta para objetos digitados, mas não é necessária para tipos anônimos.
crokusek
5

Interessante que ele funcione em C #, mas não em VB

Retorna as 26 letras:

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

Retorna 52 ...

Dim MyBet = "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuUvVwWxXyYzZ"
MyBet.ToCharArray() _
.Select(Function(x) New With {.lower = x.ToString.ToLower(), .upper = x.ToString.ToUpper()}) _
.Distinct() _
.Dump()
GeorgeBarker
fonte
11
Se você adicionar a Keypalavra-chave ao tipo anônimo, .Distinct()ela funcionará como pretendido (por exemplo New With { Key .lower = x.ToString.ToLower(), Key .upper = x.ToString.ToUpper()}).
Cᴏʀʏ
3
Cory está certo. A tradução correta do código C # new {A = b}é New {Key .A = b}. Propriedades não-chave em classes VB anônimas são mutáveis, e é por isso que são comparadas por referência. Em C #, todas as propriedades de classes anônimas são imutáveis.
Heinzi
4

Fiz um pequeno teste e descobri que, se as propriedades são do tipo valor, parece funcionar bem. Se eles não são tipos de valor, o tipo precisa fornecer suas próprias implementações Equals e GetHashCode para que funcione. Penso que as cordas funcionariam.

tvanfosson
fonte
2

Você pode criar seu próprio método de extensão distinta, que utiliza a expressão lambda. Aqui está um exemplo

Crie uma classe que deriva da interface 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();
    }       
}

Em seguida, crie seu método 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));
    }
}

e você pode usar esse método para encontrar itens distintos

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
fonte
Esta extensão não pode manipular do tipo objecte object. Se a ambos objecté stringainda retornar as linhas duplicadas. Experimente o tipo FirstNametypeof objecte atribua com o mesmo stringlá.
CallMeLaNN
0

Se Alphae Bravoambos herdarem de uma classe comum, você poderá ditar a verificação de igualdade na classe pai implementando IEquatable<T>.

Por exemplo:

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];
    }
}
ern
fonte
Então, se você usar como propriedades de seus tipos de classes anônimas que implementos IEquatable <T>, iguais é chamado em vez do comportamento padrão (verificação de todas as propriedades públicas através reflexão?)
D_Guidi
0

Olá, eu tenho o mesmo problema e encontrei uma solução. Você precisa implementar a interface IEquatable ou simplesmente substituir os métodos (Equals & GetHashCode). Mas esse não é o truque, o truque que vem no método GetHashCode. Você não deve retornar o código de hash do objeto de sua classe, mas deve retornar o hash da propriedade que deseja comparar dessa forma.

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();
    }

Como você vê, eu recebi uma classe chamada person e tenho 3 propriedades (Name, Age, IsEgyptian "Porque eu sou"). No GetHashCode, retornei o hash da propriedade Name e não o objeto Person.

Experimente e funcionará o ISA. Obrigado, Modather Sadik

Modather Sadik
fonte
1
GetHashCode deve usar todos os mesmos campos e propriedades usados ​​na comparação para igualdade, não apenas um deles. iepublic override int GetHashCode() { return this.Name.GetHashCode() ^ this.Age.GetHashCode() ^ this.IsEgyptian.GetHashCode(); }
JG em SD
Para obter informações sobre como gerar um bom algoritmo de hash: stackoverflow.com/questions/263400/…
JG no SD
0

Para que ele funcione no VB.NET, você precisa especificar o Key palavra chave antes de cada propriedade do tipo anônimo, assim:

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

Eu estava lutando com isso, pensei que o VB.NET não suporta esse tipo de recurso, mas na verdade ele suporta.

Alisson
fonte