Traduction

Ceci est la traduction la plus fidèle possible de l'article de Jon Skeet, Parameter passing in C#Parameter passing in C#.

Préambule : qu'est-ce qu'un type référence ?

En .NET (et donc en C#), il y a deux principales sortes de types : les types référence et les types valeur. Ils se comportent différemment, et une grande partie de la confusion sur le passage de paramètres est en réalité due au fait que certaines personnes ne comprennent pas la différence entre les deux. Voilà une rapide explication :

Un type référence est un type qui a pour valeur une référence aux données appropriées, plutôt que les données elles-même. Par exemple, examinez le code suivant :

 
Sélectionnez
StringBuilder sb = new StringBuilder();

(J'ai utilisé StringBuilder comme un exemple quelconque de type référence - il n'a rien de particulier). Ici, on déclare une variable sb, on crée un nouvel objet StringBuilder, et on affecte à sb une référence à l'objet. La valeur de sb n'est pas l'objet lui-même, mais la référence. L'affectation des types référence est simple - la valeur qui est affectée est la valeur de l'expression ou variable - c'est-à-dire la référence. Ceci est démontré de façon plus poussée par cet exemple :

 
Sélectionnez
StringBuilder first = new StringBuilder();
StringBuilder second = first;

Ici on déclare une variable first, on crée un nouvel objet StringBuilder, et on affecte à first une référence vers l'objet. On affecte ensuite à la variable second la valeur de first. Cela signifie qu'elles font toutes les deux référence au même objet. Cependant, ce sont toujours elles-mêmes des variables indépendantes. Changer la valeur de first ne changera pas celle de second - cependant, tant que leurs valeurs sont des références vers le même objet, les changements faits sur l'objet via la variable first seront aussi visibles via la variable second. En voici un exemple :

 
Sélectionnez
StringBuilder first = new StringBuilder();
StringBuilder second = first;
first.Append("hello");
first = null;
Console.WriteLine(second);

Résultat :

hello

Ici on déclare une variable first, on crée un nouvel objet StringBuilder, et on affecte à first une référence vers l'objet. On affecte ensuite à la variable second la valeur de first. On appelle ensuite la méthode Append sur cet objet via la référence contenue dans la variable first. Après ça, on affecte null (une valeur qui ne fait référence à aucun objet) à la variable first. Enfin, on écrit le résultat de l'appel à la méthode ToString sur l'objet StringBuilder via la référence contenue dans la variable second. Cela affiche hello, ce qui montre bien que même si la valeur de first a changé, les données dans l'objet auquel elle faisait référence n'ont pas changé - et second fait toujours référence à cet objet.

Les types classe, les types interface, les types délégué et les types tableau sont tous des types référence.

Second préambule : qu'est-ce qu'un type valeur ?

Alors que les types référence ont un niveau d'indirection entre la variable et les données réelles, ce n'est pas le cas des types valeur. Les variables de type valeur contiennent directement les données. L'affectation d'un type valeur implique la copie des données réelles. Prenons une simple struct, par exemple :

 
Sélectionnez
public struct IntHolder
{
    public int i;
}

Partout où il y a une variable de type IntHolder, la variable contient toutes les données - en l'occurrence, une simple valeur d'entier. Une affectation copie la valeur, comme démontré ci-après :

 
Sélectionnez
IntHolder first = new IntHolder();
first.i = 5;
IntHolder second = first;
first.i = 6;
Console.WriteLine(second.i);

Résultat :

5

Ici, second.i a la valeur 5, parce que c'est la valeur de first.i quand l'affectation second=first est exécutée - les valeurs dans second sont indépendantes des valeurs dans first sauf au moment où l'affectation a lieu.

Les types simples (comme float, int, char), les types énumération et les types structure sont tous des types valeur.

Remarquez que de nombreux types (comme string) semblent à certains égards être des types valeur, mais sont en fait des types référence. Ils sont appelés types immuables. Cela signifie qu'une fois que l'instance a été construite, elle ne peut pas être modifiée. Cela permet à un type référence de se comporter de façon similaire à un type valeur à certains égards - en particulier, si vous avez une référence à un objet immuable, vous pouvez la renvoyer à partir d'une méthode ou la passer en paramètre d'une autre méthode, sans crainte que l'objet soit modifié dans votre dos. C'est pourquoi, par exemple, la méthode string.Replace ne modifie pas la chaîne sur laquelle elle est appelée, mais renvoie une nouvelle instance avec les nouvelles données de chaîne - si la chaîne originale était modifiée, toutes les autres variables contenant une référence à la chaîne verraient la modification, ce qui est rarement l'effet souhaité.

Remarquez la différence avec un type « mutable » (modifiable) comme ArrayList - si une méthode renvoie une référence d'ArrayList stockée dans une variable d'instance, le code appelant pourrait alors ajouter des éléments à la liste sans que l'instance ait son mot à dire, ce qui pose généralement un problème. Bien que les types référence immuables se comportent comme des types valeur, ce ne sont pas des types valeur, et il ne faut pas les considérer comme étant des types valeur.

Pour plus d'information sur les types valeur, les types référence, et où les données de chacun sont stockées en mémoire, veuillez vous référer à mon autre article à ce sujetMemory in .NET.

Pour vérifier que vous avez compris le préambule...

À quel résultat vous attendriez-vous pour le code précédent si IntHolder était déclaré comme une classe plutôt que comme une structure ? Si vous ne comprenez pas pourquoi le résultat serait 6, veuillez relire les deux préambules, et envoyez-moi un e-mail si ce n'est toujours pas clair - si vous ne comprenez pas, c'est ma faute, pas la vôtre, et il faut que j'améliore cette page. Si vous comprenez, le passage de paramètres devient très facile à comprendre - lisez la suite.

Les différentes sortes de paramètres

Il y a quatre sortes de paramètres différents en C# : les paramètres par valeur (type par défaut), les paramètres par référence (qui utilisent le modificateur ref), les paramètres de sortie (qui utilisent le modificateur out), et les tableaux de paramètres (qui utilisent le modificateur params). Vous pouvez utiliser n'importe quelle sorte de paramètre aussi bien avec des types valeur qu'avec des types référence. Quand vous entendez les mots « référence » ou « valeur » (ou que vous les utilisez vous-même), il faut que ce soit très clair dans votre esprit s'il s'agit du fait qu'un paramètre est passé par référence ou par valeur, ou du fait que le type du paramètre est un type référence ou un type valeur. Si vous séparez bien les deux notions, elles sont très simples.

Paramètres par valeur

Par défaut, les paramètres sont passés par valeur. Cela signifie qu'un nouvel emplacement de stockage est créé pour la variable dans la déclaration de la fonction membre, et qu'il prend initialement la valeur spécifiée lors de l'invocation de la fonction membre. Si vous modifiez cette valeur, cela n'affecte pas les variables utilisées dans l'invocation. Par exemple, si nous avons :

 
Sélectionnez
void Foo(StringBuilder x)
{
    x = null;
}

...

StringBuilder y = new StringBuilder();
y.Append("hello");
Foo(y);
Console.WriteLine(y == null);

Résultat :

False

La valeur de y n'est pas modifiée du fait de l'affectation de null à x. Rappelez-vous cependant que la valeur d'une variable de type référence est la référence - si deux variables de type référence pointent vers le même objet, alors les modifications apportées aux données de l'objet seront visibles via les deux variables. Par exemple :

 
Sélectionnez
void Foo(StringBuilder x)
{
    x.Append(" world");
}

...

StringBuilder y = new StringBuilder();
y.Append("hello");
Foo(y);
Console.WriteLine(y);

Résultat :

hello world

Après l'appel à Foo, l'objet StringBuilder auquel y fait référence contient "hello world", puisque dans Foo les données " world" ont été ajoutées à cet objet via la référence contenue dans x.

Examinons maintenant ce qui se passe quand des types valeur sont passés par valeur. Comme je l'ai dit précédemment, la valeur d'une variable de type valeur est la donnée elle-même. En utilisant la définition précédente de la structure IntHolder, écrivons un code similaire à celui ci-dessus :

 
Sélectionnez
void Foo(IntHolder x)
{
    x.i = 10;
}

...

IntHolder y = new IntHolder();
y.i = 5;
Foo(y);
Console.WriteLine(y.i);

Résultat :

5

Quand Foo est appelée, x est initialement une structure avec la valeur i = 5. Sa valeur de i est ensuite changée en 10. Foo ne sait rien de la variable y, et après que la méthode Foo est terminée, la valeur dans y sera exactement la même qu'avant (c'est-à-dire 5).

