Przekazywanie stringa przez wartość a typ referencyjny (ref)

0

Chciałem się zapytać dlaczego zmienne typu string przekazywane do funkcji bez słowa ref nie są w oryginale modyfikowane? Czy zmienna string nie jest typem referencyjnym?
Przy okazji chciałem się zapytać czy słowo kluczowe ref stosuje się również do zwykłych klas czy jest ono wykorzystywane tylko do typów prostych (int, bool, double etc.)?
Z góry dziękuję za odpowiedź.

5

Jak nie ma słowa ref, to parametr jest przekazywany przez wartość. W przypadku typów wartościowych (dziedziczących po System.ValueType, czyli intów, struktur itp) jest to wartość instancji (czyli cały integer, cała struktura itp). W przypadku typów referencyjnych (dziedziczących po System.Object, czyli używających słówka class) jest to wartość referencji (czyli wskaźnika do instancji).

String jest typem referencyjnym, więc do funkcji przekazujesz wartość referencji, czyli kopiujesz wskaźnik. Jakbyś przekazał stringa przez ref, to przekazałbyś referencję do referencji (czyli wskaźnik do wskaźnika).

Ref możesz też użyć do wszystkich klas i struktur, nie tylko do typów prostych.

2

@Afish polecam prosty przykład:

static async Task Main(string[] args)
{
  string input = "string";
  Console.WriteLine($"In main before method: {input}"); // input ma wartość 'string'

  ModifyAndPrint(input); // Przekazana będzie kopia 'input'
  Console.WriteLine($"In main after method: {input}"); //input ma wartość 'string'

  ModifyRefAndPrint(ref input); // a tu referencja
  Console.WriteLine($"In main after ref method: {input}"); //input ma wartość 'string_ref'
}

static void ModifyAndPrint(string input)
{
  input = input + "_new";
  Console.WriteLine($"From method: {input}"); //input ma wartość 'string_new'
}

static void ModifyRefAndPrint(ref string input)
{
  input = input + "_ref";
  Console.WriteLine($"From ref method: {input}"); // input ma wartość 'string_ref'
}
1

@boska_cebula: i @Kofcio - mieszają się Wam niemutowalnośc stringów i przekazywanie parametrów.

boska_cebula napisał(a):

Dla normalnego typu referencyjnego nie będzie takiego zachowania

String to normalny typ referencyjny.


        private string SavedString { get; set; }
        void Test()
        {
            var s = "Hello, world";
            SavedString = s;
            ChangeString(s);

        }

        public void ChangeString(string s)
        {
            Console.WriteLine(object.ReferenceEquals(SavedString, s)); // s to nie kopia, to ten sam string co w Test
            s = "Goodbye, world"; // przekazana przez wartość referencja zostaje nadpisana nową, to nie ma nic wspólnego z modyfikacją obiektu
        }
    }
6

@boska_cebula: to nie wiesz jak działa string... jako parametr metody przekazywana jest kopia stringa, w zasadzie zachowuje się jak typ wartościowy Bzdura, string jest typem referencyjnym i jest przekazywany do funkcji przez wartość referencji. Poniżej dowody.

  1. Dokumentacja.
    https://docs.microsoft.com/en-us/dotnet/api/system.string?view=netcore-3.1
    Dziedziczy po System.object. To w sumie kończy dowód, ale pewnie niektórych nie przekonuje, więc jedziemy dalej.

  2. Kod po kompilacji
    https://sharplab.io/#v2:C4LghgzgtgNAJiA1AHwAICYCMBYAUKgZgAIMiBhIgbzyNpONQBYiBZACgEoqa7fVMAnGwBEAZWBgATsGEcA3D161FS/gAYiEIgF4iYgK4AHAKaTxkgJYA7AOYAFSBGNwAQgE8ASsYBmp41YBjY2EFXCU6BwgIc2sbNgh5FV4kunN9AOB9SWMiAA8dIitjAHciNIys405Q8NpI6OBJdMzstlzEsLoAXxUVQhJMADYSZnqY2zZxmzyOak7w/iF2mu7e+foB4aYiMcbmysm9iuyZudqBpYA6ZZTaRbbLtw7eHtxXvH6II+Ay78rudb9aw/XIrO7EYFENyhLpAA=

