为什么将空字符串连接起来是有效的,但调用“null.ToString()”却无效?

134

这是有效的C#代码

var bob = "abc" + null + null + null + "123";  // abc123

这不是有效的C#代码

var wtf = null.ToString(); // compiler error

为什么第一条语句是有效的?


103
我觉得你将 null.ToString() 命名为 wtf 很奇怪。为什么会让你感到惊讶?因为当你没有任何调用来源时,你无法调用实例方法。 - BoltClock
13
当然可以在空实例上调用实例方法。虽然在C#中不可能,但在CLR中是完全有效的 :) - leppie
1
关于String.Concat的答案几乎正确。实际上,问题中的特定示例是常量折叠之一,第一行中的null值被编译器消除-即它们在运行时不再存在-编译器已经擦除了它们。我在这里写了一个小独白,介绍了有关字符串连接和常量折叠的所有规则,以获取更多信息:https://dev59.com/sWox5IYBdhLWcg3wjE5i#9132374。 - Chris Shain
83
你可以给一个字符串添加内容,但你无法从空白创建一个字符串。 - raddrick
第二个语句无效吗?class null_extension { String ToString( Object this arg ) { return ToString(arg); } } - ctrl-alt-delor
11个回答

169

第一个代码有效的原因:

来自MSDN

在字符串连接操作中,C#编译器将null字符串视为空字符串,但不会转换原始null字符串的值。

有关+二元运算符的更多信息:

当一个或两个操作数是字符串类型时,二元+运算符执行字符串连接。

如果字符串连接的操作数为null,则会替换为空字符串。否则,任何非字符串参数都将通过调用继承自类型对象的虚拟ToString方法转换为其字符串表示形式。

如果ToString返回null,则会替换为空字符串。

第二个代码中出错的原因是:

null (C# Reference) - null关键字是表示空引用的字面量,它不引用任何对象。null是引用类型变量的默认值。


1
那这样行吗?var wtf = ((String)null).ToString(); 最近我在用Java工作,其中强制转换null是可能的,因为我已经有一段时间没有使用C#了。 - Jochem
15
你仍在试图在一个空对象上调用一个方法,所以我猜不行。将其视为null.ToString()ToString(null)的区别。 - Svish
@Svish 是的,现在我再想起来,它是一个空对象,所以你是对的,它不起作用。在Java中也不会:空指针异常。没关系。谢谢你的回复![编辑:在Java中进行了测试:NullPointerException。不同之处在于带有转换时可以编译,没有转换时不能编译]。 - Jochem
通常情况下,没有理由故意连接或ToString空关键字。如果需要一个空字符串,请使用string.Empty。如果您需要检查字符串变量是否为空,可以使用(myString == null)或string.IsNullOrEmpty(myString)。或者,要将空字符串变量转换为string.Empty,请使用myNewString = (myString == null ?? string.Empty)。 - csauve
@Jochem 强制转换会让它编译通过,但在运行时确实会引发 NullReferenceException 异常。 - jeroenh
显示剩余2条评论

111

因为C#中的+运算符在内部转换为静态方法String.Concat。此方法将null视为空字符串处理。如果你查看反射器中String.Concat的源代码,你会发现它:

// while looping through the parameters
strArray[i] = (str == null) ? Empty : str;
// then concatenate that string array

(MSDN也提到了这一点:http://msdn.microsoft.com/en-us/library/k9c94ey1.aspx

另一方面,ToString()是一个实例方法,您不能在null上调用它(应该使用什么类型来表示null?)。


2
String类中没有+运算符。那么是编译器将其转换为String.Concat吗? - superlogical
@superlogical 是的,编译器就是这么做的。所以实际上,在 C# 中对字符串使用 + 运算符只是 String.Concat 的语法糖。 - Botz3000
此外,编译器会自动选择最合适的Concat重载。可用的重载有1、2或3个对象参数,4个对象参数+__arglist和一个params对象数组版本。 - Wormbo
那里修改了哪个字符串数组?当给定一个非空字符串数组时,Concat是否总是创建一个新的非空字符串数组,还是有其他情况发生? - supercat
@supercat 修改后的数组是一个新数组,然后传递给一个私有的辅助方法。您可以使用反射器自己查看代码。 - Botz3000

30

第一个示例将被翻译为:

var bob = String.Concat("abc123", null, null, null, "abs123");

Concat 方法检查输入并将 null 转换为空字符串。

第二个示例将被翻译为:

var wtf = ((object)null).ToString();

因此,这里将生成一个null引用异常。


实际上,会抛出一个 AccessViolationException 异常 :) - leppie
وˆ‘هˆڑهˆڑهپڑن؛† :) ((object)null).ToString() => AccessViolation ((object)1 as string).ToString() => NullReference - leppie
1
我遇到了NullReferenceException异常。DN 4.0 - Viacheslav Smityukh
有趣。当我把 ((object)null).ToString() 放在 try/catch 中时,我也会得到 NullReferenceException - leppie
糟糕,我的错!:*( 我使用了 ((string)null).ToString(),这会导致访问冲突。 - leppie
3
除了它不会抛出 AccessViolation,这里会抛出 NullReferenceException。为什么会抛出呢? - jeroenh

12

你的代码的第一部分就像在 String.Concat 中一样被处理,

当你添加字符串时,C# 编译器会调用这个方法。"abc" + null 会被转换成 String.Concat("abc", null)

并且在内部,该方法会将 null 替换为 String.Empty。所以,这就是为什么你的代码的第一部分不会抛出任何异常。它就像是

var bob = "abc" + string.Empty + string.Empty + string.Empty + "123";  //abc123

你的代码第二部分会抛出异常,因为“null”不是对象,null关键字是表示空引用的字面值,它不指向任何对象。null是引用类型变量的默认值。

ToString()是一个方法,只能由对象实例调用,而不能由任何字面值调用。


3
String.Empty 不等于 null。在一些方法中(如 String.Concat)它们只是被同样对待而已。例如,如果你有一个字符串变量设置为 null,当你尝试在其上调用一个方法时,C# 不会用 String.Empty 来替换它。 - Botz3000
3
几乎是。在C#中,null并不像String.Empty那样处理,只是在连接字符串时被视为如此。当你添加字符串时,C#编译器调用的是String.Concat方法。 "abc" + null会被翻译成 String.Concat("abc", null),而在内部,该方法将null替换为String.Empty。第二部分完全正确。 - Botz3000

9
在先于.net的COM框架中,任何接收字符串的例程在完成任务后必须释放它。由于空字符串很常见地传入和传出例程,并且试图“释放”空指针被定义为合法的无操作,因此Microsoft决定将空字符串指针表示为空字符串。
为了兼容COM,.net中的许多例程将解释空对象作为空字符串的合法表示。通过进行一些细微的更改(最重要的是允许实例成员指示“不要作为虚拟调用”),.net及其语言可以使已声明类型为String的null 对象的行为更像空字符串。如果Microsoft这样做了,他们也必须使Nullable工作方式有所不同(以允许Nullable - 无论如何都应该这样做)和/或定义一个NullableString类型,它与String大多数情况下是可互换的,但不会将null视为有效的空字符串。
事实上,有一些情况下,null将被视为合法的空字符串,而其他情况则不会。这并不是非常有帮助的情况,但程序员应该意识到。通常,形式为stringValue.someMember的表达式将在stringValue为null时失败,但大多数框架方法和接受字符串参数的运算符将把null视为空字符串。

5

'+'是一个中缀运算符。像所有的运算符一样,它实际上是在调用一个方法。你可以想象一个非中缀版本"wow".Plus(null) == "wow"

实现者决定采用以下方式...

class String
{
  ...
  String Plus(ending)
  {
     if(ending == null) return this;
     ...
  }
} 

所以,你的例子变成了:
var bob = "abc".Plus(null).Plus(null).Plus(null).Plus("123");  // abc123

这与

相同。
var bob = "abc".Plus("123");  // abc123

在任何情况下,null都不会成为一个字符串。因此,null.ToString()和null.VoteMyAnswer()是没有区别的。 ;)

