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

NOt*_*Dev 24 c# clr nullable

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

作为价值类型:

  1. 它仍然需要装箱和取消装箱,而且,拳击必须与"普通"结构有点不同(将零值无效视为真实null)
  2. 检查null时需要区别对待(简单地完成Equals,没有真正的问题)
  3. 它是可变的,打破了结构应该是不可变的规则(好吧,它在逻辑上是不可变的)
  4. 它需要有特殊的限制来禁止递归 Nullable<Nullable<T>>

不制作Nullable<T>参考类型可以解决这个问题吗?

改写和更新:

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

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

Jon*_*nna 16

其原因是,它没有设计成能够起到一个参考的类型.它的设计就像一个值类型,除了一个特定的类型.让我们看一下值类型和引用类型的不同之处.

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

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

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

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

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

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

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

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


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

Nullable<T> 被定义为

struct Nullable<T>
{
    private bool hasValue;
    internal T value;
    /* methods and properties I won't go into here */
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

  • 我实际上更进了一步,说'Nullable <T>`甚至不能真正*是*`null`.作为"Nullable <T>"的每个变量或参数实际上都有一个值,即使它是"null".如果不是`==`和`Equals`重载,你甚至不能直接将它与`null`进行比较.任何"Nullable <T>"可能具有引用类型的相似之处仅仅是语法糖. (3认同)

Luc*_*ero 9

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

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

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

所以要解决你的问题:

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

  2. 不,不.在装箱和拆箱时,CLR会尊重这一点,因此您实际上从不打包Nullable<>实例.拳击Nullable<>"没有"值将返回null参考,而拆箱则相反.

  3. 不.

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

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


Mar*_*ell 6

它不可变; 再检查一遍.

拳击也不同; 一个空的"盒子"为null.

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

  • @一个.不,这根本不会改变`x`值*; `AddMinutes`**返回**你忽略的值.如果你做了`x = x.AddMinutes(1)`,那就是*重新赋值*变量`x`.可变性是指我们可以改变*值或对象*的内部状态(这与更改变量不同).所以:`cust.Name ="Fred"`改变`cust`引用的对象的内部状态; 由于复制语义变得混乱,因此使用值类型更难解释; 但是使用*struct*variable`x`,如果我可以做`x.Foo = 123`来改变它,那么x*的*值是可变的. (3认同)
  • 一般来说,不变性可以指变量(例如`readonly List <int> x;`不能替换x但可以让x的成员改变)或值本身(例如`string x ="abc";`不能有它的部分改变了,虽然x可以被替换),或两者兼而有之.当我们谈论*不可变类型*时,我们指的是第二个.使用像int这样没有组件部分的简单类型,区别消失,将`int`与不可变结构进行比较是没有意义的,允许我们将`int`视为可变或不可变,因为它适合我们.与其他值类型进行比较时,它适合我们,但并非总是如此. (2认同)