2012-01-18 13 views
13

ho avuto un pensiero prima di quando si confrontano due stringhe con le loro variabili:Il confronto delle stringhe == funziona solo perché le stringhe sono immutabili?

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

È questo semplicemente perché il runtime .NET riconosce il valore della stringa è lo stesso e perché le stringhe sono immutabili fa il riferimento di str2 pari a quella di str1?

Così, quando facciamo str1 == str2 siamo realtà confronto riferimenti e non i valori? Inizialmente pensavo che questo fosse il prodotto dello zucchero sintattico, ma non ero corretto?

Eventuali inesattezze con ciò che ho scritto?

risposta

7

Se osserviamo il codice jitted, vedremo che str2 viene assemblato utilizzando String.Concat e che in realtà non è lo stesso riferimento a str1. Vedremo anche che il confronto è fatto usando Equals. In altre parole, l'asserita passa quando le stringhe contengono gli stessi caratteri.

Questo codice

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

è jitted a (si prega di scorrere lateralmente per vedere commenti)

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

In realtà, String.Equals prima controlla se è lo stesso riferimento e se non confronta il contenuto.

+3

@EricJ. Ma se hanno lo stesso indirizzo di memoria ne consegue che il ** deve ** avere lo stesso contenuto (è la stessa * istanza * dopo tutto). – Yuck

+0

@Yuck - Solo se interning fa parte della specifica e non solo un dettaglio di implementazione. Inoltre, le stringhe in App Domain separate potrebbero essere uguali e avere indirizzi diversi. – psr

+0

@psr Giusto, ecco perché il controllo condizionale. Se il riferimento è lo stesso, allora hai finito - è tutto. Altrimenti dovresti confrontare il contenuto di ciascuna variabile per determinare l'uguaglianza logica. – Yuck

14

La risposta si trova nel C# Spec §7.10.7 operatori di uguaglianza

la stringa di confrontare i valori di stringa piuttosto che stringa referenze. Quando due istanze di stringa separate contengono esattamente la stessa sequenza di caratteri , i valori delle stringhe sono uguali, ma i riferimenti sono diversi. Come descritto in §7.10.6, gli operatori di uguaglianza del tipo di riferimento possono essere usati per confrontare i riferimenti di stringa invece dei valori di stringa .

+2

Non applicabile poiché confronta lo stesso trict stringa - internato come costante di tempo di compilazione. – TomTom

+0

@TomTom buon punto. – DaveShaw

+1

@TomTom Applicabile perché concettualmente è ciò che fa, anche se continuerà a battere il collegamento di uguaglianza di riferimento prima che arrivi al confronto del valore. –

2

È questo semplicemente perché il runtime .NET riconosce il valore della stringa è lo stesso e perché stringhe sono immutabili fa il riferimento di str2 pari a quella di str1?

No. In primo luogo, è perché str1 e str2 sono identici - sono la stessa stringa perché il compilatore può ottimizzarlo. strFoo + strFoo è una costante di tempo di compilazione itendicale a str1. Poiché le stringhe sono INTERNATE nelle classi usano la stessa stringa.

In secondo luogo, stringa OVERRIDES nel metodo ==. CHeck il codice sorgente dalle fonti di riferimento disponibili su Internet per un po 'di tempo.

+4

Se si dovesse eseguire lo snippet di codice sopra e controllare 'object.ReferenceEquals (str1, str2);', si dovrebbe ottenere 'false'. È una distrazione menzionare l'interning quando non è applicabile allo scenario attuale. La tua seconda parte è, ovviamente, interamente valida. –

+3

'string' * overload * l'operatore' == '. Non è possibile * sovrascrivere * un operatore in C# perché sono sempre 'statici'. – dan04

10

No.

== funziona perché la classe String sovraccarica l'operatore == per essere equivalente al metodo Equals.

Da Riflettore:

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

L'uguaglianza riferimento operatore == può essere sostituita; e nel caso di System.String viene sovrascritto il comportamento di uguaglianza di valore. Per la vera uguaglianza di riferimento è possibile utilizzare il metodo Object.ReferenceEquals(), che non può essere sovrascritto.

0

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

Per i tipi di valori predefiniti, l'operatore di uguaglianza (==) restituisce true se i valori dei suoi operandi sono uguali, false altrimenti. Per i tipi di riferimento diversi da string, == restituisce true se i suoi due operandi si riferiscono allo stesso oggetto.Per il tipo di stringa, == confronta i valori delle stringhe.

