代码改变世界

String虽然很简单,但他的某些特性你真理解正确了吗?

2009-05-18 09:18  周国选  阅读(316)  评论(0编辑  收藏  举报

最近在园子里看到几篇关于string的文章,感觉其中有一些误解,不知道是自己理解有误还是园友理解错误,也没发现有园友提出质疑,索性也将自己的一点理解写出来,也对一些质疑提出了自己的解释,不管怎样我希望如果是我哪里理解错误大家一定要提出来,我们一起进步,否则真的会误导很多人,也感觉到写文章是要负责任的,否则就干脆写日志,不要发表出来误导到了一些对Net不熟悉的朋友!

 

1.在DoNet中String是不可改变的,什么叫不可变呢,首先大家先看下面的例子:
例如:
string a = "1";
Console.WriteLine(a);//1    
a+="2";
Console.WriteLine(a);//12
大家可以看看下面生成的对应的IL代码,可以看到a+="2"并非是在原有分配的堆中进行修改,而是创建了一个新的字符串;
而+在IL中也是使用Concat,将两个字符串联;所以String的不可变就是当你在创建了字符串后其不可修改;
.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // 代码大小       34 (0x22)
  .maxstack  2
  .locals init ([0] string a)
  IL_0000:  nop
  IL_0001:  ldstr      "1"
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_000d:  nop
  IL_000e:  ldloc.0
  IL_000f:  ldstr      "2"
  IL_0014:  call       string [mscorlib]System.String::Concat(string,
                                                              string)
  IL_0019:  stloc.0
  IL_001a:  ldloc.0
  IL_001b:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0020:  nop
  IL_0021:  ret
} // end of method Program::Main

 

2.当创建多个字符串内容相同的对象时,都只会指向同一个引用;
例如:
            string a = "11";
            string b = "11";
            string c = "11";
            string d = "1";
            d += "1";
a和b都指向同一个a的引用,并不会为b重新分配内存;这样即可保证内存有效利用;
这一点用IL不是很好看出来,所以大家可以按下面方法去比较他们的引用地址,都是指向同一个堆;
Console.WriteLine(string.ReferenceEquals(a, b));
Console.WriteLine(string.ReferenceEquals(a, c));
Console.WriteLine(string.ReferenceEquals(c, b));
Console.WriteLine(string.ReferenceEquals(a, d));
Console.WriteLine(string.ReferenceEquals(b, d));
Console.WriteLine(string.ReferenceEquals(c, d));
运行结果:
   True
   True
   True
   False
   False
   False
这说明当我们新建一个字符串以后,将为其分配内存,而之后如何有值和其相同的字符串不会被创建分配堆而是直接分配其引用;
  


3.字符串比较:
==和!=是为了比较String对象的值是否相同而不是比较引用;
例如等号首先是比较引用是否相同,因为引用相同值一定会相同,就避免了再去对值进行比较;
而引用如果不同再去比较值,相同则返回true;
总之==一定一定是为了比较字符串的值是否相同;
string s2 = new StringBuilder().Append("My").Append("Test").ToString();
string s3 = new StringBuilder().Append("My").Append("Test").ToString();
Console.WriteLine((object)s2==(object)s3);//False
上面是返回False,大家肯定有些迷惑,而此前一位园友则解释我也不尽赞同,这里只发表我的解释:
此处比较的是两个引用类型,为什么这么说,因为大家都知道引用地址都是存放在栈中,而此处正是将存放在栈中的引用地址装箱转换成了引用类型也就是分别创建了两个值为引用地址的堆,而他们引用地址的值肯定不一样,所以结果显然就是False了;

 


4.为什么总是提倡使用StringBuilder对象;
如果用String,每进行一次字符串的拼接就要分配一个新的堆,如果只有一两次倒无所谓,如果频繁如此操作性能会很差;但StringBuilder是在为其分配的堆上做修改,不会重新分配,所以性能比String拼接字符串好;相信大家理解上面几点了我这里也纯粹是废话了,呵呵!!

 


