C#中字符串优化String.Intern、IsInterned详解

string是一种很特殊的数据类型,它既是基元类型又是引用类型,在编译以及运行时,.Net都对它做了一些优化工作,正式这些优化工作有时会迷惑编程人员,使string看起来难以琢磨。本文将给大家详细介绍关于C#字符串优化String.Intern、IsInterned的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧。

首先看一段程序:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

using System;

  

class Program

{

 static void Main(string[] args)

 {

 string a = "hello world";

 string b = a;

 a = "hello";

 Console.WriteLine("{0}, {1}", a, b);

 Console.WriteLine(a == b);

 Console.WriteLine(object.ReferenceEquals(a, b));

 }

}

这个没有什么特殊的地方,相信大家都知道运行结果:

?

1

2

3

hello, hello world

False

False

第二个WriteLine使用==比较两个字符串,返回False是因为他们不一致。而最后一个WriteLine返回False,因为a、b的引用不一致。

接下来,我们在代码的最后添加代码:

?

1

2

Console.WriteLine((a + " world") == b);

Console.WriteLine(object.ReferenceEquals((a + " world"), b));

这个的输出,相信也不会出乎大家的意料。前者返回True,因为==两边的内容相等;后者为False,因为+运算符执行完毕后,会创建一个新的string实例,这个实例与b的引用不一致。

上面这些就是对象的通常工作方式,两个独立的对象可以拥有同样的内容,但他们却是不同的个体。

接下来,我们就来说一下string不寻常的地方

看一下下面这段代码:

?

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

using System;

 

class Program

{

 static void Main(string[] args)

 {

 string hello = "hello";

 string helloWorld = "hello world";

 string helloWorld2 = hello + " world";

  

 Console.WriteLine("{0}, {1}: {2}, {3}", helloWorld, helloWorld2,

 helloWorld == helloWorld2,

 object.ReferenceEquals(helloWorld, helloWorld2));

 }

}

运行一下,结果为:

?

1

hello world, hello world: True, False

再一次,没什么意外,==返回true因为他们内容相同,ReferenceEquals返回False因为他们是不同的引用。
现在在后面添加这样的代码:

?

1

2

3

4

helloWorld2 = "hello world";

Console.WriteLine("{0}, {1}: {2}, {3}", helloWorld, helloWorld2,

 helloWorld == helloWorld2,

 object.ReferenceEquals(helloWorld, helloWorld2));

运行,结果为:

?

1

hello world, hello world: True, True

等一下,这里的hellowWorld与helloWorld2引用一致?这个结果,相信很多人都有些接受不了。这里的helloWorld2与上面的hello + " world"应该是一样的,但为什么ReferenceEquals返回的是True?

String.Intern

有经验的程序员们,应该知道,一个大型项目中,字符串的数量是巨大的。有些时候会出现几百、几千、甚至几万的重复字符串存在。这些字符串的内容相同,但却会重复分配内存,占用巨额的存储空间,这个肯定是要优化处理的。而C#在处理这个问题的时候,采用的就是普遍的做法,建立内部的池,池中每一个不同的字符串存在唯一一个个体在池中(这个方案在各种大型项目中都能见得到)。而C#毕竟是一种语言,而不是一个面向某个具体领域的技术,所以,它不能将这种内部的池技术,做成全部自动化的。因为我们不知道,将来C#会被使用到何种规模的项目中。如果完全自动化维护这个内部池,可能会在大型项目中,造成内存的巨大浪费,毕竟不是所有的字符串都有必要加到这个常驻的池中的。于是,C#提供了String.Intern和String.IsInterned接口,交给程序员自己维护内部的池。

String.Intern的工作方式很好理解,你将一个字符串作为参数使用这个接口,如果这个字符串已经存在池中,就返回这个存在的引用;如果不存在就将它加入到池中,并返回引用,例如:

?

1

Console.WriteLine(object.ReferenceEquals(String.Intern(helloWorld), String.Intern(helloWorld2)));

这段代码将返回True,尽管helloWorld与helloWorld2的引用不同,但他们的内容相同。

这里我们花几分钟,测试一下String.Intern,因为在某些情况下,它产生的结果,有点违反直觉。这里是一个例子:

?

1

2

3

4

5

string a = new string(new char[] {'a', 'b', 'c'});

object o = String.Copy(a);

Console.WriteLine(object.ReferenceEquals(o, a));

String.Intern(o.ToString());

Console.WriteLine(object.ReferenceEquals(o, String.Intern(a)));

第一个WriteLine返回False很好理解,因为String.Copy创建了一个a的新的实例,所以,o与a的引用不用。

但为什么第二个WriteLine返回的是True?思考一下吧,下面再看一个例子:

?

1

2

3

object o2 = String.Copy(a);

String.Intern(o2.ToString());

Console.WriteLine(object.ReferenceEquals(o2, String.Intern(a)));

这个看起来,与上面的做了同样的事,但为什么WriteLine返回的是False?

