Методы в языке программирования 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:
При вызове этого метода 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, а в стеке будет храниться ссылка на объект в куче.
Составные типы
Теперь рассмотим ситуацию, когда тип значений и ссылочный тип представляют составные типы – структуру и класс:
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 будет представлять адрес на место в хипе, по которому размещен данный объект..
Таким образом, в стеке окажутся все поля структуры 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 также будет храниться в куче, так как эта переменная представляет ссылочный тип:
Копирование значений
Тип данных надо учитывать при копировании значений. При присвоении данных объекту значимого типа он получает копию данных. При присвоении данных объекту ссылочного типа он получает не копию объекта, а ссылку на этот объект в хипе. Например:
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. Создание библиотеки классов
Нередко различные классы и структуры оформляются в виде отдельных библиотек, которые компилируются в файлы dll и затем могут подключать в другие проекты. Благодаря этому мы можем определить один и тот же функционал в виде библиотеки классов и подключать в различные проекты или передавать на использование другим разработчикам.
Создадим и подключим библиотеку классов.
Возьмем имеющийся проект консольного приложения .NET Core, например, созданный в прошлых темах. В структуре проекта нажмем правой кнопкой на название решения и далее в появившемся контекстном меню выберем Add -> New Project… (Добавить новый проект):
Далее в списке шаблонов проекта найдем пункт Class Library (.NET Core):
Затем дадим новому проекту какое-нибудь название, например, MyLib:
После этого в решение будет добавлен новый проект, в моем случае с названием MyLib:
По умолчанию новый проект имеет один пустой класс Class1 в файле Class1.cs. Мы можем этот файл удалить или переименовать, как нам больше нравится.
Например, переименуем файл Class1.cs в Person.cs, а класс Class1 в Person. Определим в классе Person простейший код:
1
2
3
4
5
|
public class Person { public string name; public int age; } |
Теперь скомпилируем библиотеку классов. Для этого нажмем правой кнопкой на проект библиотеки классов и в контекстном меню выберем пункт Rebuild:
После компиляции библиотеки классов в папке проекта в каталоге bin/Debug/netcoreapp3.0 мы сможем найти скомпилированный файл dll (MyLib.dll). Подключим его в основной проект. Для этого в основном проекте нажмем правой кнопкой на узел Dependencies и в контекстном меню выберем пункт Add Reference:
Далее нам откроется окно для добавления библиотек. В этом окне выберем пункт 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); } |