Tipo di riferimento stringa C #?


163

So che "stringa" in C # è un tipo di riferimento. Questo è su MSDN. Tuttavia, questo codice non funziona come dovrebbe quindi:

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";
    }
}

L'output dovrebbe essere "prima di passare" "dopo aver passato" poiché sto passando la stringa come parametro ed essendo un tipo di riferimento, la seconda istruzione di output dovrebbe riconoscere che il testo è cambiato nel metodo TestI. Tuttavia, ottengo "prima di passare" "prima di passare" facendo sembrare che sia passato per valore non per rif. Capisco che le stringhe sono immutabili, ma non vedo come ciò spiegherebbe cosa sta succedendo qui. Cosa mi sto perdendo? Grazie.


Vedi l'articolo a cui fa riferimento Jon di seguito. Il comportamento menzionato può essere riprodotto anche dai puntatori C ++.
Sesh,

Molto bella spiegazione anche in MSDN .
Dimi_Pel

Risposte:


211

Il riferimento alla stringa viene passato per valore. C'è una grande differenza tra passare un riferimento per valore e passare un oggetto per riferimento. È un peccato che la parola "riferimento" sia usata in entrambi i casi.

Se si fa passare il riferimento di stringa per riferimento, che funzionerà come ci si aspetta:

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";
    }
}

Ora è necessario distinguere tra apportare modifiche all'oggetto a cui fa riferimento un riferimento e apportare una modifica a una variabile (come un parametro) per consentirgli di fare riferimento a un oggetto diverso. Non possiamo apportare modifiche a una stringa perché le stringhe sono immutabili, ma possiamo dimostrarlo con un StringBuilderinvece:

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");
    }
}

Vedi il mio articolo sul passaggio dei parametri per maggiori dettagli.


2
d'accordo, voglio solo chiarire che l'uso del modificatore ref funziona anche per tipi non di riferimento, ovvero entrambi sono concetti piuttosto separati.
Eglasio l'

2
@Jon Skeet ha adorato il sidenote nel tuo articolo. Dovresti averlo referencedcome risposta
Nithish Inpursuit Ofhappiness il

36

Se dobbiamo rispondere alla domanda: String è un tipo di riferimento e si comporta come un riferimento. Passiamo un parametro che contiene un riferimento, non la stringa effettiva. Il problema è nella funzione:

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

Il parametro testcontiene un riferimento alla stringa ma è una copia. Abbiamo due variabili che puntano alla stringa. E poiché qualsiasi operazione con stringhe crea effettivamente un nuovo oggetto, facciamo la nostra copia locale in modo che punti alla nuova stringa. Ma la testvariabile originale non è cambiata.

Le soluzioni suggerite da inserire refnella dichiarazione di funzione e nell'invocazione funzionano perché non passeremo il valore della testvariabile ma passeremo solo un riferimento ad essa. Pertanto, qualsiasi modifica all'interno della funzione rifletterà la variabile originale.

Voglio ripetere alla fine: String è un tipo di riferimento, ma poiché è immutabile, la linea test = "after passing";crea effettivamente un nuovo oggetto e la nostra copia della variabile testviene modificata per puntare alla nuova stringa.


25

Come altri hanno già affermato, il Stringtipo in .NET è immutabile e il suo riferimento viene passato per valore.

Nel codice originale, non appena questa riga viene eseguita:

test = "after passing";

quindi testnon si riferisce più all'oggetto originale. Abbiamo creato un nuovo String oggetto e assegnato testa fare riferimento a tale oggetto sull'heap gestito.

Sento che molte persone sono inciampate qui perché non c'è nessun costruttore formale visibile che ricordi loro. In questo caso, sta accadendo dietro le quinte poiché il Stringtipo ha il supporto linguistico nel modo in cui è costruito.

Quindi, questo è il motivo per cui la modifica a testnon è visibile al di fuori dell'ambito del TestI(string)metodo: abbiamo passato il riferimento per valore e ora quel valore è cambiato! Ma se il Stringriferimento è stato passato per riferimento, quando il riferimento è cambiato lo vedremo al di fuori dell'ambito del TestI(string)metodo.

In questo caso sono necessarie la parola chiave ref o out . Ritengo che la outparola chiave potrebbe essere leggermente più adatta a questa particolare situazione.

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";
    }
}

ref = funzione esterna inizializzata, out = funzione interna inizializzata o in altre parole; ref è bidirezionale, out è solo out. Quindi sicuramente ref dovrebbe essere usato.
Paul Zahra,

@PaulZahra: outdeve essere assegnato all'interno del metodo per la compilazione del codice. refnon ha tale requisito. Anche i outparametri vengono inizializzati al di fuori del metodo: il codice in questa risposta è un controesempio.
Derek W,

Dovrebbe chiarire - i outparametri possono essere inizializzati al di fuori del metodo, ma non è necessario. In questo caso, vogliamo inizializzare il outparametro per dimostrare un punto sulla natura del stringtipo in .NET.
Derek W,

9

In realtà sarebbe stato lo stesso per qualsiasi oggetto per quella materia, cioè essere un tipo di riferimento e passare per riferimento sono 2 cose diverse in c #.

Funzionerebbe, ma si applica indipendentemente dal tipo:

public static void TestI(ref string test)

