https://www.jianshu.com/p/af6eb8d3d4bf

首先看一段程序:

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));
}
}

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

hello, hello world
False
False

第二个WriteLine使用==比较两个字符串,返回False是因为他们不一致。而最后一个WriteLine返回False,因为a、b的引用不一致。
接下来,我们在代码的最后添加代码:

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

这个的输出,相信也不会出乎大家的意料。前者返回True,因为==两边的内容相等;后者为False,因为+运算符执行完毕后,会创建一个新的string实例,这个实例与b的引用不一致。
上面这些就是对象的通常工作方式,两个独立的对象可以拥有同样的内容,但他们却是不同的个体。

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

看一下下面这段代码:

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));
}
}

运行一下,结果为:

hello world, hello world: True, False

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

helloWorld2 = "hello world";
Console.WriteLine("{0}, {1}: {2}, {3}", helloWorld, helloWorld2,
helloWorld == helloWorld2,
object.ReferenceEquals(helloWorld, helloWorld2));

运行,结果为:

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

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

这段代码将返回True,尽管helloWorld与helloWorld2的引用不同,但他们的内容相同。
这里我们花几分钟,测试一下String.Intern,因为在某些情况下,它产生的结果,有点违反直觉。这里是一个例子:

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?思考一下吧,下面再看一个例子:

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

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

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

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

String.IsInterned


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

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”。

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

改变最后一行代码为:

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

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

编译器比你想象的要聪明

改变最后一行代码为:

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却需要在运行时才能知道结果。为什么?看一下下面的代码:

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“。

作者:小匠头
链接:https://www.jianshu.com/p/af6eb8d3d4bf
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

C#的字符串优化-String.Intern、IsInterned的更多相关文章

  1. 对于JVM中方法区,永久代,元空间以及字符串常量池的迁移和string.intern方法

    在Java虚拟机(以下简称JVM)中,类包含其对应的元数据,比如类的层级信息,方法数据和方法信息(如字节码,栈和变量大小),运行时常量池,已确定的符号引用和虚方法表. 在过去(当自定义类加载器使用不普 ...

  2. 字符串常量池和String.intern()方法在jdk1.6、1.7、1.8中的变化

    字符串常量池也是运行时常量池 jdk1.6中,它是在方法区中,属于“永久代” jdk1.7中,它被移除方法区,放在java堆中 jdk1.8中,取消了“永久代”,将常量池放在元空间,与堆独立了 pub ...

  3. Java性能优化之String字符串优化

    字符串是软件开发中最重要的对象之一.通常,字符串对象在内存中是占据了最大的空间块,因此如何高效地处理字符串,必将是提高整体性能的关键所在. 1.字符串对象及其特点 Java中八大基本数据类型没有Str ...

  4. jdk1.8下字符串常量的判断,String.intern()分析

    字符串常量池在jdk升级过程中发生了一些变化 在JDK1.6中,它在方法区中,属于“永久代”. 在JDK1.7中,它被移除方法区,放在java堆中. 在JDK1.8中,取消了“永久代”,将常量池放在元 ...

  5. Knowledge Point 20180309 字符串常量池与String,intern()

    引言 什么都先不说,先看下面这个引入的例子: public static void test4(){ String str1 = new String("SEU") + new S ...

  6. 结合字符串常量池/String.intern()/String Table来谈一下你对java中String的理解

    1.字符串常量池 每创建一个字符串常量,JVM会首先检查字符串常量池,如果字符串已经在常量池中存在,那么就返回常量池中的实例引用.如果字符串不在池中,就会实例化一个字符串放到字符串池中.常量池提高了J ...

  7. String放入运行时常量池的时机与String.intern()方法解惑

    运行时常量池概述 Java运行时常量池中主要存放两大类常量:字面量和符号引用.字面量比较接近于Java语言层面的常量概念,如文本字符串.声明为final的常量值等. 而符号引用则属于编译原理方面的概念 ...

  8. string.intern

    在翻<深入理解Java虚拟机>的书时,又看到了2-7的 String.intern()返回引用的测试. 总结一句话: jdk1.7之前,调用intern()方法会判断常量池是否有该字符串, ...

  9. Java String.intern()_学习笔记

    参考:https://www.jianshu.com/p/0d1c003d2ff5 String.intern() String.intern()是native方法,底层调用c++中的StringTa ...

随机推荐

  1. Ubuntu 安装谷歌拼音输入法

    Ubuntu14.04内置的拼音输入法有一个bug,使得Live USB全拼无法使用. 安装Google拼音输入法步骤如下: 1. 打开Ubuntu软件中心 (Ubuntu Software Cent ...

  2. Freemarker-2.3.22 Demo - No04_条件判断

    package No04_条件判断; import java.io.File; import java.io.FileOutputStream; import java.io.OutputStream ...

  3. 简单的Ajax实例

    由于刚刚接触Ajax,所以在网上搜了一下如何实现简单的Ajax,在此写下来,也方便自己以后学习 什么是Ajax? 通过在后台与服务器进行少量的数据交换,Ajax可以使网页实现异步更新,这意味着可以在不 ...

  4. #include <algorithm>中sort的一般用法

    1.sort函数的时间复杂度为n*log2(n),执行效率较高. 2.sort函数的形式为sort(first,end,method)//其中第三个参数可选. 3.若为两个参数,则sort的排序默认是 ...

  5. JAVA-JSP指令元素之page指令

    相关资料:<21天学通Java Web开发> 结果总结:1.page设定JSP页面全局属性,作用于整个JSP页面,包括静态包含的文件2.<%@ page 属性1="属性值1 ...

  6. VBA代码分行

    如果是语句可以直接在要换行的位加一个空格一个下划: Dim MyPath As String, MyName As String, _ tmpPath As String 如果是字符串可以加以加一个空 ...

  7. centos6.4或者6.5使用yum的elrepo源升级内核

    本文转自:http://www.511yj.com/centos-yum-kernel.html 今天想在centos6.5安装docker,在网上查了说centos6.5需要64位的,内核需要升级到 ...

  8. 【Unity笔记】碰撞器(Collision)与触发器(Trigger)的区别

    当碰撞器Collision组件身上的属性IsTrigger勾选为True时,该碰撞器就成了触发器. 区别:1.能够产生的回调和函数不同.碰撞器:MonoBehaviour.OnCollisionEnt ...

  9. 4种用于构建嵌入式linux系统的工具_转

    转自:4种用于构建嵌入式linux系统的工具 Linux 被部署到比 Linus Torvalds 在他的宿舍里开发时所预期的更广泛的设备.令人震惊的支持了各种芯片,使得Linux 可以应用于大大小小 ...

  10. Lua中的loadfile、dofile、require详解

    1.loadfile——只编译,不运行 loadfile故名思议,它只会加载文件,编译代码,不会运行文件里的代码.比如,我们有一个hellofile.lua文件: 复制代码代码如下: print(“h ...