Ile obiektów typu String zostanie utworzonych przy użyciu znaku plus?

115

Ile obiektów typu String zostanie utworzonych przy użyciu znaku plus w poniższym kodzie?

String result = "1" + "2" + "3" + "4";

Gdyby było tak, jak poniżej, powiedziałbym trzy obiekty typu String: „1”, „2”, „12”.

String result = "1" + "2";

Wiem również, że obiekty String są buforowane w puli / tabeli String Intern w celu poprawy wydajności, ale nie o to chodzi.

Światło
źródło
Ciągi są internowane tylko wtedy, gdy jawnie wywołasz String.Intern.
Joe White
7
@JoeWhite: czy oni?
Igor Korkhov
13
Nie do końca. Wszystkie literały łańcuchowe są automatycznie internowane. Wyniki operacji na łańcuchach nie są.
Stefan Paul Noack
Co więcej, w przykładzie OP jest tylko jedna stała łańcuchowa i jest ona internowana. Zaktualizuję odpowiedź, aby zilustrować.
Chris Shain
+1. Aby zapoznać się z rzeczywistym przykładem potrzeby zakodowania kategorii ciągów znaków w tym stylu, w sekcji Przykłady witryny msdn.microsoft.com/en-us/library/… znajduje się taka, która nie byłaby możliwa, gdyby kompilator nie mógł jej zoptymalizować do jednej stałej, ze względu na ograniczenia wartości przypisanych do parametrów atrybutów.
ClickRick,

Odpowiedzi:

161

O dziwo, to zależy.

Jeśli zrobisz to metodą:

void Foo() {
    String one = "1";
    String two = "2";
    String result = one + two + "34";
    Console.Out.WriteLine(result);
}

wtedy kompilator wydaje się emitować kod używając String.Concatodpowiedzi @Joachim (+1 do niego btw).

Jeśli zdefiniujesz je jako stałe , np .:

const String one = "1";
const String two = "2";
const String result = one + two + "34";

lub jako dosłowne , jak w pierwotnym pytaniu:

String result = "1" + "2" + "3" + "4";

wtedy kompilator zoptymalizuje te +znaki. Jest to równoważne z:

const String result = "1234";

Ponadto kompilator usunie zbędne wyrażenia stałe i wyemituje je tylko wtedy, gdy są używane lub ujawniane. Na przykład ten program:

const String one = "1";
const String two = "1";
const String result = one + two + "34";

public static void main(string[] args) {
    Console.Out.WriteLine(result);
}

Generuje tylko jeden ciąg - stałą result(równą „1234”). onei twonie pojawiają się w wynikowym IL.

Pamiętaj, że w czasie wykonywania mogą pojawić się dalsze optymalizacje. Przechodzę tylko przez to, co zostało wyprodukowane przez IL.

Wreszcie, jeśli chodzi o internowanie, stałe i literały są internowane, ale wartość, która jest internowana, jest wynikową stałą wartością w IL, a nie literałem. Oznacza to, że możesz otrzymać nawet mniej obiektów łańcuchowych, niż się spodziewasz, ponieważ wiele identycznie zdefiniowanych stałych lub literałów będzie w rzeczywistości tym samym obiektem! Ilustrują to następujące elementy:

public class Program
{
    private const String one = "1";
    private const String two = "2";
    private const String RESULT = one + two + "34";

    static String MakeIt()
    {
        return "1" + "2" + "3" + "4";
    }   

    static void Main(string[] args)
    {
        string result = "1" + "2" + "34";

        // Prints "True"
        Console.Out.WriteLine(Object.ReferenceEquals(result, MakeIt()));

        // Prints "True" also
        Console.Out.WriteLine(Object.ReferenceEquals(result, RESULT));
        Console.ReadKey();
    }
}

W przypadku, gdy ciągi są łączone w pętli (lub w inny sposób dynamicznie), otrzymujesz jeden dodatkowy ciąg na konkatenację. Na przykład poniższy rysunek tworzy 12 instancji ciągu: 2 stałe + 10 iteracji, z których każda daje nową instancję ciągu:

public class Program
{
    static void Main(string[] args)
    {
        string result = "";
        for (int i = 0; i < 10; i++)
            result += "a";
        Console.ReadKey();
    }
}

Jednak (co jest również zaskakujące), kompilator łączy wiele kolejnych konkatenacji w jedną konkatenację z wieloma ciągami. Na przykład ten program również tworzy tylko 12 instancji łańcuchów! Dzieje się tak, ponieważ „ Nawet jeśli używasz kilku operatorów + w jednej instrukcji, treść ciągu jest kopiowana tylko raz ”.

