O operador == não pode ser aplicado a tipos genéricos em C #?

326

De acordo com a documentação do ==operador no MSDN ,

Para tipos de valor predefinidos, o operador de igualdade (==) retornará true se os valores de seus operandos forem iguais, caso contrário, false. Para tipos de referência diferentes de string, == retorna true se seus dois operandos se referem ao mesmo objeto. Para o tipo de string, == compara os valores das strings. Os tipos de valores definidos pelo usuário podem sobrecarregar o operador == (consulte o operador). Os tipos de referência definidos pelo usuário também podem, embora por padrão == se comporte como descrito acima para os tipos de referência predefinidos e definidos pelo usuário.

Então, por que esse snippet de código falha na compilação?

bool Compare<T>(T x, T y) { return x == y; }

Eu recebo o erro Operador '==' não pode ser aplicado a operandos do tipo 'T' e 'T' . Eu me pergunto por que, desde que eu entenda, o ==operador é predefinido para todos os tipos?

Edit: Obrigado a todos. Inicialmente, não percebi que a declaração se referia apenas a tipos de referência. Eu também pensei que a comparação bit a bit é fornecida para todos os tipos de valor, que agora sei que não estão corretos.

Mas, no caso de eu estar usando um tipo de referência, o ==operador usaria a comparação de referência predefinida ou a versão sobrecarregada do operador se um tipo o definisse?

Edit 2: Por tentativa e erro, aprendemos que o ==operador usará a comparação de referência predefinida ao usar um tipo genérico irrestrito. Na verdade, o compilador usará o melhor método possível para o argumento de tipo restrito, mas não procurará mais. Por exemplo, o código abaixo sempre será impresso true, mesmo quando Test.test<B>(new B(), new B())for chamado:

class A { public static bool operator==(A x, A y) { return true; } }
class B : A { public static bool operator==(B x, B y) { return false; } }
class Test { void test<T>(T a, T b) where T : A { Console.WriteLine(a == b); } }
Hosam Aly
fonte
Veja minha resposta novamente para obter a resposta para sua pergunta de acompanhamento.
Giovanni Galbo
Pode ser útil entender que, mesmo sem genéricos, existem alguns tipos para os quais ==não é permitido entre dois operandos do mesmo tipo. Isso vale para structtipos (exceto tipos "predefinidos") que não sobrecarregam o operator ==. Como um exemplo simples, tente o seguinte:var map = typeof(string).GetInterfaceMap(typeof(ICloneable)); Console.WriteLine(map == map); /* compile-time error */
Jeppe Stig Nielsen
Continuando meu próprio comentário antigo. Por exemplo (consulte outro thread ), com var kvp1 = new KeyValuePair<int, int>(); var kvp2 = kvp1;, então você não pode verificar kvp1 == kvp2porque KeyValuePair<,>é uma estrutura, não é um tipo predefinido de C # e não sobrecarrega o operator ==. No entanto, é dado um exemplo var li = new List<int>(); var e1 = li.GetEnumerator(); var e2 = e1;com o qual você não pode fazer e1 == e2(aqui temos a estrutura aninhada List<>.Enumerator(chamada "List`1+Enumerator[T]"pelo tempo de execução) que não sobrecarrega ==).
Jeppe Stig Nielsen
RE: "Então, por que esse snippet de código falha na compilação?" - Er ... porque você não pode retornar um boolde um void...
BrainSlugs83
1
@ BrainSlugs83 Obrigado por capturar um bug de 10 anos!
Hosam Aly

Respostas:

143

"... por padrão == se comporta como descrito acima para os tipos de referência predefinidos e definidos pelo usuário."

O tipo T não é necessariamente um tipo de referência; portanto, o compilador não pode fazer essa suposição.

No entanto, isso será compilado porque é mais explícito:

    bool Compare<T>(T x, T y) where T : class
    {
        return x == y;
    }

Continue com a pergunta adicional: "Mas, caso eu esteja usando um tipo de referência, o operador == usaria a comparação de referência predefinida ou a versão sobrecarregada do operador se um tipo o definisse?"

Eu pensaria que o == no Generics usaria a versão sobrecarregada, mas o teste a seguir demonstra o contrário. Interessante ... eu adoraria saber o porquê! Se alguém souber, por favor, compartilhe.

namespace TestProject
{
 class Program
 {
    static void Main(string[] args)
    {
        Test a = new Test();
        Test b = new Test();

        Console.WriteLine("Inline:");
        bool x = a == b;
        Console.WriteLine("Generic:");
        Compare<Test>(a, b);

    }


    static bool Compare<T>(T x, T y) where T : class
    {
        return x == y;
    }
 }

 class Test
 {
    public static bool operator ==(Test a, Test b)
    {
        Console.WriteLine("Overloaded == called");
        return a.Equals(b);
    }

