Чи не можна застосувати оператор == до загальних типів у C #?


326

Відповідно до документації ==оператора в MSDN ,

Для заданих типів значень оператор рівності (==) повертає істину, якщо значення його операндів рівні, інакше помилкові. Для типів посилань, крім рядка, == повертає значення 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: "Так чому цей фрагмент коду не вдається зібрати?" - Е-е, тому що ви не можете повернути boolз void...
BrainSlugs83

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

Вихідні дані

Вбудований: Перевантажений == викликається

Загальне:

Натисніть будь-яку кнопку, щоб продовжити . . .

Продовження 2

Я хочу зазначити, що я міняю метод порівняння на

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

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


Дякую. Я не помітив, що заява стосується лише типів посилань.
Хосам Алі

4
Re: Слідкуйте за 2: насправді компілятор зв’яже його найкращим методом, який він знайде, а це в цьому випадку Test.op_Equal. Але якщо у вас був клас, який походить від Test і переосмислює оператора, тоді оператор Test все одно буде викликаний.
Хосам Алі

4
Я добре зазначив, що я хотів би зазначити, що ви завжди повинні проводити фактичне порівняння всередині Equalsметоду, що перекрито (не в ==операторі).
jpbochi

11
Розв’язання перевантаження відбувається за час компіляції. Тож, коли ми маємо ==між загальними типами, Tі Tнайкраще перевантаження виявляється, враховуючи, які обмеження переносяться T(існує спеціальне правило, що воно ніколи не вводить для цього значення типу (що дасть безглуздий результат), отже, повинно бути деяке обмеження, яке гарантує його еталонний тип). У вашому " Follow Up 2" , якщо ви зіткнетесь з DerivedTestпредметами, і DerivedTestвипливає з Testнової перевантаження ==, але у вас виникне "проблема" знову. Яка перевантаження викликається, "спалюється" в ІЛ під час компіляції.
Джеппе Стіг Нільсен

1
це, здається, працює для загальних типів посилань (де ви б очікували, що це порівняння буде на еталонній рівності), але для рядків, здається, також використовується рівність еталону - так що ви можете в кінцевому підсумку порівняти 2 однакових рядки і мати == (коли в загальний метод із обмеженням класу) говорять, що вони різні.
JonnyRaa

291

Як вже говорили інші, вона працюватиме лише тоді, коли T обмежується бути еталонним типом. Без будь-яких обмежень, ви можете порівнювати з null, але лише з null - і це порівняння завжди буде помилковим для ненульових типів значень.

Замість того, щоб називати рівними, краще скористатись IComparer<T>- і якщо у вас немає більше інформації, EqualityComparer<T>.Default- хороший вибір:

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

Крім усього іншого, це дозволяє уникнути боксу / кастингу.


Дякую. Я намагався написати простий клас обгортки, тому просто хотів делегувати операцію фактичному загорнутому члену. Але знання EqualityComparer <T> .Default безумовно додало мені цінності. :)
Хосам Алі

Неповнолітній убік, Джон; ви можете відзначити коментар про pobox vs yoda на моєму дописі.
Марк Гравелл


1
+1 за вказівку на те, що воно може порівнюватись з нульовим, а для типу нульового значення - це завжди буде помилково
Джалал Саїд,

@BlueRaja: Так, тому що існують спеціальні правила для порівняння з нульовим буквалом. Отже, "без будь-яких обмежень, ви можете порівняти з null, але тільки з null". Це вже у відповіді. Отже, чому саме це не може бути правильним?
Джон Скіт

41

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

Якщо ж ==і Equalsчомусь реалізуються по-різному, то моя робота над загальними операторами повинна бути корисною; він підтримує операторські версії (серед інших):

  • Дорівнює (значення T1, значення T2)
  • NotEqual (значення T1, значення T2)
  • Більше ніж (значення T1, значення T2)
  • Менше ніж (значення T1, значення T2)
  • GreaterThanOrEqual (значення T1, значення T2)
  • LessThanOrEqual (значення T1, значення T2)

Дуже цікава бібліотека! :) (Бічна примітка: Чи можу я запропонувати скористатись посиланням на www.yoda.arachsys.com, тому що один попбокс був заблокований брандмауером на моєму робочому місці? Можливо, інші можуть зіткнутися з тією ж проблемою.)
Хосам Алі

