Kiedy lepiej jest używać konkatenacji String.Format vs String?


120

Mam mały fragment kodu, który analizuje wartość indeksu w celu określenia danych wejściowych komórki do programu Excel. To sprawia, że ​​myślę ...

Jaka jest różnica pomiędzy

xlsSheet.Write("C" + rowIndex.ToString(), null, title);

i

xlsSheet.Write(string.Format("C{0}", rowIndex), null, title);

Czy jeden jest „lepszy” od drugiego? I dlaczego?



możliwy duplikat Why use String.Format?
Druid

Odpowiedzi:


115

Przed C # 6

Szczerze mówiąc, myślę, że pierwsza wersja jest prostsza - chociaż uprościłbym ją do:

xlsSheet.Write("C" + rowIndex, null, title);

Podejrzewam, że inne odpowiedzi mogą mówić o hicie wydajnościowym, ale szczerze mówiąc, będzie to minimalne jeśli w ogóle będzie obecne - a ta wersja konkatenacji nie musi analizować ciągu formatu.

Ciągi formatujące są świetne do celów lokalizacji itp., Ale w takim przypadku konkatenacja jest prostsza i działa równie dobrze.

Dzięki C # 6

Interpolacja ciągów ułatwia czytanie wielu rzeczy w języku C # 6. W tym przypadku drugi kod będzie wyglądał następująco:

xlsSheet.Write($"C{rowIndex}", null, title);

co jest prawdopodobnie najlepszą opcją, IMO.



Wiem wiem. To było zrobione żartem (przy okazji przeczytałem link, który był dobrą lekturą)
nawfal

Jon. Zawsze byłem fanem pana Richtera i religijnie przestrzegałem wskazówek dotyczących boksu itp. Jednak po przeczytaniu twojego (starego) artykułu jestem teraz konwertowany. Dzięki
stevethethread

3
@ mbomb007: Jest teraz na codeblog.jonskeet.uk/2008/10/08/…
Jon Skeet

4
Teraz, gdy C # 6 jest dostępny, możesz użyć nowej składni interpolacji ciągów, co moim zdaniem jest jeszcze łatwiejsze do odczytania:xlsSheet.Write($"C{rowIndex}", null, title);
HotN

158

Moje początkowe preferencje (pochodzące z tła C ++) dotyczyły String.Format. Upuściłem to później z następujących powodów:

  • Konkatenacja ciągów jest prawdopodobnie „bezpieczniejsza”. Zdarzyło mi się (i widziałem, że zdarzyło się to kilku innym programistom), aby usunąć parametr lub przez pomyłkę zepsuć kolejność parametrów. Kompilator nie będzie porównywał parametrów z ciągiem formatu i zakończy się błędem wykonania (to znaczy, jeśli masz szczęście, że nie ma go w niejasnej metodzie, takiej jak rejestrowanie błędu). W przypadku konkatenacji usuwanie parametru jest mniej podatne na błędy. Można argumentować, że prawdopodobieństwo błędu jest bardzo małe, ale może się zdarzyć.

- konkatenacja ciągów znaków dopuszcza wartości null, String.Formatnie. Pisanie „ s1 + null + s2” nie przerywa, po prostu traktuje wartość null jako String.Empty. Cóż, może to zależeć od konkretnego scenariusza - są przypadki, w których wolisz błąd, zamiast po cichu ignorować zerową wartość FirstName. Jednak nawet w tej sytuacji osobiście wolę samodzielnie sprawdzać wartości null i zgłaszać określone błędy zamiast standardowego ArgumentNullException, który otrzymuję z String.Format.

  • Łączenie ciągów działa lepiej. Niektóre z powyższych postów już o tym wspominają (właściwie nie wyjaśniając dlaczego, co zdecydowało mnie napisać ten post :).

Pomysł jest taki, że kompilator .NET jest wystarczająco inteligentny, aby przekonwertować ten fragment kodu:

public static string Test(string s1, int i2, int i3, int i4, 
        string s5, string s6, float f7, float f8)
{
    return s1 + " " + i2 + i3 + i4 + " ddd " + s5 + s6 + f7 + f8;
}

do tego:

public static string Test(string s1, int i2, int i3, int i4,
            string s5, string s6, float f7, float f8)
{
    return string.Concat(new object[] { s1, " ", i2, i3, i4, 
                    " ddd ", s5, s6, f7, f8 });
}

