Нулевое или стандартное сравнение общего аргумента в С#

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

public void MyMethod<T>(T myArgument)

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

if (myArgument == default(T))

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

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

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

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

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

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

Ответ 1

Чтобы избежать бокса, наилучшим способом сравнения дженериков для равенства является EqualityComparer<T>.Default. Это относится к IEquatable<T> (без бокса), а также к object.Equals и обрабатывает все "t23" "поднятые" нюансы. Следовательно:

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

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

  • null для классов
  • null (пусто) для Nullable<T>
  • zero/false/etc для других структур

Ответ 2

Как насчет этого:

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

Использование метода static object.Equals() позволяет избежать необходимости выполнять проверку null самостоятельно. Явно квалифицировать вызов с помощью object., вероятно, не нужно в зависимости от вашего контекста, но я обычно префикс static вызывает имя типа, чтобы сделать код более разрешимым.

Ответ 3

Мне удалось найти статью 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 и не будет бросать "Operator" == ', не может быть применен к операндам типа "T" и "T", которые вы видите сейчас.

Другим вариантом будет ограничение T на любой тип, реализующий IComparable.

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

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

Ответ 4

Попробуйте следующее:

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

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

Ответ 5

(Edited)

У Марка Гравелла есть лучший ответ, но я хотел опубликовать простой фрагмент кода, который я разработал, чтобы продемонстрировать его. Просто запустите это в простом приложении консоли С#:

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

Ответ 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;
    }

Ответ 7

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

Если вы разрешите этому работать для любого типа, значение по умолчанию (T) всегда будет нулевым для ссылочных типов и 0 (или struct full of 0) для типов значений.

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

В качестве альтернативы вы можете установить ограничение интерфейса на это, и интерфейс может обеспечить способ проверки по умолчанию для класса/структуры.

Ответ 8

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

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

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

Используя эти методы, следующий код ведет себя так, как вы могли ожидать:

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

Ответ 9

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

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

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

Ответ 10

@ilitirit:

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

Оператор '==' не может применяться к операндам типа 'T' и 'T'

Я не могу придумать способ сделать это без явного теста null, за которым следует вызов метода Equals или object.Equals, как было предложено выше.

Вы можете разработать решение, используя System.Comparison, но на самом деле это приведет к увеличению количества строк кода и значительному увеличению сложности.

Ответ 11

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

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

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

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

default(T).Equals(myArgument);