Bierzemy ten kod:

using System;
public class C {
    public void M() {
        Console.WriteLine("Start");
        
        string s = "SuperStringPassedByReference";
        PassString(s);
        
        Structure x = new Structure();
        PassStructure(x);
    }
    
    public static void PassString(String x){
        Console.WriteLine(x);
    }
    
    public static void PassStructure(Structure x){
        Console.WriteLine(x.x);
        Console.WriteLine(x.y);
    }
}

public struct Structure {
    public int x;
    public int y;
}

Gdyby string był wartością, przekazywalibyśmy do funkcji wszystkie znaki. No to zobaczmy IL-a:

        IL_000c: ldstr "SuperStringPassedByReference"
        IL_0011: stloc.0
        IL_0012: ldloc.0
        IL_0013: call void C::PassString(string)

Wygląda jak ładowanie stringa do lokacji 0 na koncepcyjnym stosie (CLR jest maszyną stosową), a potem przekazywanie pierwszego argumentu ze stosu do funkcji. Zobaczmy kod maszynowy:

    L0032: mov ecx, [0x19f3d1a8]
    L0038: mov [ebp-8], ecx
    L003b: mov ecx, [ebp-8]
    L003e: call dword ptr [0x268ac708]

Tryb debug dodaje trochę niepotrzebnego żonglowania. W 0032 ładujemy string z puli literałów binarki, potem żonglujemy między rejestrami, ale ostatecznie w rejestrze ecx ląduje dokładnie jedna wartość, wskaźnik na stringa.

Co ze strukturą?

        IL_0019: ldloca.s 1
        IL_001b: initobj Structure
        IL_0021: ldloc.1
        IL_0022: call void C::PassStructure(valuetype Structure)

Trzymamy ją jako druga wartość na stosie. A pod spodem wywołanie funkcji:

    L0045: xor eax, eax
    L0047: lea edx, [ebp-0x10]
    L004a: mov [edx], eax
    L004c: mov [edx+4], eax
    L004f: vmovq xmm0, [ebp-0x10]
    L0054: sub esp, 8
    L0057: vmovq [esp], xmm0
    L005c: call dword ptr [0x251ac718]

i kod funkcji:

    L0012: mov ecx, [ebp+8]
    L0015: call System.Console.WriteLine(Int32)
    L001a: nop
    L001b: lea ecx, [ebp+8]
    L001e: mov ecx, [ecx+4]
    L0021: call System.Console.WriteLine(Int32)

Linie 004a i 004c inicjalizują strukturę zerami. Potem w linii 004f wczytujemy całą strukturę do rejestru xmm0, a w linii 0057 wrzucamy ją na stos. vmovq przesuwa quadworda, czyli dwa 32-bitowe integery. Potem funkcja jest wywoływana i w linii 0012 widzimy odczyt pierwszego pola ze stosu, a w linii 0013 odczyt drugiego pola ze stosu.

Struktura wymagała przeniesienia dwóch integerów, string tylko jednego. String jest typem referencyjnym, co kończy dowód. Ale że pewnie on nie przekonuje niektórych, to jedziemy dalej.

  1. Co w pamięci piszczy.
    Weźmy Twój kod, zmodyfikujmy dla łatwiejszego debugowania i pobawmy się nim:
static void Main(string[] args)
{
  string input = "string";
  Console.WriteLine($"In main before method: {input}"); // input ma wartość 'string'
  Console.WriteLine("Breakpoint 1");
  Console.ReadLine();

  ModifyAndPrint(input); // Przekazana będzie kopia 'input'
  Console.WriteLine($"In main after method: {input}"); //input ma wartość 'string'
  Console.WriteLine("Breakpoint 4");
  Console.ReadLine();

  ModifyRefAndPrint(ref input); // a tu referencja
  Console.WriteLine($"In main after ref method: {input}"); //input ma wartość 'string_ref'
  Console.WriteLine("Breakpoint 7");
  Console.ReadLine();
}

