引用类型与可空类型 ToString()

Posted

技术标签:

【中文标题】引用类型与可空类型 ToString()【英文标题】:Reference types vs Nullable types ToString() 【发布时间】:2012-08-01 06:07:52 【问题描述】:

请有人好心解释一下为什么在空引用类型上调用 ToString() 会导致异常(在我看来这很有意义,你不能在任何情况下调用方法!)但是在空引用类型上调用 ToString() Nullable(Of T) 返回String.Empty?这让我很惊讶,因为我认为这种行为在不同类型中是一致的。

Nullable<Guid> value = null;
Stock stock = null;
string result = value.ToString(); //Returns empty string
string result1 = stock.ToString(); //Causes a NullReferenceException

【问题讨论】:

【参考方案1】:

Nullable&lt;T&gt; 实际上是一个struct,它具有一些编译器支持和实现支持,其行为类似于null,而实际上不是null

您看到的是实现之间的冲突,允许您像对待任何其他引用类型一样自然地将其视为null,但允许发生方法调用,因为Nullable&lt;T&gt; 实际上不是空的,它里面的值是空的。

在视觉上看起来它不应该工作,这仅仅是因为你无法看到在后台为你做了什么。

当您在 null 引用类型上调用扩展方法时,可以看到其他此类视觉技巧...该调用有效(与视觉预期相反),因为在后台它被解析为静态方法调用,将您的 null 实例作为参数。

How does a Nullable<T> type work behind the scenes?

【讨论】:

取决于您的视觉期望。有人会说,调用不引用任何字段或虚拟方法的非虚拟方法确实会引发出乎意料的异常,因为除了 C# 强制之外没有真正的原因它(但是.NET 并不强制这样做)。 @JonHanna 不是真的,我希望它会失败,因为它 看起来 像一个实例方法调用,不管它可以被重新定义为静态的事实没有任何问题。但我明白你的观点,即期望会受到经验的影响。在 OPs 的情况下,我认为它仍然存在。 是的,但我不希望它失败,因为我不希望 class Apublic void DoNothing();/*...*/(A(null)).DoNothing(); 失败。它没有失败的理由,但是 C# 付出了额外的努力使它失败,而其他语言没有。因为我不希望它作为非扩展方法失败,所以我也不希望它作为扩展方法失败,直到我了解到 C# 有一个特殊情况,即对空对象的方法进行安全调用。 @JonHanna 你的意思是忘掉其他语言?对我来说,尝试在空项上调用实例成员而不考虑其实现似乎很自然会导致问题。我在船上,我的主要语言经验来自 C#,并且基于问题中 OP 使用的语言,我仍然认为我的术语是有效的。我最初也认为他们没有做出额外的努力使其失败,他们只是没有根据DoSomething 的实际工作做出额外的努力。 @MAfifi Null 分配并非来自实现 (Nullable&lt;bool&gt; b = null;)。除此之外我不太确定,我所知道的只是编译器芯片允许它做它所做的事情。【参考方案2】:

Nullable 是一个值类型,对null 的赋值导致它被Value=nullHasValue=false 初始化。

此外,Nullable.ToString() 的实现如下:

public override string ToString()

    if (!this.HasValue)
    
        return "";
    
    return this.value.ToString();

所以你看到的是预期的。

【讨论】:

我认为问题更多的是“ToString 是如何调用的,因为 value 为 null?”。 值不会被初始化为 null,它会被初始化为 default(T)。 value 是一个值类型,不能为空。实际上 Value 是一个属性,因此它不会被初始化为任何东西,但它的支持字段会被初始化为 default(T),尽管您永远不会看到该值,因为该属性会引发异常。【参考方案3】:

可空类型有点棘手。当您将其设置为null 时,它实际上不是null,因为它不是引用类型(它是值类型)。当您使用null 初始化此类变量时,它会创建新的结构实例,其中HasValue 属性为falseValuenull,因此当您调用ToString 方法时,它在结构实例上运行良好。

【讨论】:

