Quando usare ref e quando non è necessario in C #


104

Ho un oggetto che è il mio stato in memoria del programma e ho anche alcune altre funzioni di lavoro a cui passo l'oggetto per modificare lo stato. L'ho passato per riferimento alle funzioni di lavoro. Tuttavia mi sono imbattuto nella seguente funzione.

byte[] received_s = new byte[2048];
IPEndPoint tmpIpEndPoint = new IPEndPoint(IPAddress.Any, UdpPort_msg);
EndPoint remoteEP = (tmpIpEndPoint);

int sz = soUdp_msg.ReceiveFrom(received_s, ref remoteEP); 

Mi confonde perché entrambi received_se remoteEPstanno restituendo cose dalla funzione. Perché ha remoteEPbisogno di un refe received_snon lo fa?

Sono anche un programmatore CA quindi ho problemi a togliermi i puntatori dalla testa.

Modifica: sembra che gli oggetti in C # siano puntatori all'oggetto nascosto. Quindi, quando si passa un oggetto a una funzione, è possibile modificare il contenuto dell'oggetto tramite il puntatore e l'unica cosa passata alla funzione è il puntatore all'oggetto in modo che l'oggetto stesso non venga copiato. Puoi usare ref o out se vuoi essere in grado di cambiare o creare un nuovo oggetto nella funzione che è come un doppio puntatore.

Risposte:


217

Risposta breve: leggi il mio articolo sul passaggio di argomenti .

Risposta lunga: quando un parametro del tipo di riferimento viene passato per valore, viene passato solo il riferimento, non una copia dell'oggetto. È come passare un puntatore (per valore) in C o C ++. Modifiche al valore del parametro stesso non saranno visti dal chiamante, ma cambia nell'oggetto quali punti di riferimento per il sarà visibile.

Quando un parametro (di qualsiasi tipo) viene passato per riferimento, significa che qualsiasi modifica al parametro viene vista dal chiamante: le modifiche al parametro sono modifiche alla variabile.

L'articolo spiega tutto questo in modo più dettagliato, ovviamente :)

Risposta utile: non è quasi mai necessario utilizzare ref / out . È fondamentalmente un modo per ottenere un altro valore di ritorno e di solito dovrebbe essere evitato proprio perché significa che il metodo sta probabilmente cercando di fare troppo. Non è sempre così ( TryParseecc. Sono esempi canonici di uso ragionevole di out), ma l'uso di ref / out dovrebbe essere una rarità relativa.


38
Penso che tu abbia confuso la tua risposta breve e la risposta lunga; questo è un grande articolo!
Programmatore fuorilegge

23
@ Outlaw: Sì, ma la risposta breve stessa, la direttiva di leggere l'articolo, è lunga solo 6 parole :)
Jon Skeet

27
Un riferimento! :)
gonzobrains

5
@Liam Usare ref come te può sembrare più esplicito per te, ma in realtà potrebbe creare confusione per gli altri programmatori (quelli che sanno comunque cosa fa quella parola chiave), perché essenzialmente stai dicendo ai potenziali chiamanti: "Posso modificare la variabile tu usato nel metodo chiamante, cioè riassegnarlo a un oggetto diverso (o anche a null, se possibile) quindi non aggrapparti ad esso, o assicurati di convalidarlo quando ho finito con esso ". È abbastanza forte e completamente diverso da "questo oggetto può essere modificato", che è sempre il caso ogni volta che passi un riferimento a un oggetto come parametro.
mbargiel

1
@ M.Mimpen: C # 7 consentirà (si spera)if (int.TryParse(text, out int value)) { ... use value here ... }
Jon Skeet

26

Pensa a un parametro non ref come a un puntatore e a un parametro ref come a un doppio puntatore. Questo mi ha aiutato di più.

Non dovresti quasi mai passare i valori per rif. Sospetto che se non fosse stato per problemi di interoperabilità, il team .Net non lo avrebbe mai incluso nelle specifiche originali. Il modo OO di affrontare la maggior parte dei problemi risolti dai parametri di riferimento è:

Per più valori restituiti

  • Crea strutture che rappresentano più valori restituiti

Per le primitive che cambiano in un metodo come risultato della chiamata al metodo (il metodo ha effetti collaterali sui parametri primitivi)

  • Implementa il metodo in un oggetto come metodo di istanza e modifica lo stato dell'oggetto (non i parametri) come parte della chiamata al metodo
  • Utilizzare la soluzione del valore restituito multiplo e unire i valori restituiti al proprio stato
  • Crea un oggetto che contiene uno stato che può essere manipolato da un metodo e passa quell'oggetto come parametro e non le primitive stesse.

8
Buon Dio. Dovrei leggere questo 20x per capirlo. Mi sembra un sacco di lavoro extra solo per fare qualcosa di semplice.
PositiveGuy