static void ModifyAndPrint(string input)
{
  Console.WriteLine("Breakpoint 2");
  Console.ReadLine();
  input = input + "_new";
  Console.WriteLine($"From method: {input}"); //input ma wartość 'string_new'
  Console.WriteLine("Breakpoint 3");
  Console.ReadLine();
}

static void ModifyRefAndPrint(ref string input)
{
  Console.WriteLine("Breakpoint 5");
  Console.ReadLine();
  input = input + "_ref";
  Console.WriteLine($"From ref method: {input}"); // input ma wartość 'string_ref'
  Console.WriteLine("Breakpoint 6");
  Console.ReadLine();
}

Kompiluję na W10 x64 jako .NET Core 3.1.101 x86 w trybie debug.

Odpalamy, czekamy na wypisanie "Breakpoint 1", podłączamy WinDBG, robimy load path_to_sos.dll, zmieniamy wątek ~0s~ i robimy !clrstack -a -i. Wynik (obcięty z nudnych elementów):

0317F0F0 076c06be [DEFAULT] Void StringAsReference.Program.Main(SZArray String) (C:\Users\afish\Desktop\msp_windowsinternals\StringAsReference\bin\x86\Debug\netcoreapp3.1\StringAsReference.dll)

PARAMETERS:
  + string[] args   (empty)

LOCALS:
  + string input = "string"

Okej, mamy metodę Main na stosie, mamy lokalną zmienną input. Zobaczmy, jak nasz string jest ładowany:

!name2ee StringAsReference StringAsReference.Program.Main

Module:      04f2e848
Assembly:    StringAsReference.dll
Token:       06000001
MethodDesc:  04f2fc00
Name:        StringAsReference.Program.Main(System.String[])
JITTED Code Address: 076c0660
!U /d 076c0660

076c068a 8b0d68201406    mov     ecx,dword ptr ds:[6142068h] ("string")
076c0690 894df0          mov     dword ptr [ebp-10h],ecx

Czyli widzimy, że string jest wczytywany do [ebp-10h]. To w sumie kończy dowód, no ale lecimy dalej. Ponieważ jest to ebp, a my w tym momencie jesteśmy w innej metodzie, to musimy jakoś odzyskać ten rejestr. Albo bawimy się w ręczne obliczanie, albo podglądamy kod maszynowy, który wygląda jakoś tak:

C:\Users\afish\Desktop\msp_windowsinternals\StringAsReference\Program.cs @ 12:
076c06ad 8b0d70201406    mov     ecx,dword ptr ds:[6142070h] ("Breakpoint 1")
076c06b3 e8e8faffff      call    076c01a0 (System.Console.WriteLine(System.String), mdToken: 06000081)
076c06b8 90              nop

C:\Users\afish\Desktop\msp_windowsinternals\StringAsReference\Program.cs @ 13:
076c06b9 e872faffff      call    076c0130 (System.Console.ReadLine(), mdToken: 06000073)
076c06be 8945e8          mov     dword ptr [ebp-18h],eax
076c06c1 90              nop

W tym momencie jesteśmy w callu z instrukcji 076c06b3 , dodajmy sobie breakpoint do 076c06c1 przez bp 076c06c1 i jak wyjdziemy z funkcji, to będziemy mieli rejestry jak trzeba. Odpalamy przez g i wpisujemy cokolwiek w konsoli (plus enter):

Breakpoint 0 hit
eax=051411c8 ebx=0317f188 ecx=00000000 edx=05155ed4 esi=05149dac edi=0317f10c
eip=076c06c1 esp=0317f0f0 ebp=0317f118 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
076c06c1 90              nop

Mamy rejestr, liczymy 0317f118 - 10 = 317F108. Patrzymy:

dd 317F108

0317f108  0514a6b0 05149dac 0317f130 034094c0

Mamy wskaźnik, patrzymy, co tam jest:

!do 0514a6b0 

Name:        System.String
MethodTable: 04e2acd0
EEClass:     033b9024
Size:        26(0x1a) bytes
File:        C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App\3.1.1\System.Private.CoreLib.dll
String:      string
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
04e2697c  4000242        4         System.Int32  1 instance        6 _stringLength
04e24aa8  4000243        8          System.Char  1 instance       73 _firstChar
04e2acd0  4000244       88        System.String  0   static 051411c8 Empty

Okej, czyli wiemy, że nasz string "string" jest pod adresem 0514a6b0.

Bierzemy się za wywołanie następnej funkcji:

C:\Users\afish\Desktop\msp_windowsinternals\StringAsReference\Program.cs @ 15:
076c06c2 8b4df0          mov     ecx,dword ptr [ebp-10h]
076c06c5 ff1518fcf204    call    dword ptr ds:[4F2FC18h] (StringAsReference.Program.ModifyAndPrint(System.String), mdToken: 06000002)
076c06cb 90              nop

Widzimy, że przekazujemy parametr przez ecx i tym parametrem jest jeden integer (czyli wskaźniczek). Jedziemy dalej g i czekamy na Breakpoint 2. Potem wstrzymujemy aplikację i zrzucamy stos:

0317F0D4 076c5427 [DEFAULT] Void StringAsReference.Program.ModifyAndPrint(String) (C:\Users\afish\Desktop\msp_windowsinternals\StringAsReference\bin\x86\Debug\netcoreapp3.1\StringAsReference.dll)

PARAMETERS:
  + string input = "string"

LOCALS: (none)

Zobaczmy kod naszej funkcji:

!name2ee StringAsReference StringAsReference.Program.ModifyAndPrint


Module:      04f2e848
Assembly:    StringAsReference.dll
Token:       06000002
MethodDesc:  04f2fc10
Name:        StringAsReference.Program.ModifyAndPrint(System.String)
JITTED Code Address: 076c53f0
!U /d 076c53f0

Normal JIT generated code
StringAsReference.Program.ModifyAndPrint(System.String)
ilAddr is 051120DC pImport is 0E51DDB8
Begin 076C53F0, size 86

C:\Users\afish\Desktop\msp_windowsinternals\StringAsReference\Program.cs @ 27:
>>> 076c53f0 55              push    ebp
076c53f1 8bec            mov     ebp,esp
076c53f3 83ec14          sub     esp,14h
076c53f6 33c0            xor     eax,eax
076c53f8 8945f8          mov     dword ptr [ebp-8],eax
076c53fb 8945f4          mov     dword ptr [ebp-0Ch],eax
076c53fe 8945f0          mov     dword ptr [ebp-10h],eax
076c5401 8945ec          mov     dword ptr [ebp-14h],eax
076c5404 894dfc          mov     dword ptr [ebp-4],ecx

Linijka 076c5404 zapisuje parametr z ecx na stos. No i zrzucamy stos:

dd 0317F0D4 

0317f0d4  00000000 00000000 00000000 00000000
0317f0e4  0514a6b0 0317f118 076c06cb 00000000

No i pięknie widzimy, że na stosie jest adres powrotu 076c06cb, a przed nim parametr wyciągnięty z rejestru, którym to jest 0514a6b0 czyli nasz wskaźnik. Ergo, został on przekazany przez wartość referencji.

Dalej nie chce mi się w to bawić, jak kogoś interesuje, to można wstrzymywać się przy kolejnych breakpointach i patrzeć, co tam siedzi. Kluczowe będzie zrozumienie różnicy przy wywołaniu metody z ref:

C:\Users\afish\Desktop\msp_windowsinternals\StringAsReference\Program.cs @ 20:
076c06fb 8d4df0          lea     ecx,[ebp-10h]
076c06fe ff1528fcf204    call    dword ptr ds:[4F2FC28h] (StringAsReference.Program.ModifyRefAndPrint(System.String ByRef), mdToken: 06000003)
076c0704 90              nop

