为什么Nullable <t>是一个结构?</t>

时间:2010-11-24 22:24:21

标签: c# clr nullable

我想知道为什么Nullable<T>是值类型,如果它被设计为模仿引用类型的行为?我理解GC压力之类的东西,但我不相信 - 如果我们想让int表现得像参考,我们可能会对所有具有真实参考类型的后果感到满意。我没有理由Nullable<T>不仅仅是T struct的盒装版本。

作为价值类型:

  1. 它仍然需要装箱和取消装箱,而且,拳击必须与“普通”结构有点不同(以区分真值null的无效空值)
  2. 在检查null时需要区别对待(仅在Equals中完成,没有真正的问题)
  3. 它是可变的,打破了结构应该是不可变的规则(好吧,它在逻辑上是不可变的)
  4. 需要特殊限制才能禁止递归Nullable<Nullable<T>>
  5. 不使Nullable<T>引用类型解决问题吗?

    重新定义并更新:

    我已经修改了我的理由清单,但我的一般问题仍然是开放的:

    引用类型Nullable<T>如何比当前值类型实现更糟糕?这只是GC压力和“小的,不可改变的”规则吗?它对我来说仍然很奇怪......

4 个答案:

答案 0 :(得分:16)

原因是设计为像引用类型一样。它的设计就像一个值类型,除了一个特定的类型。让我们看一下值类型和引用类型的不同之处。

值和引用类型之间的主要区别在于值类型是自包含的(包含实际值的变量),而引用类型引用到另一个值。

这需要一些其他的差异。我们可以直接别名引用类型(具有好的和坏的效果)的事实来自于此。平等意味着差异也是如此:

值类型具有基于所包含的值的相等概念,可以选择性地重新定义(对重新定义的发生方式存在逻辑限制*)。引用类型具有对于值类型无意义的标识概念(因为它们不能直接别名,因此两个这样的值不能相同),这些值无法重新定义,这也是其相等概念的默认值。默认情况下,==在涉及值类型†时处理这种基于值的相等性,但在引用类型时具有标识。此外,即使引用类型被赋予基于值的相等概念,并且将其用于==,它也永远不会失去与另一个身份参考进行比较的能力。

这样做的另一个区别是引用类型可以为null - 引用另一个值的值允许一个不引用任何值的值,这是一个空引用。

此外,保持值类型较小的一些优点与此相关,因为基于值,它们在传递给函数时按值复制。

其他一些差异是隐含的,但不是由此引起的。使价值类型不可变的通常是一个好主意是隐含的但不是由核心差异所引起的,因为虽然在不考虑实施问题的情况下可以找到优势,但使用引用类型(实际上与安全性有关)也有一些优势。别名更直接地应用于引用类型)以及可能违反此指南的原因 - 因此它不是一个硬性规则(使用嵌套值类型,所涉及的风险大大减少,以至于我在使嵌套值类型变为可变时几乎没有疑虑,即使我的风格在很大程度上倾向于使偶数引用类型在实际上都是不可变的。)

值类型和引用类型之间的一些进一步差异可以说是实现细节。局部变量中的值类型具有存储在堆栈中的值已被认为是实现细节;如果你的实现有一个堆栈,可能是一个非常明显的一个,在某些情况下肯定是一个重要的,但不是定义的核心。它也经常被夸大(一开始,局部变量中的引用类型在堆栈中也有引用本身,而另一个有很多时候值类型值存储在堆中)。

价值类型的一些进一步优势与此有关。


现在,Nullable<T>是一种类型,其行为类似于上述所有方式的值类型,除了它可以采用空值。也许存储在堆栈中的本地值的问题并不是那么重要(更多的是实现细节而不是其他任何东西),但其余部分是如何定义的。

Nullable<T>定义为

struct Nullable<T>
{
    private bool hasValue;
    internal T value;
    /* methods and properties I won't go into here */
}

从这一点来看,大多数实施都是显而易见的。需要进行一些特殊处理,允许为其分配null - 视为已分配default(Nullable<T>) - 并且在装箱时进行一些特殊处理,然后是其余的(包括可以将其与null进行比较)。

