Невідповідне або за замовчуванням порівняння загального аргументу в C #


288

У мене є загальний метод, визначений так:

public void MyMethod<T>(T myArgument)

Перше, що я хочу зробити, це перевірити, чи значення myArgument є типовим значенням для цього типу, приблизно таким:

if (myArgument == default(T))

Але це не компілюється, тому що я не гарантував, що T реалізує оператор ==. Тому я переключив код на це:

if (myArgument.Equals(default(T)))

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

if (myArgument == null || myArgument.Equals(default(T)))

Зараз мені це здається зайвим. ReSharper навіть пропонує, що я міняю свою частину myArgument == null на myArgument == default (T), звідки я почав. Чи є кращий спосіб вирішити цю проблему?

Мені потрібно підтримувати як типи посилань, так і типи значень.


C # тепер підтримує Null Conditional Operators , який є синтатичним цукром для останнього прикладу, який ви наводите. Ваш код стане if (myArgument?.Equals( default(T) ) != null ).
чарівник07KSU

1
@ wizard07KSU Це не працює для типів значень, тобто оцінює trueв будь-якому випадку, тому що Equalsзавжди буде викликано типи значень, оскільки в цьому випадку myArgumentне може бути, nullі результат Equals(булевого) ніколи не буде null.
яшма

Не менш цінний майже-дублікат (так що голосування не закрито): Не можна оператора == застосовувати до загальних типів у C #?
GSerg

Відповіді:


585

Щоб уникнути боксу, найкращий спосіб порівняння дженериків на рівність EqualityComparer<T>.Default. Це поважає IEquatable<T>(без боксу), а також object.Equalsобробляє всі Nullable<T>"підняті" нюанси. Звідси:

if(EqualityComparer<T>.Default.Equals(obj, default(T))) {
    return obj;
}

Це буде відповідати:

  • нуль для занять
  • null (порожній) для Nullable<T>
  • zero / false / тощо для інших структур

29
Нічого собі, як приємно затьмарити! Це, безумовно, шлях, хоча, кудо.
Нік Фаріна

1
Однозначно найкраща відповідь. Після переписування не використовуються чіткі лінії в коді, щоб використовувати це рішення.
Натан Рідлі

14
Чудова відповідь! Ще краще - додавання методу розширення для цього рядка коду, щоб ви могли перейти obj.IsDefaultForType ()
rikoe

2
@nawfal в разі Person, p1.Equals(p2)буде залежати від того , він реалізує IEquatable<Person>на державному API, або через явну реалізацію - тобто може компілятор бачити публічний Equals(Person other)метод. Однак; в генериці однаковий ІЛ використовується для всіх T; T1що відбувається реалізуватиIEquatable<T1> потрібно ставитися однаково до того, T2що не відбувається - так ні, він не помітить Equals(T1 other)методу, навіть якщо він існує під час виконання. В обох випадках також nullможна подумати (будь-який об’єкт). Тож із дженериками я б використав код, який я розмістив.
Марк Гравелл

5
Я не можу вирішити, чи відповідь ця відштовхнула мене від божевілля чи ближче до нього. +1
Стівен Лікенс

118

Як щодо цього:

if (object.Equals(myArgument, default(T)))
{
    //...
}

Використання static object.Equals()методу дозволяє уникнути необхідності робити nullперевірку самостійно. Відверто кваліфікувати дзвінок, object.мабуть, не потрібно в залежності від вашого контексту, але я, як правило, префіксує staticдзвінки з назвою типу лише для того, щоб зробити код більш розв’язним.


2
Можна навіть кинути "об’єкт". частина, оскільки це зайве. if (Дорівнює (мій аргумент, типовий (T)))
Стефан Мозер

13
Щоправда, це зазвичай, але може не залежно від контексту. Можливо, існує екземпляр методу Equals (), який бере два аргументи. Я схильний явно префіксувати всі статичні дзвінки з назвою класу, хоча б полегшити читання коду.
Кент Богаарт

