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

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

For predefined value types, the equality operator (==) returns true if the values of its operands are equal, false otherwise. For reference types other than string, == returns true if its two operands refer to the same object. For the string type, == compares the values of the strings. User-defined value types can overload the == operator (see operator). So can user-defined reference types, although by default == behaves as described above for both predefined and user-defined reference types.

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

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

См. Мой ответ еще раз, чтобы получить ответ на следующий вопрос.

Giovanni Galbo 24.12.2008 15:48

Было бы полезно понять, что даже без универсальных типов есть некоторые типы, для которых == не допускается между двумя операндами одного и того же типа. Это верно для типов struct (кроме «предопределенных» типов), которые не перегружают operator ==. В качестве простого примера попробуйте следующее: var map = typeof(string).GetInterfaceMap(typeof(ICloneable)); Console.WriteLine(map == map); /* compile-time error */

Jeppe Stig Nielsen 19.08.2013 12:54

Продолжая свой старый комментарий. Например (см. другой поток), с 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]"), которая не перегружает ==).

Jeppe Stig Nielsen 18.06.2017 17:45

RE: «Так почему этот фрагмент кода не компилируется?» - Э ... потому что вы не можете вернуть bool из void ...

BrainSlugs83 10.06.2018 04:32

@ BrainSlugs83 Спасибо, что обнаружили ошибку 10-летней давности!

Hosam Aly 11.06.2018 18:03

Не менее ценный почти дубликат (так что не голосование за закрытие): Сравнение универсального аргумента в C# с нулевым значением или по умолчанию

GSerg 14.06.2019 20:53
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
351
6
116 361
12
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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.

Благодарю за разъяснение. Я не знал, что типы значений не поддерживают оператор == из коробки.

Hosam Aly 24.12.2008 11:03

Hosam, я тестировал с gmcs (моно), и он всегда сравнивает ссылки. (т.е. он не использует необязательно определенный оператор == для T)

Johannes Schaub - litb 24.12.2008 11:43

У этого решения есть одна оговорка: оператор == не может быть перегружен; см. этот вопрос StackOverflow.

Dimitri C. 27.05.2010 14:00

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

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

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

Спасибо. Я считаю, что ссылочные типы также могут переопределять оператор. Но причина неудачи теперь ясна.

Hosam Aly 24.12.2008 11:16

Токен == используется двумя разными операторами. Если для данных типов операндов существует совместимая перегрузка оператора равенства, будет использоваться эта перегрузка. В противном случае, если оба операнда являются ссылочными типами, совместимыми друг с другом, будет использоваться ссылочное сравнение. Обратите внимание, что в приведенном выше методе Compare компилятор не может сказать, что применимо первое значение, но может сказать, что применимо второе значение, поэтому токен == будет использовать последнее значение даже если T перегружает оператор проверки равенства (например, если он типа String).

supercat 31.05.2013 01:24
Ответ принят как подходящий

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

Тип 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);
    }
  }
}

Выход

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

Общий:

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

Продолжение 2

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

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

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

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

Hosam Aly 24.12.2008 11:04

Re: Follow Up 2: На самом деле компилятор свяжет его с лучшим методом, который он найдет, в данном случае Test.op_Equal. Но если у вас был класс, производный от Test и переопределяющий оператор, то оператор Test все равно будет вызываться.

Hosam Aly 24.12.2008 16:28

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

jpbochi 19.08.2009 20:34

Разрешение перегрузки происходит во время компиляции. Итак, когда у нас есть == между универсальными типами T и T, будет найдена лучшая перегрузка, учитывая, какие ограничения несет T (есть специальное правило, что он никогда не будет упаковывать тип значения для этого (что даст бессмысленный результат), следовательно, должно быть какое-то ограничение, гарантирующее, что это ссылочный тип). В вашем Продолжение 2, если вы входите с объектами DerivedTest, а DerivedTest является производным от Test, но вводит новую перегрузку ==, у вас снова будет «проблема». Какая перегрузка вызывается, «записывается» в IL во время компиляции.

Jeppe Stig Nielsen 05.01.2013 22:51

Как ни странно, это, похоже, работает для общих ссылочных типов (где вы ожидаете, что это сравнение будет на ссылочном равенстве), но для строк, похоже, также используется ссылочное равенство - так что вы можете в конечном итоге сравнить 2 идентичные строки и иметь == (когда в универсальный метод с ограничением класса) говорят, что они разные.

JonnyRaa 14.05.2013 15:33

Ах, у меня просто была эта проблема. Не понимаю, почему == не работает. Спасибо за помощь!

SamuelDavis 10.06.2013 05:51

Это потому, что подпись не совпадает - ваше ограничение для class, что фактически означает, что во время компиляции это оба экземпляра типа object - ваш оператор == требует, чтобы оба операнда были типа Test, поэтому этот оператор не будет вызываться из-за несоответствия сигнатуры (помните, что соответствие сигнатуры метода и оператора происходит во время компиляции!) - То же самое произойдет, если вы поместите в коробку экземпляр класса Test, кстати. (т.е. Test a = new Test(); object b = new Test(); Console.WriteLine(a == b); всегда будет возвращать false.)

