it-swarm-ru.tech

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

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

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

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

253
Stefan Moser

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

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

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

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

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

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

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

113
Kent Boogaart

Мне удалось найти статью о 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 .

24
Eric Schoonover

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

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

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

18
Lasse Vågsæther Karlsen

(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 годом, и мне любопытно посмотреть, будет ли это разрешено.


Edit: Вот как это работает, как метод расширения:

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

Для обработки всех типов 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;
    }
6
Nick Farina

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

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

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

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

2
Reed Copsey

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

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 мы проверяем особый случай строки. Для всех других типов мы сравниваем значение (которое уже известно как not 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) ...
}
1
Damian Powell

Я использую:

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