C# String的本质
字符串类型是基元类型,它对应着 System.String。
字符串的类型定义如下:
public sealed class String : IComparable, ICloneable, IConvertible,
IEnumerable, IComparable<string>, IEnumerable<char>, IEquatable<string>
可以通过字符串的默认值为 null 来记忆这点,对字符串的操作伴随着堆上的内存活动。
我们可以看到,字符串继承了
IEnumerable<char>
,这使得它可以使用 LINQ 查询。也许你会认为字符串完全可以被设计为结构,原因如下:
- 字符串是密封类,而结构不能被继承,所以没问题。
- 字符串继承了很多接口,而结构也可以继承接口。
- 字符串的字段和属性很少而且都是值类型。
- 字符串的比较仅仅会比较值。
- 字符串做方法参数行为类似值类型。
但是,最终微软将字符串设计为一个密封类(而且具有不变性),这基于以下原因:
- 字符串的长度可能十分巨大,而一个线程栈只有 1MB 的空间。其他值类型的长度都是确定的。
- 字符串如果被设计为值类型,则方法传入字符串将会拷贝其值而不是引用。如果字符串长度巨大,则性能会显著下降。
- 不变性使得字符串是线程安全的。
- 字符串驻留节省内存,而它只可能借助不可变性来实现。
字符串与普通的引用类型相比
字符串的行为很像值类型:- 字符串使用双等于号互相比较时,比较的是字符串的值而不是其是否指向同一个引用。这与型的比较不同,却和值类型的比较相同。
- 字符串虽然是引用类型,但如果在某方法中,将字符串传入另一方法,在另一方法内部修改,执行完之后,字符串的值并不会改变,而引用类型无论是按值传递还是引用传递,值都会发生变化。
对第一点来说,字符串的
==
操作符被重写为比较字符串的值而不是其引用。作为引用类型,
==
本来是比较引用的,但此时被重写,这也是字符串看起来像值类型的一个原因。当然,!=
操作符也会一并被重写。
[__DynamicallyInvokable]
public static bool operator ==(string a, string b)
{
return string.Equals(a, b);
}
[__DynamicallyInvokable]
publie static bool operator !=(string a, string b)
{
return !string.Equals(a, b);
}
IL中创建字符串
在 C# 中,不能使用 new 操作符建立字符串,但可以为字符串直接赋值;也支持传入一个字符集合。通过 IL 分析,我们可以知道,如果为字符串直接赋值,则创建字符串的指令是 ldstr 如果传入了一个字符集合,则指令是平常的 newobj。代码如下:
// string s = new string("1"); string s = "1"; string t = new string(new char[]{'1', '2', ' 3 ' });对应的IL代码如下:
IL_0000: nop
IL_0001: ldstr "1"
IL_0006: stloc.0
IL_0007: ldc.i4.3
IL_0008: newarr [mscorlib]System.Char
IL_000d: dup
IL_000e: ldtoken field valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=6' '<PrivateImplementationDetails>'::'0D5399508427CE79556CDA71918020C1E8D15B53'
IL_0013: call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::InitializeArray(class [mscorlib]System.Array,
valuetype [mscorlib]System.RuntimeFieldHandle)
IL_0018: newobj instance void [mscorlib]System.String::.ctor(char[])
ldstr 是 load string 的缩写,用于获得文本常量。这个操作需要在堆上分配空间,并将文本常量转换为对应的 unicode 字符数组。
而 newobj 则是常规的引用类型创建方式,其会调用 string 的构造函数。
字符串的不变性
Immutability 般翻译为不变性,也有翻译为恒等性的。而相对的,泛型的协变和逆变也有不变性(invariant),它们对应的英文不同。因此,应当在中文层面上将这两个术语加以区分。不过,现在大部分人都将“一经赋值,值就不能被更改”这个现象翻译为不变性。
字符串的不变性指的是字符串一经赋值,其值就不能被更改。当使用代码将字符串变量等于一个新的值时,堆上会出现一个新的字符串,然后,栈上的变量指向该新字符串。
没有任何办法更改原来字符串的值。如下图所示。
通过 IL 代码可以证明字符串的不变性,代码如下:
static void StringImmunity() { //idstr "1" string s = "1"; //虽然看起来s的值改变了 //但实际上,是将s指向了一个新的字符串 //证明即为可以在IL中发现idstr“2” s = "2"; s = s + "3"; Console.WriteLine(s); }通过将上面的代码编译,我们可以查到其对应的 IL 代码:
IL_0000: nop
IL_0001: ldstr "1"
IL_0006: stloc.0
IL_0007: ldstr "2"
IL_000c: stloc.0
IL_000d: ldloc.0
IL_000e: ldstr "3"
IL_0013: call string [mscorlib]System.String::Concat(string, string)
IL_0018: stloc.0
IL_0019: ldloc.0
IL_001a: call void [mscorlib]System.Console::WriteLine(string)
IL_001f: nop
IL_0020: ret
static void ProveImmutable() { string a = "1"; string b = a; //true Console.WriteLine(ReferenceEquals(a, b)); b = "2"; //false Console.WriteLine(ReferenceEquals(a, b)); }
通过成员来证明不变性
在 C# 中,字符串类型的属性和字段很少。字段除了若干常量之外还有如下内容:- 一个 readonly 的 Empty,值为空。
- 私有的 m_stringLength,无法被外界更改。
- 私有的 m_firstChar,无法被外界更改。
而属性则有:
- Chars[Int32],代表着字符串(字符集合)的成员。
- Length,代表字符串的长度。
- FirstChar,获得字符串第一个字符。
这三个属性都是只读的,代码如下:
public char this[int index] { get; } public int Length { get; } internal char FirstChar { get; }无法直接修改它们的值(假设 s 和 t 都是字符串,则下面两行代码都无法通过编译):
s.Length = 3; t[1] = "b";由于字符串所有非私有的属性和字段都是只读或常量,也没有任何办法(直接修改属性或通过微软提供的方法)修改字符数组的值,字符串的值也就不可能改变了。
对于某些方法(例如 toUpper)来说,虽然调用了它们会导致字符串的值改变,但这也是一个假象。
方法会返回全新的字符串,并将其赋给引用,而不是在原来的字符串上更改,代码如下:
string t = new string(new char[]{'a', 'b', 'c' }); //一个全新的字符串,原字符串不会变化 t.ToUpper();
注意:无论是声明了多少个字符串,变量名只是引用而已。例如上面的 t,它并非字符串本身,字符串的值取决于 t 到底指向哪里。
为什么要这么设计
字符串的不可变性允许在一个字符串上执行各种操作,而不实际地更改字符串,这意味着,多个线程操作字符串不会出现同步问题(因为值无法被改变)。另外,不可变性结合字符串驻留池可以节省内存。如果字符串是可变的,那么驻留池中多个字符串指向同一块内存,如果改变了其中一个字符串的值,其他指向这个值的字符串也会一起改变。
所有教程
- socket
- Python基础教程
- C#教程
- MySQL函数
- MySQL
- C语言入门
- C语言专题
- C语言编译器
- C语言编程实例
- GCC编译器
- 数据结构
- C语言项目案例
- C++教程
- OpenCV
- Qt教程
- Unity 3D教程
- UE4
- STL
- Redis
- Android教程
- JavaScript
- PHP
- Mybatis
- Spring Cloud
- Maven
- vi命令
- Spring Boot
- Spring MVC
- Hibernate
- Linux
- Linux命令
- Shell脚本
- Java教程
- 设计模式
- Spring
- Servlet
- Struts2
- Java Swing
- JSP教程
- CSS教程
- TensorFlow
- 区块链
- Go语言教程
- Docker
- 编程笔记
- 资源下载
- 关于我们
- 汇编语言
- 大数据
- 云计算
- VIP视频