Paramètres de méthodes

Par défaut, les arguments en C# sont passés aux fonctions par valeur. Cela signifie qu’une copie de la variable est passée à la méthode. Pour les types valeur (struct), une copie de la valeur est passée à la méthode. Pour les types référence (class), une copie de la référence est passée à la méthode. Les modificateurs de paramètres vous permettent de passer des arguments par référence. Les concepts suivants vous aident à comprendre ces distinctions et à utiliser les modificateurs de paramètres :

  • Passer par valeur signifie passer une copie de la variable à la méthode.
  • Passer par référence signifie passer l’accès à la variable à la méthode.
  • Une variable d’un type référence contient une référence à ses données.
  • Une variable d’un type valeur contient ses données directement.

Étant donné qu’un struct est un type valeur, la méthode reçoit et traite une copie de l’argument struct quand vous passez un struct par valeur à une méthode. La méthode n’a pas accès au struct d’origine dans la méthode d’appel, et ne peut donc pas le modifier de quelque manière que ce soit. La méthode peut modifier uniquement la copie.

Une instance de classe est un type référence, pas un type valeur. Quand un type référence est passé par valeur à une méthode, celle-ci reçoit une copie de la référence à l’instance de classe. Les deux variables font référence au même objet. Le paramètre est une copie de la référence. La méthode appelée ne peut pas réaffecter l’instance dans la méthode appelante. Toutefois, la méthode appelée peut utiliser la copie de la référence pour accéder aux membres de l’instance. Si la méthode appelée modifie un membre d’instance, la méthode appelante voit également ces modifications, car elle fait référence à la même instance.

La sortie de l’exemple suivant illustre la différence. La méthode ClassTaker change la valeur du champ willIChange, car la méthode utilise l’adresse mentionnée dans le paramètre pour rechercher le champ spécifié de l’instance de classe. Le champ willIChange du struct de la méthode d’appel ne change pas de l’appel à StructTaker, car la valeur de l’argument est une copie du struct lui-même, et non pas une copie de son adresse. StructTaker change la copie, et cette dernière est perdue quand l’appel à StructTaker est terminé.

class TheClass
{
    public string? willIChange;
}

struct TheStruct
{
    public string willIChange;
}

class TestClassAndStruct
{
    static void ClassTaker(TheClass c)
    {
        c.willIChange = "Changed";
    }

    static void StructTaker(TheStruct s)
    {
        s.willIChange = "Changed";
    }

    public static void Main()
    {
        TheClass testClass = new TheClass();
        TheStruct testStruct = new TheStruct();

        testClass.willIChange = "Not Changed";
        testStruct.willIChange = "Not Changed";

        ClassTaker(testClass);
        StructTaker(testStruct);

        Console.WriteLine("Class field = {0}", testClass.willIChange);
        Console.WriteLine("Struct field = {0}", testStruct.willIChange);
    }
}
/* Output:
    Class field = Changed
    Struct field = Not Changed
*/

Combinaisons de type de paramètre et de mode d’argument

Les modifications apportées à l’argument qui sont visibles à partir de l’appelant dépendent de la façon dont un argument est passé et de s’il s’agit d’un type référence ou d’un type valeur :

  • Lorsque vous passez un type valeurpar valeur :
    • Si la méthode affecte le paramètre pour faire référence à un autre objet, ces modifications ne sont pas visibles à partir de l’appelant.
    • Si la méthode modifie l’état de l’objet auquel le paramètre fait référence, ces modifications ne sont pas visibles par l’appelant.
  • Lorsque vous passez un type référencepar valeur :
    • Si la méthode affecte le paramètre pour faire référence à un autre objet, ces modifications ne sont pas visibles à partir de l’appelant.
    • Si la méthode modifie l’état de l’objet auquel le paramètre fait référence, ces modifications sont visibles à partir de l’appelant.
  • Lorsque vous transmettez un type valeurpar référence :
    • Si la méthode affecte le paramètre pour faire référence à un autre objet, ces modifications ne sont pas visibles à partir de l’appelant.
    • Si la méthode modifie l’état de l’objet auquel le paramètre fait référence, ces modifications sont visibles à partir de l’appelant.
  • Lorsque vous passez un type référencepar référence :
    • Si la méthode affecte le paramètre pour faire référence à un autre objet, ces modifications sont visibles à partir de l’appelant.
    • Si la méthode modifie l’état de l’objet auquel le paramètre fait référence, ces modifications sont visibles à partir de l’appelant.