Co dzieje się pod maską String.Concat jest łatwe do odgadnięcia (użyj Reflectora). Obiekty w tablicy są konwertowane na swój ciąg za pomocą ToString (). Następnie obliczana jest całkowita długość i przydzielany jest tylko jeden ciąg (z całkowitą długością). Na koniec każdy ciąg jest kopiowany do otrzymanego ciągu za pośrednictwem wstrcpy w niebezpiecznym fragmencie kodu.

Powody String.Concatsą znacznie szybsze? Cóż, wszyscy możemy zobaczyć, co String.Formatrobi - będziesz zaskoczony ilością kodu wymaganego do przetworzenia ciągu formatu. Poza tym (widziałem komentarze dotyczące zużycia pamięci), String.Formatużywa wewnętrznie StringBuilder. Oto jak:

StringBuilder builder = new StringBuilder(format.Length + (args.Length * 8));

Więc dla każdego przekazanego argumentu rezerwuje 8 znaków. Jeśli argument jest wartością jednocyfrową, to szkoda, mamy trochę zmarnowanego miejsca. Jeśli argument jest obiektem niestandardowym zwracającym jakiś długi tekstToString() , może być nawet potrzebna ponowna alokacja (oczywiście w najgorszym przypadku).

W porównaniu z tym konkatenacja marnuje tylko przestrzeń tablicy obiektów (nie za dużo, biorąc pod uwagę, że jest to tablica odniesień). Nie ma analizowania dla specyfikatorów formatu ani pośredniczącego StringBuilder. Obciążenie związane z pakowaniem / rozpakowywaniem występuje w obu metodach.

Jedynym powodem, dla którego wybrałbym String.Format, jest lokalizacja. Umieszczanie ciągów formatujących w zasobach umożliwia obsługę różnych języków bez ingerowania w kod (pomyśl o scenariuszach, w których sformatowane wartości zmieniają kolejność w zależności od języka, tj. „Po {0} godzinach i {1} minutach” może wyglądać zupełnie inaczej w języku japońskim: ).


Podsumowując mój pierwszy (i dość długi) post:

  • Najlepszym sposobem (pod względem wydajności w porównaniu do łatwości utrzymania / czytelności) jest dla mnie użycie konkatenacji ciągów bez żadnych ToString()wywołań
  • jeśli zależy ci na wydajności, wykonuj ToString()połączenia samodzielnie, aby uniknąć boksu (jestem nieco nastawiony na czytelność) - tak samo jak pierwsza opcja w twoim pytaniu
  • jeśli pokazujesz użytkownikowi zlokalizowane ciągi (nie w tym przypadku), String.Format()ma krawędź.

5
1) string.Formatjest „bezpieczny” podczas korzystania z ReSharper; to znaczy jest tak samo bezpieczny, jak każdy inny kod, którego można użyć [nieprawidłowo]. 2) string.Format nie pozwalają na „bezpieczne” null: string.Format("A{0}B", (string)null)wyniki w „AB”. 3) Rzadko zależy mi na tym poziomie wykonania (i do tego celu rzadko kiedy się StringBuilder

Zgadzam się na 2), wyedytuję post. Nie można zweryfikować, czy było to bezpieczne w wersji 1.1, ale najnowszy framework jest rzeczywiście null bezpieczny.
Dan C.

Czy string.Concat jest nadal używany, jeśli jeden z operandów jest wywołaniem metody z wartością zwracaną, a nie parametrem lub zmienną?
Richard Collette,

2
@RichardCollette Tak, String.Concat jest używany nawet wtedy, gdy łączysz wartości zwracane wywołań metod, np. Kompiluje string s = "This " + MyMethod(arg) + " is a test";się do String.Concat()wywołania w trybie Release.
Dan C.

Fantastyczna odpowiedź; bardzo dobrze napisane i wyjaśnione.
Frank V

6

Myślę, że pierwsza opcja jest bardziej czytelna i to powinno być twoim głównym zmartwieniem.

xlsSheet.Write("C" + rowIndex.ToString(), null, title);

string.Format używa StringBuilder pod maską (sprawdź z reflektorem ), więc nie będzie to miało żadnego wpływu na wydajność, chyba że wykonujesz znaczną ilość konkatenacji. Będzie to wolniejsze dla twojego scenariusza, ale w rzeczywistości ta decyzja dotycząca mikro optymalizacji wydajności jest nieodpowiednia przez większość czasu i naprawdę powinieneś skupić się na czytelności kodu, chyba że jesteś w pętli.

