惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Hackread – Cybersecurity News, Data Breaches, AI and More
C
Check Point Blog
Hacker News: Ask HN
Hacker News: Ask HN
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
WordPress大学
WordPress大学
P
Proofpoint News Feed
V
Visual Studio Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
N
Netflix TechBlog - Medium
C
CXSECURITY Database RSS Feed - CXSecurity.com
博客园 - 聂微东
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
博客园 - 叶小钗
Cisco Talos Blog
Cisco Talos Blog
S
Schneier on Security
T
Threat Research - Cisco Blogs
腾讯CDC
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Hacker News
The Hacker News
Google DeepMind News
Google DeepMind News
Microsoft Security Blog
Microsoft Security Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
GbyAI
GbyAI
N
News | PayPal Newsroom
L
LINUX DO - 最新话题
酷 壳 – CoolShell
酷 壳 – CoolShell
P
Palo Alto Networks Blog
T
Tenable Blog
S
Secure Thoughts
T
Threatpost
V2EX - 技术
V2EX - 技术
大猫的无限游戏
大猫的无限游戏
Martin Fowler
Martin Fowler
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Vercel News
Vercel News
罗磊的独立博客
P
Privacy & Cybersecurity Law Blog
Engineering at Meta
Engineering at Meta
小众软件
小众软件
Google DeepMind News
Google DeepMind News
N
News and Events Feed by Topic
Y
Y Combinator Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
C
Cybersecurity and Infrastructure Security Agency CISA
P
Proofpoint News Feed
L
Lohrmann on Cybersecurity
P
Privacy International News Feed
H
Heimdal Security Blog
量子位
B
Blog

博客园 - 阿哲

SqlServer 2005 快速设置字段/表 的描述字段 将Visio文件装换成HTML文件(在服务器上转换,客户端无需安装visio即可查看) SqlServer2005常用sql语句 SQL中FOR XML子句的各种用法 获得Windows中文件类型名称 完成类似QQ邮箱中‘HTML方式查看’功能查看Office文件 vs2008生成自定义dll,VS2008发布、生成网站时设置固定的dll文件名 开源.Net CMS系统 [转] 测试使用Windows Live Writter写Blog 试用Windows Live Mail OutlookExpress使用技巧 剩余的GMail邀请 实体类+自定义控件=? 用反射+特性列出所有的枚举变量及其描述信息,绑定到DropDownList上。 有关.NET和RS232设备通讯 .NET资源(收藏) 转:使用P/Invoke来开发用于与串行设备通讯的.NET基类(翻译) 最近有空,研究了一下.NET的通讯方面 实体和实体的集合-续2
string:值类型?引用类型?[转]
阿哲 · 2009-05-14 · via 博客园 - 阿哲

  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"。
  好了,结合第一节所讲到的字符串恒定性与第二节所讲到的驻留机制,来理解一下下面4行代码吧:
    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种情况讲解动态创建字符串时,驻留机制的表现:

  字符串常量连接
    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_000c:  stloc.1
    IL_000d:  ret
  其中第1、6行对应c#代码string a = “str_1” + “str_2”;
  第7、8对应c# string b = “str_1str_2”;
  可以看出,字符串常量连接时,程序在被编译为IL代码前,编译器已经计算出了字符串常量连接的结果,ldstr指令直接处理编译器计算后的字符串值,所以这种情况字符串驻留机制有效!
  字符串变量连接
    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"来自同一内存地址。

  三、其它
  这一节将主要给大家介绍一些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()方法返回的就是这个字段。

《另:再说String》

String主要具有以下的两个显著的特点: 

关于Process-wide字符串驻留机制的存在,我想我在《深入理解string和如何高效地使用string中的Sample已经很明显的证明的这点。不过文中并没有为此提供充分的理论的基础,现在我就来谈谈为什么String的驻留是跨AppDomain的。

要明白Process-wide字符串驻留机制的原理,必须首先了解一个托管程序是如何运行的。

当我们运行一个托管程序,我们知道CLR会为此创建一个Default AppDomain,但实际上Windows为我们作的事情远不止这么简单。之所以我们说一个Application是在一个托管的环境下执行的,是指的是CLR对他进行托管。所以在这之前,对CLR的加载是必须的。我们知道.NET Framework是建立在Windows平台之上的,如果说Windows是对计算机硬件的封装的话,.NET Framework则可以看成是对Windows的封装,通过.NET Framework API封装了对传统Win32的封装。正是因为Windows是.NET Framework的基础架构,所以.NET Framework只能是利用Windows所能理解的方式进行构建。而对于一个Windows来说,所有能被加载执行的都是一个PE文件(Portable Executable file),比如exe和dll。CLR也不能免俗,他实际上是一个COM Server的形式实现在一个叫做MSCorWks.Dll中,该Dll存在于.NET Framework对应的目录中。

当程序开始运行的时候,有一个称为SystemDomain的AppDomain被创建,SystemDomain加载一个名为MSCorEE.Dll,该Dll就是我们经常所说的“垫片”(shim)。通过定义在该垫片中的一个名为CorBindToRuntimeEx的函数加载对应版本的CLR,并返回一个非托管的ICLRRuntimeHost interface。SystemDomain可以说是整个Process的枢纽,它负责创建、初始化、卸载SharedDomainDefaultDomain

我们知道AppDomain是一个Assembly的托管容器,Assembly在一般情况下是基于某个单独的AppDomain的,不能与另一个AppDomain共享的。但是有些公用性很强的Assembly,比如我们经常使用的一些基元类型object, int,Array,ValueType等,却希望它被一个AppDomain加载之后,能够被其他的AppDomain共享,这样可以省去很多内存空间和Assembly加载带来的性能损失。这些Assembly就是被加载到SharedDomain中,我们常用的MScorLib.dll就是被以这样的方式被加载的SharedDomain中的。Default Domain就是为具体的Application创建的AppDomain,它一般以可执行文件名命名。DefaultDomain中可以通过AppDomain.CreateAppDomain创建另一个AppDomain。所以当我们运行一个托管的Application的时候,实际上创建了3个不同AppDomain:SystemDomain,ShatedDomain和DefaultDomain,而SystemDomainShatedDomain基于整个进程的,能够被DefaultDomain以及被它创建AppDomain共享的。

有了上面的基础,我想我们就不难理解String的驻留机制的。String的驻留机制实际上是在SystemDomain中进行的。当CLR被加载之后,会在SystemDomain对应的managed heap中创建一个Hash table的数据结构,我们可以称这个Hashtable为Interning table,因为它是被用来保存被驻留的string的,Interning table的Key为string本身,Value为string对象的地址。

当我们的托管程序(无论对于那个AppDomain)需要一个string的时候,CLR首先在这个Hashtable根据这个string的hash code试着在Interning table中找对应的Item。如果成功找到,则直接把对应的引用返回,否则就在SystemDomain对应的managed heap中创建该string,并加入到Interning table中,并把引用返回。所以我们说字符串的驻留是基于整个进程的,是可以跨AppDomain共享的,就是这个道理。

  • String的恒定性:String一经创建,它所对应的字符序列就无法更改(当然我们的前提是托管的环境下)。
  • String的驻留:CLRString的创建实行驻留机制,CLR只会维护具有不同字符序列的String。换言之,在程序中使用到的具有完全相同的字符序列的String均是对应着同一个string对象,是对同一个段内存的引用。值得一提的是String的这种驻留机制不仅仅是基于某个单独的AppDomain的,而是针对整个进程的。