在C#中,字符串是一个使用比较多的类型,本文会讲到字符串(string)的一些特殊点。
定义
我们先来定义一个字符串类型变量。
string str = "这是一个字符串";
在上面的代码中,我们试用了 stirng 关键字作为类型来定义字符串变量,其实字符串真正的类型为 String , string 是作为C#关键字的一个别名,在编码的时候,IDE会给提示,要求我们将 String 修改成 string 。
我们可以通过反编译来查看 String 的具体定义, String 其实是一个引用类型,派生于 Object 。但是我们在使用的过程中, String 又会表现出值类型的特性(多个变量即使指向了同一个值,在对其中一个变量修改时也不会污染到其他的变量)
public sealed class String : IComparable, ICloneable, IConvertible, IEnumerable, IComparable<string>, IEnumerable<char>, IEquatable<string>
{
}
可以看到 String 类型的定义使用 sealed 关键字,表明 String 类型不可被当作基类继承。 这是因为CLR 对 String 类型做了一些特殊优化,必须防止自定义类型破坏了 CLR 的优化。
这里涉及到 String 类型的一个特性——不变性,即 String 对象一旦创建,就不能再更改.。不变性带来了另一个特性——一致性,即相同的字符串在 CLR 中在表现上同一个对象(其实不是)。这样在使用上, String 类型就和其他的值类型表现一致。不过由于这些特性的存在,所在如果对字符串进行多次修改操作会产生很多临时字符串对象,对性能有影响,所以在很多的库和最佳实践中,都会推荐使用 StringBuilder 来操作。
string str1 = "这是一个字符串";
string str2 = "这是一个字符串";
Console.WriteLine(object.Equals(str1,str2));
// True
Console.WriteLine(object.ReferenceEquals(str1,str2));
// False
可以看到在对象的层面上,两个字符串对象是一致的,这是因为 Equals 比较是靠 GethashCode 方法,而由于 CLR 的特殊优化, String 类型的 GetHashCode 方法会对同样的字符返回一样的哈希值。但是如果我们使用 ReferenceEquals 来比较两个对象的引用还是可以看到其实是两个对象,那么有没有办法做到字符串对象的复用呢?
答案是肯定的,我们只需要使用 Intern 方法即可。
在 CLR 初始化的时候,会构建一个内部表(暂存池)用来存放 String 对象的引用,而 Intern 方法就是用来访问内部表的,使用 Intern 方法的时候,会先检查内部表中有没有同样的字符串存在,如果有就会直接返回该字符串的引用。所以使用 Intern 方法获得的 String 对象在引用上也是相同的。
string str1 = "这是一个字符串";
string str2 = string.Intern("这是一个字符串");
Console.WriteLine(object.Equals(str1,str2));
// True
Console.WriteLine(object.ReferenceEquals(str1,str2));
// True
当然上面的测试都是在 .net 6的环境中进行的,如果 CLR 的实现不同,对于字符串的优化也有可能不同,甚至在 CLR 中默认开启字符串池,直接把相同值得字符串对象彻底优化成一个引用也是可能的。