Ограничение универсального типа C # для всего, допускающего значение NULL

111

Итак, у меня есть этот класс:

public class Foo<T> where T : ???
{
    private T item;

    public bool IsNull()
    {
        return item == null;
    }

}

Теперь я ищу ограничение типа, которое позволяет мне использовать в качестве параметра типа все, что может быть null. Это означает все ссылочные типы, а также все типы Nullable( T?):

Foo<String> ... = ...
Foo<int?> ... = ...

должно быть возможно.

Использование classв качестве ограничения типа позволяет мне использовать только ссылочные типы.

Дополнительная информация: я пишу приложение каналов и фильтров и хочу использовать nullссылку в качестве последнего элемента, который передается в конвейер, чтобы каждый фильтр мог нормально отключаться, выполнять очистку и т. Д.

jkammerer
источник
1
@Tim, который не допускает Nullables
Рик
Эта ссылка может вам помочь: social.msdn.microsoft.com/Forums/en-US/…
Mattar
2
Сделать это напрямую невозможно. Может, расскажете подробнее о своем сценарии? Или, может быть, вы могли бы использовать IFoo<T>в качестве рабочего типа и создавать экземпляры с помощью фабричного метода? Это можно было заставить работать.
Джон
Я не уверен, почему вам нужно или нужно что-то ограничивать таким образом. Если ваша единственная цель - превратить «if x == null» в if x.IsNull () «это кажется бессмысленным и неинтуитивным для 99,99% разработчиков, которые привыкли к прежнему синтаксису. Компилятор не позволит вам сделать» if (int) x == null "в любом случае, значит, вы уже прикрыты.
RJ Lohan
1
Это довольно широко обсуждается на SO. stackoverflow.com/questions/209160/… и stackoverflow.com/questions/13794554/…
Максим Гершкович

Ответы:

22

Если вы хотите выполнить проверку времени выполнения в конструкторе Foo, а не проверять время компиляции, вы можете проверить, не является ли тип ссылочным или допускающим значение NULL, и в этом случае создать исключение.

Я понимаю, что только проверка времени выполнения может быть неприемлемой, но на всякий случай:

public class Foo<T>
{
    private T item;

    public Foo()
    {
        var type = typeof(T);

        if (Nullable.GetUnderlyingType(type) != null)
            return;

        if (type.IsClass)
            return;

        throw new InvalidOperationException("Type is not nullable or reference type.");
    }

    public bool IsNull()
    {
        return item == null;
    }
}

Затем следующий код компилируется, но последний ( foo3) вызывает исключение в конструкторе:

var foo1 = new Foo<int?>();
Console.WriteLine(foo1.IsNull());

var foo2 = new Foo<string>();
Console.WriteLine(foo2.IsNull());

var foo3= new Foo<int>();  // THROWS
Console.WriteLine(foo3.IsNull());
Мэтью Уотсон
источник
31
Если вы собираетесь это сделать, убедитесь, что вы выполнили проверку в статическом конструкторе, иначе вы замедляете создание каждого экземпляра вашего универсального класса (без необходимости)
Иамон Нербон
2
@EamonNerbonne Вы не должны вызывать исключения из статических конструкторов: msdn.microsoft.com/en-us/library/bb386039.aspx
Мэтью Уотсон,
5
Рекомендации не являются абсолютными. Если вы хотите эту проверку, вам придется найти компромисс между стоимостью проверки во время выполнения и неудобством исключений в статическом конструкторе. Поскольку вы действительно реализуете здесь статический анализатор для бедных, это исключение никогда не должно создаваться, кроме как во время разработки. Наконец, даже если вы хотите избежать статических исключений конструкции любой ценой (неразумно), вы все равно должны выполнять как можно больше статической работы и как можно меньше в конструкторе экземпляра - например, установив флаг isBorked или что-то еще.
Eamon Nerbonne
Кстати, я не думаю, что вам вообще стоит пытаться это делать. В большинстве случаев я бы предпочел просто принять это как ограничение C #, а не пытаться работать с ненадежной абстракцией, подверженной сбоям. Например, другое решение может заключаться в том, чтобы просто потребовать классы или просто потребовать структуры (и явно сделать em обнуляемыми) - или сделать то и другое и иметь две версии. Это не критика этого решения; просто эта проблема не может быть решена должным образом - если, конечно, вы не захотите написать собственный анализатор roslyn.
Eamon Nerbonne
1
Вы можете получить лучшее из обоих миров - сохраните static bool isValidTypeполе, которое вы установили в статическом конструкторе, затем просто проверьте этот флаг в конструкторе экземпляра и бросьте, если это недопустимый тип, чтобы вы не выполняли всю работу по проверке каждый раз, когда вы создаете экземпляр. Я часто использую этот паттерн.
Майк Мариновски
20

