C# generic type constraint for everything nullable

后端 未结 8 1998
无人共我
无人共我 2020-12-09 14:11

So I have this class:

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

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

}


        
相关标签:
8条回答
  • 2020-12-09 14:46

    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.

    0 讨论(0)
  • 2020-12-09 14:46

    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);
        }
    }
    
    0 讨论(0)
  • 2020-12-09 14:47
        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();
    
    0 讨论(0)
  • 2020-12-09 14:52

    I use

    public class Foo<T> where T: struct
    {
        private T? item;
    }
    
    0 讨论(0)
  • 2020-12-09 14:55

    If you only want to allow nullable value types and reference types, and disallow non-nullable value types, then I think you're out of luck as of C# 9.

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

    In other words, you need to reserve a special value that indicates the end-of-stream.

    Consider creating a wrapper type that provides this. It'd be similar to how Nullable<T> is implemented, and has the additional benefit of allowing a non-end-of-stream null value to be transmitted, should that be useful.

    public readonly struct StreamValue<T>
    {
        public bool IsEndOfStream { get; }
        public T Value { get; }
    }
    
    0 讨论(0)
  • 2020-12-09 14:57

    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());
    
    0 讨论(0)
提交回复
热议问题