XamarinStudents

Xamarin lectures for students


Project maintained by PavlenkoDR Hosted on GitHub Pages — Theme by mattgraham

Домой

Lesson 5

Передача параметров по ссылке и значению. Выходные параметры

Существует два способа передачи параметров в метод в языке C#: по значению и по ссылке.

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

Наиболее простой способ передачи параметров представляет передача по значению, по сути это обычный способ передачи параметров:

class Program
{
    static void Main(string[] args)
    {
        Sum(10, 15);        // параметры передаются по значению
        Console.ReadKey();
    }
    static int Sum(int x, int y)
    {
        return x + y;
    }
}

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

При передаче параметров по ссылке перед параметрами используется модификатор ref:

static void Main(string[] args)
{
    int x = 10;
    int y = 15;
    Addition(ref x, y); // вызов метода
    Console.WriteLine(x);   // 25
 
    Console.ReadLine();
}
// параметр x передается по ссылке
static void Addition(ref int x, int y)
{
    x += y;
}

Обратите внимание, что модификатор ref указывается, как при объявлении метода, так и при его вызове в методе Main.

Сравнение передачи по значению и по ссылке

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

Рассмотрим два аналогичных примера. Первый пример - передача параметра по значению:

class Program
{
    static void Main(string[] args)
    {
        int a = 5;
        Console.WriteLine($"Начальное значение переменной a = {a}");
 
        //Передача переменных по значению
        //После выполнения этого кода по-прежнему a = 5, так как мы передали лишь ее копию
        IncrementVal(a);
        Console.WriteLine($"Переменная a после передачи по значению равна = {a}");
        Console.ReadKey();
    }
    // передача по значению
    static void IncrementVal(int x)
    {
        x++;
        Console.WriteLine($"IncrementVal: {x}");
    }
}

Консольный вывод:

Начальное значение переменной a = 5
IncrementVal: 6
Переменная a после передачи по значению равна = 5

При вызове метод IncrementVal получает копию переменной a и увеличивает значение этой копии. Поэтому в самом методе IncrementVal мы видим, что значение параметра x увеличилось на 1, но после выполнения метода переменная a имеет прежнее значение - 5. То есть изменяется копия, а сама переменная не изменяется.

Второй пример - аналогичный метод с передачей параметра по ссылке:

class Program
{
    static void Main(string[] args)
    {
        int a = 5;
        Console.WriteLine($"Начальное значение переменной a  = {a}");
        //Передача переменных по ссылке
        //После выполнения этого кода a = 6, так как мы передали саму переменную
        IncrementRef(ref a);
        Console.WriteLine($"Переменная a после передачи ссылке равна = {a}");
         
        Console.ReadKey();
    }
    // передача по ссылке
    static void IncrementRef(ref int x)
    {
        x++;
        Console.WriteLine($"IncrementRef: {x}");
    }
}

Консольный вывод:

Начальное значение переменной a = 5
IncrementRef: 6
Переменная a после передачи по ссылке равна = 6

В метод IncrementRef передается ссылка на саму переменную a в памяти. И если значение параметра в IncrementRef изменяется, то это приводит и к изменению переменной a, так как и параметр и переменная указывают на один и тот же адрес в памяти.

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

Выше мы использовали входные параметры. Но параметры могут быть также выходными. Чтобы сделать параметр выходным, перед ним ставится модификатор out:

static void Sum(int x, int y, out int a)
{
    a = x + y;
}

Здесь результат возвращается не через оператор return, а через выходной параметр. Использование в программе:

static void Main(string[] args)
{
    int x = 10;
     
    int z;
     
    Sum(x, 15, out z);
     
    Console.WriteLine(z);
 
    Console.ReadKey();
}

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

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

static void Sum(int x, int y, out int a)
{
    Console.WriteLine(x+y);
}

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