实际上为空。零和虚无的概念早于计算中的引用概念。我们可以通过不引用的引用来表示这个概念,这不是唯一可能的方式。可空值类型是另一种。 @JonHanna:你说对了一半。诚然,无效概念一般早于指称概念。但是这里的 user854301 是说,当您将 in C# 的可空类型设置为 null 时,它实际上不是 null - 这是 100% 正确的。 null 一词(尤其是在以代码字体编写时)在 C# 的上下文中具有特定的含义,它与一般的无效概念分开(尽管相关)。这就像说42.0 是一个整数——它肯定,以一般存在的方式,但从语言的角度来看,它是一个double @DanielPryden 如果我们执行double? d = null;,我们将null 分配给d。如果我们测试double == null,那么我们得到true。就null 的C# 含义而言,dnull。然而,它不是 null 引用,因为虽然它是 null,但它不是引用。 Equals 方法被可空类型覆盖。所有这些东西都是为了支持与 db(观点)的通信而设计的,而不是简单地添加“另一种类型”。 @user854301 数据库工作远非它们的唯一用途(数据库可以为空是有原因的,这个原因适用于其他地方)。【参考方案4】:

调用default(object).ToString() 引发的异常被称为NullReferenceException 是有原因的,它在空引用上调用方法。另一方面,default(int?) 不是空引用,因为它不是引用;它是一个值类型,其值等效为null。

最重要的一点是,如果这样做了,那么以下操作将失败:

default(int?).HasValue // should return false, or throw an exception?

这也会破坏我们混合可空值和不可空值的能力:

((int?)null).Equals(1) // should return false, or throw an exception?

以下内容变得完全无用:

default(int?).GetValueOrDefault(-1);

我们可以摆脱 HasValue 并强制与 null 进行比较,但是如果在某些情况下与 null 相比,使为 null 的值类型的相等覆盖可以返回 true 会怎样。这可能不是一个好主意,但它是可以做到的,而且语言必须应付。

让我们回想一下为什么要引入可空类型。引用类型可以为 null 的可能性是引用类型概念中固有的,除非努力强制执行不可为空性:引用类型是引用某事物的类型,这意味着一个人不引用任何事物的可能性,这我们称​​null

虽然在很多情况下很麻烦,但我们可以在多种情况下使用它,例如表示“未知值”,“没有有效值”等等(我们可以将它用于什么null 表示在数据库中,例如)。

此时,我们已经赋予 null 在给定上下文中的含义,而不仅仅是给定引用不引用任何对象这一简单事实。

由于这很有用,因此我们可以将 intDateTime 设置为 null,但我们不能,因为它们不是引用其他内容的类型,因此不能在不提及任何事物的状态,就像我作为哺乳动物一样会失去我的羽毛。

2.0 引入的可空类型通过与引用类型不同的机制,为我们提供了一种具有语义 null 的值类型。如果不存在,大部分内容您可以自己编写代码,但特殊的装箱和提升规则允许更明智的装箱和操作员使用。

好的。现在让我们考虑一下为什么NullReferenceExceptions 首先会发生。两个是不可避免的,一个是 C# 中的设计决定(并不适用于所有 .NET)。

    您尝试调用虚拟方法或属性,或访问空引用上的字段。这必须失败,因为没有办法查找应该调用什么覆盖,也没有这样的字段。 您在空引用上调用非虚拟方法或属性,而该引用又调用虚拟方法或属性,或访问字段。这显然是第一点的变体,但我们接下来要进行的设计决策的优势是保证它在一开始就失败,而不是部分完成(这可能会令人困惑并有长期的副作用) . 您在不调用虚拟方法或属性的空引用上调用非虚拟方法或属性,或访问字段。没有内在的理由不允许这样做,并且某些语言允许这样做,但是在 C# 中,他们决定使用 callvirt 而不是 call 来强制使用 NullReferenceException 以保持一致性(不能说我同意,但是你去)。

这些情况都不适用于可空值类型。不可能将可空值类型放入无法知道要访问哪个字段或方法覆盖的条件中。 NullReferenceException 的整个概念在这里没有意义。