如果Nullable<T>是引用类型,那么我们必须进行特殊处理以允许所有其他内容发生,以及特殊处理.NET如何帮助开发人员的功能(例如我们' d需要特殊处理才能使其从ValueType下降。我甚至不确定它是否可能。

*对于我们如何重新定义平等有一些限制。将这些规则与默认值中使用的规则组合在一起,通常我们可以允许两个值被认为是相等的,默认情况下会被认为是不相等的,但是考虑两个不等于默认值相等的值是很有意义的。一个例外是struct只包含value-types,但所说的value-types重新定义了相等。这是优化的结果,通常被认为是错误而不是设计。

†异常是浮点类型。由于CLI标准中值类型的定义,double.NaN.Equals(double.NaN)float.NaN.Equals(float.NaN)返回true。但由于ISO 60559中对NaN的定义,float.NaN == float.NaNdouble.NaN == double.NaN都返回false。

答案 1 :(得分:9)

已编辑以解决更新后的问题......

如果要使用结构作为参考,可以装箱和取消装箱对象。

但是,Nullable<>类型基本上允许使用附加状态标志来增强任何值类型,该状态标志指示该值是否应用作null或者stuct是否为“有效”。

所以要解决你的问题:

  1. 在集合中使用时,或者由于语义不同(复制而不是引用),这是一个优势

  2. 不,不。在装箱和拆箱时,CLR会尊重这一点,因此您实际上从不打包Nullable<>实例。装箱“{1}}哪个”没有“将返回Nullable<>引用,而拆箱则相反。

  3. 不。

  4. 同样,情况并非如此。实际上,struct的泛型约束不允许使用可为空的结构。由于特殊的装箱/拆箱行为,这是有道理的。因此,如果您有null来约束泛型类型,则不允许使用可空类型。由于此约束也是在where T: struct类型上定义的,因此您无法对其进行嵌套,而无需采取任何特殊处理来防止这种情况。

  5. 为什么不使用参考?我已经提到了重要的语义差异。但除此之外,引用类型使用更多的内存空间:每个引用,特别是在64位环境中,不仅用于实例的堆内存,还用于引用的内存,实例类型信息,锁定位等。因此,除了语义和性能差异(通过引用间接)之外,您最终会使用用于实体本身的多个内存用于大多数常见实体。 GC可以处理更多的对象,这将使整个性能与结构相比更加糟糕。

答案 2 :(得分:6)

它不可变;再次检查。

拳击也不同;一个空的“盒子”为空。

但;它很小(几乎不大于T),不可变,只封装结构 - 理想的结构。也许更重要的是,只要T真的是一个“价值”,那么T也是如此吗?一个合乎逻辑的“价值”。

答案 3 :(得分:-1)

我将MyNullable编码为一个类。 除了避免堆内存压力之外,不能真正理解为什么它不能成为一个类。

namespace ClassLibrary1

{     使用NFluent;

using NUnit.Framework;

[TestFixture]
class MyNullableShould
{
    [Test]
    public void operator_equals_btw_nullable_and_value_works()
    {
        var myNullable = new MyNullable<int>(1);

        Check.That(myNullable == 1).IsEqualTo(true);
        Check.That(myNullable == 2).IsEqualTo(false);
    }

    [Test]
    public void Can_be_comparedi_with_operator_equal_equals()
    {
        var myNullable = new MyNullable<int>(1);
        var myNullable2 = new MyNullable<int>(1);

        Check.That(myNullable == myNullable2).IsTrue();
        Check.That(myNullable == myNullable2).IsTrue();

        var myNullable3 = new MyNullable<int>(2);
        Check.That(myNullable == myNullable3).IsFalse();
    }
}

} namespace ClassLibrary1 {     使用System;

public class MyNullable<T> where T : struct
{
    internal T value;

    public MyNullable(T value)
    {
        this.value = value;
        this.HasValue = true;
    }

    public bool HasValue { get; }

    public T Value
    {
        get
        {
            if (!this.HasValue) throw new Exception("Cannot grab value when has no value");
            return this.value;
        }
    }

    public static explicit operator T(MyNullable<T> value)
    {
        return value.Value;
    }

    public static implicit operator MyNullable<T>(T value)
    {
        return new MyNullable<T>(value);
    }

    public static bool operator ==(MyNullable<T> n1, MyNullable<T> n2)
    {
        if (!n1.HasValue) return !n2.HasValue;
        if (!n2.HasValue) return false;
        return Equals(n1.value, n2.value);
    }

    public static bool operator !=(MyNullable<T> n1, MyNullable<T> n2)
    {
        return !(n1 == n2);
    }

    public override bool Equals(object other)
    {
        if (!this.HasValue) return other == null;
        if (other == null) return false;
        return this.value.Equals(other);
    }

    public override int GetHashCode()
    {
        return this.HasValue ? this.value.GetHashCode() : 0;
    }

    public T GetValueOrDefault()
    {
        return this.value;
    }

    public T GetValueOrDefault(T defaultValue)
    {
        return this.HasValue ? this.value : defaultValue;
    }

    public override string ToString()
    {
        return this.HasValue ? this.value.ToString() : string.Empty;
    }
}

}