Нулевое или стандартное сравнение универсального аргумента в 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), с которой я начал. Есть ли лучший способ решить эту проблему?
Мне нужно поддерживать как ссылочные типы, так и типы значений.
14 ответов
Чтобы избежать бокса, лучший способ сравнить генерики на равенство 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 незапечатанным ссылочным типом (таким как Исключение), компилятор может определить конкретную перегрузку для использования, и следующий код скомпилирует:
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.
Попробуй это:
if (EqualityComparer<T>.Default.Equals(myArgument, default(T)))
это должно скомпилировать, и делать то, что вы хотите.
(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));
}
}
Для обработки всех типов 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;
}
Способ продления на основе принятого ответа.
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;
}
Здесь будет проблема -
Если вы хотите, чтобы это работало для любого типа, 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 не могут быть нулевыми по определению, поэтому, если значение оказывается классом, производным от 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) ...
}
Просто хакерский ответ и как напоминание для себя. Но я считаю это очень полезным для своего проекта. Причина, по которой я пишу это так, заключается в том, что я не хочу, чтобы целое число по умолчанию 0 было помечено как нулевое, если значение равно 0
private static int o;
public static void Main()
{
//output: IsNull = False -> IsDefault = True
Console.WriteLine( "IsNull = " + IsNull( o ) + " -> IsDefault = " + IsDefault(o));
}
public static bool IsNull<T>(T paramValue)
{
if( string.IsNullOrEmpty(paramValue + "" ))
return true;
return false;
}
public static bool IsDefault<T>(T val)
{
return EqualityComparer<T>.Default.Equals(val, default(T));
}
Я использую:
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 и т. Д.
Я думаю, что вы были близки.
if (myArgument.Equals(default(T)))
Теперь это компилируется, но потерпит неудачу, если myArgument
является нулем, что является частью того, что я тестирую. Я могу добавить явную проверку нуля следующим образом:
Вам просто нужно поменять объект, на котором вызывается метод equals, на элегантный нуль-безопасный подход.
default(T).Equals(myArgument);
@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, но на самом деле это приведет к увеличению количества строк кода и существенному увеличению сложности.