Il framework .NET non segue la tua regola n. 1. ('Per più valori di ritorno creare strutture'). Prendiamo ad esempio IPAddress.TryParse(string, out IPAddress).
Swen Kooij

@SwenKooij Hai ragione. Presumo che nella maggior parte dei luoghi in cui usano i parametri sia (a) stanno avvolgendo un'API Win32, o (b) erano i primi giorni ei programmatori C ++ stavano prendendo decisioni.
Michael Meadows

@SwenKooij So che questa risposta è in ritardo di molti anni, ma è così. Siamo abituati a TryParse, ma non significa che sia buono. Sarebbe stato meglio se invece di if (int.TryParse("123", out var theInt) { /* use theInt */ }avessimo var candidate = int.TrialParse("123"); if (candidate.Parsed) { /* do something with candidate.Value */ }È più codice, ma è molto più coerente con il design del linguaggio C #.
Michael Meadows

9

Probabilmente potresti scrivere un'intera app C # e non passare mai alcun oggetto / struttura per riferimento.

Ho avuto un professore che mi ha detto questo:

L'unico posto in cui useresti gli ref è dove:

  1. Vuoi passare un oggetto di grandi dimensioni (cioè, l'oggetto / struttura ha oggetti / strutture al suo interno a più livelli) e copiarlo sarebbe costoso e
  2. Stai chiamando un Framework, un'API di Windows o un'altra API che lo richiede.

Non farlo solo perché puoi. Puoi essere morso nel culo da alcuni bug fastidiosi se inizi a modificare i valori in un parametro e non stai prestando attenzione.

Sono d'accordo con il suo consiglio e nei miei cinque anni più dalla scuola, non ne ho mai avuto bisogno al di fuori di chiamare il Framework o l'API di Windows.


3
Se si prevede di implementare "Swap", potrebbe essere utile passare per ref.
Brian

@ Chris farà qualche problema se uso la parola chiave ref per piccoli oggetti?
ManirajSS

@TechnikEmpire "Tuttavia, le modifiche all'oggetto nell'ambito della funzione chiamata non vengono riportate al chiamante." Questo è sbagliato. Se passo una persona a SetName(person, "John Doe"), la proprietà del nome cambierà e tale modifica si rifletterà sul chiamante.
M. Mimpen

@ M.Mimpen Commento eliminato. A quel punto avevo a malapena preso C # e stavo chiaramente parlando del mio * $$. Grazie per averlo portato alla mia attenzione.

@ Chris - Sono abbastanza sicuro che passare un oggetto "grande" non sia costoso. Se lo passi per valore, stai ancora passando un singolo puntatore, giusto?
Ian

3

Poiché ricevuto_s è un array, stai passando un puntatore a quell'array. La funzione manipola i dati esistenti in posizione, senza modificare la posizione o il puntatore sottostante. La parola chiave ref indica che stai passando il puntatore effettivo alla posizione e aggiornando quel puntatore nella funzione esterna, quindi il valore nella funzione esterna cambierà.

Ad esempio, l'array di byte è un puntatore alla stessa memoria prima e dopo, la memoria è stata appena aggiornata.

Il riferimento all'endpoint sta effettivamente aggiornando il puntatore all'endpoint nella funzione esterna a una nuova istanza generata all'interno della funzione.


3

Pensa a un riferimento nel senso che stai passando un puntatore per riferimento. Non usare un ref significa che stai passando un puntatore per valore.

Meglio ancora, ignora ciò che ho appena detto (probabilmente è fuorviante, specialmente con i tipi di valore) e leggi questa pagina MSDN .


In realtà, non è vero. Almeno la seconda parte. Qualsiasi tipo di riferimento verrà sempre passato per riferimento, indipendentemente dal fatto che si utilizzi ref o meno.
Erik Funkenbusch

In realtà, a un'ulteriore riflessione, non è venuto fuori bene. Il riferimento viene effettivamente passato per valore senza il tipo ref. Ciò significa che la modifica dei valori puntati dal riferimento modifica i dati originali, ma la modifica del riferimento stesso non modifica il riferimento originale.
Erik Funkenbusch

2
Un tipo di riferimento non ref non viene passato per riferimento. Un riferimento al tipo di riferimento viene passato per valore. Ma se vuoi pensare a un riferimento come un puntatore a qualcosa a cui viene fatto riferimento, ciò che ho detto ha senso (ma pensare in questo modo potrebbe essere fuorviante). Da qui il mio avvertimento.
Brian

Il collegamento è morto - prova questo invece - docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
GIVE-ME-CHICKEN

0

la mia comprensione è che tutti gli oggetti derivati ​​dalla classe Object vengono passati come puntatori mentre i tipi ordinari (int, struct) non vengono passati come puntatori e richiedono ref. Non sono sicuro della stringa (è in definitiva derivato dalla classe Object?)


Questo potrebbe richiedere qualche chiarimento. Qual è la differenza tra valori e riferimenti. E perché è importante utilizzare la parola chiave ref su un parametro?
dal

In .net, tutto tranne i puntatori, i parametri di tipo e le interfacce è derivato da Object. È importante capire che anche i tipi "ordinari" (chiamati correttamente "tipi valore") ereditano dall'oggetto. Hai ragione da questo in poi: i tipi di valore (per impostazione predefinita) vengono passati per valore, mentre i tipi di riferimento vengono passati per riferimento. Se si desidera modificare un tipo di valore, invece di restituirne uno nuovo (gestirlo come un tipo di riferimento all'interno di un metodo), è necessario utilizzare la parola chiave ref su di esso. Ma questo è un pessimo stile e non dovresti farlo a meno che tu non sia assolutamente sicuro di
doverlo

1
per rispondere alla tua domanda: la stringa è derivata dall'oggetto È un tipo di riferimento che si comporta come un tipo di valore (per motivi logici e di prestazioni)
buddybubble

0

Mentre sono d'accordo con la risposta di Jon Skeet in generale e alcune delle altre risposte, esiste un caso d'uso per l'utilizzo ref, ovvero per rafforzare le ottimizzazioni delle prestazioni. Durante la profilazione delle prestazioni è stato osservato che l'impostazione del valore di ritorno di un metodo ha lievi implicazioni sulle prestazioni, mentre l'utilizzo refcome argomento in cui il valore di ritorno viene popolato in quel parametro provoca la rimozione di questo leggero collo di bottiglia.

Questo è davvero utile solo quando gli sforzi di ottimizzazione sono portati a livelli estremi, sacrificando la leggibilità e forse la testabilità e la manutenibilità per risparmiare millisecondi o forse frazioni di millisecondi.


-1

Prima regola ground zero, le primitive vengono passate per valore (stack) e non primitive per riferimento (heap) nel contesto dei TIPI coinvolti.

I parametri coinvolti vengono passati da Value per impostazione predefinita. Buon post che spiega le cose in dettaglio. http://yoda.arachsys.com/csharp/parameters.html

Student myStudent = new Student {Name="A",RollNo=1};

ChangeName(myStudent);

static void ChangeName(Student s1)
{
  s1.Name = "Z"; // myStudent.Name will also change from A to Z
                // {AS s1 and myStudent both refers to same Heap(Memory)
                //Student being the non-Primitive type
}

ChangeNameVersion2(ref myStudent);
static void ChangeNameVersion2(ref Student s1)
{
  s1.Name = "Z"; // Not any difference {same as **ChangeName**}
}

static void ChangeNameVersion3(ref Student s1)
{
    s1 = new Student{Name="Champ"};

    // reference(myStudent) will also point toward this new Object having new memory
    // previous mystudent memory will be released as it is not pointed by any object
}

Possiamo dire (con avvertimento) I tipi non primitivi non sono altro che Puntatori E quando li passiamo per ref possiamo dire che stiamo passando il Doppio Puntatore


Tutti i parametri vengono passati per valore, per impostazione predefinita, in C #. Qualsiasi parametro può essere passato per riferimento. Per i tipi di riferimento, il valore passato (per riferimento o per valore) è esso stesso un riferimento. Questo è del tutto indipendente da come viene passato.
Servy

D'accordo @Servy! "Quando sentiamo le parole" riferimento "o" valore "usate, dovremmo essere molto chiari se intendiamo che un parametro è un riferimento o un parametro di valore, o se intendiamo che il tipo in questione è un riferimento o un tipo di valore 'Poco Confusione là fuori dalla mia parte, quando ho detto prima regola Ground zero, le primitive vengono passate per valore (stack) e non primitive per riferimento (heap), intendevo per i TIPI coinvolti, non il parametro! Quando parliamo di parametri sei corretto , Tutti i parametri vengono passati per valore, per impostazione predefinita in C #.
Surender Singh Malik

Dire che un parametro è un parametro di riferimento o di valore non è un termine standard ed è davvero ambiguo su cosa intendi. Il tipo del parametro può essere un tipo di riferimento o un tipo di valore. Qualsiasi parametro può essere passato per valore o per riferimento. Questi sono concetti ortogonali per un dato parametro. Il tuo post descrive questo in modo errato.
Servy

1
Si passano i parametri per riferimento o per valore. I termini "per riferimento" e "per valore" non vengono utilizzati per descrivere se un tipo è un tipo di valore o un tipo di riferimento.
Servy

1
No, è non è una questione di percezione. Quando usi il termine errato per riferirti a qualcosa, quell'affermazione diventa sbagliata . È importante che le dichiarazioni corrette utilizzino la terminologia corretta per fare riferimento ai concetti.
Servy
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.