Основы C#
November 14, 2022

Модификаторы параметров методов. Передача аргументов по значению и по ссылке

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

Сигнатура метода, параметры и аргументы

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

Под сигнатурой метода понимают совокупность:

  • имени метода;
  • количества параметров;
  • типов параметров;
  • порядка типа параметров;
  • модификаторов параметров.

При изменении имени метода мы получаем новый метод, а при измении любого другого пункта в сигнатуре мы получаем перегруженный метод)

Например, следующие два метода являются перегруженными по сигнатуре:

class Robot
{
    public void SaySomething(string words, int a)
    {
        Console.WriteLine(words);
        Console.WriteLine(a + 3);
    }

    public void SaySomething(int a, string words)
    {
        Console.WriteLine(words);
        Console.WriteLine(a + 3);
    }
}

Сигнатура первого метода будет: SaySomething(string, int);
Сигнатура второго: SaySomething(string, int). Такой код является абсолютно валидным с точки зрения компилятора и он скомпилируется без ошибок.

Параметры методов - это переменные, которые будут содержать данные, передаваемые методу при его вызове.

типМетода ИмяМетода (типПараметра1 параметр1, типПараметра2 параметр2, ...)
{
    // действия метода
}

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

void PrintHello(string name)
{
    Console.WriteLine(quot;Hello, {name}");
} 

PrintHello("Artem");

В данном примере метод PrintHello принимает 1 параметр name типа string, при вызове PrintHello параметру name передается аргумент, в данном случае строка "Artem".

Передача по значению

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

public class SomeClass
{
    public static void Sum(int a)
    {
        a = a + 3;
    }
}

На первый взгляд может показаться, что данный метод увеличивает значение переданной переменной на 3. На деле же, прогнав этот метод, мы получим иной результат:

int a = 5;
SomeClass.Sum(a); // Ожидаем, что значение a увеличится на 3
Console.WriteLine(a); // Получаем иной результат, вывод: 5

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

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

class Robot
{
    public int Brain { get; set; }

    public static void Changer(Robot bot)
    {
        bot.Brain = 10;
    }
}
var bot = new Robot(); // Создаем объект класса Robot
bot.Brain = 3; // Присваиваем свойству значение 3
Robot.Changer(bot);
Console.WriteLine(bot.Brain); // Вывод: 10

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

class Robot
{
    public int Brain { get; set; }

    public static void Changer(Robot bot)
    {
        bot = new Robot();
        bot.Brain = 5;
    }
}
var bender = new Robot(); // Создаем объект класса Robot
bender.Brain = 3; // Присваиваем свойству значение 3
Robot.Changer(bender);
Console.WriteLine(bender.Brain); // Вывод: 3

В классе Robot имеем метод Changer, который принимает значение ссылки на экземпляр класса Robot в переменную bot и присваивает ей ссылку на новый экземпляр.
Здесь при копировании работает такая же логика как с типами значений: наш объект класса Robot хранится где-то в куче, переменная bender же на стеке хранит ссылку на этот объект, а при передаче переменной в качестве аргумента в методе работа будет происходить уже с переменной bot метода, которая содержит копию ссылки, поэтому изменение ссылки у данной переменной ни к каким изменениям объекта не приведет)

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

Передача по ссылке и модификаторы параметров

Для изменения стандартной передачи аргументов по значению используются модификаторы параметров методов, с помощью которых можно управлять способами передачи аргументов. Существует 4 модификатора: ref, out, in, params. Сейчас мы про каждый из них поговорим.

Ссылочные параметры, модификатор ref

Для передачи по ссылке можно использовать модификатор ref (от reference) перед типом параметра в сигнатуре метода, а также перед передаваемым аргументом при вызове метода:

public static void Swap(ref int a, ref int b)
{
    //вариант без создания 3-й переменной:)
    a = a + b;
    b = a - b;
    a = a - b;
}

