为什么字符串类型的默认值是null而不是空字符串?

243

在我可以安全地应用 ToUpper()StartWith() 等方法之前,测试所有字符串是否为 null 很烦人。

如果默认值的 string 是空字符串,则我不必进行测试,并且这会更加符合其他值类型(例如 intdouble)的一致性。此外,Nullable<String> 也是有意义的。

那么,为什么 C# 的设计者选择使用 null 作为字符串的默认值呢?

注意:这与这个问题相关,但更专注于为什么而不是该怎么做。


57
你认为这对于其他引用类型来说是一个问题吗? - Jon Skeet
20
不,只是因为我最初错误地认为字符串是值类型。 - Marcel
24
@Marcel:这是一个非常好的怀疑的理由。 - T.J. Crowder
7
@JonSkeet 是的,非常赞同。 (但你对于不可为空的引用类型讨论也并不陌生...) - Konrad Rudolph
8
@JohnCastle 我敢你去问那些理解三态值的数据库开发人员,看看他们是否愿意放弃 null 值。之所以 null 值不好是因为人们不会思考三态,人们只会想到左或右、上或下、是或否。关系代数需要一个三态值。 - jcolebrand
显示剩余3条评论
15个回答

337
为什么字符串类型的默认值是null而不是空字符串?
因为string是引用类型,所有引用类型的默认值都是null。
测试所有字符串是否为null后才能安全地应用ToUpper()、StartWith()等方法非常麻烦。
这与引用类型的行为一致。在调用它们的实例成员之前,应该放置一个检查以防止空引用。
如果字符串的默认值是空字符串,我就不必进行测试,而且会觉得它更符合其他值类型,比如int或double。
将默认值分配给特定的引用类型(而不是null)将使其不一致。
此外,Nullable是有意义的。

Nullable<T>适用于值类型。需要注意的是,Nullable在最初的.NET平台上并未被引入,因此如果他们改变了这个规则,会有很多代码出现错误。(来自@jcolebrand的礼貌提醒)


10
可以实现很多东西,但为什么要引入如此明显的不一致性呢? - user395760
4
这里的问题是“为什么”。 - H H
8
“Consistency”是对你提出的“字符串可以与其他引用类型不同对待”的观点的反驳。 - user395760
6
作为一名将字符串视为值类型的语言和在dotnet上工作了2年的人,我同意Henk的看法。我认为这是dotnet的一个重大缺陷 - Fabricio Araujo
1
@delnan:可以创建一个值类型,其行为基本上类似于“String”,除了(1)具有可用的默认值的值类型行为,以及(2)每次将其转换为“Object”时不幸的额外装箱间接层。鉴于“string”的堆表示是唯一的,特殊处理以避免额外的装箱不会太过分(实际上,能够指定非默认装箱行为对其他类型也是一件好事)。 - supercat
显示剩余6条评论

41

Habib是对的--因为string是一个引用类型。

但更重要的是,你不需要每次使用它都检查是否为null。虽然你应该在有人把一个null引用传递给你的函数时抛出一个ArgumentNullException异常。

事实上,如果你试图在一个字符串上调用.ToUpper(),框架会自动为你抛出NullReferenceException异常。请记住,即使你测试了参数的null值,由于传递给你的函数作为参数的对象的任何属性或方法可能会计算为null,这种情况仍然可能发生。

话虽如此,检查空字符串或null值是一种常见的做法,所以他们提供了String.IsNullOrEmpty()String.IsNullOrWhiteSpace()来解决这个问题。