Comme précédemment, vérifiez que vous comprenez bien ce qui se passerait si IntHolder était une classe plutôt qu'une structure. Vous devriez comprendre pourquoi y.i vaudrait 10 après l'appel à Foo dans ce cas.

Paramètres par référence

Les paramètres par référence ne passent pas les valeurs des variables utilisées pour l'invocation de la fonction membre - ils utilisent la variable elle-même. Plutôt que de créer un nouvel emplacement de stockage pour la variable dans la déclaration de la fonction membre, c'est le même emplacement qui est utilisé, donc la valeur de la variable dans la fonction membre et la valeur du paramètre par référence seront toujours la même. Les paramètres par référence requièrent le modificateur ref aussi bien dans la déclaration que dans l'invocation - cela implique qu'il est toujours clair que vous passez quelque chose par référence. Regardons les exemples précédents, en changeant juste le paramètre pour qu'il soit par référence :

 
Sélectionnez
void Foo(ref StringBuilder x)
{
    x = null;
}

...

StringBuilder y = new StringBuilder();
y.Append("hello");
Foo(ref y);
Console.WriteLine(y == null);

Résultat :

True

Ici, parce qu'une référence à y est passée plutôt que sa valeur, les modifications de la valeur du paramètre x sont immédiatement répercutées dans y. Dans l'exemple ci-dessus, y devient null. Comparez cela avec le résultat du même code sans le modificateur ref.