Le passage d’un type référence par référence permet à la méthode appelée de remplacer l’objet auquel fait référence le paramètre de référence dans l’appelant. L'emplacement de stockage de l'objet est passé à la méthode comme valeur du paramètre de référence. Si vous modifiez la valeur de l'emplacement de stockage du paramètre (pour pointer vers un nouvel objet), vous modifiez également l'emplacement de stockage auquel fait référence l'appelant. L'exemple suivant passe une instance d'un type référence en tant que paramètre ref.

class Product
{
    public Product(string name, int newID)
    {
        ItemName = name;
        ItemID = newID;
    }

    public string ItemName { get; set; }
    public int ItemID { get; set; }
}

private static void ChangeByReference(ref Product itemRef)
{
    // Change the address that is stored in the itemRef parameter.
    itemRef = new Product("Stapler", 12345);
}

private static void ModifyProductsByReference()
{
    // Declare an instance of Product and display its initial values.
    Product item = new Product("Fasteners", 54321);
    System.Console.WriteLine("Original values in Main.  Name: {0}, ID: {1}\n",
        item.ItemName, item.ItemID);

    // Pass the product instance to ChangeByReference.
    ChangeByReference(ref item);
    System.Console.WriteLine("Calling method.  Name: {0}, ID: {1}\n",
        item.ItemName, item.ItemID);
}

// This method displays the following output:
// Original values in Main.  Name: Fasteners, ID: 54321
// Calling method.  Name: Stapler, ID: 12345

Contexte sécurisé des références et des valeurs

Les méthodes peuvent stocker les valeurs des paramètres dans des champs. Lorsque les paramètres sont passés par valeur, cela est généralement fait de façon sécurisée. Les valeurs sont copiées et les types de référence sont accessibles lorsqu’ils sont stockés dans un champ. La transmission de paramètres par référence de façon sûre nécessite que le compilateur définisse quand il est sûr d’affecter une référence à une nouvelle variable. Pour chaque expression, le compilateur définit un contexte sécurisé qui limite l’accès à une expression ou à une variable. Le compilateur utilise deux étendues : safe-context et ref-safe-context.

  • L’étendue safe-context définit l’étendue dans laquelle n’importe quelle expression est accessible en toute sécurité.
  • L’étendue ref-safe-context définit l’étendue dans laquelle une référence à n’importe quelle expression peut être accessible ou modifiée en toute sécurité.

De manière informelle, vous pouvez considérer ces étendues comme le mécanisme permettant de vous assurer que votre code n’accède jamais ou ne modifie jamais une référence qui n’est plus valide. Une référence est valide tant qu’elle fait référence à un objet ou à un struct valide. L’étendue safe-context définit quand une variable peut être affectée ou réaffectée. L’étendue ref-safe-context définit quand une variable peut être affectée par référence ou réaffectée par référence. L’affectation affecte une variable à une nouvelle valeur ; l’affectation par référence affecte la variable pour faire référence à un autre emplacement de stockage.

Paramètres de référence

Vous appliquez l’un des modificateurs suivants à une déclaration de paramètre pour passer des arguments par référence plutôt que par valeur :

  • ref : l’argument doit être initialisé avant d’appeler la méthode. La méthode peut affecter une nouvelle valeur au paramètre, mais n’est pas obligée de le faire.
  • out : la méthode appelante n’est pas obligée d’initialiser l’argument avant d’appeler la méthode. La méthode doit affecter une valeur au paramètre.
  • readonly ref : l’argument doit être initialisé avant d’appeler la méthode. La méthode ne peut pas affecter de nouvelle valeur au paramètre.
  • in : l’argument doit être initialisé avant d’appeler la méthode. La méthode ne peut pas affecter de nouvelle valeur au paramètre. Le compilateur peut créer une variable temporaire pour contenir une copie de l’argument pour les paramètres in.

Les membres d’une classe ne peuvent pas avoir de signatures qui diffèrent uniquement par ref, ref readonly, in ou out. Une erreur de compilation se produit si la seule différence entre deux membres d’un type est que l’un d’eux a un paramètre ref et que l’autre un a un paramètre out, ref readonly ou in. Toutefois, les méthodes peuvent être surchargées quand une méthode a un paramètre ref, ref readonly, in, ou out et que l’autre a un paramètre transmis par valeur, comme illustré dans l’exemple suivant. Dans d'autres situations nécessitant une correspondance de signature, comme le masquage ou la substitution, in, ref, ref readonly et out font partie de la signature et ne correspondent pas l’un à l’autre.

