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.