Examinons maintenant le code avec la structure que nous avions précédemment, mais en utilisant des paramètres par référence :

 
Sélectionnez
void Foo(ref IntHolder x)
{
    x.i = 10;
}

...

IntHolder y = new IntHolder();
y.i = 5;
Foo(ref y);
Console.WriteLine(y.i);

Résultat :

10

Les deux variables partagent le même emplacement de stockage, donc les modifications dans x sont également visibles via y, donc y.i a la valeur 10 à la fin de ce code.

Parenthèse : quelle est la différence entre passer un objet valeur par référence et passer un objet référence par valeur ?

Vous avez peut-être remarqué que le dernier exemple, où on passait une structure par référence, avait le même effet dans ce code que de passer un une classe par valeur. Cela ne veut cependant pas dire que c'est la même chose. Examinez le code suivant :

 
Sélectionnez
void Foo(??? IntHolder x)
{
    x = new IntHolder();
}

...

IntHolder y = new IntHolder();
y.i = 5;
Foo(??? y);

Dans le cas où IntHolder est une structure (donc un type valeur) et où le paramètre est passé par référence (c'est-à-dire qu'on remplace ??? par ref ci-dessus), y devient une nouvelle valeur IntHolder - c'est-à-dire que y.i vaut 0. Dans le cas où IntHolder est une classe (donc un type référence) et où le paramètre est passé par valeur (c'est-à-dire qu'on enlève le ??? ci-dessus), la valeur de y n'est pas modifiée - c'est une référence vers le même objet qu'avant l'appel à la fonction membre. Cette différence est absolument cruciale à la compréhension du passage de paramètres en C#, et c'est pourquoi je pense qu'il est extrêmement déroutant de dire que les objets sont passés par référence par défaut, plutôt que l'affirmation correcte qui est que les références d'objet sont passées par valeur par défaut.

Paramètres de sortie

Comme les paramètres par référence, les paramètres de sortie ne créent pas un nouvel emplacement de stockage, mais utilisent l'emplacement de stockage de la variable spécifiée dans l'invocation. Les paramètres de sortie requièrent le modificateur out aussi bien dans la déclaration que dans de l'invocation - cela implique qu'il est toujours clair que vous passez quelque chose comme paramètre de sortie.