32
如果你的方法无法接受空引用,就应该抛出ArgumentNullException,而不是自己抛出NullReferenceException(http://msdn.microsoft.com/en-us/library/ms173163.aspx)。此外,当你修复问题时,`NullRef`通常是最难诊断的异常之一,所以我认为不检查空引用的建议并不是一个很好的建议。 - Andy
4
“Null引用通常是最难诊断的异常之一。”我非常不同意,如果记录数据,很容易找到和修复(只需处理空情况)。 - Louis Kottmann
6
抛出 ArgumentNullException 的额外好处在于能够提供参数名称。在调试期间,这可以节省... 嗯,几秒钟时间。但这些是重要的几秒钟。 - Kos
2
@DaveMarkle 你可能也想包括 IsNullOrWhitespace,参见 http://msdn.microsoft.com/zh-cn/library/system.string.isnullorwhitespace.aspx - Nathan Koop
1
我真的认为在每个地方都检查 null 是代码膨胀的一个巨大源头。它很丑陋,看起来像是 hacky,并且很难保持一致性。我认为(至少在类似 C# 的语言中)一个好的规则是“禁止在生产代码中使用 null 关键字,在测试代码中大量使用它”。 - sara
显示剩余2条评论

23

你可以编写一个扩展方法(如果值得的话):

public static string EmptyNull(this string str)
{
    return str ?? "";
}

现在这个功能是安全的:

string str = null;
string upper = str.EmptyNull().ToUpper();

111
但请不要这样做。另一个程序员最不想看到的就是到处都有 .EmptyNull() 的代码,仅仅因为第一个人害怕异常。请注意,这可能使得代码变得混乱且难以维护。 - Dave Markle
17
但显然这正是楼主在寻找的。"在能够安全地应用方法如 ToUpper(),StartWith() 等之前,必须测试所有字符串是否为空值,这相当烦人。" - Tim Schmelter
20
这条评论是针对文章原作者,而非你本人的。虽然你的回答显然是正确的,但对于像程序员这样提出基础问题的人,我们应该强烈警告他们不要将你的解决方案在实际开发中广泛使用,因为他们往往会这样做。在你的回答中有许多权衡并未被讨论,比如不透明性、增加了复杂性、重构的难度、扩展方法的潜在过度使用以及性能等。有时(很多时候)一个正确的答案并不是正确的路径,这就是我为什么要发表评论的原因。 - Dave Markle
5
解决没有进行适当的空值检查的问题,应该是正确地检查空值,而不是给问题打个临时补丁。 - Dave Markle
7
如果你费心编写 .EmptyNull() 方法,为什么不在需要时直接使用 (str ?? "") 呢?话虽如此,我同意 @DaveMarkle 评论中表达的情感:你可能不应该这样做。null 和 String.Empty 在概念上是不同的,并不能将它们视为相同处理。 - user
显示剩余4条评论

18

从 C# 6.0 开始,您也可以使用以下内容

string myString = null;
string result = myString?.ToUpper();

字符串的结果将为 null。


2
要准确无误,自从c# 6.0以后,IDE的版本与此毫无关系,因为这是一种语言特性。 - Stijn Van Antwerpen
4
另一种选项 - public string Name { get; set; } = string.Empty; 该代码表示一个公共属性 "Name",它被定义为字符串类型,并且具有默认值为空字符串。 - Jaja Harris
这叫什么?myString?.ToUpper(); - Hunter Nelson
1
它被称为 Null-Conditional 运算符。您可以在此处阅读有关它的信息:https://msdn.microsoft.com/zh-cn/magazine/dn802602.aspx - russelrillema

14

空字符串和null是根本不同的。Null是缺少值,而空字符串是一个没有值的值。

编程语言对变量“值”的假设,特别是空字符串,在这种情况下,将与使用任何其他不会引起空引用问题的值初始化字符串一样好。

此外,如果将该字符串变量的句柄传递给应用程序的其他部分,则该代码将无法验证您是否有意传递了空白值或者您是否忘记了填充该变量的值。

另一个可能会出现问题的场合是字符串是某些函数返回的值。由于字符串是引用类型并且在技术上可以具有null和empty作为值,因此该函数也可以在技术上返回null或empty(没有阻止它这样做)。现在,由于“值的缺失”有两种概念,即空字符串和null,因此所有使用此函数的代码都必须进行两个检查。一个是为空,另一个是为null。

简而言之,始终只有一个表示单个状态的表示形式是很好的。有关空值和null的更广泛讨论,请参见下面的链接。

https://softwareengineering.stackexchange.com/questions/32578/sql-empty-string-vs-null-value

NULL vs Empty when dealing with user input


2
你是如何在文本框中看待这种差异的呢?是用户忘记输入字段值,还是有意留空?编程语言中的Null确实有特定的含义:未分配。我们知道它没有值,这与数据库null不同。 - Andy
1
当你在文本框中使用它时,没有太大的区别。无论哪种方式,用一个符号来表示字符串中缺少值是至关重要的。如果我必须选择一个,我会选择null。 - Abbas Gadhia
在Delphi中,字符串是值类型,因此不能为null。这在某种程度上使生活变得更加轻松 - 我真的觉得将字符串作为引用类型非常令人讨厌。 - Fabricio Araujo
1
在 .net 之前的 COM(Common Object Model)中,字符串类型要么保存指向字符串数据的指针,要么使用 null 表示空字符串。.net 有很多种实现类似语义的方式可供选择,尤其是考虑到 String 具有一些独特的特性 [例如,它和两个数组类型是唯一的分配大小不是常量的类型]。 - supercat

8
根本问题是,CLS规范的设计者(它定义了语言与.net的交互方式)没有定义一种方法,使类成员能够指定必须直接调用它们而不是通过callvirt调用,而不需要调用方执行空引用检查;也没有提供一种定义结构的方法,这些结构不会受到“正常”装箱的影响。
如果CLS规范定义了这样的方法,那么.net就可以一致地遵循通用对象模型(COM)所建立的先例,其中空字符串引用被认为在语义上等同于空字符串,并且其他用户定义的不可变类类型,其应具有值语义,同样可以定义默认值。基本上,每个String成员(例如Length)都将被编写为以下内容:[InvokableOnNull()] int String Length { get { if (this==null) return 0; else return _Length;} }。这种方法对于应该像值一样行事但由于实现问题需要存储在堆上的东西来说,提供了非常好的语义。这种方法的最大困难在于这种类型和Object之间的转换语义可能会变得有点混乱。
另一种方法是允许定义特殊的结构类型,这些类型不继承自Object,而是具有自定义的装箱和拆箱操作(它们将转换为/从某些其他类类型)。在这种方法下,将存在一个类类型NullableString,其行为方式与当前的字符串相同,并且一个自定义装箱的结构类型String,它将包含一个Value字段,该字段的类型为String。如果尝试将String转换为NullableStringObject,则如果非空,则返回Value,否则返回String.Empty。尝试将非空引用强制转换为String时,NullableString实例会将该引用存储在Value中(如果长度为零,则可能存储null);尝试将任何其他引用强制转换为String时,都会抛出异常。
即使字符串必须存储在堆上,但在概念上没有理由让它们不像具有非空默认值的值类型一样“行为”。将它们存储为保存引用的“正常”结构对于使用它们作为类型“string”的代码来说是有效的,但在转换为“object”时会增加额外的间接性和低效性。虽然我不预见.net会在这个晚期添加以上任何功能,但也许未来框架的设计者可以考虑将它们包含在内。

1
作为一个经常使用SQL的人,曾经遇到过Oracle不区分NULL和零长度的头疼问题,我非常高兴.NET能够做出区分。"Empty"是一个值,而"null"则不是。 - user565869
@JonofAllTrades:我不同意。在应用程序代码中,除了处理数据库代码之外,将字符串视为类别是没有意义的。它是一个值类型和基本类型。Supercat:赞同你的观点。 - Fabricio Araujo
1
数据库代码是一个很大的“例外”。只要存在一些问题域,您需要区分“存在/已知、空字符串”和“不存在/未知/不适用”,例如数据库,那么语言就需要支持它。当然,现在 .NET 有 Nullable<>,字符串可以重新实现为值类型;我无法评论这种选择的成本和收益。 - user565869
3
处理数字的代码必须有一种超出范围的方法来区分默认值零和“未定义”状态。因为目前处理可空字符串和数字的代码必须使用不同的方法来处理它们。即使可空类类型stringNullable<string>更高效,但是必须使用"更高效"的方法会比能够将所有可空的数据库值都用同一种方式处理更加繁琐。 - supercat

7

为什么C#的设计者选择使用null作为字符串的默认值?

因为字符串是引用类型,引用类型的默认值是null。引用类型的变量存储对实际数据的引用。

让我们为这种情况使用default关键字;

string str = default(string); 

str 是一个字符串(string)类型,因此它是一个引用类型(reference type),因此默认值为 null

int str = (default)(int);

str 是一个 int,因此它是一个值类型,所以默认值是


5
也许在分配字符串变量时,您可以使用 ?? 运算符来帮助您。
string str = SomeMethodThatReturnsaString() ?? "";
// if SomeMethodThatReturnsaString() returns a null value, "" is assigned to str.

5
因为字符串变量是一个引用而不是实例。默认情况下将其初始化为空可能是可行的,但这将在整个代码中引入许多不一致性。

3
没有特定的理由要求string必须是引用类型。确实,组成字符串的实际字符必须存储在堆上,但鉴于字符串在CLR中已经拥有专门的支持,将System.String作为一个值类型并具有单个私有字段Value(类型为HeapString)也不算太难。该字段将是一个引用类型,并默认为null,但Value字段为nullString结构体将表现为空字符串。这种方法的唯一缺点是... - supercat
1
在运行时没有特殊情况的代码的情况下,将String转换为Object会导致在堆上创建一个装箱的String实例,而不是仅仅复制对HeapString的引用。 - supercat
1
@supercat - 没有人说字符串应该/可以是值类型。 - H H
1
除了我以外没有人。如果将字符串作为“特殊”值类型(带有私有引用类型字段),则大多数处理方式基本上与现在一样高效,除了在像.Length等方法/属性上添加空值检查之外。这样,持有空引用的实例不会尝试取消引用它,而是表现为适合空字符串的行为。无论框架是否更好或更差,如果想要default(string)成为空字符串... - supercat
1
string作为一个值类型封装在引用类型字段上可能是需要对.NET的其他部分进行最少更改的方法[的确,如果愿意接受从StringObject的转换创建额外的装箱项,那么可以简单地让String成为一个具有Char[]类型的字段的普通结构体,它永远不会被公开暴露]。我认为拥有一个HeapString类型可能更好,但在某些方面,持有Char[]的值类型字符串会更简单。 - supercat
你知道的,当一个评论不足以表达时,你可能不应该将其发布为评论。缺乏格式会增加阅读难度。 - H H

4
如果默认值的“string”是空字符串,我将不必测试。
错误!更改默认值并不会改变它仍然是引用类型的事实,某人仍然可以明确地将引用设置为“null”。
另外,Nullable 是有意义的。
正确。不允许任何引用类型为空,而是需要使用 Nullable 来实现该功能,这将更有意义。
那么为什么C#的设计者选择使用“null”作为字符串的默认值呢?
与其他引用类型保持一致。为什么在引用类型中允许“null”呢?可能是为了让它感觉像C语言,尽管这在同时提供 Nullable 的语言中是一个值得怀疑的设计决策。

4
可能是因为 Nullable 类型只在 .NET 2.0 框架中引入,所以在此之前它是不可用的。 - jcolebrand
3
感谢Dan Burton指出,某人后来可以将引用类型的初始化值设置为null。经过思考,我意识到我在问题中最初的意图没有用处。 - Marcel

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