Che senso ha passare un parametro per riferimento in C #? [duplicare]

10

C # non invia gli oggetti stessi? Quindi, a meno che non si tratti di una sorta di funzione di scambio per variabili tipizzate primitive - perché dovrei inviare un oggetto per riferimento?

    
posta JNF 02.07.2012 - 09:11
fonte

6 risposte

13

C # non passa gli oggetti stessi. C # passa ciò che è nella pila. Per i tipi di valore che hanno un ambito locale e quindi vivono nello stack, il valore viene passato al livello successivo dello stack di chiamate. Per tutti gli altri oggetti, i dati sono sull'heap e un riferimento all'indirizzo heap viene archiviato nello stack e viene passato in giro.

In entrambi i casi, i dati che risiedono nello stack, indipendentemente dal fatto che siano i dati che ti interessano o un "puntatore" di riferimento ai dati che ti interessano, non vengono modificati, perché quando viene passato, viene copiato in un nuova posizione sullo stack e quel valore è ciò che viene manipolato (e poi scartato quando il metodo esce e quel frame si spegne dallo stack di chiamate). Se si passa un tipo di valore a un metodo che modifica il valore, quando tale metodo ritorna al proprio chiamante il valore sarà invariato, poiché quella copia dei dati non è mai stata toccata dal metodo.

Se si passa un tipo di riferimento, è il riferimento che viene copiato. Ciò significa che i dati sull'heap, indicati dal riferimento, possono essere modificati e che cambieranno i dati per il chiamante. MA, se il parametro passato viene reinizializzato o altrimenti assegnato a un riferimento diverso, tale modifica verrà ripristinata al ritorno del metodo, poiché si tratta di una modifica al riferimento all'oggetto, che è stato passato "in base al valore" e non ha effetto la copia del chiamante di quel riferimento.

Per prova, esegui questo test in NUnit:

    [Test]
    public void TestPassObject()
    {
        //instantiate a new instance of some reference type
        var myObj = new object();

        //make a "shallow" copy. This copy will never be touched.
        var myLocalCopy = myObj;

        //Pass the original copy by reference to a method that will alter it.
        var result = TestMethod(myObj);

        //Our original reference value did not change even though
        //the passed reference was reinstantiated.
        //myObj and myLocalCopy are still the same object...
        Assert.AreEqual(myObj, myLocalCopy);
        //and are different from result, even though TestMethod 
        //assigned the reference in result to its passed myObj parameter.
        Assert.AreNotEqual(myObj, result);
    }

    private object TestMethod(object myObj)
    {
        //myObj is a local "copy" of the reference passed by the caller.
        //if you changed myObj's children, the caller would see that,
        //but reinitializing it points the copy to new data on the heap,
        //and does not affect the caller.
        return myObj = new object();
    }

Pertanto, dato il comportamento predefinito quando non si utilizza ref , la risposta alla tua domanda è che la parola chiave ref modifica ciò che viene passato. Invece di passare una copia del valore che si trova nello stack, a un parametro con la parola chiave ref viene assegnato un "puntatore" ai dati residenti nello stack, indipendentemente dal fatto che si tratti di un valore dati o di un riferimento a un valore dati su il mucchio. Inserisco il "puntatore" tra virgolette perché il termine è anatema per la maggior parte dei codificatori .NET, ma scavo abbastanza in profondità e questo è più o meno quello che sta succedendo; ti viene assegnato un puntatore immutabile a quell'elemento del frame dello stack del chiamante.

Dietro le quinte, quando si utilizza o si modifica il parametro ref passato, si sta utilizzando o modificando il valore della copia del chiamante, come indicato dal parametro ref . Questo è vero indipendentemente da ciò che è stato passato (dati o riferimento ai dati). Ciò significa che è possibile modificare i dati di un tipo di valore o reinizializzare / riassegnare il riferimento di un tipo di riferimento e ciò influirà sul chiamante. Tuttavia, la sintassi rimane la stessa, perché la specifica C # dice che deve essere così per evitare la confusione che ha C ++; in C ++, quando si ha a che fare con un figlio del parametro passato, devi sapere se ti è stato dato un puntatore a un oggetto e devi usare . , o un puntatore a un puntatore a un oggetto e devi usare -> (o un puntatore a un puntatore a un puntatore a un oggetto e deve dereferenziazione, ecc. ecc.)

    
risposta data 02.07.2012 - 17:38
fonte
16

