Notez que ref et out sont tous deux des passages par référence. Les différences entre les deux sont que :
Le passage de paramètres en C# se fait selon quatre modalités :
Ce qui suit donne quelques détails sur chacune de ces modalités.
Le passage par valeur est la modalité par défaut de C#. Aucun mot clé n'est nécessaire. Toutefois, du fait que C# favorise (tristement) l'accès indirect aux objets, il faut comprendre que toute instance d'une classe est manipulée par référence, et que copier une référence implique partager un référé.
Quelques exemples :
À droite, G appelle F en lui passant par valeur la variable n locale à G. Conséquemment, la variable n locale à F est une copie de la variable n locale à G. Quand F modifie n, elle modifie la copie locale à F, et laisse la variable n locale à G intacte (ce sont deux variables distinctes). L'affichage dans G montre donc que n dans G est demeurée intacte suivant les actions de F. |
|
De manière semblable, à droite, Adapter reçoit en paramètre une string. Les string en C# sont des variables de types références (des class), donc Adapter reçoit une copie d'une référence (initialement, dans Adapter, la variable msg réfère au même endroit que la variable s dans Déclamer). Toutefois, l'écriture msg = msg + "!" dans Adapter fait pointer msg, qui est une référence locale à Adapter, ailleurs. La chaîne originale (s dans Déclamer) demeure intacte... De toute manière, les string de C# sont immuables, donc non-modifiables une fois construites. |
|
Avec un objet mutable, le passage par valeur peut être trompeur, étant donné le modèle de C# qui mène par défaut au partage des objets. À droite, le type Entier est mutable (sa propriété Valeur est modifiable post-construction). Quand la fonction Zut appelle la fonction Oups, elle lui passe l'instance et de Entier par valeur, ce qui provoque une copie de e (autrement dit, les variables e et Oups et de Zut sont distinctes), mais les deux variables réfèrent au même objet. Pour cette raison, quand Oups modifie la valeur de l'objet référé par e, elle modifie aussi la valeur de l'objet référé par e dans Zut. |
|
Exemple simple : à droite, la fonction Neg transforme les valeurs du tableau qui lui est passé en paramètre en leur négation arithmétique, ce qui modifie les éléments du tableau suppléé par le code client du fait que le tab, passé par valeur, est une copie d'une référence |
static void Neg(int [] tab)
{
for(int i = 0; i != tab.Length; ++i)
tab[i] = -tab[i];
} |
Le mot clé ref appliqué à un paramètre en C# indique un passage par référence. Avec ce mode de passage de paramètre, le paramètre doit être initialisé avant l'appel, mais l'appelant peut faire référer le paramètre ailleurs que là où il référait à l'origine. C# exige que le code client appose ref au point d'appel dans un tel cas (ce ne sont pas tous les langages qui imposent cette répétition).
L'exemple classique de passage par référence est une fonction qui permute les valeurs de deux variables (ici, des int). Une telle fonction est utile dans une multitude de situations, incluant à peu près tous les algorithmes de tri. |
|
Notez qu'un passage par référence ne peut se faire que sur une variable dans laquelle l'appelé peut écrire. Le code à droite montre un appel correct à la fonction Permuter. Ainsi, ceci serait illégal :
... et ceci serait illégal aussi :
|
|
Mon collègue Philippe Simard m'a proposé cet exemple (que j'ai un peu simplifié) qui montre sous un autre angle l'impact d'appliquer ou pas ref à un paramètre :
|
|
Le mot clé out appliqué à un paramètre en C# indique un paramètre sortant. Avec ce mode de passage de paramètre, le paramètre peut être initialisé avant l'appel mais n'a pas à l'être, car l'appelant a l'obligation d'y écrire. C# exige que le code client appose out au point d'appel dans un tel cas (ce ne sont pas tous les langages qui imposent cette répétition).
Un exemple classique de passage sortant est une fonction qui doit avoir deux extrants, par exemple int.TryParse qui doit à la fois indiquer à l'appelant (a) si la traduction d'une string en entier a fonctionné ou non, et (b) si cette traduction a fonctionné, quel est l'entier résultant. Comme dans le cas de ref, il faut qu'un paramètre passé selon la modalité out soit une variable dans laquelle il est possible d'écrire, ce qui exclut une valeur temporaire anonyme retournée par une fonction, le résultat d'une expression arithmétique ou un littéral par exemple. |
|
C# permet aussi de déclarer un paramètre out à son point d'utilisation. Cette syntaxe étrange est équivalente à la précédente, plus classique, et vise à réduire la quantité de variables non-initialisées flottant dans un programme. Elle n'a d'ailleurs de sens que pour out, car tous les autres modes de passage de paramètres requièrent que les paramètres soient initialisés avant l'appel. |
|
Une implémentation très naïve d'une fonction semblable à TryParse pourrait être celle (inefficace) proposée à droite. Notez que la variable nb est initialisée (ici, à zéro, mais c'est arbitraire) même si la traduction de la string en int a échoué, car une fonction a l'obligation d'écrire dans chaque paramètre out. |
|
Le mot clé in appliqué à un paramètre en C# indique un paramètre intrant. C'est un parent pauvre des paramètres const de C++, mais qui vise un objectif connexe, soit celui de faire un paramètre que l'appelé ne peut pas modifier. Notez la distinction entre passage par valeur et passage in : le passage par valeur provoque une copie, que l'appelé peut modifier à loisir, alors qu'un passage in est un passage par référence mais qui ne permet pas à l'appelé de modifier le paramètre.
Un exemple de passage intrant est proposé à droite. Permettons-nous du même coup quelques remarques recoupant les autres modalités de passage de paramètre :
Le programme principal montre pour sa part quelques exemples d'appels utilisant un paramètre in :
|
|
La « protection » d'un paramètre in est toutefois très « locale » (c'est aussi le cas d'un paramètre const en C++, d’ailleurs). L'exemple suivant (que j'ai pris à Jon Skeet) en montre les limites :
using System;
string? text = "Not null";
Action action = () => text = null;
// boum!
int length = LengthIfPossible(in text, action);
static int LengthIfPossible(in string? text, Action action)
{
if (text == null)
return -1;
action();
return text.Length; // boum!
}
Quelques liens pour complémenter le propos.