    public static bool operator !=(Test a, Test b)
    {
        Console.WriteLine("Overloaded != called");
        return a.Equals(b);
    }
  }
}

Resultado

Inline: Sobrecarregado == chamado

Genérico:

Pressione qualquer tecla para continuar . . .

Acompanhamento 2

Quero ressaltar que alterar meu método de comparação para

    static bool Compare<T>(T x, T y) where T : Test
    {
        return x == y;
    }

faz com que o operador == sobrecarregado seja chamado. Eu acho que sem especificar o tipo (como um local ), o compilador não pode inferir que ele deve usar o operador sobrecarregado ... embora eu ache que teria informações suficientes para tomar essa decisão, mesmo sem especificar o tipo.

Giovanni Galbo
fonte
Obrigado. Eu não percebi que a declaração era apenas sobre tipos de referência.
Hosam Aly 24/12/08
4
Re: Acompanhamento 2: Na verdade, o compilador vinculará o melhor método que encontrar, que é, neste caso, Test.op_Equal. Mas se você teve uma classe que deriva de Teste e substitui o operador, o operador do Teste ainda será chamado.
Hosam Aly 24/12/08
4
Uma boa prática que gostaria de salientar é que você deve sempre fazer a comparação real dentro de um Equalsmétodo substituído (não no ==operador).
jpbochi
11
A resolução de sobrecarga acontece em tempo de compilação. Portanto, quando temos ==entre tipos genéricos Te T, a melhor sobrecarga é encontrada, considerando as restrições que são transportadas T(existe uma regra especial de que nunca incluirá um tipo de valor para isso (o que daria um resultado sem sentido), portanto, deve haver alguma restrição que garanta que é um tipo de referência). No Follow Up 2 , se você entrar com DerivedTestobjetos e DerivedTestderiva, Testmas introduz uma nova sobrecarga ==, terá o "problema" novamente. Qual sobrecarga é chamada, é "queimada" na IL no tempo de compilação.
Jeppe Stig Nielsen
1
De maneira estranha, isso parece funcionar para tipos de referência geral (onde você espera que essa comparação seja sobre igualdade de referência), mas para strings parece usar também igualdade de referência - para que você possa comparar duas strings idênticas e ter == (quando em um método genérico com restrição de classe) dizem que são diferentes.
21813 JonnyRaa
292

Como outros já disseram, ele só funcionará quando T for restrito a ser um tipo de referência. Sem restrições, você pode comparar com nulo, mas apenas nulo - e essa comparação sempre será falsa para os tipos de valores não nulos.

Em vez de chamar Igual, é melhor usar uma IComparer<T>- e se você não tiver mais informações, EqualityComparer<T>.Defaulté uma boa opção:

public bool Compare<T>(T x, T y)
{
    return EqualityComparer<T>.Default.Equals(x, y);
}

Além de qualquer outra coisa, isso evita boxe / fundição.

Jon Skeet
fonte
Obrigado. Eu estava tentando escrever uma classe de wrapper simples, então só queria delegar a operação ao membro de quebra real. Mas conhecer o EqualityComparer <T> .Default certamente agregou valor para mim. :)
Hosam Aly
Menor à parte, Jon; você pode querer observar o comentário re pobox vs yoda no meu post.
Marc Gravell
4
Dica boa sobre o uso de EqualityComparer <T>
chakrit
1
+1 por apontar que ele pode comparar a nula e por tipo de valor não nulo será sempre falsa
Jalal Disse
@BlueRaja: Sim, porque existem regras especiais para comparações com o literal nulo. Portanto "sem restrições, você pode comparar com nulo, mas apenas nulo". Já está na resposta. Então, por que exatamente isso não pode estar correto?
Jon Skeet
41

Em geral, EqualityComparer<T>.Default.Equalsdeve fazer o trabalho com qualquer coisa que implemente IEquatable<T>, ou que tenha uma Equalsimplementação sensata .

Se, no entanto, ==e Equalsforem implementados de maneira diferente por algum motivo, meu trabalho com operadores genéricos deve ser útil; suporta as versões do operador de (entre outros):

  • Igual (valor T1, valor T2)
  • NotEqual (valor T1, valor T2)
  • GreaterThan (valor T1, valor T2)
  • LessThan (valor T1, valor T2)
  • GreaterThanOrEqual (valor T1, valor T2)
  • LessThanOrEqual (valor T1, valor T2)