8
Потрібно зазначити, що це спричинить за собою бокс, а в деяких випадках це може бути важливо
нічний кодер

2
Для мене це не працює при використанні цілих чисел, які вже є коробкою. Тому що тоді це буде об'єкт, а за замовчуванням для об’єкта - нуль замість 0.
riezebosch

28

Мені вдалося знайти статтю Microsoft Connect, яка детально обговорює цю проблему:

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

Якщо типи, як відомо, є еталонними типами, перевантаження за замовчуванням визначених на об’єктних тестах змінних для рівності опор, хоча тип може визначати власну власну перевантаження. Компілятор визначає, яку перевантаження використовувати на основі статичного типу змінної (визначення не є поліморфним). Отже, якщо ви зміните свій приклад, щоб обмежити параметр загального типу T нетипованим типовим типом (наприклад, виняток), компілятор може визначити конкретну перевантаження, яку слід використовувати, і буде скомпільований наступний код:

public class Test<T> where T : Exception

Якщо типи, як відомо, є типовими типами, виконує специфічні тести рівності значень на основі точних використаних типів. Тут немає хорошого "за замовчуванням" порівняння, оскільки порівняння посилань не має значення для типів значень і компілятор не може знати, яке саме порівняння значення виділяти. Компілятор може викликати виклик до ValueType.Equals (Object), але цей метод використовує відображення і є досить неефективним порівняно з конкретними порівняннями значень. Тому, навіть якщо ви мали б вказати обмеження типу значення на T, компілятор не може створити тут:

public class Test<T> where T : struct

У представленому вами випадку, коли компілятор навіть не знає, чи є T значенням або еталонним типом, аналогічно нічого не можна створити, що було б дійсним для всіх можливих типів. Порівняльне посилання не було б дійсним для типів значень, і якесь порівняння значення було б несподіваним для типів посилань, які не перевантажують.

Ось що ви можете зробити ...

Я підтвердив, що обидва ці методи працюють для загального порівняння еталонних і значущих типів:

object.Equals(param, default(T))

або

EqualityComparer<T>.Default.Equals(param, default(T))

Для порівняння з оператором "==" вам потрібно буде скористатися одним із таких методів:

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

public void MyMethod<T>(T myArgument) where T : MyBase

Потім компілятор розпізнає, як виконувати операції над MyBase і не кидає "Оператор" == "не може бути застосований до операндів помилки типу" T "і" T ", які ви зараз бачите.

Іншим варіантом було б обмежити T будь-яким типом, який реалізується IComparable.

public void MyMethod<T>(T myArgument) where T : IComparable

А потім скористайтеся CompareToметодом, визначеним інтерфейсом IComparable .


4
"така поведінка є конструктивною, і не існує простого рішення, щоб дозволити використовувати параметри типу, які можуть містити типи значень." Насправді Microsoft помиляється. Існує просте рішення: MS повинен розширити код ceq для функціонування на типах значень як побітовий оператор. Тоді вони можуть надати внутрішнє, що просто використовує цей опкод, наприклад, object.BitwiseOrReferenceEquals <T> (значення, за замовчуванням (T)), який просто використовує ceq. Як для значень, так і для еталонних типів це дозволить перевірити наявність побітової рівності значення (але для типів посилань посилання по бітовій рівності те саме, що об'єкт. ReferenceEquals)
Qwertie

1
Я думаю, що
потрібне

18

Спробуйте це:

if (EqualityComparer<T>.Default.Equals(myArgument, default(T)))

що має складати, і робити те, що ти хочеш.


Чи не надлишок <code> за замовчуванням (T) </code>? <code> EqualityComparer <T> .Default.Equals (myArgument) </code> повинен зробити трюк.
Joshcodes

2
1) ви спробували це, і 2) з чим ви порівнюєте цей об'єкт порівняння? EqualsМетод IEqualityComparerприймає два аргументи, два об'єкти не порівняти, так ні, це не є надмірною.
Лассе В. Карлсен

