CA1021 : Éviter les paramètres out

Propriété Value
Identificateur de la règle CA1021
Titre Éviter les paramètres out
Catégorie Conception
Le correctif est cassant ou non cassant Rupture
Activé par défaut dans .NET 8 Non

Cause

Une méthode publique ou protégée dans un type public a un paramètre out.

Par défaut, cette règle examine uniquement les types visibles en externe, mais elle est configurable.

Description de la règle

Passer des types par référence (en utilisant out ou ref) nécessite une certaine expérience des pointeurs, de comprendre la différence entre les types valeur et les types référence, ainsi que la gestion de méthodes impliquant plusieurs valeurs de retour. Par ailleurs, la différence entre les paramètres out et ref est généralement peu comprise.

Lorsqu’un type de référence est passé « par référence », la méthode a l’intention d’utiliser le paramètre pour retourner une autre instance de l’objet. La transmission d’un type référence par référence est également appelée l’utilisation d’un pointeur double, d’un pointeur vers un pointeur ou d’une double indirection. En utilisant la convention d’appel par défaut, qui consiste à passer « par valeur », un paramètre qui prend un type référence reçoit déjà un pointeur vers l’objet . Le pointeur, et non l’objet vers lequel il pointe, est passé par valeur. Passer par valeur signifie que la méthode ne peut pas modifier le pointeur pour qu’il pointe vers une nouvelle instance du type référence. Toutefois, il peut modifier le contenu de l’objet vers lequel il pointe. Pour la plupart des applications, cela est suffisant et génère le comportement souhaité.

Si une méthode doit retourner une autre instance, utilisez la valeur de retour de la méthode pour ce faire. Consultez la classe System.String pour une variété de méthodes qui opèrent sur des chaînes et retournent une nouvelle instance d’une chaîne. Lorsque ce modèle est utilisé, l’appelant doit décider si l’objet d’origine est conservé.

Bien que les valeurs de retour soient courantes et fortement utilisées, l’application correcte des paramètres out et ref nécessite des compétences de conception et de codage intermédiaires. Les architectes de bibliothèques qui réalisent un travail de conception destiné à une audience générale ne doivent pas s’attendre à ce que les utilisateurs maîtrisent l’utilisation des paramètres out ou ref.

Comment corriger les violations

Pour corriger une violation de cette règle causée par un type valeur, faites en sorte que la méthode retourne l’objet comme valeur de retour. Si la méthode doit retourner plusieurs valeurs, redéfinissez-la pour retourner une seule instance d’un objet qui contient les valeurs.

Pour corriger une violation de cette règle causée par un type référence, assurez-vous que le comportement souhaité est de retourner une nouvelle instance de la référence. Si tel est le cas, la méthode doit utiliser sa valeur de retour pour ce faire.

Quand supprimer les avertissements

Vous pouvez supprimer un avertissement de cette règle. Toutefois, cette conception peut entraîner des problèmes d’utilisation.

Supprimer un avertissement

Si vous voulez supprimer une seule violation, ajoutez des directives de préprocesseur à votre fichier source pour désactiver et réactiver la règle.

#pragma warning disable CA1021
// The code that's violating the rule is on this line.
#pragma warning restore CA1021

Pour désactiver la règle sur un fichier, un dossier ou un projet, définissez sa gravité sur none dans le fichier de configuration.

[*.{cs,vb}]
dotnet_diagnostic.CA1021.severity = none

Pour plus d’informations, consultez Comment supprimer les avertissements de l’analyse de code.

Configurer le code à analyser

Utilisez l’option suivante pour configurer les parties de votre codebase sur lesquelles exécuter cette règle.

Vous pouvez configurer cette option pour cette règle uniquement, pour toutes les règles auxquelles elle s’applique ou pour toutes les règles de cette catégorie (Conception) auxquelles elle s’applique. Pour plus d’informations, consultez Options de configuration des règles de qualité du code.

Inclure des surfaces d’API spécifiques

