W jaki sposób przekazywane są ciągi znaków w .NET?

121

Kiedy przekazuję a stringdo funkcji, czy wskaźnik do zawartości ciągu jest przekazywany, czy też cały ciąg jest przekazywany do funkcji na stosie, tak jak structbyłby to?

Cole Johnson
źródło

Odpowiedzi:

278

Odwołanie jest przekazywane; jednak nie jest to technicznie przekazywane przez odniesienie. To subtelne, ale bardzo ważne rozróżnienie. Rozważ następujący kod:

void DoSomething(string strLocal)
{
    strLocal = "local";
}
void Main()
{
    string strMain = "main";
    DoSomething(strMain);
    Console.WriteLine(strMain); // What gets printed?
}

Aby zrozumieć, co się tutaj dzieje, musisz wiedzieć trzy rzeczy:

  1. Ciągi są typami odwołań w C #.
  2. Są również niezmienne, więc za każdym razem, gdy robisz coś, co wygląda tak, jakbyś zmieniał ciąg, tak nie jest. Tworzony jest zupełnie nowy ciąg, wskazywany jest na niego odnośnik, a stary jest wyrzucany.
  3. Mimo że ciągi są typami referencyjnymi, strMainnie są przekazywane przez odwołanie. Jest to typ referencyjny, ale samo odwołanie jest przekazywane przez wartość . Za każdym razem, gdy przekazujesz parametr bez refsłowa kluczowego (nie licząc outparametrów), przekazujesz coś według wartości.

To musi oznaczać, że ... przekazujesz odwołanie według wartości. Ponieważ jest to typ referencyjny, tylko referencja została skopiowana na stos. Ale co to znaczy?

Przekazywanie typów referencyjnych według wartości: już to robisz

Zmienne C # są typami odwołań lub typami wartości . Parametry języka C # są przekazywane przez odwołanie lub przez wartość . Terminologia jest tutaj problemem; brzmią jak to samo, ale tak nie jest.

Jeśli przekazujesz parametr DOWOLNEGO typu i nie używasz refsłowa kluczowego, to przekazujesz go według wartości. Jeśli przekazałeś to według wartości, tak naprawdę przekazałeś kopię. Ale jeśli parametr był typem referencyjnym, to skopiowana rzecz była referencją, a nie tym, na co wskazywała.

Oto pierwsza linia Mainmetody:

string strMain = "main";

Stworzyliśmy w tej linii dwie rzeczy: ciąg znaków z wartością mainprzechowywaną gdzieś w pamięci oraz zmienną referencyjną o nazwie strMainwskazującą na nią.

DoSomething(strMain);

Teraz przekazujemy to odniesienie do DoSomething. Przekazaliśmy to według wartości, więc oznacza to, że utworzyliśmy kopię. Jest to typ referencyjny, co oznacza, że ​​skopiowaliśmy referencję, a nie sam ciąg. Teraz mamy dwa odniesienia, z których każdy wskazuje na tę samą wartość w pamięci.

Wewnątrz callee

Oto początek DoSomethingmetody:

void DoSomething(string strLocal)

Brak refsłowa kluczowego, więc strLocali strMainsą dwa różne odwołania wskazujące tę samą wartość. Jeśli zmienimy przypisanie strLocal...

strLocal = "local";   

... nie zmieniliśmy przechowywanej wartości; wzięliśmy odwołanie o nazwie strLocali wycelowaliśmy go w zupełnie nowy ciąg. Co się stanie, strMaingdy to zrobimy? Nic. Wciąż wskazuje na stary sznurek.

string strMain = "main";    // Store a string, create a reference to it
DoSomething(strMain);       // Reference gets copied, copy gets re-pointed
Console.WriteLine(strMain); // The original string is still "main" 

Niezmienność

Zmieńmy na chwilę scenariusz. Wyobraź sobie, że nie pracujemy z ciągami, ale jakimś zmiennym typem referencyjnym, takim jak utworzona przez Ciebie klasa.