Lorsqu’un paramètre a l’un des modificateurs précédents, l’argument correspondant peut avoir un modificateur compatible :

  • Un argument pour un paramètre ref doit inclure le modificateur ref.
  • Un argument pour un paramètre out doit inclure le modificateur out.
  • Un argument pour un paramètre in peut éventuellement inclure le modificateur in. Si le modificateur ref est utilisé sur l’argument à la place, le compilateur émet un avertissement.
  • Un argument pour un paramètre ref readonly doit inclure soit le modificateur in, soit le ref, mais pas les deux. Si aucun modificateur n’est inclus, le compilateur émet un avertissement.

Lorsque vous utilisez ces modificateurs, ils décrivent comment l’argument est utilisé :

  • ref signifie que la méthode peut lire ou écrire la valeur de l’argument.
  • out signifie que la méthode définit la valeur de l’argument.
  • ref readonly signifie que la méthode lit, mais ne peut pas écrire la valeur de l’argument. L’argument doit être passé par référence.
  • in signifie que la méthode lit, mais ne peut pas écrire la valeur de l’argument. L’argument est passé par référence ou par le biais d’une variable temporaire.

Les propriétés ne sont pas des variables. Elles sont des méthodes et ne peuvent pas être transmises aux paramètres ref. Vous ne pouvez pas utiliser les modificateurs de paramètres précédents dans les types de méthodes suivants :

  • Méthodes async, que vous définissez à l’aide du modificateur async.
  • Les méthodes Iterator, qui incluent une instruction yield return ou yield break.

Les méthodes d’extension ont également des restrictions sur l’utilisation de ces mots clés d’argument :

  • Le out mot clé ne peut pas être utilisé sur le premier argument d’une méthode d’extension.
  • Le mot clé ref ne peut pas être utilisé sur le premier argument d’une méthode d’extension lorsque l’argument n’est pas un struct, ou qu’un type générique n’est pas contraint d’être un struct.
  • Les mots clés ref readonly et in ne peuvent pas être utilisés, sauf si le premier argument est un struct.
  • Les mots clés ref readonly et in ne peuvent pas être utilisés sur n’importe quel type générique, même s’ils sont contraints d’être un struct.

Modificateur de paramètre ref

Pour utiliser un paramètre ref, la définition de la méthode et la méthode d'appel doivent utiliser explicitement le mot clé ref, comme indiqué dans l'exemple suivant. (Sauf que la méthode appelante peut omettre ref lors de l’appel COM.)

void Method(ref int refArgument)
{
    refArgument = refArgument + 44;
}

int number = 1;
Method(ref number);
Console.WriteLine(number);
// Output: 45

Un argument passé à un paramètre ref doit être initialisé avant d'être transmis.

Modificateur de paramètre out

Pour utiliser un paramètre out, la définition de la méthode et la méthode d'appel doivent utiliser explicitement le mot clé out. Par exemple :

int initializeInMethod;
OutArgExample(out initializeInMethod);
Console.WriteLine(initializeInMethod);     // value is now 44

void OutArgExample(out int number)
{
    number = 44;
}

Les variables passées comme arguments out n’ont pas à être initialisées avant d’être passées dans un appel de méthode. Toutefois, la méthode appelée doit assigner une valeur avant le retour de la méthode.

Les méthodes deconstruct déclarent leurs paramètres avec le modificateur out pour retourner plusieurs valeurs. D’autres méthodes peuvent retourner des tuples de valeur pour plusieurs valeurs renvoyées.

Vous pouvez déclarer une variable dans une instruction distincte avant de la passer en tant qu’outargument. Vous pouvez également déclarer la out variable dans la liste d’arguments de l’appel de méthode au lieu de le faire dans une déclaration de variable distincte. Les déclarations de variable out rendent le code plus simple et plus lisible, et vous empêchent également d’assigner par inadvertance une valeur à la variable avant l’appel de méthode. L’exemple suivant définit la variable number dans l’appel de la méthode Int32.TryParse.

string numberAsString = "1640";

