字符串比较==之所以有效,是因为字符串是不可变的

本文关键字:字符串 不可变 是因为 有效 比较 之所以 | 更新日期: 2023-09-27 18:20:54

我之前在比较两个字符串及其变量时有一个想法:

string str1 = "foofoo";
string strFoo = "foo";
string str2 = strFoo + strFoo;
// Even thought str1 and str2 reference 2 different
//objects the following assertion is true.
Debug.Assert(str1 == str2);

这纯粹是因为.NET运行时认识到字符串的值是相同的,并且因为字符串是不可变的,所以str2的引用等于str1的引用吗?

所以,当我们执行str1 == str2时,我们实际上是在比较引用,而不是值?我本来以为这是句法糖的产物,但我错了吗?

我写的有什么不准确的地方吗?

字符串比较==之所以有效,是因为字符串是不可变的

答案在C#规范§7.10.7 中

字符串相等运算符比较字符串值而不是字符串参考文献。当两个单独的字符串实例包含完全相同的字符串时字符序列,字符串的值相等,但参考文献不同。如§7.10.6所述,参考类型相等运算符可用于比较字符串引用,而不是字符串值。

否。

==之所以有效,是因为String类重载了==运算符,使其等效于Equals方法。

来自反射器:

[TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
public static bool operator ==(string a, string b)
{
    return Equals(a, b);
}

如果我们看一下移植的代码,我们会发现str2是使用String.Concat组装的,事实上它与str1不是同一个引用。我们还将看到使用Equals进行比较。换句话说,断言在字符串包含相同字符时传递。

此代码

static void Main(string[] args)
{
    string str1 = "foofoo";
    string strFoo = "foo";
    string str2 = strFoo + strFoo;
    Console.WriteLine(str1 == str2);
    Debugger.Break();
}

被转为(请横向滚动查看评论)

C:'dev'sandbox'cs-console'Program.cs @ 22:
00340070 55              push    ebp
00340071 8bec            mov     ebp,esp
00340073 56              push    esi
00340074 8b3530206003    mov     esi,dword ptr ds:[3602030h] ("foofoo")  <-- Note address of "foofoo"
C:'dev'sandbox'cs-console'Program.cs @ 23:
0034007a 8b0d34206003    mov     ecx,dword ptr ds:[3602034h] ("foo")  <-- Note different address for "foo"
C:'dev'sandbox'cs-console'Program.cs @ 24:
00340080 8bd1            mov     edx,ecx
00340082 e81977fe6c      call    mscorlib_ni+0x2b77a0 (6d3277a0)     (System.String.Concat(System.String, System.String), mdToken: 0600035f)  <-- Call String.Concat to assemble str2
00340087 8bd0            mov     edx,eax
00340089 8bce            mov     ecx,esi
0034008b e870ebfd6c      call    mscorlib_ni+0x2aec00 (6d31ec00)     (System.String.Equals(System.String, System.String), mdToken: 060002d2)  <-- Compare using String.Equals
00340090 0fb6f0          movzx   esi,al
00340093 e83870f86c      call    mscorlib_ni+0x2570d0 (6d2c70d0) (System.Console.get_Out(), mdToken: 060008fd)
00340098 8bc8            mov     ecx,eax
0034009a 8bd6            mov     edx,esi
0034009c 8b01            mov     eax,dword ptr [ecx]
0034009e 8b4038          mov     eax,dword ptr [eax+38h]
003400a1 ff5010          call    dword ptr [eax+10h]
C:'dev'sandbox'cs-console'Program.cs @ 28:
003400a4 e87775596d      call    mscorlib_ni+0x867620 (6d8d7620) (System.Diagnostics.Debugger.Break(), mdToken: 0600239a)
C:'dev'sandbox'cs-console'Program.cs @ 29:
>>> 003400a9 5e              pop     esi
003400aa 5d              pop     ebp
003400ab c3              ret

实际上,String.Equals首先检查它是否是相同的引用,如果不是,则比较内容。

这纯粹是因为.NET运行时识别出字符串的值相同吗字符串是不可变的使得str2的引用等于str1的引用?

没有。首先,这是因为str1和str2是相同的——它们是相同的字符串,因为编译器可以优化这一点。strFoo+strFoo是一个与str1垂直的编译时常数。由于字符串在类中是INTERNET的,所以它们使用相同的字符串。

第二,字符串OVERRIDES tthe==方法。从互联网上的参考源中检查源代码一段时间。

可以覆盖引用相等运算符==;并且在CCD_ 10的情况下,它被重写以使用值相等行为。对于真正的引用相等,可以使用不能重写的Object.ReferenceEquals()方法。

按照代码命中的顺序…

==被覆盖。这意味着"abc" == "ab" + "c"不是为引用类型(比较引用而不是值)调用默认==,而是调用string.Equals(a, b)

现在,它执行以下操作:

  1. 如果两者确实是相同的引用,则返回true
  2. 如果其中一个为null,则返回false(如果它们都为null,我们已经在上面返回true了)
  3. 如果两者长度不同,则返回false
  4. 对一个字符串进行优化循环,将其与其余字符串进行逐字符比较(实际上,int对int被视为内存中的两个int块,这是所涉及的优化之一)。如果到达末尾时没有不匹配,则返回true,否则返回false

换言之,它从以下内容开始:

public static bool ==(string x, string y)
{
  //step 1:
  if(ReferenceEquals(x, y))
    return true;
  //step 2:
  if(ReferenceEquals(x, null) || ReferenceEquals(y, null))
    return false;
  //step 3;
  int len = x.Length;
  if(len != y.Length)
    return false;
  //step 4:
  for(int i = 0; i != len; ++i)
    if(x[i] != y[i])
      return false;
  return true;
}

除了步骤4是一个基于指针的版本,带有一个展开的循环,因此理想情况下应该更快。我不会展示这一点,因为我想谈谈整体逻辑。

有明显的捷径。第一步是步骤1。由于相等是自反的(同一性意味着相等,a == a),因此,如果与本身相比,即使是大小为几MB的字符串,我们也可以在纳秒内返回true。

步骤2不是捷径,因为它是一个必须测试的条件,但请注意,因为我们已经为(string)null == (string)null返回了true,所以我们不需要另一个分支。因此,打电话的顺序是为了快速取得结果。

步骤3允许两件事。它既缩短了不同长度的字符串(总是错误的),也意味着不能意外地超过步骤4中比较的字符串之一的末尾。

请注意,其他字符串比较的情况并非如此,因为例如WEISSBIERweißbier是不同的长度,但相同的单词的大写不同,因此不区分大小写的比较不能使用步骤3。所有的等式比较都可以执行第1步和第2步,因为所使用的规则始终有效,所以您应该在自己的规则中使用它们,只有一些可以执行第3步。

因此,虽然你认为比较的是引用而不是值是错误的,但确实,首先比较引用是一条非常重要的捷径。还需要注意的是,内部字符串(通过编译或调用string.Intern放置在内部池中的字符串)因此会经常触发此捷径。示例中的代码就是这样,因为编译器在每种情况下都会使用相同的引用。

如果你知道一个字符串被扣留了,你可以依赖它(只需进行引用相等测试),但即使你不确定你是否可以从中受益(引用相等测试至少会缩短一些时间)。

如果你有一堆字符串,你想经常对其中的一些进行测试,但你不想像interning那样延长它们在内存中的寿命,那么你可以使用XmlNameTable或LockFreeAtomizer(很快将被重命名为ThreadSafeAtomizer,文档移到http://hackcraft.github.com/Ariadne/documentation/html/T_Ariadne_ThreadSafeAtomizer_1.htm-应该首先根据函数而不是实现细节进行命名)。

前者由XmlTextReader内部使用,因此由System.Xml的许多其他代码使用,也可以由其他代码使用。我之所以写后者,是因为我想要一个类似的想法,对于不同类型的并发调用来说是安全的,并且我可以覆盖相等比较。

在任何一种情况下,如果你把50个不同的字符串都是"abc",你就会得到一个"abc"引用,允许其他字符串被垃圾收集。如果你知道这种情况已经发生,你可以单独依靠ReferenceEquals,如果你不确定,在这种情况下,你仍然会从捷径中受益。

根据msdn(http://msdn.microsoft.com/en-us/library/53k8ybth.aspx):

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