Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Основы объектно ориентированного программирования на языке С# базовые сведения о языке. Учебное пособие

.pdf
Скачиваний:
1
Добавлен:
07.09.2026
Размер:
2 Мб
Скачать
☆
Присваивание выполняется только в том случае, если 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
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]