将泛型限制为可以为空的事物

Posted

技术标签:

【中文标题】将泛型限制为可以为空的事物【英文标题】:Restricting a generic to things that can be null 【发布时间】:2011-04-20 23:55:01 【问题描述】:

我想将我正在编码的泛型限制为可以是null 的任何内容。这基本上是任何类 + System.Nullable(例如 int? 等)。

对于课堂部分,这很容易:

public class MyGeneric<T> where T : class 

但是,这不允许我这样做:

var myGeneric = new MyGeneric<int?>();

或者这个:

var myGeneric = new MyGeneric<Nullable<int>>();

编译器报错:错误 CS0452:类型 'int?'必须是引用类型才能将其用作泛型类型或方法“Test.MyGeneric”中的参数“T”

所以我尝试将System.Nullable 添加为T 的可接受类型:

public class MyGeneric<T> where T : class, System.Nullable 

但它不会做。编译器返回以下错误:错误 CS0717: 'System.Nullable': 静态类不能用作约束

然后我尝试了

public class MyGeneric<T> where T : class, INullable 

它确实可以编译,但是当我编译时:

var myGeneric = new MyGeneric<string>();

编译器返回此错误:错误 CS0311:类型“字符串”不能用作泛型类型或方法“Test.MyGeneric”中的类型参数“T”。没有从 'string' 到 'System.Data.SqlTypes.INullable' 的隐式引用转换。

所以,问题是:是否有可能将泛型限制为任何可以是null 的东西,以及如何限制?

作为参考,我使用的是 VS2010 / C# 4.0

编辑 有人问我想用它做什么。这是一个例子:

namespace Test

    public class MyGeneric<T> where T : class
    
        private IEnumerable<T> Vals  get; set; 

        public MyGeneric(params T[] vals)
        
            Vals = (IEnumerable<T>)vals;
        

        public void Print()
        
            foreach (var v in Vals.Where(v => v != default(T)))
            
                Trace.Write(v.ToString());
            
            Trace.WriteLine(string.Empty);
        
    

    class Program
    
        static void Main(string[] args)
        
            MyGeneric<string> foo = 
                new MyGeneric<string>("a", "b", "c", null, null, "g");
            foo.Print();
        
    

此程序在调试控制台中打印abcg

【问题讨论】:

值得注意的是int? foo = null; 并不是真的 null。让你假装是糖只是糖。 “我想将我正在编码的泛型限制为任何可能的东西” - 你能举例说明你为什么要这样做吗? 哼。这带来了一个问题,default(T)int? 来说是什么。我假设它是null 约束是anded在一起的;有办法“或”他们。我不认为你能做到这一点。 @Joce,因为int? 是结构Nullable&lt;int&gt; 的简写,所以您会得到一个new Nullable&lt;int&gt;(),其中HasValue 成员设置为false。适当的特殊语义允许您在代码中有效地将其视为 null。它甚至在适用时将其框为 null。 【参考方案1】:

不,没有办法在编译时这样做。

就个人而言,我会让T 成为任何东西,然后我会在静态构造函数中检查它的有效性:

public class MyGeneric<T>

    static MyGeneric()
    
        var def = default(T); 
        if (def is ValueType && Nullable.GetUnderlyingType(typeof(T)) == null)
        
            throw new InvalidOperationException(
                string.Format("Cannot instantiate with non-nullable type: 0",
                    typeof(T)));
        
    

【讨论】:

在静态构造函数中抛出异常是不好的做法,不是吗?可能会产生难以调试的涟漪效应。 假设您适当地记录了 T 必须可以为空的事实,并且鉴于无法在编译时强制执行此操作,那么我不明白为什么这是不好的做法。为什么很难调试?它会在一旦使用类,在调用任何方法之前被抛出。我的意思是,我很想告诉你是否有更好的方法,但是在编译时根本没有办法做到这一点,而且我看不出除了静态构造函数之外的任何地方检查它有什么更好的方法。 @Mehrdad 我认为这个问题值得一读,正如@DuckMastro 所说...***.com/questions/4579016/… @Harsh:杀死应用程序域不是重点吗?这样你就不会在未确定的状态下继续运行,而且它与错误一样明显。我不知道这有什么不好? 顺便说一句,我觉得如果你犯了一个如此严重的错误,以至于你以无效的方式实例化了整个 class 并且你没有看文档,杀死应用程序是一种非常公正和有用的惩罚。 >:]【参考方案2】:

不,您不能将泛型限制为只能为空的事物。例如,参见How can I return NULL from a generic method in C#?。提出的唯一解决方案是改用default(T) 或使用class 限制,因为无法限制为只能为空的类型。