Vous pouvez configurer les parties de votre codebase sur lesquelles exécuter cette règle, en fonction de leur accessibilité. Par exemple, pour spécifier que la règle doit s’exécuter uniquement sur la surface d’API non publique, ajoutez la paire clé-valeur suivante à un fichier .editorconfig dans votre projet :

dotnet_code_quality.CAXXXX.api_surface = private, internal

Exemple 1

La bibliothèque suivante montre deux implémentations d’une classe qui génère des réponses aux commentaires des utilisateurs. La première implémentation (BadRefAndOut) force l’utilisateur de la bibliothèque à gérer trois valeurs de retour. La deuxième implémentation (RedesignedRefAndOut) simplifie l’expérience utilisateur en retournant une instance d’une classe de conteneur (ReplyData) qui gère les données comme une seule unité.

public enum Actions
{
    Unknown,
    Discard,
    ForwardToManagement,
    ForwardToDeveloper
}

public enum TypeOfFeedback
{
    Complaint,
    Praise,
    Suggestion,
    Incomprehensible
}

public class BadRefAndOut
{
    // Violates rule: DoNotPassTypesByReference.

    public static bool ReplyInformation(TypeOfFeedback input,
       out string reply, ref Actions action)
    {
        bool returnReply = false;
        string replyText = "Your feedback has been forwarded " +
                           "to the product manager.";

        reply = String.Empty;
        switch (input)
        {
            case TypeOfFeedback.Complaint:
            case TypeOfFeedback.Praise:
                action = Actions.ForwardToManagement;
                reply = "Thank you. " + replyText;
                returnReply = true;
                break;
            case TypeOfFeedback.Suggestion:
                action = Actions.ForwardToDeveloper;
                reply = replyText;
                returnReply = true;
                break;
            case TypeOfFeedback.Incomprehensible:
            default:
                action = Actions.Discard;
                returnReply = false;
                break;
        }
        return returnReply;
    }
}

// Redesigned version does not use out or ref parameters.
// Instead, it returns this container type.

public class ReplyData
{
    bool _returnReply;

    // Constructors.
    public ReplyData()
    {
        this.Reply = String.Empty;
        this.Action = Actions.Discard;
        this._returnReply = false;
    }

    public ReplyData(Actions action, string reply, bool returnReply)
    {
        this.Reply = reply;
        this.Action = action;
        this._returnReply = returnReply;
    }

    // Properties.
    public string Reply { get; }
    public Actions Action { get; }

    public override string ToString()
    {
        return String.Format("Reply: {0} Action: {1} return? {2}",
           Reply, Action.ToString(), _returnReply.ToString());
    }
}

public class RedesignedRefAndOut
{
    public static ReplyData ReplyInformation(TypeOfFeedback input)
    {
        ReplyData answer;
        string replyText = "Your feedback has been forwarded " +
           "to the product manager.";

        switch (input)
        {
            case TypeOfFeedback.Complaint:
            case TypeOfFeedback.Praise:
                answer = new ReplyData(
                   Actions.ForwardToManagement,
                   "Thank you. " + replyText,
                   true);
                break;
            case TypeOfFeedback.Suggestion:
                answer = new ReplyData(
                   Actions.ForwardToDeveloper,
                   replyText,
                   true);
                break;
            case TypeOfFeedback.Incomprehensible:
            default:
                answer = new ReplyData();
                break;
        }
        return answer;
    }
}

Exemple 2

L’application suivante illustre l’expérience de l’utilisateur. L’appel à la bibliothèque repensée (méthode UseTheSimplifiedClass) est plus simple et les informations retournées par la méthode sont facilement gérées. La sortie des deux exemples est identique.

public class UseComplexMethod
{
    static void UseTheComplicatedClass()
    {
        // Using the version with the ref and out parameters.
        // You do not have to initialize an out parameter.

        string[] reply = new string[5];

        // You must initialize a ref parameter.
        Actions[] action = {Actions.Unknown,Actions.Unknown,
                         Actions.Unknown,Actions.Unknown,
                         Actions.Unknown,Actions.Unknown};
        bool[] disposition = new bool[5];
        int i = 0;

        foreach (TypeOfFeedback t in Enum.GetValues(typeof(TypeOfFeedback)))
        {
            // The call to the library.
            disposition[i] = BadRefAndOut.ReplyInformation(
               t, out reply[i], ref action[i]);
            Console.WriteLine("Reply: {0} Action: {1}  return? {2} ",
               reply[i], action[i], disposition[i]);
            i++;
        }
    }