if (Int32.TryParse(numberAsString, out int number))
    Console.WriteLine($"Converted '{numberAsString}' to {number}");
else
    Console.WriteLine($"Unable to convert '{numberAsString}'");
// The example displays the following output:
//       Converted '1640' to 1640

Vous pouvez également déclarer une variable locale implicitement typée.

Modificateur ref readonly

Le modificateur ref readonly doit être présent dans la déclaration de méthode. Un modificateur sur le site d’appel est facultatif. Vous pouvez utiliser le modificateur in ou ref. Le modificateur ref readonly n’est pas valide sur le site d’appel. Le modificateur que vous utilisez sur le site d’appel peut vous aider à décrire les caractéristiques de l’argument. Vous ne pouvez utiliser ref que si l’argument est une variable et est accessible en écriture. Vous ne pouvez utiliser in que lorsque l’argument est une variable. Il peut être accessible en écriture ou en lecture seule. Vous ne pouvez pas ajouter ces modificateurs si l’argument n’est pas une variable, mais qu’il s’agit d’une expression. Les exemples suivants illustrent ces conditions. La méthode suivante utilise le modificateur ref readonly pour indiquer qu’un struct volumineux doit être passé par référence pour des raisons de performances :

public static void ForceByRef(ref readonly OptionStruct thing)
{
    // elided
}

Vous pouvez appeler la méthode à l’aide du modificateur ref ou in. Si vous omettez le modificateur, le compilateur émet un avertissement. Lorsque l’argument est une expression, et non une variable, vous ne pouvez pas ajouter les modificateurs in ou ref. Vous devez donc supprimer l’avertissement :

ForceByRef(in options);
ForceByRef(ref options);
ForceByRef(options); // Warning! variable should be passed with `ref` or `in`
ForceByRef(new OptionStruct()); // Warning, but an expression, so no variable to reference

Si la variable est une variable readonly, vous devez utiliser le modificateur in. Le compilateur émet une erreur si vous utilisez le modificateur ref à la place.

Le modificateur ref readonly indique que la méthode s’attend à ce que l’argument soit une variable plutôt qu’une expression qui n’est pas une variable. Des constantes, des valeurs de retour de méthode et des propriétés sont des exemples d’expressions qui ne sont pas des variables. Si l’argument n’est pas une variable, le compilateur émet un avertissement.

Modificateur de paramètre in

Le modificateur in est requis dans la déclaration de méthode, mais inutile sur le site d’appel.

int readonlyArgument = 44;
InArgExample(readonlyArgument);
Console.WriteLine(readonlyArgument);     // value is still 44

void InArgExample(in int number)
{
    // Uncomment the following line to see error CS8331
    //number = 19;
}

Le modificateur in permet au compilateur de créer une variable temporaire pour l’argument et de passer une référence en lecture seule à cet argument. Le compilateur crée toujours une variable temporaire lorsque l’argument doit être converti, lorsqu’il existe une conversion implicite à partir du type d’argument ou lorsque l’argument est une valeur qui n’est pas une variable. Par exemple, lorsque l’argument est une valeur littérale ou la valeur retournée à partir d’un accesseur de propriété. Lorsque votre API nécessite que l’argument soit passé par référence, choisissez le modificateur ref readonly et non le in.

Les méthodes définies à l’aide des paramètres in peuvent potentiellement optimiser les performances. Certains arguments de type struct peuvent être volumineux et, quand les méthodes sont appelées dans des boucles serrées ou des chemins de code critiques, le coût de la copie de ces structures est important. Les méthodes déclarent des paramètres in pour spécifier que des arguments peuvent être passés par référence en toute sécurité, car la méthode appelée ne modifie pas l’état de l’argument. Le passage de ces arguments par référence permet d’éviter une copie (potentiellement) coûteuse. Vous ajoutez explicitement le modificateur in sur le site d’appel pour vérifier que l’argument est passé par référence, non par valeur. L’utilisation explicite de in a les deux effets suivants :

  • La spécification de in sur le site d’appel force le compilateur à sélectionner une méthode définie avec un paramètre in correspondant. Sinon, quand deux méthodes diffèrent uniquement par la présence de in, la surcharge par valeur convient mieux.
  • En spécifiant in, vous déclarez votre intention de passer un argument par référence. L’argument utilisé avec in doit représenter un emplacement directement référençable. Les mêmes règles générales pour les arguments out et ref s’appliquent : vous ne pouvez pas utiliser de constantes, de propriétés ordinaires ou d’autres expressions qui produisent des valeurs. Sinon, l’omission de in sur le site d’appel informe le compilateur qu’il est autorisé à créer une variable temporaire à passer par référence en lecture seule à la méthode. Le compilateur crée une variable temporaire pour surmonter plusieurs restrictions avec les arguments in :
    • Une variable temporaire autorise des constantes au moment de la compilation comme paramètres in.
    • Une variable temporaire autorise des propriétés ou d’autres expressions pour les paramètres in.
    • Une variable temporaire autorise des arguments pour lesquels il existe une conversion implicite de type d’argument en type de paramètre.

