Typ odwołania do ciągu C #?

164

Wiem, że „string” w C # to typ referencyjny. To jest w MSDN. Jednak ten kod nie działa tak, jak powinien:

class Test
{
    public static void Main()
    {
        string test = "before passing";
        Console.WriteLine(test);
        TestI(test);
        Console.WriteLine(test);
    }

    public static void TestI(string test)
    {
        test = "after passing";
    }
}

Wyjście powinno być „przed przekazaniem” „po przekazaniu”, ponieważ przekazuję ciąg jako parametr i jest to typ referencyjny, druga instrukcja wyjściowa powinna rozpoznawać, że tekst zmienił się w metodzie TestI. Jednak otrzymuję „przed zdaniem” „przed zdaniem”, co sprawia wrażenie, że jest przekazywana przez wartość, a nie przez odniesienie. Rozumiem, że ciągi znaków są niezmienne, ale nie rozumiem, jak to wyjaśniałoby, co się tutaj dzieje. czego mi brakuje? Dzięki.


źródło
Zobacz artykuł, do którego odnosi się Jon poniżej. Wspomniane zachowanie można również odtworzyć za pomocą wskaźników C ++.
Sesh,
Bardzo ładne wyjaśnienie również w MSDN .
Dimi_Pel

Odpowiedzi:

211

Odniesienie do ciągu jest przekazywane przez wartość. Istnieje duża różnica między przekazywaniem odwołania przez wartość a przekazywaniem obiektu przez odwołanie. Szkoda, że ​​w obu przypadkach użyto słowa „odniesienie”.

Jeśli zrobić przekazać odwołanie ciąg przez odniesienie, to będzie działać zgodnie z oczekiwaniami:

using System;

class Test
{
    public static void Main()
    {
        string test = "before passing";
        Console.WriteLine(test);
        TestI(ref test);
        Console.WriteLine(test);
    }

    public static void TestI(ref string test)
    {
        test = "after passing";
    }
}

Teraz musisz rozróżnić pomiędzy wprowadzeniem zmian w obiekcie, do którego odwołuje się odniesienie, a dokonaniem zmiany w zmiennej (takiej jak parametr), aby umożliwić odwołanie się do innego obiektu. Nie możemy wprowadzać zmian w ciągu, ponieważ ciągi są niezmienne, ale StringBuilderzamiast tego możemy to zademonstrować :

using System;
using System.Text;

class Test
{
    public static void Main()
    {
        StringBuilder test = new StringBuilder();
        Console.WriteLine(test);
        TestI(test);
        Console.WriteLine(test);
    }

    public static void TestI(StringBuilder test)
    {
        // Note that we're not changing the value
        // of the "test" parameter - we're changing
        // the data in the object it's referring to
        test.Append("changing");
    }
}

Więcej szczegółów znajdziesz w moim artykule na temat przekazywania parametrów .

Jon Skeet
źródło
2
zgadzam się, chcę tylko wyjaśnić, że użycie modyfikatora ref działa również dla typów niereferencyjnych, tj. oba są dość osobnymi koncepcjami.
eglasius
2
@Jon Skeet uwielbiał sidenote w Twoim artykule. Powinieneś to mieć referencedjako odpowiedź
Nithish Inpursuit Ofhappiness
36

Jeśli musimy odpowiedzieć na pytanie: String jest typem referencyjnym i zachowuje się jak referencja. Przekazujemy parametr, który zawiera odniesienie, a nie rzeczywisty ciąg. Problem tkwi w funkcji:

public static void TestI(string test)
{
    test = "after passing";
}

Parametr testzawiera odniesienie do ciągu, ale jest kopią. Mamy dwie zmienne wskazujące na łańcuch. A ponieważ każda operacja na łańcuchach w rzeczywistości tworzy nowy obiekt, tworzymy lokalną kopię tak, aby wskazywała na nowy łańcuch. Ale oryginalna testzmienna nie ulega zmianie.

Proponowane rozwiązania, które należy umieścić refw deklaracji funkcji i przy wywołaniu działają, ponieważ nie przekażemy wartości testzmiennej, ale przekażemy tylko odniesienie do niej. Zatem wszelkie zmiany wewnątrz funkcji będą odzwierciedlać oryginalną zmienną.

Chcę powtórzyć na końcu: String jest typem referencyjnym, ale ponieważ jest niezmienny, linia test = "after passing";faktycznie tworzy nowy obiekt, a nasza kopia zmiennej testjest zmieniana tak, aby wskazywała na nowy ciąg.

Martin Dimitrov
źródło
25

Jak powiedzieli inni, Stringtyp w .NET jest niezmienny, a jego odwołanie jest przekazywane przez wartość.

W oryginalnym kodzie, gdy tylko ta linia zostanie wykonana:

test = "after passing";

