CA1021: не используйте параметры out

Свойство Значение
Идентификатор правила CA1021
Заголовок Не используйте параметры out
Категория Проектирование
Исправление является критическим или не критическим Критическое
Включен по умолчанию в .NET 8 No

Причина

Открытый или защищенный метод в открытом типе имеет параметр out.

По умолчанию это правило проверяет только видимые извне типы, но это поведение можно настроить.

Описание правила

Для реализации передачи типов по ссылке (с помощью out или ref) от разработчика требуется опыт работы с указателями, понимание отличия между типами значения и ссылочными типами и умение работать с методами с несколькими возвращаемыми значениями. Кроме того, далеко не все понимают разницу между параметрами out и ref.

Когда ссылочный тип передается "по ссылке", метод намеревается использовать этот параметр для возврата другого экземпляра объекта. Передача ссылочного типа по ссылке также называется использованием двойного указателя, указателя на указатель или двойного косвенного обращения. Используя соглашение о вызовах по умолчанию, которое передается "по значению", параметр, который принимает ссылочный тип, уже получает указатель на объект. Указатель, а не объект, на который он указывает, передается по значению. Передача по значению означает, что метод не может изменить указатель, чтобы он указывал на новый экземпляр ссылочного типа. Однако он может изменить содержимое объекта, на который он указывает. Для большинства приложений этого достаточно, и это дает желаемое поведение.

Если метод должен возвращать другой экземпляр, используйте для этого возвращаемое значение метода. См. класс System.String для различных методов, которые работают со строками и возвращают новый экземпляр строки. При использовании этой модели вызывающий объект должен решить, сохраняется ли исходный объект.

Хотя возвращаемые значения являются наиболее распространенными и часто используются, правильное применение параметров out и ref требует среднего уровня навыков проектирования и программирования. Архитекторам, разрабатывающим библиотеки для широкого использования, не следует рассчитывать, что пользователи отлично разбираются в использовании параметров out и ref.

Устранение нарушений

Чтобы устранить нарушение этого правила, вызванное типом значения, метод должен вернуть объект в качестве возвращаемого значения. Если метод должен возвращать несколько значений, перепроектируйте его для возврата одного экземпляра объекта, содержащего значения.

Чтобы устранить нарушение этого правила, вызванное ссылочным типом, убедитесь, что желаемое поведение состоит в возвращении нового экземпляра ссылки. Если это так, метод должен использовать его возвращаемое значение для этого.

Когда лучше отключить предупреждения

Вы можете безопасно скрыть предупреждения, связанные с этим правилом. Однако такая схема может создавать неудобства в использовании.

Отключение предупреждений

Если вы просто хотите отключить одно нарушение, добавьте директивы препроцессора в исходный файл, чтобы отключить и повторно включить правило.

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

Чтобы отключить правило для файла, папки или проекта, задайте его серьезность none в файле конфигурации.

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

Дополнительные сведения см. в разделе Практическое руководство. Скрытие предупреждений анализа кода.

Настройка кода для анализа

Используйте следующий параметр, чтобы выбрать части базы кода для применения этого правила.

Этот параметр можно настроить только для этого правила, для всех правил, к которым он применяется, или для всех правил в этой категории (конструкторе), к которым она применяется. Дополнительные сведения см. в статье Параметры конфигурации правила качества кода.

Включение определенных контактных зон API

Вы можете настроить, для каких частей базы кода следует выполнять это правило в зависимости от их доступности. Например, чтобы указать, что правило должно выполняться только для закрытой контактной зоны API, добавьте следующую пару "ключ-значение" в файл EDITORCONFIG в своем проекте:

dotnet_code_quality.CAXXXX.api_surface = private, internal

Пример 1

В следующей библиотеке показаны две реализации класса, которые создают ответы на отзывы пользователей. Первая реализация (BadRefAndOut) заставляет пользователя библиотеки управлять тремя возвращаемыми значениями. Вторая реализация (RedesignedRefAndOut) упрощает взаимодействие с пользователем, возвращая экземпляр класса контейнера (ReplyData), который управляет данными как единым целым.

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;
    }
}

Пример 2

В следующем приложении показана работа пользователя. Вызов переработанной библиотеки (метода UseTheSimplifiedClass) более прост, и сведениями, возвращаемыми методом, легко управлять. Оба метода дают одинаковые результаты.

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();
    }
}

Пример 3

В следующем примере библиотеки показано, как используются параметры ref для ссылочных типов, и демонстрируется лучший способ реализации этой функции.

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";
    }
}

Пример 4

Следующее приложение вызывает каждый метод в библиотеке, чтобы продемонстрировать поведение.

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);
    }
}

В примере получается следующий вывод.

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

Методы шаблона try

Методы, реализующие шаблон "Попробовать<что-то> ", например System.Int32.TryParse, не вызывают это нарушение. В следующем примере показана структура (тип значения), реализующая метод 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: не передавайте типы по ссылке