Quando si passa un oggetto per riferimento, è possibile modificare il valore del riferimento.

Supponiamo tu abbia una classe Person:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public override string ToString()
    {
        return String.Format("Name: {0} - Age: {1}", Name, Age);
    }
}

Hai anche queste funzioni:

private static void NormalChangeProperties(Person person)
{
    person.Name = "Bob";
    person.Age = 17;
}

private static void NormalAssignNewPerson(Person person)
{
    person = new Person { Age = 55, Name = "John" };
}

private static void NormalSetNull(Person person)
{
    person = null;
}

private static void ByRefChangeProperties(ref Person person)
{
    person.Name = "Bob";
    person.Age = 17;
}

private static void ByRefAssignNewPerson(ref Person person)
{
    person = new Person { Age = 55, Name = "John" };
}

private static void ByRefSetNull(ref Person person)
{
    person = null;
}

E poi hai questa app per console:

static void Main(string[] args)
{
    Person p = new Person { Age = 29, Name = "Kristof" };
    Console.WriteLine("Initial: {0}", p);

    NormalChangeProperties(p);
    Console.WriteLine("After NormalChangeProperties: {0}", p);

    NormalAssignNewPerson(p);
    Console.WriteLine("After NormalAssignNewPerson: {0}", p);

    NormalSetNull(p);
    Console.WriteLine("After NormalSetNull: {0}", p);

    Console.WriteLine("");
    Console.WriteLine("");

    Person p2 = new Person { Age = 29, Name = "Kristof" };
    Console.WriteLine("Initial: {0}", p2);

    ByRefChangeProperties(ref p2);
    Console.WriteLine("After ByRefChangeProperties: {0}", p2);

    ByRefAssignNewPerson(ref p2);
    Console.WriteLine("After ByRefAssignNewPerson: {0}", p2);

    ByRefSetNull(ref p2);
    Console.WriteLine("After ByRefSetNull: {0}", p2 != null ? p2.ToString() : "null");

    Console.ReadLine();
}

L'output sarà:

Initial: Name: Kristof - Age: 29
After NormalChangeProperties: Name: Bob - Age: 17
After NormalAssignNewPerson: Name: Bob - Age: 17
After NormallSetNull: Name: Bob - Age: 17


Initial: Name: Kristof - Age: 29
After ByRefChangeProperties: Name: Bob - Age: 17
After ByRefAssignNewPerson: Name: John - Age: 55
After ByRefSetNull: null

Come puoi vedere, assegnando un nuovo valore (al contrario delle proprietà modificabili) o impostandolo su null (che modifica anche il valore) del riferimento stesso , è necessario passare per riferimento.

Per una spiegazione migliore e più dettagliata, puoi dare un'occhiata a questo articolo di Jon Skeet .

    
risposta data 02.07.2012 - 09:54
fonte
6

È necessario utilizzare quando si desidera che un metodo modifichi un tipo di valore o modifichi un riferimento a un tipo di riferimento. La ragione più comune per eseguire uno di questi nel codice che ho visto è l'interoperabilità con C || Librerie C ++, ad es. ref int viene eseguito il marshalling su *int .

Nel puro codice gestito tende ad essere più raro avere bisogno di ref o out in quanto si utilizzano principalmente tipi di riferimento che già forniscono riferimenti ad oggetti piuttosto che copie di oggetti come fanno i tipi di valore. cioè un livello di riferimento indiretto è tutto ciò di cui hai bisogno per la maggior parte del tempo (in effetti se hai bisogno di ref molto potresti essere un programmatore a tre stelle ;)

    
risposta data 02.07.2012 - 09:44
fonte
2

