C # ograniczenie typu ogólnego dla wszystkiego dopuszcza wartość null


111

Mam więc tę klasę:

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

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

}

Teraz szukam ograniczenia typu, które pozwala mi użyć wszystkiego jako parametru typu, który może być null. Oznacza to wszystkie typy odwołań, a także wszystkie Nullable( T?) typy:

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

powinno być możliwe.

Używanie classjako ograniczenia typu pozwala mi używać tylko typów referencyjnych.

Dodatkowe informacje: Piszę aplikację potoków i filtrów i chcę użyć nullodniesienia jako ostatniego elementu, który przechodzi do potoku, aby każdy filtr mógł ładnie się zamknąć, wyczyścić itp.


1
@Tim, który nie pozwala na Nullables
Rik


2
Nie można tego zrobić bezpośrednio. Może możesz nam powiedzieć więcej o swoim scenariuszu? A może mógłbyś użyć IFoo<T>jako typu roboczego i tworzyć instancje za pomocą metody fabrycznej? To mogłoby zadziałać.
Jon

Nie jestem pewien, dlaczego chcesz lub musisz ograniczać coś w ten sposób. Jeśli Twoim jedynym zamiarem jest zamiana "if x == null" na if x.IsNull () ", wydaje się to bezcelowe i nieintuicyjne dla 99,99% programistów przyzwyczajonych do poprzedniej składni. Kompilator ci na to nie pozwoli" if (int) x == null "w każdym razie, więc jesteś już objęty.
RJ Lohan

Odpowiedzi:


22

Jeśli chcesz przeprowadzić kontrolę środowiska wykonawczego w konstruktorze Foo zamiast sprawdzać w czasie kompilacji, możesz sprawdzić, czy typ nie jest typem referencyjnym lub dopuszczającym wartość null, i zgłosić wyjątek, jeśli tak jest.

Zdaję sobie sprawę, że samo sprawdzenie działania może być niedopuszczalne, ale na wszelki wypadek:

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;
    }
}

Następnie kompilowany jest poniższy kod, ale ostatnia ( foo3) zgłasza wyjątek w konstruktorze:

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
Jeśli masz zamiar to zrobić, upewnij się, że wykonałeś test w konstruktorze statycznym , w przeciwnym razie będziesz spowalniać konstrukcję każdej instancji swojej klasy ogólnej (niepotrzebnie)
Eamon Nerbonne

2
@EamonNerbonne Nie należy zgłaszać wyjątków od konstruktorów statycznych: msdn.microsoft.com/en-us/library/bb386039.aspx
Matthew Watson

5
Wytyczne nie są absolutne. Jeśli chcesz tego sprawdzenia, będziesz musiał znaleźć kompromis między kosztem sprawdzenia w czasie wykonywania a nieporęcznością wyjątków w konstruktorze statycznym. Ponieważ naprawdę implementujesz tutaj analizator statyczny poor-mans, ten wyjątek nigdy nie powinien być zgłaszany, z wyjątkiem podczas programowania. Wreszcie, nawet jeśli chcesz za wszelką cenę uniknąć statycznych wyjątków konstrukcji (nierozsądnych), nadal powinieneś wykonywać jak najwięcej pracy statycznej i jak najmniejszej w konstruktorze instancji - np. Ustawiając flagę „isBorked” lub cokolwiek innego.
Eamon Nerbonne,

Nawiasem mówiąc, nie sądzę, abyś w ogóle próbował tego robić. W większości przypadków wolałbym po prostu zaakceptować to jako ograniczenie języka C #, zamiast próbować pracować z nieszczelną, podatną na awarie abstrakcją. Na przykład innym rozwiązaniem może być po prostu wymaganie klas lub po prostu wymaganie struktur (i jawne nadanie wartości null) - lub zrobienie obu i posiadanie dwóch wersji. To nie jest krytyka tego rozwiązania; po prostu tego problemu nie można rozwiązać dobrze - chyba że chcesz napisać niestandardowy analizator Roslyn.
Eamon Nerbonne,

1
Możesz uzyskać to, co najlepsze z obu światów - zachowaj static bool isValidTypepole, które ustawiłeś w konstruktorze statycznym, a następnie po prostu sprawdź tę flagę w konstruktorze instancji i wyrzuć, jeśli jest to nieprawidłowy typ, aby nie wykonywać wszystkich czynności sprawdzających za każdym razem, gdy konstruujesz instancja. Często używam tego wzoru.
Mike Marynowski

20

Nie wiem, jak zaimplementować odpowiednik OR w typach ogólnych. Mogę jednak zaproponować użycie domyślnego słowa kluczowego w celu utworzenia wartości null dla typów dopuszczających wartość null i wartości 0 dla struktur:

public class Foo<T>
{
    private T item;

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

Możesz również zaimplementować swoją wersję 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;
    }
}

Przykład:

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
    }
}

Oryginalny Nullable <T> w ramach jest strukturą, a nie klasą. Nie sądzę, aby dobrym pomysłem było utworzenie opakowania typu referencyjnego, które będzie naśladować typ wartości.
Niall Connaughton,

1
Pierwsza sugestia dotycząca użycia default jest idealna! Teraz mój szablon z zwracanym typem ogólnym może zwracać wartość null dla obiektów i domyślną wartość dla typów wbudowanych.
Casey Anderson,

13

Natknąłem się na ten problem w prostszym przypadku, gdy potrzebowałem ogólnej metody statycznej, która mogłaby przyjmować wszystko, co „dopuszcza wartość zerową” (typy referencyjne lub wartości Nullables), co doprowadziło mnie do tego pytania bez zadowalającego rozwiązania. Tak więc wymyśliłem własne rozwiązanie, które było stosunkowo łatwiejsze do rozwiązania niż zadane pytanie PO, po prostu mając dwie przeciążone metody, jedną, która przyjmuje a Ti ma ograniczenie, where T : classa drugą, która przyjmuje a T?i ma where T : struct.

Wtedy zdałem sobie sprawę, że rozwiązanie można również zastosować do tego problemu, aby stworzyć rozwiązanie, które można sprawdzić w czasie kompilacji, ustawiając konstruktor jako prywatny (lub chroniony) i używając statycznej metody fabryki:

    //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);
        }
    }

Teraz możemy to wykorzystać w ten sposób:

        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

Jeśli potrzebujesz konstruktora bez parametrów, nie uzyskasz subtelności przeciążenia, ale nadal możesz zrobić coś takiego:

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

I użyj tego w ten sposób:

        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

To rozwiązanie ma kilka wad, jedną z nich jest to, że możesz preferować używanie „nowego” do konstruowania obiektów. Innym jest to, że nie będą mogli używać Foo<T>jako argumentu typu rodzajowego dla typu przymusu czegoś podobnego: where TFoo: new(). Na koniec jest trochę dodatkowego kodu, którego potrzebujesz tutaj, który zwiększyłby się, szczególnie jeśli potrzebujesz wielu przeciążonych konstruktorów.


8

Jak wspomniano, nie można tego sprawdzić w czasie kompilacji. Brakuje ogólnych ograniczeń w .NET i nie obsługują one większości scenariuszy.

Uważam jednak, że jest to lepsze rozwiązanie do sprawdzania w czasie wykonywania. Można go zoptymalizować w czasie kompilacji JIT, ponieważ obie są stałymi.

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

Takie ograniczenie typu nie jest możliwe. Zgodnie z dokumentacją ograniczeń typu nie ma ograniczenia, które przechwytuje zarówno typy dopuszczające wartość null, jak i typy referencyjne. Ponieważ ograniczenia można łączyć tylko w koniunkcji, nie ma możliwości utworzenia takiego ograniczenia przez kombinację.

Możesz jednak, dla swoich potrzeb, wrócić do parametru typu nieograniczonego, ponieważ zawsze możesz sprawdzić == null. Jeśli typ jest typem wartości, sprawdzenie zawsze da wynik fałszywy. Wtedy prawdopodobnie otrzymasz ostrzeżenie R # „Możliwe porównanie typu wartości z wartością null”, które nie jest krytyczne, o ile semantyka jest dla Ciebie odpowiednia.

Alternatywą może być użycie

object.Equals(value, default(T))

zamiast sprawdzania wartości null, ponieważ default (T), gdzie T: class jest zawsze null. Oznacza to jednak, że nie można odróżnić pogody, a wartość nie dopuszczająca wartości null nigdy nie została jawnie ustawiona lub po prostu została ustawiona na wartość domyślną.


Myślę, że problem polega na tym, jak sprawdzić, czy ta wartość nigdy nie została ustawiona. Różne niż null wydają się wskazywać, że wartość została zainicjowana.
Ryszard Dżegan

Nie unieważnia to podejścia, ponieważ typy wartości są zawsze ustawiane (przynajmniej niejawnie do ich odpowiednich wartości domyślnych).
Sven Amann

3

używam

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

To typowanie pozwala new Foo <int> (42) i IsNull () zwróci false, co, chociaż semantycznie poprawne, nie jest szczególnie znaczące.
RJ Lohan,

1
42 to „Odpowiedź na ostateczną kwestię życia, wszechświata i wszystkiego”. Mówiąc najprościej: IsNull dla każdej wartości int zwróci false (nawet dla wartości 0).
Ryszard Dżegan
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.