Я не знаю, как реализовать эквивалент OR в дженериках. Однако я могу предложить использовать ключевое слово по умолчанию , чтобы создать значение null для типов, допускающих значение NULL, и значение 0 для структур:

public class Foo<T>
{
    private T item;

    public bool IsNullOrDefault()
    {
        return Equals(item, default(T));
    }
}

Вы также можете реализовать свою версию Nullable:

class MyNullable<T> where T : struct
{
    public T Value { get; set; }

    public static implicit operator T(MyNullable<T> value)
    {
        return value != null ? value.Value : default(T);
    }

    public static implicit operator MyNullable<T>(T value)
    {
        return new MyNullable<T> { Value = value };
    }
}

class Foo<T> where T : class
{
    public T Item { get; set; }

    public bool IsNull()
    {
        return Item == null;
    }
}

Пример:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(new Foo<MyNullable<int>>().IsNull()); // true
        Console.WriteLine(new Foo<MyNullable<int>> {Item = 3}.IsNull()); // false
        Console.WriteLine(new Foo<object>().IsNull()); // true
        Console.WriteLine(new Foo<object> {Item = new object()}.IsNull()); // false

        var foo5 = new Foo<MyNullable<int>>();
        int integer = foo5.Item;
        Console.WriteLine(integer); // 0

        var foo6 = new Foo<MyNullable<double>>();
        double real = foo6.Item;
        Console.WriteLine(real); // 0

        var foo7 = new Foo<MyNullable<double>>();
        foo7.Item = null;
        Console.WriteLine(foo7.Item); // 0
        Console.WriteLine(foo7.IsNull()); // true
        foo7.Item = 3.5;
        Console.WriteLine(foo7.Item); // 3.5
        Console.WriteLine(foo7.IsNull()); // false

        // var foo5 = new Foo<int>(); // Not compile
    }
}
Рышард Деган
источник
Исходный Nullable <T> в структуре - это структура, а не класс. Я не думаю, что будет хорошей идеей создавать оболочку ссылочного типа, которая имитирует тип значения.
Найл Коннотон,
1
Первый вариант использования по умолчанию идеален! Теперь мой шаблон с возвращаемым универсальным типом может возвращать значение null для объектов и значение по умолчанию для встроенных типов.
Кейси Андерсон,
13

Я столкнулся с этой проблемой из-за более простого случая, когда мне нужен общий статический метод, который мог бы принимать все, что допускает значение NULL (ссылочные типы или Nullables), что привело меня к этому вопросу без удовлетворительного решения. Итак, я придумал свое собственное решение, которое было относительно проще решить, чем заданный в OP вопрос, просто имея два перегруженных метода, один из которых принимает a Tи имеет ограничение, where T : classа другой принимает T?и имеет where T : struct.

Затем я понял, что это решение также может быть применено к этой проблеме для создания решения, которое можно проверить во время компиляции, сделав конструктор закрытым (или защищенным) и используя статический фабричный метод:

    //this class is to avoid having to supply generic type arguments 
    //to the static factory call (see CA1000)
    public static class Foo
    {
        public static Foo<TFoo> Create<TFoo>(TFoo value)
            where TFoo : class
        {
            return Foo<TFoo>.Create(value);
        }

        public static Foo<TFoo?> Create<TFoo>(TFoo? value)
            where TFoo : struct
        {
            return Foo<TFoo?>.Create(value);
        }
    }

    public class Foo<T>
    {
        private T item;

        private Foo(T value)
        {
            item = value;
        }

        public bool IsNull()
        {
            return item == null;
        }

        internal static Foo<TFoo> Create<TFoo>(TFoo value)
            where TFoo : class
        {
            return new Foo<TFoo>(value);
        }

        internal static Foo<TFoo?> Create<TFoo>(TFoo? value)
            where TFoo : struct
        {
            return new Foo<TFoo?>(value);
        }
    }

Теперь мы можем использовать это так:

        var foo1 = new Foo<int>(1); //does not compile
        var foo2 = Foo.Create(2); //does not compile
        var foo3 = Foo.Create(""); //compiles
        var foo4 = Foo.Create(new object()); //compiles
        var foo5 = Foo.Create((int?)5); //compiles