static void Main(string[] args)
{
    int x = 10;
    int area;
    int perimetr;
    GetData(x, 15, out area, out perimetr);
    Console.WriteLine("Площадь : " + area);
    Console.WriteLine("Периметр : " + perimetr);
 
    Console.ReadKey();
}
static void GetData(int x, int y, out int area, out int perim)
{
    area= x * y;
    perim= (x + y)*2; 
}

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

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

Стоит отметить, что начиная с версии C# 7.0 можно определять переменные в непосредственно при вызове метода. То есть вместо:

int x = 10;
int area;
int perimetr;
GetData(x, 15, out area, out perimetr);
Console.WriteLine($"Площадь : {area}");
Console.WriteLine($"Периметр : {perimetr}");
```cs

Мы можем написать:

```cs
int x = 10;
GetData(x, 15, out int area, out int perimetr);
Console.WriteLine($"Площадь : {area}");
Console.WriteLine($"Периметр : {perimetr}");
```cs

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

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

```cs
static void GetData(in int x, int y, out int area, out int perim)
{
    // x = x + 10; нельзя изменить значение параметра x
    y = y + 10;
    area = x * y;
    perim = (x + y) * 2;
}

В данном случае через параметры x и y в метод передаются значения, но в самом методе можно изменить только значение параметра y, так как параметр x указан с модификатором in.

Кортежи

Кортежи предоставляют удобный способ для работы с набором значений, который был добавлен в версии C# 7.0.

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

var tuple = (5, 10);

В данном случае определен кортеж tuple, который имеет два значения: 5 и 10. В дальнейшем мы можем обращаться к каждому из этих значений через поля с названиями Item[порядковый_номер_поля_в_кортеже]. Например:

static void Main(string[] args)
{
    var tuple = (5, 10);
    Console.WriteLine(tuple.Item1); // 5
    Console.WriteLine(tuple.Item2); // 10
    tuple.Item1 += 26;
    Console.WriteLine(tuple.Item1); // 31
    Console.Read();
}

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

(int, int) tuple = (5, 10);

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

(string, int, double) person = ("Tom", 25, 81.23);

Первый элемент кортежа в данном случае представляет строку, второй элемент - тип int, а третий - тип double.

Мы также можем дать названия полям кортежа:

var tuple = (count:5, sum:10);
Console.WriteLine(tuple.count); // 5
Console.WriteLine(tuple.sum); // 10

Теперь чтобы обратиться к полям кортежа используются их имена, а не названия Item1 и Item2.

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

static void Main(string[] args)
{
    var (name, age) = ("Tom", 23);
    Console.WriteLine(name);    // Tom
    Console.WriteLine(age);     // 23
    Console.Read();
}

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

Использование кортежей

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

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

static void Main(string[] args)
{
    var tuple = GetValues();
    Console.WriteLine(tuple.Item1); // 1
    Console.WriteLine(tuple.Item2); // 3
     
    Console.Read();
}
private static (int, int) GetValues()
{
    var result = (1, 3);
    return result;
}

Здесь определен метод GetValues(), который возвращает кортеж. Кортеж определяется как набор значений, помещенных в круглые скобки. И в данном случае мы возвращаем кортеж из двух элементов типа int, то есть два числа.

Другой пример:

static void Main(string[] args)
{
    var tuple = GetNamedValues(new int[]{ 1,2,3,4,5,6,7});
    Console.WriteLine(tuple.count);
    Console.WriteLine(tuple.sum);
     
    Console.Read();
}
private static (int sum, int count) GetNamedValues(int[] numbers)
{
    var result = (sum:0, count: 0);
    for (int i=0; i < numbers.Length; i++)
    {
        result.sum += numbers[i];
        result.count++;
    }
    return result;
}

И также кортеж может передаваться в качестве параметра в метод:

static void Main(string[] args)
{
    var (name, age) = GetTuple(("Tom", 23), 12);
    Console.WriteLine(name);    // Tom
    Console.WriteLine(age);     // 35
    Console.Read();
}
         
