1-26 METONIT C#

Методы в языке программирования C#

static int GetSum()
{
    int x = 2;
    int y = 3;
    return x + y;
}
static int GetSum()
{
    int x = 2;
    int y = 3;
    return "5"; // ошибка - надо возвращать число
}
using System;
namespace HelloApp
{
    class Program
    {
        static void Main(string[] args)
        {
            string message = GetHello();
            int sum = GetSum();
            Console.WriteLine(message);  // Hello
            Console.WriteLine(sum);     // 5
            Console.ReadKey();
        }
        static string GetHello()
        {
            return "Hello";
        }
        static int GetSum()
        {
            int x = 2;
            int y = 3;
            return x + y;
        }
    }
}
выходл из метода
static string GetHello()
{
    return "Hello";
    Console.WriteLine("After return");
}
С точки зрения синтаксиса данный метод корректен, однако его инструкция Console.WriteLine("After return") не имеет смысла – она никогда не выполнится, так как до ее выполнения оператор return возвратит значение и произведет выход из метода.
static void SayHello()
{
    int hour = 23;
    if(hour > 22)
    {
        return;
    }
    else
    {
        Console.WriteLine("Hello");
    }

Сокращенная запись методов

static void SayHello()
{
    Console.WriteLine("Hello");
}
или  static void SayHello() => Console.WriteLine("Hello");

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

class Program
{
static void Main(string[] args)
{
int result = Sum(10, 15);
Console.WriteLine(result);  // 25
Console.ReadKey();
}
static int Sum(int x, int y)
{
return x + y;
}
}

При вызове метода Sum значения передаются параметрам по позиции. Например, в вызове Sum(10, 15) число 10 передается параметру x, а число 15 – параметру y. Значения, которые передаются параметрам, еще называются аргументами. То есть передаваемые числа 10 и 15 в данном случае являются аргументами.

Иногда можно встретить такие определения как формальные параметры и фактические параметры. Формальные параметры – это собственно параметры метода (в данном случае x и y), а фактические параметры – значения, которые передаются формальным параметрам. То есть фактические параметры – это и есть аргументы метода.

class Program
{
static void Main(string[] args)
{
int a = 25;
int b = 35;
int result = Sum(a, b);
Console.WriteLine(result);  // 60
result = Sum(b, 45);
Console.WriteLine(result);  // 80
result = Sum(a + b + 12, 18); // "a + b + 12" представляет значение параметра x
Console.WriteLine(result);  // 90
Console.ReadKey();
}
static int Sum(int x, int y)
{
return x + y;
}
}
class Program
{
static void Main(string[] args)
{
int a;
int b = 9;
Sum(a, b);  // Ошибка - переменной a не присвоено значение 

     ПОЧЕМУ ОШИБКА?

Console.ReadKey();
}
static int Sum(int x, int y)
{
return x + y;
}
}
ри передаче значений параметрам важно учитывать тип параметров: между аргументами и параметрами должно быть соответствие по типу. Например:
class Program
{
static void Main(string[] args)
{
Display("Tom", 24); // Name: Tom  Age: 24
Console.ReadKey();
}
static void Display(string name, int age)
{
Console.WriteLine($"Name: {name}  Age: {age}");
}
}
static int OptionalParam(int x, int y, int z=5, int s=4)
{
    return x + y + z + s;
}
static void Main(string[] args)
{
    OptionalParam(x:2, y:3);
    
    //Необязательный параметр z использует значение по умолчанию
    OptionalParam(y:2, x:3, s:10); //   ПОЧЕМУ ТАК  
    Console.ReadKey();
}

14. Массив параметров и ключевое слово params

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void Addition(params int[] integers)
{
    int result = 0;
    for (int i = 0; i < integers.Length; i++)
    {
        result += integers[i];
    }
    Console.WriteLine(result);
}
static void Main(string[] args)
{
    Addition(1, 2, 3, 4, 5);
    
    int[] array = new int[] { 1, 2, 3, 4 };
    Addition(array);
    Addition();
    Console.ReadLine();
}

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

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

1
2
3
//Так работает
static void Addition( int x, string mes, params int[] integers)
{}

Вызов подобного метода:

1
Addition(2, "hello", 1, 3, 4);

Однако после параметра с модификатором params мы НЕ можем указывать другие параметры. То есть следующее определение метода недопустимо:

1
2
3
//Так НЕ работает
static void Addition(params int[] integers, int x, string mes)
{}

Массив в качестве параметра

Также этот способ передачи параметров надо отличать от передачи массива в качестве параметра:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// передача параметра с params
static void Addition(params int[] integers)
{
    int result = 0;
    for (int i = 0; i < integers.Length; i++)
    {
        result += integers[i];
    }
    Console.WriteLine(result);
}
// передача массива
static void AdditionMas(int[] integers, int k)
{
    int result = 0;
    for (int i = 0; i < integers.Length; i++)
    {
        result += (integers[i]*k);
    }
    Console.WriteLine(result);
}
static void Main(string[] args)
{
    Addition(1, 2, 3, 4, 5);
    int[] array = new int[] { 1, 2, 3, 4 };
    AdditionMas(array, 2);
    Console.ReadLine();
}

Так как метод AdditionMas принимает в качестве параметра массив без ключевого слова params, то при его вызове нам обязательно надо передать в качестве параметра массив

14. Область видимости (контекст) переменных

Каждая переменная доступна в рамках определенного контекста или области видимость. Вне этого контекста переменная уже не существует.

Существуют различные контексты:

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

Например, пусть класс Program определен следующим образом:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Program // начало контекста класса
{
    static int a = 9; // переменная уровня класса
    
    static void Main(string[] args) // начало контекста метода Main
    {
        int b = a - 1; // переменная уровня метода
        { // начало контекста блока кода
            
            int c = b - 1; // переменная уровня блока кода
        // конец контекста блока кода, переменная с уничтожается
        //так нельзя, переменная c определена в блоке кода
        //Console.WriteLine(c);
        //так нельзя, переменная d определена в другом методе
        //Console.WriteLine(d);
        Console.Read();
    } // конец контекста метода Main, переменная b уничтожается
    void Display() // начало контекста метода Display
    {
        // переменная a определена в контексте класса, поэтому доступна
        int d = a + 1;
    } // конец конекста метода Display, переменная d уничтожается
} // конец контекста класса, переменная a уничтожается

Здесь определенно четыре переменных: a, b, c, d. Каждая из них существует в своем контексте. Переменная a существует в контексте всего класса Program и доступна в любом месте и блоке кода в методах Main и Display.

Переменная b существует только в рамках метода Main. Также как и переменная d существует в рамках метода Display. В методе Main мы не можем обратиться к переменной d, так как она в другом контексте.

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

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

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

1
2
3
4
5
6
7
8
9
10
class Program
{
    static int a = 9; // переменная уровня класса
    
    static void Main(string[] args)
    {
        int a = 5; // скрывает переменную a, которая объявлена на уровне класса
        Console.WriteLine(a); // 5
    }
}

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

 

15. Рекурсивные функции

Отдельно остановимся на рекурсивных функциях. Рекурсивная функция представляет такую конструкцию, при которой функция вызывает саму себя.

Возьмем, к примеру, вычисление факториала, которое использует формулу n! = 1 * 2 * … * n. Например, факториал числа 5 равен 120 = 1 * 2 * 3 * 4 * 5.

Определим метод для нахождения факториала:

1
2
3
4
5
6
7
8
9
10
11
static int Factorial(int x)
{
    if (x == 0)
    {
        return 1;
    }
    else
    {
        return x * Factorial(x - 1);
    }
}

Итак, здесь у нас задается условие, что если вводимое число не равно 0, то мы умножаем данное число на результат этой же функции, в которую в качестве параметра передается число x-1. То есть происходит рекурсивный спуск. И так, пока не дойдем того момента, когда значение параметра не будет равно единице.

При создании рекурсивной функции в ней обязательно должен быть некоторый базовый вариант, который использует оператор return и помещается в начале функции. В случае с факториалом это if (x == 0) return 1;.

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

Другим распространенным показательным примером рекурсивной функции служит функция, вычисляющая числа Фиббоначчи. n-й член последовательности Фибоначчи определяется по формуле: f(n)=f(n-1) + f(n-2), причем f(0)=0, а f(1)=1. То есть последовательность Фибоначчи будет выглядеть так 0 (0-й член), 1 (1-й член), 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, …. Для определения чисел этой последовательности определим следующий метод:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static int Fibonachi(int n)
{
    if (n == 0)
    {
        return 0;
    }
    else if (n == 1)
    {
        return 1;
    }
    else
    {
        return Fibonachi(n - 1) + Fibonachi(n - 2);
    }
}

Либо, если сократить первые две условные конструкции, так:

1
2
3
4
5
6
7
8
9
10
11
static int Fibonachi(int n)
{
    if (n == 0 || n == 1)
    {
        return n;
    }
    else
    {
        return Fibonachi(n - 1) + Fibonachi(n - 2);
    }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static int Fibonacci(int n)
{
    int a = 0;
    int b = 1;
    int tmp;
    for (int i = 0; i < n; i++)
    {
        tmp = a;
        a = b;
        b += tmp;
    }
    return a;
}

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

Кроме примитивных типов данных в C# есть такой тип как enum или перечисление. Перечисления представляют набор логически связанных констант. Объявление перечисления происходит с помощью оператора enum. Далее идет название перечисления, после которого указывается тип перечисления – он обязательно должен представлять целочисленный тип (byte, int, short, long). Если тип явным образом не указан, то по умолчанию используется тип int. Затем идет список элементов перечисления через запятую:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum Days
{
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
}
enum Time : byte
{
Morning,
Afternoon,
Evening,
Night
}

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

1
2
3
4
5
6
7
enum Operation
{
Add = 1,   // каждый следующий элемент по умолчанию увеличивается на единицу
Subtract, // этот элемент равен 2
Multiply, // равен 3
Divide    // равен 4
}

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

1
2
3
4
5
6
7
enum Operation
{
Add = 2,
Subtract = 4,
Multiply = 8,
Divide = 16
}

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

1
2
3
4
5
6
7
enum Color
{
White = 1,
Black = 2,
Green = 2,
Blue = White // Blue = 1
}

Каждое перечисление фактически определяет новый тип данных. Затем в программе мы можем определить переменную этого типа и использовать ее:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum Operation
{
Add = 1,
Subtract,
Multiply,
Divide
}
class Program
{
static void Main(string[] args)
{
Operation op;
op = Operation.Add;
Console.WriteLine(op); // Add
Console.ReadLine();
}

В программе мы можем присвоить значение этой переменной. При этом в качестве ее значения должна выступать одна из констант, определенных в данном перечислении. То есть несмотря на то, что каждая константа сопоставляется с определенным числом, мы не можем присвоить ей числовое значение, например, Operation op = 1;. И также если мы будем выводить на консоль значение этой переменной, то мы получим им константы, а не числовое значение. Если же необходимо получить числовое значение, то следует выполнить приведение к числовому типу:

1
2
3
Operation op;
op = Operation.Multiply;
Console.WriteLine((int)op); // 3

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum Operation
{
Add = 1,
Subtract,
Multiply,
Divide
}
class Program
{   
static void Main(string[] args)
{
Operation op;
op = Operation.Add;
Console.WriteLine(op); // Add
Console.ReadLine();
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Program
{
enum Operation
{
Add = 1,
Subtract,
Multiply,
Divide
}
static void MathOp(double x, double y, Operation op)
{
double result = 0.0;
switch (op)
{
case Operation.Add:
result = x + y;
break;
case Operation.Subtract:
result = x - y;
break;
case Operation.Multiply:
result = x * y;
break;
case Operation.Divide:
result = x / y;
break;
}
Console.WriteLine("Результат операции равен {0}", result);
}
static void Main(string[] args)
{
// Тип операции задаем с помощью константы Operation.Add, которая равна 1
MathOp(10, 5, Operation.Add);
// Тип операции задаем с помощью константы Operation.Multiply, которая равна 3
MathOp(11, 5, Operation.Multiply);
Console.ReadLine();
}

Здесь у нас имеется перечисление Operation, которое представляет арифметические операции. Также у нас определен метод MathOp, который в качестве параметров принимает два числа и тип операции. В основном методе Main мы два раза вызываем процедуру MathOp, передав в нее два числа и тип операции

 

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

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

1
var tuple = (5, 10);

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

1
2
3
4
5
6
7
8
9
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();
}

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

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

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

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

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

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

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

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

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

1
2
3
4
5
6
7
static void Main(string[] args)
{
var (name, age) = ("Tom", 23);
Console.WriteLine(name);    // Tom
Console.WriteLine(age);     // 23
Console.Read();
}

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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, то есть два числа.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
}

    21. КЛАССЫ

using System;

namespace HelloApp
{
class Person
{
public string name; // имя
public int age = 18;     // возраст

public void GetInfo()
{
Console.WriteLine($”Имя: {name}  Возраст: {age}”);
}
}
class Program
{
static void Main(string[] args)
{
Person tom;
}
}
}

КОНСТРУКТОР ПО УМОЛЧАНИЮ

Если в классе не определено ни одного конструктора

class Person
{
public string name; // имя
public int age;     // возраст
public void GetInfo()
{
Console.WriteLine($"Имя: {name}  Возраст: {age}");
}
}
class Program
{
static void Main(string[] args)
{
Person tom = new Person();
tom.GetInfo();      // Имя: Возраст: 0
tom.name = "Tom";
tom.age = 34;
tom.GetInfo();  // Имя: Tom Возраст: 34
Console.ReadKey();
}
}
Имя:	Возраст: 0
Имя: Tom	Возраст: 34

Создание конструкторов

Выше для инициализации объекта использовался конструктор по умолчанию. Однако мы сами можем определить свои конструкторы:

class Person
{
public string name;
public int age;
public Person() { name = "Неизвестно"; age = 18; }      // 1 конструктор
public Person(string n) { name = n; age = 18; }         // 2 конструктор
public Person(string n, int a) { name = n; age = a; }   // 3 конструктор
public void GetInfo()
{
Console.WriteLine($"Имя: {name}  Возраст: {age}");
}
}

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

1
2
3
4
5
6
7
8
9
10
11
static void Main(string[] args)
{
Person tom = new Person();          // вызов 1-ого конструктора без параметров
Person bob = new Person("Bob");     //вызов 2-ого конструктора с одним параметром
Person sam = new Person("Sam", 25); // вызов 3-его конструктора с двумя параметрами
bob.GetInfo();          // Имя: Bob  Возраст: 18
tom.GetInfo();          // Имя: Неизвестно  Возраст: 18
sam.GetInfo();          // Имя: Sam  Возраст: 25
}

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

Имя: Неизвестно  Возраст: 18
Имя: Bob  Возраст: 18
Имя: Sam  Возраст: 25

При этом если в классе определены конструкторы, то при создании объекта необходимо использовать один из этих конструкторов.

Ключевое слово this

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person
{
public string name;
public int age;
public Person() : this("Неизвестно")
{
}
public Person(string name) : this(name, 18)
{
}
public Person(string name, int age)
{
this.name = name;
this.age = age;
}
public void GetInfo()
{
Console.WriteLine($"Имя: {name}  Возраст: {age}");
}
}

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

1
2
3
public Person(string name) : this(name, 18)
{
}

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

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

1
2
3
4
5
public Person(string name, int age)
{
this.name = name;
this.age = age;
}

И чтобы разграничить параметры и поля класса, к полям класса обращение идет через ключевое слово this. Так, в выражении this.name = name; первая часть this.name означает, что name – это поле текущего класса, а не название параметра name. Если бы у нас параметры и поля назывались по-разному, то использовать слово this было бы необязательно. Также через ключевое слово this можно обращаться к любому полю или методу.

Инициализаторы объектов

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

1
2
Person tom = new Person { name = "Tom", age=31 };
tom.GetInfo();          // Имя: Tom  Возраст: 31

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

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

  • С помощью инициализатора мы можем установить значения только доступных из внешнего кода полей и свойств объекта. Например, в примере выше поля name и age имеют модификатор доступа public, поэтому они доступны из любой части программы.
  • Инициализатор выполняется после конструктора, поэтому если и в конструкторе, и в инициализаторе устанавливаются значения одних и тех же полей и свойств, то значения, устанавливаемые в конструкторе, заменяются значениями из инициализатора.{
    | class Program
    {
    | static void Main(string[] args)
    {
    User tom = new User { name = “Tom”, age = 23 };
    //tom.name = “Tom”;
    //tom.age = 22;
    tom. Info();
    Use~ bob = new User(“Bob”) { age = 26 };
    //bob.name = “Bob”; I
    //bob.age = 22;
    bob. Info();
    Console. ReadKey();
    | }
    4}
    ] class User
    {
    public string name;
    public int age;

class User
{
public string name;
public int age;
public User()
{
public User(string n)
{
name = n;
public User(string n, int a)
{
name = n;
age = a;
}
public void Info()
{
Console. .wWriteLine($”{name} – {age}”);
— }
4}
}

 22 СТРУКТУРЫ

Наряду с классами структуры представляют еще один способ создания собственных типов данных в C#. Более того многие примитивные типы, например, int, double и т.д., по сути являются структурами.

Например, определим структуру, которая представляет человека:

1
2
3
4
5
6
7
8
9
10
struct User
{
public string name;
public int age;
public void DisplayInfo()
{
Console.WriteLine($"Name: {name}  Age: {age}");
}
}

Как и классы, структуры могут хранить состояние в виде переменных и определять поведение в виде методов. Так, в данном случае определены две переменные – name и age для хранения соответственно имени и возраста человека и метод DisplayInfo для вывода информации о человеке.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;
namespace HelloApp
{
struct User
{
public string name;
public int age;
public void DisplayInfo()
{
Console.WriteLine($"Name: {name}  Age: {age}");
}
}
class Program
{
static void Main(string[] args)
{
User tom;
tom.name = "Tom";
tom.age = 34;
tom.DisplayInfo();
Console.ReadKey();
}
}
}

В данном случае создается объект tom. У него устанавливаются значения глобальных переменных, и затем выводится информация о нем.

Конструкторы структуры

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

1
User tom;

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

1
2
3
User tom;
int x = tom.age;    // Ошибка
tom.DisplayInfo();  // Ошибка

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

1
2
User tom = new User();
tom.DisplayInfo();  // Name:   Age: 0

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

Также мы можем определить свои конструкторы. Например, изменим структуру User:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using System;
namespace HelloApp
{
struct User
{
public string name;
public int age;
public User(string name, int age)
{
this.name = name;
this.age = age;
}
public void DisplayInfo()
{
Console.WriteLine($"Name: {name}  Age: {age}");
}
}
class Program
{
static void Main(string[] args)
{
User tom = new User("Tom", 34);
tom.DisplayInfo();
User bob = new User();
bob.DisplayInfo();
Console.ReadKey();
}
}
}

Важно учитывать, что если мы определяем конструктор в структуре, то он должен инициализировать все поля структуры, как в данном случае устанавливаются значения для переменных name и age.

Также, как и для класса, можно использовать инициализатор для создания структуры:

1
User person = new User { name = "Sam", age = 31 };

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

1
2
3
4
5
6
7
8
9
struct User
{
public string name = "Sam";     // ! Ошибка
public int age = 23;            // ! Ошибка
public void DisplayInfo()
{
Console.WriteLine($"Name: {name}  Age: {age}");
}

    23  Типы значений и ссылочные типы

а нее мы рассматривали следующие элементарные типы данных: int, byte, double, string, object и др. Также есть сложные типы: структуры, перечисления, классы. Все эти типы данных можно разделить на типы значений, еще называемые значимыми типами, (value types) и ссылочные типы (reference types). Важно понимать между ними различия.

Типы значений:

  • Целочисленные типы (byte, sbyte, short, ushort, int, uint, long, ulong)
  • Типы с плавающей запятой (float, double)
  • Тип decimal
  • Тип bool
  • Тип char
  • Перечисления enum
  • Структуры (struct)

Ссылочные типы:

  • Тип object
  • Тип string
  • Классы (class)
  • Интерфейсы (interface)
  • Делегаты (delegate)

В чем же между ними различия? Для этого надо понять организацию памяти в .NET. Здесь память делится на два типа: стек и куча (heap). Параметры и переменные метода, которые представляют типы значений, размещают свое значение в стеке. Стек представляет собой структуру данных, которая растет снизу вверх: каждый новый добавляемый элемент помещается поверх предыдущего. Время жизни переменных таких типов ограничено их контекстом. Физически стек – это некоторая область памяти в адресном пространстве.

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

Например:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Program
{
static void Main(string[] args)
{
Calculate(5);
Console.ReadKey();
}
static void Calculate(int t)
{
int x = 6;
int y = 7;
int z = y + t;
}
}

Пи запуске такой программы в стеке будут определяться два фрейма – для метода Main (так как он вызывается при запуске программы) и для метода Calculate:

Структура стека в языке программирования C#

При вызове этого метода Calculate в его фрейм в стеке будут помещаться значения t, x, y и z. Они определяются в контексте данного метода. Когда метод отработает, область памяти, которая выделялась под стек, впоследствии может быть использована другими методами.

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

Ссылочные типы хранятся в куче или хипе, которую можно представить как неупорядоченный набор разнородных объектов. Физически это остальная часть памяти, которая доступна процессу.

При создании объекта ссылочного типа в стеке помещается ссылка на адрес в куче (хипе). Когда объект ссылочного типа перестает использоваться, в дело вступает автоматический сборщик мусора: он видит, что на объект в хипе нету больше ссылок, условно удаляет этот объект и очищает память – фактически помечает, что данный сегмент памяти может быть использован для хранения других данных.

Так, в частности, если мы изменим метод Calculate следующим образом:

1
2
3
4
5
6
static void Calculate(int t)
{
object x = 6;
int y = 7;
int z = y + t;
}

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

Ссылочные типы в куче в языке программирования C#

Составные типы

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Program
{
private static void Main(string[] args)
{
State state1 = new State(); // State - структура, ее данные размещены в стеке
Country country1 = new Country(); // Country - класс, в стек помещается ссылка на адрес в хипе
// а в хипе располагаются все данные объекта country1
}
}
struct State
{
public int x;
public int y;
public Country country;
}
class Country
{
public int x;
public int y;
}

Здесь в методе Main в стеке выделяется память для объекта state1. Далее в стеке создается ссылка для объекта country1 (Country country1), а с помощью вызова конструктора с ключевым словом new выделяется место в хипе (new Country()). Ссылка в стеке для объекта country1 будет представлять адрес на место в хипе, по которому размещен данный объект..

Ссылычные типы и типы значений в C#

Таким образом, в стеке окажутся все поля структуры state1 и ссылка на объект country1 в хипе.

Однако в структуре State также определена переменная ссылочного типа Country. Где она будет хранить свое значение, если она определена в типе значений?

1
2
3
4
5
6
private static void Main(string[] args)
{
State state1 = new State();
state1.country = new Country();
Country country1 = new Country();
}

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

Стек и куча в языке программирования C#

Копирование значений

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static void Main(string[] args)
{
State state1 = new State(); // Структура State
State state2 = new State();
state2.x = 1;
state2.y = 2;
state1 = state2;
state2.x = 5; // state1.x=1 по-прежнему
Console.WriteLine(state1.x); // 1
Console.WriteLine(state2.x); // 5
Country country1 = new Country(); // Класс Country
Country country2 = new Country();
country2.x = 1;
country2.y = 4;
country1 = country2;
country2.x = 7; // теперь и country1.x = 7, так как обе ссылки и country1 и country2
// указывают на один объект в хипе
Console.WriteLine(country1.x); // 7
Console.WriteLine(country2.x); // 7
Console.Read();
}

Так как state1 – структура, то при присвоении state1 = state2 она получает копию структуры state2. А объект класса country1 при присвоении country1 = country2; получает ссылку на тот же объект, на который указывает country2. Поэтому с изменением country2, так же будет меняться и country1.

Ссылочные типы внутри типов значений

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Program
{
private static void Main(string[] args)
{
State state1 = new State();
State state2 = new State();
state2.country = new Country();
state2.country.x = 5;
state1 = state2;
state2.country.x = 8; // теперь и state1.country.x=8, так как state1.country и state2.country
// указывают на один объект в хипе
Console.WriteLine(state1.country.x); // 8
Console.WriteLine(state2.country.x); // 8
Console.Read();
}
}
struct State
{
public int x;
public int y;
public Country country;
}
class Country
{
public int x;
public int y;
}

Переменные ссылочных типов в структурах также сохраняют в стеке ссылку на объект в хипе. И при присвоении двух структур state1 = state2; структура state1 также получит ссылку на объект country в хипе. Поэтому изменение state2.country повлечет за собой также изменение state1.country.

Объекты классов как параметры методов

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Program
{
static void Main(string[] args)
{
Person p = new Person { name = "Tom", age=23 };
ChangePerson(p); 
Console.WriteLine(p.name); // Alice
Console.WriteLine(p.age); // 23
Console.Read();
}
static void ChangePerson(Person person)
{
// сработает
person.name = "Alice";
// сработает только в рамках данного метода
person = new Person { name = "Bill", age = 45 };
Console.WriteLine(person.name); // Bill
}
}
class Person
{
public string name;
public int age;
}

При передаче объекта класса по значению в метод передается копия ссылки на объект. Эта копия указывает на тот же объект, что и исходная ссылка, потому мы можем изменить отдельные поля и свойства объекта, но не можем изменить сам объект. Поэтому в примере выше сработает только строка person.name = "Alice".

А другая строка person = new Person { name = "Bill", age = 45 } создаст новый объект в памяти, и person теперь будет указывать на новый объект в памяти. Даже если после этого мы его изменим, то это никак не повлияет на ссылку p в методе Main, поскольку ссылка p все еще указывает на старый объект в памяти.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Program
{
static void Main(string[] args)
{
Person p = new Person { name = "Tom", age=23 };
ChangePerson(ref p); 
Console.WriteLine(p.name); // Bill
Console.WriteLine(p.age); // 45
Console.Read();
}
static void ChangePerson(ref Person person)
{
// сработает
person.name = "Alice";
// сработает
person = new Person { name = "Bill", age = 45 };
}
}
class Person
{
public string name;
public int age;
}

Операция new создаст новый объект в памяти, и теперь ссылка person (она же ссылка p из метода Main) будет указывать уже на новый объект в памяти.

  24 Пространства имен, псевдонимы и статический импорт

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

1
2
3
4
5
6
7
8
9
namespace HelloApp
class Program 
{
static void Main(string[] args)
{
}
}
}

Пространство имен определяется с помощью ключевого слова namespace, после которого идет название. Так в данном случае полное название класса Program будет HelloApp.Program.

Класс Program видит все классы, которые объявлены в том же пространстве имен:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace HelloApp
class Program 
{
static void Main(string[] args)
{
Account account = new Account(4);
}
}
class Account
{
public int Id { get; private set;} // номер счета
public Account(int _id)
{
Id = _id;
}
}
}

Но чтобы задействовать классы из других пространств имен, эти пространства надо подключить с помощью директивы using:

1
2
3
4
5
6
7
8
9
10
11
using System;
namespace HelloApp
class Program 
{
static void Main(string[] args)
{
Console.WriteLine("hello");
}
}
}

Здесь подключается пространство имен System, в котором определен класс Console. Иначе нам бы пришлось писать полный путь к классу:

1
2
3
4
static void Main(string[] args)
{
System.Console.WriteLine("hello");
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using HelloApp.AccountSpace;
namespace HelloApp
class Program 
{
static void Main(string[] args)
{
Account account = new Account(4);
}
}
namespace AccountSpace
{
class Account
{
public int Id { get; private set;}
public Account(int _id)
{
Id = _id;
}
}
}
}

В этом случае для подключения пространства указывается его полный путь с учетом внешних пространств имен: using HelloApp.AccountSpace;

25.  Псевдонимы

Для различных классов мы можем использовать псевдонимы. Затем в программе вместо названия класса используется его псевдоним. Например, для вывода строки на экран применяется метод Console.WriteLine(). Но теперь зададим для класса Console псевдоним:

1
2
3
4
5
6
7
8
9
using printer = System.Console;
class Program
{
static void Main(string[] args)
{
printer.WriteLine("Hello from C#");
printer.Read();
}
}

С помощью выражения using printer = System.Console указываем, что псевдонимом для класса System.Console будет имя printer. Это выражение не имеет ничего общего с подключением пространств имен в начале файла, хотя и использует оператор using. При этом используется полное имя класса с учетом пространства имен, в котором класс определен. И далее, чтобы вывести строку, применяется выражение printer.WriteLine("Hello from C#").

И еще пример. Определим класс и для него псевдоним:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using Person = HelloApp.User;
using Printer = System.Console;
namespace HelloApp
{
class Program
{
static void Main(string[] args)
{
Person person = new Person();
person.name = "Tom";
Printer.WriteLine(person.name);
Printer.Read();
}
}
class User
{
public string name;
}
}

Класс называется User, но в программе для него используется псевдоним Person.

Также в C# имеется возможность импорта функциональности классов. Например, импортируем возможности класса Console:

1
2
3
4
5
6
7
8
9
10
11
12
using static System.Console;
namespace HelloApp
{
class Program
{
static void Main(string[] args)
{
WriteLine("Hello from C# 8.0");
Read();
}
}
}

Выражение using static подключает в программу все статические методы и свойства, а также константы. И после этого мы можем не указывать название класса при вызове метода.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using static System.Console;
using static System.Math;
using static HelloApp.Geometry;
namespace HelloApp
{
class Program
{
static void Main(string[] args)
{
double radius = 50;
double result = GetArea(radius); //Geometry.GetArea
WriteLine(result); //Console.WriteLine
Read(); // Console.Read
}
}
class Geometry
{
public static double GetArea(double radius)
{
return PI * radius * radius; // Math.PI
}
}
  24}

26.   Создание библиотеки классов

Последнее обновление: 22.09.2019

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

Создадим и подключим библиотеку классов.

Возьмем имеющийся проект консольного приложения .NET Core, например, созданный в прошлых темах. В структуре проекта нажмем правой кнопкой на название решения и далее в появившемся контекстном меню выберем Add -> New Project… (Добавить новый проект):

Создание библиотеки классов в C#Далее в списке шаблонов проекта найдем пункт Class Library (.NET Core):

Библиотека классов в C# и .NET CoreЗатем дадим новому проекту какое-нибудь название, например, MyLib:

Class Library in .NET CoreПосле этого в решение будет добавлен новый проект, в моем случае с названием MyLib:

Добавление нового проекта в C# и .NET CoreПо умолчанию новый проект имеет один пустой класс Class1 в файле Class1.cs. Мы можем этот файл удалить или переименовать, как нам больше нравится.

Например, переименуем файл Class1.cs в Person.cs, а класс Class1 в Person. Определим в классе Person простейший код:

1
2
3
4
5
public class Person
{
    public string name;
    public int age;
}

Новый проект в C# и .NET CoreТеперь скомпилируем библиотеку классов. Для этого нажмем правой кнопкой на проект библиотеки классов и в контекстном меню выберем пункт Rebuild:

Компиляция библиотеки классов в C# и .NET CoreПосле компиляции библиотеки классов в папке проекта в каталоге bin/Debug/netcoreapp3.0 мы сможем найти скомпилированный файл dll (MyLib.dll). Подключим его в основной проект. Для этого в основном проекте нажмем правой кнопкой на узел Dependencies и в контекстном меню выберем пункт Add Reference:

Добавление библиотеки классов в проекте на C# и .NET CoreДалее нам откроется окно для добавления библиотек. В этом окне выберем пункт Solution,который позволяет увидеть все библиотеки классов из проектов текущего решения, поставим отметку рядом с нашей библиотекой и нажмем на кнопку OK:

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

После успешного подключения библиотеки в главном проекте изменим класс Program, чтобы он использовал класс Person из библиотеки классов:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;
using MyLib;    // подключение пространства имен из библиотеки классов
namespace HelloApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Person tom = new Person { name = "Tom", age = 35 };
            Console.WriteLine(tom.name);
        }