C#泛型类型约束:适用于所有可为空的内容

147

所以我有这个类:

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 不允许使用可空类型。 - Rik
2
这是不可能直接实现的。也许您可以告诉我们更多关于您的情况?或者您可以使用 IFoo<T> 作为工作类型,并通过工厂方法创建实例?那样是可行的。 - Jon
我不确定为什么您想要或需要以这种方式限制某些内容。如果您的唯一目的是将“if x == null”转换为“if x.IsNull()”,那么这似乎对于习惯于前一种语法的99.99%的开发人员来说是毫无意义和不直观的。编译器无论如何都不会让您执行“if (int)x == null”,因此您已经得到了保障。 - RJ Lohan
也许你可以将这个方法实现为一对泛型静态方法:IsItNull<T>(Nullable<T> i) where T : structIsItNull<T>(T i) where T : class - Guru Stron
1
这个问题在SO上已经被广泛讨论了。https://dev59.com/XXVC5IYBdhLWcg3wsTfv?rq=1和https://dev59.com/aGYr5IYBdhLWcg3wR4Ys?lq=1 - Maxim Gershkovich
显示剩余3条评论
8个回答

24

我不知道如何在泛型中实现等同于OR的功能。但是,我可以建议使用default关键字来创建可空类型的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
2
除非默认值有意义,否则在许多情况下它是没有意义的。 - Dai

24
如果你愿意在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());

38
如果你打算这样做,请确保在静态构造函数中进行检查,否则你会不必要地减缓每个泛型类实例的构造速度。 - Eamon Nerbonne
2
@EamonNerbonne 你不应该从静态构造函数中引发异常:https://msdn.microsoft.com/zh-cn/library/bb386039.aspx - Matthew Watson
8
指南并不是绝对的。如果您想要此检查,您需要权衡运行时检查的成本和静态构造函数中异常的不便之间的折衷。既然您正在实现一种简陋的静态分析器,在开发过程中除外(未被处理)外,这个异常不应该被抛出。最后,即使您想尽一切可能避免静态构造异常(不明智),那么您仍然应尽可能在静态环节完成更多工作,并在实例构造函数中完成更少的工作 - 例如通过设置一个" isBorked"或其他标志。 - Eamon Nerbonne
1
你可以兼顾两全 - 在静态构造函数中设置一个 static bool isValidType 字段,然后在实例构造函数中仅检查该标志并抛出异常,如果它是无效类型,则不必每次构造实例时都进行所有检查工作。我经常使用这种模式。 - Mike Marynowski
1
静态构造函数设置一个标志,在普通构造函数中抛出异常是个好主意。除非它是一个泛型方法,在这种情况下,没有构造函数。在这种情况下,你可以在#if DEBUG部分中的每个方法调用中验证。这些异常通常只在开发过程中抛出。 - Etienne Charland
显示剩余4条评论

22
我遇到了一个更简单的问题,想要一个通用的静态方法来处理“可空”类型(可以是引用类型或 Nullables),这使我找到了这个没有令人满意的解决方案的问题。所以我想出了自己的解决方案,相对于 OP 所述的问题,我的解决方案相对容易解决,只需要两个重载的方法,一个使用约束 where T : class 接受 T,另一个使用 where T : struct 接受 T?

然后我意识到,这个解决方案也可以应用于这个问题,通过将构造函数设为私有(或受保护)并使用静态工厂方法来创建一个在编译时可检查的解决方案:

    //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()。最后是您在此处需要的额外代码位,特别是如果您需要多个重载构造函数,则会增加代码量。

11

正如提到的那样,您无法在编译时进行此检查。.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

我使用

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

3
这种类型约束是不可能的。根据 类型约束文档,没有一种约束既可捕获可空类型又可捕获引用类型。由于约束只能在连词中组合,所以无法通过组合创建这样的约束。
不过,您可以为您的需求回退到未受约束的类型参数,因为您始终可以检查 == null。如果该类型是值类型,则检查将始终评估为 false。然后,您可能会收到 R# 警告 "可能与 null 比较值类型",这并不重要,只要语义对您来说是正确的即可。
另一种选择是使用
object.Equals(value, default(T))

与其进行空值检查,不如使用 default(T) where T : class,因为 T 类型的默认值始终为 null。然而,这意味着您无法区分非可空值是从未被显式设置过还是刚刚被设置为其默认值。


我认为问题在于如何检查变量是否从未被设置过。与 null 不同的值似乎表明了变量已经被初始化了。 - Ryszard Dżegan
这并不无效,因为值类型总是被设置为它们各自的默认值(至少是隐式设置)。 - Sven Amann

2
如果您只想允许可空值类型和引用类型,并禁止非可空值类型,则我认为在C# 9中您将没有这个选择。
我正在编写一个管道和过滤器应用程序,并希望使用空引用作为传递到管道的最后一个项目,以便每个过滤器都可以优雅地关闭、清理等等...
换句话说,您需要保留一个特殊的值来表示流的结束。
考虑创建一个提供此功能的包装类型。它类似于如何实现Nullable,并且具有额外的好处,即允许传输非流结束的null值(如果有用的话)。
public readonly struct StreamValue<T>
{
    public bool IsEndOfStream { get; }
    public T Value { get; }
}

-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是“生命、宇宙和一切的终极问题的答案”。简单说:对于每个整数值,IsNull函数都会返回false(甚至对于0值)。 - Ryszard Dżegan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接