Если вам нужен конструктор без параметров, вы не получите тонкости перегрузки, но вы все равно можете сделать что-то вроде этого:

    public static class Foo
    {
        public static Foo<TFoo> Create<TFoo>()
            where TFoo : class
        {
            return Foo<TFoo>.Create<TFoo>();
        }

        public static Foo<TFoo?> CreateNullable<TFoo>()
            where TFoo : struct
        {
            return Foo<TFoo?>.CreateNullable<TFoo>();
        }
    }

    public class Foo<T>
    {
        private T item;

        private Foo()
        {
        }

        public bool IsNull()
        {
            return item == null;
        }

        internal static Foo<TFoo> Create<TFoo>()
            where TFoo : class
        {
            return new Foo<TFoo>();
        }

        internal static Foo<TFoo?> CreateNullable<TFoo>()
            where TFoo : struct
        {
            return new Foo<TFoo?>();
        }
    }

И используйте это так:

        var foo1 = new Foo<int>(); //does not compile
        var foo2 = Foo.Create<int>(); //does not compile
        var foo3 = Foo.Create<string>(); //compiles
        var foo4 = Foo.Create<object>(); //compiles
        var foo5 = Foo.CreateNullable<int>(); //compiles

У этого решения есть несколько недостатков, один из них заключается в том, что вы можете предпочесть использовать «новый» для создания объектов. Другим является то , что вы не будете в состоянии использовать в Foo<T>качестве общего аргумента типа для типа ограничения что - то вроде: where TFoo: new(). Наконец, вам нужен дополнительный код, который может увеличиться, особенно если вам нужно несколько перегруженных конструкторов.

Дэйв М
источник
8

Как уже упоминалось, у вас не может быть проверки во время компиляции. Общие ограничения в .NET сильно отсутствуют и не поддерживают большинство сценариев.

Однако я считаю, что это лучшее решение для проверки во время выполнения. Его можно оптимизировать во время JIT-компиляции, поскольку они оба константы.

public class SomeClass<T>
{
    public SomeClass()
    {
        // JIT-compile time check, so it doesn't even have to evaluate.
        if (default(T) != null)
            throw new InvalidOperationException("SomeClass<T> requires T to be a nullable type.");

        T variable;
        // This still won't compile
        // variable = null;
        // but because you know it's a nullable type, this works just fine
        variable = default(T);
    }
}
Айдиакапи
источник
3

Такое ограничение типа невозможно. Согласно документации по ограничениям типов, не существует ограничения, которое фиксирует как допускающие значение NULL, так и ссылочные типы. Поскольку ограничения можно комбинировать только в сочетании, невозможно создать такое ограничение путем комбинации.

Однако для ваших нужд вы можете вернуться к параметру неограниченного типа, поскольку вы всегда можете проверить == null. Если тип является типом значения, проверка всегда будет иметь значение false. Тогда вы, возможно, получите предупреждение R # «Возможное сравнение типа значения с null», что не критично, если семантика вам подходит.

Альтернативой может быть использование

object.Equals(value, default(T))

вместо проверки на null, поскольку default (T), где T: class всегда имеет значение null. Это, однако, означает, что вы не можете отличить погоду: значение, не допускающее значения NULL, никогда не задавалось явно или было просто установлено значение по умолчанию.

Свен Аманн
источник
Думаю, проблема в том, как проверить, что значение никогда не устанавливалось. Отличие от нуля, похоже, указывает на то, что значение было инициализировано.
Ryszard Dżegan
Это не отменяет подход, поскольку типы значений всегда устанавливаются (по крайней мере, неявно на их соответствующее значение по умолчанию).
Sven Amann
3

я использую

public class Foo<T> where T: struct
{
    private T? item;
}
эла
источник
-2
    public class Foo<T>
    {
        private T item;

        public Foo(T item)
        {
            this.item = item;
        }

        public bool IsNull()
        {
            return object.Equals(item, null);
        }
    }

    var fooStruct = new Foo<int?>(3);
        var b = fooStruct.IsNull();

        var fooStruct1 = new Foo<int>(3);
        b = fooStruct1.IsNull();

        var fooStruct2 = new Foo<int?>(null);
        b = fooStruct2.IsNull();

        var fooStruct3 = new Foo<string>("qqq");
        b = fooStruct3.IsNull();

        var fooStruct4 = new Foo<string>(null);
        b = fooStruct4.IsNull();
SeeSharp
источник
Эта типизация позволяет new Foo <int> (42), а IsNull () вернет false, что, хотя и является семантически правильным, не имеет особого смысла.
RJ Lohan
1
42 - это «Ответ на главный вопрос жизни, Вселенной и всего остального». Проще говоря: IsNull для каждого значения int вернет false (даже для значения 0).
Ryszard Dżegan