Wann ist es besser, String.Format gegen String-Verkettung zu verwenden?

119

Ich habe einen kleinen Code, der einen Indexwert analysiert, um eine Zelleingabe in Excel zu bestimmen. Ich muss nachdenken ...

Was ist der Unterschied zwischen

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

und

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

Ist einer "besser" als der andere? Und warum?

Gavin Miller
quelle
4
Dies ist ähnlich zu stackoverflow.com/questions/16432/…
Jonathan S.
Mögliches Duplikat von Warum String.Format verwenden?
Druide

Antworten:

115

Vor C # 6

Um ehrlich zu sein, denke ich, dass die erste Version einfacher ist - obwohl ich sie vereinfachen würde, um:

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

Ich vermute , dass andere Antworten können über die Leistung Hit sprechen, aber um ehrlich zu sein es minimal sein würde wenn überhaupt - und diese Verkettung Version entspricht nicht das Format - String analysieren muss.

Formatzeichenfolgen eignen sich hervorragend zum Lokalisieren usw., aber in einem solchen Fall ist die Verkettung einfacher und funktioniert genauso gut.

Mit C # 6

Die String-Interpolation vereinfacht das Lesen vieler Dinge in C # 6. In diesem Fall wird Ihr zweiter Code zu:

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

Das ist wahrscheinlich die beste Option, IMO.

Jon Skeet
quelle
Ich weiß, ich weiß. Es wurde im Scherz gemacht (habe den Link übrigens schon einmal gelesen, was eine gute Lektüre war)
nawfal
Jon. Ich war schon immer ein Fan von Herrn Richter und habe mich religiös an die Anleitung zum Boxen usw. gehalten. Nachdem ich Ihren (alten) Artikel gelesen habe, bin ich jetzt ein Konvertit. Danke
stevethethread
3
@ mbomb007: Es ist jetzt bei codeblog.jonskeet.uk/2008/10/08/…
Jon Skeet
4
Jetzt, da C # 6 verfügbar ist, können Sie die neue String-Interpolationssyntax für eine meiner Meinung nach noch einfachere Lesbarkeit verwenden:xlsSheet.Write($"C{rowIndex}", null, title);
HotN
157

Meine anfängliche Präferenz (aus einem C ++ - Hintergrund) war String.Format. Ich habe dies später aus folgenden Gründen fallen gelassen:

  • Die Verkettung von Zeichenfolgen ist wohl "sicherer". Es ist mir passiert (und ich habe gesehen, dass es mehreren anderen Entwicklern passiert ist), einen Parameter zu entfernen oder die Parameterreihenfolge versehentlich durcheinander zu bringen. Der Compiler vergleicht die Parameter nicht mit der Formatzeichenfolge und es kommt zu einem Laufzeitfehler (wenn Sie das Glück haben, ihn nicht in einer undurchsichtigen Methode zu haben, z. B. beim Protokollieren eines Fehlers). Bei der Verkettung ist das Entfernen eines Parameters weniger fehleranfällig. Sie könnten argumentieren, dass die Fehlerwahrscheinlichkeit sehr gering ist, aber es kann vorkommen .

- String-Verkettung erlaubt Nullwerte, String.Formatnicht. Das Schreiben von " s1 + null + s2" wird nicht unterbrochen, sondern behandelt den Nullwert lediglich als String.Empty. Nun, dies kann von Ihrem spezifischen Szenario abhängen - es gibt Fälle, in denen Sie einen Fehler wünschen, anstatt einen Null-Vornamen stillschweigend zu ignorieren. Aber selbst in dieser Situation bevorzuge ich es persönlich, selbst nach Nullen zu suchen und bestimmte Fehler zu werfen, anstatt der Standard-ArgumentNullException, die ich von String.Format erhalte.

  • Die Verkettung von Zeichenfolgen ist besser. Einige der obigen Beiträge erwähnen dies bereits (ohne tatsächlich zu erklären, warum, was mich dazu veranlasste, diesen Beitrag zu schreiben :).

Die Idee ist, dass der .NET-Compiler intelligent genug ist, um diesen Code zu konvertieren:

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

dazu:

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

