C#String字符串案例详解
string是⼀种很特殊的数据类型,它既是基元类型⼜是引⽤类型,在编译以及运⾏时,.Net都对它做了⼀些优化⼯作,正式这些优化⼯作有时会迷惑编程⼈员,使string看起来难以琢磨。这篇⽂章共四节,来讲讲关于string的陌⽣⼀⾯。
⼀.恒定的字符串
要想⽐较全⾯的了解stirng类型,⾸先要清楚.Net中的值类型与引⽤类型。
在C#中,以下数据类型为值类型: bool、byte、char、enum、sbyte以及数字类型(包括可空类型)
以下数据类型为引⽤类型: class、interface、delegate、object、stirng
看到了吗,我们要讨论的stirng赫然其中。被声明为string型变量存放于堆中,是⼀个彻头彻尾的引⽤类型。那么许多同学就会对如下代码产⽣有疑问了,难道string类型也会“牵⼀发⽽动全⾝”吗?让我们先来看看以下三⾏代码有何⽞机:
string a = "str_1";
string b = a;
a = "str_2";
不要说⽆聊,这⼀点时必须讲清楚的!在以上代码中,第3⾏的“=”有⼀个隐藏的秘密:它的作⽤我们可以理解为新建,⽽不是对变量“a”的修改。以下是IL代码,可以说明这⼀点:
.maxstack  1
.locals init ([0] string a,[1] string b)
IL_0000:  nop
IL_0001:  ldstr      "str_1"
IL_0006:  stloc.0
IL_0007:  ldloc.0
IL_0008:  stloc.1
IL_0009:  ldstr      "str_2"
IL_000e:  stloc.0  //以上2⾏对应 C#代码 a = "str_2";
IL_0015:  ret
  可以看出IL代码的第1、6⾏,由ldstr指令创建字符串"str_1",并将其关联到了变量“a”中;7、8⾏直接将堆栈顶部的值弹出并关联到变量“b”中;9、10由ldstr创建字符串"str_2",关联在变量“a”中(并没有像我们想象的那样去修改变量a的旧值,⽽是产⽣了新的字符串);
  在C#中,如果⽤new关键字实例化⼀个类,对应是由IL指令newobj来完成的;⽽创建⼀个字符串,则由ldstr指令完成,看到ldstr指令,我们即可认为,IL希望创建⼀个新的字符串。(注意:是IL希望创建⼀个字符串,⽽最终是否创建,还要在运⾏时由字符串的驻留机制决定,这⼀点下⾯的章节会有介绍。)
  所以,第三⾏C#代码(a = "str_2";)的样⼦看起来是在修改变量a的旧值"str_1",但实际上是创建了⼀个新的字符串"str_2",然后将变量a的指针指向了"str_2"的内存地址,⽽"str_1"依然在内存中没有受到任何影响,所以变量b的值没有任何改变---这就是string的恒定性,同学们,⼀定要牢记这⼀点,在.Net中,string类型的对象⼀旦创建即不可修改!包括ToUpper、SubString、Trim等操作都会在内存中产⽣新的字符串。
本节重点回顾:由于stirng类型的恒定性,让同学友们经常误解,string虽属引⽤类型但经常表现出值的特性,这是由于不了解string的恒定性造成的,根本不是“值的特性”。例如:
string a = "str_1";
a = "str_2";
这样会在内存中创建"str_1"和"str_2"两个字符串,但只有"str_2"在被使⽤,"str_1"不会被修改或消失,这样就浪费了内存资源,这也是为什么在做⼤量字符串操作时,推荐使⽤StringBuilder的原因。
⼆..Net中字符串的驻留(重要)
  在第⼀节中,我们讲了字符串的恒定性,该特性⼜为我们引出了字符串的另⼀个重要特性:字符串驻留。
从某些⽅⾯讲,正是字符串的恒定性,才造就了字符串的驻留机制,也为字符串的线程同步⼯作⼤开⽅便之门(同⼀个字符串对象可以在不同的应⽤程序域中被访问,所以驻留的字符串是进程级的,垃圾回收不能释放这些字符串对象,只有进程结束这些对象才被释放)。
我们⽤以下2⾏代码来说明字符串的驻留现象:
string a = "str_1";
string b = "str_1";
  请各位同学友思考⼀下,这2⾏代码会在内存中产⽣了⼏个string对象?你可能会认为产⽣2个:由于声明了2个变量,程序第1⾏会在内存中产⽣"str_1"供变量a所引⽤;第2⾏会产⽣新的字符串"str_1"供变量b所引⽤,然⽽真的是这样吗?我们⽤ReferenceEquals这个⽅法来看⼀下变量a与b的内存引⽤地址:
string a = "str_1";
string b = "str_1";
Response.Write(ReferenceEquals(a,b));  //⽐较a与b是否来⾃同⼀内存引⽤
//输出:True
  哈,各位同学看到了吗,我们⽤ReferenceEquals⽅法⽐较a与b,虽然我们声明了2个变量,但它们竟然来⾃同⼀内存地址!这说明string b = "str_1";根本没有在内存中产⽣新的字符串。
  这是因为,在.Net中处理字符串时,有⼀个很重要的机制,叫做字符串驻留机制。由于string是编程中⽤到的频率较⾼的⼀种类型,CLR对相同的字符串,只分配⼀次内存。CLR内部维护着⼀块特殊的数据结构,我们叫它字符串池,可以把它理解成是⼀个HashTable,这个HashTable维护着程序中⽤到的⼀部分字符串,HashTable的Key是字符串的值,⽽Value 则是字符串的内存地址。⼀般情况下,程序中如果创建⼀个string类型的变量,CLR会⾸先在HashTable遍历具有相同Hash Code的字符串,如果到,则
直接把该字符串的地址返回给相应的变量,如果没有才会在内存中新建⼀个字符串对象。
  所以,这2⾏代码只在内存中产⽣了1个string对象,变量b与a共享了内存中的"str_1"。
好了,结合第⼀节所讲到的字符串恒定性与第⼆节所讲到的驻留机制,来理解⼀下下⾯3⾏代码吧:
string a = "str_1"; //声明变量a,将变量a的指针指向内存中新产⽣的"str_1"的地址
a = "str_2";  //CLR先会在字符串池中遍历,查看"str_2"是否已存在,如果没有,则新建"str_2",并修改变量a的指针,指向"str_2"内存地址,"str_1"保持不变。(字符串恒定)
string c = "str_2"; //CLR先会在字符串池中遍历"str_2"是否已存在,如果存在,则直接将变量c的指针指向"str_2"的地址。(字符串驻留)
那么如果是动态创建字符串呢?字符串还会不会有驻留现象呢?
我们分3种情况讲解动态创建字符串时,驻留机制的表现:
(1).字符串常量连接
string a = "str_1" + "str_2";
string b = "str_1str_2";
Response.Write(ReferenceEquals(a,b));  //⽐较a与b是否来⾃同⼀内存引⽤
//输出:True
IL代码:
.maxstack  1
.locals init ([0] string a,[1] string b)
IL_0000:  nop
IL_0001:  ldstr      “str_1str_2”
IL_0006:  stloc.0
IL_0007:  ldstr      “str_1str_2”
IL_000d:  ret
  其中第1、6⾏对应c#代码string a = “str_1” + “str_2”;第7、8对应c# string b = “str_1str_2”;可以看出,字符串常量连接时,程序在被编译为IL代码前,编译器已经计算出了字符串常量连接的结果,ldstr指令直接处理编译器计算后的字符串值,所以这种情况字符串驻留机制有效!
(2).字符串变量连接
string a = "str_1";
string b = a + "str_2";
string c = "str_1str_2";
Response.Write(ReferenceEquals(b,c));
//输出:False
IL代码:
.maxstack  2
.locals init ([0] string a, [1] string b, [2] string c)
IL_0000:  nop
IL_0001:  ldstr      “str_1”
IL_0006:  stloc.0
IL_0007:  ldloc.0
IL_0008:  ldstr      “str_2”
IL_000d:  call      string [mscorlib]System.String::Concat(string,string)
IL_0012:  stloc.1
IL_0013:  ldstr      “str_1str_2”
IL_0018:  stloc.2
IL_0019:  ret
  其中第1、6⾏对应string a = “str_1”;第7、8、9⾏对应string b = a + “str_2”;,IL⽤的是Concat⽅法连接字符串,第13、18⾏对应string c = “str_1str_2”;可以看出,字符串变量连接