Tak czy inaczej, najpierw napisz pod kątem czytelności, a następnie użyj profilera wydajności, aby zidentyfikować punkty aktywne, jeśli naprawdę uważasz, że masz obawy dotyczące wydajności.



5

W prostym przypadku, w którym jest to prosta pojedyncza konkatenacja, uważam, że nie jest to warte złożoności string.Format(i nie testowałem, ale podejrzewam, że w prostym przypadku, takim jak ten, string.Format może być nieco wolniejsze, co z parsowaniem ciągu formatu i wszystkie). Podobnie jak Jon Skeet, wolę nie wywoływać jawnie .ToString(), ponieważ zostanie to zrobione niejawnie przez string.Concat(string, object)przeciążenie i myślę, że kod jest bardziej przejrzysty i łatwiejszy do odczytania bez niego.

Ale dla więcej niż kilku konkatenacji (ile jest subiektywnych), zdecydowanie wolę string.Format. W pewnym momencie myślę, że zarówno czytelność, jak i wydajność niepotrzebnie cierpią z powodu konkatenacji.

Jeśli w ciągu formatu jest wiele parametrów (ponownie „wiele” jest subiektywne), zwykle wolę dołączać skomentowane indeksy do zastępowanych argumentów, aby nie stracić orientacji, która wartość trafia do którego parametru. Wymyślony przykład:

Console.WriteLine(
    "Dear {0} {1},\n\n" +

    "Our records indicate that your {2}, \"{3}\", is due for {4} {5} shots.\n" +
    "Please call our office at 1-900-382-5633 to make an appointment.\n\n" +

    "Thank you,\n" +
    "Eastern Veterinary",

    /*0*/client.Title,
    /*1*/client.LastName,
    /*2*/client.Pet.Animal,
    /*3*/client.Pet.Name,
    /*4*/client.Pet.Gender == Gender.Male ? "his" : "her",
    /*5*/client.Pet.Schedule[0]
);

Aktualizacja

Wydaje mi się, że podany przeze mnie przykład jest nieco zagmatwany, ponieważ wygląda na to, że użyłem zarówno konkatenacji, jak i string.Formattutaj. I tak, logicznie i leksykalnie, właśnie to zrobiłem. Ale wszystkie konkatenacje zostaną zoptymalizowane przez kompilator 1 , ponieważ wszystkie są literałami łańcuchowymi. Więc w czasie wykonywania będzie jeden ciąg. Więc chyba powinienem powiedzieć, że wolę unikać wielu konkatenacji w czasie wykonywania .

Oczywiście większość tego tematu jest teraz nieaktualna, chyba że nadal utkniesz w C # 5 lub starszym. Teraz mamy interpolowane ciągi znaków , które pod względem czytelności są o wiele lepsze niż string.Formatw prawie wszystkich przypadkach. W dzisiejszych czasach, chyba że po prostu konkatenuję wartość bezpośrednio na początku lub na końcu literału ciągu, prawie zawsze używam interpolacji ciągów. Dzisiaj napisałbym mój wcześniejszy przykład w ten sposób:

Console.WriteLine(
    $"Dear {client.Title} {client.LastName},\n\n" +

    $"Our records indicate that your {client.Pet.Animal}, \"{client.Pet.Name}\", " +
    $"is due for {(client.Pet.Gender == Gender.Male ? "his" : "her")} " +
    $"{client.Pet.Schedule[0]} shots.\n" +
    "Please call our office at 1-900-382-5633 to make an appointment.\n\n" +

    "Thank you,\n" +
    "Eastern Veterinary"
);

W ten sposób tracisz konkatenację w czasie kompilacji. Każdy interpolowany ciąg jest przekształcany w wywołanie string.Formatprzez kompilator, a ich wyniki są łączone w czasie wykonywania. Oznacza to, że jest to poświęcenie wydajności w czasie wykonywania na rzecz czytelności. W większości przypadków jest to opłacalne poświęcenie, ponieważ kara za bieganie jest znikoma. Jednak w przypadku kodu krytycznego dla wydajności może być konieczne profilowanie różnych rozwiązań.


1 Możesz to zobaczyć w specyfikacji C # :

... następujące konstrukcje są dozwolone w wyrażeniach stałych:

...

  • Predefiniowany operator binarny + ...

Możesz to również zweryfikować za pomocą małego kodu:

const string s =
    "This compiles successfully, " +
    "and you can see that it will " +
    "all be one string (named `s`) " +
    "at run time";

1
fyi, możesz użyć @ "... ciąg wieloliniowy" zamiast wszystkich konkatenacji.
Aaron Palmer

Tak, ale wtedy musisz wyrównać tekst do lewej. Ciągi @ zawierają wszystkie nowe wiersze i znaki tabulacji między cudzysłowami.
P Daddy,

Wiem, że to jest stare, ale jest to przypadek, w którym powiedziałbym, że umieść ciąg formatu w pliku resx.
Andy,

2
Wow, wszyscy skupiają się na dosłownym znaczeniu tekstu zamiast na sednie sprawy.
P Daddy

heheh - właśnie zauważyłem konkatenację ciągów wewnątrz twojegoString.Format()
Kristopher

3

Jeśli twój ciąg był bardziej złożony i zawierało wiele zmiennych, to wybrałbym string.Format (). Ale jeśli chodzi o rozmiar ciągu i liczbę łączonych zmiennych w twoim przypadku, wybrałbym twoją pierwszą wersję, jest bardziej spartańska .


3

Przyjrzałem się String.Format (przy użyciu reflektora) i faktycznie tworzy StringBuilder, a następnie wywołuje na nim AppendFormat. Jest to więc szybsze niż łączenie wielu mieszań. Najszybciej (jak sądzę) byłoby utworzenie StringBuildera i ręczne wykonanie wywołań Append. Oczywiście liczba „wielu” jest do odgadnięcia. Użyłbym + (właściwie i ponieważ jestem głównie programistą VB) do czegoś tak prostego jak twój przykład. Ponieważ robi się bardziej skomplikowane, używam String.Format. Jeśli istnieje DUŻO zmiennych, to wybrałbym StringBuilder i Append, na przykład mamy kod, który buduje kod, tam używam jednej linii rzeczywistego kodu, aby wyświetlić jedną linię wygenerowanego kodu.

Wydaje się, że istnieją spekulacje na temat liczby tworzonych ciągów dla każdej z tych operacji, więc weźmy kilka prostych przykładów.

"C" + rowIndex.ToString();

„C” jest już ciągiem znaków.
rowIndex.ToString () tworzy kolejny ciąg. (@manohard - nie wystąpi boksowanie rowIndex)
Następnie otrzymujemy końcowy ciąg.
Jeśli weźmiemy przykład

String.Format("C(0)",rowIndex);

wtedy mamy „C {0}” jako ciąg string
rowIndex zostaje zapakowany w celu przekazania do funkcji
Tworzony jest nowy program do tworzenia
ciągów W konstruktorze ciągów wywoływana jest funkcja AppendFormat - nie znam szczegółów działania AppendFormat, ale załóżmy, że tak jest bardzo wydajna, nadal będzie musiała przekształcić prostokątny rowIndex w ciąg.
Następnie przekonwertuj program budujący ciąg na nowy ciąg.
Wiem, że StringBuilders próbują zapobiec wykonywaniu niepotrzebnych kopii pamięci, ale String.Format nadal kończy się dodatkowym narzutem w porównaniu do zwykłej konkatenacji.

Jeśli weźmiemy teraz przykład z kilkoma innymi ciągami

"a" + rowIndex.ToString() + "b" + colIndex.ToString() + "c" + zIndex.ToString();

mamy na początek 6 łańcuchów, które będą takie same we wszystkich przypadkach.
Korzystając z konkatenacji mamy również 4 ciągi pośrednie oraz wynik końcowy. To właśnie te pośrednie wyniki są eliminowane przy użyciu String, Format (lub StringBuilder).
Pamiętaj, że aby utworzyć każdy ciąg pośredni, poprzedni musi zostać skopiowany do nowej lokalizacji pamięci, a nie tylko alokacja pamięci jest potencjalnie wolna.


4
Nitpick. W "a" + ... + "b" + ... + "c" + ... tak naprawdę nie będziesz mieć 4 pośrednich ciągów. Kompilator wygeneruje wywołanie metody statycznej String.Concat (params string [] values) i wszystkie zostaną połączone jednocześnie. Nadal wolałbym ciąg znaków, ale formatuj ze względu na czytelność.
P Daddy

2

Podoba mi się String.Format, ponieważ może znacznie ułatwić śledzenie i czytanie sformatowanego tekstu niż konkatenacja inline, a także jest znacznie bardziej elastyczny, umożliwiając formatowanie parametrów, jednak w przypadku krótkich zastosowań, takich jak twoje, nie widzę problemu z łączeniem.

