Wprowadzenie
Typ danych typu string jest jednym z podstawowych typów danych, obok numerycznych (int, long, double) i logicznych (Boolean). Trudno sobie wyobrazić co najmniej jeden przydatny program, który nie wykorzystuje tego typu.
Na platformie .NET typ ciągu jest prezentowany jako niezmienna klasa String. Ponadto jest silnie zintegrowany ze środowiskiem CLR i jest również obsługiwany przez kompilator C#.
Ten artykuł jest poświęcony konkatenacji – operacji wykonywanej na ciągach tak często, jak operacja dodawania na liczbach. Możesz pomyśleć:„Co tu powiedzieć?”, przecież wszyscy wiemy o operatorze łańcucha „+”, ale jak się okazało, ma on swoje dziwactwa.
Specyfikacja języka dla operatora ciągu „+”
Specyfikacja języka C# zapewnia trzy przeciążenia dla operatora ciągu „+”:
string operator + (string x, string y) string operator + (string x, object y) string operator + (object x, string y)
Jeśli jeden z argumentów konkatenacji ciągów ma wartość NULL, wstawiany jest pusty ciąg. W przeciwnym razie każdy argument, który nie jest ciągiem, jest reprezentowany jako ciąg przez wywołanie metody wirtualnej ToString. Jeśli metoda ToString zwraca wartość NULL, wstawiany jest pusty ciąg. Należy zauważyć, że zgodnie ze specyfikacją ta operacja nigdy nie powinna zwracać wartości NULL.
Opis operatora jest wystarczająco jasny, jednak jeśli przyjrzymy się implementacji klasy String, znajdziemy jasną definicję tylko dwóch operatorów „==” i „!=”. Powstaje uzasadnione pytanie:co dzieje się za kulisami konkatenacji strun? Jak kompilator obsługuje operator łańcucha „+”?
Odpowiedź na to pytanie nie okazała się taka trudna. Przyjrzyjmy się bliżej statycznej metodzie String.Concat. Metoda String.Concat łączy jedno lub więcej wystąpień klasy String lub widoków jako wartości String co najmniej jednego wystąpienia Object. Istnieją następujące przeciążenia tej metody:
public static String Concat (String str0, String str1) public static String Concat (String str0, String str1, String str2) public static String Concat (String str0, String str1, String str2, String str3) public static String Concat (params String[] values) public static String Concat (IEnumerable <String> values) public static String Concat (Object arg0) public static String Concat (Object arg0, Object arg1) public static String Concat (Object arg0, Object arg1, Object arg2) public static String Concat (Object arg0, Object arg1, Object arg2, Object arg3, __arglist) public static String Concat <T> (IEnumerable <T> values)
Szczegóły
Załóżmy, że mamy następujące wyrażenie s =a + b, gdzie aib to łańcuchy. Kompilator konwertuje je na wywołanie statycznej metody Concat, tj.
s = string.Concat (a, b)
Operacja konkatenacji ciągów, podobnie jak każda inna operacja dodawania w języku C#, jest lewostronna.
Wszystko jest jasne w przypadku dwóch rzędów, ale co, jeśli jest ich więcej? Wyrażenie s =a + b + c, biorąc pod uwagę lewą asocjatywność operacji, można by zastąpić przez:
s = string.Concat(string.Concat (a, b), c)
Jednak biorąc pod uwagę przeciążenie, które wymaga trzech argumentów, zostanie ono przekonwertowane na:
s = string.Concat (a, b, c)
Podobna sytuacja jest z konkatenacją czterech strun. Aby połączyć 5 lub więcej ciągów, mamy przeciążenie string.Concat (params string[]), więc konieczne jest uwzględnienie narzutu związanego z alokacją pamięci dla tablicy.
Należy również zauważyć, że operator konkatenacji ciągów jest w pełni asocjacyjny :nie ma znaczenia, w jakiej kolejności łączymy ciągi, więc wyrażenie s =a + (b + c), pomimo wyraźnie wskazanego priorytetu wykonania konkatenacji, należy przetwarzać w następujący sposób
s = (a + b) + c = string.Concat (a, b, c)
zamiast oczekiwanych:
s = string.Concat (a, string.Concat (b, c))
Tak więc, podsumowując powyższe:operacja łączenia ciągów jest zawsze reprezentowana od lewej do prawej i wywołuje statyczną metodę String.Concat.
Optymalizujący kompilator pod kątem dosłownych ciągów
Kompilator C# ma optymalizacje związane z ciągami literowymi. Na przykład wyrażenie s =„a” + „b” + c, biorąc pod uwagę lewą łączność operatora „+”, jest równoważne s =(„a” + „b”) + c jest konwertowane na
s = string.Concat ("ab", c)
Wyrażenie s =c + „a” + „b”, pomimo lewego zespolenia operacji konkatenacji (s =(c + „a”) + „b”) jest konwertowane na
s = string.Concat (c, "ab")
Ogólnie pozycja literałów nie ma znaczenia, kompilator konkatenuje wszystko, co może, a dopiero potem próbuje wybrać odpowiednie przeciążenie metody Concat. Wyrażenie s =a + „b” + „c” + d jest konwertowane na
s = string.Concat (a, "bc", d)
Należy również wspomnieć o optymalizacjach związanych z ciągami pustymi i NULL. Kompilator wie, że dodanie pustego żądła nie wpływa na wynik konkatenacji, więc wyrażenie s =a + „” + b jest konwertowane na
s = string.Concat (a, b),
zamiast oczekiwanego
s = string.Concat (a, "", b)
Podobnie w przypadku łańcucha const, którego wartość wynosi NULL, mamy:
const string nullStr = null; s = a + nullStr + b;
jest konwertowany na
s = string.Concat (a, b)
Wyrażenie s =a + nullStr jest konwertowane na s =a ?? „”, jeśli a jest ciągiem i wywołanie metody string.Concat(a), jeśli a nie jest ciągiem, na przykład s =17 + nullStr, jest konwertowane na s =string.Concat (17) .
Ciekawa funkcja związana z optymalizacją przetwarzania literałów i lewą asocjatywnością operatora ciągu znaków „+”.
Rozważmy wyrażenie:
var s1 = 17 + 17 + "abc";
biorąc pod uwagę lewą asocjatywność, jest to równoważne
var s1 = (17 + 17) + "abc"; // сalling the string.Concat method (34, "abc")
W rezultacie w czasie kompilacji dodawane są liczby, dzięki czemu wynik będzie wynosił 34abc.
Z drugiej strony wyrażenie
var s2 = "abc" + 17 + 17;
jest odpowiednikiem
var s2 = ( "abc" + 17) + 17; // calling the string.Concat method ("abc", 17, 17)
wynikiem będzie abc1717.
Tak więc, ten sam operator konkatenacji prowadzi do różnych wyników.
String.Concat VS StringBuilder.Append
O tym porównaniu trzeba powiedzieć kilka słów. Rozważmy następujący kod:
string name = "Timur"; string surname = "Guev"; string patronymic = "Ahsarbecovich"; string fio = surname + name + patronymic;
Można go zastąpić kodem za pomocą StringBuilder:
var sb = new StringBuilder (); sb.Append (surname); sb.Append (name); sb.Append (patronymic); string fio = sb.ToString ();
Jednak w tym przypadku z trudem odniesiemy korzyści z używania StringBuilder. Pomijając fakt, że kod stał się mniej czytelny, stał się on mniej lub bardziej efektywny, ponieważ implementacja metody Concat oblicza długość wynikowego ciągu i alokuje pamięć tylko raz, w przeciwieństwie do StringBuildera, który nic nie wie o długości wynikowego ciągu.
Implementacja metody Concat dla 3 ciągów:
public static string Concat (string str0, string str1, string str2) { if (str0 == null && str1 == null && str2 == null) return string.Empty; if (str0 == null) str0 = string.Empty; if (str1 == null) str1 = string.Empty; if (str2 == null) str2 = string.Empty; string dest = string.FastAllocateString (str0.Length + str1.Length + str2.Length); // Allocate memory for strings string.FillStringChecked (dest, 0, str0); / string.FillStringChecked (dest, str0.Length, str1); string.FillStringChecked (dest, str0.Length + str1.Length, str2); return dest; }
Operator „+” w Javie
Kilka słów o operatorze łańcucha „+” w Javie. Co prawda nie programuję w Javie, ale interesuje mnie, jak to tam działa. Kompilator Java optymalizuje operator „+” tak, że używa klasy StringBuilder i wywołuje metodę append.
Poprzedni kod jest konwertowany na
String fio = new StringBuilder(String.valueOf(surname)).append(name).append (patronymic).ToString()
Warto zauważyć, że celowo odmówili takiej optymalizacji w C#, post na ten temat ma Eric Lippert. Chodzi o to, że taka optymalizacja nie jest optymalizacją jako taką, tylko przepisywaniem kodu. Poza tym twórcy języka C# uważają, że programiści powinni znać aspekty pracy z klasą String i, jeśli to konieczne, przełączyć się na StringBuilder.
Nawiasem mówiąc, Eric Lippert był tym, który pracował nad optymalizacją kompilatora C# związanego z łączeniem łańcuchów.
Wniosek
Być może na pierwszy rzut oka może wydawać się dziwne, że klasa String nie definiuje operatora „+”, dopóki nie pomyślimy o możliwościach optymalizacji kompilatora związanych z widocznością większego fragmentu kodu. Na przykład, jeśli operator „+” został zdefiniowany w klasie String, wyrażenie s =a + b + c + d doprowadziłoby do utworzenia dwóch ciągów pośrednich, pojedynczego wywołania ciągu.Concat (a, b, c, d) metoda pozwala na efektywniejsze wykonanie konkatenacji.