张银的博客


Eat to live, but do not live to eat.

导航

深入.NET的String

Posted on 2008-12-07 16:02  张银  阅读(834)  评论(3编辑  收藏  举报

主要内容:
C# String 和 string 的区别
字符串的不变性
引用类型
字符串的比较
字符串驻留
StringBuilder对象
错误的new操作符
字符串存储



----------------------------------
@ C# String 和 string 的区别
----------------------------------
1、string是c#中的关键字,String是.net Framework的类;
2、c# string映射为.net Framework的String,如果用string,编译器会把它编译成String,所以如果直接用String就可以让编译器少做一点点工作;
3、string始终代表System.String,String只有在前面有using System的时候并且当前命名空间中没有名为String的类型(class、struct、delegate、enum)的时候才代表System.String


----------------------------------
@ 字符串的不变性
----------------------------------
  String在任何语言中,都有它的特殊性,在.NET中也是如此。它属于基本数据类型,也是基本数据类型中唯一的引用类型。字符串可以声明为常量,但是它却放在了堆中。
  .NETString是不可改变对象,一旦创建了一个String对象并为它赋值,它就不可能再改变,也就是说你不可能改变一个字符串的值。

string a="1234";
a
+="5678";

创建了一个String对象它的值是“1234”a指向了它在内存中的地址,
创建了一个新的String对象它的值是“12345678”,a指向了新的内存地址。
这时在堆中其实存在着两个字符串对象,尽管我们只引用了它们中的一个,但是字符串“
1234”仍然在内存中驻留。

  一旦你的string在堆中创建后,其在内存中都是以const存在,任何的修改都会使其被重新创建为新的string,而指向以前的string的引用将会指向这个新的string!!

string s = "1";                //初始化string
Console.WriteLine(String.IsInterned(s) != null);//输出true,声名一个string s并且赋予"1",这个时候s在CLR的内置池中表示为引用

+= "2";                    //追加string
Console.WriteLine(String.IsInterned(s) != null);//输出false,修改了s的值之后,它已经不在内置池中

String.Intern(s);            
//重新设置为引用,将s再次放置到内置池中
Console.WriteLine(String.IsInterned(s) != null);//这个时候依然输出为true

 

string a = "1";//第一次内置string
+= "2";//第二次分配,复制第一次的1到新的地址中,需重新分配内存
+= "3";//第三次分配,复制前两次的1,2到新的地址中,需重新分配内存
+= "4";//第四次分配,复制前三次的1,2,3到新的地址中,需重新分配内存


  优化的办法很多,第一种,使用string[]数组来代替

string[] Arr1 = new string[4];//声名需要内置4个string
Arr1[0= "1";//内置了1
Arr1[1= "2";//内置了2
Arr1[2= "3";//内置了3
Arr1[3= "4";//内置了4

 可以这么理解:每个数组的子项都是一个被内置的string

  第二种解决办法是char[],如果你知道你的字符串大小,可以这么写char[] c = new char[4]{'1','2','3','4'};这个做法在C/C++中是很不错的,但是在C#似乎用的不多,而且用起来也比较麻烦.因为它不能像C/C++这样: char[] c = {"1234"}


----------------------------------
@ 引用类型
----------------------------------
  前面说过String是引用类型,这就是如果我们创建很多个相同值的字符串对象,它在内存中的指向地址应该是一样的。也就是说,当我们创建了字符串对象a,它的值是“1234”,当我们再创建一个值为“1234”的字符串对象b时它不会再去分配一块内存空间,而是直接指向了a在内存中的地址。这样可以确保内存的有效利用。

public class Test
{
  
public static void Main(string[] args)
  {
    string a = "1234";
    Console.WriteLine(a);
    Test.Change(a);
    Console.WriteLine(a);
    Console.ReadLine();
  }
  public static void Change(string s)
  {
    s 
= "5678";
  }
}

结果是两行1234
如果将Change(string s)改成Change(ref string s),Chang(a)改成Change(ref a),则结果是1234和5678


----------------------------------
@ 字符串的比较
----------------------------------
  在.NET中,对字符串的比较操作并不仅仅是简单的比较二者的值,==操作首先比较两个字符串的引用,如果引用相同,就直接返回True;如果不同再去比较它们的值。所以如果两个值相同的字符串的比较相对于引用相同的字符串的比较要慢,中间多了一步判断引用是否相同。

string a = "1234";
string b = "1234";
string c = "123";
+= "4";
int times = 1000000000;
int start,end;
//测试引用相同所用的实际时间
start = Environment.TickCount;
for(int i=0;i<times;i++)
{
  if(a==b)
  {}
}
end 
= Environment.TickCount;
Console.WriteLine((end
-start));
//测试引用不同而值相同所用的实际时间
start = Environment.TickCount;
for(int i=0;i<times;i++)
{
  if(a==c)
  {}
}
end 
= Environment.TickCount;
Console.WriteLine((end
-start));

打印出来的值后面的大于前面的
==比较字符串时,值相同比引用相同慢许多

.NET中==跟Equals()内部机制完全是一样的,==是它的一个重载。

public static bool operator ==(string a, string b)
{
  return string.Equals(a, b);
}
public static bool Equals(string a, string b)
{
  if (a == b)
  {
    return true;
  }
  if ((a != null&& (b != null))
  {
    return a.Equals(b);
  }
  return false;
}

 

string s1 = "Hello";
string s2 = "Hel";
string s3 = s2 + "lo";
Console.WriteLine(Object.ReferenceEquals(s1,s3));
Console.WriteLine(Equals(s1,s3));
Console.WriteLine(s1 
== s3);

False True True

  说明:虽然 s1 和 s3 引用不同,但String 的 Equals(string,string) 方法还会对字符串的值进行比较,因为值相同,所以返回结果为 True。显然用 Equals(string,string)/== 效率上要比仅比较引用的 ReferenceEquals(string,string) 差很多,如果应用程序中所有的字符串比较都仅比较引用,性能将会大大提升,String 类提供的两个静态方法允许我们做到这一点。

string s1 = "Hello";
string s2 = "Hel";
string s3 = s2 + "lo";
s3 
= String.Intern(s3);
Console.WriteLine(Object.ReferenceEquals(s1,s3));

True True True

  说明:方法 Inter(string) 在 CLR 内部散列表中查找参数指定的字符串,如果能找到返回其引用,如果未找到,字符串将被添加到散列表中,并返回引其引用。
  如果 String 对象不再被进程中的所有应用程序域所引用(作为参数传递给 Intern 方法的那个 String 对象),垃圾收集器可以收回其所占内存及在三列表中的记录。
  一个字符串对象可以被同一个进程中的多个应用程序域访问,即字符串的驻留是以进程为单位的。

public static void Main(string[] args)
{
  string s1 = "Hello";
  string s2 = s1;
  string s3 = s1 + "aidd2008";

  //MyIntern();

  Console.WriteLine(String.IsInterned(s1) 
!= null);
  Console.WriteLine(String.IsInterned(s2) 
!= null);
  Console.WriteLine(String.IsInterned(s3) 
!= null);
}

public static void MyIntern()
{  
  String.Intern(
"Hello aidd2008");
}

True True False

如果包含注释部分,则True True True


----------------------------------
@ 字符串驻留
---------------------------------- 

string a = "1234";
string s = "123";
+= "4";
string b = s;
string c = String.Intern(s);
Console.WriteLine((
object)a == (object)b);
Console.WriteLine((
object)a == (object)c);

 

结果前者False后者True

比较这两个对象发现它的引用并不是一样的。
如果要想是它们的引用相同,可以用Intern()函数来进行字符串的驻留(如果有这样的值存在)。

String s = "Hello";
Console.WriteLine(Object.ReferenceEquals(
"Hello",s));

True

  说明:当 CLR 初始化时,它会创建一个内部的散列表,其中键位字符串,值为指向托管堆中字符串对象的引用,初始化为空。
  当JIT编译器编译方法时,它会在散列表中查找每一个常量字符串,并添加到散列表中,值相同的字符串不重复添加。
  对于上面的代码,编译器会对查找到的第一个"Hello"字符串,将现在托管堆中构造一个新的 String 对象(指向在字符串),然后将 "Hello" 字符串和指向该对象的引用添加到散列表中。
  由于散列表中已经存在 "Hello"字符串,对于查找到的第二个 "Hello" 字符串,将不执行任何操作。
  代码执行时,它会在第一行发现一个 "Hello" 字符串引用,于是便在内部散列表中查找 "Hello",找到后将先前创建的 String 对象的引用保存到变量 s 中。
  当执行第二行代码时,CLR 会再一次在内部散列表中查找 "Hello",并把对应的 String 对象的引用传递给 Object 的静态方法 ReferenceEquals 作为参数。
  通过上面的分析,显然结果为 True。
  当一个引用字符串的方法被 JIT 编译时,所有嵌入在源代码中的常量字符串总会被添加到 CLR 内部的散列表中,但是,运行时动态创建的字符串却不会,参看下面的代码:

string s1 = "Hello";
string s2 = "Hel";
string s3 = s2 + "lo";
Console.WriteLine(Object.ReferenceEquals(s1,s3));  

False

  说明:在上面的代码中,s2 引用的字符串 "Hel" 和一个文本常量字符串 "lo" 连接构造一个新的位于托管堆中的字符串对象,该引用保存在 s3 中,但并未添加到散列表中,与散列表中的 "Hello" 在托管堆中各有一份存储,因此返回的结果是 False。
  需要注意的是,如果 s3 连接的是两个字符串常量 "Hel" + "lo",则又将返回 True。
  这是因为C# 编译器在将代码编译成 IL 指令时会将两者连接。


----------------------------------
@ StringBuilder对象
----------------------------------
  String类型在做字符串的连接操作时,效率是相当低的,并且由于每做一个连接操作,都会在内存中创建一个新的对象,占用了大量的内存空间。这样就引出StringBuilder对象,StringBuilder对象在做字符串连接操作时是在原来的字符串上进行修改,改善了性能。这一点我们平时使用中也许都知道,连接操作频繁的时候,使用StringBuilder对象。但是这两者之间的差别到底有多大呢?

string a = "";
StringBuilder s 
= new StringBuilder();
int times = 10000;
int start,end;
            
//测试String所用的时间//long StartTime  = DateTime.Now.Ticks;
start = Environment.TickCount;
for(int i=0;i<times;i++)
{
+= i.ToString();
}
end 
= Environment.TickCount;
Console.WriteLine((end
-start));
            
//测试StringBuilder所用的时间
start = Environment.TickCount;
for(int i=0;i<times;i++)
{
s.Append(i.ToString());
}
end 
= Environment.TickCount;
Console.WriteLine((end
-start));

结果是前者比后者大很多

当我们连接很少的字符串时可以用String,但当做大量的或频繁的字符串连接操作时,就一定要用StringBuilder

  发现了string的不便之处,而string的替代解决方案就是StringBuilder的使用。

System.Text.StringBuilder sb = new System.Text.StringBuilder();

  这样就初始化了一个StringBuilder 。之后我们可以通过Append()来追加字符串填充到sb中。在你初始化一个StringBuilder 之后,它会自动申请一个默认的StringBuilder 容量(默认值是16),这个容量是由Capacity来控制的,并且允许我们根据需要来控制Capacity的大小,也可以通过Length来获取或设置StringBuilder 的长度。

System.Text.StringBuilder sb = new System.Text.StringBuilder();
sb.Append( 
"123456789" );//添加一个字符串
sb.Length = 3;//设置容量为3
Console.WriteLine( sb.ToString() );//这里输出:123

sb.Length 
= 30;//重新设置容量为30
Console.WriteLine( sb.ToString() + ",结尾");//这里在原来字符串后面补齐空格,至到Length的为30
Console.WriteLine( sb.Length );//这里输出的长度为30

  如果StringBuilder 中的字符长度小于Length的值,则StringBuilder 将会用空格硬填充StringBuilder ,以满足符合长度的设置。
  如果StringBuilder 中的字符长度大于Length的值,则StringBuilder 将会截取从第一位开始的Length个字符。而忽略超出的部分。

System.Text.StringBuilder sb = new System.Text.StringBuilder();//初始化一个StringBuilder
Console.Write("Capacity:" + sb.Capacity);//这里的Capacity会自动扩大
Console.WriteLine("\t Length:" + sb.Length);

sb.Append(
'1'17);//添加一个字符串,这里故意添加17个字符,是为了看到Capacity是如何被扩充的
Console.Write("Capacity:" + sb.Capacity);//这里的Capacity会自动扩大
Console.WriteLine("\t Length:" + sb.Length);

sb.Append(
'2'32);//添加一个字符串
Console.Write("Capacity:" + sb.Capacity);//这里的Capacity会自动扩大
Console.WriteLine("\t Length:" + sb.Length);

sb.Append(
'3'64);//添加一个字符串
Console.Write("Capacity:" + sb.Capacity);//这里的Capacity会自动扩大
Console.WriteLine("\t Length:" + sb.Length);

//注意这里:如果你取消Remove这步操作,将会引发ArgumentOutOfRangeException异常,因为当前容量小于Length,这在自己控制StringBuilder的时候务必要注意容量溢出的问题

sb.Remove(
0, sb.Length);//移出全部内容,再测试
sb.Capacity = 1;//重新定义了容量
sb.Append('a'2);
Console.Write(
"Capacity:" + sb.Capacity);//这里的Capacity会自动扩大
Console.WriteLine("\t Length:" + sb.Length);

sb.Append(
'b'4);
Console.Write(
"Capacity:" + sb.Capacity);//这里的Capacity会自动扩大
Console.WriteLine("\t Length:" + sb.Length);

sb.Append(
'c'6);
Console.Write(
"Capacity:" + sb.Capacity);//这里的Capacity会自动扩大
Console.WriteLine("\t Length:" + sb.Length);

 

Capacity:16     Length:0    //输出第一次,默认的Capacity是16
Capacity:32     Length:17    //第二次,我们故意添加了17个字符,于是Capacity=Capacity*2
Capacity:64     Length:49    //继续超出,则Capacity=Capacity*2
Capacity:128     Length:113
Capacity:
3     Length:2    //清空内容后,设置Capacity=1,重新添加了字符
Capacity:7      Length:6    //后面的结果都类似
Capacity:14     Length:12


  从上面的代码和结果可以说明StringBuilder中容量Capacity是如何增加的:创建一个StringBuilder之后,默认的Capacity初始化为16,接着我们添加17个字符,以方便看到Capacity的扩充后的值。
  大家在修改Capacity的时候,一定要确保Capacity >= Length,否则会引发ArgumentOutOfRangeException异常。
  看完结果,就可以推断出Capacity的公式:
if ( Capacity < Length && Capacity > 0 ){
      Capacity *= 2;
}
  StringBuilder是以当前的Capacity*2来扩充的..所以,在使用StringBuilder需要特别注意,尤其是要拼接或追加N多字符的时候,要注意技巧的使用,可以适当的,有预见性的设置Capacity的值,避免造成过大内存的浪费,节约无谓的内存空间..例如,下列代码就可以根据情况自动的扩展,而避免了较大的内存浪费.

System.Text.StringBuilder sb = new System.Text.StringBuilder();
int i = 0;
long StartTime = DateTime.Now.Ticks;
while (i < 100000)
{
    sb.Append(i.ToString());
    i
++;
}
long EndTime = DateTime.Now.Ticks;

Console.WriteLine(
"时间:" + (EndTime - StartTime) + "\t Capacity:" + sb.Capacity + "\t Length:" + sb.Length);

System.Text.StringBuilder sb1 
= new System.Text.StringBuilder();
= 0;
StartTime 
= DateTime.Now.Ticks;
while (i < 100000)
{
    
if (sb1.Capacity <= sb1.Length)//先判断是否>Length
        sb1.Capacity += 7;//这里一定要根据情况的增加容量,否则会有性能上的消耗
    sb1.Append(i.ToString());
    i
++;
}
EndTime 
= DateTime.Now.Ticks;

Console.WriteLine(
"时间:" + (EndTime - StartTime) + "\t Capacity:" + sb1.Capacity + "\tLength:" + sb1.Length);

自动增加的容量,一定要根据实际预见的情况而改变,否则不但起不到优化的作用,反而会影响到程序的性能


----------------------------------
@ 错误的new操作符
----------------------------------
string a= new string("123");//错误的语句,编译错误。没有string(string)构造函数


----------------------------------
@ 字符串存储
----------------------------------
相同字符串在内存中只保存一份,相同字符串GetHashCode()返回相同值

string a = "0";
string b = "0";
string c = "1";
Console.WriteLine(a.GetHashCode());
Console.WriteLine(b.GetHashCode());
Console.WriteLine(c.GetHashCode());



参考资料:
你真的了解.NET中的String吗?
博客园的集体智慧