Was unter der Haube von String.Concat passiert, ist leicht zu erraten (verwenden Sie Reflector). Die Objekte im Array werden über ToString () in ihre Zeichenfolge konvertiert. Dann wird die Gesamtlänge berechnet und nur eine Zeichenfolge zugewiesen (mit der Gesamtlänge). Schließlich wird jede Zeichenfolge über wstrcpy in einem unsicheren Code in die resultierende Zeichenfolge kopiert.

Gründe String.Concatist viel schneller? Nun, wir können alle sehen, was String.Formatpassiert - Sie werden überrascht sein, wie viel Code für die Verarbeitung der Formatzeichenfolge erforderlich ist. Darüber hinaus (ich habe Kommentare zum Speicherverbrauch gesehen) wird String.Formatintern ein StringBuilder verwendet. Hier ist wie:

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

Für jedes übergebene Argument werden 8 Zeichen reserviert. Wenn das Argument ein einstelliger Wert ist, haben wir schade Platz verschwendet. Wenn es sich bei dem Argument um ein benutzerdefiniertes Objekt handelt, für das Langtext zurückgegeben wird ToString(), ist möglicherweise sogar eine Neuzuweisung erforderlich (natürlich im schlimmsten Fall).

Im Vergleich dazu verschwendet die Verkettung nur den Speicherplatz des Objektarrays (nicht zu viel, wenn man bedenkt, dass es sich um ein Array von Referenzen handelt). Es gibt keine Analyse für Formatspezifizierer und keinen zwischengeschalteten StringBuilder. Der Box- / Unboxing-Overhead ist bei beiden Methoden vorhanden.

Der einzige Grund, warum ich mich für String.Format entschieden habe, ist die Lokalisierung. Durch das Einfügen von Formatzeichenfolgen in Ressourcen können Sie verschiedene Sprachen unterstützen, ohne den Code zu beeinträchtigen (denken Sie an Szenarien, in denen formatierte Werte die Reihenfolge je nach Sprache ändern, dh "nach {0} Stunden und {1} Minuten" auf Japanisch möglicherweise ganz anders aussehen: ).


Um meinen ersten (und ziemlich langen) Beitrag zusammenzufassen:

  • Der beste Weg (in Bezug auf Leistung vs. Wartbarkeit / Lesbarkeit) für mich ist die Verwendung der Zeichenfolgenverkettung ohne ToString()Aufrufe
  • Wenn Sie nach der Aufführung sind, ToString()rufen Sie selbst an, um das Boxen zu vermeiden (ich bin etwas voreingenommen gegenüber der Lesbarkeit) - genau wie bei der ersten Option in Ihrer Frage
  • Wenn Sie dem Benutzer lokalisierte Zeichenfolgen anzeigen (hier nicht der Fall), String.Format()hat dies eine Kante.
Dan C.
quelle
5
1) string.Formatist "sicher" bei Verwendung von ReSharper; Das heißt, es ist so sicher wie jeder andere Code, der [falsch] verwendet werden kann. 2) string.Format nicht erlauben , für einen „sicheren“ null: string.Format("A{0}B", (string)null)Ergebnisse in „AB“. 3) Ich kümmere mich selten um dieses Leistungsniveau (und zu diesem Zweck ist es ein seltener Tag, an dem ich mich StringBuilder
Stimme zu 2), ich werde den Beitrag bearbeiten. Ich kann nicht überprüfen, ob dies in 1.1 sicher war, aber das neueste Framework ist in der Tat null-sicher.
Dan C.
Wird string.Concat weiterhin verwendet, wenn einer der Operanden ein Methodenaufruf mit einem Rückgabewert ist, anstatt ein Parameter oder eine Variable zu sein?
Richard Collette
2
@RichardCollette Ja, String.Concat wird auch verwendet, wenn Sie Rückgabewerte von Methodenaufrufen verketten, z. B. wenn sie im Release-Modus string s = "This " + MyMethod(arg) + " is a test";zu einem String.Concat()Aufruf kompiliert werden.
Dan C.
Fantastische Antwort; sehr gut geschrieben und erklärt.
Frank V
6

Ich denke, die erste Option ist besser lesbar und das sollte Ihr Hauptanliegen sein.

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

