C#夯实基础之字符串

string作为我们在编程当中用的最多的数据类型,同时又由于它的特殊性,怎么强调它的重要性都不为过,理解string的一些类型和存储机制,有助于我们写出正确且高效的代码.

一.string类型

1.string的类型

string类型直接继承Object类型,Object类型是引用类型,因而string类型是引用类型无疑.

我们借助VS的类视图可以看到这一点:

这意味着:

(a).string类型不会在线程的堆栈中存储任何字符串,而是存储在堆上

(b).未初始时,它被设置为null

PS:在内部,string是用字符串char的集合来维护的

2.string声明的IL描述

   在IL中,构造新实例的IL指令是newobj,是不是string也是这样?

我们使用如下代码:

 1 class Program
 2 {
 3      static void Main(string[] args)
 4     {
 5          string str = "Hello World!";
 6          string str2 = "Hello" + " My" + " World!";
 7           Person person = new Person();
 8      }
 9  }
10
11 class Person
12 {
13     string Name;
14 }

我们查看IL代码如下:

可以看出

(a).对比1和3,构造Person对象使用了newobj指令,但是在构造字符串的时候,使用了专门的ldstr(load string)指令

(b).更进一步,编译器将这些字面值字符串放到模块的元数据中,在运行时加载和引用它们

(c).看2,对于使用+符合将各literal连接起来的写法,编译器在编译的过程中会直接连接他们.

二.string的操作带来的疑问

OK,通过第1部分,我们知道了,string是引用类型,它存储在堆中.

我们知道对于引用类型,赋值操作=会传递的是引用,不是值,但构造不同的引用类型时通常它们的引用也不同.如下面这种:

 1 class Program
 2 {
 3      static void Main(string[] args)
 4      {
 5            //Person实害?例
 6            Person person1 = new Person("A");
 7            Person person2 = new Person("A");
 8            Console.WriteLine(object.ReferenceEquals(person1, person2));
 9
10            //string
11            string str1 = "Hello World!";
12            string str2 = "Hello World!";
13            string str3 = "Hello " + "World!";
14            Console.WriteLine(object.ReferenceEquals(str1, str2));
15            Console.WriteLine(object.ReferenceEquals(str1, str3));
16
17            Console.Read();
18        }
19 }
20
21class Person
22 {
23    public Person(string strName)
24    {
25
26    }
27 }

我们先给出运行结果:

我们知道object.ReferenceEquals是比较两个对象的引用是否一样,对于第1种Person的情况,我们可以理解,因为他们都是构造了不同的对象,引用的存储地址也是不同的.但对于第2种,第3种,string就像成为了值类型一样,返回了True,那么问题来了:

A.在声明的时候,string存储的是什么?

B.什么原因使得两个string的引用地址是一样的?

这就引出了我们要讨论的核心问题:字符串驻留.

三.字符串驻留

1.string存储的是引用

        string对象存储的是引用,引用对象存储在堆中,会生成一个对象,同时将这个对象的地址(引用)给堆栈去使用.也就是说两个string引用了堆中同一块对象.

    2.字符串驻留让两个string的引用地址是一样

       在CLR初始化时,会创建一个Hash表,在这个表中,Key是字符串,值是字符串在堆中的地址.当声明一个字符串的时候,会先去这个HashTable中去找是否存在这个Key,如果存在则返回对应的引用,如果不存在则纳入HashTable.如下图所示:

Step1:当执行语句string str1 = "Hello World!";时,str1拿到了Add1;

 Step2:当执行语句string str2= "Hello World!";时,CLR会去HashTable中去找,找到,返回Add1给str2;

Step3:现在用object.ReferenceEquals比较str1和str2的引用,因为都是Add1,因而返回True.

我们现在通过内存分析工具ANTS Memory Profile来证明,字符串驻留机制是确实存在的.

代码如下:

1 static void Main(string[] args)
2 {
3      Console.ReadLine();//第台?一?次?快ì照?位?置?
4      string str1 = "Hello World!";
5      string str2 = "Hello World!";
6      Console.ReadLine();//第台?二t次?快ì照?位?置?
7 }

加载两次快照,对比差异:

我们可以看到,在这里有一个string的实例进去了,而且整个过程当中,也只有这一个string实例进去了,我们可以进一步看下进去的内容是什么.

我们在这里发现了”Hello World!”字符串,并且只有一个.这也就从内存分析的角度证明了字符串驻留的存在.

        3.驻留字符串的HashTable是不受GC管理,但表达式中存在variable时,则不驻留在HashTable

            我们实验如下:

 1 static void Main(string[] args)
 2 {
 3      Console.ReadLine();//第1次快照位置
 4      Test();
 5      GC.Collect();
 6      Console.ReadLine();//第3次快照位置
 7 }
 8
 9 static void Test()
10 {
11      string str1 = "Hello World!";
12      string str2 = "Hello World!" + str1;
13 Console.ReadLine();//第2次快照位置
14 }

