Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Основы объектно ориентированного программирования на языке С# базовые сведения о языке. Учебное пособие
.pdf
Присваивание выполняется только в том случае, если b имеет значение
null.
Еще одно выражение с проверкой на null используется для обращения
к членам класса:
var c = a?.b?.x;
Это эквивалентно по смыслу следующему выражению (но выполняется
обычно быстрее):
var c = a == null?null: (a.b == null? null: a.b.x);
Заметим, что при включенной проверке допустимости пустых ссылок
иногда можно использовать еще одну возможность – подавление проверки
компилятором пустой ссылки. Для этого используется символ «!»:
var c = a?.b!.x;
Данное выражение означает, что программист уверен, что поле или
свойство b – не null. Если во время выполнения программы окажется, что
там все-таки null, то будет выброшено соответствующее исключение
(NullReferenceException).
5.10. Делегаты
Одним из распространенных приемов программирования во многих
языках является передача объекту некоторой внешней функции, которая
будет вызываться в специальных случаях. В языке C# для этого существует специальный механизм событий и подписки на них с использованием делегатов.
Делегат – это специальный тип, описывающий тип и параметры
метода, который может быть передан куда-либо в качестве параметра.
Описание делегата имеет вид <модификатор доступа> delegate <тип зна-
чения или void> <идентификатор> ( <список параметров метода> ).
Приведем небольшой (и несколько искусственный) пример:
double DegreeConverter(double degree) => degree / 180 * Math.PI;
var angle = new Angle() { Converter = DegreeConverter };
angle.SetThrowConverter(90);
Console.WriteLine(angle.Degrees);
angle.Radians = Math.PI;
Console.WriteLine(angle.Degrees);
class Angle
41

{
public static double DefaultConverter(double value) => value;
public double Radians { get; set; }
public double Degrees => Radians / Math.PI * 180;
public delegate double ConvertToRadians(double x);
public void SetThrowConverter(double value) { Radians = Converter(value);}
public ConvertToRadians Converter {private get; init;} = DefaultConverter;
}
Поскольку делегат – это тип, то можно объявлять переменные, поля и
свойства этого типа. При этом само ключевое слово delegate можно использовать еще и другим способом, для создания анонимных делегатов.
Например, для инициализации свойства Converter в последнем примере
необязательно было создавать статическую функцию, можно написать
public ConvertToRadians Converter { private get; init; } =
delegate (double value) { return value; };
Правда, анонимные делегаты в современных версиях языка практически не используются, чаще используются лямбда-выражения с более коротким синтаксисом
public ConvertToRadians Converter { private get; init; } = v => v;
которые мы подробнее рассмотрим позже.
Для переменной типа «делегат» определены некоторые операции.
Например, сумма делегатов – это делегат, последовательно выполняющий
суммируемые операции. Определены также операции сравнения ‘==’ и
‘!=’.
5.11. События
События – это расширение возможностей делегатов, в каком-то
смысле похожее на то, как свойства расширяют возможности полей.
Одним из распространенных сценариев использования делегатов является паттерн программирования «наблюдатель» (observer). В этом паттерне наблюдаемый объект (observable) должен оповещать другие объекты (наблюдателей) о том, что произошло какое-то событие (например,
изменились какие-то значения свойств). Наблюдаемый объект должен при
этом предоставить наблюдателю три возможности: подписаться на оповещение (делегатом), отказаться от дальнейших оповещений (отписаться) и,
собственно, само оповещение. В C# для реализации этого паттерна ввели
42

специальное ключевое слово event (событие). Событие имеет тип делегата
и для «наблюдателя» снаружи класса имеет простой синтаксис подписки
и отписки – операторами ‘+=’ и ‘–=’ соответственно. Внутри класса ob-
servable простейшее описание события имеет вид <модификатор доступа> event <тип делегата> <идентификатор>. Приведем пример
реализации паттерна:
var Observable = new ObservableClass();
var observer1 = new ObserverClass() { Name = "Observer1" };
var observer2 = new ObserverClass() { Name = "Observer2" };
observer1.Subscribe(Observable);
observer2.Subscribe(Observable);
Observable.X = 1;
observer1.Unsubscribe(Observable);
Observable.X = 2;
public class ObservableClass
{
public delegate void Notifier(string property, double value);
double x = 0;
public double X
{
get => x;
set
{
x = value;
Notify?.Invoke("X", x);
}
}
public event Notifier? Notify;
}
public class ObserverClass
{
public string Name { get; init; } = "";
public void OnObservablePropertyChange(string propertyName,
double value) =>
Console.WriteLine(Name + ": property changed " + propertyName +
", new value:" + value);
public void Subscribe(ObservableClass observable)
=> observable.Notify += OnObservablePropertyChange;
public void Unsubscribe(ObservableClass observable)
=> observable.Notify -= OnObservablePropertyChange;
}
Результат работы приведенной программы:
Observer1: property changed X, new value:1
Observer2: property changed X, new value:1
Observer2: property changed X, new value:2
43

