String字符串总结

String类型的变量代表一个字符串,属于.Net应用程序中最常用的类型之一,然后它包含着很对容易被人忽略的地方。

首先String不是值类型,它属于引用类型,由于String变量定义不适用new,因此很对人认为它是值类型变量,但是它实际上属于引用类型,然而因它拥有一些不同于一般引用类型的特性,因此我们将String看成一个特殊的引用类型。

String变量内容是只读的

string s1 = "a";
s1 = "abcd"

上述代码执行之后,s1的值确实为"abcd"。但是这里s1的第二个值“abcd”并不是在第一个值的基础上扩展来的,实际上“abcd”和“a”属于两个独立的String对象。

    当第一句代码执行时,CLR会在托管堆上分配一块内存,创建一个字符串对象,将“a”保存到此内存区域中,然后将此对象的引用保存在变量s1中。

   当执行第二句代码时,CLR会在托管堆中重新分配一块新的内存存放“abcd”,然后修改s1的值,让它引用新创建的字符串对象。所以,上述代码实际导致了CLR分配了两次内存。另外,String类提供了很多诸如Insert、Remove、ToUpper、ConCat等方法,这些方法返回的都是新字符串(CLR新创建的或者是从字符串驻留池中取出来的),与原来的字符串无关。

String的加法操作

string s1 = "ab";
s1 = s1 + "cd";

    由于String变量的内容不可更改,因此CLR会根据要连接的字符串长度新分配一个足够大的空间,将连接后的字符串复制到此空间中。通过测试我们可发现,String类型的+操作符不仅可以连接字符串,还可以连接其他类型的数据,如:

string s1 = "ab";
s1 = s1 + 12;

甚至可以和一个对象连接

string s1 = "ab" + new A();

执行得到s1的值为“UseString.A”,使用ildasm工具查看c#编译器为字符串“+”法生成的IL指令就会发现,String类对象的加法运算是通过在内部调用String类的静态方法ConCat实现的。

它有多个重载形式,如果加法两边的操作数类型不一致,实际上调用的是Concat方法的以下重载形式:

public static String Concat(object arg0, object arg1);

当字符串与值类型相加时,会先创一个匿名的object类型对象,将值类型数字放入此对象中(装箱),然后再调用上面的Concat重载函数。当字符串与引用类型的变量相加时,会默认调用此变量所引用对象的ToString方法将此对象转换为一个字符串,然后再与原字符串相加得到一个新的字符串。

String对象驻留池

    CLR通过维护一个表来存放字符串,该表称为“驻留池”(Intern Pool),有的书叫昝存池,它包含程序中以编程方式声明的字符串常量或者显示添加到池中的所有字符串对象引用。

每个进程都有自己的驻留池,因此同时运行多个.Net应用程序的字符串驻留池不会相互影响。

string s1 = "ab";   //"ab"在驻留池中
string s2 = "ab";   //s2引用驻留池的"ab"字符串
s2 += "cd";         //"abcd"在托管堆上

如下测试代码

string s1 = "aaaa";   
string s2 = "aaaa";   
string s3 = new string('a', 4);
//比对s1和s2是否指向同一字符串
Console.WriteLine((Object)s1 == (Object)s2);//  输出:True
//比对s1和s3是否指向同一字符串
Console.WriteLine((Object)s1 == (Object)s3);//  输出:False

代码中s1和s2在编译时就可知道值,而且他们的值相等,程序运行时,CLR将值"aaaa"放入驻留池中,并且让s1和s2引用这一字符串,所以第4句为True,s3的值虽然也是”aaaa“,但它是在运行时动态创建的,默认情况没有加入驻留池中,所以第5句为False。

   我们也可以使用String类的Intern将一个字符串加入到驻留池中,并返回这一字符串的引用。如果要加入到的字符串在驻留池中已存在,则仅仅返回这一现成的字符串引用,不会导致驻留池中出现两个一模一样的字符串。例如下面代码

string s1 = "aaaa";             //"aaaa"在驻留池中,s1引用它
string s2 = new string('a', 4); //"aaaaa"在托管堆中,s2引用它
//将"aaaa"加入到驻留池,因为原来已有一个同样的字符串在驻留池中,因此不再重复加入,s3引用驻留池的这一字符串
string s3 = string.Intern(s2);
//比对s1和s2是否指向同一字符串
Console.WriteLine((Object)s1 == (Object)s2);//  输出:False
//比对s1和s3是否指向同一字符串
Console.WriteLine((Object)s1 == (Object)s3);//  输出:True

String类性的使用建议

在实际的开发中,String类型的使用非常广泛,但是由于.Net对于String类型的独特管理方式,使用不当可能会对程序的性能有负面影响。因此在是实际开发中我们尽量采取以下措施:

1、避免使用加法运算发连接不同类型的数据

string s1 = "100+100=" + 12;
Console.WriteLine(s1);

如上面两句代码在IL中生成如下两条指令

可以看到这两句代码不仅导致了String类的Concat方法被调用,而且还引发了装箱操作。调用Concat方法会导致CLR为新字符串重新分配内存空间(除非驻留池中有同样的字符串),而装箱操作不仅要分配内存,还需要创建一个匿名对象,匿名对象创建之后还必须有一个数据复制的过程,代价不菲。

  当在一个大循环中出现上述情况时,装箱和字符串连接将导致CLR频繁的分配内存,对程序性能会有较大影响。

2、循环中使用StringBuilder代替String实现字符串连接

     如果在需要动态的将多个子串连接成一个大字符串,并且这种操作非常频繁,则应该避免使用String类的加法运算符。如

string str = "";
for(int i = 1; i <= 10000; ++i)
{
    str += i;   //引发装箱操作
    if(i< 10000)
    {
        str += "+";//引发内存分配操作
    }
}

有了上面知识我们可以知道,上述代码将导致10000此装箱操作,19999此新字符串内存分配操作,改为以代码,程序性能会得到改善。

StringBuilder buffer = new StringBuilder(4096);//预先分配4K的内存空间
for(int i = 1; i <= 10000; ++i)
{
    buffer.Append(i);
    if(i < 10000)
    {
        buffer.Append("+");
    }
}
string result = buffer.ToString();

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值