string.Format verwendet einen StringBuilder unter der Haube (mit Reflektor prüfen ), sodass er keinen Leistungsvorteil bietet , es sei denn, Sie führen eine erhebliche Verkettung durch. Es wird für Ihr Szenario langsamer sein, aber die Realität ist, dass diese Entscheidung zur Optimierung der Mikroleistung die meiste Zeit unangemessen ist und Sie sich wirklich auf die Lesbarkeit Ihres Codes konzentrieren sollten, es sei denn, Sie befinden sich in einer Schleife.

Schreiben Sie in jedem Fall zuerst zur besseren Lesbarkeit und verwenden Sie dann einen Leistungsprofiler , um Ihre Hotspots zu identifizieren, wenn Sie wirklich glauben, dass Sie Leistungsprobleme haben.

Martin Hollingsworth
quelle
5

Für einen einfachen Fall , in dem es eine einfache einzelne Verkettung ist, glaube ich , dass es die Komplexität des nicht wert ist string.Format(und ich habe nicht getestet, aber ich vermute , dass für einen einfachen Fall wie diesen, string.Format vielleicht etwas langsamer sein, was mit dem Format - String - Parsing und alles). Wie Jon Skeet ziehe ich es vor, nicht explizit aufzurufen .ToString(), da dies implizit durch die string.Concat(string, object)Überlastung erfolgt und ich denke, dass der Code sauberer aussieht und ohne ihn einfacher zu lesen ist.

Aber für mehr als ein paar Verkettungen (wie viele sind subjektiv) bevorzuge ich definitiv string.Format. Ab einem bestimmten Punkt denke ich, dass sowohl die Lesbarkeit als auch die Leistung unnötig unter der Verkettung leiden.

Wenn die Formatzeichenfolge viele Parameter enthält (wiederum ist "viele" subjektiv), ziehe ich es normalerweise vor, kommentierte Indizes in die Ersetzungsargumente aufzunehmen, damit ich nicht den Überblick verliere, welcher Wert zu welchem ​​Parameter gehört. Ein erfundenes Beispiel:

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

Aktualisieren

Mir fällt auf, dass das Beispiel, das ich gegeben habe, etwas verwirrend ist, weil es den Anschein hat, dass ich sowohl die Verkettung als auch string.Formathier verwendet habe. Und ja, logisch und lexikalisch habe ich das getan. Die Verkettungen werden jedoch alle vom Compiler 1 optimiert , da sie alle Zeichenfolgenliterale sind. Zur Laufzeit gibt es also eine einzelne Zeichenfolge. Ich sollte also sagen, dass ich es vorziehen würde, viele Verkettungen zur Laufzeit zu vermeiden .

Natürlich ist der größte Teil dieses Themas nicht mehr aktuell, es sei denn, Sie verwenden noch C # 5 oder älter. Jetzt haben wir Zeichenfolgen interpoliert , die aus Gründen der Lesbarkeit string.Formatin fast allen Fällen weit überlegen sind. Heutzutage verwende ich fast immer die Zeichenfolgeninterpolation, es sei denn, ich verkette einen Wert direkt mit dem Anfang oder Ende eines Zeichenfolgenliteral. Heute würde ich mein früheres Beispiel so schreiben:

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

Auf diese Weise verlieren Sie die Verkettung während der Kompilierung. Jede interpolierte Zeichenfolge wird string.Formatvom Compiler in einen Aufruf umgewandelt , und ihre Ergebnisse werden zur Laufzeit verkettet. Dies bedeutet, dass dies ein Opfer der Laufzeitleistung für die Lesbarkeit ist. Meistens ist es ein lohnendes Opfer, da die Laufzeitstrafe vernachlässigbar ist. In leistungskritischem Code müssen Sie jedoch möglicherweise unterschiedliche Lösungen profilieren.


1 Sie können dies in der C # -Spezifikation sehen :

... die folgenden Konstrukte sind in konstanten Ausdrücken zulässig:

...

  • Der vordefinierte + ... Binäroperator ...

Sie können es auch mit einem kleinen Code überprüfen:

const string s =
    "This compiles successfully, " +
    "and you can see that it will " +
    "all be one string (named `s`) " +
    "at run time";