Fare una copia profonda di un oggetto è un'operazione potenzialmente molto costosa in termini di memoria e CPU. In alcuni casi ( DataTable e gli amici vengono in mente), c'è persino un codice che impedisce una copia ingenua e profonda, sebbene si possa interrogare qui qual è la causa e l'effetto. Se il tipo dell'istanza ha un qualsiasi tipo di stato globale, rendere potenzialmente pericoloso potrebbe essere pericoloso.

D'altra parte, fare una copia profonda del valore di una variabile tipizzata in modo primitivo è abbastanza economica, e in alcuni casi (ad esempio, l'immutabilità delle stringhe) viene essenzialmente gratuita.

In generale, ritengo che le funzioni non debbano toccare i loro parametri di input a meno che il chiamante non lo voglia realmente. Un modo semplice per garantire che il chiamante sia almeno consapevole che il destinatario possa modificare l'input deve richiedere il passaggio per riferimento, o ref o out a seconda dei casi.

    
risposta data 02.07.2012 - 09:44
fonte
2

C # invia riferimenti agli oggetti tutto il tempo. Cioè, quando lo fai

void Bar (myObject obj)
{
    obj.Name = "Hi";
    obj = new myObject();
}

obj è un valore che contiene un riferimento a un'istanza di myObject allocata nell'heap. Se rimaniamo dietro la metafora del superamento di interi oggetti in qualsiasi modo, non è affatto diverso da un intero. Il riferimento stesso viene passato per valore (essendo copiato byte per byte), ma la cosa a cui punta il riferimento rimane ovviamente la stessa.

Quando si utilizza la parola chiave ref , ciò che si passa è un riferimento a una variabile (notare l'enfasi; confrontare con quanto sopra). ref int sarebbe un riferimento a una variabile intera. Ancora una volta, il riferimento viene copiato byte per byte (un processo completamente opaco per te), ma la cosa a cui fa riferimento rimarrà invariata.

Quindi, proprio come obj è un riferimento a un'istanza di myObject, ref myObject obj2 sarebbe un riferimento a una variabile che contiene un riferimento a un'istanza di un oggetto o, in altre parole, un riferimento a un riferimento di %codice%.

La parola chiave myObject fa la stessa cosa, tranne che si presume che il riferimento punta a null.

Ok, ora che abbiamo superato questo, contendiamo la domanda di perché diavolo fai questo .

Bene, la risposta breve è, non utilizzare la parola chiave out . La risposta leggermente più lunga è, Non utilizzare la parola chiave ref a meno che tu non possa davvero evitarlo. . Soprattutto non usarlo con nulla tranne le variabili locali (ad esempio non utilizzarlo con campi e simili), e davvero non usarlo sui tipi di riferimento.

Ci sono diversi motivi per questa logica. Prima di tutto, mantenere il numero di ritorni di funzione al minimo aiuta a comprendere. Riduce il numero di percorsi divergenti che il tuo codice può assumere.

Quindi, passare le cose per riferimento apre una gran quantità di problemi di digitazione e di mutevolezza. Diciamo che ho un'istanza ref , e c'è un metodo che prende MyChild : MyParent per riferimento. Quando passo MyParent al metodo, non posso mai sapere se sto ottenendo un oggetto di tipo MyChild indietro . In termini più generali, non hai davvero idea di quale tipo di oggetto tornerai indietro quando invii qualcosa in MyChild . Potrebbe essere la stessa istanza, potrebbe essere un'altra istanza, potrebbe essere un altro tipo del tutto.

    
risposta data 02.07.2012 - 13:32
fonte
1

Invia oggetti per riferimento quando vuoi avere la possibilità di modificare l'oggetto originale. Altrimenti, invia per valore se hai solo bisogno del valore dell'oggetto.

Il passaggio per riferimento avviene sempre e non solo per le funzioni di scambio.

    
risposta data 02.07.2012 - 09:28
fonte

Leggi altre domande sui tag