C # загальне обмеження типу для всього зведеного нанівець


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посилання як останній елемент, який переходить у трубопровід, щоб кожен фільтр міг добре вимикатися, робити очищення тощо ...


1
@Tim, що не дозволяє Nullables
— Рік

Це посилання може допомогти вам: social.msdn.microsoft.com/Forums/en-US / ...
— Ред Mattar

2
Це зробити безпосередньо неможливо. Можливо, ви можете розповісти більше про свій сценарій? Чи, можливо, ви могли б використовувати IFoo<T>як робочий тип і створювати екземпляри за допомогою фабричного методу? Це могло б змусити працювати.
— Jon

Я не впевнений, чому б ви хотіли чи потрібно щось обмежувати таким чином. Якщо ваш єдиний намір - перетворити "if x == null" на if x.IsNull () ", це здається безглуздим і неінтуїтивним для 99,99% розробників, які звикли до колишнього синтаксису. Компілятор не дозволить вам це зробити" якщо (int) x == null "все одно, значить, ви вже охоплені.
— RJ Lohan

1
Це досить широко обговорюється на SO. stackoverflow.com/questions/209160/… та stackoverflow.com/questions/13794554/…
— Максим Гершкович

Відповіді:


22

Якщо ви готові перевірити час виконання в конструкторі Foo, а не перевірку часу компіляції, ви можете перевірити, чи не тип є посилальним чи нульовим, і викинете виняток, якщо це так.

Я розумію, що лише перевірка часу виконання може бути неприйнятною, але про всяк випадок:

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
Якщо ви збираєтеся це робити, переконайтеся, що ви зробите перевірку в статичному конструкторі, інакше ви сповільнить побудову кожного екземпляра свого загального класу (без необхідності)
— Eamon Nerbonne

2
@EamonNerbonne Не слід створювати винятки зі статичних конструкторів: msdn.microsoft.com/en-us/library/bb386039.aspx
— Меттью Уотсон

5
Настанови не є абсолютними. Якщо ви хочете цього перевірити, вам доведеться компенсувати витрати на перевірку часу виконання проти неспроможності винятків у статичному конструкторі. Оскільки тут ви реально впроваджуєте статичний аналізатор з поганими манами, цей виняток ніколи не слід кидати, окрім під час розробки. Нарешті, навіть якщо ви хочете уникнути винятків статичної конструкції будь-якою ціною (нерозумно), вам все одно слід виконати якомога більше роботи статично і якомога менше в конструкторі екземплярів - наприклад, встановивши прапор "isBorked" або будь-що інше.
— Еймон Нербонна

До речі, я не думаю, що вам варто взагалі намагатися це робити. У більшості обставин я вважаю за краще сприймати це як обмеження C #, а не намагатися працювати з непрохідною абстракцією, не схильною до відмов. Наприклад, іншим рішенням може бути просто вимагати класів, або просто вимагати конструкцій (і явно робити їх незмінними) - або робити обидві та мати дві версії. Це не критика цього рішення; просто ця проблема не може бути вирішена добре, якщо тільки ви не готові написати спеціальний аналізатор розлину.
— Еймон Нербонна

1
Ви можете отримати найкраще з обох світів - збережіть static bool isValidTypeполе, яке ви встановили в статичному конструкторі, а потім просто перевірте цей прапор у конструкторі екземпляра та киньте, якщо він недійсний тип, щоб ви не виконували всі перевірочні роботи щоразу, коли будуєте екземпляр. Я часто користуюся цією схемою.
— Майк Мариновський

20

Я не знаю, як реалізувати еквівалентні АБО в генериці. Однак я можу запропонувати використовувати ключове слово за замовчуванням , щоб створити 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> у рамках - це структура, а не клас. Я не думаю, що це гарна ідея створити обгортку еталонного типу, яка буде імітувати тип значення.
— Niall Connaughton

1
Перша пропозиція, що використовує типовий , ідеально! Тепер мій шаблон із загальним типом, що повертається, може повернути нуль для об'єктів і значення за замовчуванням для вбудованих типів.
— Кейсі Андерсон

13

Я зіткнувся з цим питанням для більш простого випадку, коли потрібно загальний статичний метод, який міг би прийняти що-небудь "змінне" (або типи посилань, або Nullables), що привело мене до цього питання, не маючи задовільного рішення. Тож я придумав власне рішення, яке було порівняно простіше вирішити, ніж заявлене питання ОП, просто маючи два перевантажені методи: один, який приймає 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. Якщо тип є типовим типом, чек просто завжди оцінюватиметься як хибне. Тоді ви, можливо, отримаєте попередження R # "Можливе порівняння типу значення з нульовим", що не є критичним, якщо семантика підходить саме вам.

Альтернативою може бути використання

object.Equals(value, default(T))

замість нульової перевірки, оскільки за замовчуванням (T), де клас T: завжди є нульовим. Це, однак, означає, що ви не можете розрізнити погоду, а не нульове значення ніколи не було встановлено явно або було просто встановлено за замовчуванням.


Я думаю, що проблема полягає в тому, як перевірити це значення ніколи не встановлювали. На відміну від null, схоже, вказується на те, що значення було ініціалізовано.
— Ryszard Dżegan

Це не скасовує підхід, оскільки типи значень завжди встановлюються (принаймні неявно до відповідного значення за замовчуванням).
— Свен Аман

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();

Це введення тексту дозволяє новому Foo <int> (42), а IsNull () поверне помилкове значення, що, хоча й семантично правильно, не має особливого значення.
— RJ Lohan

1
42 - "Відповідь на остаточне питання життя, Всесвіту та всього". Простіше кажучи: IsNull для кожного значення int поверне помилкове значення (навіть для 0 значення).
— Ryszard Dżegan
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.