Anche sulla stringa è un tipo di riferimento, è anche uno speciale. È progettato per essere immutabile, quindi tutti i suoi metodi non modificheranno l'istanza (ne restituiranno una nuova). Ha anche alcune cose in più per le prestazioni.


7

Ecco un buon modo di pensare alla differenza tra tipi di valore, passaggio per valore, tipi di riferimento e passaggio per riferimento:

Una variabile è un contenitore.

Una variabile di tipo valore contiene un'istanza. Una variabile del tipo di riferimento contiene un puntatore a un'istanza memorizzata altrove.

La modifica di una variabile di tipo valore modifica l'istanza in essa contenuta. La modifica di una variabile del tipo di riferimento comporta la mutazione dell'istanza a cui punta.

Le variabili separate del tipo di riferimento possono puntare alla stessa istanza. Pertanto, la stessa istanza può essere modificata tramite qualsiasi variabile che punti ad essa.

Un argomento passato per valore è un nuovo contenitore con una nuova copia del contenuto. Un argomento passato per riferimento è il contenitore originale con il suo contenuto originale.

Quando un argomento di tipo valore viene passato per valore: la riassegnazione del contenuto dell'argomento non ha alcun effetto al di fuori dell'ambito, poiché il contenitore è univoco. La modifica dell'argomento non ha alcun effetto al di fuori dell'ambito, poiché l'istanza è una copia indipendente.

Quando un argomento del tipo di riferimento viene passato per valore: la riassegnazione del contenuto dell'argomento non ha alcun effetto al di fuori dell'ambito, poiché il contenitore è univoco. La modifica del contenuto dell'argomento influisce sull'ambito esterno, poiché il puntatore copiato punta a un'istanza condivisa.

Quando un argomento viene passato per riferimento: la riassegnazione del contenuto dell'argomento influisce sull'ambito esterno, poiché il contenitore è condiviso. La modifica del contenuto dell'argomento influisce sull'ambito esterno, poiché il contenuto è condiviso.

In conclusione:

Una variabile stringa è una variabile di tipo riferimento. Pertanto, contiene un puntatore a un'istanza memorizzata altrove. Quando viene passato per valore, il suo puntatore viene copiato, quindi la modifica di un argomento stringa dovrebbe influire sull'istanza condivisa. Tuttavia, un'istanza di stringa non ha proprietà modificabili, quindi un argomento di stringa non può essere modificato comunque. Se passato per riferimento, il contenitore del puntatore è condiviso, quindi la riassegnazione influirà comunque sull'ambito esterno.


6

" Un'immagine vale più di mille parole ".

Ho un semplice esempio qui, è simile al tuo caso.

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

Questo è quello che è successo:

inserisci qui la descrizione dell'immagine

  • Riga 1 e 2: s1e le s2variabili fanno riferimento allo stesso "abc"oggetto stringa.
  • Riga 3: poiché le stringhe sono immutabili , quindi l' "abc"oggetto stringa non si modifica da solo (in "def"), ma "def"viene creato un nuovo oggetto stringa e quindi fa s1riferimento ad esso.
  • Riga 4: fa s2ancora riferimento "abc"all'oggetto stringa, quindi questo è l'output.

5

Le risposte sopra sono utili, vorrei solo aggiungere un esempio che penso stia dimostrando chiaramente cosa succede quando passiamo i parametri senza la parola chiave ref, anche quando quel parametro è un tipo di riferimento:

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.
        }

1
Questa spiegazione ha funzionato per me al meglio. Quindi sostanzialmente passiamo tutto per valore nonostante il fatto che la variabile stessa sia valore o tipo di riferimento a meno che non utilizziamo la parola chiave ref (o out). Non è rilevante con la nostra codifica quotidiana perché generalmente non impostiamo i nostri oggetti su null o su un'istanza diversa all'interno di un metodo in cui sono stati passati, piuttosto impostiamo le loro proprietà o chiamiamo i loro metodi. Nel caso di "string", l'impostazione su una nuova istanza avviene sempre, ma il nuovo aggiornamento non è visibile e ciò fornisce una falsa interpretazione all'occhio non allenato. Correggimi se sbaglio.
Ε Г И І И О

3

Per menti curiose e per completare la conversazione: Sì, String è un tipo di riferimento :

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

Ma nota che questa modifica funziona solo in un blocco non sicuro ! perché le stringhe sono immutabili (da MSDN):

Il contenuto di un oggetto stringa non può essere modificato dopo la creazione dell'oggetto, sebbene la sintassi lo faccia apparire come se fosse possibile farlo. Ad esempio, quando si scrive questo codice, il compilatore crea effettivamente un nuovo oggetto stringa per contenere la nuova sequenza di caratteri e quel nuovo oggetto viene assegnato a b. La stringa "h" è quindi idonea per la garbage collection.

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

E tieni presente che:

Sebbene la stringa sia un tipo di riferimento, gli operatori di uguaglianza ( ==e !=) sono definiti per confrontare i valori degli oggetti stringa, non i riferimenti.


0

Credo che il tuo codice sia analogo al seguente e non dovresti aspettarti che il valore sia cambiato per lo stesso motivo per cui non sarebbe qui:

 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
 }

-1

Provare:


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

3
La tua risposta dovrebbe contenere più di un semplice codice. Dovrebbe anche contenere una spiegazione del perché funziona.
Charles Caldwell,
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.