Не может ли оператор == быть применен к универсальным типам в C #?

326

Согласно документации ==оператора в MSDN ,

Для предопределенных типов значений оператор равенства (==) возвращает true, если значения его операндов равны, в противном случае - false. Для ссылочных типов, отличных от string, == возвращает true, если два его операнда ссылаются на один и тот же объект. Для типа строки == сравнивает значения строк. Пользовательские типы значений могут перегружать оператор == (см. Оператор). То же самое можно сказать и о пользовательских ссылочных типах, хотя по умолчанию == ведет себя так, как описано выше, как для предопределенных, так и для пользовательских ссылочных типов.

Так почему этот фрагмент кода не компилируется?

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

Я получаю сообщение об ошибке. Оператор '==' не может быть применен к операндам типа 'T' и 'T' . Интересно почему, поскольку, насколько я понимаю, ==оператор предопределен для всех типов?

Редактировать: Спасибо всем. Сначала я не заметил, что утверждение касается только ссылочных типов. Я также подумал, что побитовое сравнение предоставляется для всех типов значений, что, как я теперь знаю, не является правильным.

Но если я использую ссылочный тип, будет ли ==оператор использовать предопределенное сравнение ссылок, или он будет использовать перегруженную версию оператора, если тип определен?

Редактировать 2: методом проб и ошибок мы узнали, что ==оператор будет использовать предопределенное эталонное сравнение при использовании неограниченного универсального типа. Фактически, компилятор будет использовать лучший метод, который он может найти для аргумента ограниченного типа, но не будет искать дальше. Например, приведенный ниже код будет всегда печататься true, даже когда Test.test<B>(new B(), new B())вызывается:

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); } }
Хосам Али
источник
Смотрите мой ответ снова для ответа на ваш дополнительный вопрос.
Джованни Гальбо
Возможно, было бы полезно понять, что даже без обобщенных типов есть некоторые типы, для которых ==нельзя использовать два операнда одного и того же типа. Это верно для structтипов (кроме «предопределенных» типов), которые не перегружают operator ==. В качестве простого примера попробуйте следующее:var map = typeof(string).GetInterfaceMap(typeof(ICloneable)); Console.WriteLine(map == map); /* compile-time error */
Джеппе Стиг Нильсен
Продолжая мой собственный старый комментарий. Например (см. Другой поток ), с var kvp1 = new KeyValuePair<int, int>(); var kvp2 = kvp1;, тогда вы не можете проверить, kvp1 == kvp2потому что KeyValuePair<,>это структура, это не предопределенный тип C #, и он не перегружает operator ==. Тем не менее, приведен пример, var li = new List<int>(); var e1 = li.GetEnumerator(); var e2 = e1;с которым вы не можете справиться e1 == e2(здесь у нас есть вложенная структура List<>.Enumerator( "List`1+Enumerator[T]"вызывается средой выполнения), которая не перегружается ==).
Джеппе Стиг Нильсен
RE: «Так почему этот фрагмент кода не компилируется?» - boolvoid
Э-
1
@ BrainSlugs83 Спасибо за обнаружение 10-летней ошибки!
Хосам Али

Ответы:

143

«... по умолчанию == ведет себя так, как описано выше как для предопределенных, так и для пользовательских типов ссылок.»

Тип T не обязательно является ссылочным типом, поэтому компилятор не может сделать это предположение.

Тем не менее, это скомпилируется, потому что это более явно:

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

Ответьте на дополнительный вопрос: «Но если я использую ссылочный тип, будет ли оператор == использовать предопределенное сравнение ссылок или будет перегруженная версия оператора, если тип определен?»

Я бы подумал, что == на Generics будет использовать перегруженную версию, но следующий тест демонстрирует обратное. Интересно ... Я хотел бы знать, почему! Если кто-то знает, пожалуйста, поделитесь.

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

Вывод

Встроенный: перегружен == называется

Общий:

Нажмите любую клавишу для продолжения . , ,

Follow Up 2