Заметим, что если из приведенной программы убрать ключевое слово
event и больше ничего не менять, то программа будет работать точно
так же. Возникает очевидный вопрос: а зачем же нужен event? В приведен-
ном примере он не нужен. Вообще же, поведение делегата, объявленного
с ключевым словом event и без него, практически не отличается. Небольшое отличие лишь в том, что, по утверждению стандарта языка, подписка
и отписка для event является потокобезопасной (т. е. может быть вызвана
параллельно из разных потоков выполнения).
Ключевое слово event предоставляет дополнительную возможность
управления подпиской и отпиской, задавая действия в явном виде. Синтаксически это похоже на get и set у свойства класса, только для подписки
используется ключевое слово add, а для отписки – remove. Например, Ob-
servableClass из последнего примера можно переписать следующим образом, полностью эквивалентным исходному:
public class ObservableClass
{
public delegate void Notifier(string property, double value);
double x = 0;
public double X
{
get => x;
set
{
x = value;
notify?.Invoke("X", x);
}
}
event Notifier? notify;
public event Notifier Notify
{
add { notify += value; }
remove { notify -= value; }
}
}
Заметим, что в современном C# события (event) стараются использовать реже, а для реализации паттерна «наблюдатель» рекомендуется использовать другие способы (например, см.: https://learn.microsoft.com/en-
us/dotnet/standard/events/observer-design-pattern). Это связано с тем, что при
подписке на событие через лямбда-выражение от него невозможно отписаться, и это создает дополнительные проблемы с освобождением занятой
памяти.
44

5.12. Структуры
Объект типа-значения можно описать ключевым словом struct.
Правила описания структуры почти полностью совпадают с правилами описания класса: <атрибуты структуры> <модификаторы струк-
туры> partial struct <идентификатор> <описание параметров для универсальной структуры> : <интерфейсы> <требования к типам-параметрам> { <Тело структуры> };
Тело структуры может содержать те же объявления, что и тело класса
(небольшое отличие состоит в том, что у структуры не может быть метода
завершения (финализатора), о котором мы расскажем в разделе 5.14).
Еще одно отличие заключается в невозможности наследования структур
(наследование мы рассмотрим в разделе 9). До стандарта C# 11 структура
должна была в любом конструкторе инициализировать значения всех полей, и компилятор строго следил за этим. С этого же стандарта появилась
возможность инициализировать все поля значениями по умолчанию.
5.13. Перегрузка операторов
Язык C#, как и многие другие, позволяет переопределять операторы
для создаваемых программистом типов. Рассмотрим пример определения
типа «двумерный вектор»:
Vector2D a = new(1, 3);
Vector2D b = new(2, -1);
Console.WriteLine((a + b).Text);
Console.WriteLine(a * b);
Console.WriteLine((a * b * a).Text);
Console.WriteLine((10 * a).Text);
Console.WriteLine((10 * a).Norm);
var c = a + b;
c *= -1;
Console.WriteLine(c.Text);
public readonly struct Vector2D
{
public double X { get; init; }
public double Y { get; init; }
public string Text => $"({X}, {Y})";
public Vector2D(double x, double y) { X = x; Y = y; }
public double Norm => Math.Sqrt(X * X + Y * Y);
public static Vector2D operator +(Vector2D a, Vector2D b)
=> new Vector2D(a.X + b.X, a.Y + b.Y);
public static Vector2D operator -(Vector2D a, Vector2D b)
=> new Vector2D(a.X - b.X, a.Y - b.Y);
public static double operator *(Vector2D a, Vector2D b)
45

=> (a.X * b.X + a.Y * b.Y);
public static Vector2D operator *(Vector2D a, double b) =>
new Vector2D() { X = a.X * b, Y = a.Y * b };
public static Vector2D operator *(double b, Vector2D a) =>
new Vector2D() { X = a.X * b, Y = a.Y * b };
}
Результат работы этой программы:
(3, 2)
-1
(-1, -3)
(10, 30)
31.622776601683793
(-3, -2)
Как видно из примера, для переопределения оператора используется
статический метод со специальным именем, у которого число параметров
совпадает с числом параметров оператора. Одним из параметров обязан
быть тот тип, для которого переопределяется оператор. При переопределении арифметической операции автоматически переопределяется соответствующий ей оператор присваивания.
Обратим внимание, что в примере использовано ключевое слово
readonly. Оно сообщает компилятору, что структура никогда не будет модифицирована после создания, что позволяет ему выполнять дополнительные оптимизации. Это ключевое слово может использоваться также у полей, причем если структура объявлена readonly, то все ее поля должны
быть объявлены также readonly.
Кроме того, из примера видно, что у структур, в отличие от классов,
публичный конструктор без параметров существует всегда, вне зависимости от наличия других конструкторов, при этом его даже нельзя объявить
как приватный.
5.14. Сборка мусора и методы завершения
В C#, в отличие от С++, отсутствует оператор delete. Освобождение
памяти и удаление экземпляров производится специальным сборщиком
мусора, который должен выяснять, есть ли ссылки на существующий экземпляр, и имеет право удалить его после потери последней ссылки.
При этом потерянной считается не только ссылка, которая не хранится ни
в одной переменной, но и ссылка, до которой невозможно добраться из
выполняемой программы (например, если класс хранит ссылку на самого
себя, то это не гарантирует его вечное существование).
46