Dans toutes les instances précédentes, le compilateur crée une variable temporaire qui stocke la valeur de la constante, la propriété ou une autre expression.

Le code suivant illustre ces règles :

static void Method(in int argument)
{
    // implementation removed
}

Method(5); // OK, temporary variable created.
Method(5L); // CS1503: no implicit conversion from long to int
short s = 0;
Method(s); // OK, temporary int created with the value 0
Method(in s); // CS1503: cannot convert from in short to in int
int i = 42;
Method(i); // passed by readonly reference
Method(in i); // passed by readonly reference, explicitly using `in`

Supposons à présent qu’une autre méthode utilisant des arguments par valeur est disponible. Les résultats changent comme illustré dans le code suivant :

static void Method(int argument)
{
    // implementation removed
}

static void Method(in int argument)
{
    // implementation removed
}

Method(5); // Calls overload passed by value
Method(5L); // CS1503: no implicit conversion from long to int
short s = 0;
Method(s); // Calls overload passed by value.
Method(in s); // CS1503: cannot convert from in short to in int
int i = 42;
Method(i); // Calls overload passed by value
Method(in i); // passed by readonly reference, explicitly using `in`

Le seul appel de méthode dans lequel l’argument est passé par référence est le dernier.

Notes

Le code précédent utilise int comme type d’argument par souci de simplicité. Comme int n’est pas plus volumineux qu’une référence dans la plupart des ordinateurs modernes, il n’y aucun avantage à passer un seul int comme référence en lecture seule.

Modificateur params

Dans une déclaration de méthode, vous ne pouvez pas spécifier d’autre paramètre après le mot clé params et vous pouvez utiliser un seul mot clé params.

Si le type déclaré du paramètre params n’est pas un tableau unidimensionnel, l’erreur de compilateur CS0225 se produit.

Lorsque vous appelez une méthode avec un paramètre params, vous pouvez passer :

  • Une liste séparée par des virgules d’arguments du type des éléments de tableau.
  • Un tableau d’arguments du type spécifié.
  • Aucun argument. Si vous n’envoyez aucun argument, la longueur de la liste params est égale à zéro.

L’exemple suivant montre différentes façons d’envoyer des arguments à un paramètre params.

public class MyClass
{
    public static void UseParams(params int[] list)
    {
        for (int i = 0; i < list.Length; i++)
        {
            Console.Write(list[i] + " ");
        }
        Console.WriteLine();
    }

    public static void UseParams2(params object[] list)
    {
        for (int i = 0; i < list.Length; i++)
        {
            Console.Write(list[i] + " ");
        }
        Console.WriteLine();
    }

    static void Main()
    {
        // You can send a comma-separated list of arguments of the
        // specified type.
        UseParams(1, 2, 3, 4);
        UseParams2(1, 'a', "test");

        // A params parameter accepts zero or more arguments.
        // The following calling statement displays only a blank line.
        UseParams2();

        // An array argument can be passed, as long as the array
        // type matches the parameter type of the method being called.
        int[] myIntArray = { 5, 6, 7, 8, 9 };
        UseParams(myIntArray);

        object[] myObjArray = { 2, 'b', "test", "again" };
        UseParams2(myObjArray);

        // The following call causes a compiler error because the object
        // array cannot be converted into an integer array.
        //UseParams(myObjArray);

        // The following call does not cause an error, but the entire
        // integer array becomes the first element of the params array.
        UseParams2(myIntArray);
    }
}
/*
Output:
    1 2 3 4
    1 a test

    5 6 7 8 9
    2 b test again
    System.Int32[]
*/