Я хочу отметить, что изменение моего метода сравнения на

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

вызывает перегруженный оператор ==. Я предполагаю, что без указания типа (как где ) компилятор не может сделать вывод, что он должен использовать перегруженный оператор ... хотя я думаю, что у него будет достаточно информации, чтобы принять это решение, даже не указав тип.

Джованни Гальбо
источник
Спасибо. Я не заметил, что это утверждение касалось только ссылочных типов.
Хосам Али
4
Re: Follow Up 2: На самом деле компилятор свяжет его с лучшим методом, который он найдет, в данном случае это Test.op_Equal. Но если у вас есть класс, производный от Test и переопределяющий оператор, то оператор Test все равно будет вызываться.
Хосам Али
4
Хорошей практикой, на которую я хотел бы обратить внимание, является то, что вы всегда должны выполнять фактическое сравнение внутри переопределенного Equalsметода (а не в ==операторе).
jpbochi
11
Разрешение перегрузки происходит во время компиляции. Таким образом, когда мы имеем ==между универсальными типами Tи T, наилучшая перегрузка найдена, учитывая, какие ограничения переносятся T(есть специальное правило, что он никогда не будет помечать тип значения для этого (который дал бы бессмысленный результат), следовательно, должно быть некоторое ограничение, гарантирующее, что это ссылочный тип). В вашем Follow Up 2 , если вы входите с DerivedTestобъектами и DerivedTestпроизводите, Testно ==вводите новую перегрузку , у вас снова будет «проблема». Какая перегрузка называется, «сгорает» в IL во время компиляции.
Джеппе Стиг Нильсен
1
странно, что это работает для общих ссылочных типов (где вы ожидаете, что это сравнение будет на равенстве ссылок), но для строк оно также использует равенство ссылок - так что вы можете в конечном итоге сравнить 2 одинаковые строки и иметь == (когда в универсальный метод с ограничением класса) говорят, что они разные.
JonnyRaa
292

Как уже говорили другие, он будет работать только тогда, когда T ограничен ссылочным типом. Без каких-либо ограничений вы можете сравнивать со значением NULL, но только со значением NULL - и это сравнение всегда будет ложным для типов значений, не допускающих значения NULL.

Вместо вызова Equals лучше использовать IComparer<T>- и если у вас нет больше информации, EqualityComparer<T>.Defaultэто хороший выбор:

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

Помимо всего прочего, это позволяет избежать бокса / кастинга.

Джон Скит
источник
Спасибо. Я пытался написать простой класс-обертку, поэтому я просто хотел делегировать операцию фактическому обернутому члену. Но знание EqualityComparer <T> .Default определенно добавило мне ценность. :)
Хосам Али
Незначительное в стороне, Джон; Вы можете отметить комментарий re pobox vs yoda в моем сообщении.
Марк Гравелл
4
Хороший совет по использованию EqualityComparer <T>
chakrit
1
+1 за указание на то, что он может сравниваться со значением NULL, а для типа, отличного от NULL, он всегда будет ложным
Джалал Сказал
@BlueRaja: Да, потому что существуют специальные правила для сравнения с нулевым литералом. Следовательно, «без каких-либо ограничений вы можете сравнить с нулем, но только с нулем». Это уже в ответе. Итак, почему именно это не может быть правильным?
Джон Скит
41

В общем, EqualityComparer<T>.Default.Equalsследует выполнять работу с чем-либо, что реализует IEquatable<T>или имеет разумную Equalsреализацию.

Если, однако, ==и Equalsпо какой-то причине реализованы по-другому, то моя работа над универсальными операторами должна быть полезной; поддерживает операторские версии (среди прочих):

  • Равен (значение T1, значение T2)
  • NotEqual (значение T1, значение T2)
  • GreaterThan (значение T1, значение T2)
  • LessThan (значение T1, значение T2)
  • GreaterThanOrEqual (значение T1, значение T2)
  • LessThanOrEqual (значение T1, значение T2)