private static (string name, int age) GetTuple((string n, int a) tuple, int x)
{
    var result = (name: tuple.n, age: tuple.a + x);
    return result;
}

Перегрузка методов

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

И в языке C# мы можем создавать в классе несколько методов с одним и тем же именем, но разной сигнатурой. Что такое сигнатура? Сигнатура складывается из следующих аспектов:

Но названия параметров в сигнатуру НЕ входят. Например, возьмем следующий метод:

public int Sum(int x, int y) 
{ 
    return x + y;
}

У данного метода сигнатура будет выглядеть так: Sum(int, int)

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

Например, пусть у нас есть следующий класс:

class Calculator
{
    public void Add(int a, int b)
    {
        int result = a + b;
        Console.WriteLine($"Result is {result}");
    }
    public void Add(int a, int b, int c)
    {
        int result = a + b + c;
        Console.WriteLine($"Result is {result}");
    }
    public int Add(int a, int b, int c, int d)
    {
        int result = a + b + c + d;
        Console.WriteLine($"Result is {result}");
        return result;
    }
    public void Add(double a, double b)
    {
        double result = a + b;
        Console.WriteLine($"Result is {result}");
    }
}

Здесь представлены четыре разных версии метода Add, то есть определены четыре перегрузки данного метода.

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

То есть мы можем представить сигнатуры данных методов следующим образом:

Add(int, int)
Add(int, int, int)
Add(int, int, int, int)
Add(double, double)

После определения перегруженных версий мы можем использовать их в программе:

class Program
{
    static void Main(string[] args)
    {
        Calculator calc = new Calculator();
        calc.Add(1, 2); // 3
        calc.Add(1, 2, 3); // 6
        calc.Add(1, 2, 3, 4); // 10
        calc.Add(1.4, 2.5); // 3.9
         
        Console.ReadKey();
    }
}

Консольный вывод:

```Result is 3 Result is 6 Result is 10 Result is 3.9

Также перегружаемые методы могут отличаться по используемым модификаторам. Например:

```cs
void Increment(ref int val)
{
    val++;
    Console.WriteLine(val);
}
 
void Increment(int val)
{
    val++;
    Console.WriteLine(val);
}

В данном случае обе версии метода Increment имеют одинаковый набор параметров одинакового типа, однако в первом случае параметр имеет модификатор ref. Поэтому обе версии метода будут корректными перегрузками метода Increment.

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

int Sum(int x, int y)
{
    return x + y;
}
int Sum(int number1, int number2)
{
    return x + y;
}
void Sum(int x, int y)
{
    Console.WriteLine(x + y);
}

Сигнатура у всех этих методов будет совпадать:

Sum(int, int)

Поэтому данный набор методов не представляет корректные перегрузки метода Sum и работать не будет.

?? и ??=

Оператор объединения с NULL ?? возвращает значение своего операнда слева, если его значение не равно null. В противном случае он вычисляет операнд справа и возвращает его результат. Оператор ?? не выполняет оценку своего операнда справа, если его операнд слева имеет значение, отличное от NULL. Начиная с C# 8.0 можно использовать оператор присваивания объединения со значением NULL ??= для присваивания значения правого операнда левому операнду только в том случае, если левый операнд принимает значение null. Оператор ??= не выполняет оценку своего операнда справа, если его операнд слева имеет значение, отличное от NULL.

List<int> numbers = null;
int? a = null;

(numbers ??= new List<int>()).Add(5);
Console.WriteLine(string.Join(" ", numbers));  // output: 5

numbers.Add(a ??= 0);
Console.WriteLine(string.Join(" ", numbers));  // output: 5 0
Console.WriteLine(a);  // output: 0

Сокрытие

Фактически сокрытие представляет определение в классе-наследнике метода или свойства, которые соответствует по имени и набору параметров методу или свойству базового класса. Для сокрытия членов класса применяется ключевое слово new. Например:

class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
 
    public void Display()
    {
        Console.WriteLine($"{FirstName} {LastName}");
    }
}
 
class Employee : Person
{
    public string Company { get; set; }
    public Employee(string firstName, string lastName, string company)
            : base(firstName, lastName)
    {
        Company = company;
    }
    public new void Display()
    {
        Console.WriteLine($"{FirstName} {LastName} работает в {Company}");
    }
}

Здесь определен класс Person, представляющий человека, и класс Employee, представляющий работника предприятия. Employee наследует от Person все свойства и методы. Но в классе Employee кроме унаследованных свойств есть также и собственное свойство Company, которое хранит название компании. И мы хотели бы в методе Display выводить информацию о компании вместе с именем и фамилией на консоль. Для этого определяется метод Display с ключевым словом new, который скрывает реализацию данного метода из базового класса.

В каких ситуациях можно использовать сокрытие? Например, в примере выше метод Display в базовом классе не является виртуальным, мы не можем его переопределить, но, допустим, нас не устраивает его реализация для производного класса, поэтому мы можем воспользоваться сокрытием, чтобы определить нужный нам функционал.

Используем эти классы в программе в методе Main:

class Program
{
    static void Main(string[] args)
    {
        Person bob = new Person("Bob", "Robertson");
        bob.Display();      // Bob Robertson
 
        Employee tom = new Employee("Tom", "Smith", "Microsoft");
        tom.Display();      // Tom Smith работает в Microsoft
 
        Console.ReadKey();
    }
}

Консольный вывод программы:

```Bob Robertson Tom Smith работает в Microsoft

Подобным обазом мы можем организовать сокрытие свойств:

```cs
class Person
{
    protected string name;
    public string Name
    {
        get { return name; }
        set { name = value; }
    }
}
class Employee : Person
{
    public new string Name
    {
        get { return "Employee " + base.Name; }
        set { name = value; }
    }
}

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

Более того мы даже можем применять сокрытие к переменным и константам, также используя ключевое слово new:

class ExampleBase
{
    public readonly int x = 10;
    public const int G = 5;
}
class ExampleDerived : ExampleBase
{
    public new readonly int x = 20;
    public new const int G = 15;
}

Методы расширения

Методы расширения (extension methods) позволяют добавлять новые методы в уже существующие типы без создания нового производного класса. Эта функциональность бывает особенно полезна, когда нам хочется добавить в некоторый тип новый метод, но сам тип (класс или структуру) мы изменить не можем, поскольку у нас нет доступа к исходному коду. Либо если мы не можем использовать стандартный механизм наследования, например, если классы определенны с модификатором sealed.

Например, нам надо добавить для типа string новый метод:

class Program
{
    static void Main(string[] args)
    {
        string s = "Привет мир";
        char c = 'и';
        int i = s.CharCount(c);
        Console.WriteLine(i);
 
        Console.Read();
    }
}
 
public static class StringExtension
{
    public static int CharCount(this string str, char c)
    {
        int counter = 0;
        for (int i = 0; i<str.Length; i++)
        {
            if (str[i] == c)
                counter++;
        }
        return counter;
    }
}

Для того, чтобы создать метод расширения, вначале надо создать статический класс, который и будет содержать этот метод. В данном случае это класс StringExtension. Затем объявляем статический метод. Суть нашего метода расширения - подсчет количества определенных символов в строке.

Собственно метод расширения - это обычный статический метод, который в качестве первого параметра всегда принимает такую конструкцию: this имя_типа название_параметра, то есть в нашем случае this string str. Так как наш метод будет относиться к типу string, то мы и используем данный тип.

Затем у всех строк мы можем вызвать данный метод: int i = s.CharCount(c);. Причем нам уже не надо указывать первый параметр. Значения для остальных параметров передаются в обычном порядке.

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

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