potem testjuż nie odnosząc się do oryginalnego obiektu. Utworzyliśmy nowy String obiekt i przypisaliśmy go testdo odwoływania się do tego obiektu na zarządzanym stercie.

Wydaje mi się, że wiele osób się tutaj potyka, ponieważ nie ma widocznego formalnego konstruktora, który mógłby im o tym przypomnieć. W tym przypadku dzieje się to za kulisami od czasuString typ ma wsparcie językowe w sposobie jego budowy.

Dlatego właśnie zmiana na testnie jest widoczna poza zakresem TestI(string)metody - przekazaliśmy odwołanie według wartości i teraz ta wartość się zmieniła! Ale jeśli Stringreferencja została przekazana przez referencję, to kiedy referencja ulegnie zmianie, zobaczymy ją poza zakresem TestI(string)metody.

Albo ref lub out hasła są potrzebne w tym przypadku. Wydaje mi się, że outsłowo kluczowe może być nieco lepiej dopasowane do tej konkretnej sytuacji.

class Program
{
    static void Main(string[] args)
    {
        string test = "before passing";
        Console.WriteLine(test);
        TestI(out test);
        Console.WriteLine(test);
        Console.ReadLine();
    }

    public static void TestI(out string test)
    {
        test = "after passing";
    }
}
Derek W
źródło
ref = zainicjalizowana poza funkcją, out = zainicjalizowana wewnątrz funkcji lub innymi słowy; ref jest dwukierunkowe, out jest tylko out. Z pewnością należy użyć ref.
Paul Zahra
@PaulZahra: outmusi być przypisane w ramach metody, aby kod mógł zostać skompilowany. refnie ma takiego wymagania. Również outparametry są inicjalizowane poza metodą - kod w tej odpowiedzi jest kontrprzykładem.
Derek W
Powinno wyjaśnić - outparametry można inicjalizować poza metodą, ale nie trzeba. W tym przypadku chcemy zainicjować outparametr, aby zademonstrować kwestię natury stringtypu w .NET.
Derek W
9

Właściwie byłoby tak samo dla każdego obiektu w tej kwestii, tj. Bycie typem referencyjnym i przekazywanie przez referencję to dwie różne rzeczy w języku C #.

To zadziała, ale ma to zastosowanie niezależnie od typu:

public static void TestI(ref string test)

Również o tym, że string jest typem referencyjnym, jest również specjalny. Został zaprojektowany tak, aby był niezmienny, więc wszystkie jego metody nie modyfikują instancji (zwracają nową). Ma też kilka dodatkowych elementów do wydajności.

eglasius
źródło
7

Oto dobry sposób na zastanowienie się nad różnicą między typami wartości, przekazywaniem przez wartość, typami odwołań i przekazywaniem przez odwołanie:

Zmienna to kontener.

Zmienna typu wartości zawiera instancję. Zmienna typu referencyjnego zawiera wskaźnik do instancji przechowywanej w innym miejscu.

Modyfikacja zmiennej typu wartości powoduje mutację wystąpienia, które zawiera. Modyfikacja zmiennej typu odwołania powoduje mutację wystąpienia, na które wskazuje.

Oddzielne zmienne typu referencyjnego mogą wskazywać na to samo wystąpienie. Dlatego ta sama instancja może zostać zmutowana za pomocą dowolnej zmiennej, która na nią wskazuje.

Argument przekazany przez wartość to nowy kontener z nową kopią zawartości. Argument przekazany przez odwołanie to oryginalny kontener z oryginalną zawartością.

Gdy argument typu wartości jest przekazywany przez wartość: ponowne przypisanie zawartości argumentu nie ma skutku poza zakresem, ponieważ kontener jest unikalny. Modyfikacja argumentu nie ma skutku poza zakresem, ponieważ instancja jest niezależną kopią.

Gdy argument typu odwołania jest przekazywany według wartości: ponowne przypisanie zawartości argumentu nie ma wpływu poza zakresem, ponieważ kontener jest unikalny. Modyfikacja zawartości argumentu wpływa na zakres zewnętrzny, ponieważ skopiowany wskaźnik wskazuje na współdzielone wystąpienie.

Gdy jakikolwiek argument jest przekazywany przez odwołanie: ponowne przypisanie zawartości argumentu ma wpływ na zakres zewnętrzny, ponieważ kontener jest współużytkowany. Modyfikacja treści argumentu wpływa na zakres zewnętrzny, ponieważ zawartość jest udostępniana.

Podsumowując:

Zmienna łańcuchowa jest zmienną typu referencyjnego. Dlatego zawiera wskaźnik do instancji przechowywanej w innym miejscu. Po przekazaniu przez wartość jego wskaźnik jest kopiowany, więc modyfikacja argumentu ciągu powinna wpływać na współdzielone wystąpienie. Jednak wystąpienie ciągu nie ma modyfikowalnych właściwości, więc argument ciągu nie może zostać zmodyfikowany. Po przekazaniu przez odwołanie kontener wskaźnika jest współużytkowany, więc ponowne przypisanie nadal będzie miało wpływ na zakres zewnętrzny.