public class Program
{
    static void Main(string[] args)
    {
        string result = "";
        for (int i = 0; i < 10; i++)
            result += "a" + result;
        Console.ReadKey();
    }
}
Chris Shain
źródło
a co z wynikiem typu String = „1” + „2” + trzy + cztery; gdzie dwa i trzy są zadeklarowane jak łańcuch trzy = "3"; Ciąg cztery = "4" ;?
The Light
Nawet to daje jeden ciąg. Po prostu przepuściłem to przez LinqPad, aby sprawdzić się dwukrotnie.
Chris Shain
1
@Servy - wydaje się, że komentarz został zaktualizowany. Kiedy zmieniasz komentarz, nie jest on oznaczany jako zmieniany.
Security Hound
1
Jednym z przypadków, które warto rozważyć, aby uzyskać kompletność, jest konkatenacja w pętli. Np. Ile obiektów łańcuchowych alokuje następujący kod:string s = ""; for (int i = 0; i < n; i++) s += "a";
Joren
1
Używam LINQPad ( linqpad.net ) lub Reflector ( reflector.net ). Pierwsza pokazuje ilu dowolnych fragmentów kodu, druga dekompiluje zestawy na IL i może ponownie wygenerować równoważny C # z tego IL. Istnieje również wbudowane narzędzie o nazwie ILDASM ( msdn.microsoft.com/en-us/library/f7dy01k1(v=vs.80).aspx ) Zrozumienie języka IL jest trudne - patrz codebetter.com/raymondlewallen/2005/ 02/07 /…
Chris Shain
85

Odpowiedź Chrisa Shaina jest bardzo dobra. Jako osoba, która napisała optymalizator konkatenacji ciągów, dodałbym tylko dwa dodatkowe interesujące punkty.

Po pierwsze, optymalizator konkatenacji zasadniczo ignoruje zarówno nawiasy, jak i lewe skojarzenie, jeśli może to zrobić bezpiecznie. Załóżmy, że masz metodę M (), która zwraca ciąg. Jeśli powiesz:

string s = M() + "A" + "B";

następnie kompilator wnioskuje, że operator dodawania jest pozostawiony asocjacyjny, a zatem jest to to samo, co:

string s = ((M() + "A") + "B");

Ale to:

string s = "C" + "D" + M();

jest taki sam jak

string s = (("C" + "D") + M());

więc to jest konkatenacja stałego ciągu "CD" z M().

W rzeczywistości optymalizator konkatenacji zdaje sobie sprawę, że konkatenacja ciągów jest asocjacyjna i generuje String.Concat(M(), "AB")dla pierwszego przykładu, nawet jeśli narusza to lewe skojarzenie.

Możesz nawet to zrobić:

string s = (M() + "E") + ("F" + M()));

i nadal będziemy generować String.Concat(M(), "EF", M()).

Drugim interesującym punktem jest to, że puste i puste ciągi są zoptymalizowane. Więc jeśli to zrobisz:

string s = (M() + "") + (null + M());

dostaniesz String.Concat(M(), M())

Pojawia się wtedy interesujące pytanie: co z tym?

string s = M() + null;

Nie możemy tego zoptymalizować do

string s = M();

ponieważ M()może zwrócić wartość null, ale String.Concat(M(), null)zwróci pusty ciąg, jeśli M()zwróci wartość null. Więc zamiast tego zmniejszamy

string s = M() + null;

do

string s = M() ?? "";

W ten sposób wykazanie, że konkatenacja ciągów nie musi w rzeczywistości String.Concatw ogóle wywoływać .

Więcej informacji na ten temat znajdziesz w

Dlaczego String.Concat nie jest zoptymalizowany do StringBuilder.Append?

Eric Lippert
źródło
Myślę, że mogło tam wpaść kilka błędów. Z pewnością ("C" + "D") + M())generuje String.Concat("CD", M()), a nie String.Concat(M(), "AB"). A niżej (M() + "E") + (null + M())powinien generować String.Concat(M(), "E", M()), a nie String.Concat(M(), M()).
hammar
21
+1 za pierwszy akapit. :) Odpowiedzi takie jak ta zawsze mnie zadziwiają w przypadku przepełnienia stosu.
brichins
23

Znalazłem odpowiedź w MSDN. Jeden.

Instrukcje: łączenie wielu ciągów (przewodnik programowania w języku C #)

Konkatenacja to proces dołączania jednego ciągu na końcu innego. W przypadku konkatenacji literałów ciągów lub stałych ciągów przy użyciu operatora + kompilator tworzy pojedynczy ciąg. Nie występuje konkatenacja w czasie wykonywania. Jednak zmienne łańcuchowe można łączyć tylko w czasie wykonywania. W takim przypadku należy rozumieć wpływ różnych podejść na wydajność.

David
źródło
22

Tylko jeden. Kompilator C # zwinie stałe ciągów i dlatego zasadniczo kompiluje się do

String result = "1234";
JaredPar
źródło
Pomyślałem, że ilekroć użyjesz „”, utworzy obiekt String.
The Light
1
@William ogólnie tak. Ale ciągłe pasowanie usunie niepotrzebne kroki pośrednie
JaredPar
13

Wątpię, czy jest to wymagane przez jakąkolwiek normę lub specyfikację. Jedna wersja może prawdopodobnie robić coś innego niż inna.

Nędzna zmienna
źródło
3
Jest to udokumentowane zachowanie przynajmniej dla kompilatora C # firmy Microsoft dla VS 2008 i 2010 (patrz odpowiedź @ David-Stratton). To powiedziawszy, masz rację - o ile mogę stwierdzić po szybkim przejrzeniu, specyfikacja C # nie określa tego i prawdopodobnie należy ją uznać za szczegół implementacji.
Chris Shain,
13

Po pierwsze, ponieważ są statyczne, kompilator będzie w stanie zoptymalizować je do pojedynczego ciągu w czasie kompilacji.

Gdyby były dynamiczne, zostałyby zoptymalizowane pod kątem pojedynczego wywołania String.Concat (string, string, string, string) .

Joachim Isaksson
źródło