Marc Gravell
fonte
Biblioteca muito interessante! :) (Observação: posso sugerir o uso do link para www.yoda.arachsys.com, porque o pobox foi bloqueado pelo firewall no meu local de trabalho? É possível que outros possam enfrentar o mesmo problema.)
Hosam Aly
A ideia é que pobox.com/~skeet irá sempre apontar para o meu site - mesmo se ele se move em outro lugar. Costumo postar links via pobox.com por motivos de posteridade - mas atualmente você pode substituir o yoda.arachsys.com.
Jon Skeet
O problema com o pobox.com é que ele é um serviço de email baseado na Web (ou é o que o firewall da empresa diz), por isso está bloqueado. Por isso não consegui seguir o link.
Hosam Aly
"Se, no entanto, == e iguais forem implementados de maneira diferente por algum motivo" - Santo fuma! Que porém! Talvez eu só precise ver um caso de uso ao contrário, mas uma biblioteca com semânticas iguais divergentes provavelmente terá problemas maiores do que problemas com genéricos.
Edward Brey
@EdwardBrey você não está errado; seria bom se o compilador poderia impor isso, mas ...
Marc Gravell
31

Tantas respostas, e nenhuma explica o PORQUÊ? (que Giovanni perguntou explicitamente) ...

Os genéricos do .NET não agem como modelos C ++. Nos modelos C ++, a resolução de sobrecarga ocorre depois que os parâmetros reais do modelo são conhecidos.

Nos genéricos do .NET (incluindo C #), a resolução de sobrecarga ocorre sem conhecer os parâmetros genéricos reais. As únicas informações que o compilador pode usar para escolher a função a chamar são provenientes de restrições de tipo nos parâmetros genéricos.

Ben Voigt
fonte
2
mas por que o compilador não pode tratá-los como um objeto genérico? afinal ==funciona para todos os tipos, sejam tipos de referência ou tipos de valor. Essa deveria ser a pergunta à qual acho que você não respondeu.
Nawfal
4
@awawfal: Na verdade não, ==não funciona para todos os tipos de valor. Mais importante, ele não tem o mesmo significado para todos os tipos; portanto, o compilador não sabe o que fazer com ele.
Ben Voigt
1
Ben, sim, eu perdi as estruturas personalizadas que podemos criar sem nenhuma ==. Você pode incluir essa parte também em sua resposta como eu acho que esse é o ponto principal aqui
Nawfal
12

A compilação não pode saber que T não pode ser uma estrutura (tipo de valor). Então você tem que dizer que só pode ser do tipo de referência, eu acho:

bool Compare<T>(T x, T y) where T : class { return x == y; }

É porque se T poderia ser um tipo de valor, poderia haver casos em x == yque seria mal formado - nos casos em que um tipo não tem um operador == definido. O mesmo acontecerá com isso, que é mais óbvio:

void CallFoo<T>(T x) { x.foo(); }

Isso também falha, porque você poderia passar um tipo T que não teria uma função foo. O C # obriga a garantir que todos os tipos possíveis sempre tenham uma função foo. Isso é feito pela cláusula where.

Johannes Schaub - litb
fonte
1
Obrigado pelo esclarecimento. Eu não sabia que os tipos de valor não suportavam o operador == pronto para uso.
Hosam Aly 24/12/08
1
Hosam, eu testei com gmcs (mono), e ele sempre compara referências. (isto é, não utilize um operador opcionalmente definido para == T)
Johannes Schaub - litb
Há uma ressalva nesta solução: o operador == não pode ser sobrecarregado; veja esta pergunta StackOverflow .
Dimitri C.
8

Parece que sem a restrição de classe:

bool Compare<T> (T x, T y) where T: class
{
    return x == y;
}

Deve-se perceber que, enquanto classrestrito Equalsno ==operador herda Object.Equals, enquanto o de uma estrutura substitui ValueType.Equals.

Observe que:

bool Compare<T> (T x, T y) where T: struct
{
    return x == y;
}

também fornece o mesmo erro do compilador.

Até agora não entendo por que a comparação de operadores de igualdade de tipo de valor é rejeitada pelo compilador. Eu realmente sei que isso funciona:

bool Compare<T> (T x, T y)
{
    return x.Equals(y);
}
Jon Limjap
fonte
Você sabe que sou um total de c # noob. mas acho que falha porque o compilador não sabe o que fazer. como T ainda não é conhecido, o que é feito depende do tipo T se tipos de valores forem permitidos. para referências, as referências são comparadas apenas independentemente de T. se você fizer .Equals, então .Equal será chamado.
Johannes Schaub - litb 24/12/08
mas se você fizer == em um tipo de valor, ele não precisará implementar esse operador.
Johannes Schaub - litb 24/12/08
Isso faria sentido, litb :) É possível que estruturas definidas pelo usuário não sobrecarreguem ==, portanto, o compilador falha.
Jon Limjap
2
O primeiro método de comparação não usa, Object.Equalsmas testa a igualdade de referência. Por exemplo, Compare("0", 0.ToString())retornaria false, pois os argumentos seriam referências a seqüências distintas, ambas com zero como único caractere.
Supercat
1
Um problema menor nesse último - você não o restringiu às estruturas, para que isso NullReferenceExceptionpudesse acontecer.
Flynn1179
6

