2012-01-18 17 views
13

miałem myśl, zanim przy porównywaniu dwóch ciągów z ich zmiennych:Czy porównywanie ciągów == działa tylko dlatego, że ciągi są niezmienne?

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

Czy to wyłącznie dlatego, że środowisko wykonawcze NET rozpoznaje wartość numeryczna łańcucha znaków jest taka sama, a ponieważ łańcuchy są niezmienne czyni odniesienie str2 równą z str1?

Więc kiedy robimy str1 == str2 jesteśmy faktycznie porównujące referencje i nie wartości? Początkowo sądziłem, że jest to produkt syntaktycznego cukru, ale czy byłem niepoprawny?

Jakieś nieścisłości z tym, co napisałem?

Odpowiedz

7

Jeśli przyjrzymy się uszkodzonemu kodowi, zobaczymy, że str2 jest złożony przy użyciu String.Concat i że w rzeczywistości nie jest to samo odniesienie co str1. Zobaczymy także, że porównanie odbywa się za pomocą Equals. Innymi słowy, asemat przechodzi, ponieważ łańcuchy zawierają te same znaki.

Ten kod

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

jest jitted do (proszę przewinąć w bok, aby zobaczyć komentarze)

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

W rzeczywistości najpierw sprawdza, czy jest to ten sam numer referencyjny, a jeśli nie porównuje zawartości.

+3

@EricJ. Ale jeśli mają ten sam adres pamięci, wynika to z tego, że ** must ** mają tę samą zawartość (jest to jednak * ta sama instancja). – Yuck

+0

@Yuck - Tylko jeśli interning jest częścią specyfikacji, a nie tylko szczegółem implementacji. Ponadto ciągi w oddzielnych domenach aplikacji mogą być równe i mieć różne adresy. – psr

+0

@psr Właśnie, dlatego sprawdzanie warunkowe. Jeśli odniesienie jest takie samo, to gotowe - to wszystko. W przeciwnym razie musisz porównać zawartość każdej zmiennej, aby ustalić logiczną równość. – Yuck

14

Odpowiedź jest w C# Spec §7.10.7 operatorów równości

ciąg porównać wartości ciągów zamiast ciąg referencje. Gdy dwie oddzielne instancje łańcuchów zawierają dokładnie taką samą sekwencję znaków, wartości łańcuchów są równe, ale odwołania do różnych wartości są różne. Jak opisano w §7.10.6, typ odniesienia operatorów równości może być używany do porównywania wartości łańcuchowych zamiast wartości łańcuchów .

+2

Nie dotyczy, ponieważ porównuje ten sam ciąg znaków - internowany jako stała czasowa kompilacji. – TomTom

+0

@TomTom dobry punkt. – DaveShaw

+1

@ TomTom Obowiązuje, ponieważ koncepcyjnie to robi, chociaż nadal będzie trafiał w skrót odniesienia do równości, zanim dojdzie do porównania wartości. –

2

Czy to wyłącznie dlatego, że środowisko wykonawcze NET rozpoznaje wartość numeryczna łańcucha znaków jest taka sama i dlatego ciągi są niezmienne czyni odniesienie słowo2 równą str1?

Nie. FIrst, ponieważ str1 i str2 są identyczne - są tym samym ciągiem, ponieważ kompilator może je zoptymalizować. strFoo + strFoo jest stałą czasu kompilacji będącą prostą dla str1. Ponieważ łańcuchy są INTERNED w klasach, używają tego samego ciągu znaków.

Po drugie, ciąg OVERRIDES t = = metoda. Odczytaj kod źródłowy ze źródeł referencyjnych dostępnych w Internecie przez jakiś czas.

+4

Jeśli miałbyś uruchomić powyższy fragment kodu i sprawdzić 'object.ReferenceEquals (str1, str2);', powinieneś otrzymać 'false'. Odwracanie uwagi dotyczy interningów, gdy nie ma zastosowania do bieżącego scenariusza. Twoja druga część jest oczywiście całkowicie ważna. –

+3

'string' * przeciąża * operator' == '. Nie można * nadpisywać * operatora w C#, ponieważ zawsze są 'statyczne'. – dan04

10

nr

== działa, ponieważ ciąg klasy przeciążenia operatora == za równoważne metody Wynik.

Z Reflektor

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

Równość odniesienia operatora == może być zniesione; aw przypadku System.String jest przesłonięte, aby użyć zachowania równości. Aby uzyskać prawdziwą referencyjną równość, można użyć metody Object.ReferenceEquals(), której nie można przesłonić.

0

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

Dla typów wartości predefiniowanych, operator równości (==) zwraca wartość true, jeśli wartości jego operandy są równe, false w przeciwnym wypadku. Dla typów odniesienia innych niż łańcuch, == zwraca wartość true, jeśli jej dwa argumenty odnoszą się do tego samego obiektu.Dla typu łańcucha, == porównuje wartości ciągów.