Це навіть краще, ніж прийнята відповідь IMHO, оскільки вона обробляє бокс / розпакування та інші види. Подивіться цей «закритий , як надути» питання відповідь: stackoverflow.com/a/864860/210780
ashes999

7

(Відредаговано)

Марк Гравелл має найкращу відповідь, але я хотів опублікувати простий фрагмент коду, над яким я продемонстрував це. Просто запустіть це в простому консольному додатку C #:

public static class TypeHelper<T>
{
    public static bool IsDefault(T val)
    {
         return EqualityComparer<T>.Default.Equals(obj,default(T));
    }
}

static void Main(string[] args)
{
    // value type
    Console.WriteLine(TypeHelper<int>.IsDefault(1)); //False
    Console.WriteLine(TypeHelper<int>.IsDefault(0)); // True

    // reference type
    Console.WriteLine(TypeHelper<string>.IsDefault("test")); //False
    Console.WriteLine(TypeHelper<string>.IsDefault(null)); //True //True

    Console.ReadKey();
}

Ще одне: чи може хтось із VS2008 спробувати це як метод розширення? Я тут застряг у 2005 році, і мені цікаво побачити, чи дозволено це.


Редагувати: Ось як змусити його працювати як метод розширення:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // value type
        Console.WriteLine(1.IsDefault());
        Console.WriteLine(0.IsDefault());

        // reference type
        Console.WriteLine("test".IsDefault());
        // null must be cast to a type
        Console.WriteLine(((String)null).IsDefault());
    }
}

// The type cannot be generic
public static class TypeHelper
{
    // I made the method generic instead
    public static bool IsDefault<T>(this T val)
    {
        return EqualityComparer<T>.Default.Equals(val, default(T));
    }
}

3
Це "працює" як метод розширення. Що цікаво, оскільки воно працює, навіть якщо ви говорите o.IsDefault <object> (), коли o - недійсне. Страшно =)
Нік Фаріна

6

Для обробки всіх типів T, включаючи, де T є примітивним типом, вам потрібно буде скласти обидва способи порівняння:

    T Get<T>(Func<T> createObject)
    {
        T obj = createObject();
        if (obj == null || obj.Equals(default(T)))
            return obj;

        // .. do a bunch of stuff
        return obj;
    }

1
Зауважте, що функція була змінена, щоб прийняти Func <T> і повернути T, що, на мою думку, було випадково пропущено з коду запитувача.
Нік Фаріна

Здається, Решарпер зі мною возиться. Не усвідомлював своє попередження про можливе порівняння між типом значення та null, це не було попередженням компілятора.
Натан Рідлі

2
FYI: Якщо T виявляється типом значення, то джиттер буде порівняно з нулем трактуватися як завжди помилковим.
Ерік Ліпперт

Має сенс - час виконання буде порівнювати покажчик із типом значення. Проте перевірка Equals () працює і в цьому випадку (що цікаво, оскільки, здається, дуже динамічною є вимова 5.Equals (4), яка компілює).
Нік Фаріна

2
Дивіться відповідь EqualityComparer <T> на альтернативу, яка не передбачає боксу тощо
Марк Гравелл

2

Тут буде проблема -

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

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

Крім того, ви можете покласти на це обмеження інтерфейсу, і інтерфейс може запропонувати спосіб перевірити стандартну умову класу / структури.


1

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

public static bool IsNullOrEmpty<T>(T value)
{
    if (IsNull(value))
    {
        return true;
    }
    if (value is string)
    {
        return string.IsNullOrEmpty(value as string);
    }
    return value.Equals(default(T));
}

public static bool IsNull<T>(T value)
{
    if (value is ValueType)
    {
        return false;
    }
    return null == (object)value;
}

У методі IsNull ми покладаємось на той факт, що об'єкти ValueType не можуть бути нульовими за визначенням, тому якщо значення трапляється як клас, що походить від ValueType, ми вже знаємо, що це не нуль. З іншого боку, якщо він не є типом значення, то ми можемо просто порівняти передане значення з об'єктом з null. Ми могли б уникнути перевірки ValueType, перейшовши прямо до кадру для об'єкта, але це означатиме, що тип значення отримає коробку, чого, можливо, ми хочемо уникати, оскільки це означає, що новий об’єкт створюється на купі.

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