实际上,它更像是 var bob = Plus(Plus(Plus(Plus("abc",null),null),null),"123");。 操作符重载本质上是静态方法:http://msdn.microsoft.com/en-us/library/s53ehcz3(v=VS.71).aspx 如果不是这样,那么 var bob = null + "abc"; 或尤其是 string bob = null + null; 将无效。 - Ben Mosher
你说得对,我觉得如果我用那种方式解释会让人感到困惑。 - Nigel Thorne

3
我猜是因为它是一个不指向任何对象的字面量。ToString() 需要一个对象。

2

在这个讨论串中,有人说你不能从空无中创造字符串(我认为这是一个不错的说法)。但是是可以的 :-),下面的示例说明了这一点:

var x = null + (string)null;     
var wtf = x.ToString();

代码完全正常,没有抛出异常。唯一的区别是您需要将其中一个null强制转换为字符串 - 如果删除(string)强制转换,则示例仍会编译,但会引发运行时异常:“运算符'+'在类型为''和''的操作数上不明确”。

N.B.在上面的代码示例中,x的值并不是null,而是在将操作数之一转换为字符串后变成了空字符串。

稍微有些不同的是 var a = ((string)null).ToString(); - 它可以编译,但会抛出NullReferenceException异常。在这种情况下,因为null值上不允许使用“.”操作符,所以会抛出异常。在此处使用?.将起作用(但此时不会执行ToString)。编译器将正确地将变量a创建为字符串。


C# / .NET中另一个有趣的事实是,如果考虑不同的数据类型,则处理null的方式并不总是相同。

int? x = 1;  //  string x = "1";
x = x + null + null;
Console.WriteLine((x==null) ? "<null>" : x.ToString());

考虑代码片段的第一行:如果x是一个可空整数变量(即int?),包含值1,那么你会得到结果<null>。如果它是一个字符串(如注释所示)且值为"1",则你会得到"1"而不是<null>注意:还有一个有趣的事实:如果你使用var x = 1;作为第一行,则会导致运行时错误。为什么?因为赋值将变量x转换为数据类型int,它不是可空的。编译器在此处不假定int?,因此在第二行中添加null时失败。

2
null添加到字符串中会被简单地忽略。在您的第二个示例中,null不是任何对象的实例,因此它甚至没有ToString()方法。它只是一个字面量。

1

因为在连接字符串时,string.Emptynull没有区别。 你也可以将null传递给string.Format。但是,如果您试图在null上调用方法,则始终会导致NullReferenceException并因此生成编译器错误。
如果出于某种原因您真的想这样做,您可以编写一个扩展方法,检查null,然后返回string.Empty。但是,在我看来,只有在绝对必要的情况下才应使用这样的扩展。


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