C# generic type constraint for everything nullable

62,798

Solution 1

If you are willing to make a runtime check in Foo's constructor rather than having a compile-time check, you can check if the type is not a reference or nullable type, and throw an exception if that's the case.

I realise that only having a runtime check may be unacceptable, but just in case:

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

Then the following code compiles, but the last one (foo3) throws an exception in the constructor:

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

Solution 2

I don't know how to implement equivalent to OR in generics. However I can propose to use default key word in order to create null for nullable types and 0 value for structures:

public class Foo<T>
{
    private T item;

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

You could also implement you version of 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;
    }
}

Example:

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

Solution 3

I ran into this issue for a simpler case of wanting a generic static method that could take anything "nullable" (either reference types or Nullables), which brought me to this question with no satisfactory solution. So I came up with my own solution which was relatively easier to solve than the OP's stated question by simply having two overloaded methods, one that takes a T and has the constraint where T : class and another that takes a T? and has where T : struct .

I then realized, that solution can also be applied to this problem to create a solution that is checkable at compile time by making the constructor private (or protected) and using a static factory method:

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

Now we can use it like this:

        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

If you want a parameterless constructor, you won't get the nicety of overloading, but you can still do something like this:

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

And use it like this:

        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

There are few disadvantages to this solution, one is that you may prefer using 'new' to construct objects. Another is that you won't be able to use Foo<T> as a generic type argument for a type constraint of something like: where TFoo: new(). Finally is the bit of extra code you need here which would increase especially if you need multiple overloaded constructors.

Solution 4

As mentioned, you cannot have a compile-time check for it. Generic constraints in .NET are severely lacking, and do not support most scenarios.

However I consider this to be a better solution for run-time checking. It can be optimized at JIT compilation time, since they're both constants.

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

Solution 5

I use

public class Foo<T> where T: struct
{
    private T? item;
}
Share:
62,798
jkammerer
Author by

jkammerer

Updated on June 21, 2021

