C# 由范式编程==运算符引发对string内存分配的思考

今天在看C#编程指南时(类型参数的约束http://msdn.microsoft.com/zh-cn/library/d5x73970.aspx)看到一段描述:

在应用 where T : class 约束时,避免对类型参数使用 ==!= 运算符,因为这些运算符仅测试引用同一性而不测试值相等性。即使在用作参数的类型中重载这些运算符也是如此。下面的代码说明了这一点;即使 String 类重载 == 运算符,输出也为 false。 

并给出的代码

public static void OpTest<T>(T s, T t) where T : class
{
    System.Console.WriteLine(s == t);
}
static void Main()
{
    string s1 = "target";
    System.Text.StringBuilder sb = new System.Text.StringBuilder("target");
    string s2 = sb.ToString();
    OpTest<string>(s1, s2);
}

返回的结果为False即s1!=s2
到这里都十分容易理解,因为在C#中==运算符对于值类型,如果对象的值相等,则相等运算符 (==) 返回 true,否则返回 false。对于引用类型,如果两个对象引用同一个对象,则 == 返回 true。

所以如果我将代码改为:

static void Main()
{
  string s1 = "target";
  System.Text.StringBuilder sb
= new System.Text.StringBuilder("target");
  string s2 = sb.ToString();
  s2
= s1;
  OpTest
<string>(s1, s2); }

返回的结果为TRUE即s1==s2,因为s1和s2引用同一个对象。

接下来我又做了一个实验(重点来了):

static void Main()
{  
    string s1 = "target";  
    string s2 = "target";  
    OpTest<string>(s1, s2);
}

返回的结果依然是TRUE即s1==s2,这个结果完全出乎我的预料!思考中......

==这里其实有一个很大的陷阱!上面的理解有一处很大的破绽!因为笔者忘记了很重要的一条!

在C#参考中有以下描述(== 运算符(C# 参考)http://127.0.0.1:47873/help/1-30636/ms.help?product=VS&productVersion=100&method=f1&query=%3D%3D_CSharpKeyword%00%3D%3D&locale=zh-CN&category=DevLang%3acsharp%00TargetFrameworkMoniker%3a.NETFramework,Version%3Dv4.0

对于预定义的值类型,如果操作数的值相等,则相等运算符 ( ==) 返回 true,否则返回 false。 对于 string 以外的引用类型,如果两个操作数引用同一个对象,则 == 返回 true。 对于 string 类型, == 比较字符串的值。

也就是说string类型其实有对==运算符进行重载(实时上作者在刚开始阅读C#编程指南时明显理解错误),实时如此:

string s1 = "target";
System.Text.StringBuilder sb = new System.Text.StringBuilder("target");
string s2 = sb.ToString();

if (s1 == s2)
{
  System.Console.WriteLine(
"=="); } else {
  System.Console.WriteLine(
"!="); }

结果显示s1 == s2 返回True,而在本文开头提出的C#编程指南时(类型参数的约束)例子中讲的就是虽然string有对==运算符进行重载,但是在范式编程中会被无视,所以要慎用(好吧,笔者的基本功堪忧啊!),那么为啥

string s1 = "target";  
string s2 = "target";  
OpTest<string>(s1, s2);

会得到s1==s2的结果?

让我们换个思考方式:

1.在OpTest<string>(s1, s2);中==判断的是两个操作数是否引用的同一个对象,如果是则返回True,反之False(包含string类型)

2.在上述例子中s1==s2,则证明s1和s2引用的是同一个对象

于是笔者又尝试了

string[] str = new string[3];
str[0] = "123";
str[1] = "123";
str[2] = "123";

OpTest<string>(str[0], str[2]);
string str0 = @"D:mysher";
string str1 = "D:\mysher";
OpTest<string>(str0, str1);

等到的结果都是两个string变量引用了同一个对象
所以笔者得到结论当有多个字符串变量包含了同样的字符串实际值时,CLR让它们向同一个字符串对象。

那么既然是这样,Microsoft在C#编程中给出的例子

string s1 = "target";
System.Text.StringBuilder sb = new System.Text.StringBuilder("target");
string s2 = sb.ToString();
OpTest<string>(s1, s2);

s1和s2也应该满足条件,指向同一个对象啊,可事实却相反。

后来笔者在园子里找到了答案,感谢cyoooo7给出了十分详细的解释:

CLR默默地维护了一个叫做驻留池(Intern Pool)的表。这个表记录了所有在代码中使用字面量声明的字符串实例的引用。这说明使用字面量声明的字符串会进入驻留池,而其他方式声明的字符串并不会进入,也就不会自动享受到CLR防止字符串冗余的机制的好处了。但是在不使用字面量声明时,如CLR在为ToString()方法的返回值分配内存时,并不会到驻留池中去检查是否字符串已经存在了,所以会重新分配内存。

为了让编程者能够强制CLR检查驻留池,以避免冗余的字符串副本,String类的设计者提供了一个名为Intern的类方法,如下

string s1 = "target";
System.Text.StringBuilder sb = new System.Text.StringBuilder("target");
string s2 = String.Intern(sb.ToString());
OpTest<string>(s1, s2);

这样s1和s2又指向同一个对象了。
如果有兴趣的朋友可以去看看cyoooo7《原来是这样:C#中字符串的内存分配与驻留池》一文。

原文地址:https://www.cnblogs.com/max198727/p/3429972.html