[Forward].Net Discovery 系列之一--string从入门到精通(上)

2024-01-24 05:08

本文主要是介绍[Forward].Net Discovery 系列之一--string从入门到精通(上),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

 

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的内部秘密,大家可以通过它测试一下自己对string的了解程度,敬请期待!

这篇关于[Forward].Net Discovery 系列之一--string从入门到精通(上)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/638613

相关文章

C#使用Spire.Doc for .NET实现HTML转Word的高效方案

《C#使用Spire.Docfor.NET实现HTML转Word的高效方案》在Web开发中,HTML内容的生成与处理是高频需求,然而,当用户需要将HTML页面或动态生成的HTML字符串转换为Wor... 目录引言一、html转Word的典型场景与挑战二、用 Spire.Doc 实现 HTML 转 Word1

从入门到精通详解Python虚拟环境完全指南

《从入门到精通详解Python虚拟环境完全指南》Python虚拟环境是一个独立的Python运行环境,它允许你为不同的项目创建隔离的Python环境,下面小编就来和大家详细介绍一下吧... 目录什么是python虚拟环境一、使用venv创建和管理虚拟环境1.1 创建虚拟环境1.2 激活虚拟环境1.3 验证虚

C++ STL-string类底层实现过程

《C++STL-string类底层实现过程》本文实现了一个简易的string类,涵盖动态数组存储、深拷贝机制、迭代器支持、容量调整、字符串修改、运算符重载等功能,模拟标准string核心特性,重点强... 目录实现框架一、默认成员函数1.默认构造函数2.构造函数3.拷贝构造函数(重点)4.赋值运算符重载函数

redis数据结构之String详解

《redis数据结构之String详解》Redis以String为基础类型,因C字符串效率低、非二进制安全等问题,采用SDS动态字符串实现高效存储,通过RedisObject封装,支持多种编码方式(如... 目录一、为什么Redis选String作为基础类型?二、SDS底层数据结构三、RedisObject

Java List 使用举例(从入门到精通)

《JavaList使用举例(从入门到精通)》本文系统讲解JavaList,涵盖基础概念、核心特性、常用实现(如ArrayList、LinkedList)及性能对比,介绍创建、操作、遍历方法,结合实... 目录一、List 基础概念1.1 什么是 List?1.2 List 的核心特性1.3 List 家族成

Go语言使用net/http构建一个RESTful API的示例代码

《Go语言使用net/http构建一个RESTfulAPI的示例代码》Go的标准库net/http提供了构建Web服务所需的强大功能,虽然众多第三方框架(如Gin、Echo)已经封装了很多功能,但... 目录引言一、什么是 RESTful API?二、实战目标:用户信息管理 API三、代码实现1. 用户数据

在ASP.NET项目中如何使用C#生成二维码

《在ASP.NET项目中如何使用C#生成二维码》二维码(QRCode)已广泛应用于网址分享,支付链接等场景,本文将以ASP.NET为示例,演示如何实现输入文本/URL,生成二维码,在线显示与下载的完整... 目录创建前端页面(Index.cshtml)后端二维码生成逻辑(Index.cshtml.cs)总结

c++日志库log4cplus快速入门小结

《c++日志库log4cplus快速入门小结》文章浏览阅读1.1w次,点赞9次,收藏44次。本文介绍Log4cplus,一种适用于C++的线程安全日志记录API,提供灵活的日志管理和配置控制。文章涵盖... 目录简介日志等级配置文件使用关于初始化使用示例总结参考资料简介log4j 用于Java,log4c

史上最全MybatisPlus从入门到精通

《史上最全MybatisPlus从入门到精通》MyBatis-Plus是MyBatis增强工具,简化开发并提升效率,支持自动映射表名/字段与实体类,提供条件构造器、多种查询方式(等值/范围/模糊/分页... 目录1.简介2.基础篇2.1.通用mapper接口操作2.2.通用service接口操作3.进阶篇3

Python自定义异常的全面指南(入门到实践)

《Python自定义异常的全面指南(入门到实践)》想象你正在开发一个银行系统,用户转账时余额不足,如果直接抛出ValueError,调用方很难区分是金额格式错误还是余额不足,这正是Python自定义异... 目录引言:为什么需要自定义异常一、异常基础:先搞懂python的异常体系1.1 异常是什么?1.2