Марк Гравелл
источник
Очень интересная библиотека! :) (Примечание: Могу ли я предложить ссылку на www.yoda.arachsys.com, потому что почтовый ящик был заблокирован брандмауэром на моем рабочем месте? Возможно, другие могут столкнуться с той же проблемой.)
Хосам Али
Идея в том, что pobox.com/~skeet всегда будет указывать на мой сайт - даже если он будет перемещен в другое место. Я , как правило , размещать ссылки через pobox.com ради потомства , - но вы можете в настоящее время заменить yoda.arachsys.com вместо этого.
Джон Скит
Проблема с pobox.com заключается в том, что это веб-служба электронной почты (или так говорит брандмауэр компании), поэтому она заблокирована. Вот почему я не мог перейти по его ссылке.
Хосам Али
«Если, однако, == и Equals по разным причинам реализованы по-разному», - Святой курит! Что за однако! Может быть, мне просто нужно увидеть вариант использования наоборот, но библиотека с расходящейся равной семантикой, скорее всего, столкнется с большими проблемами, чем с дженериками.
Эдвард Брей
@ EdwardBrey, ты не ошибаешься; было бы неплохо, если бы компилятор мог обеспечить это, но ...
Марк Гравелл
31

Так много ответов, и ни один не объясняет ПОЧЕМУ? (который явно спросил Джованни) ...

Обобщения .NET не действуют как шаблоны C ++. В шаблонах C ++ разрешение перегрузки происходит после того, как фактические параметры шаблона известны.

В универсальных .NET (в том числе C #) разрешение перегрузки происходит без знания фактических универсальных параметров. Единственная информация, которую компилятор может использовать для выбора вызываемой функции, связана с ограничениями типов общих параметров.

Бен Фойгт
источник
2
но почему компилятор не может рассматривать их как универсальный объект? в конце концов ==работает для всех типов, будь то ссылочные типы или типы значений. Это должен быть вопрос, на который я не думаю, что вы ответили.
Nawfal
4
@nawfal: На самом деле нет, ==не работает для всех типов значений. Что еще более важно, он не имеет одинакового значения для всех типов, поэтому компилятор не знает, что с ним делать.
Бен Фойгт
1
Бен, о да, я пропустил обычаи, которые мы можем создать безо всяких ==. Можете ли вы включить эту часть в свой ответ, так как я думаю, что это главное здесь
nawfal
12

Компиляция не может знать, что T не может быть структурой (типом значения). Так что вы должны сказать, что это может быть только ссылочный тип, я думаю:

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

Это потому, что если T может быть типом значения, могут быть случаи, когда x == yон будет плохо сформирован - в тех случаях, когда для типа не определен оператор ==. То же самое произойдет для этого, что более очевидно:

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

Это тоже не помогает, потому что вы можете передать тип T, у которого не будет функции foo. C # заставляет вас убедиться, что все возможные типы всегда имеют функцию foo. Это делается с помощью предложения where.

Йоханнес Шауб - Литб
источник
1
Спасибо за разъяснения. Я не знал, что типы значений не поддерживают оператор == из коробки.
Хосам Али
1
Хосам, я тестировал с gmcs (mono), и он всегда сравнивает ссылки. (то есть он не использует опционально определенный оператор == для T)
Йоханнес Шауб - litb
В этом решении есть одна оговорка: оператор == не может быть перегружен; посмотрите этот вопрос StackOverflow .
Дмитрий С.
8

Похоже, что без ограничения класса:

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

Следует понимать, что в то время как classограничение Equalsв ==операторе наследуется от Object.Equals, а у структуры переопределяется ValueType.Equals.

Обратите внимание, что:

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

также выдает ту же ошибку компилятора.

Пока я не понимаю, почему компилятор отвергает сравнение операторов равенства типов значений. Я точно знаю, что это работает:

bool Compare<T> (T x, T y)
{
    return x.Equals(y);
}
Джон Лимджап
источник
ты знаешь, что я в целом c # нуб. но я думаю, что это не удается, потому что компилятор не знает, что делать. поскольку T еще не известно, то, что делается, зависит от типа T, если допустимы типы значений. для ссылок ссылки просто сравниваются независимо от T. если вы делаете .Equals, то просто вызывается .Equal.
Йоханнес Шауб -
но если вы делаете == для типа значения, тип значения не обязательно должен реализовывать этот оператор.
Йоханнес Шауб -
Это имеет смысл, литб :) Возможно, что определяемые пользователем структуры не перегружают ==, следовательно, компилятор не работает.
Джон Лимджап
2
Первый метод сравнения не использует, Object.Equalsно вместо этого проверяет равенство ссылок. Например, Compare("0", 0.ToString())вернет false, поскольку аргументы будут ссылками на разные строки, каждая из которых имеет ноль в качестве единственного символа.
суперкат
1
Незначительное замечание по поводу последнего - вы не ограничивали его структурами, так что NullReferenceExceptionможет случиться.
Flynn1179
6