W przypadku konkatenacji wewnątrz pętli lub w dużych ciągach zawsze należy próbować użyć klasy StringBuilder.


2

Ten przykład jest prawdopodobnie zbyt trywialny, aby zauważyć różnicę. W rzeczywistości myślę, że w większości przypadków kompilator może w ogóle zoptymalizować każdą różnicę.

Jednak gdybym musiał zgadywać, dałbym string.Format()przewagę bardziej skomplikowanym scenariuszom. Ale to bardziej intuicyjne przeczucie, że prawdopodobnie lepiej zrobi, wykorzystując bufor zamiast tworzyć wiele niezmiennych ciągów, a nie w oparciu o żadne rzeczywiste dane.


1

Zgadzam się z wieloma punktami powyżej, kolejną kwestią, o której uważam, że należy wspomnieć, jest łatwość utrzymania kodu. string.Format pozwala na łatwiejszą zmianę kodu.

tj. mam wiadomość "The user is not authorized for location " + locationlub "The User is not authorized for location {0}"

gdybym kiedykolwiek chciał zmienić wiadomość, aby powiedzieć: location + " does not allow this User Access"lub "{0} does not allow this User Access"

za pomocą string.Format wszystko, co muszę zrobić, to zmienić ciąg. do konkatenacji muszę zmodyfikować tę wiadomość

jeśli jest używany w wielu miejscach, pozwala zaoszczędzić czas.


1

Miałem wrażenie, że format string.format był szybszy, w tym teście wydaje się być 3 razy wolniejszy

string concat = "";
        System.Diagnostics.Stopwatch sw1 = new System.Diagnostics.Stopwatch    ();
        sw1.Start();
        for (int i = 0; i < 10000000; i++)
        {
            concat = string.Format("{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}","1", "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" , "10" , i);
        }
        sw1.Stop();
        Response.Write("format: "  + sw1.ElapsedMilliseconds.ToString());
        System.Diagnostics.Stopwatch sw2 = new System.Diagnostics.Stopwatch();
        sw2.Start();
        for (int i = 0; i < 10000000; i++)
        {
            concat = "1" + "2" + "3" + "4" + "5" + "6" + "7" + "8" + "9" + "10" + i;
        }
        sw2.Stop();

string.format zajęło 4,6 sekundy, a przy użyciu „+” 1,6 sekundy.


7
Kompilator rozpoznaje "1" + "2" + "3" + "4" + "5" + "6" + "7" + "8" + "9" + "10"jako jeden literał ciągu, więc wiersz skutecznie staje "12345678910" + isię szybszy od poprzedniegostring.Format(...)
wertzui

0

string.Format jest prawdopodobnie lepszym wyborem, gdy szablon formatu („C {0}”) jest przechowywany w pliku konfiguracyjnym (takim jak Web.config / App.config)


0

Zrobiłem trochę profilowania różnych metod ciągów, w tym string.Format, StringBuilder i konkatenacja ciągów. Łączenie ciągów prawie zawsze przewyższało inne metody budowania łańcuchów. Jeśli więc wydajność jest kluczowa, to jest lepsza. Jeśli jednak wydajność nie jest krytyczna, osobiście uważam, że string.Format jest łatwiejszy do śledzenia w kodzie. (Ale to subiektywny powód) Jednak StringBuilder jest prawdopodobnie najbardziej efektywny pod względem wykorzystania pamięci.



-1

Łączenie ciągów zajmuje więcej pamięci w porównaniu do String.Format. Dlatego najlepszym sposobem łączenia ciągów jest użycie obiektu String.Format lub System.Text.StringBuilder.

Weźmy pierwszy przypadek: "C" + rowIndex.ToString () Załóżmy, że rowIndex jest typem wartości, więc metoda ToString () musi wykonać Box, aby przekonwertować wartość na String, a następnie CLR tworzy pamięć dla nowego ciągu zawierającego obie wartości.

Gdzie jako string.Format oczekuje parametru obiektu i przyjmuje rowIndex jako obiekt i konwertuje go na łańcuch wewnętrznie poza kursem, będzie Boxing, ale jest on nieodłączny, a także nie zajmuje tak dużo pamięci, jak w pierwszym przypadku.

W przypadku krótkich strun to chyba nie ma większego znaczenia ...

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.