当前位置:主页 > 软件编程 > .NET代码 >

c#基础系列之System.String的深入理解

时间:2020-10-11 10:47:43 | 栏目:.NET代码 | 点击:

前言

几乎任何一个项目都离不开对字符串的处理,在C和C++编程中,许多程序的漏洞都是由于字符串缓冲区溢出造成的。为了避免在C#中出现类似的问题,同时也为了使用更方便,C#中专门设置了两个字符串处理类:String类和StringBuilder类。

本文主要给大家介绍了关于c#基础系列之string的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧

扩展阅读:深入理解值类型和引用类型

基本概念

string(严格来说应该是System.String) 类型是我们日常coding中用的最多的类型之一。那什么是String呢?^ ~ ^

String是一个不可变的连续16位的Unicode代码值的集合,它直接派生自System.Object类型。

与之对应的还有一个不常用的安全字符串类型System.Security.SecureString,它会在非托管的内存上分配,以便避开GC的黑手。主要用于安全性特高的场景。[具体可查看msdn这里不展开讨论了。=>msdn查看详情

特性


注意事项

  static void Main(string[] args)
   {
    string a = "c:\\temp\\1";
    string b = @"c:\temp\1";
    Console.WriteLine(a);
    Console.WriteLine(b);
    Console.Read();   
   } 

性能

String.Intern

String.IsInterned

具体请查看msdn(https://msdn.microsoft.com/zh-cn/library/system.string.isinterned(v=vs.110).aspx)

但是c#编译器默认是不开启字符串留用功能的,因为如果程序大量把字符串留用,应用程序总体性能可能会变得更慢。(微软也是挺纠结的,程序员TMD的更纠结)

如果我们的程序中有很多个一模一样值的常量字符串, c#的编译器会在编译期间把这些字符串合并为一个并写入模块的元数据中,然后修改所有引用该字符串的代码。这也是一种字符串重用技术,学名‘字符串池'。这意味着什么呢?这意味着所有值相同的常量字符串其实引用的是同一个内存地址的实例,在相同值非常多的情况下能显著提高性能和节省大量内存。

string s1 = "hello 大菜";
string s2 = "hello 大菜";
unsafe
{
 fixed (char* p = s1)
 {
  Console.WriteLine("字符串地址= 0x{0:x}", (int)p);

 }
 fixed (char* p = s2)
 {
  Console.WriteLine("字符串地址= 0x{0:x}", (int)p);

 }
} 

输出结果:

字符串地址= 0x80002d84
字符串地址= 0x80002d84

可见实例的值只分配了一次,但是有一点需要说明,字符串仅用于编译期能确定值的字符串,也就是常量字符串。如果我的程序修改为:

args = new string[] { "dfasfdsa"};
string s1 = "hello 大菜"+ args[0];
string s2 = "hello 大菜"+args[0];
unsafe
{
 fixed (char* p = s1)
 {
  Console.WriteLine("字符串地址= 0x{0:x}", (int)p);

 }
 fixed (char* p = s2)
 {
  Console.WriteLine("字符串地址= 0x{0:x}", (int)p);

 }
}

运行结果:

字符串地址= 0x2e3c
字符串地址= 0x2e7c

平时coding避免不了字符串的连接,如果一个频繁拼接字符串的场景下使用‘+',对程序整体性能和GC影响还是挺大的,为此c#推出了 StringBuilder类型来优化字符串的拼接。相对于String类型的不变性来说,StringBuilder更像是可变的字符串类型。它的底层数据结构是一个Char的数组。另外还有容量(默认为16),最大容量(默认为int.MaxValue)等属性。StringBuilder的优势在于字符总数未超过‘容量'的时候,底层数组不会重新分配,这和String每次都重新分配形成最大的对比。如果字符总数超过‘容量',StringBuilder会自动倍增容量属性,用一个新的数组来容纳原来的值,原来数组将会被GC回收。可见如果StringBuilder频繁的动态扩容也会损害性能,但是影响可能会比String小的多。 合理的设置StringBuilder初始容量对程序有很大帮助。测试如下:

int count = 100000;
Stopwatch sw = new Stopwatch();
sw.Start();
string s = "";
for (int i = 0; i < count; i++)
 {
  s += i.ToString();
 }
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);

运行结果:

14221

查看GC的情况

Gc执行的是如此频繁。 性能是可想而知的。接着看一下StringBuilder

int count = 100000;
Stopwatch sw = new Stopwatch();
sw.Start();   
StringBuilder sb = new StringBuilder();//听说程序员都这样命名StringBuilder
for (int i = 0; i < count; i++)
 {
 sb.Append(i.ToString());
}
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);

运行结果:

12

GC情况:


几乎没有GC(可能还未达到触发GC的临界点),如果我合理初始化了StringBuilder 容量,生产环境中结果差距将会更大。 呵呵 ^ ~ ^

其他

关于字符串留用和字符串池

一个程序集加载的时候,CLR默认会留用该程序集元数据中描述的所有文本常量字符串。由于可能会出现额外的哈希表查找造成的性能下降的现象,所以现在可以禁用这个特性了。

coding中我们平常比较两个字符串是否相等,那这个过程是怎么样的呢?

这个场景是适合字符串留用的。因为不再需要经过以上的两个步骤,直接哈希表拿到value就可以对比确定了。

关于字符串拼接性能

基于以上所有知识,那是不是StringBuilder拼接字符串性能永远都高于符号‘+'呢?答案是否定的。

 static void Main(string[] args)
  {
   int count = 10000000;
   Stopwatch sw = new Stopwatch();
   sw.Start();   
   string str1 = "str1", str2 = "str2", str3 = "str3";
   for (int i = 0; i < count; i++)
   {
    string s = str1 + str2 + str3;
   }
   sw.Stop();
   Console.WriteLine($@"+用时: {sw.ElapsedMilliseconds}" );

   sw.Reset();
   sw.Start();
   for (int i = 0; i < count; i++)
   {
    StringBuilder sb = new StringBuilder();//听说程序员都这样命名StringBuilder
    sb.Append(str1).Append(str2).Append(str3);
   }
   sw.Stop();
   Console.WriteLine($@"StringBuilder.Append 用时: {sw.ElapsedMilliseconds}");

   Console.Read();
  }

运行结果:

+用时: 553
StringBuilder.Append 用时: 975

符号‘+'最终会调用String.Concat方法,当同时连接几个字符串时,并不是每连接一个都分配一次内存,而是把几个字符都作为 String.Concat方法的参数,只分配一次内存。所以在拼接的字符串个数比较少的场景下,String.Concat 性能是略高于StringBuilder.Append。string.Format 方法最终调用的是StringBuilder,这里不做展开讨论了,请自行参考其他文档。

所以万事都不是绝对的!!每个事物都有适合自己的场景,我们都需要自己去探索。(程序员太累了)

以上都是非生产环境测试结果,如果错误,请及时指正

总结

您可能感兴趣的文章:

相关文章