1

Nell'ordine in cui il codice lo colpisce ...

== viene ignorata. Ciò significa che anziché "abc" == "ab" + "c" chiamando il valore predefinito == per i tipi di riferimento (che confronta riferimenti e non valori) chiama in string.Equals(a, b).

Ora, questo fa la seguente:

  1. Se i due sono in effetti lo stesso riferimento, tornare vero.
  2. Se entrambi sono nulli, restituiscono false (avremmo già restituito true se erano entrambi nulli).
  3. se i due sono di lunghezza diversa, restituiscono false;
  4. Esegui un ciclo ottimizzato attraverso una stringa, confrontandolo char-for-char con il resto (in realtà int-for-int visto come due blocchi di ints in memoria, che è una delle ottimizzazioni in questione). Se raggiunge la fine senza una mancata corrispondenza, restituisce true, altrimenti restituisce false.

In altre parole, si inizia con qualcosa di simile:

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

parte il fatto che il punto 4 è una versione puntatore-based con un loop srotolato che dovrebbe quindi essere idealmente più veloce. Non lo mostrerò perché voglio parlare della logica generale.

Esistono scorciatoie significative. Il primo è al punto 1. Poiché l'uguaglianza è riflessiva (l'identità implica l'uguaglianza, a == a), allora possiamo restituire true in nanosecondi anche per una stringa di diverse dimensioni di MB, se confrontata con se stessa.

Il passaggio 2 non è una scorciatoia, perché è una condizione che deve essere testata, ma si noti che poiché avremo già restituito true per (string)null == (string)null non abbiamo bisogno di un altro ramo. Quindi l'ordine di chiamata è orientato verso un risultato rapido.

Il passaggio 3 consente due cose. È sia scorciatoie su stringhe di lunghezza diversa (sempre false) e significa che non è possibile accidentalmente sparare oltre la fine di una delle stringhe confrontate nel passaggio 4.

Si noti che questo non è il caso per altri confronti di stringhe , poiché ad es WEISSBIER e weißbier sono lunghezze diverse ma la stessa parola in diverse lettere maiuscole, quindi il confronto senza distinzione tra maiuscole e minuscole non può utilizzare il passaggio 3. Tutti i confronti di uguaglianza possono eseguire i passaggi 1 e 2 in base alle regole utilizzate sempre, quindi è necessario utilizzarli solo alcuni possono fare il passaggio 3.

Quindi, mentre si sbagliano nel suggerire che si tratta di riferimenti piuttosto che di valori confrontati, è vero che i riferimenti vengono prima confrontati come una scorciatoia molto significativa. Si noti inoltre che le stringhe internate (stringhe inserite nel pool interno mediante la compilazione o da string.Intern chiamate) attiveranno quindi questa scorciatoia spesso. Questo sarebbe il caso nel codice dell'esempio, in quanto il compilatore avrà utilizzato lo stesso riferimento in ciascun caso.

Se sai che una stringa è stata internata, puoi fare affidamento su questo (fai solo un test di uguaglianza di riferimento), ma anche se non sai per certo che puoi trarne beneficio (test di uguaglianza di riferimento sarà almeno una scorciatoia qualche volta).

Se si dispone di un gruppo di stringhe in cui si desidera testare spesso alcune di esse l'una contro l'altra, ma non si desidera estendere la loro durata in memoria tanto quanto internare, è possibile utilizzare uno XmlNameTable o LockFreeAtomizer (presto rinominato ThreadSafeAtomizer e il documento spostato in http://hackcraft.github.com/Ariadne/documentation/html/T_Ariadne_ThreadSafeAtomizer_1.htm - dovrebbe essere stato nominato per la funzione anziché i dettagli di implementazione in primo luogo).

Il primo è utilizzato internamente da XmlTextReader e quindi da molto del resto di System.Xml e può essere utilizzato anche da altro codice. Quest'ultimo ho scritto perché volevo un'idea simile, che fosse sicura per le chiamate simultanee, per tipi diversi e dove potevo ignorare il confronto di uguaglianza.

In entrambi i casi, se si mettono 50 stringhe diverse che sono tutte "abc" in esso, si otterrà un unico riferimento "abc" che consentirà agli altri di essere triturati. Se sai che questo è successo, puoi contare solo su ReferenceEquals e, se non sei sicuro, trarrai comunque vantaggio dalla scorciatoia quando è il caso.

Problemi correlati