限制泛型为可空类型

18
我正在寻找类似以下用法的示例:
Foo<string> stringFoo = new Foo<string>("The answer is");
Foo<int> intFoo = new Foo<int>(42);
// The Value of intFoo & stringFoo are strongly typed
stringFoo.Nullify();
intFoo.Nullify();
if (stringFoo == null && intFoo == null)
   MessageBox.Show("Both are null);

考虑到这个类Foo,我可以自动将T包装成可空类型:

public class Foo1<T>
   where T : struct
{
   private T? _value;

   public Foo(T? initValue)
   {
      _value = initValue;
   }

   public T? Value { get { return _value; } }

   public void Nullify { _value = null; }

}

这适用于原始类型,但不适用于字符串或其他类。

下一个版本适用于字符串,但不适用于原始类型:

public class Foo2<T>
{
   private T _value;

   public Foo(T initValue)
   {
      _value = initValue;
   }

   public T Value { get { return _value; } }

   public void Nullify { _value = default(T); }

}

我可以在Foo2中使用Nullable<int>,代码将按如下方式工作:

Foo2<int?> intFoo = new Foo<int?>(42);

但这种方法容易出错,因为它无法处理Foo2。如果我能限制T为支持可空性的类型,那就没问题了。

所以经过所有这些,有没有办法限制T为可为空类型?

一些额外的注意事项:.NET 4.0,VS2010。我在这里找到了一个类似的问题,但没有成功的答案。

3个回答

21

对于这个问题,你无法应用任何约束,但是你可以在执行时进行测试:

if (default(T) != null)
{
    throw new SomeAppropriateException(typeof(T) + " is not a nullable type");
}

你甚至可以把它放到静态构造函数里,这样可以确保每个构造类型只执行一次 - 任何试图在任何地方使用 Foo<int> 的人都会很难忽略TypeInitializerException。对于公共 API 来说这并不是太友好,但我认为对于内部 API 来说这是合理的。
编辑:有一种可怕的方法可以使创建 Foo<int> 实例变得更加困难...你可以使用本博客文章中的代码(结合默认参数和一些受限制的泛型类型来使用重载解析规则),并标记旨在非空值类型的重载已经过时并出现错误。这样,Foo<int>仍将是一个有效类型,但你将很难创建其实例。不过,我不建议你这样做...

@NikolayKhil:你在这里回复的评论已经消失了,但是这个答案中的代码对于int?确实有效,因为对于可空类型参数,空值检查会“做正确的事情”。 - Jon Skeet
我错了还是可空类型都是结构体?我的意思是,“可空类型”没有约束,但结构体有约束。但我进行了自己的测试,如果你给一些泛型参数添加 T:struct 约束,后来你就不能使用可空类型作为泛型参数。例如,X<int?> 在这个约束下无法工作。 - Matías Fidemraizer
显然,这是因为int?是Nullable<int>,而Nullable是引用类型,但也许.NET devteam可以制作一些语法糖来允许这种用例。 - Matías Fidemraizer
我原本打算将现实生活中的非平凡Foo移入一个库中,该库在我公司的类似项目中使用。因此它不是面向最终客户,而是面向“内部客户”。 - tcarvin
@MatíasFidemraizer - Nullable<T> 是一个结构体。 - Lee
1
答案中的链接似乎已经失效了。我认为它原本指向的文章是http://codeblog.jonskeet.uk/2010/11/02/evil-code-overload-resolution-workaround/。 - michaelmsm89

12

您可以将Foo<T>的构造函数设置为internal,并要求通过工厂类创建新实例:

public class Foo<T>
{
    private T value;
    internal Foo(T value)
    {
        this.value = value;
    }

    public void Nullify()
    {
        this.value = default(T);
    }

    public T Value { get { return this.value; } }
}

public class Foo
{
    public static Foo<T> Create<T>(T value) where T : class
    {
        return new Foo<T>(value);
    }

    public static Foo<T?> Create<T>(T? value) where T : struct
    {
        return new Foo<T?>(value);
    }
}

有趣。在现实生活中,Foo更加复杂,并且是由框架自动创建的。我需要看看是否有一种方法来挂钩/定制对象创建的方式,以使这种方法可行。 - tcarvin
接受,因为这将给库的内部用户带来最少的惊喜(设计/编译时间与运行时间相比)。但@JonSkeet使用的测试也很棒! - tcarvin

-1

我不太喜欢它像Foo1的语法那样,但这是Foo3:

public class Foo3<T>
   where T : struct
{

   private T _value;
   private T _nullValue;

   public Foo3(T initValue)
       : this(initValue, default(T))
   {
   }

   public Foo3(T initValue, T nullValue)
   {
      _value = initValue;
      _nullValue = nullValue;
   }

   public T Value { get { return _value; } }

    public bool IsNull
    {
        get 
        {
           return object.Equals(_value, _nullValue);
        }
    }

    public void Nullify() { _value = _nullValue; }

}

然后我的使用变成了:

Foo3<string> stringFoo = new Foo<string>("The answer is");
Foo3<int> intFoo = new Foo<int>(42, int.MinValue);
stringFoo.Nullify();
intFoo.Nullify();
if (stringFoo.IsNull && intFoo.IsNull)
   MessageBox.Show("Both are null);

这仍然容易出错,因为获取Foo3(和Foo2)的Value属性不是一件直接的事情。Foo1是最好的,因为自动包装了支持null的Value。

我可能只需要ValueTypeFoo和ObjectFoo并处理两个版本。


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