Возвращение nullable и null в один общий метод С#?
Возможно ли в обобщенном методе С# возвращать либо тип объекта, либо тип Nullable?
Например, если у меня есть надежный индексный аксессуар для List
, и я хочу вернуть значение, которое я могу проверить позже с помощью == null
или .HasValue()
.
В настоящее время у меня есть следующие два метода:
static T? SafeGet<T>(List<T> list, int index) where T : struct
{
if (list == null || index < 0 || index >= list.Count)
{
return null;
}
return list[index];
}
static T SafeGetObj<T>(List<T> list, int index) where T : class
{
if (list == null || index < 0 || index >= list.Count)
{
return null;
}
return list[index];
}
Если я попытаюсь объединить методы в один метод.
static T SafeGetTest<T>(List<T> list, int index)
{
if (list == null || index < 0 || index >= list.Count)
{
return null;
}
return list[index];
}
Я получаю ошибку компиляции:
Невозможно преобразовать значение null в тип "T", потому что это может быть тип значения, не равный nullable. Вместо этого используйте "default (T)".
Но я не хочу использовать default(T)
, потому что в случае примитивов 0
, который является значением по умолчанию для int
, является возможным реальным значением, которое мне нужно отличить от недоступного значения,
Возможно ли, чтобы эти методы были объединены в один метод?
(Для записи я использую .NET 3.0, и пока я заинтересован в том, что может сделать более современный С#, я лично смогу использовать ответы, которые работают в версии 3.0)
Ответы
Ответ 1
Не то, что вы хотите, но возможное обходное решение - вернуть Tuple (или другой класс-оболочку):
static Tuple<T> SafeGetObj<T>(List<T> list, int index)
{
if (list == null || index < 0 || index >= list.Count)
{
return null;
}
return Tuple.Create(list[index]);
}
Нуль всегда означает, что никакое значение не может быть получено, один кортеж сам будет означать значение (даже если само значение может быть нулевым).
В vs2015 вы можете использовать нотацию ?.
при вызове: var val = SafeGetObj(somedoublelist, 0)?.Item1;
Конечно, вместо Tuple вы можете создать свою собственную общую оболочку.
Как указано, не совсем оптимально, но это будет работоспособная работа и даст дополнительное преимущество, чтобы увидеть разницу между недействительным выбором и нулевым элементом.
Пример реализации пользовательской оболочки:
struct IndexValue<T>
{
T value;
public bool Succes;
public T Value
{
get
{
if (Succes) return value;
throw new Exception("Value could not be obtained");
}
}
public IndexValue(T Value)
{
Succes = true;
value = Value;
}
public static implicit operator T(IndexValue<T> v) { return v.Value; }
}
static IndexValue<T> SafeGetObj<T>(List<T> list, int index)
{
if (list == null || index < 0 || index >= list.Count)
{
return new IndexValue<T>();
}
return new IndexValue<T>(list[index]);
}
Ответ 2
Есть еще один вариант, который вы, возможно, не рассматривали...
public static bool TrySafeGet<T>(IList<T> list, int index, out T value)
{
value = default(T);
if (list == null || index < 0 || index >= list.Count)
{
return false;
}
value = list[index];
return true;
}
Что позволяет делать такие вещи:
int value = 0;
if (!TrySafeGet(myIntList, 0, out value))
{
//Error handling here
}
else
{
//value is a valid value here
}
И на верхней стороне, совместим с TryXXX
многих других коллекций типов и даже API преобразования/анализа. Также очень очевидно, что функция от имени метода, он "пытается" получить значение, а если он не может, он возвращает false.
Ответ 3
Вы можете сделать что-то подобное, но другое. Результат почти такой же. Это зависит от правил перегрузки и разрешения метода.
private static T? SafeGetStruct<T>(IList<T> list, int index) where T : struct
{
if (list == null || index < 0 || index >= list.Count)
{
return null;
}
return list[index];
}
public static T SafeGet<T>(IList<T> list, int index) where T : class
{
if (list == null || index < 0 || index >= list.Count)
{
return null;
}
return list[index];
}
public static int? SafeGet(IList<int> list, int index)
{
return SafeGetStruct(list, index);
}
public static long? SafeGet(IList<long> list, int index)
{
return SafeGetStruct(list, index);
}
etc...
Не так ли? Но он работает.
Затем я обернул бы все это в шаблон T4, чтобы уменьшить количество написания кода.
EDIT: мой OCD заставил меня использовать IList вместо List.
Ответ 4
Короткий ответ - нет, это невозможно. Основная причина, по которой я могу думать о том, что, если бы вы смогли сказать "Я хочу A или B в этом общем методе", это будет намного сложнее компилировать. Как бы компилятор знал, что A и B могут использоваться одинаково? То, что у вас есть, так же хорошо, как и будет.
Для справки см. этот вопрос и ответ..
Ответ 5
Я думаю, что одна проблема с вашим требованием заключается в том, что вы пытаетесь сделать T
как структурой, так и соответствующим типом Nullable<>
этой структуры. Поэтому я бы добавил параметр второго типа к сигнатуре типа, сделав его:
static TResult SafeGet<TItem, TResult>(List<TItem> list, int index)
Но есть еще одна проблема: нет общих ограничений для отношений, которые вы хотите выразить между типом источника и типом результата, поэтому вам придется выполнять некоторые проверки выполнения.
Если вы хотите сделать это выше, вы можете пойти на что-то вроде этого:
static TResult SafeGet<TItem, TResult>(List<TItem> list, int index)
{
var typeArgumentsAreValid =
// Either both are reference types and the same type
(!typeof (TItem).IsValueType && typeof (TItem) == typeof (TResult))
// or one is a value type, the other is a generic type, in which case it must be
|| (typeof (TItem).IsValueType && typeof (TResult).IsGenericType
// from the Nullable generic type definition
&& typeof (TResult).GetGenericTypeDefinition() == typeof (Nullable<>)
// with the desired type argument.
&& typeof (TResult).GetGenericArguments()[0] == typeof(TItem));
if (!typeArgumentsAreValid)
{
throw new InvalidOperationException();
}
var argumentsAreInvalid = list == null || index < 0 || index >= list.Count;
if (typeof (TItem).IsValueType)
{
var nullableType = typeof (Nullable<>).MakeGenericType(typeof (TItem));
if (argumentsAreInvalid)
{
return (TResult) Activator.CreateInstance(nullableType);
}
else
{
return (TResult) Activator.CreateInstance(nullableType, list[index]);
}
}
else
{
if (argumentsAreInvalid)
{
return default(TResult);
}
else
{
return (TResult)(object) list[index];
}
}
}
Ответ 6
Мое решение заключается в написании лучшего Nullable<T>
. Это не так элегантно, в основном вы не можете использовать int?
, но это позволяет использовать значение с нулевым значением, не зная, является ли он классом или структурой.
public sealed class MyNullable<T> {
T mValue;
bool mHasValue;
public bool HasValue { get { return mHasValue; } }
public MyNullable() {
}
public MyNullable(T pValue) {
SetValue(pValue);
}
public void SetValue(T pValue) {
mValue = pValue;
mHasValue = true;
}
public T GetValueOrThrow() {
if (!mHasValue)
throw new InvalidOperationException("No value.");
return mValue;
}
public void ClearValue() {
mHasValue = false;
}
}
Может возникнуть соблазн написать метод GetValueOrDefault
, но именно там вы столкнетесь с проблемой. Чтобы использовать это, вам всегда нужно будет проверить, есть ли это значение или нет.