Параметры методов

По умолчанию аргументы в C# передаются в функции по значению. Это означает, что копия переменной передается методу. Для типов значений (struct) копия значения передается методу. Для ссылочных (class) типов копия ссылки передается методу. Модификаторы параметров позволяют передавать аргументы по ссылке. Следующие понятия помогают понять эти различия и как использовать модификаторы параметров:

  • Передача по значению означает передачу копии переменной в метод.
  • Передача по ссылке означает передачу доступа к переменной методу.
  • Переменная ссылочного типа содержит ссылку на свои данные.
  • Переменная типа значения содержит данные напрямую.

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

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

Различия демонстрируются в выходных данных следующего примера. Метод изменяет значение willIChange поля, так как метод ClassTaker использует адрес в параметре для поиска указанного поля экземпляра класса. willIChange Поле структуры в вызывающем методе не изменяется от вызоваStructTaker, так как значение аргумента является копией самой структуры, а не копией своего адреса. StructTaker изменяет саму копию, которая будет утрачена после завершения вызова StructTaker.

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
*/

Сочетания типов параметров и режима аргументов

Как передается аргумент и является ли он ссылочным типом или типом значения, определяет, какие изменения, внесенные в аргумент, видны вызывающему объекту:

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

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

Сейф контекст ссылок и значений

Методы могут хранить значения параметров в полях. Когда параметры передаются по значению, это обычно безопасно. Значения копируются, а ссылочные типы доступны при хранении в поле. Для безопасного передачи параметров по ссылке требуется, чтобы компилятор определил, когда он безопасно назначить ссылку новой переменной. Для каждого выражения компилятор определяет безопасный контекст , ограничивающий доступ к выражению или переменной. Компилятор использует два область: safe-context и ref-safe-context.

  • Безопасный контекст определяет область, к которой можно безопасно получить доступ к любому выражению.
  • Контекст ref-safe-определяет область, где ссылка на любое выражение может быть безопасно доступ к или изменена.

В неформальном режиме эти область можно рассматривать как механизм, чтобы ваш код никогда не обращается к ссылке или изменяет ссылку, которая больше не является допустимой. Ссылка действительна, если она ссылается на допустимый объект или структуру. Безопасный контекст определяет, когда переменная может быть назначена или переназначна. Контекст ref-safe-определяет , когда переменная может быть назначена или переназначирована. Назначение назначает переменную новому значению; Назначение ссылок назначает переменную для ссылки на другое расположение хранилища.

Параметры ссылок

К объявлению параметров применяется один из следующих модификаторов для передачи аргументов по ссылке, а не по значению:

  • ref: аргумент необходимо инициализировать перед вызовом метода. Метод может назначить новое значение параметру, но не требуется для этого.
  • out: вызов метода не требуется для инициализации аргумента перед вызовом метода. Метод должен назначить значение параметру.
  • readonly ref: аргумент необходимо инициализировать перед вызовом метода. Метод не может назначить новому значению параметру.
  • in: аргумент необходимо инициализировать перед вызовом метода. Метод не может назначить новому значению параметру. Компилятор может создать временную переменную для хранения копии аргумента в in параметрах.

Члены класса не могут иметь сигнатуры, которые отличаются только по ref, ref readonlyinилиout. Ошибка компилятора возникает, если единственное различие между двумя членами типа заключается в том, что один из них имеет параметр, а другой имеет refoutref readonlyпараметр или in параметр. Однако методы могут быть перегружены, если один метод имеет refпараметр , inref readonlyили out параметр, а другой имеет параметр, передаваемый значением, как показано в следующем примере. В других ситуациях, требующих сопоставления подписей, таких как скрытие или переопределение, in, refref readonlyи out являются частью подписи и не совпадают друг с другом.

Если параметр имеет один из предыдущих модификаторов, соответствующий аргумент может иметь совместимый модификатор:

  • Аргумент параметра ref должен включать ref модификатор.
  • Аргумент параметра out должен включать out модификатор.
  • Аргумент для in параметра может дополнительно включать in модификатор. ref Если модификатор используется вместо аргумента, компилятор выдает предупреждение.
  • Аргумент параметра ref readonly должен содержать либо inref модификаторы, но не оба. Если модификатор не включен, компилятор выдает предупреждение.

При использовании этих модификаторов они описывают, как используется аргумент:

  • ref означает, что метод может считывать или записывать значение аргумента.
  • out означает, что метод задает значение аргумента.
  • ref readonly означает, что метод считывает, но не может записывать значение аргумента. Аргумент должен передаваться по ссылке.
  • in означает, что метод считывает, но не может записывать значение аргумента. Аргумент передается по ссылке или через временную переменную.

Свойства не являются переменными. Они методы и не могут передаваться в ref параметры. Предыдущие модификаторы параметров нельзя использовать в следующих типах методов:

  • Асинхронные методы, которые определяются с помощью модификатора async.
  • Методы итератора, которые включают оператор yield return или yield break.

Методы расширения также имеют ограничения на использование этих аргументов ключевое слово:

  • Ключевое слово out нельзя использовать в первом аргументе метода расширения.
  • Ключевое слово ref нельзя использовать в первом аргументе метода расширения, если аргумент не structявляется или универсальным типом, который не ограничен структурой.
  • Нельзя ref readonly использовать и in ключевое слово, если первый аргумент не являетсяstruct.
  • Нельзя ref readonly использовать и in ключевое слово для любого универсального типа, даже если они ограничены структурой.

Модификатор параметра ref

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

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

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

Аргумент, передаваемый параметру, должен быть инициализирован перед передачей ref .

