Нулевое или стандартное сравнение универсального аргумента в C #

288

У меня есть общий метод, определенный следующим образом:

public void MyMethod<T>(T myArgument)

Первое, что я хочу сделать, это проверить, является ли значение myArgument значением по умолчанию для этого типа, примерно так:

if (myArgument == default(T))

Но это не компилируется, потому что я не гарантировал, что T будет реализовывать оператор ==. Поэтому я переключил код на это:

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

Теперь это скомпилируется, но потерпит неудачу, если myArgument будет нулевым, что является частью того, что я тестирую. Я могу добавить явную проверку нуля следующим образом:

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

Теперь это кажется мне излишним. ReSharper даже предлагает заменить часть myArgument == null на myArgument == default (T), с которой я начал. Есть ли лучший способ решить эту проблему?

Мне нужно поддерживать как ссылочные типы, так и типы значений.

Стефан Мозер
источник
C # теперь поддерживает нулевые условные операторы , которые являются синтетическим сахаром для последнего приведенного вами примера. Ваш код станет if (myArgument?.Equals( default(T) ) != null ).
wizard07KSU
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;
}

Это будет соответствовать:

  • нуль для классов
  • пустой (пустой) для Nullable<T>
  • ноль / ложь / и т. д. для других структур
Марк Гравелл
источник
29
Вау, как восхитительно малоизвестно! Это, безусловно, путь, слава тебе.
Ник Фарина
1
Определенно лучший ответ. Нет перекошенных строк в моем коде после переписывания, чтобы использовать это решение.
Натан Ридли
14
Отличный ответ! Еще лучше добавить метод расширения для этой строки кода, чтобы вы могли использовать obj.IsDefaultForType ()
rikoe
2
@nawfal в случае Person, p1.Equals(p2)будет зависеть от того, реализует ли он IEquatable<Person>открытый API или через явную реализацию - т.е. может ли компилятор видеть открытый Equals(Person other)метод. Тем не мение; в дженериках один и тот же IL используется для всех T; a, 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 (Equals (myArgument, default (T)))
Стефан Мозер
13
Правда, обычно это так, но может и не быть в зависимости от контекста. Может существовать экземплярный метод Equals (), который принимает два аргумента. Я склонен явно ставить все статические вызовы перед именем класса, чтобы облегчить чтение кода.
Кент Boogaart
8
Необходимо отметить, что это вызовет бокс, а в некоторых случаях это может быть важно
ночной кодер
2
Для меня это не работает при использовании целых чисел, которые уже в штучной упаковке. Потому что тогда это будет объект, а по умолчанию для объекта будет
ноль
28

Мне удалось найти статью Microsoft Connect, в которой эта проблема обсуждается более подробно:

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

Если известно, что типы являются ссылочными типами, перегрузка по умолчанию для определенных в объекте тестирует переменные на равенство ссылок, хотя тип может указывать свою собственную пользовательскую перегрузку. Компилятор определяет, какую перегрузку использовать, основываясь на статическом типе переменной (определение не является полиморфным). Поэтому, если вы измените свой пример, чтобы ограничить параметр универсального типа T незапечатанным ссылочным типом (таким как Exception), компилятор может определить конкретную перегрузку, которую нужно использовать, и будет скомпилирован следующий код:

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 любым типом, который реализует IComparable.

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

А затем используйте CompareToметод, определенный интерфейсом IComparable .

Эрик Шуновер
источник
4
«Такое поведение является заданным, и не существует простого решения, позволяющего использовать параметры типа, которые могут содержать типы значений». На самом деле Microsoft ошибается. Существует простое решение: MS должна расширить код операции ceq для работы с типами значений как побитовый оператор. Затем они могут предоставить встроенную функцию, которая просто использует этот код операции, например, object.BitwiseOrReferenceEquals <T> (значение, default (T)), которая просто использует ceq. Как для значений, так и для ссылочных типов это будет проверять битовое равенство значения (но для ссылочных типов ссылочное битовое равенство совпадает с object.ReferenceEquals)
Qwertie
1
Я думаю, что ссылка на Microsoft Connect, которую вы хотели получить, была connect.microsoft.com/VisualStudio/feedback/details/304501/…
Qwertie
18

Попробуй это:

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

это должно скомпилировать, и делать то, что вы хотите.

Лассе В. Карлсен
источник
Разве <code> default (T) </ code> не является избыточным? <code> EqualityComparer <T> .Default.Equals (myArgument) </ code> должен сделать свое дело.
Джошкодес
2
1) пробовал ли ты, и 2) с чем ты сравниваешь объект сравнения? EqualsМетод IEqualityComparerпринимает два аргумента, два объекта не сравнить, так нет, это не является избыточным.
Лассе В. Карлсен
Это даже лучше, чем принятый ответ IMHO, потому что он обрабатывает бокс / распаковку и другие типы. См. Ответ на этот вопрос "закрыто как обман": stackoverflow.com/a/864860/210780
ashes999
7

(Edited)

У Марка Гравелла лучший ответ, но я хотел опубликовать простой фрагмент кода, над которым я работал, чтобы продемонстрировать его. Просто запустите это в простом консольном приложении 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 равно null. Страшно =)
Ник Фарина
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, который, я думаю, был случайно исключен из кода спрашивающего.
Ник Фарина
Кажется, ReSharper балуется со мной. Не реализованное предупреждение о возможном сравнении между типом значения и нулем не было предупреждением компилятора.
Натан Ридли
2
К вашему сведению: если T оказывается типом значения, то сравнение с нулем будет считаться джиттером всегда ложным.
Эрик Липперт
Имеет смысл - среда выполнения будет сравнивать указатель на тип значения. Однако проверка Equals () работает в этом случае (что интересно, поскольку язык 5.Equals (4), который компилируется, кажется очень динамичным).
Ник Фарина
2
Смотрите ответ EqualityComparer <T> для альтернативы, которая не включает в себя бокс и
Марк Гравелл
2

Здесь будет проблема -

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

Заменить на ноль для упрощения:

   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;
   }
dynamiclynk
источник
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;
  }
}
kofifus
источник
-1

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

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

Возможно, есть и другие интерфейсы, которые вы могли бы использовать, например, IEquitable и т. Д.

caryden
источник
ОП беспокоится о 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.Comparison, но на самом деле это приведет к увеличению количества строк кода и существенному увеличению сложности.

cfeduke
источник
-3

Я думаю, что вы были близки.

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

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

Вам просто нужно поменять объект, на котором вызывается метод equals, на элегантный нуль-безопасный подход.

default(T).Equals(myArgument);
Скотт Маккей
источник
Я думал точно так же.
Крис Гесслер
6
default (T) ссылочного типа имеет значение null и приводит к гарантированному исключению NullReferenceException.
Стефан Штайнеггер