int a = 1;
int b = 2;
Console.WriteLine(quot;a={a} b={b}"); //a=1 b=2
Swap(ref a, ref b);
Console.WriteLine(quot;a={a} b={b}"); //a=2 b=1

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

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

class Robot
{
    public int Brain { get; set; }

    public static void Changer(ref Robot bot)
    {
        bot = new Robot();
        bot.Brain = 5;
    }
}

var bender = new Robot(); // Создаем объект класса Robot
bender.Brain = 3;
Robot.Changer(ref bender); // передаем ссылочный тип по ссылке в метод, где будет изменена ссылка
Console.WriteLine(bender.Brain); // Вывод: 5

Выходные параметры, модификатор out

Также для передачи по ссылке можно использовать модификатор out. Он похож по функционалу на модификатор ref, за исключением того, что ref обязывает проинициализировать переменную перед ее передачей, если же передаваемый аргумент не будет приоинциализирован, компилятор выдаст ошибку.
При использовании же модификатора out компилятор позволяет передавать неинициализированные переменные, которые должны быть проинициализированы внутри метода. Это связано с тем, что выходному параметру в методе обязательно должно быть выполнено присваивание.

public static void Sum(int a, int b, out int sum)
{
    sum = a + b;
}

Кроме того, передаваемые аргументы в выходные параметры не обязательно объявлять до вызова метода, это можно сделать прямо внутри метода:)

Sum(1, 2, out int result);
Console.WriteLine(result); // 3

Но зачем использовать выходной параметр для возвращения обычной суммы, можно же вернуть результат через return?!

Да, такой пример с суммой носит лишь демонстрационный характер для понимания, как работать с выходными параметрам, и в данном случае в реальной ситуации лучше было бы просто вернуть значение через return.
Реальную же пользу от модификатора out мы можем получить, когда из одного метода необходимо получить несколько выходных значений. Например, вычисляем в методе периметр и площадь прямоугольника:

void GetRectangleData(int width, int height, out int area, out int perimetr)
{
    area = width * height; 
    perimetr = (width + height) * 2;
} 

GetRectangleData(1, 2, out int area, out int perimetr); 
Console.WriteLine(quot;area = {area}, perimetr = {perimetr}"); // area = 2, perimetr = 6

Также преимуществом out является использование TryGet паттерна. Такой подход используется для получения некоторого значения в виде out параметра и возврата bool (если значение не было получено, вернется false, если получено - true).

Рассмотрим на примере методов Parse и TryParse у типа int. Метод int.Parse() используется для преобразования строки в число типа int. В случае, если число неудачи в преобразовании будет выброшено исключение (об исключениях мы уже скоро поговорим в следующей статье). Метод же TryParse не выбрасывает исключения в таком случае, а возвращает false и эту ситуацию можно обработать в условной конструкции. Это довольно упрощает код и не несет дополнительных расходов для обработки всех ситуаций.

int a = int.Parse("1"); // a = 1
a = int.Parse("aaa"); //FormatException

if (int.TryParse("15", out int b))
{
    Console.WriteLine(quot;b = {b}");
}
else
{
    Console.WriteLine("Wow, this is not number");
}

Входные параметры, модификатор in

Модификатор in позволяет передавать значение по ссылке, но запрещает его модификацию внутри метода. Это может быть полезно, когда мы хотим снизить потенциальные расходы на память, передав ссылку вместо создания копии, но мы хотим указать, что данное значение не должно быть изменено внутри метода.

public static int Sum(in int a, in int b)
{
    int sum = a + b;
    return sum;
}

int a = 5;
int b = 4;
int c = Sum(a, b);
Console.WriteLine(c);

При попытке изменить параметр с модификатором in мы получим ошибку.

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

Что если мы встретимся с необходимостью передавать неопределенное число аргументов в метод? Для решения таких проблем существует модификатор params. Он указывает, что в качестве аргументов будет передано произвольное число аргументов или не передано вообще ничего:

МодификаторДоступа ВозвращаемыйТип ИмяМетода(params ИмяТипа[] ИмяПеременной)
{
    тело метода
}

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

Стоит запомнить ряд требований:

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

Примеры методов с использованием данного ключевого слова:

class SomeClass
{
    public void DoSomething1(params int[] c)
    {
        for (int i = 0; i < c.Length; i++)
            Console.WriteLine(c[i]);
    }
    
    public void DoSomething2(int a, params int[] b)
    {
    }
    
    public void DoSomething3(string a, int b, params int[] c)
    {
    }
}

Тогда валидными конструкциями вызова данных методов будут:

SomeClass a = new SomeClass(); // Создали экземпляр класса
int[] array = { 1, 2, 3 }; // Создали массив из трех элементов

a.DoSomething1(); // params может принимать 0 аргументов!
a.DoSomething1(1);
a.DoSomething1(1, 2, 3, 4, 5);
a.DoSomething1(5, 2);
a.DoSomething1(array);

a.DoSomething2(1); // В данном методе обязан быть хотя бы 1 целочисленный
                   // аргумент, поскольку помимо переменного числа
                   // аргументов указан обязательный целочисленый параметр
a.DoSomething2(2, array);
                   
a.DoSomething3("hi", 1); // Аналогично методу DoSomething2                 
a.DoSomething3("bye", 2, 3, 4, 5, 6);
a.DoSomething3("hey!", 5, array);

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

public void FirstMethod(int[] array) // При вызове обязательно принимает
{                                    // аргумент – массив
}

public void SecondMethod(params int[] array) // При вызове может не
{                                            // принимать ничего
}

Необязательные параметры

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

И это просто замечательно, как эти жабисты существуют без параметров по умолчанию, я не понимаю:)

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

static void PrintRobotInfo(string msg, int age = 500, string name = "Bender")
{
    Console.WriteLine(quot;I'm {name}, {age} years");
    Console.WriteLine(msg);
}

PrintRobotInfo("Kill all humans", 100);
// I'm Bender, 100 years
// Kill all humans


Как-то так)

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


p.s. Сейчас вылетел из графика из-за загруженности, пишу по возможности, все будет как только, так сразу. Накидывайте лайки на посты в телеге, буду видеть, что мой материал вам интересен, буду стараться делать быстрее.