C#-String的恒定性和驻留池
脑图概览#
字符串恒定性#
字符串一旦创建,就会在托管堆上分配一块连续的内存空间,对其任何改变都不会影响到原String
对象,而是重新创建新的String
对象。
为什么设计string具有恒定性呢#
《你必须知道的.NET》解释是:String类型是不变模式在.NET 中的典型应用。
带来的好处#
- 保证对String对象的任意操作不会改变原字符串;
- 意味着操作字符串不会出现线程同步;
- 恒定性一定意义上成就了字符串驻留;
对象恒定意味着String类型必须为密封类。
字符串驻留池#
字符串因为其恒定的特点,对字符串的操作都会创建新字符串,性能和内存会有很大消耗。鉴于string类型的频繁使用,CLR针对其采用字符串驻留机制进行优化。
总得原理是:对于相同的字符串,CLR不会为其分别分配内存,而是共享同一内存。
实现:CLR内部有一个哈希表(Hash Table) 管理创建的大部分string对象。key就是string本身,value就是string的内存地址。
字符串驻留池实现原理#
CLR维护一个类似于哈希表的内部结构,用于维护对于字符串的统一管理,但JIT编译时,CLR首先查找哈希表,如果没有找到匹配的字符串记录,则在托管堆中创建新的string实例,并为哈希表添加一个键值对记录;下一次查找相同string时,则只返回该记录的值给第二次创建的string对象。
通过这种方式,字符串驻留机制有效实现了对string的池管理,节省了大量的内存空间。
注意:哈希表中不维护动态生成的字符串。
IsInterned()和Intern()#
-
IsInterned()
:str位于驻留池时,则IsInterned(str)返回str的引用;否则返回null引用。 -
Intern()
:str位于驻留池时,则IsInterned(str)返回str的引用;否则将该str字符串添加的哈希表中,并返回引用 。
相同点:都是在哈希表中查找是否存在str参数字符串,找的到就返回已存在string对象的引用。区别在于找不到的话Intern会向哈希表添加字符串,IsInterned不会。
动态字符串维护在拘留中吗?#
例1:
void Main()
{
string strA = "abcdef";
string strC = "abc";
string strD = strC + "def";
Console.WriteLine(ReferenceEquals(strA, strD));//False
string strE = "abc"+"def";
Console.WriteLine(ReferenceEquals(strA, strE));//True
}
strA和strD的值相同,但strD是动态添加的不在驻留池中为维护,所以是两个内存地址。
strE是字符串常量相加,在驻留池中维护,所以引用同一内存地址。
结论:动态生成字符不在驻留池中维护
IsInterned返回非null一定在拘留池中吗?#
通过以下代码发现答案是否定的。
s1和s1返回的引用虽然相同但是,s1是维护在哈希表中,s2是动态字符串不维护在哈希表中
string s1 = "abc";
string s2 = "ab";
s2 += "c";
string s3 = "ab";
Console.WriteLine(string.IsInterned(s1) ?? "null");//abc
Console.WriteLine(string.IsInterned(s2) ?? "null");//abc
Console.WriteLine(ReferenceEquals(s1,s3));//False
检测你是不是理解了驻留池#
场景一:#
s1 在哈希表中可以找到
string s1 = "abc";
Console.WriteLine(string.IsInterned(s1) ?? "null");//abc
场景二:#
动态字符串不在哈希表中维护
string s1 = "ab";
s1 += "c";
Console.WriteLine(string.IsInterned(s1) ?? "null");//null
场景三:#
s1,s3 都在哈希表中维护;s2不维护在哈希表中,但是在哈希表可以找到key为‘abc’的结果,也就是指向s1的内存地址;s1和s2的内存地址是不一样的
string s1 = "abc";
string s2 = "ab";
s2 += "c";
string s3 = "ab";
Console.WriteLine(string.IsInterned(s1) ?? "null");//abc
Console.WriteLine(string.IsInterned(s2) ?? "null");//abc
Console.WriteLine(string.IsInterned(s3) ?? "null");//ab
Console.WriteLine(ReferenceEquals(s1,s2));//False
场景四:#
s3 不维护在哈希表中,返回值s1地址
string s1 = "abc";
string s2 = "ab";
string s3 = s2 + "c";
Console.WriteLine(string.IsInterned(s3) ?? "null");//abc
场景五:#
驻留池在CLR加载时创建的,所以s1在s2定义前就被添加在哈希表中。所以
s2虽然不维护在哈希表中但返回的是s1的地址。
string s2 = "ab";
s2 += "c";
Console.WriteLine(string.IsInterned(s2) ?? "null");//abc
string s1 = "abc";
场景六:#
GetStr()在s2定义后调用
void Main()
{
string s2 = "ab";
s2 += "c";
Console.WriteLine(string.IsInterned(s2) ?? "null");//null
string s1 = GetStr();
}
private static string GetStr()
{
return "abc";
}
GetStr()在s2定义前调用
void Main()
{
string s1 = GetStr();
string s2 = "ab";
s2 += "c";
Console.WriteLine(string.IsInterned(s2) ?? "null");//null
}
private static string GetStr()
{
return "abc";
}
只因为GetStr()位置不同,结果就不同,这又是如何解释呢?
场景七:#
全局静态字符串变量和局部字符串变量一样
public static string s1="abc";
void Main()
{
string s2 = "ab";
s2 += "c";
Console.WriteLine(string.IsInterned(s2) ?? "null");//abc
}
字符串驻留跨域#
字符串驻留是进程级别,可以跨应用程序域(AppDomain)而存在。因为驻留池是在CLR加载时创建的,分配在System Domain中,被进程中的所有AppDomain所共享,生命周期不受GC控制。只能在进程结束后哈希表中引用的字符串对象才会被释放。
string和System.String#
- string 是c#基元类型,System.String是框架类库(FCL)的基本类型;string和System.String有直接映射关系。
- 在IL层面两者没有不同。
string 和StringBuilder#
string 对象恒定不变,StringBuilder对象表示的字符串是可变的。后者是.NET 提供的动态创建String对象的高效方式,解决了String对象恒定性带来的影响,比如在对string对象多次修改会创建大量string对象的问题。
StringBuilder 使用场景:大量无法预知次数的字符串操作
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步