P Papa
quelle
1
Zu Ihrer Information, Sie können @ "... mehrzeilige Zeichenfolge" anstelle aller Verkettungen verwenden.
Aaron Palmer
Ja, aber dann müssen Sie Ihre Zeichenfolge linksbündig ausrichten. @ Strings enthalten alle neuen Zeilen und Tabulatorzeichen zwischen den Anführungszeichen.
P Daddy
Ich weiß, dass dies alt ist, aber dies ist ein Fall, in dem ich sagen würde, dass die Formatzeichenfolge in eine Resx-Datei eingefügt wird.
Andy
2
Wow, alle haben sich auf das String-Literal konzentriert, anstatt auf den Kern der Sache.
P Daddy
heheh - Ich habe gerade die String-Verkettung in IhremString.Format()
Kristopher
3

Wenn Ihre Zeichenfolge komplexer wäre und viele Variablen verkettet würden, würde ich die Zeichenfolge auswählen.Format (). Aber für die Größe der Zeichenfolge und die Anzahl der Variablen, die in Ihrem Fall verkettet werden, würde ich mich für Ihre erste Version entscheiden, sie ist spartanischer .

Aaron Palmer
quelle
3

Ich habe mir String.Format (mit Reflector) angesehen und es erstellt tatsächlich einen StringBuilder und ruft dann AppendFormat darauf auf. So ist es schneller als concat für mehrere stirngs. Am schnellsten (glaube ich) wäre es, einen StringBuilder zu erstellen und die Aufrufe zum manuellen Anhängen auszuführen. Natürlich kann die Anzahl der "vielen" erraten werden. Ich würde + (eigentlich & weil ich meistens ein VB-Programmierer bin) für etwas so Einfaches wie Ihr Beispiel verwenden. Da es immer komplexer wird, verwende ich String.Format. Wenn es VIELE Variablen gibt, würde ich mich für einen StringBuilder entscheiden und anhängen. Zum Beispiel haben wir Code, der Code erstellt. Dort verwende ich eine Zeile tatsächlichen Codes, um eine Zeile generierten Codes auszugeben.

Es scheint einige Spekulationen darüber zu geben, wie viele Zeichenfolgen für jede dieser Operationen erstellt werden. Nehmen wir also einige einfache Beispiele.

"C" + rowIndex.ToString();

"C" ist bereits eine Zeichenfolge.
rowIndex.ToString () erstellt eine weitere Zeichenfolge. (@manohard - es wird kein Boxing von rowIndex stattfinden)
Dann erhalten wir die endgültige Zeichenfolge.
Wenn wir das Beispiel von nehmen

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

dann haben wir "C {0}" als Zeichenfolge
rowIndex wird eingerahmt, um an die Funktion übergeben zu werden.
Ein neuer Zeichenfolgengenerator wird erstellt.
AppendFormat wird im Zeichenfolgengenerator aufgerufen. Ich kenne die Details der Funktionsweise von AppendFormat nicht, nehme aber an, dass dies der Fall ist Sehr effizient, es wird immer noch erforderlich sein, den boxed rowIndex in einen String umzuwandeln.
Konvertieren Sie dann den Stringbuilder in einen neuen String.
Ich weiß, dass StringBuilder versuchen, sinnlose Speicherkopien zu verhindern, aber das String.Format hat im Vergleich zur einfachen Verkettung immer noch zusätzlichen Aufwand.

Nehmen wir jetzt ein Beispiel mit ein paar weiteren Zeichenfolgen

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

Wir haben zunächst 6 Zeichenfolgen, die in allen Fällen gleich sind.
Mit der Verkettung haben wir auch 4 Zwischenzeichenfolgen plus das Endergebnis. Es sind diese Zwischenergebnisse, die mithilfe von String, Format (oder einem StringBuilder) eliminiert werden.
Denken Sie daran, dass zum Erstellen jeder Zwischenzeichenfolge die vorherige an einen neuen Speicherort kopiert werden muss. Nicht nur die Speicherzuordnung ist möglicherweise langsam.

pipTheGeek
quelle
4
Nitpick. In "a" + ... + "b" + ... + "c" + ... haben Sie eigentlich keine 4 Zwischenzeichenfolgen. Der Compiler generiert einen Aufruf der statischen Methode String.Concat (params string [] values) und alle werden gleichzeitig verkettet. Ich würde trotzdem string.Format aus Gründen der Lesbarkeit bevorzugen.
P Daddy
2

