string详解

特殊的string

string的特殊之处在于即像值类型,有像引用类型的特点。为什么会有这样的设计呢,先得看看值类型和引用类型的特点。

  • 值类型通常被人们称为轻量级的类型,因为在大多数情况下,值类型的的实例都分配在线程栈中,因此它不受垃圾回收的控制,缓解了托管堆中的压力,减少了应用程序的垃圾回收的次数,提高性能。
  • 引用类型所有的引用类型的实例都分配在托管堆上,c#中new操作符会返回一个内存地址指向当前的对象。在分配对象时,可能会进行一次垃圾回收操作(如果托管堆上的内存不够分配一次对象时)

string类型是程序开发中使用率非常高的一种类型,因此希望他可以像值类型一样的进行赋值和操作,但又不会占用过多的线程栈。特需要如下的特点:

  • 不用new即可直接赋值常量
  • 非引用操作,即操作直接对值进行操作,而非对值的地址操作
  • 可以容纳长字符串

字符串恒定性 (immutable)

和其他类型比较,string最为显著的一个特点就是它具有恒定不变性,在.net平台下,为了实现我们对字符串类型的需求,.net采用如下的方式解决字符串的问题:

  1. 内存驻留:字符串在内存上的驻留方式是堆上保留一块特殊的空间,这块空间以键值对的形式存在的interning table。当新建一个字符串时,系统为这个字符串开辟一块内存,然后以这个字符串为key,以这个字符串的地址为Value添加到table中,下次再使用相同字符串时,则直接再字典中检索该key然后即可得到该字符串的地址。

  2. 编辑:当需要对字符串进行修改时,系统为得到的新字符串重新开辟一块内存并保存新字符串,原字符串不做任何修改;这样既可保证相同的字符串在内存中是同一个地址,又可以保证当修改一个字符串时,不会影响其他对该字符的引用。

  3. 动态创建: 对于一个字符串常量与一个字符串变量动态计算出来的字符串(如;str1 = “abc”,str2 = str1+”abc”),是不会被放到table中的;但是对于两个字符串常量的相加(如:”abc”+”123″)的结果是被保存到table中的,因为在编译成IL的时候结果就已经被计算出来了
  4. 作用域:==string的恒定性不单单是针对某一个单独的AppDomain,而是针对一个进程的==。

  5. 垃圾回收:我们知道在一个托管的环境下,一个对象的生命周期被GC管理和控制。一个对象只有在他不被引用的时候,GC才会对他进行垃圾回收。而对于一个string来说,它始终被interning table引用,而这个interning table是针对一个Process的,是被该Process所有AppDomain共享的,所以一个string的生命周期相对比较长,只有所有的AppDomain都不具有对该string的引用时,他才有可能被垃圾回收。

示例

string str1 = "ABCD1234";
string str2 = "ABCD1234";
string str3 = "ABCD";
string str4 = "1234";
string str5 = "ABCD" + "1234";
string str6 = "ABCD" + str4;
string str7 = str3 + str4;

Console.WriteLine("string str1 =\"ABCD1234\";");
Console.WriteLine("string str2 =\"ABCD1234\";");
Console.WriteLine("string str3 =\"ABCD\";");
Console.WriteLine("string str4 =\"1234\";");
Console.WriteLine("string str5 =\"ABCD\" + \"1234\";");
Console.WriteLine("string str6 =\"ABCD\" + str4;");
Console.WriteLine("string str7 = str3 + str4;");

Console.WriteLine("\nobject.ReferenceEquals(str1, str2) = {0}", object.ReferenceEquals(str1, str2));
Console.WriteLine("object.ReferenceEquals(str1, \"ABCD1234\") = {0}", object.ReferenceEquals(str1, "ABCD1234"));

Console.WriteLine("\nobject.ReferenceEquals(str1, str5) = {0}", object.ReferenceEquals(str1, str5));
Console.WriteLine("object.ReferenceEquals(str1, str6) = {0}", object.ReferenceEquals(str1, str6));
Console.WriteLine("object.ReferenceEquals(str1, str7) = {0}", object.ReferenceEquals(str1, str7));

//String.Intern表示将一个字符串加入到table中
Console.WriteLine("\nobject.ReferenceEquals(str1, string.Intern(str6)) = {0}", object.ReferenceEquals(str1, string.Intern(str6)));
Console.WriteLine("object.ReferenceEquals(str1, string.Intern(str7)) = {0}", object.ReferenceEquals(str1, string.Intern(str7)));