5.最后就是看到有人提到Intern方法,我感觉他们对此方法具体功能理解不透彻存在误解;
首先看下面例子:
        static void Main(string[] args)
        {
            string a = "12";
            string b = "1";
            b += "2";
            string c = string.Intern(b);
            Console.WriteLine((object)a == (object)b);//false
            Console.WriteLine((object)a == (object)c);//true
            Console.WriteLine((object)b == (object)c);//false
        }
       
  static void Main(string[] args)
        {
            string a = "1212";
            string b = "1";
            b += "2";
            string c = string.Intern(b);
            Console.WriteLine((object)a == (object)b);//false
            Console.WriteLine((object)a == (object)c);//false
            Console.WriteLine((object)b == (object)c);//true
        }
       
  static void Main(string[] args)
        {
            string b = "1";
            b += "2";
            string c = string.Intern("12");
            Console.WriteLine((object)b == (object)c);//false
        }
先看第一个Main,我根据第一个Main来解释:
如果传递给Intern的是一个变量的引用地址,那么他会检索是否用与此引用地址所指向的堆的值相同的其他堆,如果有他会返回此堆的引用地址,也就是a;

我们再来看第二个Main来继续解释:
如果没有找到与所指向的堆的值相同的其他堆,他将返回b的引用地址;

最后看第三个Main做最后解释:
如果传递进去的是字符串而不是引用地址,他一样会检索是否存在与此字符串相同的堆,如果有则返回此堆的引用地址,如无则驻留此字符串并返回其引用地址;

而这里所将的驻留也是String的特殊之处,字符串在被创建后不会被立即回收,即使已不存在对其引用;他会在被创建之后一直驻留在内存中直至程序结束,大家可以查看MSDN解释,下面将拿出一些MSDN的解释来配合说明一下;
其中=>符号的是我做的注释:
------MSDN解释--Start------------------------------------------------------------------------------------
在1.1中
string str1 = String.Empty;
string str2 = String.Intern(String.Empty);
if ((object) str1) == ((object) str2);
...
==>是返回false,但在2.0中返回true,所以更加证实了如上所说;下面主要是讲解了String的拘留池概念,大家理解上面概念后相信理解他也不成问题;

公共语言运行库通过维护一个表来存放字符串,该表称为拘留池,它包含程序中以编程方式声明或创建的每个唯一的字符串的一个引用.因此,具有特定值的字符串的实例在系统中只有一个.例如,如果将同一字符串分配给几个变量,运行库就会从拘留池中检索对该字符串的相同引用,并将它分配给各个变量.Intern 方法使用拘留池来搜索与 str 值相等的字符串.如果存在这样的字符串,则返回拘留池中它的引用.如果不存在,则向拘留池添加对 str 的引用,然后返回该引用.如果要减少应用程序分配的内存总量,请记住留用字符串有两个不希望出现的副作用:首先,为留用的 String 对象分配的内存在公共语言运行库 (CLR) 终止之前不大可能释放.这是因为 CLR 对留用的 String 对象的引用可能保持到应用程序终止之后,甚至可能保持到应用程序域终止之后.其次,要留用字符串,必须先创建字符串.即使 String 对象使用的内存最终将通过垃圾回收,仍然必须分配该内存.

==>上面就说到了[为留用的 String 对象分配的内存在公共语言运行库 (CLR) 终止之前不大可

能释放.]

.NET Framework 2.0 版引入了 CompilationRelaxations.NoStringInterning 枚举成员.
NoStringInterning 成员将程序集标记为不需要字符串拘留.可以使用 CompilationRelaxationsAttribute 属性将 NoStringInterning 应用于某个程序集.使用本机映像生成器 (Ngen.exe) 将该程序集安装到本地计算机上的本机映像缓存时,不使用字符串拘留.

==>这里所提到的本机映像可以改善内存使用情况,大家可以查MSDN详细了解;
------MSDN解释--End-------------------------------------------------------------------------------------