第2次快照,我们可以看到:

进去了3个对象,分别是:byteIndex,”Hello World!”,”Hello World!Hello World!”

第3次快照是在调用了GC.Collect()后再进行的快照,以快照2为对比线,我们查看第3次快照.

我们看到,有一个对象被GC回收掉了,具体是什么被回收了?我们再看:

现在只剩下byteIndex,”Hello World!”两个对象,什么被回收了呢?显然是:”Hello World!Hello World!”

这也就证明了我们所说的:驻留字符串的HashTable是不受GC管理,但表达式中存在variable时,则不驻留在HashTable.

进一步:除非卸载AppDomain或进程终止,否则HashTable引用的string对象不能被释放.

4.字符串的驻留是基于整个进程的

我们添加两个不同的AppDomain,在各自的应用程序域中执行BuildString()方法,同时由于应用程序域之间本是不能访问彼此对象的,我们使用"封送(Marshaling)"机制,封送又分为按值分送(主要采用序列化的方式)和按引用封送(如采用.Net Remoting).这里,要实现按引用封送,Test类继承MarshalByRefObject类.

测试代码

class Program
{
     static void Main(string[] args)
     {
           Console.ReadLine();
            AppDomain domina1 = AppDomain.CreateDomain("First");
            Test t1 = (Test)domina1.CreateInstanceAndUnwrap(typeof(Test).Assembly.FullName, typeof(Test).FullName);
            t1.BuildString();

            AppDomain domina2 = AppDomain.CreateDomain("Second");
            Test t2 = (Test)domina1.CreateInstanceAndUnwrap(typeof(Test).Assembly.FullName, typeof(Test).FullName);
            t2.BuildString();

            Console.ReadLine();

        }
    }

public class Test : MarshalByRefObject
{
     public void BuildString()
     {
          var str1 = "Hello";
          var str2 = "Hello";
          var str3 = "World";
          var str4 = "World";
       }
}

我们拿到两张快照,在第1张跟第2张快照对比后我们发现:

我们再具体查看内容(“World”字符串就不截图了):

通过以上的分析,我们确信,字符串的驻留是基于整个进程的.

5.我们可以通过string.Intern方法来将字符串强制加入HashTable,也可以通过string.IsInterned来判断字符串是否在HashTable中存在。

四.字符串池

在编译时,编译器会处理所有的literal字符串,并嵌入托管模块的元数据中,但如果每次都写入元数据,假设这个字符串在程序中多次出现,那就需要多次写入元数据,这会使生成的文件无限地增大.

C#编译器,只在元数据中将literal字符串写入一次,将多个实例合并成一个实例,所有引用该字符串的代码都被修改成引用元数据中的同一个字符串,这能显著地减少生成文件的大小.这种特性,我们称之为字符串池.

五.string的不可变性

string是不可变的,这意味着:

a.字符串一经创建便不能更改,不能变长、变短或修改其中的任何字符;

b.每次对于字符串的变更操作,如果是带变量操作,都会在堆上生成新的字符串,并返回新的引用,会造成频繁的GC回收,从而造成性能问题,如果不带变量操作则会采用字符串驻留;

c.操作和访问字符串不会发生线程同步问题,线程安全;

d.String类是sealed(密封)的,这是为了保护string的不可变性。

问题来了,如何实现string的不可变性呢?

string在内部是用char数组实现的,在char数据中,我们不可以改变数组的引用,但是我们可以直接修改char数组的值,为了实现string的不可变性,string在实现各种方法时,不会触动char数组中的元素。

参见博客7.

六.StringBuilder:为解决string的性能而生

通过前面的内容我们可以知道,string容易产生性能问题,StringBuilder可以解决这个问题。

它的内部使用char[]来进行操作,默认为16,如果超过容量,则在堆中产生一个倍增容易的新char[]数组,复制字符,并开始使用新数组,前一个数组则被GC回收。如果不超过当前容量,是不是会产生一个新的char[]数组的。

使用ToString()方法也会在堆中产生一个新的对象。

七.总结

1.string是引用类型

2.string使用了字符串池来减少元数据文件的大小

3.string使用了字符串驻留来提升效率,驻留的字符串采用HashTable来存储,它不受GC管辖,HashTable是基于进程共享的.

4.string是不可变的,由此带来的性能问题,可以通过StringBuilder来解决.

参考文档

博客1:http://www.cnblogs.com/justForMe/archive/2010/09/09/1822203.html#3163869

博客2:http://www.cnblogs.com/lucybloguniquecom/p/5301627.html

博客3:http://www.tuicool.com/articles/Unq6z2

博客4:http://blog.sina.com.cn/s/blog_7b60d05f0101s25l.html

博客5:http://www.cnblogs.com/artech/archive/2010/11/25/internedstring.html