Tutaj robimy lea zamiast mov.

To kończy dowód. Ale można podać kilka innych przesłanek.

  1. Na logikę
    String może być ogromny (do dwóch gigabajtów), gdyby był trzymany na stosie (tak jak typy wartościowe), to aplikacja nie miałaby szans działać (bo stos ma domyślnie 1MB na Windowsie).
    Kopiowanie stringa, który jest niezmienny (immutable) nie ma sensu, przecież i tak się nie zmieni. Nie lepiej przekazać wskaźnik?
    Przekazywanie długich stringów byłoby wolniejsze od przekazywania krótkich, powinniśmy móc to zmierzyć profilerem lub benchmarkiem (polecam spróbować).
    Skoro string się nie zmienia, to po co mieć masę kopii tej samej wartości w pamięci?
0

Referencję możesz przekazać do funkcji przez wartość albo przez referencję.
Referencję przez referencję .
Tzn. możesz przekazać oryginalną referencję albo jej kopię .
Sam obiekt string znajduje się na stercie i nie jest kopiowany .
Zapamiętaj sobie!!!
Modyfikator ref i out w zasadzie przydaje się do tego żeby zwrócić z funkcji więcej niż jedną wartość

Jeszcze jedno . Raz utworzonego stringa nie możesz już zmodyfikować . Możesz jedynie utworzyć nowego stringa, który ma nową referencję .
Zapamiętaj i najlepiej zainwestuj 30 zł w jakąś książkę o C# jeśli nie znasz angielskiego .

0

Najprościej to możesz zrobić migawkę sterty i zobaczyć ile jest aktualnie obiektów .
Żeby wyświetlić głupi napis "Hello World !" system tworzy 310 obiektów na stercie, które zajmują prawie 56 KB pamięci na stercie.
Dzięki temu że stringi są niezmienne to

using System;
namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            string str1 = "tekst";
            string str2 = "tekst";
            string str3 = "tekst";          
        }
    }
}

mamy 1 obiekt w powyższym programie a nie 3 .

using System;
namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            string str1 = "tekst";
            string str2 = str1;
            string str3 = str2;          
        }
    } 
}
0

@Zimny Krawiec:

mamy 1 obiekt w powyższym programie a nie 3 .

Da się jakimś cudem wyciągnąć ile nasz kod (bez libek) stworzyły obiektów? albo dany namespace/class?

czy tylko gcdumpa robić i przeglądać?

1
Kofcio napisał(a):

Chciałem się zapytać dlaczego zmienne typu string przekazywane do funkcji bez słowa ref nie są w oryginale modyfikowane?

Stringi w C# są niemutowalne. Jak przekazujesz stringa bez ref, to przekazujesz owszem referencję, ale nie możesz w żaden (niehakerski) sposób zmienić stringa który znajduje się pod tą referencją.
Przekazanie stringa przez ref przekazuje referencję do referencji, więc możesz podmienić referowaną referencję na nową referencję na nowego stringa.

0

Żeby zobaczyć ile jest aktualnie obiektów na zarządzanej stercie , robisz punkt przerwania - klikasz myszą na lewym bocznym pasku obok instrukcji gdzie ma nastąpić zatrzymanie programu. . Jeśli dobrze to zrobisz to pojawi ci się czerwona kropka . Potem klikasz na - rozpocznij z debugowaniem , F5 albo .na zieloną strzałkę rozpocznij. Jak się program zatrzyma to pod podsumowaniem klikasz na ikonkę aparatu i robisz migawkę obiektów na stercie. Taką utworzoną migawkę możesz przeglądać. Następnie możesz przewijać krokowo program do przodu i robić kolejne migawki i sprawdzać zużycie pamięci , liczbę obiektów.

1 użytkowników online, w tym zalogowanych: 0, gości: 1