不清楚你的目的是什么。仅更改几行示例代码即可使其适用于任何类型,而不仅仅是可为空的类型,因此我不明白您为什么要尝试限制它。

此示例也适用于 MyGeneric&lt;int?&gt;MyGeneric&lt;int&gt;

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Test

    public class MyGeneric<T> // removed "where T : class"
    
        public void Print(params T[] vals)
        
            Print((IEnumerable<T>) vals);
        

        public void Print(IEnumerable<T> vals)
        
            foreach (var v in vals.OfType<T>()) // use "OfType" instead of "Where"
            
                Trace.WriteLine(v.ToString());
            
            Trace.WriteLine(string.Empty);
        
    

    class Program
    
        static void Main(string[] args)
        
            MyGeneric<string> foo = new MyGeneric<string>();
            foo.Print("a", "b", "c", null, null, "g");
        
    

【讨论】:

是的,但这对于我使用的算法不起作用,我需要 default(T) 成为 null @Joce:对不起,我不明白你的问题。如果您从不使用不可为空的类型作为T,那么default(T) 将始终为null。你只是想防止人们不小心不正确地使用这个类吗? 抱歉,我误解了您在“OfType”而不是“Where”中所做的更改的性质。嗯...我认为这确实可以很好地满足我的需求!!!谢谢! 但它没有回答这个问题:是否有可能将泛型限制为任何可以是 null 的东西,以及如何?【参考方案3】:

您的示例中不清楚的一件事是为什么您要使用类级别的泛型而不是方法级别的泛型来执行此操作。根据您的示例,您可以执行以下操作:

public class NonGenericClass

    public void Print<T>(IEnumerable<T?> vals) where T : struct
    
        PrintRestricted(vals.Where(v =>  v.HasValue));
    

    public void Print<U>(IEnumerable<T> vals) where T : class
    
        PrintRestricted(vals.Where(v => v != default(T)));
    

    private void PrintRestricted<U>(IEnumerable<T> vals)
    
        foreach (var v in vals)
        
            Trace.WriteLine(v.ToString());
        
        Trace.WriteLine(string.Empty);
    

对于编写执行限制的包装方法的成本,您可以获得相同的功能。

【讨论】:

我同意根据我的示例,这可能是一个有效的解决方案。但是,如果您需要在类中存储List&lt;T&gt;,它会失败。我将更新我的示例以反映这一点。【参考方案4】:

有时类型系统根本无法满足您的需求。在这些情况下,您要么更改您想要的内容,要么解决它。

考虑Tuple&lt;&gt; 类的示例。它的“最大”版本看起来像Tuple&lt;T1, T2, T3, T4, T5, T6, T7, TRest&gt;,其中TRest 必须是Tuple&lt;&gt;。这不是编译时间限制,而是严格的运行时验证。如果您想在Foo&lt;T&gt; 中强制执行T 的可空性要求,您可能不得不求助于类似的东西,同时支持典型的类和可空结构。

/// <summary>
/// Class Foo requires T to be type that can be null. E.g., a class or a Nullable&lt;T&gt;
/// </summary>
/// <typeparam name="T"></typeparam>
class Foo<T>

    public Foo()
    
        if (default(T) != null)
            throw new InvalidOperationException(string.Format("Type 0 is not valid", typeof(T)));
    

    // other members here

在此过程中,我记录了该类将要求 T 与可空类型兼容,如果不兼容,则将其抛出构造函数。

Foo<string> foo = new Foo<string>(); // OK
Foo<int?> works = new Foo<int?>(); // also OK
Foo<int> broken = new Foo<int>(); // not OK

【讨论】:

是的,我尝试了类似的方法,但是之后您无法将T 类型的变量与default(T) 进行比较。用我的例子试试看。我得到:错误 CS0019:运算符 '!=' 不能应用于类型为“T”和“T”的操作数,因为不能保证所有类型都存在 =。 :-/ 请注意,default(T) 将为 null,您始终可以将对象与 null 进行比较。所以只需使用if (t == null) 而不是if (t == default(T))。执行空值检查后,您可以利用.Equals 虚拟方法来比较对象是否相等。

以上是关于将泛型限制为可以为空的事物的主要内容,如果未能解决你的问题,请参考以下文章

可以将泛型类型限制为枚举吗? [复制]

如何将泛型类型参数限制为 System.Enum [重复]

是否存在将我的泛型方法限制为数字类型的约束?

Delphi:泛型和类型约束

仅适用于数值类型的泛型类型约束

解决Jenkins发送测试报告中用例成功失败数量为空的问题