2012-01-18 5 views
13

que tenían un pensamiento antes al comparar dos cadenas con sus variables:¿La comparación de cadenas == solo funciona porque las cadenas son inmutables?

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

Es éste simplemente porque el tiempo de ejecución .NET reconoce el valor de la cadena es la misma, y ​​porque las cadenas son inmutables hace la referencia de str2 igual a la de str1?

Así que cuando hacemos str1 == str2 que son en realidad que comparan las referencias y no los valores ? Originalmente pensé que este era el producto del azúcar sintáctico, pero ¿estaba siendo incorrecto?

¿Alguna inexactitud con lo que he escrito?

Respuesta

7

Si echamos un vistazo al código compilados JIT, veremos que str2 se ensambla usando String.Concat y que, de hecho, no es la misma referencia que str1. También veremos que la comparación se realiza usando Equals. En otras palabras, la afirmación pasa ya que las cadenas contienen los mismos caracteres.

Este código

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

se compilados JIT a (por favor, desplácese hacia los lados para ver los comentarios)

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

En realidad, String.Equals primero comprueba si es la misma referencia y si no compara el contenido.

+3

@EricJ. Pero si tienen la misma dirección de memoria, se deduce que ** debe ** tener el mismo contenido (es la * misma * instancia después de todo). – Yuck

+0

@Yuck: solo si el internamiento es parte de la especificación, y no solo un detalle de implementación. Además, las cadenas en diferentes dominios de aplicaciones podrían ser iguales y tener diferentes direcciones. – psr

+0

@psr Correcto, es por eso que el cheque condicional. Si la referencia es la misma, ya terminaste, eso es todo. De lo contrario, tendría que comparar los contenidos de cada variable para determinar la igualdad lógica. – Yuck

14

La respuesta está en el C# Spec §7.10.7 operadores de igualdad

la cadena comparan los valores de cadena en lugar de cadena referencias. Cuando dos instancias de cadena separadas contienen exactamente la misma secuencia de caracteres , los valores de las cadenas son iguales, pero las referencias son diferentes. Como se describe en §7.10.6, los operadores de igualdad de tipo de referencia se pueden usar para comparar las referencias de cadena en lugar de los valores de cadena .

+2

No aplicable, ya que compara el mismo trice de cadena - interno como constante de tiempo de compilación. – TomTom

+0

@TomTom buen punto. – DaveShaw

+1

@TomTom Aplicable porque conceptualmente eso es lo que hace, aunque todavía golpeará el atajo de igualdad de referencia antes de llegar a comparar el valor. –

2

Es éste simplemente porque el tiempo de ejecución .NET reconoce el valor de la cadena es la misma, y ​​porque las cadenas son inmutables hace la referencia de str2 igual a la de str1?

No. En primer lugar, es porque str1 y str2 SON idénticos, son la misma cadena porque el compilador puede optimizarla. strFoo + strFoo es una constante de tiempo de compilación itendical a str1. Como las cadenas son INTERNAS en las clases, usan la misma cadena.

En segundo lugar, la cadena OVERRIDES tthe == method. Evite el código fuente de las fuentes de referencia disponibles en Internet durante un tiempo.

+4

Si tuviera que ejecutar el fragmento de código anterior y verificar 'object.ReferenceEquals (str1, str2);', debería obtener 'false'. Es una distracción mencionar el internamiento cuando no es aplicable al escenario actual. Su segunda parte es, por supuesto, completamente válida. –

+3

'string' * sobrecarga * el operador' == '. No puede * anular * un operador en C# porque siempre son 'estáticos '. – dan04

10

== funciona porque la clase String sobrecarga el operador == para ser equivalente al método Equals.

De Reflector:

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

La igualdad referencia operador == se puede anular; y en el caso de System.String, se anula el uso del comportamiento de igualdad de valores. Para una verdadera igualdad de referencia, puede usar el método Object.ReferenceEquals(), que no se puede anular.

0