Ідея полягає в тому , що pobox.com/~skeet буде завжди вказувати на мій сайт - навіть якщо він рухається в іншому місці. Я , як правило , розміщувати посилання через pobox.com заради потомства , - але ви можете в даний час замінити yoda.arachsys.com замість цього.
Джон Скіт

Проблема pobox.com полягає в тому, що це веб-сервіс електронної пошти (або так говорить брандмауер компанії), тому він заблокований. Тому я не міг перейти за його посиланням.
Хосам Алі

"Якщо, однак, == і Рівні чомусь реалізуються по-різному" - Святий курить! Що втім! Можливо, мені просто потрібно побачити випадок використання навпаки, але бібліотека з розбіжною семантикою рівнозначна, можливо, матиме більші проблеми, ніж проблеми з дженериками.
Едвард Брей

@EdwardBrey ти не помилився; Було б добре, якби компілятор міг це застосувати, але ...
Марк Гравелл

31

Чому багато відповідей, а жодна не пояснює ЧОМУ? (про що відверто запитав Джованні) ...

.NET generics не діють як шаблони C ++. У шаблонах C ++ роздільна здатність перевантаження виникає після того, як будуть відомі фактичні параметри шаблону.

У .NET generics (включаючи 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. Це робиться за допомогою пункту де.


1
Дякуємо за роз’яснення. Я не знав, що типи значень не підтримують оператора == поза полем.
Хосам Алі

1
Хосам, я тестував gmcs (моно), і він завжди порівнює посилання. (тобто він не використовує необов'язково визначений оператор == для 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 # noob. але я думаю, що це не вдається, оскільки компілятор не знає, що робити. оскільки T ще невідомо, що робити, залежить від типу T, якщо типи значень будуть дозволені. для посилань, посилання просто порівнюються незалежно від T., якщо ви робите .Equals, тоді .Equal просто називається.
Йоханнес Шауб - ліб

але якщо ви робите == для типу значення, тип значення не повинен обов'язково реалізовувати цього оператора.
Йоханнес Шауб - ліб

Це мало б сенс, litb :) Можливо, що визначені користувачем структури не перевантажуються ==, отже, компілятор виходить з ладу.
Джон Лім’яп

2
Перший метод порівняння не використовує, Object.Equalsа натомість тестує еталонну рівність. Наприклад, Compare("0", 0.ToString())повернеться помилково, оскільки аргументами будуть посилання на окремі рядки, обидва з яких мають нуль як єдиний характер.
supercat

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

Це викликає ваш перевантажений оператор, а не той, який визначений обмеженнями, застосованими до загального параметра. Це може бути не практично, але може стати в нагоді для тестування підрозділів операторів при використанні загального базового класу, який містить пару тестів.


3

Я написав таку функцію, дивлячись на останній msdn. Тут можна легко порівняти два об'єкти xта y:

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

4
Ви можете позбутися своїх буленів і написатиreturn ((IComparable)(x)).CompareTo(y) <= 0;
aloisdg переїжджаючи на codidact.com

1

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

Вищезазначене буде працювати, оскільки == доглядають у випадку визначених користувачем посилальних типів.
У випадку типів значень == може бути замінено У цьому випадку також слід визначити "! =".

Я думаю, що це може бути причиною, це відключає загальне порівняння, використовуючи "==".


2
Дякую. Я вважаю, типи посилань також можуть перемогти оператора. Але причина відмови тепер зрозуміла.
Хосам Алі

1
==Маркер використовується для двох різних операторів. Якщо для заданих типів операндів існує сумісна перевантаження оператора рівності, це перевантаження буде використано. В іншому випадку, якщо обидва операнди є еталонними типами, сумісні між собою, буде використано порівняльне порівняння. Зауважте, що у Compareвищевказаному методі компілятор не може сказати, що застосовується перше значення, але може сказати, що застосовується друге значення, тому ==маркер використовуватиме останнє навіть якщо Tперевантажує оператор перевірки рівності (наприклад, якщо це тип String) .
supercat

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.
Хосам Алі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.