string作为我们在编程当中用的最多的数据类型,同时又由于它的特殊性,怎么强调它的重要性都不为过,理解string的一些类型和存储机制,有助于我们写出正确且高效的代码.
一.string类型
1.string的类型
string类型直接继承Object类型,Object类型是引用类型,因而string类型是引用类型无疑.
我们借助VS的类视图可以看到这一点:
这意味着:
(a).string类型不会在线程的堆栈中存储任何字符串,而是存储在堆上
(b).未初始时,它被设置为null
PS:在内部,string是用字符串char的集合来维护的
2.string声明的IL描述
在IL中,构造新实例的IL指令是newobj,是不是string也是这样?
我们使用如下代码:
class Program
{
static void Main(string[] args)
{
string str = "Hello World!";
string str2 = "Hello" + " My" + " World!";
Person person = new Person();
}
} class Person
{
string Name;
}
我们查看IL代码如下:
可以看出
(a).对比1和3,构造Person对象使用了newobj指令,但是在构造字符串的时候,使用了专门的ldstr(load string)指令
(b).更进一步,编译器将这些字面值字符串放到模块的元数据中,在运行时加载和引用它们
(c).看2,对于使用+符合将各literal连接起来的写法,编译器在编译的过程中会直接连接他们.
二.string的操作带来的疑问
OK,通过第1部分,我们知道了,string是引用类型,它存储在堆中.
我们知道对于引用类型,赋值操作=会传递的是引用,不是值,但构造不同的引用类型时通常它们的引用也不同.如下面这种:
class Program
{
static void Main(string[] args)
{
//Person实害?例
Person person1 = new Person("A");
Person person2 = new Person("A");
Console.WriteLine(object.ReferenceEquals(person1, person2)); //string
string str1 = "Hello World!";
string str2 = "Hello World!";
string str3 = "Hello " + "World!";
Console.WriteLine(object.ReferenceEquals(str1, str2));
Console.WriteLine(object.ReferenceEquals(str1, str3)); Console.Read();
}
} class Person
{
public Person(string strName)
{ }
}
我们先给出运行结果:
我们知道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来证明,字符串驻留机制是确实存在的.
代码如下:
static void Main(string[] args)
{
Console.ReadLine();//第台?一?次?快ì照?位?置?
string str1 = "Hello World!";
string str2 = "Hello World!";
Console.ReadLine();//第台?二t次?快ì照?位?置?
}
加载两次快照,对比差异:
我们可以看到,在这里有一个string的实例进去了,而且整个过程当中,也只有这一个string实例进去了,我们可以进一步看下进去的内容是什么.
我们在这里发现了”Hello World!”字符串,并且只有一个.这也就从内存分析的角度证明了字符串驻留的存在.
3.驻留字符串的HashTable是不受GC管理,但表达式中存在variable时,则不驻留在HashTable
我们实验如下:
static void Main(string[] args)
{
Console.ReadLine();//第1次快照位置
Test();
GC.Collect();
Console.ReadLine();//第3次快照位置
} static void Test()
{
string str1 = "Hello World!";
string str2 = "Hello World!" + str1;
Console.ReadLine();//第2次快照位置
}
第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版)》
C#夯实基础系列之字符串的更多相关文章
-
夯实基础系列四:Linux 知识总结
前言 前三节内容传送门: 夯实基础系列一:Java 基础总结 夯实基础系列二:网络知识总结 夯实基础系列三:数据库知识总结 现在很多公司项目部署都使用的是 Linux 服务器,互联网公司更是如此.对于 ...
-
夯实基础系列一:Java 基础总结
前言 大学期间接触 Java 的时间也不短了,不论学习还是实习,都让我发觉基础的重要性.互联网发展太快了,各种框架各种技术更新迭代的速度非常快,可能你刚好掌握了一门技术的应用,它却已经走在淘汰的边缘了 ...
-
JavaScript夯实基础系列(四):原型
在JavaScript中有六种数据类型:number.string.boolean.null.undefined以及对象,ES6加入了一种新的数据类型symbol.其中对象称为引用类型,其他数据类 ...
-
JavaScript夯实基础系列(三):this
在JavaScript中,函数的每次调用都会拥有一个执行上下文,通过this关键字指向该上下文.函数中的代码在函数定义时不会执行,只有在函数被调用时才执行.函数调用的方式有四种:作为函数调用.作为 ...
-
JavaScript夯实基础系列(二):闭包
在JavaScript中函数是一等公民.所谓一等公民是指函数跟其他对象一样,很普通,可以进行把函数存在数组中.作为参数传递.赋值给变量等操作.当函数作为另一个函数的返回值在外部调用时,跟该函数在函 ...
-
JavaScript夯实基础系列(一):词法作用域
作用域是一组规则,规定了引擎如何通过标识符名称来查询一个变量.作用域模型有两种:词法作用域和动态作用域.词法作用域是在编写时就已经确定的:通过阅读包含变量定义的数行源码就能知道变量的作用域.Jav ...
-
mysql 开发基础系列5 字符串函数
字符串函数 1. concat (s1,s2,...sn) 连接里面的参数成一个字符串(注意上面写错了函数名称) SELECT CONCAT('ddd','CCC'); 2. insert(str ...
-
es6基础系列四--字符串的拓展
1 for...of 字符串的遍历接口 for(let i of "abc"){ console.log(i); } // a // b // c 2 includes 是否包含某 ...
-
【PHP夯实基础系列】PHP日期,文件系统等知识点
1. PHP时间 1)strtotime() //日期转成时间戳 2) date()//时间戳变成日期 <?php date_default_timezone_set("PRC&quo ...
随机推荐
-
hibernate(七)组件映射与多对一映射
一.组件映射 用注解配置组件映射: Husband为我们映射的类,wife是这个类的一部分(属性不能与husband中属性重名,不要写Entity注解,不要有主键) Husband类:(在getWif ...
-
为EasyUI 的Tab 标签添加右键菜单
在网上看了很多demo 自己实现了一个效果如下 ps jquery1.7.2 jQuery EasyUI 1.3.6easyui QQ群:15129679 <!doctype html> ...
-
HDU 2709 Sumsets(递推)
Sumsets http://acm.hdu.edu.cn/showproblem.php?pid=2709 Problem Description Farmer John commanded his ...
-
JSON对象配合jquery.tmpl.min.js插件,手动攒出一个table
jquery.tmpl.min.js 首先下载这个插件 1.绑定json那头的键 //TemplateDDMX 这个是这段JS的ID,这个必须写!!!!!! //${}为json的键的值,必须要填写正 ...
-
Android View的绘制机制流程深入详解(一)
本系列文章主要着重深入介绍Android View的绘制机制及流程,第一篇主要介绍并分析LayoutInflater的原理, 从而理解setContentView的加载原理.对于LayoutInfla ...
-
Java之sleep和wait的区别
这个问题在面试线程方面的知识时,基本上属于必问的问题.因此这里有必要做一个较为详细的总结. 区别一 首先需要明白的是这两个方法根本来自不同的类,sleep来自Thread,wait来自Object类. ...
-
zabbix杂文二
ps1:主要是一些遇到的问题,不一定对所有人都有用... PS2:安装前就一定要去看下官方文档 PS3:安装zabbix的时候会参照这上面的 http://blog.chinaunix.net/uid ...
-
为什么选择 Visual Studio Code
为什么选择 Visual Studio Code 你在 VS Code 中找到的每个功能都完成一项出色的工作,构建了一些简单的功能集,包括语法高亮.智能补全.集成 git 和编辑器内置调试工具等,将使 ...
-
aplication.properties配置
1.设置使用的properties文件 spring.profiles.active=dev 设置激活使用哪个properties一般设置两个,一个是开发环境的,一个是本地的测试环境 可设置默认使用开 ...
-
[AS3]as3中splice和slice的用法介绍说明
splice 删除数组一段连续的元素,返回被删除的元素数组 var arr:Array = ["a","b","c","d&quo ...