首先,需要说明一下ToString的工作方式,它总是返回它自身的引用。o是一个指向“abc”的变量,调用ToString返回的就是这个引用。所以,对于上面的内容,可以这样解释:

  • 开始,变量a指向字符串对象“abc”(#1),变量o指向另一个字符串对象(#2),也包含“abc”。
  • 调用String.Intern(o.ToString())将对象#2的引用添加到内部池中。
  • 现在#2对象已经存在池中了,任何时候,使用“abc”调用String.Intern都将返回#2的引用(o指向了这个对象)。
  • 所以,当你使用ReferenceEquals比较o和String.Intern(a)时,返回True。因为String.Intern(a)返回的是#2的引用。
  • 现在我们创建一个新的变量o2,使用String.Copy(a)创建一个新的对象#3,它也包含“abc”。
  • 调用String.Intern(o2.ToString())没有向内部池中添加任何内容,因为“abc”已经存在(#2)。
  • 所以,此时调用Intern返回的是对象#2的引用。注意,这里并没有使用类似o2 = String.Intern(o2.ToString())这样的代码。
  • 这就是为什么最后一行WriteLine打印的False的原因,因为我们在尝试比较#3与#2的引用。如果如7中所说,添加o2 = String.Intern(o2.ToString())这样的代码,WriteLine返回的就是True。

String.IsInterned

IsInterned,正如它的名字,判断一个字符串是不是已经在内部池中。如果传入的字符串已经在池中,则返回这个字符串对象的引用,如果不再池中,返回null。

下面是一个IsInterned例子:

?

1

2

3

4

5

6

string s = new string(new char[] {'x', 'y', 'z'});

Console.WriteLine(String.IsInterned(s) ?? "not interned");

String.Intern(s);

Console.WriteLine(String.IsInterned(s) ?? "not interned");

Console.WriteLine(object.ReferenceEquals(

String.IsInterned(new string(new char[] { 'x', 'y', 'z' })), s));

第一个WriteLine打印的是“not interned”,因为“xyz”还没有存在于内部池中;第二个WriteLine打印了“xyz”因为现在内部池中有了“xyz”;第三个WriteLine打印True,因为对象引用的就是内部池中的“xyz”。

常量字符串自动被加入内部池

改变最后一行代码为:

?

1

Console.WriteLine(object.ReferenceEquals("xyz", s));

你会发现,奇怪的事情发生了,这些代码不再输出“not interned”了,并且最后的两个WriteLine输出的是False!发生了什么?

原因就是这个最后添加的那行代码中的常量“xyz”,CLR会将程序中使用的字符常量自动添加到内部池中。所以,当最后一行被添加之后,“xyz”在程序“运行之前”(避免严谨,这里用引号)就已经存在于内部池中。所以,当调用String.IsInterned的时候,返回的不再是null,而是指向“xyz”的引用。这也解释了,为什么后面的ReferenceEquals返回False,因为s从来没有被加到内部池中,其指向也不是内部池的"xyz"。

编译器比你想象的要聪明

改变最后一行代码为:

?

1

Console.WriteLine(object.ReferenceEquals("x" + "y" + "z", s));

运行一下,你会发现运行结果和直接使用“xyz”一样。但这里使用了+运算符啊?编译器不应该知道”x“+"y"+"z"最终的结果吧?

实际上,如果你将”x“+"y"+"z"替换为String.Format("{0}{1}{2}",'x','y','z'),结果确实就不一样了。某种原因,CLR会将使用+运算符链接的字符串视为常量,而String.Format却需要在运行时才能知道结果。为什么?看一下下面的代码:

?

1

2

3

4

5

6

7

using System;

  

class Program {

 public static void Main() {

 Console.WriteLine("x" + "y" + "z");

 }

}

这段代码编译之后,使用Ildasm.exe查看,会看到:


Screenshot - ILDasm intern-xyz Main method.png

看到了吧,编译器足够聪明,将”x“+"y"+"z"替换为”xyz“。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。

原文链接:http://www.jianshu.com/p/af6eb8d3d4bf

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C# ,`string.Intern` 方法用于将字符串实例添加到字符串池(string pool)字符串池是一个全局的字符串字典,用于存储和重用字符串对象,以提高性能和节省内存。 当你使用 `string.Intern` 方法将一个字符串实例添加到字符串时,系统会检查字符串是否已经存在相同内容的字符串。如果存在,则返回字符串的引用;如果不存在,则将该字符串实例添加到字符串,并返回这个实例的引用。 以下是 `string.Intern` 方法的基本语法: ```csharp string internedString = string.Intern(originalString); ``` 其,`originalString` 是要添加到字符串字符串实例。`internedString` 是返回的字符串引用,它可能是原始字符串实例,也可能是字符串已存在的引用。 使用 `string.Intern` 方法可以帮助减少内存占用和提高字符串比较的性能。当你需要频繁地比较字符串时,使用字符串池可以避免创建大量的重复字符串对象。 下面是一个示例,展示了如何使用 `string.Intern` 方法: ```csharp string a = "Hello"; string b = "Hello"; Console.WriteLine(object.ReferenceEquals(a, b)); // 输出:False string internedA = string.Intern(a); string internedB = string.Intern(b); Console.WriteLine(object.ReferenceEquals(internedA, internedB)); // 输出:True ``` 在上述示例,首先创建了两个相同内容的字符串 `a` 和 `b`。使用 `object.ReferenceEquals` 方法可以检查两个字符串是否引用同一个对象。由于这是两个不同的字符串实例,所以返回 `False`。 然后,通过调用 `string.Intern` 方法将 `a` 和 `b` 添加到字符串,并获取字符串的引用。再次使用 `object.ReferenceEquals` 方法比较两个字符串,返回 `True`,证明它们引用的是字符串同一个字符串对象。 需要注意的是,虽然 `string.Intern` 方法可以减少字符串对象的创建,但在不必要的情况下过度使用它可能会导致内存浪费。只有在需要频繁比较字符串或者需要显式控制字符串对象的生命周期时,才建议使用 `string.Intern` 方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值