时,IL使⽤Concat⽅法,在运⾏时⽣成最终的连接结果,所以这种情况字符串驻留机制⽆效!
(3).显式实例化
string a = "a";
string b = new string('a',1);
Response.Write(ReferenceEquals(a, b));
//输出 False
IL代码:
.maxstack  3
.locals init ([0] string a,[1] string b)
IL_0000:  nop
IL_0001:  ldstr      "a"
IL_0006:  stloc.0
IL_0007:  ldc.i4.s  97
IL_0009:  ldc.i4.1
IL_000a:  newobj    instance void [mscorlib]System.String::.ctor(char, int32)
IL_000f:  stloc.1
IL_0010:  ret
  这种情况⽐较好理解,IL使⽤newobj来实例化⼀个字符串对象,驻留机制⽆效。从string b = new string('a',1);这⾏代码我们可以看出,其实string类型实际上是由char[]实现的,⼀个string的诞⽣绝不像我们想想的那样简单,要由栈、堆同时配合,才会有⼀个string的诞⽣。这⼀点在第四节会有介绍。
当然,当字符串驻留机制⽆效时,我们可以很简便的使⽤string.Intern⽅法将其⼿动驻留⾄字符串池中,例如以下代码:
string a = "a";
string b = new string('a',1);   
Response.Write(ReferenceEquals(a, string.Intern(b)));
//输出:True (程序返回Ture,说明变量"a"与"b"来⾃同⼀内存地址。)
三.有趣的⽐较操作
  在第⼀节与第⼆节中,我们分别介绍了字符串的恒定性与与驻留性,如果这位同学友觉得完全掌握了以上内容,那么就在第三节中检验⼀下⾃⼰的学习成果吧!以下10段简单的代码将通过值⽐较与地址引⽤⽐较,来说明前两节讲到的内容,⼤家也可以通过这些代码来检测⼀下⾃⼰对string的了解程度。
代码⼀:
string a = "str_1";
string b = "str_1";
Response.Write(a.Equals(b));
Response.Write(ReferenceEquals(a,b));
//输出:True (Equals⽐较字符串对象的值)
//输出:True (ReferenceEquals⽐较字符串对象的引⽤,由于字符串驻留机制,a与b的引⽤相同)
代码⼆:
string a = "str_1str_2";
string b = "str_1";
string c = "str_2";
string d = b + c;
Response.Write(a.Equals(d));
字符串函数应用详解
Response.Write(ReferenceEquals(a, d));
//输出:True (Equals⽐较字符串对象的值)
//输出:False(ReferenceEquals⽐较字符串对象的引⽤,由于变量d的值为变量连接的结果,字符串驻留机制⽆效)
代码三:
string a = "str_1str_2";
string b = "str_1" + "str_2";
Response.Write(a.Equals(b));
Response.Write(ReferenceEquals(a, b));
//输出:True (Equals⽐较字符串对象的值)
//输出:True (ReferenceEquals⽐较字符串对象的引⽤,由于变量b的值为常量连接的结果,字符串驻留机制有效。如果变量b的值由“常量+变量”的⽅式得出,则字符串驻留⽆效)
代码四:
string a = "str_1";
string b = String.Copy(a);
Response.Write(a.Equals(b));
Response.Write(ReferenceEquals(a, b));
//输出:True (Equals⽐较字符串对象的值)
//输出:False(ReferenceEquals⽐较字符串对象的引⽤,Copy操作产⽣了新的string对象)
string a = "str_1";
string b = String.Copy(a);
b = String.Intern(b);
Response.Write(a.Equals(b));
Response.Write(ReferenceEquals(a, b));
//输出:True (Equals⽐较字符串对象的值)
//输出:True (ReferenceEquals⽐较字符串对象的引⽤,String.Intern实现了字符串驻留)
代码六:
string a = "str_1";
string b = String.Copy(a);
string c = "str_1";
Response.Write((object)a == (object)b);
Response.Write((object)a == (object)c);
//输出:False(“==”在两边为引⽤类型时,则⽐较引⽤的地址,所以a与b为不同引⽤)
//输出:True (“==”在两边为引⽤类型时,则⽐较引⽤的地址,所以a与c的引⽤相同)(原⽂:ReferenceEquals⽐较字符串对象的引⽤,a与c由于字符串驻留机制,引⽤相同)
代码七:
string a = "str_1";
string c = "str_1";
Response.Write(a == c);
//输出:True(刚才我们提到过,“==”在两边为引⽤类型时,则⽐较引⽤的地址;如果是值类型时则需要⽐较引⽤和值。string为引⽤类型,那么上⾯的代码是⽐较了变量a与c的地址还是地址和值呢?
       答案是:⽐较了地址和值!因为在string类型⽐较的时候,“==”已经被重载为“Equals”了,所以,虽然你在⽤“==”⽐较两个引⽤类型,但实际上是在⽤“Equals”⽐较它们的地址和值!(先⽐较地址,地址不等再⽐较值))代码⼋:
string a = "a";
string b = new string('a', 1);
Response.Write(a.Equals(b));
Response.Write(ReferenceEquals(a, b));
//输出:True (Equals⽐较值,a与b的值相同)
//输出:False(ReferenceEquals⽐较字符串对象的引⽤)
代码九:
string a = "a";
string b = new string('a', 1);
Response.Write(a.Equals(string.Intern(b)));
Response.Write(ReferenceEquals(a, string.Intern(b)));
//输出:True (Equals⽐较值,⽆论是否Intern都会相同)
//输出:True (ReferenceEquals⽐较字符串对象的引⽤,Intern已经将b驻留⾄字符串池内)
代码⼗:
string a = "str";
string b = "str_2".Substring(0,3);
Response.Write(a.Equals(b));
Response.Write(ReferenceEquals(a, b));
/
/输出:True (Equals⽐较值,a与c的值相同)
//输出:False(ReferenceEquals⽐较字符串对象的引⽤,Substring操作产⽣了新的字符串对象)
四.艺海拾贝
  这⼀节将主要给⼤家介绍⼀些string的常见问题。
(1)“string = ”与“new stirng()”的区别
string test = "a";
string test = new string('a', 1);
  以上两⾏代码的效果是⼀样的,它们的区别在于加载”a”的时间不同:第⼀⾏的“a”是⼀个常量,在编译期就已经被放在⼀个叫做常量池的地⽅了,常量池通常装载⼀些在编译期被确
定下来的数据,例如类、接⼝等等;⽽第⼆⾏是运⾏时CLR在堆中⽣成的值为“a”的字符串对象,所以后者没有字符串驻留。
(2). string 与 String的区别
  String的⼤名叫做System.String,在编译为IL代码时,string和System.String会⽣成完全相同的代码:(ps:long和System.Int64,float和System.Single等也有此特性)
C#代码:
string str_test = "test";
System.String Str_test = "test";
IL代码:
// 代码⼤⼩      14 (0xe)
.maxstack  1
.locals init ([0] string str_test,[1] string Str_test)
IL_0000:  nop
IL_0001:  ldstr      "test"
IL_0006:  stloc.0
IL_0007:  ldstr      "test"
IL_000c:  stloc.1
IL_000d:  ret
  所以,⼆者的区别并不在于底层,⽽是在于string是类似于int的基元类型;System. String是框架类库(FCL)的基本类型,⼆者之间有直接的对应关系。
(3).StringBuilder
  StringBuilder提供了⾼效创建字符串的⽅法,由StringBuilder表⽰的字符串是可变的(⾮恒定的),在需要多处使⽤“+”连接字符串变量的时候,推荐使⽤StringBuilder来完成,最后调
⽤其ToString()⽅法输出。当调⽤了StringBuilder的ToString()⽅法之后,StringBuilder将返回其内部维护的⼀个字符串字段引⽤,如再次修改StringBuilder,它将会创建⼀个新的字符
串,这时被修改的是新的字符串,原来已经返回的字符串才不会发⽣改变。
StringBuilder有两个⽐较重要的内部字段,⼤家需要掌握:
  m_MaxCapacity:StringBuilder的最⼤容量,它规定了最多可以放置到       
  m_StringValue的字符个数,默认值为Int32.MaxValue。m_MaxCapacity⼀旦被指定就不能再更改。
  m_StringValue:StringBuilder维护的⼀个字符数组串,实际上可以理解为⼀个字符串。StringBuilder重写的Tostring()⽅法返回的就是这个字段。
到此这篇关于C# String字符串案例详解的⽂章就介绍到这了,更多相关C# string字符串详解内容请搜索以前的⽂章或继续浏览下⾯的相关⽂章希望⼤家以后多多⽀持!

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。