Возвращение 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, но именно там вы столкнетесь с проблемой. Чтобы использовать это, вам всегда нужно будет проверить, есть ли это значение или нет.