Деконструкторы

Деконструкторы (не путать с деструкторами) позволяют выполнить декомпозицию объекта на отдельные части.

Например, пусть у нас есть следующий класс Person:

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
 
    public void Deconstruct(out string name, out int age)
    {
        name = this.Name;
        age = this.Age;
    }
}

В этом случае мы могли бы выполнить декомпозицию объекта Person так:

Person person = new Person { Name = "Tom", Age = 33 };
 
(string name, int age) = person;
 
Console.WriteLine(name);    // Tom
Console.WriteLine(age);     // 33

По сути деконструкторы это не более,чем синтаксический сахар. Это все равно, что если бы мы написали в предыдущих версиях C# следующий набор выражений:

Person person = new Person { Name = "Tom", Age = 33 };
 
string name; int age;
person.Deconstruct(out name, out age);

При использовании деконструкторов следует учитывать, что метод Deconstruct должен принимать как минимум два выходных параметра. То есть следующее определение метода работать не будет:

public void Deconstruct(out string name)
{
    name = this.Name;
}

Отложенная инициализация и тип Lazy

Приложение может использовать множество классов и объектов. Однако есть вероятность, что не все создаваемые объекты будут использованы. Особенно это касается больших приложений. Например:

class Reader
{
    Library library = new Library();
    public void ReadBook()
    {
        library.GetBook();
        Console.WriteLine("Читаем бумажную книгу");
    }
 
    public void ReadEbook()
    {
        Console.WriteLine("Читаем книгу на компьютере");
    }
}
 
class Library
{
    private string[] books = new string[99];
 
    public void GetBook()
    {
        Console.WriteLine("Выдаем книгу читателю");
    }
}

Есть класс Library, представляющий библиотеку и хранящий некоторый набор книг в виде массива. Есть класс читателя Reader, который хранит ссылку на объект библиотеки, в которой он записан. У читателя определено два метода: для чтения электронной книги и для чтения обычной книги. Для чтения обычной книги необходимо обратиться к методу класса Library, чтобы получить эту книгу.

Но что если читателю вообще не придется читать обычную книгу, а только электронные? В этом случае объект library в классе читателя никак не будет использоваться и будет только занимать место памяти. Хотя надобности в нем не будет.

Для подобных случаев в .NET определен специальный класс Lazy<T>. Изменим класс читателя следующим образом:

class Reader
{
    Lazy<Library> library = new Lazy<Library>();
    public void ReadBook()
    {
        library.Value.GetBook();
        Console.WriteLine("Читаем бумажную книгу");
    }
 
    public void ReadEbook()
    {
        Console.WriteLine("Читаем книгу на компьютере");
    }
}

Класс Library остается прежнем. Но теперь класс читателя содержит ссылку на библиотеку в виде объекта Lazy<Library>. А чтобы обратиться к самой библиотеке и ее методам, надо использовать выражение library.Value - это и есть объект Library.

Что меняет в поведении класса Reader эта замена? Рассмотрим его применение:

Reader reader = new Reader();
reader.ReadEbook();
reader.ReadBook();

Непосредственно объект Library задействуется здесь только на третьей строке в методе reader.ReadBook(), который вызывает в свою очередь метод library.Value.GetBook(). Поэтому вплоть до третьей строки объект Library, используемый читателем, не будет создан. Если мы не будем применять в программе метод reader.ReadBook(), то объект библиотеки тогда вобще не будет создан, и мы избежим лишних затрат памяти. Таким образом, Lazy<T> гарантирует нам, что объект будет создан только тогда, когда в нем есть необходимость.

Примеры использования тут

Необходимые ссылки

  1. Передача параметров по ссылке и значению. Выходные параметры
  2. Кортежи
  3. Перегрузка методов
  4. ?? и ??=
  5. Сокрытие
  6. Методы расширения
  7. Деконструкторы
  8. Отложенная инициализация и тип Lazy