1

W porządku, w którym kod trafia go ...

== jest przesłonięte. Oznacza to, że zamiast "abc" == "ab" + "c" wywoływanie domyślnego == dla typów odniesienia (które porównuje odniesienia i nie wartości) wywołuje string.Equals(a, b).

Teraz ten wykonuje następujące operacje:

  1. Jeśli te dwie rzeczy są rzeczywiście takie same odniesienia, return true.
  2. Jeśli jedna z nich ma wartość NULL, zwracana jest wartość false (wartość true byłaby zwrócona powyżej, gdyby oba miały wartość NULL).
  3. jeśli dwie mają inną długość, return false;
  4. Wykonaj zoptymalizowany cykl za pomocą jednego ciągu, porównując go char-for-char z resztą (faktycznie int-for-int, jak to widać w dwóch blokach ints w pamięci, co jest jedną z wymaganych optymalizacji). Jeśli dojdzie do końca bez niezgodności, to zwróci true, w przeciwnym razie zwróci false.

Innymi słowy, zaczyna się coś takiego:

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

z wyjątkiem tego kroku 4 jest wersja wskaźnika opartego o rozwiniętej pętli, które powinny zatem być idealnie szybciej. Nie pokażę tego, ponieważ chcę mówić o ogólnej logice.

Istnieją znaczące skróty. Pierwsza jest w kroku 1. Ponieważ równość jest odruchowa (tożsamość pociąga za sobą równość, a == a), wówczas możemy zwrócić wartość true w nanosekundach nawet dla ciągu o wielkości kilku MB, jeśli porównać go z samym sobą.

Krok 2 nie jest skrótem, ponieważ jest to warunek, który musi zostać przetestowany, ale pamiętaj, że ponieważ powróciliśmy już do wersji (string)null == (string)null, nie potrzebujemy kolejnego oddziału. Tak więc kolejność połączeń jest nastawiona na szybki wynik.

Krok 3 pozwala na dwie rzeczy. Oba skróty ciągi o różnej długości (zawsze fałszywe) i oznacza, że ​​nie można przypadkowo strzelać poza koniec jednego z ciągów porównywanych w kroku 4.

Należy zauważyć, że tak nie jest w przypadku innych porównań łańcuchów , ponieważ np WEISSBIER i weißbier mają różną długość, ale to samo słowo z różną wielkością liter, więc porównywanie bez rozróżniania wielkości liter nie może użyć kroku 3. Wszystkie porównania równości mogą wykonywać etapy 1 i 2, ponieważ używane reguły zawsze się trzymają, więc powinieneś używać ich we własnym, tylko niektórzy mogą zrobić krok 3.

Dlatego też, podczas gdy mylisz się sugerując, że są to odniesienia, a nie wartości, które są porównywane, prawdą jest, że odniesienia są najpierw porównywane jako bardzo znaczące skróty. Zauważ też, że internowane ciągi (łańcuchy umieszczone w puli internowanej przez kompilację lub wywołane przez string.Intern) będą często wyzwalać to skrótowe. Tak będzie w przypadku kodu w twoim przykładzie, ponieważ kompilator użyje tego samego odniesienia w każdym przypadku.

Jeśli wiesz, że struna została internowana, możesz na tym polegać (po prostu wykonaj test równości referencyjnej), ale nawet jeśli nie wiesz na pewno, możesz odnieść z tego korzyść (test równości skrótu co najmniej trochę czasu).

Jeśli masz kilka ciągów, w których często chcesz testować niektóre z nich, ale nie chcesz przedłużyć ich życia w pamięci tak, jak robi to interning, możesz użyć XmlNameTable lub LockFreeAtomizer (wkrótce zostanie zmieniona nazwa ThreadSafeAtomizer, a dokument przeniesiony do http://hackcraft.github.com/Ariadne/documentation/html/T_Ariadne_ThreadSafeAtomizer_1.htm - powinien zostać nazwany dla funkcji, a nie tylko szczegółów implementacji).

To pierwsze jest używane wewnętrznie przez XmlTextReader, a przez to do reszty System.Xml i może być używane również przez inny kod. Ten ostatni napisałem, ponieważ chciałem podobny pomysł, który byłby bezpieczny dla jednoczesnych połączeń, dla różnych typów i gdzie mógłbym zastąpić porównanie równości.

W obu przypadkach, jeśli umieścisz w nim 50 różnych ciągów, które są wszystkie "abc", otrzymasz pojedyncze odwołanie "abc", pozwalając innym zebrać śmieci. Jeśli wiesz, że tak się stało, możesz polegać wyłącznie na ReferenceEquals, a jeśli nie masz pewności, nadal będziesz korzystać z skrótu, gdy tak się stanie.

Powiązane problemy