为什么在初始化字符串时不使用new运算符?

37

我在面试中被问到这个问题:字符串是引用类型还是值类型。

我回答说它是引用类型。然后他问我为什么在初始化字符串时不使用new运算符?我回答说因为C#语言有一个更简单的语法来创建字符串,编译器会自动将代码转换为调用System.String类的构造函数。

这个答案正确吗?


4
大部分正确,但是字符串更加复杂,并且以奇怪的方式被缓存和共享。祝你好运。 - Kobi
7个回答

33

字符串是不可变的引用类型。有一个ldstr IL指令,允许将一个新的对象引用推送到字符串字面值。所以当你写:

string a = "abc";
编译器会检查元数据中是否已经定义了 "abc" 字面量,如果没有,则声明它。然后将此代码转换为以下 IL 指令:
ldstr "abc"
基本上这使得a本地变量指向在元数据中定义的字符串文字。所以我认为你的答案不完全正确,因为编译器不会将其转换为对构造函数的调用。

31

并不完全是正确的答案。 字符串 是“特殊”的引用类型,它们是不可变的。你说得没错,编译器确实在内部执行了一些操作,但这不是构造函数调用。它调用ldstr,该方法会将一个指向储存在元数据中的字符串文字的新对象引用推入堆栈。

C# 代码示例:

class Program
{
    static void Main()
    {
        string str;
        string initStr = "test";
    }
}

以下是IL代码

.method private hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       8 (0x8)
  .maxstack  1
  .locals init ([0] string str,
           [1] string initStr)
  IL_0000:  nop
  IL_0001:  ldstr      "test"
  IL_0006:  stloc.1
  IL_0007:  ret
} // end of method Program::Main

你可以看到上面的ldstr调用。

由于字符串的不可变性,使得只保留不同/唯一字符串成为可能。所有字符串都保存在哈希表中,其中键是字符串值,值是该字符串的引用。每次有新字符串时,CLR检查哈希表中是否已经存在这样的字符串。如果存在,则不会分配新内存,而是将引用设置为现有字符串的引用。

您可以运行此代码进行检查:

class Program
{
    static void Main()
    {
        string someString = "abc";
        string otherString = "efg";

        // will retun false
        Console.WriteLine(Object.ReferenceEquals(someString, otherString));

        someString = "efg";

        // will return true
        Console.WriteLine(Object.ReferenceEquals(someString, otherString));
    }
}    

1
太好了!感谢您提供深入的解释,特别是关于内部哈希表的部分。我从未想过这一点。 - NDeveloper
并非所有字符串都保存在哈希表中,只有被整合的字符串才会。字符串字面量是被整合的,但是任何新创建的字符串都不会自动被整合。 - Guffa
很好的解释。我只是想知道当我们给字符串变量赋新值时会发生什么。因为我发现字符串是一个类。 - ExpertLoser

14

其实编译器确实有特殊语法来简化字符串的创建。

但是关于编译器生成构造函数调用的部分并不完全正确。字符串字面量在应用程序启动时就已经被创建了,所以在使用字符串字面量时,只是将引用赋值给一个已经存在的对象。

如果在循环中赋值字符串字面量:

string[] items = new string[10];
for (int i = 0; i < 10; i++) {
  items[i] = "test";
}

它不会为每次迭代创建新的字符串对象,而只会将相同的引用复制到每个项目中。

关于字符串字面值,还有两件值得注意的事情:编译器不会创建重复项,并且如果您连接它们,它会自动合并它们。如果您多次使用相同的字面字符串,它将使用相同的对象:

string a = "test";
string b = "test";
string c = "te" + "st";

变量abc都指向同一个对象。

字符串类还有可以使用的构造函数:

string[] items = new string[10];
for (int i = 0; i < 10; i++) {
  items[i] = new String('*', 42);
}
在这种情况下,您实际上将获得十个单独的字符串对象。

4
不是的。编译器不会改变构造方式。那么构造函数参数应该是什么类型?字符串?;-)
字符串字面量是没有名称的常量。
此外,如果类支持运算符,您可以使用字符串字面量初始化任何类:
   public class UnitTest1 {
      class MyStringable {
         public static implicit operator MyStringable(string value) {
            return new MyStringable();
         }
      }

      [TestMethod]
      public void MyTestMethod() {
         MyStringable foo = "abc";
      }
   }


编辑 更加清晰明了: 你所问的,如果string会被转换成任何构造函数调用,让我们来看一下IL代码。

以这个测试方法为例:

   [TestClass]
   class MyClass {
      [TestMethod]
      public void MyTest() {
         string myString = "foo";
         if (myString == "bar")
            Console.WriteLine("w00t");
      }
   }

生成以下IL代码:

.method public hidebysig instance void MyTest() cil managed
{
    .custom instance void [Microsoft.VisualStudio.QualityTools.UnitTestFramework]Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute::.ctor()
    .maxstack 2
    .locals init (
        [0] string myString,
        [1] bool CS$4$0000)
    L_0000: nop 
    L_0001: ldstr "foo"
    L_0006: stloc.0 
    L_0007: ldloc.0 
    L_0008: ldstr "bar"
    L_000d: call bool [mscorlib]System.String::op_Equality(string, string)
    L_0012: ldc.i4.0 
    L_0013: ceq 
    L_0015: stloc.1 
    L_0016: ldloc.1 
    L_0017: brtrue.s L_0024
    L_0019: ldstr "w00t"
    L_001e: call void [mscorlib]System.Console::WriteLine(string)
    L_0023: nop 
    L_0024: ret 
}

正如您所看到的,所有字符串值(foo、bar和w00t)仍然是字符串,并且不调用任何隐藏的构造函数。

希望这更易于理解。


5
非常酷,但我不太明白它如何帮助解释字符串。 - Kobi
第一部分试图解释。字符串是内置的C#语言特性。字符串字面量是字符串,编译器永远不会将其更改为提供给字符串构造函数的任何内容。也许答案太草率了...抱歉! - Florian Reischl
需要构造函数的规范才能准确地完成。C#规范仅说明同一程序集中的两个文字应指向同一实例。因此,将表达式重写为使用String(char[])构造函数并缓存该实例以供以后使用是完全可以的。 此外,要创建实例,需要在某个时刻进行创建 :) - Rune FS

1

正如大家所说,字符串是不可变的,因此隐式地没有构造函数调用。我想为你添加以下参考资料,它可能会更加清晰明了:

String Immutability


0

但是我们可以使用new运算符来初始化字符串

String str = new char[] {'s','t','r'};

这个答案正确吗?不是的,字符串被缓存并且原样在IL中使用。

0

这是我的看法,我不完全确定,所以请谨慎对待我的答案。

.NET 中的字符串字面量是自包含的,其长度或其他数据结构在字面值本身中被内部包含。因此,与 C 不同,在 .NET 中分配字符串字面量只是分配整个字符串数据结构的内存地址的问题。在 C 中,我们需要在字符串类中使用 new,因为它需要分配围绕空终止字符串的其他数据结构,例如长度。


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