Bem, no meu caso, eu queria testar o operador de igualdade. Eu precisava chamar o código sob os operadores de igualdade sem definir explicitamente o tipo genérico. As recomendações EqualityComparernão foram úteis como método EqualityComparerchamado Equals, mas não o operador de igualdade.

Aqui está como eu consegui isso trabalhando com tipos genéricos criando um LINQ. Ele chama o código certo para operadores ==e !=:

/// <summary>
/// Gets the result of "a == b"
/// </summary>
public bool GetEqualityOperatorResult<T>(T a, T b)
{
    // declare the parameters
    var paramA = Expression.Parameter(typeof(T), nameof(a));
    var paramB = Expression.Parameter(typeof(T), nameof(b));
    // get equality expression for the parameters
    var body = Expression.Equal(paramA, paramB);
    // compile it
    var invokeEqualityOperator = Expression.Lambda<Func<T, T, bool>>(body, paramA, paramB).Compile();
    // call it
    return invokeEqualityOperator(a, b);
}

/// <summary>
/// Gets the result of "a =! b"
/// </summary>
public bool GetInequalityOperatorResult<T>(T a, T b)
{
    // declare the parameters
    var paramA = Expression.Parameter(typeof(T), nameof(a));
    var paramB = Expression.Parameter(typeof(T), nameof(b));
    // get equality expression for the parameters
    var body = Expression.NotEqual(paramA, paramB);
    // compile it
    var invokeInequalityOperator = Expression.Lambda<Func<T, T, bool>>(body, paramA, paramB).Compile();
    // call it
    return invokeInequalityOperator(a, b);
}
U. Bulle
fonte
4

Há uma entrada do MSDN Connect para isso aqui

A resposta de Alex Turner começa com:

Infelizmente, esse comportamento ocorre por design e não há uma solução fácil para habilitar o uso de == com parâmetros de tipo que podem conter tipos de valor.

Recep
fonte
4

Se você quiser garantir que os operadores do seu tipo personalizado sejam chamados, faça isso por meio de reflexão. Basta obter o tipo usando seu parâmetro genérico e recuperar o MethodInfo para o operador desejado (por exemplo, op_Equality, op_Inequality, op_LessThan ...).

var methodInfo = typeof (T).GetMethod("op_Equality", 
                             BindingFlags.Static | BindingFlags.Public);    

Em seguida, execute o operador usando o método Invoke do MethodInfo e passe os objetos como parâmetros.

var result = (bool) methodInfo.Invoke(null, new object[] { object1, object2});

Isso chamará seu operador sobrecarregado e não aquele definido pelas restrições aplicadas no parâmetro genérico. Pode não ser prático, mas pode ser útil para testar a unidade de seus operadores ao usar uma classe base genérica que contém alguns testes.

Christophe
fonte
3

Eu escrevi a seguinte função olhando o último msdn. Pode facilmente comparar dois objetos xe y:

static bool IsLessThan(T x, T y) 
{
    return ((IComparable)(x)).CompareTo(y) <= 0;
}
Charlie
fonte
4
Você pode se livrar de seus booleanos e escreverreturn ((IComparable)(x)).CompareTo(y) <= 0;
aloisdg movendo para codidact.com 26/06/16
1

bool Compare(T x, T y) where T : class { return x == y; }

O acima funcionará porque == é resolvido no caso de tipos de referência definidos pelo usuário.
No caso de tipos de valor, == pode ser substituído. Nesse caso, "! =" Também deve ser definido.

Eu acho que esse poderia ser o motivo, ele não permite a comparação genérica usando "==".

shahkalpesh
fonte
2
Obrigado. Acredito que os tipos de referência também podem substituir o operador. Mas a razão do fracasso agora está clara.
Hosam Aly
1
O ==token é usado para dois operadores diferentes. Se para os tipos de operando especificados existir uma sobrecarga compatível do operador de igualdade, essa sobrecarga será usada. Caso contrário, se os dois operandos forem tipos de referência compatíveis entre si, uma comparação de referência será usada. Observe que no Comparemétodo acima, o compilador não pode dizer que o primeiro significado se aplica, mas pode dizer o segundo significado, então o ==token o utilizará mesmo que Tsobrecarregue o operador de verificação de igualdade (por exemplo, se for do tipo String) .
Supercat
0

O .Equals()trabalho para mim TKeyé um tipo genérico.

public virtual TOutputDto GetOne(TKey id)
{
    var entity =
        _unitOfWork.BaseRepository
            .FindByCondition(x => 
                !x.IsDelete && 
                x.Id.Equals(id))
            .SingleOrDefault();


    // ...
}
Masoud Darvishian
fonte
Isso x.Id.Equalsnão é id.Equals. Presumivelmente, o compilador sabe algo sobre o tipo de x.
Hosam Aly