Работает ли сравнение строк == только потому, что строки неизменны? - PullRequest
13 голосов
/ 18 января 2012

Раньше у меня была мысль при сравнении двух строк с их переменными:

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, мы на самом деле сравниваем ссылки и не значения ?Первоначально я думал, что это продукт синтаксического сахара, но был ли я не прав?

Есть какие-либо неточности в том, что я написал?

Ответы [ 8 ]

14 голосов
/ 18 января 2012

Ответ содержится в спецификации C # §7.10.7

Операторы равенства строк сравнивают строковые значения, а не строковые ссылки.Когда два отдельных экземпляра строки содержат одинаковую последовательность символов, значения строк равны, но ссылки различаются.Как описано в §7.10.6, операторы равенства ссылочного типа могут использоваться для сравнения строковых ссылок вместо строковых значений.

10 голосов
/ 18 января 2012

Нет.

== работает, потому что класс String перегружает оператор ==, чтобы быть эквивалентным методу Equals.

From Reflector:

[TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
public static bool operator ==(string a, string b)
{
    return Equals(a, b);
}
7 голосов
/ 18 января 2012

Если мы посмотрим на объединенный код, то увидим, что str2 собирается с использованием String.Concat и что на самом деле это не та же ссылка, что и str1. Мы также увидим, что сравнение выполняется с использованием Equals. Другими словами, assert проходит, поскольку строки содержат одинаковые символы.

Этот код

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
7 голосов
/ 18 января 2012

На самом деле, String.Equals сначала проверяет, является ли это та же ссылка, и если нет, сравнивает содержимое.

2 голосов
/ 18 января 2012

Оператор равенства ссылок == может быть переопределен; и в случае System.String он переопределяется для использования поведения равенства значений. Для истинного равенства ссылок вы можете использовать метод Object.ReferenceEquals(), который нельзя переопределить.

2 голосов
/ 18 января 2012

Это чисто потому, что среда выполнения .NET распознает, что значение строки одинаково, а поскольку строки неизменны, ссылка на str2 равна ссылке на str1?

Нет.Во-первых, это потому, что str1 и str2 одинаковы - это одна и та же строка, потому что компилятор может оптимизировать это.strFoo + strFoo - константа времени компиляции, итеративная к str1.Поскольку строки ВНУТРЕННИЕ в классах, они используют одну и ту же строку.

Во-вторых, строка ОБЗОРЫ метод ==.Проверьте исходный код из справочных источников, доступных в Интернете в течение некоторого времени.

1 голос
/ 18 января 2012

В том порядке, в котором ваш код попадает в него ...

== отменено. Это означает, что вместо "abc" == "ab" + "c" вызов по умолчанию == для ссылочных типов (который сравнивает ссылки, а не значения) он вызывает в string.Equals(a, b).

Теперь, это делает следующее:

  1. Если два действительно являются одной и той же ссылкой, вернуть true.
  2. Если любой из них равен нулю, вернуть false (мы бы уже вернули true выше, если бы они оба были нулевыми).
  3. если два значения имеют разную длину, вернуть false;
  4. Выполните оптимизированный цикл по одной строке, сравнивая ее char-for-char с остальными (на самом деле int-for-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), то мы можем вернуть true в наносекундах даже для строки размером в несколько МБ, если сравнивать с собой.

Шаг 2 не является кратчайшим, потому что это условие, которое необходимо проверить, но учтите, что, поскольку мы уже вернули true для (string)null == (string)null, нам не нужна другая ветвь. Так что порядок звонков ориентирован на быстрый результат.

Шаг 3 допускает две вещи. Это сокращает строки различной длины (всегда ложные) и означает, что нельзя случайно выстрелить за конец одной из строк, сравниваемых на шаге 4.

Обратите внимание, что это не относится к другим сравнениям строк, так как, например, WEISSBIER и weißbier - это разные длины, но одно и то же слово в разных заглавных буквах, поэтому сравнение без учета регистра не может использовать шаг 3. Все сравнения на равенство могут выполнять шаги 1 и 2, так как используемые правила всегда выполняются, поэтому их следует использовать в самостоятельно, только некоторые могут выполнить шаг 3.

Следовательно, несмотря на то, что вы ошибаетесь, полагая, что сравниваются не ссылки, а значения, верно, что ссылки сравниваются в первую очередь как очень существенное сокращение. Также обратите внимание, что интернированные строки (строки, помещенные в внутренний пул путем компиляции или с помощью string.Intern вызываемого), следовательно, будут часто вызывать этот ярлык. Это будет иметь место в коде в вашем примере, так как компилятор будет использовать одну и ту же ссылку в каждом случае.

Если вы знаете, что строка была интернирована, вы можете зависеть от этого (просто сделайте тест на равенство ссылок), но даже если вы не знаете наверняка, вы можете извлечь из этого пользу (тест на равенство ссылок позволит сократить по крайней мере некоторые времени).

Если у вас есть набор строк, в которых вы захотите часто тестировать некоторые из них друг против друга, но вы не хотите продлевать их время жизни в памяти так же, как интернирование, тогда вы можете использовать XmlNameTable или LockFreeAtomizer (вскоре будет переименован в ThreadSafeAtomizer и документ будет перемещен в http://hackcraft.github.com/Ariadne/documentation/html/T_Ariadne_ThreadSafeAtomizer_1.htm - должен был быть назван для функции, а не для деталей реализации в первую очередь).

Первый используется внутри XmlTextReader и, следовательно, большинством остальных System.Xml и может использоваться другим кодом. Последнее я написал, потому что хотел подобную идею, которая была бы безопасна для одновременных вызовов, для разных типов и где я мог переопределить сравнение на равенство.

В любом случае, если вы поместите в него 50 различных строк, которые все являются «abc», вы получите одну ссылку «abc», позволяющую другим собирать мусор. Если вы знаете, что это произошло, вы можете зависеть только от ReferenceEquals, а если вы не уверены, вы все равно выиграете от быстрого доступа, когда это так.

0 голосов
/ 18 января 2012

По данным МСДН (http://msdn.microsoft.com/en-us/library/53k8ybth.aspx):

Для предопределенных типов значений оператор равенства (==) возвращает true, если значения его операндов равны, в противном случае - false. Для ссылочных типов, отличных от string, == возвращает true, если два его операнда ссылаются на один и тот же объект. Для типа строки == сравнивает значения строк.

...