Використовуючи ці методи, наступний код поводиться так, як ви могли очікувати:

class Program
{
    public class MyClass
    {
        public string MyString { get; set; }
    }

    static void Main()
    {
        int  i1 = 1;    Test("i1", i1); // False
        int  i2 = 0;    Test("i2", i2); // True
        int? i3 = 2;    Test("i3", i3); // False
        int? i4 = null; Test("i4", i4); // True

        Console.WriteLine();

        string s1 = "hello";      Test("s1", s1); // False
        string s2 = null;         Test("s2", s2); // True
        string s3 = string.Empty; Test("s3", s3); // True
        string s4 = "";           Test("s4", s4); // True

        Console.WriteLine();

        MyClass mc1 = new MyClass(); Test("mc1", mc1); // False
        MyClass mc2 = null;          Test("mc2", mc2); // True
    }

    public static void Test<T>(string fieldName, T field)
    {
        Console.WriteLine(fieldName + ": " + IsNullOrEmpty(field));
    }

    // public static bool IsNullOrEmpty<T>(T value) ...

    // public static bool IsNull<T>(T value) ...
}

1

Метод розширення на основі прийнятої відповіді.

   public static bool IsDefault<T>(this T inObj)
   {
       return EqualityComparer<T>.Default.Equals(inObj, default);
   }

Використання:

   private bool SomeMethod(){
       var tValue = GetMyObject<MyObjectType>();
       if (tValue == null || tValue.IsDefault()) return false;
   }

Чергуйте з null для спрощення:

   public static bool IsNullOrDefault<T>(this T inObj)
   {
       if (inObj == null) return true;
       return EqualityComparer<T>.Default.Equals(inObj, default);
   }

Використання:

   private bool SomeMethod(){
       var tValue = GetMyObject<MyObjectType>();
       if (tValue.IsNullOrDefault()) return false;
   }

0

Я використовую:

public class MyClass<T>
{
  private bool IsNull() 
  {
    var nullable = Nullable.GetUnderlyingType(typeof(T)) != null;
    return nullable ? EqualityComparer<T>.Default.Equals(Value, default(T)) : false;
  }
}

-1

Не знаю, чи працює це з вашими вимогами чи ні, але ви можете обмежити T бути типом, який реалізує інтерфейс, такий як IComparable, а потім використовувати метод ComparesTo () з цього інтерфейсу (який IIRC підтримує / обробляє нулі), як цей :

public void MyMethod<T>(T myArgument) where T : IComparable
...
if (0 == myArgument.ComparesTo(default(T)))

Ймовірно, є й інші інтерфейси, які ви могли б використовувати як IEquitable, так і т.д.


ОП переживає під NullReferenceException, і ви гарантуєте йому те саме.
nawfal

-2

@ilitirit:

public class Class<T> where T : IComparable
{
    public T Value { get; set; }
    public void MyMethod(T val)
    {
        if (Value == val)
            return;
    }
}

Оператор '==' не можна застосувати до операндів типу 'T' і 'T'

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

Ви можете розробити рішення за допомогою System.Comppare, але це дійсно в кінцевому підсумку набагато більше рядків коду та значно збільшить складність.


-3

Я думаю, ти був поруч.

if (myArgument.Equals(default(T)))

Тепер це компілюється, але вийде з ладу, якщо myArgumentє null, що є частиною того, для чого я тестую. Я можу додати явний нульовий чек так:

Вам просто потрібно перетворити об'єкт, на який викликається рівний, для елегантного підходу, що захищає нуль.

default(T).Equals(myArgument);

Я думав саме те саме.
Кріс Гесслер

6
типовий (T) еталонного типу є нульовим і призводить до гарантованого NullReferenceException.
Стефан Штейнеггер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.