Les paramètres de sortie sont très similaires aux paramètres par référence. Les seules différences sont les suivantes :

  • la variable spécifiée dans l'invocation n'a pas besoin d'être affectée avant d'être passée en paramètre de la fonction membre. Si la fonction membre se termine normalement, elle est considérée comme initialisée après le retour de la fonction (vous pouvez donc ensuite la « lire ») ;
  • le paramètre est considéré comme non-initialisé (autrement dit, vous devez lui affecter une valeur avant de pouvoir le « lire » dans la fonction membre) ;
  • la fonction membre doit affecter une valeur au paramètre avant de se terminer normalement.

Voici un exemple de code qui illustre cela, avec un paramètre int (int est un type valeur, mais si vous avez correctement compris les paramètres par référence, vous devriez être capable de voir quel serait le comportement avec un type référence) :

 
Sélectionnez
void Foo(out int x)
{
    // On ne peut pas lire x ici - il est considéré comme non-initialisé

    // Affectation - cela doit être fait avant que la méthode puisse se terminer normalement
    x = 10;

    // La valeur de x peut maintenant être lue
    int a = x;
}

...

// Déclaration d'une variable mais sans lui affecter de valeur
int y;

// Passage de la variable en tant que paramètre de sortie, bien qu'elle ne soit pas initialisée
Foo(out y);

// Une valeur a été affectée, on peut donc l'afficher
Console.WriteLine(y);

Résultat :

10

Tableaux de paramètres

Les tableaux de paramètres permettent de passer un nombre variable d'arguments à une fonction membre. La définition du paramètre doit inclure le modificateur params, mais il n'y a pas de mot-clé à spécifier pour l'utilisation du paramètre. Un tableau de paramètres doit se trouver à la fin de la liste des paramètres, et doit être un tableau à une dimension. Lors de l'utilisation de la fonction membre, n'importe quel nombre de paramètre (zéro inclus) peut apparaître dans l'invocation, tant qu'ils sont tous compatibles avec le type du tableau de paramètres. Une autre option est de passer un seul tableau, et dans ce cas le paramètre se comporte comme un paramètre par valeur normal. Par exemple :

 
Sélectionnez
void ShowNumbers(params int[] numbers)
{
    foreach(int x in numbers)
    {
        Console.Write(x + " ");
    }
    Console.WriteLine();
}

...

int[] x = {1, 2, 3};
ShowNumbers(x);
ShowNumbers(4, 5);

Résultat :

1 2 3
4 5

Dans la première invocation, la valeur de la variable x est passée par valeur, car le type de x est déjà un tableau. Dans la deuxième invocation, un nouveau tableau d'entiers est créé et contient les deux valeurs spécifiées, et une référence à ce tableau est passée (par valeur).

Mini-glossaire

Quelques définitions informelles et rappels des termes utilisés :

  • fonction membre: une fonction membre est une méthode, une propriété, un événement, un indexeur, un opérateur défini par l'utilisateur, un constructeur d'instance, un constructeur statique, ou un destructeur ;
  • paramètre de sortie : un paramètre similaire à un paramètre par référence, mais avec des règles d'assignation définitive différentes ;
  • paramètre par référence (sémantique de passage par référence) : un paramètre qui partage l'emplacement de stockage de la variable utilisée dans l'invocation de la méthode membre. Puisqu'ils partagent le même emplacement de stockage, ils ont toujours la même valeur (donc changer la valeur du paramètre change aussi la valeur de la variable utilisée dans l'invocation) ;
  • type référence: type pour lequel la valeur d'une variable ou expression de ce type est une référence vers un objet plutôt que l'objet lui-même ;
  • emplacement de stockage : zone de de mémoire qui contient la valeur d'une variable ;
  • paramètre par valeur (sémantique par défaut de passage par valeur) : paramètre qui a son propre emplacement de stockage, et donc sa propre valeur. Sa valeur initiale est celle de l'expression utilisée dans l'invocation de la fonction membre ;
  • type valeur : type pour lequel la valeur d'une variable ou expression de ce type est les données de l'objet lui-même ;
  • variable : nom associé à un emplacement de stockage et à un type. (Habituellement une seule variable est associée à un emplacement de stockage ; les exceptions sont les paramètres par référence et de sortie).

Remerciements

Je tiens à remercier Jon Skeet pour son aimable autorisation de traduire cet article, ainsi que _Max_Profil de _Max_ pour sa relecture attentive et ses corrections.