总之,不抛出 NullReferenceException 与其他类型一致 - 当且仅当使用空引用时才通过它进行类型。

请注意,有时调用 null 可空类型会抛出异常,GetType() 会抛出这种情况,因为 GetType() 不是虚拟的,并且在调用值类型时总是会隐含装箱。其他值类型也是如此:

(1).GetType()

被视为:

((object)1).GetType()

但在可空类型的情况下,装箱会将那些带有错误HasValue 的类型变为空,因此:

default(int?).GetType()

被视为:

((object)default(int?)).GetType()

这导致GetType() 在一个空对象上被调用,并因此被抛出。

这顺便让我们明白了为什么不伪装NullReferenceType 是更明智的设计决策——需要这种行为的人总是可以装箱。如果您希望它通过,请使用((object)myNullableValue).GetString(),因此语言无需将其视为特殊情况来强制异常。

编辑

哦,我忘了提到NullReferenceException 背后的机制。

NullReferenceException 的测试非常便宜,因为它大多只是忽略问题,然后在发生异常时从操作系统捕获异常。换句话说,没有测试

请参阅 What is the CLR implementation behind raising/generating a null reference exception? 并注意这些都不适用于可空值类型。

【讨论】:

恕我直言,.net 应该提供一种可以显式标记实例方法以便在空实例上调用的方法;虽然这不是可变引用类型的预期行为,但这样的设计将允许像 String 这样的不可变引用类型表现得像具有有意义的默认值的值类型。【参考方案5】:

如果您调查 Nullable&lt;&gt; 定义,则存在覆盖 ToString 定义。在此函数中,ToString 被覆盖以返回 String.Empty。

    // Summary:
    //     Returns the text representation of the value of the current System.Nullable<T>
    //     object.
    //
    // Returns:
    //     The text representation of the value of the current System.Nullable<T> object
    //     if the System.Nullable<T>.HasValue property is true, or an empty string ("")
    //     if the System.Nullable<T>.HasValue property is false.
    public override string ToString();

另一方面,Stock 是一个自定义类,我假设 ToString 没有被覆盖。因此它返回 NullReferenceException,因为它使用默认行为。

【讨论】:

这个答案忽略了 Nullable 是一种值类型的关键事实。对任何引用类型调用 ToString 都会抛出 NullReferenceException,无论该类型是否具有 ToString 覆盖。 @phoog 这个答案直接或间接地并不暗示与引用类型相关的任何事情。对不起,如果我不能明白这一点。我还说如果是“一个类”并且值为“null”;然后默认行为返回 NullReferenceException。 您的回答暗示将ToString() 覆盖添加到Stock 类会改变程序的行为,但这是不正确的。 NullReferenceException 与Stock 一起发生,因为Stock 是引用类型,而Nullable&lt;Guid&gt; 不会发生,因为Nullable&lt;&gt; 是值类型。 ToString() 覆盖的存在与否与是否抛出 NullReferenceException 无关。换句话说,答案的问题恰恰在于它没有提到值类型与引用类型的问题。【参考方案6】:

根据 MSDN 备注

Guid.ToSTring() 方法返回一个字符串表示 根据提供的格式,此 Guid 实例的值 说明符。

根据 Nullable 上的 MSDN 备注

如果一个类型可以被赋值或者可以 赋值为 null,这意味着该类型没有任何值。 因此,可为空的类型可以表示一个值,也可以表示没有值 存在。例如,String 等引用类型可以为空, 而像 Int32 这样的值类型则不是。值类型不能 可以为空,因为它有足够的能力只表达值 适合该类型;它没有额外的容量 需要表示 null 值。

【讨论】:

请阅读我的完整答案,阅读时不完整

以上是关于引用类型与可空类型 ToString()的主要内容,如果未能解决你的问题,请参考以下文章

可空类型产生警告,因为项目已启用可空引用类型

可空引用类型 - 通过接受的参数返回类型可空性

C# 8中的可空引用类型

可空类型是引用类型吗?

可空引用类型意外 CS8629 可空值类型可能为带有临时变量的空

具有泛型返回类型的可空引用类型