В каждом классе можно описать метод завершения (финализатор,
finalizer). Синтаксически метод завершения выглядит так:
~<имя класса>(){<тело метода>}. Объявление синтаксически напоминает деструктор в С++, и смысл его тоже похож: метод завершения должен вызываться при удалении экземпляра класса. Однако в C# вызова метода завершения может не произойти. Это связано со спецификой работы
сборщика мусора. Да, по стандарту языка он обязан вызвать метод завершения класса перед удалением экземпляра класса. Однако тот же стандарт
языка говорит, что сборщик мусора может удалять экземпляры классов,
на которые потеряны все ссылки, но не обязан это делать. При этом сборщик мусора может вообще не удалять экземпляры классов, если программа завершается, поскольку имеет право считать, что все выделенные
программе ресурсы памяти при завершении освободятся операционной
системой автоматически.
Поэтому программист не должен рассчитывать на то, что метод завершения класса обязательно когда-то вызовется, и для освобождения занятых ресурсов должен использовать другие способы, например интерфейс
IDisposable, который мы рассмотрим в разделе 10.2 . Методы завершения
должны использоваться только как часть реализации паттерна Dispose
(см.: https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/
implementing-dispose#implement-the-dispose-pattern).

6. ОДНОМЕРНЫЕ И МНОГОМЕРНЫЕ МАССИВЫ
Одномерные массивы существуют практически во всех языках программирования, это одна из базовых структур данных. Под таким массивом обычно понимают непрерывный участок памяти, хранящий несколько
переменных одного типа. Описание переменной типа одномерного массива в C# имеет вид
<тип> [] <переменная>
где <тип> – это тип хранимых объектов. Храниться в массиве могут как
ссылочные типы, так и типы-значения. Естественно, для ссылочных типов
в массиве лежат не сами объекты, а ссылки на них.
В C# массивы могут быть и многомерными. Например, описание трехмерного массива имеет вид
<тип> [,,] <переменная>
При описании переменной типа «массив» (в том числе и параметра
метода) размерности массива не указываются, но при создании экземпляра
массива все размерности должны быть указаны, например:
var x = new int[100, 20];
При создании экземпляра массива все элементы массива инициализируются значениями по умолчанию, при этом ссылочные типы получают
значение null даже в том случае, если включено nullable и тип элементов
массива не допускает значение null. Например, программа
#nullable enable
C[] c = new C[10];
C с0 = c[0];
Console.WriteLine(c0.x);
class C
{
public int x = 10;
}
компилируется без ошибок и предупреждений, но при выполнении выбрасывает исключение NullReferenceException при обращении к c0.x.
Обращение к элементам массива происходит через указание индексов
(нумерация с нуля) в квадратных скобках, например x[i, j].
48

Обратим внимание на то, что в языке C# при обращении к элементам
массива всегда контролируется выход за границу размерностей. Однако
это не означает, что работа с массивами происходит существенно медленнее, чем в других языках. Дело в том, что компиляторы C# достаточно эффективны, и, например, при работе с одномерным массивом в цикле проверяют выход за границы массива только для начального и конечного значения переменной цикла (если, конечно, индексное выражение это позволяет). К сожалению, работа с многомерными массивами не столь эффективна, и часто для повышения скорости работы программ нужно использовать массив массивов или ручное вычисление индекса в одномерном
массиве. Заметим, что все размерности массива (и индексы) имеют тип int,
поэтому массивы не могут иметь размерность больше 2
31
– 1.
В последних версиях языка появились дополнительные возможности
индексации массивов. Например, можно индексировать массив с конца.
Выражение x[^1] означает последний элемент массива, x[^2] – предпослед-
ний и т. д. Возможно создавать массивы как подмассивы существующего
(создается копия части массива). Например, x[2..] – массив копируется со
второго элемента до конца, x[5..8] – массив копируется с пятого элемента
до восьмого, пятый включается, восьмой не включается.
В C#, в отличие от многих других языков, массив является полноценным ссылочным типом, унаследованным (см. раздел 9) от абстрактного базового класса Array. Поэтому у массива кроме индексации есть много дополнительных полезных возможностей.
Свойство int Length возвращает общее число элементов массива по
всем измерениям. Это свойство может вызвать целое переполнение, если
суммарное число элементов массива превышает int.MaxValue (такое возможно, если массив неодномерный). Существует также свойство long
LongLength, которое не может вызвать переполнение.
Свойство int Rank возвращает число измерений массива. Для одномер-
ного массива возвращает единицу. Для получения значения элементов
многомерного массива (кроме индексации) можно использовать различные модификации метода GetValue.
У класса Array есть также много статических членов, среди которых
функции сортировки, бинарного поиска в отсортированном массиве, копирования, инициализации и т. д.
Массив можно инициализировать при создании, например:
int[] a = { 1, 2, 3, 4 };
int[,] b = { { 1, 2 }, { 3, 4} };
49

7. РАБОТА СО СТРОКАМИ
7.1. Редактирование строк. StringBuilder
Основная проблема при работе со строками в C#, которая появляется
у программистов, ранее писавших программы на С++, – это необходимость осознать, что в C# строка не является массивом символов с нулевым
символом в конце. В C# string – отдельный тип, у которого, правда, есть
индексатор, возвращающий символ по порядковому номеру, но этот индексатор не позволяет присвоить символу другое значение. Тип string –
неизменяемый! Любые функции, которые есть у этого типа, не модифицируют строку. Например, функции замены подстроки, обрезки конца всегда
создают новую строку, а не возвращают часть существующей. Если необходимо сконструировать строку каким-то сложным способом, то для того
чтобы избежать лишних копирований строк, следует пользоваться специальным классом StringBuilder, который фактически является изменяемым
массивом символов с дополнительными удобными функциями. Например,
программа
using System.Text;
int[] a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
string ForPrint=a[0].ToString();
for(int i = 1; i < a.Length; i++)
ForPrint +="; " + a[i].ToString();
ForPrint += ";";
Console.WriteLine(ForPrint);
var ForPrint2 = new StringBuilder();
ForPrint2.Append(a[0].ToString());
for (int i = 1; i < a.Length; i++)
ForPrint2.Append("; " + a[i].ToString());
ForPrint2.Append(";");
Console.WriteLine(ForPrint2.ToString());
var ForPrint3 = new StringBuilder();
ForPrint3.AppendJoin("; ", a);
ForPrint3.Append(";");
Console.WriteLine(ForPrint3.ToString());
напечатает три раза одинаковую строку (при заданном массиве a она приведена ниже).
1; 2; 3; 4; 5; 6; 7; 8; 9; 0;
50
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
