C# : Passage de paramètres
Passage par valeur, par référence (ref), out, in - comment C# transmet les données aux méthodes et ce que ça implique en mémoire.
C# : Passage de paramètres
Quand tu appelles une méthode avec des arguments, que se passe-t-il exactement ? Est-ce que la méthode travaille sur les données originales ou sur une copie ? La réponse dépend du type et du mot-clé utilisé.
Si tu n'as pas encore lu l'article sur la mémoire, commence par là - la distinction entre stack et heap est essentielle pour comprendre ce qui suit.
# Passage par défaut (par valeur)
Par défaut, C# passe une copie de la variable à la méthode. Mais ce qui est copié dépend du type :
Avec un value type
La valeur elle-même est copiée. La méthode travaille sur sa propre copie - l'original ne bouge pas.
void Doubler(int nombre)
{
nombre *= 2; // modifie la copie locale
}
int x = 10;
Doubler(x);
Console.WriteLine(x); // 10 - x n'a pas changévoid Doubler(int nombre){ nombre *= 2; // modifie la copie locale} int x = 10;Doubler(x);Console.WriteLine(x); // 10 - x n'a pas changénombre dans Doubler est une copie indépendante de x. Modifier nombre ne touche pas x.

Avec un reference type
La référence (l'adresse mémoire) est copiée, pas l'objet. Les deux variables pointent donc vers le même objet sur le heap.
void AjouterElement(List<int> liste)
{
liste.Add(99); // modifie l'objet original !
}
List<int> nombres = new List<int> { 1, 2, 3 };
AjouterElement(nombres);
Console.WriteLine(nombres.Count); // 4 - l'objet a été modifiévoid AjouterElement(List<int> liste){ liste.Add(99); // modifie l'objet original !} List<int> nombres = new List<int> { 1, 2, 3 };AjouterElement(nombres);Console.WriteLine(nombres.Count); // 4 - l'objet a été modifiéLa méthode reçoit une copie de la référence, mais cette copie pointe vers le même objet. Modifier l'objet via la copie modifie l'original.

Attention au piège
Copier la référence et modifier l'objet, c'est différent de réassigner la référence :
void Remplacer(List<int> liste)
{
liste = new List<int> { 99, 100 }; // crée un NOUVEL objet
// la variable locale 'liste' pointe maintenant vers le nouveau
// mais l'appelant pointe toujours vers l'ancien
}
List<int> nombres = new List<int> { 1, 2, 3 };
Remplacer(nombres);
Console.WriteLine(nombres.Count); // 3 - inchangé !void Remplacer(List<int> liste){ liste = new List<int> { 99, 100 }; // crée un NOUVEL objet // la variable locale 'liste' pointe maintenant vers le nouveau // mais l'appelant pointe toujours vers l'ancien} List<int> nombres = new List<int> { 1, 2, 3 };Remplacer(nombres);Console.WriteLine(nombres.Count); // 3 - inchangé !liste = new List<>() ne modifie que la copie locale de la référence. L'appelant garde son objet original.
# Le mot-clé ref
ref passe la variable elle-même, pas une copie. La méthode a un accès direct à la variable de l'appelant.
Avec un value type
void Doubler(ref int nombre)
{
nombre *= 2;
}
int x = 10;
Doubler(ref x);
Console.WriteLine(x); // 20 - x a été modifié directementvoid Doubler(ref int nombre){ nombre *= 2;} int x = 10;Doubler(ref x);Console.WriteLine(x); // 20 - x a été modifié directementSans ref, x reste à 10. Avec ref, la méthode modifie x sur la stack de l'appelant.

Avec un reference type
ref sur un reference type permet de réassigner la variable de l'appelant - ce qui est impossible par défaut :
void Remplacer(ref List<int> liste)
{
liste = new List<int> { 99, 100 };
}
List<int> nombres = new List<int> { 1, 2, 3 };
Remplacer(ref nombres);
Console.WriteLine(nombres.Count); // 2 - nombres pointe maintenant vers la nouvelle liste !void Remplacer(ref List<int> liste){ liste = new List<int> { 99, 100 };} List<int> nombres = new List<int> { 1, 2, 3 };Remplacer(ref nombres);Console.WriteLine(nombres.Count); // 2 - nombres pointe maintenant vers la nouvelle liste !C'est la différence clé : sans ref, réassigner dans la méthode ne change rien pour l'appelant. Avec ref, ça le change.

Contrainte
La variable doit être initialisée avant d'être passée en ref. Le compilateur refuse sinon.
int x; // pas initialisé
Doubler(ref x); // Erreur de compilation !int x; // pas initialiséDoubler(ref x); // Erreur de compilation !# Le mot-clé out
out est le cousin de ref. La différence : la méthode doit assigner une valeur au paramètre, et la variable n'a pas besoin d'être initialisée avant l'appel.
bool TryDivide(int a, int b, out int resultat)
{
if (b == 0)
{
resultat = 0; // obligatoire, même en cas d'échec
return false;
}
resultat = a / b;
return true;
}
if (TryDivide(10, 3, out int res))
{
Console.WriteLine(res); // 3
}bool TryDivide(int a, int b, out int resultat){ if (b == 0) { resultat = 0; // obligatoire, même en cas d'échec return false; } resultat = a / b; return true;} if (TryDivide(10, 3, out int res)){ Console.WriteLine(res); // 3}Le pattern le plus courant : les méthodes TryXxx qui retournent un bool (succès/échec) et remplissent le résultat via out.
ref = "je lis et je modifie ta variable". out = "je t'écris une valeur dedans, peu importe ce qu'il y avait avant".
# Le mot-clé in (C# 7.2+)
in est l'inverse de ref : la variable est passée par référence mais la méthode ne peut pas la modifier. C'est une référence en lecture seule.
void Afficher(in int valeur)
{
Console.WriteLine(valeur);
// valeur = 42; // Erreur de compilation ! in = lecture seule
}
int x = 10;
Afficher(in x); // le 'in' côté appelant est optionnel
Afficher(x); // fonctionne aussivoid Afficher(in int valeur){ Console.WriteLine(valeur); // valeur = 42; // Erreur de compilation ! in = lecture seule} int x = 10;Afficher(in x); // le 'in' côté appelant est optionnelAfficher(x); // fonctionne aussiQuand l'utiliser ?
in est utile pour les gros structs. Passer un struct par valeur copie toutes ses données. Avec in, on passe une référence (pas de copie) tout en garantissant que la méthode ne modifie rien.
// Sans in : le struct est copié entièrement à chaque appel
void Traiter(GrosStruct data) { }
// Avec in : seule une référence est passée, pas de copie
void Traiter(in GrosStruct data) { }// Sans in : le struct est copié entièrement à chaque appelvoid Traiter(GrosStruct data) { } // Avec in : seule une référence est passée, pas de copievoid Traiter(in GrosStruct data) { }Pour les types simples (int, bool...), in n'apporte rien - la copie est déjà aussi rapide qu'un passage par référence.
# Récapitulatif
| Mot-clé | Copie ? | Lecture | Écriture | Initialisation requise |
|---|---|---|---|---|
| (aucun) | Oui | Oui | Locale seulement | Oui |
ref | Non | Oui | Oui (modifie l'original) | Oui |
out | Non | Non (avant assignation) | Obligatoire | Non |
in | Non | Oui | Non (lecture seule) | Oui |
# La suite
Tu maîtrises maintenant tous les modes de passage de paramètres en C#. Les prochains articles de la série :
- Les interfaces, structs, records & enums
- C# vs .NET - ce qui est du langage et ce qui est du framework