Console.WriteLine("object.ReferenceEquals(str4, 1234.ToString()) = {0}", object.ReferenceEquals(str4, 1234.ToString()));

Console.ReadLine();

运行结果

string str1 = "ABCD1234";
string str2 = "ABCD1234";
string str3 = "ABCD";
string str4 = "1234";
string str5 = "ABCD" + "1234";
string str6 = "ABCD" + str4;
string str7 = str3 + str4;

object.ReferenceEquals(str1, str2) = True //指向table中的同一个key的值
object.ReferenceEquals(str1, "ABCD1234") = True//同上

object.ReferenceEquals(str1, str5) = True//str5是两个常量相加的结果,所以结果是""ABCD1234",因此完全相同
object.ReferenceEquals(str1, str6) = False//str6是字符串加变量的结果,不保存在table中
object.ReferenceEquals(str1, str7) = False//str7是字符串变量相加的结果,不保存在table中

object.ReferenceEquals(str1, string.Intern(str6)) = True//已经将str6的结果加入到string table中了 所以引用相同
object.ReferenceEquals(str1, string.Intern(str7)) = True
object.ReferenceEquals(str4, 1234.ToString()) = False//tostring方法的结果也是动态生成的字符串,不被放入到string table 中

StringBuilder

由于string的这些特性,导致string既像值类型又像引用类型,而且当频繁对string进行操作时,将出现效率的问题。因此C#中引入了StringBuilder来解决这个问题。
StringBuilder是一个标准的引用类型,被分配在标准的堆上,这使得你可以很清楚他在内存中的状态,便于管理且效率极高。

StringBuilder sb = new StringBuilder(20);//指定分配大小,指定分配内存大小,性能就会得到提升。
//如果超过指定大小系统,系统会倍增,自动增加的,40,60,80
sb.Append("aaa");//分配到堆区
sb.Append("ddd");//不会被销毁,直接加到后面

因此当我们需要频繁操作字符串时,最好将其定义为StringBuilder类型的。
当然,你可以可以使用string.Format的格式化方法来处理,因为该方法底层也是生成一个StringBuilder来拼接字符串。在C#的新语法中,引入了string。Format的语法糖$,使得生成StringBuider更加的方便。

string str1=string.Format("学号:{0};姓名:{1}",student.ID,student.Name);
string str2= $"学号:{student.ID};姓名:{student.Name}" //string.Format语法糖

string ref

值类型变量做引用传递时,必须加入ref;引用类型的变量传递的就是引用,不需要ref。那么string作为一种特殊的引用类型是否需要加Ref呢。

static void Main(string[] args)
{
string str1 = "1";
string str2 = "2";
Console.WriteLine($"str1={str1},str2={str2}");
Console.WriteLine($"Swap(str1, str2)");
Swap( str1, str2);
Console.WriteLine($"str1={str1},str2={str2}");
Console.WriteLine($"SwapRef(ref str1,ref str2)");
SwapRef(ref str1,ref str2);
Console.WriteLine($"str1={str1},str2={str2}");
Console.ReadLine();
}
public static void Swap( string s1, string s2)
{
string temp = s1;
s1 = s2;
s2 = temp;
}
public static void SwapRef(ref string s1,ref string s2)
{
string temp = s1;
s1 = s2;
s2 = temp;
}

运行结果

str1=1,str2=2
Swap(str1, str2)//未加ref传值,无法完成交换
str1=1,str2=2
SwapRef(ref str1,ref str2)//加了ref,完成交换
str1=2,str2=1

很明显,string类型的引用传值必须加ref。因为方法的参数传递实际是执行了s1=str1的操作。由于string的赋值符号实际是新建了一个字符串指向了string table中key为“1”的位置,因此此时的s1与str1已经不是同一个对象了,所以是无法完成交换操作的;只有加入了ref传参,才是将str1的地址传给了s1,这样对s1的操作才会传递到str1上。

本文参考

蒋金楠《深入理解string和如何高效地使用string

posted on 2018-09-11 12:53 MyWay_ 阅读() 评论() 编辑 收藏

版权声明:本文为cug-weipeng原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/cug-weipeng/p/9626969.html