Ну, в моем случае я хотел провести модульное тестирование оператора равенства. Мне нужно было вызывать код под операторами равенства без явной установки универсального типа. Советы для EqualityComparerне были полезны как EqualityComparerвызываемый Equalsметод, но не оператор равенства.

Вот как у меня это работает с универсальными типами путем построения LINQ. Он называет правильный код ==и !=операторов:

/// <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);
}
У. Булле
источник
4

Существует запись MSDN Connect для этого здесь

Ответ Алекса Тернера начинается с:

К сожалению, такое поведение разработано специально, и не существует простого решения, позволяющего использовать == с параметрами типа, которые могут содержать типы значений.

Реджеп
источник
4

Если вы хотите убедиться, что операторы вашего пользовательского типа вызываются, вы можете сделать это с помощью отражения. Просто получите тип, используя ваш универсальный параметр, и получите MethodInfo для нужного оператора (например, op_Equality, op_Inequality, op_LessThan ...).

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

Затем выполните оператор, используя метод Invoke для MethodInfo, и передайте объекты в качестве параметров.

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

Это вызовет ваш перегруженный оператор, а не тот, который определен ограничениями, примененными к универсальному параметру. Может быть непрактичным, но может пригодиться для модульного тестирования ваших операторов при использовании базового базового класса, который содержит пару тестов.

Christophe
источник
3

Я написал следующую функцию, глядя на последний MSDN. Он может легко сравнить два объекта xи y:

static bool IsLessThan(T x, T y) 
{
    return ((IComparable)(x)).CompareTo(y) <= 0;
}
Чарли
источник
4
Вы можете избавиться от своих логических return ((IComparable)(x)).CompareTo(y) <= 0;
выражений
1

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

Вышесказанное будет работать, потому что == позаботится в случае пользовательских типов ссылок.
В случае типов значений == может быть переопределено. В этом случае "! =" Также должно быть определено.

Я думаю, что это может быть причиной, это запрещает общее сравнение, используя "==".

shahkalpesh
источник
2
Спасибо. Я считаю, что ссылочные типы также могут переопределять оператор. Но причина неудачи теперь понятна.
Хосам Али
1
==Маркер используется для двух разных операторов. Если для данных типов операндов существует совместимая перегрузка оператора равенства, эта перегрузка будет использоваться. В противном случае, если оба операнда являются ссылочными типами, совместимыми друг с другом, будет использовано сравнение ссылок. Обратите внимание, что в приведенном Compareвыше методе компилятор не может сказать, что первое значение применимо, но может сказать, что второе значение применимо, поэтому ==токен будет использовать последнее, даже если Tперегружает оператор проверки равенства (например, если он имеет тип String) .
суперкат
0

.Equals()Работает для меня в то время как TKeyэто общий тип.

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


    // ...
}
Масуд Дарвишян
источник
Это x.Id.Equalsне так id.Equals. Предположительно, компилятор знает что-то о типе x.
Хосам Али