我在面试中被问到这个问题:字符串是引用类型还是值类型。
我回答说它是引用类型。然后他问我为什么在初始化字符串时不使用new运算符?我回答说因为C#语言有一个更简单的语法来创建字符串,编译器会自动将代码转换为调用System.String类的构造函数。
这个答案正确吗?
字符串是不可变的引用类型。有一个ldstr IL指令,允许将一个新的对象引用推送到字符串字面值。所以当你写:
string a = "abc";
编译器会检查元数据中是否已经定义了 "abc"
字面量,如果没有,则声明它。然后将此代码转换为以下 IL 指令:ldstr "abc"
基本上这使得a
本地变量指向在元数据中定义的字符串文字。所以我认为你的答案不完全正确,因为编译器不会将其转换为对构造函数的调用。并不完全是正确的答案。 字符串 是“特殊”的引用类型,它们是不可变的。你说得没错,编译器确实在内部执行了一些操作,但这不是构造函数调用。它调用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));
}
}
其实编译器确实有特殊语法来简化字符串的创建。
但是关于编译器生成构造函数调用的部分并不完全正确。字符串字面量在应用程序启动时就已经被创建了,所以在使用字符串字面量时,只是将引用赋值给一个已经存在的对象。
如果在循环中赋值字符串字面量:
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";
变量a
、b
和c
都指向同一个对象。
字符串类还有可以使用的构造函数:
string[] items = new string[10];
for (int i = 0; i < 10; i++) {
items[i] = new String('*', 42);
}
在这种情况下,您实际上将获得十个单独的字符串对象。 public class UnitTest1 {
class MyStringable {
public static implicit operator MyStringable(string value) {
return new MyStringable();
}
}
[TestMethod]
public void MyTestMethod() {
MyStringable foo = "abc";
}
}
以这个测试方法为例:
[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)仍然是字符串,并且不调用任何隐藏的构造函数。
希望这更易于理解。
但是我们可以使用new运算符来初始化字符串
String str = new char[] {'s','t','r'};
这是我的看法,我不完全确定,所以请谨慎对待我的答案。
.NET 中的字符串字面量是自包含的,其长度或其他数据结构在字面值本身中被内部包含。因此,与 C 不同,在 .NET 中分配字符串字面量只是分配整个字符串数据结构的内存地址的问题。在 C 中,我们需要在字符串类中使用 new,因为它需要分配围绕空终止字符串的其他数据结构,例如长度。