Quand utiliser ref et quand ce n'est pas nécessaire en C #

104

J'ai un objet qui est mon état en mémoire du programme et j'ai également d'autres fonctions de travail auxquelles je passe l'objet pour modifier l'état. Je l'ai passé par référence aux fonctions de travail. Cependant, je suis tombé sur la fonction suivante.

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

Cela me déroute parce que les deux received_set remoteEPretournent des trucs de la fonction. Pourquoi a-t-il remoteEPbesoin d'un refet received_snon?

Je suis également programmeur ac donc j'ai du mal à sortir les pointeurs de ma tête.

Edit: On dirait que les objets en C # sont des pointeurs vers l'objet sous le capot. Ainsi, lorsque vous passez un objet à une fonction, vous pouvez alors modifier le contenu de l'objet via le pointeur et la seule chose passée à la fonction est le pointeur vers l'objet afin que l'objet lui-même ne soit pas copié. Vous utilisez ref ou out si vous voulez pouvoir basculer ou créer un nouvel objet dans la fonction qui est comme un double pointeur.

Rex Logan
la source

Réponses:

217

Réponse courte: lisez mon article sur le passage des arguments .

Réponse longue: lorsqu'un paramètre de type référence est passé par valeur, seule la référence est passée, pas une copie de l'objet. C'est comme passer un pointeur (par valeur) en C ou C ++. Les modifications de la valeur du paramètre lui-même ne seront pas vues par l'appelant, mais les modifications de l'objet vers lequel pointe la référence seront vues.

Lorsqu'un paramètre (de n'importe quel type) est passé par référence, cela signifie que toute modification du paramètre est vue par l'appelant - les modifications apportées au paramètre sont des modifications de la variable.

L'article explique tout cela plus en détail, bien sûr :)

Réponse utile: vous n'avez presque jamais besoin d'utiliser ref / out . C'est fondamentalement un moyen d'obtenir une autre valeur de retour, et devrait généralement être évité précisément parce que cela signifie que la méthode essaie probablement d'en faire trop. Ce n'est pas toujours le cas ( TryParseetc. sont les exemples canoniques d'utilisation raisonnable de out) mais l'utilisation de ref / out devrait être une rareté relative.

Jon Skeet
la source
38
Je pense que vous avez confondu votre réponse courte et votre réponse longue; c'est un gros article!
Programmeur hors
23
@Outlaw: Oui, mais la réponse courte elle-même, la directive pour lire l'article, ne fait que 6 mots :)
Jon Skeet
27
Une référence! :)
gonzobrains
5
@Liam Utiliser ref comme vous le faites peut vous sembler plus explicite, mais cela pourrait en fait être déroutant pour d'autres programmeurs (ceux qui savent ce que fait ce mot-clé de toute façon), car vous dites essentiellement aux appelants potentiels: "Je peux modifier la variable vous utilisé dans la méthode appelante, c'est-à-dire le réaffecter à un objet différent (ou même à null, si possible) donc ne vous y accrochez pas, ou assurez-vous de le valider quand j'en ai fini avec lui ». C'est assez fort et complètement différent de "cet objet peut être modifié", ce qui est toujours le cas à chaque fois que vous passez une référence d'objet en paramètre.
mbargiel
1
@ M.Mimpen: C # 7 permettra (espérons-le)if (int.TryParse(text, out int value)) { ... use value here ... }
Jon Skeet
26

Pensez à un paramètre non-ref comme étant un pointeur et à un paramètre ref comme un double pointeur. Cela m'a le plus aidé.

Vous ne devriez presque jamais passer de valeurs par réf. Je soupçonne que si ce n'était pas pour des questions d'interopérabilité, l'équipe .Net ne l'aurait jamais inclus dans la spécification d'origine. La manière OO de traiter la plupart des problèmes que les paramètres ref résolvent est de:

Pour plusieurs valeurs de retour

  • Créer des structures qui représentent les multiples valeurs de retour

Pour les primitives qui changent dans une méthode à la suite de l'appel de méthode (la méthode a des effets secondaires sur les paramètres primitifs)

  • Implémenter la méthode dans un objet en tant que méthode d'instance et manipuler l'état de l'objet (pas les paramètres) dans le cadre de l'appel de méthode
  • Utilisez la solution de valeurs de retour multiples et fusionnez les valeurs de retour avec votre état
  • Créez un objet contenant un état qui peut être manipulé par une méthode et passez cet objet comme paramètre, et non les primitives elles-mêmes.