    static void UseTheSimplifiedClass()
    {
        ReplyData[] answer = new ReplyData[5];
        int i = 0;
        foreach (TypeOfFeedback t in Enum.GetValues(typeof(TypeOfFeedback)))
        {
            // The call to the library.
            answer[i] = RedesignedRefAndOut.ReplyInformation(t);
            Console.WriteLine(answer[i++]);
        }
    }

    public static void UseClasses()
    {
        UseTheComplicatedClass();

        // Print a blank line in output.
        Console.WriteLine("");

        UseTheSimplifiedClass();
    }
}

Exemple 3

L’exemple de bibliothèque suivant illustre comment les paramètres ref des types référence sont utilisés et montre une meilleure façon d’implémenter cette fonctionnalité.

public class ReferenceTypesAndParameters
{
    // The following syntax will not work. You cannot make a
    // reference type that is passed by value point to a new
    // instance. This needs the ref keyword.

    public static void BadPassTheObject(string argument)
    {
        argument += " ABCDE";
    }

    // The following syntax works, but is considered bad design.
    // It reassigns the argument to point to a new instance of string.
    // Violates rule DoNotPassTypesByReference.

    public static void PassTheReference(ref string argument)
    {
        argument += " ABCDE";
    }

    // The following syntax works and is a better design.
    // It returns the altered argument as a new instance of string.

    public static string BetterThanPassTheReference(string argument)
    {
        return argument + " ABCDE";
    }
}

Exemple 4

L’application suivante appelle chaque méthode de la bibliothèque pour illustrer le comportement.

public class Test
{
    public static void MainTest()
    {
        string s1 = "12345";
        string s2 = "12345";
        string s3 = "12345";

        Console.WriteLine("Changing pointer - passed by value:");
        Console.WriteLine(s1);
        ReferenceTypesAndParameters.BadPassTheObject(s1);
        Console.WriteLine(s1);

        Console.WriteLine("Changing pointer - passed by reference:");
        Console.WriteLine(s2);
        ReferenceTypesAndParameters.PassTheReference(ref s2);
        Console.WriteLine(s2);

        Console.WriteLine("Passing by return value:");
        s3 = ReferenceTypesAndParameters.BetterThanPassTheReference(s3);
        Console.WriteLine(s3);
    }
}

Cet exemple génère la sortie suivante :

Changing pointer - passed by value:
12345
12345
Changing pointer - passed by reference:
12345
12345 ABCDE
Passing by return value:
12345 ABCDE

Essayer des méthodes de modèle

Les méthodes qui implémentent le modèle Essayer<quelque chose>, telles que System.Int32.TryParse, ne déclenchent pas cette violation. L’exemple suivant montre une structure (type valeur) qui implémente la méthode System.Int32.TryParse.

public struct Point
{
    public Point(int axisX, int axisY)
    {
        X = axisX;
        Y = axisY;
    }

    public int X { get; }

    public int Y { get; }

    public override int GetHashCode()
    {
        return X ^ Y;
    }

    public override bool Equals(object? obj)
    {
        if (!(obj is Point))
            return false;

        return Equals((Point)obj);
    }

    public bool Equals(Point other)
    {
        if (X != other.X)
            return false;

        return Y == other.Y;
    }

    public static bool operator ==(Point point1, Point point2)
    {
        return point1.Equals(point2);
    }

    public static bool operator !=(Point point1, Point point2)
    {
        return !point1.Equals(point2);
    }

    // Does not violate this rule
    public static bool TryParse(string value, out Point result)
    {
        // TryParse Implementation
        result = new Point(0, 0);
        return false;
    }
}

CA1045 : Ne pas passer de types par référence