Según la MSDN (http://msdn.microsoft.com/en-us/library/53k8ybth.aspx):

Para los tipos de valor predefinido, el operador de igualdad (==) devuelve verdadero si los valores de sus operandos son iguales, falso en caso contrario. Para tipos de referencia que no sean cadenas, == devuelve verdadero si sus dos operandos se refieren al mismo objeto.Para el tipo de cadena, == compara los valores de las cadenas.

1

En el orden en que su código pega ...

== se anula. Esto significa que en lugar de "abc" == "ab" + "c" llamando al == predeterminado para tipos de referencia (que compara referencias y no valores) llama al string.Equals(a, b).

Ahora bien, esto hace lo siguiente:

  1. Si los dos son de hecho la misma referencia, vuelva realidad.
  2. Si alguno de ellos es nulo, devuelve falso (ya habríamos devuelto cierto anteriormente si ambos fueran nulos).
  3. si las dos son de diferente longitud, devuelve falso;
  4. Realice un ciclo optimizado a través de una cadena, comparándola char-for-char con el resto (en realidad int-for-int como dos bloques de entradas en la memoria, que es una de las optimizaciones involucradas). Si llega al final sin un desajuste, entonces devuelve verdadero; de lo contrario, devuelve falso.

En otras palabras, se comienza con algo como:

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

Excepto que el paso 4 es una versión basada en puntero con un bucle de desenrollado que debería ser idealmente por lo tanto más rápido. No lo mostraré porque quiero hablar sobre la lógica general.

Hay atajos significativos. El primero está en el paso 1. Dado que la igualdad es reflexiva (la identidad implica igualdad, a == a), podemos devolver true en nanosegundos incluso para una cadena de varios MB de tamaño, si se compara con ella.

El paso 2 no es un atajo, porque es una condición que debe probarse, pero tenga en cuenta que, como ya habremos devuelto cierto para (string)null == (string)null, no necesitamos otra rama. Entonces, el orden de las llamadas está orientado a un resultado rápido.

El paso 3 permite dos cosas. Ambos atajos en cadenas de diferente longitud (siempre falso) y significa que no se puede disparar accidentalmente al final de una de las cuerdas que se comparan en el paso 4.

Tenga en cuenta que este no es el caso para otras comparaciones de cuerdas , ya que por ejemplo WEISSBIER y weißbier son diferentes longitudes pero la misma palabra en mayúsculas diferentes, por lo que la comparación insensible a mayúsculas y minúsculas no puede usar el paso 3. Todas las comparaciones de igualdad pueden hacer los pasos 1 y 2 ya que las reglas siempre se mantienen, por lo que debe usarlas solo. algunos pueden hacer el paso 3.

Por lo tanto, aunque se equivoca al sugerir que se trata de referencias en lugar de valores que se comparan, es cierto que las referencias se comparan primero como un atajo muy significativo. Tenga en cuenta también que las cadenas internas (cadenas colocadas en el grupo interno por compilación o por string.Intern llamado) activarán por lo tanto este atajo. Este sería el caso en el código de su ejemplo, ya que el compilador habrá utilizado la misma referencia en cada caso.

Si sabe que una cadena fue internada puede confiar en esto (simplemente haga una prueba de igualdad de referencia), pero incluso si no está seguro, puede beneficiarse de ella (la prueba de igualdad de referencia reducirá al mínimo) Algo de tiempo).

Si tiene un montón de cadenas en las que querrá probar algunas de ellas una contra la otra a menudo, pero no desea extender su vida útil en memoria tanto como lo hace la internación, entonces podría usar XmlNameTable o LockFreeAtomizer (que pronto se llamará ThreadSafeAtomizer y el documento se movió a http://hackcraft.github.com/Ariadne/documentation/html/T_Ariadne_ThreadSafeAtomizer_1.htm) debería haber sido nombrado para la función en lugar de los detalles de implementación en primer lugar).

La primera se usa internamente por XmlTextReader y, por lo tanto, por el resto de System.Xml y también puede ser utilizada por otros códigos. Este último lo escribí porque quería una idea similar, que fuera segura para llamadas simultáneas, para diferentes tipos, y donde pudiera anular la comparación de igualdad.

En cualquier caso, si pone 50 cadenas diferentes que son todas "abc" en él, obtendrá una sola referencia de referencia "abc" permitiendo que los demás sean basura. Si sabe que esto ha sucedido, puede depender solo del ReferenceEquals, y si no está seguro, igual se beneficiará del atajo cuando sea el caso.

Cuestiones relacionadas