Модификатор параметра out

Для применения параметра out определение метода и метод вызова должны явно использовать ключевое слово out. Например:

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

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

Переменные, передаваемые в качестве out аргументов, не должны быть инициализированы перед передачей в вызове метода. Но перед передачей управления из вызванного метода он должен присвоить значение.

Деконструктивные методы объявляют свои параметры модификатором out для возврата нескольких значений. Другие методы могут возвращать кортежи значений для нескольких возвращаемых значений.

Перед передачей переменной в качестве аргумента можно объявить переменную в отдельном операторе out . Можно также объявить out переменную в списке аргументов вызова метода, а не в отдельном объявлении переменной. out Объявления переменных создают более компактный, удобочитаемый код, а также непреднамеренно присваивают переменной значение перед вызовом метода. В следующем примере переменная определяется number в вызове метода 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

Вы также можете объявить неявно типизированные локальные переменные.

ref readonly Модификатор

Модификатор ref readonly должен присутствовать в объявлении метода. Модификатор на сайте вызова является необязательным. in Можно использовать модификатор или ref модификатор. Модификатор ref readonly недействителен на сайте вызова. Какой модификатор, используемый на сайте вызова, может помочь описать характеристики аргумента. Можно использовать ref только в том случае, если аргумент является переменной и доступен для записи. Можно использовать in только в том случае, если аргумент является переменной. Это может быть запись или чтение. Нельзя добавить модификатор, если аргумент не является переменной, но является выражением. В следующих примерах показаны эти условия. Следующий метод использует ref readonly модификатор, чтобы указать, что большая структура должна передаваться по ссылке по причинам производительности:

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

Метод можно вызвать с помощью ref модификатора или in модификатора. Если не указать модификатор, компилятор выдает предупреждение. Если аргумент является выражением, а не переменной, нельзя добавить in или ref модификаторы, поэтому вы должны отключить предупреждение:

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

Если переменная является переменной readonly , необходимо использовать in модификатор. Компилятор выдает ошибку, если вместо этого используется ref модификатор.

Модификатор ref readonly указывает, что метод ожидает, что аргумент будет переменной, а не выражением, которое не является переменной. Примеры выражений, которые не являются переменными, являются константами, возвращаемыми методом значениями и свойствами. Если аргумент не является переменной, компилятор выдает предупреждение.

Модификатор параметра in

Модификатор in требуется в объявлении метода, но не требуется на сайте вызова.

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

Модификатор in позволяет компилятору создать временную переменную для аргумента и передать ссылку на этот аргумент. Компилятор всегда создает временную переменную, когда аргумент должен быть преобразован, при неявном преобразовании из типа аргумента или когда аргумент является значением, которое не является переменной. Например, если аргумент является литеральным значением или значением, возвращаемым методом доступа к свойствам. Если API требует, чтобы аргумент был передан по ссылке, выберите ref readonly модификатор вместо in модификатора.

Методы, определенные с помощью in параметров, потенциально получают оптимизацию производительности. Некоторые struct аргументы типа могут иметь большой размер, и когда методы вызываются в жестких циклах или критически важных путях кода, стоимость копирования этих структур является существенной. Методы объявляют in параметры, чтобы указать, что аргументы можно передавать по ссылке безопасно, так как вызываемый метод не изменяет состояние этого аргумента. Передача этих аргументов по ссылке позволяет избежать (потенциально) дорогого копирования. Вы явным образом добавляете модификатор in в место вызова, чтобы аргумент передавался по ссылке, а не по значению. Явное использование in приводит к двум результатам.

  • Указание in на сайте вызова заставляет компилятора выбрать метод, определенный с соответствующим in параметром. В противном случае, когда два метода отличаются только наличием in, перегрузка по значению подходит лучше.
  • Указывая in, вы объявляете намерение передать аргумент по ссылке. Аргумент, используемый с in, должен представлять расположение, на которое можно сослаться напрямую. Те же общие правила и outref аргументы применяются: нельзя использовать константы, обычные свойства или другие выражения, которые создают значения. В противном случае опущение in на сайте вызова сообщает компилятору, что это нормально, чтобы создать временную переменную для передачи ссылки только для чтения в метод. Компилятор создает временную переменную для преодоления нескольких ограничений с in аргументами:
    • Временная переменная позволяет использовать константы времени компиляции, например параметры in.
    • Временная переменная позволяет использовать свойства или другие выражения для параметров in.
    • Временная переменная позволяет аргументам, где существует неявное преобразование типа аргумента в тип параметра.

Во всех предыдущих случаях компилятор создает временную переменную, хранящую значение константы, свойства или другого выражения.

Эти правила проиллюстрированы в следующем коде:

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`

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

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`

Аргумент передается по ссылке только в последнем вызове метода.

Примечание.

Для простоты предыдущий код использует int в качестве типа аргумента. Так как по размеру int не больше ссылки на большинстве современных компьютеров, не имеет смысла передавать один int в качестве ссылки, доступной только для чтения.

params Модификатор

Другие параметры не разрешены после params ключевое слово в объявлении метода, и в объявлении метода разрешено только одно params ключевое слово.

Если объявленный тип params параметра не является одномерным массивом, возникает ошибка компилятора CS0225 .

При вызове метода с параметром params можно передать следующие объекты:

  • разделенный запятыми список аргументов типа элементов массива;
  • массив аргументов указанного типа;
  • не передавать аргументы. Если аргументы не отправляются, длина списка params равна нулю.

В следующем примере показаны различные способы оправки аргументов параметру 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[]
*/