BrainSlugs83 10.06.2018 05:08

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

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# noob. но я думаю, что это не удается, потому что компилятор не знает, что делать. поскольку T еще не известен, то, что будет сделано, зависит от типа T, если типы значений будут разрешены. для ссылок ссылки просто сравниваются независимо от T. если вы используете .Equals, то просто вызывается .Equal.

Johannes Schaub - litb 24.12.2008 10:02

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

Johannes Schaub - litb 24.12.2008 10:05

В этом есть смысл, litb :) Возможно, пользовательские структуры не перегружают ==, поэтому компилятор не работает.

Jon Limjap 24.12.2008 10:37

Первый метод сравнения использует нетObject.Equals, но вместо этого проверяет ссылочное равенство. Например, Compare("0", 0.ToString()) вернет false, поскольку аргументы будут ссылками на отдельные строки, единственными символами которых являются ноль.

supercat 31.05.2013 01:27

Незначительная ошибка с последним - вы не ограничили его структурами, поэтому может произойти NullReferenceException.

Flynn1179 20.04.2017 17:13

Как говорили другие, это будет работать только тогда, когда 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, безусловно, добавило мне ценности. :)

Hosam Aly 24.12.2008 11:15

Не считая второстепенного, Джон; Вы можете отметить комментарий re pobox vs yoda к моему посту.

Marc Gravell 24.12.2008 15:38

Хороший совет по использованию EqualityComparer <T>

chakrit 23.08.2011 17:01

+1 за указание на то, что он может сравниваться с нулевым значением, а для типа значения, не допускающего значения NULL, он всегда будет ложным

Jalal Said 24.07.2012 15:43

@BlueRaja: Да, потому что существуют особые правила сравнения с нулевым литералом. Следовательно, «без каких-либо ограничений вы можете сравнивать с null, но только с null». Это уже в ответе. Итак, почему именно это не может быть правильным?

Jon Skeet 10.10.2015 00:38

Отличный ответ, он также отвечает на мой вопрос (через 12 лет после того, как вы опубликовали свой ответ ;-)) - мне нужна была общая функция, чтобы проверить, содержит ли переменная значение по умолчанию. Было легко написать функцию, возвращающую значение по умолчанию: T GetDefault<T>() => (T)default;. Но я не мог написать функцию IsDefault<T>(value), не ограничивая ее только IComparable, которая работает не для всех типов. С вашим ответом теперь все просто: bool IsDefault<T>(T value) => Compare((T)default, value); - это ответ на мой вопрос. Спасибо, Джон!

Matt 12.11.2020 12:32

В общем, 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, потому что первый почтовый ящик был заблокирован брандмауэром на моем рабочем месте? Возможно, другие могут столкнуться с той же проблемой.)

Hosam Aly 24.12.2008 15:32

Идея состоит в том, что pobox.com/~skeet будет указывать на мой веб-сайт всегда, даже если он переместится в другое место. Я предпочитаю размещать ссылки через pobox.com ради потомства, но вместо этого вы можете заменить В настоящее время yoda.arachsys.com.

Jon Skeet 24.12.2008 17:26

Проблема с pobox.com в том, что это веб-служба электронной почты (по крайней мере, так сказано в брандмауэре компании), поэтому она заблокирована. Вот почему я не мог перейти по его ссылке.

Hosam Aly 04.01.2009 00:34

«Если же == и Equals по какой-то причине реализованы по-разному» - Святой дым! Какая же! Возможно, мне просто нужно увидеть противоположный вариант использования, но библиотека с расходящейся семантикой равенства, вероятно, столкнется с более серьезными проблемами, чем проблемы с дженериками.

Edward Brey 20.07.2017 19:53

@EdwardBrey, ты не ошибся; было бы неплохо, если бы компилятор мог это обеспечить, но ...

Marc Gravell 20.07.2017 20:56

Для этого здесь есть запись MSDN Connect

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

Unfortunately, this behavior is by design and there is not an easy solution to enable use of == with type parameters that may contain value types.

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

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

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

но почему компилятор не может рассматривать их как универсальный объект? в конце концов, == работает для всех типов, будь то ссылочные типы или типы значений. Думаю, вы не ответили на этот вопрос.

nawfal 03.02.2013 22:11

@nawfal: На самом деле нет, == работает не для всех типов значений. Что еще более важно, он не имеет одинакового значения для всех типов, поэтому компилятор не знает, что с ним делать.

Ben Voigt 04.02.2013 04:17

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

nawfal 04.02.2013 04:20

Если вы хотите убедиться, что операторы вашего настраиваемого типа вызываются, вы можете сделать это через отражение. Просто получите тип, используя свой общий параметр, и получите 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});

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

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

static bool IsLessThan(T x, T y) 
{
    return ((IComparable)(x)).CompareTo(y) <= 0;
}

Вы можете избавиться от логических значений и написать return ((IComparable)(x)).CompareTo(y) <= 0;

aloisdg 26.06.2016 21:23

В моем случае я хотел провести модульное тестирование оператора равенства. Мне нужно было вызвать код под операторами равенства без явной установки универсального типа. Советы для 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);
}

.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.

Hosam Aly 19.10.2019 10:39

Другие вопросы по теме