Сравнение общего аргумента с нулевым значением или по умолчанию в C#

StackOverflow https://stackoverflow.com/questions/65351

  •  09-06-2019
  •  | 
  •  

Вопрос

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

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), с которой я и начал.Есть ли лучший способ решить эту проблему?

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

Это было полезно?

Решение

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

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

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

  • ноль для классов
  • ноль (пустой) для Nullable<T>
  • ноль/ложь/и т. д. для других структур

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

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

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

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

мне удалось найти Статья 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' и 'T'», которую вы видите сейчас.

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

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

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

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

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

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

(Отредактировано)

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

Чтобы обрабатывать все типы 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;
    }

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

Если вы хотите, чтобы это работало для любого типа, значение default(T) всегда будет равно нулю для ссылочных типов и 0 (или структуре, полной 0) для типов значений.

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

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

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

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

Я использую:

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

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

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

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

@ilitirit:

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

Оператор «==» не может быть применен к операндам типа «Т» и «Т».

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

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

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

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

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

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

default(T).Equals(myArgument);
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top