博客6:http://www.cnblogs.com/artech/archive/2010/10/18/CLR_Memory_Mgt_01.html

博客7:https://www.zhihu.com/question/31345592/answer/114126087

《CLR via C#(第4版)》

时间: 2024-10-11 06:49:04

C#夯实基础之字符串的相关文章

SQL 基础之字符串和操作符使用(三)

SQL 基础之字符串使用 : 字符串可以是 SELECT 列表中的一个字符.数字.日期 但日期和字符只能在单引号中出现.每当返回一行时,字符串被输出一次 1.接上文使用过程中如果想把两个列串在一起,并加入一些自定义的显示,如下下图: select first_name || '  is a ' || job_id as "Emp Details" from employees; 2.显示first_name 的人的工资 是多少钱怎么显示: select first_name || '

c#编程基础之字符串函数

c#常用的字符串函数 例一: 获取字符串的大小写函数 ToLower():得到字符串的小写形式 ToUpper():得到字符串的大写形式 注意: 字符串时不可变的,所以这些函数都不会直接改变字符串的内容,而是把修改后的字符串通过函数返回值的形式返回. 源码如下: using System; using System.Collections.Generic; using System.Text; namespace 字符串函数学习 { class Program { static void Mai

夯实基础——快速排序

逻辑结构:递归栈 物理结构:数组 快速排序分析: 最优时间复杂度:O(nlog2n)在乱序情况下 最坏时间复杂度:O(n^2) 在顺序情况下 平均时间复杂度:O(nlog2n) 空间复杂度:O(n) 稳定性:不稳定 快速排序主要有两个函数: 1 一次划归 int partition(int a[],int low,int high); 2 递归快速排序 void QuickSort(int a[],int low,int high); 3 非递归快速排序 void NonQuickSort(in

夯实基础——堆排序

堆结构:任意的一个父节点大于其子节点. 逻辑结构:二叉树 物理结构:数组 如果从角标0开始 父节点左孩子节点:2*i+1 父节点右孩子节点:2*i+2 最后一个非叶节点:(n-1)/2 如果从角标1开始 父节点左孩子节点:2*i 父节点右孩子节点:2*i+1 最后一个非叶节点:n/2 堆排序分析: 最优时间复杂度:O(nlog2n) 最坏时间复杂度:O(nlog2n) 平均时间复杂度:O(nlog2n) 空间复杂度:O(1) 稳定性:不稳定 堆排序主要分三个函数: 1 调整成堆结构 void H

【夯实基础】Spring在ssh中的作用

尊重版权:http://blog.csdn.net/qjlsharp/archive/2009/03/21/4013255.aspx 写的真不错. 在SSH框假中spring充当了管理容器的角色.我们都知道Hibernate用来做持久层,因为它将JDBC做了一个良好的封装,程序员在与数据库进行交互时可以不用书写大量的SQL语句.Struts是用来做应用层的,他它负责调用业务逻辑serivce层.所以SSH框架的流程大致是:Jsp页面----Struts------Service(业务逻辑处理类)

夯实基础——插入排序

物理结构:数组 插入排序分析: 最优时间复杂度:O(n) 顺序的情况下 最坏时间复杂度:O(n^2) 平均时间复杂度:O(n^2) 最坏空间复杂度:O(n) 辅助空间O(1) 稳定性:稳定 快速排序法函数: int InsertSort(int a[],int length); //插入排序 int InsertSort(int a[],int length) { int tmp,i,j; for(i=1;i<length;i++) { tmp=a[i]; j=i; while(j>0&

c#编程基础之字符串基础

1.C#中单个的字符串用单引号包含就是char类型,('a'),单引号中放且只能放一个字符 2.单个字符也可以表示为字符串,还可以有长度为0的字符串. 3.使用s.Length属性来获得字符串中的字符个数. 4.string 可以看做是char类型的只读数组.char c=s[1];例子:遍历输出string中的每个元素. 5.c#中字符串有一个重要的特性:不可变性.字符串一旦声明,就不再可以改变. 所以只能通过索引来读取指定位置的char,不能对指定位置的char进行修改. 6.如果要对cha

redis基础的字符串类型

redis —— 第二篇 基础的字符串类型 我们都知道redis是采用C语言开发,那么在C语言中表示string都是采用char[]数组的,然后你可能会想,那还不简单,当我执行如下命令,肯定是直 接塞给char[]数组的. 如果你真的这么想的话,会有几个问题就要过来砍你了,先我们来找一个redis手册,http://doc.redisfans.com/ 第一:如果你每次都执行Append函数,那是不是redis的char[]每次都需要再次扩容,这样是不是每次都是耗时操作呢? 第二:如果你每次执行

【夯实基础】java关键字synchronized 详解

尊重版权:http://www.cnblogs.com/GnagWang/archive/2011/02/27/1966606.html Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 一.当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行.另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块. 二.然而,当一个线程访问object的一个sy