Porównywanie string’ów część II – ustawienia regionalne, StringComparison

W poprzednim poście podałem krótki fragment kodu, który można było zoptymalizować. Wspomniałem o parametrze StringComparison, który zwykle sprawia problemy w zrozumieniu. W dzisiejszym wpisie postaram się wyjaśnić różnicę między poszczególnymi porównaniami. Dla przypomnienia istnieją następujące wartości StringComparison:

1. Ordinal  – domyślna wartość. Znaki zamieniane są na wartości numeryczne  i wtedy porównywane. Zdecydowanie najszybsza metoda jeśli chodzi o wydajność.

2. OrdinalIgnoreCase – tak jak wyżej ale ignorowana jest wielkość liter.

3. CurrentCulture – brana jest pod uwagę dana kultura. Zatem wynik może być różny w zależności od danej konfiguracji komputera (tzn. ustawień regionalnych).

4. InvariantCulture – niebrane są pod uwagę ustawienia regionalne – zawsze wynik będzie taki sam.

5. CurrentCultureIgnoreCase, InvariantCultureIgnoreCase – analogicznie jak wyżej z tym, że wielkość liter jest ignorowana.

Ordinal jest domyślną wartością dla funkcji string.Equals. Stanowi porównanie binarne a nie leksykalne. Z tego względu żadne wartości związane z danym językiem nie są brane pod uwagę. Warto zwrócić uwagę, że dla funkcji porównującej dwa napisy (Equals) jest to dokładnie co w większości sytuacjach chcemy. Porównanie binarne w końcu zagwarantuje nam, że dwa łańcuchy znaków są dokładnie takie same. Na przykład porównanie “DOM” z “DOM1” wygląda następująco:

68 79 77(DOM) !=  68 79 77 49(DOM1),

Każdy znak zamieniany jest na liczbę (unicode) i wtedy operacja porównywania dokonywana jest właśnie na tych wartościach. Niepotrzebne nam są w większości przypadków uwarunkowania językowe. Chcemy wiedzieć po prostu czy napisy są identyczne. Z tego względu Ordinal dla funkcji Equal jest dobrą wartością domyślną.

OrdinalIgnoreCase wykonuje podobną operację z tym, że ignoruje wielkość liter (“DOM” == “dom” itp.).

Wiemy, już jakie jest domyślne zachowanie metody Equal, bez podania StringCompairson. Poniższy kod również wykonuje standardowe porównanie Ordinal:

if(str1==str2)
{ 
}

Podsumowując, dla operacji ==(Equals) Ordinal lub OrdinalIgnoreCase jest wartością, którą chcemy w większości przypadków. Porównanie binarne (Ordinal) jest najszybsze dlatego nie warto go zmieniać ponieważ wydajność może dość znacząco spaść.

Kolejne wartości to CurrentCulture oraz CurrentCultureIgnoreCase. Jak łatwo się domyślić biorą one pod uwagę wszelkie uwarunkowania językowe. Na przykład w języku niemieckim litera “ß” ma takie same znaczenie jak “ss”. Domyślnie poniższe porównanie zwróci false:

if (string.Equals("ß", "ss")) // zawsze false
{

}

Zapis binarny tych dwóch łańcuchów jest kompletnie różny a wiemy, że domyślna wartość to Ordinal. Jeśli jednak użyjemy CurrentCulture wtedy funkcja zwróci true:

if (string.Equals("ß", "ss",StringComparison.CurrentCulture)) // zawsze true
{

}

Czy jest zatem sens używania CurrentCulture dla Equals? Zwykle nie, bo jakby nie patrzeć są to jednak różne napisy. CultureCulture ma znaczenie jednak dla porównywania napisów w celu ich wyświetlenia użytkownikowi np. w formie posortowanej listy. Z tego względu, funkcja string.Compare przyjmuje domyślnie CurrentCulture:

Console.WriteLine(string.Compare("ß", "ss"));

Kod zwróci 0 ponieważ napisy według CurrentCulture są takie same – dla przypomnienia Compare zwraca liczbę całkowitą, ujemną, dodatnią lub zerową (0 – napisy takie same, <0 napis A przed B powinien pojawić się, >0 A po B).