Bryan
źródło
6

Obraz jest wart tysiąca słów ”.

Mam tutaj prosty przykład, jest podobny do twojego przypadku.

string s1 = "abc";
string s2 = s1;
s1 = "def";
Console.WriteLine(s2);
// Output: abc

To jest to, co się stało:

wprowadź opis obrazu tutaj

  • Wiersze 1 i 2: s1i s2zmienne odnoszą się do tego samego "abc"obiektu łańcuchowego.
  • Linia 3: Ponieważ łańcuchy są niezmienne , więc "abc"obiekt ciągu nie modyfikuje się (do "def"), ale "def"zamiast tego tworzony jest nowy obiekt ciągu, a następnie s1odniesienia do niego.
  • Linia 4: s2nadal odwołuje się do "abc"obiektu string, więc to jest wynik.
Messi
źródło
5

Powyższe odpowiedzi są pomocne, chciałbym tylko dodać przykład, który moim zdaniem jasno pokazuje, co się dzieje, gdy przekazujemy parametr bez słowa kluczowego ref, nawet jeśli ten parametr jest typem referencyjnym:

MyClass c = new MyClass(); c.MyProperty = "foo";

CNull(c); // only a copy of the reference is sent 
Console.WriteLine(c.MyProperty); // still foo, we only made the copy null
CPropertyChange(c); 
Console.WriteLine(c.MyProperty); // bar


private void CNull(MyClass c2)
        {          
            c2 = null;
        }
private void CPropertyChange(MyClass c2) 
        {
            c2.MyProperty = "bar"; // c2 is a copy, but it refers to the same object that c does (on heap) and modified property would appear on c.MyProperty as well.
        }
BornToCode
źródło
1
To wyjaśnienie zadziałało dla mnie najlepiej. Tak więc w zasadzie przekazujemy wszystko według wartości, pomimo faktu, że sama zmienna jest typu wartościowego lub referencyjnego, chyba że użyjemy słowa kluczowego ref (lub out). Nie jest to widoczne w naszym codziennym kodowaniu, ponieważ generalnie nie ustawiamy naszych obiektów na null lub na inne wystąpienie w ramach metody, do której zostały przekazane, a raczej ustawiamy ich właściwości lub wywołujemy ich metody. W przypadku „string” ustawianie go na nową instancję ma miejsce przez cały czas, ale nowe ustawienie nie jest widoczne, co daje fałszywą interpretację niewprawnemu oku. Popraw mnie, jeśli się mylisz.
Ε Г И І И О
3

Dla ciekawskich i dla zakończenia rozmowy: Tak, String jest typem referencyjnym :

unsafe
{
     string a = "Test";
     string b = a;
     fixed (char* p = a)
     {
          p[0] = 'B';
     }
     Console.WriteLine(a); // output: "Best"
     Console.WriteLine(b); // output: "Best"
}

Pamiętaj jednak, że ta zmiana działa tylko w niebezpiecznym bloku! ponieważ ciągi są niezmienne (z MSDN):

Zawartość obiektu łańcuchowego nie może zostać zmieniona po utworzeniu obiektu, chociaż składnia sprawia, że ​​wygląda na to, że można to zrobić. Na przykład podczas pisania tego kodu kompilator faktycznie tworzy nowy obiekt ciągu do przechowywania nowej sekwencji znaków, a ten nowy obiekt jest przypisywany do b. Ciąg „h” kwalifikuje się wtedy do czyszczenia pamięci.

string b = "h";  
b += "ello";  

I pamiętaj, że:

Chociaż ciąg jest typem referencyjnym, operatory równości ( ==i !=) są zdefiniowane w celu porównania wartości obiektów ciągu, a nie odwołań.

NY
źródło
0

Uważam, że Twój kod jest analogiczny do poniższego i nie powinieneś oczekiwać, że wartość zmieni się z tego samego powodu, dla którego nie byłoby to tutaj:

 public static void Main()
 {
     StringWrapper testVariable = new StringWrapper("before passing");
     Console.WriteLine(testVariable);
     TestI(testVariable);
     Console.WriteLine(testVariable);
 }

 public static void TestI(StringWrapper testParameter)
 {
     testParameter = new StringWrapper("after passing");

     // this will change the object that testParameter is pointing/referring
     // to but it doesn't change testVariable unless you use a reference
     // parameter as indicated in other answers
 }
Dave Cousineau
źródło
-1

Próbować:


public static void TestI(ref string test)
    {
        test = "after passing";
    }
Marius Kjeldahl
źródło
3
Twoja odpowiedź powinna zawierać więcej niż tylko kod. Powinien również zawierać wyjaśnienie, dlaczego to działa.
Charles Caldwell