Comments

  • jkammerer
    jkammerer almost 3 years

    So I have this class:

    public class Foo<T> where T : ???
    {
        private T item;
    
        public bool IsNull()
        {
            return item == null;
        }
    
    }
    

    Now I am looking for a type constraint that allows me to use everything as type parameter that can be null. That means all reference types, as well as all the Nullable (T?) types:

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

    should be possible.

    Using class as the type constraint only allows me to use the reference types.

    Additional Information: I am writing a pipes and filters application, and want to use a null reference as the last item that passes into the pipeline, so that every filter can shut down nicely, do cleanup, etc...

    • Rik
      Rik over 10 years
      @Tim that doesn't allow for Nullables
    • Réda Mattar
      Réda Mattar over 10 years
    • Waihon Yew
      Waihon Yew over 10 years
      It's not possible to do this directly. Perhaps you can tell us more about your scenario? Or perhaps you could use IFoo<T> as the working type and create instances through a factory method? That could be made to work.
    • RJ Lohan
      RJ Lohan over 10 years
      I'm not sure why you would want or need to constrain something this way. If your only intent is to turn "if x == null" into if x.IsNull()" this seems pointless and unintuitive to the 99.99% of developers who are used to the former syntax. The compiler won't let you do "if (int)x == null" anyway, so you're already covered.
    • Guru Stron
      Guru Stron over 10 years
      may be you can implement this method as pair of generic static methods:IsItNull<T>(Nullable<T> i) where T : struct and IsItNull<T>(T i) where T : class
    • Maxim Gershkovich
      Maxim Gershkovich over 10 years
    • mireazma
      mireazma over 3 years
      @RJLohan There might be many reasons for a non-nullable constraint to exist. But I can't think of one right now. But there are type constraints for non-nullable value type, non-nullable reference type, nullable or non-nullable reference type. As for a reason for nullable: I just need to be able to return null when return type is generic. And after all, for completeness/consistency.
    • mireazma
      mireazma over 3 years
      Although not a fit-all solution, in many cases new() could fill in the role of a nullable constraint. In fact I'd be thankful to learn some counterexamples, apart from the factory and singleton patterns.
  • Ryszard Dżegan
    Ryszard Dżegan over 10 years
    I think that the problem is how to check that value have never be set. Different than null seems to point that value have been initialized.
  • Sven Amann
    Sven Amann over 10 years
    That does not invalidate the approach, since value types are always set (at least implicitly to their respective default value).
  • RJ Lohan
    RJ Lohan over 10 years
    This typing allows new Foo<int>(42) and IsNull() will return false, which, whilst semantically correct, is not particularly meaningful.
  • Ryszard Dżegan
    Ryszard Dżegan over 10 years
    42 is "The Answer to the Ultimate Question of Life, the Universe, and Everything". Simply put: IsNull for every int value will return false (even for 0 value).
  • Eamon Nerbonne
    Eamon Nerbonne about 10 years
    If you're going to do this, make sure you do the check in the static constructor, otherwise you'll be slowing down the construction of every instance of your generic class (unnecessarily)
  • Niall Connaughton
    Niall Connaughton about 9 years
    The original Nullable<T> in the framework is a struct, not a class. I don't think it's a good idea to create a reference type wrapper that will mimic a value type.
  • Casey Anderson
    Casey Anderson over 7 years
    The first suggestion using default is perfect! Now my template with a generic type being returned can return a null for objects and the default value for built-in types.
  • Matthew Watson
    Matthew Watson about 7 years
    @EamonNerbonne You should not raise exceptions from static constructors: msdn.microsoft.com/en-us/library/bb386039.aspx
  • Eamon Nerbonne
    Eamon Nerbonne about 7 years
    Guidelines aren't absolutes. If you want this check, you're going to have to trade-off the cost of a runtime check vs. the unhandiness of exceptions in a static constructor. Since you're really implementing a poor-mans static analyzer here, this exception should never be thrown except during development. Finally, even if you want to avoid static construction exceptions at all costs (unwise), then you should still do as much work as possible statically and as little as possible in the instance constructor - e.g. by setting a flag "isBorked" or whatever.
  • Eamon Nerbonne
    Eamon Nerbonne about 7 years
    Incidentally, I don't think you should try to do this at all. In most circumstances I'd prefer to just accept this as a C# limitation, rather than try and work with a leaky, failure-prone abstraction. E.g. a different solution might be to just require classes, or just require structs (and explicitly make em nullable) - or do both and have two versions. That's not a criticism of this solution; it's just that this problem cannot be solved well - unless, that is, you're willing to write a custom roslyn analyzer.
  • Mike Marynowski
    Mike Marynowski over 5 years
    You can get the best of both worlds - keep a static bool isValidType field that you set in the static constructor, then just check that flag in the instance constructor and throw if it's an invalid type so you're not doing all the checking work each time you construct an instance. I use this pattern often.
  • Matthew Watson
    Matthew Watson over 5 years
    @MikeMarynowski I think that's what Eamon said in his second comment? e.g. by setting a flag "isBorked" or whatever. - I think that's the same idea.
  • Mike Marynowski
    Mike Marynowski over 5 years
    Oh oops, not sure how I missed that part. In that case I'm not sure why he's suggesting that's not a good solution in the next comment. It seems like a perfectly good solution to me and has come in really handy in many situations, but to each their own I guess. I think 99% of the time it is better to avoid static constructor exceptions at all costs, they are incredibly annoying to debug.
  • Matthew Watson
    Matthew Watson over 5 years
    @MikeMarynowski Yes, I agree that it's a good solution (and in fact, it's what I actually do myself).
  • Etienne Charland
    Etienne Charland over 3 years
    Static constructor setting a flag that throws an exception in the normal constructor is a good idea. Unless it's a generic method, in which case there is no constructor. In that case, you can validate on every method call within a #if DEBUG section. Those exceptions normally should only be thrown during development.
  • Dai
    Dai almost 3 years
    @CaseyAnderson Unless the default value is meaningful, which in many cases it is.