Ich mag String.Format, weil Ihr formatierter Text viel einfacher zu verfolgen und zu lesen ist als die Inline-Verkettung. Außerdem ist er viel flexibler, sodass Sie Ihre Parameter formatieren können. Für kurze Verwendungszwecke wie Ihren sehe ich jedoch kein Problem mit der Verkettung.

Bei Verkettungen innerhalb von Schleifen oder in großen Zeichenfolgen sollten Sie immer versuchen, die StringBuilder-Klasse zu verwenden.

CMS
quelle
2

Dieses Beispiel ist wahrscheinlich zu trivial, um einen Unterschied zu bemerken. Tatsächlich denke ich, dass der Compiler in den meisten Fällen jeden Unterschied überhaupt optimieren kann.

Wenn ich jedoch raten müsste, würde ich string.Format()einen Vorteil für kompliziertere Szenarien bieten. Aber das ist eher ein Bauchgefühl, dass es wahrscheinlich besser ist, einen Puffer zu verwenden, anstatt mehrere unveränderliche Zeichenfolgen zu erzeugen, und nicht auf realen Daten basiert.

Joel Coehoorn
quelle
1

Ich stimme vielen der oben genannten Punkte zu. Ein weiterer Punkt, von dem ich glaube, dass er erwähnt werden sollte, ist die Wartbarkeit des Codes. string.Format erleichtert das Ändern von Code.

dh ich habe eine Nachricht "The user is not authorized for location " + locationoder "The User is not authorized for location {0}"

wenn ich jemals die Nachricht ändern wollte, um zu sagen: location + " does not allow this User Access"oder "{0} does not allow this User Access"

mit string.Format muss ich nur den String ändern. Zur Verkettung muss ich diese Nachricht ändern

Bei Verwendung an mehreren Orten kann viel Zeit gespart werden.

Deankarn
quelle
1

Ich hatte den Eindruck, dass string.format schneller war und in diesem Test 3 x langsamer zu sein scheint

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 dauerte 4,6 Sekunden und bei Verwendung von '+' 1,6 Sekunden.

Kitemark76
quelle
7
Der Compiler erkennt "1" + "2" + "3" + "4" + "5" + "6" + "7" + "8" + "9" + "10"als ein String-Literal, so dass die Zeile effektiv "12345678910" + ischneller wird als die vorherigestring.Format(...)
wertzui
0

string.Format ist wahrscheinlich die bessere Wahl, wenn die Formatvorlage ("C {0}") in einer Konfigurationsdatei (z. B. Web.config / App.config) gespeichert ist.

Andrei Rînea
quelle
0

Ich habe ein bisschen verschiedene Zeichenfolgenmethoden profiliert, einschließlich string.Format, StringBuilder und String-Verkettung. Die Verkettung von Zeichenfolgen übertraf fast immer die anderen Methoden zum Erstellen von Zeichenfolgen. Wenn also Leistung der Schlüssel ist, ist es besser. Wenn die Leistung jedoch nicht kritisch ist, finde ich string.Format im Code einfacher zu befolgen. (Aber das ist ein subjektiver Grund) StringBuilder ist jedoch wahrscheinlich am effizientesten in Bezug auf die Speichernutzung.

dviljoen
quelle
0

Ich bevorzuge String.Format in Bezug auf die Leistung

Farzad J.
quelle
-1

Die Verkettung von Zeichenfolgen benötigt im Vergleich zu String.Format mehr Speicher. Die beste Möglichkeit, Zeichenfolgen zu verketten, ist die Verwendung von String.Format oder System.Text.StringBuilder Object.

Nehmen wir den ersten Fall: "C" + rowIndex.ToString () Nehmen wir an, dass rowIndex ein Werttyp ist, sodass die ToString () -Methode Box muss, um den Wert in String zu konvertieren, und dann erstellt CLR Speicher für den neuen String, wobei beide Werte enthalten sind.

Wobei als Zeichenfolge.Format Objektparameter erwartet und rowIndex als Objekt aufnimmt und es intern natürlich in Zeichenfolge konvertiert, wird es Boxing geben, aber es ist intrinsisch und es wird auch nicht so viel Speicher beanspruchen wie im ersten Fall.

Für kurze Saiten ist es nicht so wichtig, denke ich ...


quelle