Michael Meadows
la source
8
Bon dieu. Je devrais lire ce 20x pour le comprendre. Cela me semble être un travail supplémentaire pour faire quelque chose de simple.
PositiveGuy
Le framework .NET ne suit pas votre règle n ° 1. ('Pour plusieurs valeurs de retour, créez des structures'). Prenons par exemple IPAddress.TryParse(string, out IPAddress).
Swen Kooij
@SwenKooij Vous avez raison. Je suppose que dans la plupart des endroits où ils utilisent des paramètres, soit (a) ils enveloppent une API Win32, soit (b) c'était le début, et les programmeurs C ++ prenaient des décisions.
Michael Meadows
@SwenKooij Je sais que cette réponse est en retard de plusieurs années, mais c'est le cas. Nous sommes habitués à TryParse, mais cela ne veut pas dire que c'est bon. Cela aurait été mieux si, plutôt que if (int.TryParse("123", out var theInt) { /* use theInt */ }nous, il y avait var candidate = int.TrialParse("123"); if (candidate.Parsed) { /* do something with candidate.Value */ }plus de code, mais c'est beaucoup plus cohérent avec la conception du langage C #.
Michael Meadows
9

Vous pourriez probablement écrire une application C # entière et ne jamais transmettre d'objets / structures par ref.

J'ai eu un professeur qui m'a dit ceci:

Le seul endroit où vous utiliseriez des références est celui où vous:

  1. Vous voulez passer un gros objet (c'est-à-dire que les objets / struct contient des objets / structures à l'intérieur à plusieurs niveaux) et le copier serait coûteux et
  2. Vous appelez un Framework, une API Windows ou une autre API qui en a besoin.

Ne le faites pas simplement parce que vous le pouvez. Vous pouvez être mordu par des bugs désagréables si vous commencez à changer les valeurs d'un paramètre et que vous ne faites pas attention.

Je suis d'accord avec ses conseils, et au cours de mes cinq années et plus depuis l'école, je n'en ai jamais eu besoin en dehors d'appeler le Framework ou l'API Windows.

Chris
la source
3
Si vous prévoyez d'implémenter "Swap", passer par ref pourrait être utile.
Brian
@Chris cela posera-t-il un problème si j'utilise le mot-clé ref pour de petits objets?
ManirajSS
@TechnikEmpire "Mais, les modifications apportées à l'objet dans la portée de la fonction appelée ne sont pas renvoyées à l'appelant." C'est faux. Si je passe une personne à SetName(person, "John Doe"), la propriété name changera et ce changement sera répercuté sur l'appelant.
M. Mimpen
@ M.Mimpen Commentaire supprimé. J'avais à peine pris C # à ce moment-là et je parlais clairement de mon * $$. Merci de l'avoir porté à mon attention.
@Chris - Je suis presque sûr que passer un "gros" objet n'est pas cher. Si vous le passez simplement par valeur, vous ne passez toujours qu'un seul pointeur, n'est-ce pas?
Ian
3

Puisque receive_s est un tableau, vous passez un pointeur vers ce tableau. La fonction manipule ces données existantes en place, sans modifier l'emplacement ou le pointeur sous-jacent. Le mot-clé ref signifie que vous passez le pointeur réel vers l'emplacement et que vous mettez à jour ce pointeur dans la fonction extérieure, de sorte que la valeur de la fonction extérieure change.

Par exemple, le tableau d'octets est un pointeur vers la même mémoire avant et après, la mémoire vient d'être mise à jour.

La référence Endpoint met en fait à jour le pointeur vers Endpoint dans la fonction extérieure vers une nouvelle instance générée à l'intérieur de la fonction.

Chris Hynes
la source
3

Pensez à une référence comme signifiant que vous passez un pointeur par référence. Ne pas utiliser de référence signifie que vous passez un pointeur par valeur.

Mieux encore, ignorez ce que je viens de dire (c'est probablement trompeur, en particulier avec les types de valeur) et lisez cette page MSDN .

Brian
la source
En fait, pas vrai. Au moins la deuxième partie. Tout type de référence sera toujours passé par référence, que vous utilisiez ref ou non.
Erik Funkenbusch
En fait, après réflexion, cela ne s'est pas bien passé. La référence est en fait passée par valeur sans le type ref. Cela signifie que la modification des valeurs pointées par la référence modifie les données d'origine, mais la modification de la référence elle-même ne change pas la référence d'origine.
Erik Funkenbusch
2
Un type de référence non-ref n'est pas passé par référence. Une référence au type de référence est passée par valeur. Mais si vous voulez penser à une référence comme un pointeur vers quelque chose qui est référencé, ce que j'ai dit a du sens (mais penser de cette façon peut être trompeur). D'où mon avertissement.
Brian
Le lien est mort - essayez celui-ci à la place - docs.microsoft.com/en-us/dotnet/csharp/language-reference
GIVE-ME-CHICKEN
0

ma compréhension est que tous les objets dérivés de la classe Object sont passés en tant que pointeurs alors que les types ordinaires (int, struct) ne sont pas passés en tant que pointeurs et nécessitent une référence. Je ne suis ni sûr de la chaîne (est-elle finalement dérivée de la classe Object?)

Lucas
la source
Cela pourrait nécessiter des éclaircissements. Quelle est la différence entre les tyoes de valeur et de référence. Ans pourquoi est-ce pertinent pour l'utilisation du mot clé ref sur un paramètre?
oɔɯǝɹ
Dans .net, tout sauf les pointeurs, les paramètres de type et les interfaces est dérivé d'Object. Il est important de comprendre que les types "ordinaires" (correctement appelés "types valeur") héritent également de l'objet. Vous avez raison à partir de là: les types valeur (par défaut) sont passés par valeur, tandis que les types référence sont passés par référence. Si vous vouliez modifier un type de valeur, plutôt que d'en renvoyer un nouveau (le gérer comme un type de référence dans une méthode), vous devrez utiliser le mot clé ref dessus. Mais c'est un mauvais style et vous ne devriez tout simplement pas le faire à moins d'être absolument sûr que vous devez le faire :)
buddybubble
1
pour répondre à votre question: la chaîne est dérivée de l'objet.C'est un type de référence qui se comporte comme un type de valeur (pour des raisons de performances et de logique)
buddybubble
0

Bien que je sois d'accord avec la réponse de Jon Skeet dans l'ensemble et certaines des autres réponses, il existe un cas d'utilisation pour l'utilisation ref, et c'est pour renforcer les optimisations des performances. Lors du profilage des performances, il a été observé que la définition de la valeur de retour d'une méthode a de légères implications sur les performances, tandis que l'utilisation refcomme argument par lequel la valeur de retour est renseignée dans ce paramètre entraîne la suppression de ce léger goulot d'étranglement.

Cela n'est vraiment utile que lorsque les efforts d'optimisation sont poussés à des niveaux extrêmes, sacrifiant la lisibilité et peut-être la testabilité et la maintenabilité pour gagner des millisecondes ou peut-être des millisecondes fractionnées.

Jon Davis
la source
-1

Règle de base zéro d'abord, les primitives sont passées par valeur (pile) et non primitives par référence (tas) dans le contexte des TYPES impliqués.

Les paramètres impliqués sont transmis par valeur par défaut. Bon article qui explique les choses en détail. 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
}

Nous pouvons dire (avec avertissement) que les types non primitifs ne sont rien d'autre que des pointeurs Et quand nous les passons par ref, nous pouvons dire que nous passons Double Pointer

Surender Singh Malik
la source
Tous les paramètres sont passés par valeur, par défaut, en C #. Tout paramètre peut être passé par référence. Pour les types référence, la valeur transmise (par référence ou par valeur) est elle-même une référence. Cela est totalement indépendant de la manière dont il est adopté.
Servy le
D'accord @Servy! "Lorsque nous entendons les mots" référence "ou" valeur "utilisés, nous devons être très clairs à l'esprit si nous voulons dire qu'un paramètre est un paramètre de référence ou de valeur, ou si nous voulons dire que le type impliqué est un type de référence ou de valeur 'Peu Confusion là-bas de mon côté, quand j'ai dit la règle de zéro en premier, les primitives sont passées par valeur (pile) et non primitives par référence (tas) Je voulais dire des TYPES impliqués, pas du paramètre! Quand nous parlons de paramètres, vous avez raison , Tous les paramètres sont passés par valeur, par défaut en C #.
Surender Singh Malik
Dire qu'un paramètre est un paramètre de référence ou de valeur n'est pas un terme standard et est vraiment ambigu quant à ce que vous entendez. Le type du paramètre peut être soit un type référence, soit un type valeur. Tout paramètre peut être passé par valeur ou par référence. Ce sont des concepts orthogonaux pour tout paramètre donné. Votre message décrit cela de manière incorrecte.
Servy le
1
Vous passez des paramètres par référence ou par valeur. Les termes «par référence» et «par valeur» ne sont pas utilisés pour décrire si un type est un type valeur ou un type référence.
Servy le
1
Non, ce n'est pas une question de perception. Lorsque vous utilisez le terme incorrect pour faire référence à quelque chose, cette déclaration devient fausse . Il est important que les déclarations correctes utilisent la terminologie correcte pour faire référence aux concepts.
Servy le