Zatem podczas sortowania danych (gdzie Compare jest wykorzystywany) chcemy zachować własności leksykalne. Załóżmy, że chcemy posortować litery “z” oraz “ó”. Jeśli użyjemy porównania binarnego wtedy okaże się, że “z” powinno zostać wyświetlone przed "ó”:

// Compare zwróci wartość ujemną
Console.WriteLine(string.Compare("z", "ó",StringComparison.Ordinal)); 

Dla porównania == byłoby to w porządku bo litery są zdecydowanie różne od siebie. W celu posortowania jednak jest to niepoprawne. Jeśli użyjemy CurrentCulture wtedy dostaniemy prawidłową, dodatnią wartość:

Console.WriteLine(string.Compare("z", "ó",StringComparison.CurrentCulture));

CurrentCulture wykona porównanie takie jakby dokonał to człowiek – bazując na rzeczywistych zasadach językowych a nie binarnej, wewnętrznej reprezentacji.

Ostatnie dwa typy to InvariantCulture oraz InvariantCultureIgnoreCase. InvariantCulture to kultura niezależna od ustawień regionalnych zatem na każdej maszynie zawsze rezultat będzie taki sam. Jeśli porównujemy CurrentCulture to wynik (np. sortowania) będzie różny w zależności od ustawień regionalnych. InvariantCulture jest predefiniowaną kulturą, niezależną od ustawień dokonywanych w panelu sterowania OS.

Ktoś mógłby zapytać, jaka jest różnica między Ordinal a InvariantCulture? Obie w końcu nie nie mają wiedzy o danej kulturze  i zachowują się identycznie na wszystkich komputerach. InvariantCulture bierze pod uwagę część zasad językowych z kolei Ordinal to po prostu porównanie liczb. Jako przykład warto rozważyć następujący kod:

// zwróci wartość dodatnią
Console.WriteLine(string.Compare("ab","Az",StringComparison.Ordinal));
// zwróci wartość ujemną
Console.WriteLine(string.Compare("ab","Az", StringComparison.InvariantCulture));

Przyjrzyjmy się najpierw wartości ASCII liter ‘a’ oraz ‘A”:

1. ‘a’ – 97

2. ‘A’ – 65

Z tego względu według Ordinal ‘a’ oraz ‘A’ są kompletnie różne i porównanie tych znaków zwróciłoby 32 (odległość między znakami). Przez to porównanie ‘ab’ z ‘Az’ zwróci wartość dodatnią oznaczającą,  że wynik posortowany powinien wyglądać:

‘Az’, ‘ab’

Użytkownik jednak spodziewałby się następującej sekwencji:

‘ab’, ‘Az’

InvariantCulture zagwarantuje to ponieważ sortuje on według alfabetu zbliżonego do angielskiego czyli ‘aAbBcCdDeE…’. Z kolei Ordinal to Unicode czyli  wygląda to bardziej jak ‘ABCDE….abcde’. W praktyce  InvariantCulture jest rzadko wykorzystywany a nawet odradzany przez Microsoft.

Podsumowując warto przytoczyć kilka wskazówek sugerowanych przez MS:

  1. Używaj Ordinal\OrdinalIgnoreCase dla porównań Equal. Gwarantuje to wysoką wydajność i ma sens w zdecydowanej części przypadków.
  2. Używaj CurrentCulture\CurrentCultureIgnoreCase gdy wynik jest wyświetlany użytkownikowi.
  3. Jeśli aktualnie używasz gdzieś w kodzie InvariantCulture, zastanów się nad zastąpieniem tego Ordinal – InvariantCulture nie jest w pełni prawidłowy z punktu językowego więc lepiej użyć wydajnego Ordinal jeśli na tym nam nie zależy.
  4. Preferuj przeładowania, które akceptują parametr StringCompairson. Domyślne wywołanie jest trudne w czytaniu ponieważ domyślna wartość zależy od funkcji (Equals,Compare). Jawne dostarczenie parametru rozwiewa wszelkie wątpliwości.
  5. Unikaj InvariantCulture w większości przypadków.

W następnym poście chciałbym napisać kilka słów o SortKey oraz metodach ToUpperInvariant\ToLowerInvariant…

One thought on “Porównywanie string’ów część II – ustawienia regionalne, StringComparison”

Leave a Reply

Your email address will not be published.