class MutableThing
{
    public int ChangeMe { get; set; }
}

Jeśli podążasz za odniesieniem objLocaldo obiektu, na który wskazuje, możesz zmienić jego właściwości:

void DoSomething(MutableThing objLocal)
{
     objLocal.ChangeMe = 0;
} 

W MutableThingpamięci wciąż jest tylko jeden i zarówno skopiowane odniesienie, jak i oryginalne odniesienie nadal do niego wskazują. Właściwości MutableThingsamego siebie uległy zmianie :

void Main()
{
    var objMain = new MutableThing();
    objMain.ChangeMe = 5; 
    Console.WriteLine(objMain.ChangeMe); // it's 5 on objMain

    DoSomething(objMain);                // now it's 0 on objLocal
    Console.WriteLine(objMain.ChangeMe); // it's also 0 on objMain   
}

Ach, ale struny są niezmienne! Nie ma ChangeMewłaściwości do ustawienia. Nie możesz tego zrobić strLocal[3] = 'H'w C # tak, jak w przypadku chartablicy w stylu C ; musisz zamiast tego skonstruować cały nowy ciąg. Jedynym sposobem zmiany strLocaljest wskazanie referencji na inny ciąg, a to oznacza, że ​​nic, co robisz, nie ma strLocalwpływu strMain. Wartość jest niezmienna, a odwołanie jest kopią.

Przekazywanie referencji przez referencje

Aby udowodnić, że istnieje różnica, oto co się dzieje, gdy przekazujesz referencję przez referencję:

void DoSomethingByReference(ref string strLocal)
{
    strLocal = "local";
}
void Main()
{
    string strMain = "main";
    DoSomethingByReference(ref strMain);
    Console.WriteLine(strMain);          // Prints "local"
}

Tym razem ciąg w Mainnaprawdę się zmienia, ponieważ przekazałeś referencję bez kopiowania jej na stos.

Więc nawet jeśli łańcuchy są typami referencyjnymi, przekazywanie ich przez wartość oznacza, że ​​cokolwiek dzieje się w wywoływanym, nie wpłynie na ciąg w wywołującym. Ale ponieważ są to typy referencyjne, nie musisz kopiować całego ciągu do pamięci, gdy chcesz go przekazać.

Dalsze zasoby:

Justin Morgan
źródło
3
@TheLight - Przepraszamy, ale mylisz się, mówiąc: „Typ referencyjny jest domyślnie przekazywany przez referencję”. Domyślnie wszystkie parametry są przekazywane według wartości, ale w przypadku typów odwołań oznacza to, że odwołanie jest przekazywane przez wartość. Mylisz typy referencyjne z parametrami referencyjnymi, co jest zrozumiałe, ponieważ jest to bardzo mylące rozróżnienie. Zobacz sekcję Przekazywanie typów odwołań według wartości tutaj. Twój linkowany artykuł jest całkiem poprawny, ale faktycznie potwierdza mój punkt widzenia.
Justin Morgan,
1
@JustinMorgan Nie po to, żeby wywoływać martwy wątek komentarzy, ale myślę, że komentarz TheLight ma sens, jeśli myślisz w C. W języku C dane to tylko blok pamięci. Odniesienie jest wskaźnikiem do tego bloku pamięci. Przekazanie całego bloku pamięci do funkcji nazywa się „przekazywaniem przez wartość”. Przekazywanie wskaźnika nazywa się „przekazywaniem przez referencję”. W C # nie ma pojęcia przekazywania w całym bloku pamięci, więc przedefiniowali „przekazywanie przez wartość”, aby oznaczać przekazywanie wskaźnika do środka. Wydaje się to błędne, ale wskaźnik jest też tylko blokiem pamięci! Dla mnie terminologia jest dość arbitralna
rliu
@roliu - Problem polega na tym, że nie pracujemy w C, a C # jest skrajnie inny pomimo podobnej nazwy i składni. Po pierwsze, odniesienia to nie to samo, co wskaźniki , a myślenie o nich w ten sposób może prowadzić do pułapek. Największym problemem jest jednak to, że „przekazywanie przez odwołanie” ma bardzo specyficzne znaczenie w języku C #, wymagając refsłowa kluczowego. Aby udowodnić, że przekazywanie przez odniesienie robi różnicę, obejrzyj to demo: rextester.com/WKBG5978
Justin Morgan,
1
@JustinMorgan Zgadzam się, że mieszanie terminologii C i C # jest złe, ale chociaż podobał mi się post lipperta, nie zgadzam się, że myślenie o odwołaniach jako wskaźnikach szczególnie zamglenia cokolwiek tutaj. W poście na blogu opisano, jak myślenie o odwołaniu jako wskaźniku daje mu zbyt dużą moc. Zdaję sobie sprawę, że refsłowo kluczowe ma użyteczność, próbowałem tylko wyjaśnić, dlaczego można pomyśleć o przekazywaniu typu referencyjnego przez wartość w C # wydaje się być "tradycyjnym" (tj. C) pojęciem przekazywania przez referencję (i przekazywania typu referencyjnego przez odwołanie w C # wydaje się bardziej przypominać przekazywanie odwołania do odwołania według wartości).
rliu
2
Masz rację, ale myślę, że @roliu odnosił się do tego, jak funkcja taka Foo(string bar)może być traktowana jako Foo(char* bar)podczas gdy Foo(ref string bar)byłaby Foo(char** bar)( Foo(char*& bar)lub Foo(string& bar)w C ++). Jasne, nie tak powinieneś o tym myśleć każdego dnia, ale tak naprawdę pomogło mi to w końcu zrozumieć, co dzieje się pod maską.
Cole Johnson,
23

Ciągi w języku C # są niezmiennymi obiektami referencyjnymi. Oznacza to, że odniesienia do nich są przekazywane (według wartości), a po utworzeniu ciągu nie można go modyfikować. Metody, które tworzą zmodyfikowane wersje ciągu (podciągi, wersje przycięte itp.) Tworzą zmodyfikowane kopie oryginalnego ciągu.

dasblinkenlight
źródło
10

Ciągi znaków to szczególne przypadki. Każda instancja jest niezmienna. Kiedy zmieniasz wartość ciągu, przydzielasz nowy ciąg w pamięci.

Więc tylko referencja jest przekazywana do twojej funkcji, ale kiedy ciąg jest edytowany, staje się nową instancją i nie modyfikuje starej instancji.

Enigmativity
źródło
4
W tym aspekcie struny nie są szczególnym przypadkiem. Tworzenie niezmiennych obiektów, które mogą mieć tę samą semantykę, jest bardzo łatwe. (To jest instancja typu, który nie ujawnia metody do jej mutacji ...)
Ciągi znaków są przypadkami specjalnymi - są faktycznie niezmiennymi typami odwołań, które wydają się być zmienne, ponieważ zachowują się jak typy wartości.
Enigmativity
1
@Enigmativity Zgodnie z tą logiką, wtedy Uri(klasa) i Guid(struktura) są również przypadkami specjalnymi. Nie widzę, jak System.Stringdziała jak „typ wartości” bardziej niż inne niezmienne typy… pochodzenia klasowego lub strukturalnego.
3
@pst - Ciągi mają specjalną semantykę tworzenia - w przeciwieństwie do Uri& Guid- możesz po prostu przypisać wartość literału ciągu do zmiennej ciągu. Ciąg wydaje się być zmienny, jak intprzypisanie, ale niejawnie tworzy obiekt - bez newsłowa kluczowego.
Enigmativity
3
Ciąg to szczególny przypadek, ale nie ma to związku z tym pytaniem. Typ wartości, typ odniesienia, jakikolwiek typ będzie działał tak samo w tym pytaniu.
Kirk Broadhurst