为什么Nullable 是一个结构?

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

作为价值类型:

  1. 它仍然需要装箱和取消装箱,而且更多,装箱必须与“普通”结构有点不同(对待像null值一样的null值可null
  2. 检查null时需要区别对待 (仅在Equals完成,没有真正的问题)
  3. 它是可变的,打破了结构应该是不可变的规则 (好吧,它在逻辑上是不可变的)
  4. 它需要有特殊限制来禁止Nullable<Nullable>等递归

是否使Nullable成为引用类型解决问题?

改写和更新:

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

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

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

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

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

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

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

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

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

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

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


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

Nullable定义为

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

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

如果Nullable是一个引用类型,那么我们必须有特殊的处理来允许所有其余的发生,以及特殊处理.NET如何帮助开发人员(例如我们需要特殊处理)使它从ValueType下降)。 我甚至不确定它是否可能。

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

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

编辑以解决更新的问题……

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

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

所以要解决你的问题:

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

  2. 不,不。 当装箱和拆箱时,CLR确实尊重这一点,因此您实际上从不Nullable<>实例。 拳击Nullable<>哪个“没有”将返回null引用,而拆箱则反之。

  3. 不。

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

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

它不可变; 再检查一遍。

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

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

我将MyNullable编写为一个类。 无法真正理解为什么它不能成为一个类,旁边避免堆内存压力。

 namespace ClassLibrary1 

{使用NFluent;

 using NUnit.Framework; [TestFixture] class MyNullableShould { [Test] public void operator_equals_btw_nullable_and_value_works() { var myNullable = new MyNullable(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(1); var myNullable2 = new MyNullable(1); Check.That(myNullable == myNullable2).IsTrue(); Check.That(myNullable == myNullable2).IsTrue(); var myNullable3 = new MyNullable(2); Check.That(myNullable == myNullable3).IsFalse(); } } 

} namespace ClassLibrary1 {using System;

 public class MyNullable 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 value) { return value.Value; } public static implicit operator MyNullable(T value) { return new MyNullable(value); } public static bool operator ==(MyNullable n1, MyNullable n2) { if (!n1.HasValue) return !n2.HasValue; if (!n2.HasValue) return false; return Equals(n1.value, n2.value); } public static bool operator !=(MyNullable n1, MyNullable 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; } } 

}