一切可为空的C#通用类型约束


111

所以我有这个课:

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

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

}

现在,我正在寻找一种类型约束,该约束允许我将所有内容都可以用作类型参数null。这意味着所有引用类型以及所有NullableT?)类型:

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

应该是可能的。

使用class的类型约束,只允许我使用引用类型。

附加信息: 我正在编写管道和过滤器应用程序,并希望使用null引用作为传递到管道中的最后一项,以便每个过滤器都可以正常关闭,进行清理等。


1
@Tim不允许Nullables
Rik


2
无法直接执行此操作。也许您可以告诉我们更多有关您的情况的信息?也许您可以将其IFoo<T>用作工作类型并通过工厂方法创建实例?那可以使它起作用。
2013年

我不确定为什么您会想要或需要以此方式约束某些东西。如果您唯一的意图是将“ if x == null”转换为if x.IsNull()”,对于习惯了前一种语法的99.99%的开发人员来说,这似乎毫无意义,而且是不直观的。编译器不会让您执行“如果(INT)x == NULL”无论如何,所以你已经覆盖。
RJ罗汉

Answers:


22

如果您愿意在Foo的构造函数中进行运行时检查,而不是进行编译时检查,则可以检查该类型是否不是引用类型或可为null的类型,并在这种情况下引发异常。

我意识到仅进行运行时检查可能是不可接受的,但以防万一:

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 2014年

2
@EamonNerbonne您不应从静态构造函数中引发异常:msdn.microsoft.com/en-us/library/bb386039.aspx
Matthew Watson

5
指导方针不是绝对的。如果要进行此检查,则必须权衡运行时检查的成本与静态构造函数中异常的不便之处。由于您实际上是在这里实现穷人静态分析器,因此除非在开发过程中,否则不应引发此异常。最后,即使您不惜一切代价避免静态构造异常(不明智),那么您仍应在实例构造函数中尽可能多地静态地完成尽可能少的工作-例如,通过设置标志“ isBorked”或其他方式。
Eamon Nerbonne

顺便说一句,我认为您根本不应该尝试这样做。在大多数情况下,我宁愿只接受C#限制,而不是尝试使用容易出错的泄漏抽象。例如,另一种解决方案可能是只需要类,或者只需要结构(并显式地使em为可空)-或同时做两个并有两个​​版本。这不是对该解决方案的批评;只是这个问题不能很好地解决-除非您愿意编写自定义的roslyn分析器。
Eamon Nerbonne

1
您可以两全其美-保留static bool isValidType在静态构造函数中设置的字段,然后仅在实例构造函数中检查该标志,并抛出是否为无效类型的标记,这样就不必在每次构造时都进行所有检查工作一个实例。我经常使用这种模式。
Mike Marynowski

20

我不知道如何在泛型中实现等价于OR。但是,我可以建议使用默认关键字,以便为可为空的类型创建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
使用default的第一个建议是完美的!现在,我的返回了通用类型的模板可以为对象返回null,而对于内置类型则返回默认值。
Casey Anderson

13

我遇到了一个更简单的情况,即想要一个通用静态方法,该方法可以采用任何“可为空”(引用类型或可为空)的方法,这使我想到了这个问题,但没有令人满意的解决方案。因此,我想出了自己的解决方案,该解决方案比OP提出的问题要容易得多,因为它仅具有两个重载方法,一个重载方法具有a T并具有约束where T : class,另一个重载方法具有a 并具有T?and具有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

该解决方案几乎没有缺点,一个缺点是您可能更喜欢使用“ new”构造对象。另一个原因是,你将无法使用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,因此您可以根据需要使用不受约束的类型参数。如果类型是值类型,则检查将始终评估为false。然后,您可能会收到R#警告“可能将值类型与null进行比较”,这并不重要,只要语义适合您即可。

另一种可能是使用

object.Equals(value, default(T))

而不是null检查,因为default(T),其中T:class始终为null。但是,这意味着您无法区分天气,从来没有明确设置非空值或仅将其设置为默认值。


我认为问题在于如何检查从未设置过的值。与null不同似乎表明该值已被初始化。
2013年

这不会使该方法无效,因为总是设置值类型(至少隐式地设置为其各自的默认值)。
斯文·阿曼

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()将返回false,尽管在语义上正确,但并不特别有意义。
RJ Lohan

1
42是“对生命,宇宙和一切最终问题的答案”。简而言之:对于每个int值,IsNull都将返回false(即使对于0值)。
RyszardDżegan13年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.