Come vengono passate le stringhe in .NET?


121

Quando passo a stringa una funzione, viene passato un puntatore al contenuto della stringa o l'intera stringa viene passata alla funzione sullo stack come structsarebbe?

Risposte:


278

Viene passato un riferimento; tuttavia, non è tecnicamente passato per riferimento. Questa è una distinzione sottile, ma molto importante. Considera il codice seguente:

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

Ci sono tre cose che devi sapere per capire cosa succede qui:

  1. Le stringhe sono tipi di riferimento in C #.
  2. Sono anche immutabili, quindi ogni volta che fai qualcosa che sembra che tu stia cambiando la stringa, non lo fai. Viene creata una stringa completamente nuova, il riferimento viene puntato su di essa e quella vecchia viene gettata via.
  3. Anche se le stringhe sono tipi di riferimento, strMainnon vengono passate per riferimento. È un tipo di riferimento, ma il riferimento stesso viene passato per valore . Ogni volta che passi un parametro senza la refparola chiave (senza contare i outparametri), hai passato qualcosa per valore.

Quindi questo deve significare che stai ... passando un riferimento per valore. Poiché è un tipo di riferimento, solo il riferimento è stato copiato nello stack. Ma cosa significa?

Passaggio di tipi di riferimento per valore: lo stai già facendo

Le variabili C # sono tipi di riferimento o tipi di valore . I parametri C # vengono passati per riferimento o passati per valore . La terminologia è un problema qui; questi suonano come la stessa cosa, ma non lo sono.

Se si passa un parametro di QUALSIASI tipo e non si utilizza la refparola chiave, l'hai passato per valore. Se l'hai passato per valore, quello che hai veramente passato era una copia. Ma se il parametro era un tipo di riferimento, la cosa che hai copiato era il riferimento, non qualunque cosa stesse puntando.

Ecco la prima riga del Mainmetodo:

string strMain = "main";

Abbiamo creato due cose su questa riga: una stringa con il valore mainmemorizzato da qualche parte in memoria e una variabile di riferimento chiamata che strMainpunta ad esso.

DoSomething(strMain);

Ora passiamo quel riferimento a DoSomething. L'abbiamo passato per valore, quindi significa che ne abbiamo fatto una copia. È un tipo di riferimento, quindi significa che abbiamo copiato il riferimento, non la stringa stessa. Ora abbiamo due riferimenti che puntano ciascuno allo stesso valore in memoria.

All'interno del chiamato

Ecco la parte superiore del DoSomethingmetodo:

void DoSomething(string strLocal)

Nessuna refparola chiave, quindi strLocale strMainsono due riferimenti diversi che puntano allo stesso valore. Se riassegniamo strLocal...

strLocal = "local";   

... non abbiamo cambiato il valore memorizzato; abbiamo preso il riferimento chiamato strLocale lo abbiamo puntato su una stringa nuova di zecca. Cosa succede strMainquando lo facciamo? Niente. Sta ancora indicando la vecchia corda.

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" 

Immutabilità

Cambiamo lo scenario per un secondo. Immagina di non lavorare con le stringhe, ma con qualche tipo di riferimento mutevole, come una classe che hai creato.

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

Se segui il riferimento objLocalall'oggetto a cui punta, puoi modificarne le proprietà:

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

Ce n'è ancora solo uno MutableThingin memoria e sia il riferimento copiato che il riferimento originale lo indicano ancora. Le proprietà dello MutableThingstesso sono cambiate :

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   
}

Ah, ma le stringhe sono immutabili! Non ci sono ChangeMeproprietà da impostare. Non puoi fare strLocal[3] = 'H'in C # come potresti fare con un chararray in stile C ; devi invece costruire una stringa completamente nuova. L'unico modo per cambiare strLocalè puntare il riferimento su un'altra stringa, e questo significa che nulla di ciò che fai strLocalpuò influire strMain. Il valore è immutabile e il riferimento è una copia.

Passaggio di un riferimento per riferimento

Per dimostrare che c'è una differenza, ecco cosa succede quando passi un riferimento per riferimento:

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

Questa volta, la stringa in viene Maindavvero modificata perché hai passato il riferimento senza copiarlo nello stack.

Quindi, anche se le stringhe sono tipi di riferimento, passarle per valore significa che qualsiasi cosa accada nel chiamato non influenzerà la stringa nel chiamante. Ma poiché sono tipi di riferimento, non è necessario copiare l'intera stringa in memoria quando si desidera passarla.

Ulteriori risorse:


3
@TheLight - Scusa, ma qui non sei corretto quando dici: "Un tipo di riferimento viene passato per riferimento per impostazione predefinita". Per impostazione predefinita, tutti i parametri vengono passati per valore, ma con i tipi di riferimento, ciò significa che il riferimento viene passato per valore. Stai fondendo tipi di riferimento con parametri di riferimento, il che è comprensibile perché è una distinzione molto confusa. Vedere la sezione Passaggio di tipi di riferimento per valore qui. Il tuo articolo collegato è abbastanza corretto, ma in realtà supporta il mio punto.
Justin Morgan

1
@JustinMorgan Non per far apparire un thread di commento morto, ma penso che il commento di TheLight abbia senso se pensi in C. In C, i dati sono solo un blocco di memoria. Un riferimento è un puntatore a quel blocco di memoria. Se si passa l'intero blocco di memoria a una funzione, si parla di "passaggio per valore". Se passi il puntatore si chiama "passaggio per riferimento". In C #, non esiste la nozione di passare l'intero blocco di memoria, quindi hanno ridefinito "passaggio per valore" nel senso di passare il puntatore. Sembra sbagliato, ma anche un puntatore è solo un blocco di memoria! Per me, la terminologia è piuttosto arbitraria
rliu

@roliu - Il problema è che non stiamo lavorando in C e C # è estremamente diverso nonostante il nome e la sintassi simili. Per prima cosa, i riferimenti non sono la stessa cosa dei puntatori e pensarli in questo modo può portare a insidie. Il problema più grande, tuttavia, è che "passare per riferimento" ha un significato molto specifico in C #, che richiede la refparola chiave. Per dimostrare che passare per riferimento fa la differenza, guarda questa demo: rextester.com/WKBG5978
Justin Morgan

1
@JustinMorgan Sono d'accordo sul fatto che mescolare la terminologia C e C # sia un male, ma, sebbene mi sia piaciuto il post di lippert, non sono d'accordo che pensare ai riferimenti come a dei puntatori offuschi particolarmente qualcosa qui. Il post del blog descrive come pensare a un riferimento come a un puntatore gli dia troppo potere. Sono consapevole che la refparola chiave ha utilità, stavo solo cercando di spiegare perché si potrebbe pensare di passare un tipo di riferimento per valore in C # sembra la nozione "tradizionale" (cioè C) di passare per riferimento (e passare un tipo di riferimento per riferimento in C # sembra più come passare un riferimento a un riferimento in base al valore).
rliu

2
Lei ha ragione, ma penso che @roliu stato riferimento a come una funzione come Foo(string bar)potrebbe essere pensato come Foo(char* bar), mentre Foo(ref string bar)sarebbe Foo(char** bar)(o Foo(char*& bar)o Foo(string& bar)in C ++). Certo, non è come dovresti pensarci tutti i giorni, ma in realtà mi ha aiutato a capire finalmente cosa sta succedendo sotto il cofano.
Cole Johnson

23

Le stringhe in C # sono oggetti di riferimento immutabili. Ciò significa che i riferimenti ad essi vengono passati (per valore) e, una volta creata una stringa, non è possibile modificarla. I metodi che producono versioni modificate della stringa (sottostringhe, versioni tagliate, ecc.) Creano copie modificate della stringa originale.


10

Le stringhe sono casi speciali. Ogni istanza è immutabile. Quando si modifica il valore di una stringa si alloca una nuova stringa in memoria.

Quindi solo il riferimento viene passato alla tua funzione, ma quando la stringa viene modificata diventa una nuova istanza e non modifica la vecchia istanza.


4
Le corde non sono un caso speciale in questo aspetto. È molto facile creare oggetti immutabili che potrebbero avere la stessa semantica. (Cioè, un'istanza di un tipo che non espone un metodo per modificarlo ...)

Le stringhe sono casi speciali: sono effettivamente tipi di riferimento immutabili che sembrano essere mutabili in quanto si comportano come tipi di valore.
Enigmativity

1
@Enigmativity In base a questa logica, anche Uri(class) e Guid(struct) sono casi speciali. Non vedo come si System.Stringcomporti come un "tipo di valore" più di altri tipi immutabili ... di origine di classe o struttura.

3
@pst - Le stringhe hanno una semantica di creazione speciale - a differenza di Uri& Guid- puoi semplicemente assegnare un valore letterale stringa a una variabile stringa. La stringa sembra essere modificabile, come un intessere riassegnato, ma sta creando un oggetto implicitamente, nessuna newparola chiave.
Enigmativity

3
La stringa è un caso speciale, ma non ha rilevanza per questa domanda. Tipo di valore, tipo di riferimento, qualunque tipo agirà tutti allo stesso modo in questa domanda.
Kirk Broadhurst,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.