`
Kingson_Wu
  • 浏览: 123696 次
文章分类
社区版块
存档分类
最新评论

C#基础知识梳理系列七:字符串

 
阅读更多

摘要

字符串是保存文本的System.String类型对象。它跟值类型(如:Int32Int64等)有着相似的使用方法及表达目的,但它并不是值类型。由于在编程中会大量使用字符串,所以CLR为了提高性能及开发方便,对它进行了特殊处理。这一章,我们来介绍一下字符串的驻留机制、字符串池及字符串的比较等特性。

注意,本系列所有测试代码均运行于.NET4.0

第一节字符串的定义(System.String字符串被定义为System.String类型的对象,既然它是引用类型,那么一个未初始化的对象声明将保留为null,并且它的内存只能在堆上分配。它在内部维护的是字符Char的集合,所以它有一个属性Length来表示Char集合中元数的个数。来看一下String类型的定义:

String实际上是继承了System.Object类型,同时还实现了一系列接口,如IenumberableICompareable等,所以字符串提供了对字符集合、比较等相关的操作。

尽管它是引用类型,但是编译器不允许使用new根据一个文本常量来创建一个字符串对象,而是必须使用简明的声明语法来声明及初始化,对字符串的初始化值是直接被编译进元数据的。比如如下定义一个字符串变量:

stringname1="Jack";

IL:

.methodpublichidebysigspecialnamertspecialname

instancevoid.ctor()cilmanaged{

//代码大小19(0x13)

.maxstack8

IL_0000:ldarg.0

IL_0001:ldstr"Jack"

IL_0006:stfldstringConsoleApp.Example07.Code_07::name1

IL_000b:ldarg.0

IL_000c:callinstancevoid[mscorlib]System.Object::.ctor()

IL_0011:nop

IL_0012:ret}//endofmethodCode_07::.ctor

我们知道,通常对于引用类型创建对象是使用newobj指令,但上面的并没有使用该指令,只是使用ldstr指令加载了字符串Jack,从IL_0000-IL_0006可以看到是直接加载Jack串赋给变量,这是CLR的一种特殊的构造方式。

第二节字符串的不可变性字符串对象一旦创建,在整个进程的生命周期中是不可变的,无法对其进行加长、缩短、改变等操作,既然它不会变,所以也就不存在线程同步的问题,哪怕是皇天老儿创建的线程都无法对其进行改变。如下代码:

stringstr1="Jim";

stringstr2=str1;

Console.WriteLine(object.ReferenceEquals(str1,str2));//True

str1+="China";

Console.WriteLine(object.ReferenceEquals(str1,str2));//False

在第一次调用object.ReferenceEquals方法比较的str1str2,它们指向的是同一个字符串对象引用,所以结果为true,而str1+="China";的过程是重新创建了一个对象,且把新的对象引用赋给str1,此时str1str2指向的不是同一个对象引用,所以在第二次调用object.ReferenceEquals方法时返回的是false。无论是使用+=操作符还是其他的对字符串修改的方法,都会引起重新创建字符串对象,并且复制旧的字符串到新的内存区,而不是我们常说的XX字符串进行修改,如果非要说改变,那就是对对象引用的改变。对于str1+="China";操作,CLR会执行以下操作:

1)开辟新的足够大的临时存储区内存来容纳str1China

2)复制str1串到临时区的开始处;

3)复制China到临时存储区的结尾处;

4str1丢弃对旧对象的引用

5)为str1再一次分配内存区;

6)将临时存储区内的字符值复制到4)新开辟的内存区,将str1指向这个内存区的引用。

所以对字符串的连接操作会大大损伤性能。我们在下面会讲到.NET提供的一个专门对付字符串连接的类StringBuilder

第三节字符串的驻留通过前面的描述,我们已经知道字符串的内存是分配在托管堆上,且它是不可改变的,而在编程中,我们会大量使用字符串,这就会导致不停地创建字符串对象,不停地分配内存,并且很有可能不停地执行垃圾回收,如此以来会大大损伤性能,所以CLR对字符串进行了特殊的优化机制,下面我们来对这些机制及特性进行描述。

字符串驻留是CLR提供的一种提高性能的对待字符串的机制,它保证在一个进程内的某个字符串在内存中只分配一次。看以下代码:

stringstr1="abc";

stringstr2="abc";

Console.WriteLine(object.ReferenceEquals(str1,str2));//True

明明声明了两个对象str1str2,调用object.ReferenceEquals方法返回的是True,为什么它们指向的是同一个引用呢?这就说明了CLR的字符串驻留,相同的字符串在托管内存中只分配一次,再次声明相同的字符串对象时,会将后来一次的声明指向第一次声明所引用的对象。那么CLR如何保证做到的呢?原来,在CLR初始化时创建一个内部的哈希表,我们知道哈希表在处理表内数据时是非常快的,这个表相当于一个字典表(Dictionary<TKey,TValue>),键就是字符串,而值是指向托管堆中该字符串对象的引用,当在声明一个字符串时,会调用对象的Intern方法,该方法接收一个string对象,它会先在哈希表中检查该字符串是否存在?如果存在,则返回这个字符串对应的对象引用;否则,将创建该字符串的副本,并将副本添加到哈希表中,最后返回对该副本对象的引用。String类还提供了一个IsInterned方法,该方法会根据字符串在哈希表中检查是否已经存在相同的串,如果存在,则返回该字符串对象的引用,否则,返回null,但它是它不会向哈希表中添加字符串。我们对上面的代码进行改造:

voidTestIntern()

{

stringstr1="abc";

stringstr2="abc";

Console.WriteLine(object.ReferenceEquals(str1,str2));//True

str1+=str2;

Console.WriteLine(str1);

}

接下来通过内存分析器来看一下字符串驻留:

可以看到字符串abc是有驻留的。从前面的讲解中我们知道,+=操作是要重新创建对象的,但CLR对临时计算的新对象abcabc没有进行驻留呢。继续改造上面的代码:

stringstr1="abc";

stringstr2="abc";

Console.WriteLine(object.ReferenceEquals(str1,str2));//True

str1+=str2;

str1=string.Intern(str1);

Console.WriteLine(str1);

这次我们使用了string.Intern方法,再用内存分析器看一下:

这次我们看到字符串abcabc是进行了驻留,这里就验证了刚才上面对驻留机制的讨论。

有一点要注意,尽管String.Intern(string)方法的字符串参数(上面代码中的str1)被垃圾回收器回收,但是CLR已将这个str1的副本添加到哈希表中,垃圾回收器是无法对哈希表引用的字符串进行回收。

字符串驻留也有发的时候,看以下代码:

voidTest()

{

stringstr1="abc";

stringstr2=newstring(newchar[]{'a','b','c'});

Console.WriteLine(object.ReferenceEquals(str1,str2));//False

}

对于变量str2最终的字符串也是abc,可是为什么这次object.ReferenceEquals方法返回了False呢?原因是CLR会为newstring(char[])创建的字符串对象会重新分配内存,不再使用字符串驻留机制对它进行处理,于是str1str2就指向了不同的对象引用。

第四节字符串池字符串池与字符串驻留机制是分不开的,最能体现字符串池的是在在C#编译器编译的过程中,在编译过程中,同样的字符串(比如上面代码中的abc)会被程序中的很多地方使用,通过第一节我们已经知道字符串的声明是直接将字符中写入元数据中,如果编译器在每个使用的地方都将该相同的字符串写入元数据中,则会大大增加元数据的体积,且也没有必要。所以编译器会只在模块的元数据中写入该字符串一次,并将引用该相同字符串的代码都修改为指向这同一个字符串对象,如此以来,就会大大减小元数据的体积。

第五节字符串的比较对字符串的比较,通常有两层意义,一个是判断两个字符串对象是否具有相同的引用,另一个是判断两个字符串对象是不具有相同的,我们一般的编程中经常使用后者。

1Unicode编码与字符

为了解决使用相同的字符集表示不同的语言,于是一群人就捣鼓出了UnicodeUnicode对每一个字符都提供了唯一的值,它不依赖于任何平台和任何区域语言。Unicode采用2个字节的编码方式,可以表示65536个字符,就是我们常说的16Unicode编码,然而,仅中文就有85000多个字符,所以16位的Unicode不能满足语言文化的需要。当然如果采用32位的Unicode编码一定能满足,但32位编码的每个字符占4个字节,所以Unicode定制了另一个使用代理对的机制来满足各种语言文化的需要。

.NETFramework使用16位的Unicode编码,每个字符对应一个确定的Unicode码值。在第一节我们已经知道,字符串是由字符组成,所以一个字符串是由一系列的Unicode码组成。我们通常用Int32值来表示一个字符的编码值,如:a97A65。如果你感兴趣,可以使用如下代码来测试看看每个数值所代表的字符是什么,当然,无对应数值,可能无法转换:

for(inti=1;i<10000;i++)

{

chartemp=(char)i;

Debug.WriteLine(temp+""+i);

}

计算机只能识别0/1,任何一个数字都可以用0/1对其进行编码,每个字符都是由Unicode码值来表示,所以计算机就能识别出所有字符,而字符是组成字符串的元素,所以计算机进而能识别出字符串。

字符文本与区域语言文化有很大的依赖关系。

2)区域语言文化

由于全世界各地的人类文化不同,也就导致的语言文化的不同,这就是区域语言文化的不同。计算机系统使用国际化标准来处理各种语言文化的差异。.NETFramework为了方便处理各种语言文化,提供了一个System.Globalization.CultureInfo类,它提供对特殊文化信息的支持,如文化名称、相关语言、国家/区域等。如en代表英文、en-CA代表加拿大、zh-CHS代表中文简体。

一个应用程序一般即要处理国际化数据,也要处理本地化数据,CultureInfo为处理这两类数据扮演了重要角色,为了完美地支持这两类数据处理,CultureInfo类提供了两个重要的属性CurrentUICultureCurrentCultureCurrentUICulture的值决定了如何加载窗体资源及窗体元素以什么语言来显示。CurrentCulture决定了除CurrentUICulture外的其他方面,如日期格式、数字格式、货币符号、字符串大小写及比较等。CurrentUICultureCurrentCulture在应用的线程级设定,如果未设定,那么系统将从Windows中获取一个值来进行实初始化,这个值通常在控制面板的语言和区域中设置。

3)字符串的比较

我们知道,字符串是由字符组成的,而每个字符可以由一个Int32值来表示,我们当然可以比较两个Int32的值,同样也可以比较两个字符的大小,进而可以比较两个字符串的大小,事实上字符串都是表示一系列文本内容的,它们不可能用大小来衡量,只能长度对其本身的特性进行一方面的描述,我们通常所说的比较,是对其内包含的字符对应的Int32值进行比较。在对字符或串排序的时候,这个比较很有用,另外一个,我们通常是比较两个字符串是否相等。

注意:字符串的比较,是按顺序逐个比较每个字符的Int32值。

字符串的比较通常有以下几种方式:

比较符号==、实例级和静态的Equals方法、CompareTo方法、String.Compare方法、String.CompareOrdinal方法

字符串与语言文化有很大的依赖关系,所以任何一个的字符串,都会直接或间接的使用某种语言文化信息。下面我们分别介绍一下每个比较方法。

a)等于号==

如下代码:

voidTestEqualto()

{

stringstr1="abc";

stringstr2="def";

boolchk=str1==str2;

}

我们来看一下IL

.methodprivatehidebysiginstancevoidTestEqualto()cilmanaged{

//代码大小22(0x16)

.maxstack2

.localsinit([0]stringstr1,

[1]stringstr2,

[2]boolchk)

IL_0000:nop

IL_0001:ldstr"abc"

IL_0006:stloc.0

IL_0007:ldstr"def"

IL_000c:stloc.1

IL_000d:ldloc.0

IL_000e:ldloc.1

IL_000f:callbool[mscorlib]System.String::op_Equality(string,

string)

IL_0014:stloc.2

IL_0015:ret}//endofmethodCode_07::TestEqualto

它是调用了op_Equality方法,再来看看op_Equality方法的IL:

.methodpublichidebysigspecialnamestatic

boolop_Equality(stringa,

stringb)cilmanaged{

.custominstancevoidSystem.Runtime.TargetedPatchingOptOutAttribute::.ctor(string)=(01003B506572666F726D616E63652063//..;Performancec

7269746963616C20746F20696E6C696E//riticaltoinlin

65206163726F7373204E47656E20696D//eacrossNGenim

61676520626F756E6461726965730000)//ageboundaries..

//代码大小8(0x8)

.maxstack8

IL_0000:ldarg.0

IL_0001:ldarg.1

IL_0002:callboolSystem.String::Equals(string,

string)

IL_0007:ret}//endofmethodString::op_Equality

我们可以看到op_Equality方法内部调用的是Equals方法,也就是说,只要使用==来比较两个字符串,最终调用的还是Equals方法,二者是等价的。

b)Equals

Equals有两种:实例方法和类方法,它们是普通的序号比较。来看一下两者的实现。

publicboolEquals(stringvalue)

{

if(this==null)

{

thrownewNullReferenceException();

}

if(value==null)

{

returnfalse;

}

return(object.ReferenceEquals(this,value)||EqualsHelper(this,value));

}

publicstaticboolEquals(stringa,stringb)

{

return((a==b)||(((a!=null)&&(b!=null))&&EqualsHelper(a,b)));

}

可以看到,实例方法进行了一次引用的比较,判断两个对象是否指向了同一个对象地址;静态方法是判断两个对象是否相等。对于两个方法,如果前面部分不成立,则它们会继续调用EqualsHelper方法进行逐字符比较。逐字符比较是先比较第一个字符的码值,再比较第二个字符的码值,依次类推,比较过程中以第一个字符串的长度为基准。比如:

chara='a';//97

charb='b';//98

charc='c';//99

"ab"<"ac"

"abc">"ab"

"ab"<"abc"

"ab"="ab"

"ac">"ab"

但是,如果要执行区域敏感规则的比较,可能就不同了,比较中可能会为非字母数字的Unicode字符分配特殊权重,使用字词排序规则和特定区域的约定

c)CompareTo

CompareTo(string)方法是拿一个字符串对象与另一个串对象进行比较,返回一个Int32值,如果前者的码值小于后者码值,则返回-1,如果相等,则返回0,如果前者码值大于后者码值,则返回1。来看一下它的定义:

publicintCompareTo(stringstrB)

{

if(strB==null)

{

return1;

}

returnCultureInfo.CurrentCulture.CompareInfo.Compare(this,strB,CompareOptions.None);

}

它在内部使用了包含区域语言特性信息的Compare比较。CompareInfo.Compare方法接收一个CompareOptions枚举,各个枚举的用意可参考MSDN文档

d)String.Compare

CompareCompareTo的静态化版本,在内部都是对CultureInfo.CurrentCulture.CompareInfo.Compare方法进行调用。它同样返回Int32值:-101Compare的重载版本会另外的参数,如:

publicstaticintCompare(stringstrA,stringstrB,boolignoreCase)

接收一个boolignoreCase表示是否忽略大小写。

publicstaticintCompare(stringstrA,stringstrB,StringComparisoncomparisonType)

接收一个StringComparison枚举,表示要使用的区域文化信息、排序规则等。该枚举的详细可参考MSDN文档

e)String.CompareOrdinal

CompareOrdial方法执行的是忽略区域文化信息的序号比较,不执行转换和提供国际化支持所需的系统开销,该方法可用于比较文件路径、IPURL等字符串。很显然它的性能会比提供区域语言文化信息的Compare方法快很多。

最后,如果进行不区分大小写的比较,或是想对字符串中的大小写进行更改,建议使用ToUpperInvariant()ToLowerInvariant()方法,这两个方法使用了固定区域性的大小写规则,而ToUpper()方法和ToLower()方法是依赖区域语言文化信息的,所以性能会差一些。

第六节StringBuilder在第2节,我们知道字符串的不可变性,如果要对字符串进行更改,则会重新创建字符串对象,这个过程会导致性能问题,CLR提供了驻留和池来提高性能,同时.NETFramework还提供了一个StringBuilder类可以高效地对字符串进行动态管理,大提高了性能,它不像我们像常说的在内部维护一个Char[]数组那么简单。在后面,我们准备专用一个章节来讨论StringBuilder

小结

本博客文章版权归博客园和solan3000共同所有。

分享到:
评论

相关推荐

    C++基础知识梳理

    总的来说,C++的基础知识涵盖了许多关键概念,包括数据类型、指针、数组和字符串。理解和掌握这些基础是成为一名熟练的C++程序员的前提。在编写C++代码时,需要注意类型安全、内存管理和指针操作,以确保程序的正确...

    博客园c#文章精华2

    - **基础知识**:熟悉C#的基本语法和面向对象的编程思想。 - **框架理解**:深入理解.NET Framework的核心组件和服务。 - **设计模式**:掌握常见的设计模式,如工厂模式、单例模式等。 - **代码规范**:遵循一定的...

    C#官网下载的文档

    通过以上内容的梳理,我们可以看到C#官网下载的文档覆盖了从基础知识到高级概念的广泛内容,旨在帮助不同层次的学习者掌握这门语言。无论是初学者还是经验丰富的开发者,都能从中找到有价值的信息。

    C#知识点归纳,关于C#

    本篇将详细梳理C#的基础知识,涵盖语法、面向对象特性、异常处理、集合、多线程等方面。 1. **基础语法** - 关键字:如`class`、`void`、`int`、`if`、`else`等,用于定义程序结构。 - 变量:存储数据的容器,C#...

    C#详细基础教程(推荐使用)

    ### C#基础教程知识点概述 ...以上是对《C#详细基础教程》主要内容的梳理,涵盖了C#语言的基础知识、Windows编程基础知识以及常用控件和类的使用,旨在帮助初学者系统地掌握C#语言的核心概念和技术要点。

    华南师范大学《C#》期末复习资料.pdf

    2. 字符串处理的方法:包括字符串的常见操作如拼接、截取等。 3. 信息摘要算法、对称加密算法的实现:涉及到.NET框架提供的加密算法类以及如何使用它们进行安全通信。 六、注册表与进程管理 1. 主要注册表项与...

    https://download.csdn.net/download/p_xiaojia/9756173

    以上知识点涵盖了C#中的一些基本概念和技术细节,包括指针操作、字符串处理以及控制流结构等方面。这些内容对于理解和掌握C#语言的基础非常重要,尤其对于那些需要进行底层编程或优化程序性能的应用场景来说更是不可...

    数据结构C#(C#语言)

    此外,还涵盖了与数据结构相关的数学知识和C#语言的基础知识。 #### 三、常用数据结构及其应用 - **线性表**:线性表是最基本的数据结构之一,包括顺序表和链表两种形式。本章通过C#代码详细介绍了线性表的操作,...

    C#数据结构(1)

    **书籍内容**:本书共分为 8 章,涵盖了数据结构的基础概念、常用数据结构如线性表、栈、队列、字符串、数组、树型结构、图结构以及排序和查找算法等内容。每章末尾还介绍了在 .NET 框架中如何使用这些数据结构和...

    计算机c#总复习资料

    #### 一、C#基础知识概述 在C#编程语言中,理解和掌握基础概念对于进一步学习高级特性至关重要。本文档将从多个角度出发,帮助读者理解C#的核心概念,并提供实际应用场景,以便更好地进行复习。 #### 二、值类型与...

    C#学习笔记.pdf

    在C#中,可以通过多种方式将数值类型转换为字符串类型: 1. **直接调用ToString()方法**: ```csharp int number = 10000; string strNumber = number.ToString(); ``` 2. **使用System.Convert类**: ```...

    C#数据结构

    - **串和数组**: 讲解字符串处理的基本操作及数组的高级应用。 - **树型结构**: 涵盖二叉树、搜索树等多种树结构。 - **图结构**: 包括图的表示方法、遍历算法等。 - **高级主题**: - 第7、8章: 深入探讨排序和...

    c#.net面试题目

    ### C#.NET 面试题目详解 #### 1. 访问修饰符的理解与应用 ...通过以上知识点的梳理,我们可以看到 .NET 技术体系的广泛性和深度。对于开发者而言,掌握这些基础且核心的概念对于深入理解和应用 .NET 平台至关重要。

    .net c#测试

    ### .NET C#基础知识知识点梳理 #### 一、判断题解析 1. **在C#中,所有的数据类型都能转换成string类型** - 正确。C#中几乎所有基本数据类型都可以通过显式或隐式的方式转换为`string`类型。例如,使用`Convert....

    c#gaojibiancheng

    #### 二、C#基础知识点梳理 根据题目提供的内容,以下是关于C#基础知识的一些重要知识点: ##### 1. 声明变量 - 在C#中,可以通过指定数据类型和变量名来声明变量,例如 `int x;`。 - 变量可以被初始化,例如 `int...

    C#学习笔记_20100613

    1. **使用Split方法**:根据分隔符分割字符串: ```csharp string input = "apple, orange, banana"; string[] words = input.Split(','); ``` 2. **使用正则表达式**:适用于更复杂的分割场景: ```csharp ...

    C#数据结构 教程

    - **串和数组**:探讨字符串处理、数组操作的基本技术和.NET框架中的相关类。 - **树型结构**:涵盖树的基本概念、二叉树的操作及.NET框架中的树结构实现。 - **图结构**:讲解图的基本理论、图的表示方法和图的...

Global site tag (gtag.js) - Google Analytics