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

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

.pdf
Скачиваний:
1
Добавлен:
07.09.2026
Размер:
2 Мб
Скачать
☆
мы даже не упомянем в этом пособии). Например, можно вместо конструк- ции !( a is double) написать a is not double. Кроме того, что гораздо нужнее, можно определить новую переменную с явно заданным типом, куда ско­пируется значение исходной. Например, оператор
var b = a is double с;
не только присваивает значение переменной b, но и описывает перемен­ную c типа double, которая принимает значение переменной a, если выра­жение истинно (при ложности выражения значение переменной c является неопределенным). До введения такого синтаксиса приходилось то же са­мое записывать через явное преобразование типа, например, так:
var b = a is double; double c; if (b) c = (double)a;
Заметим, что существует еще оператор as явного преобразования типа, который позволяет для типа T написать
var c = a as T;
В этом случае если переменная a реально не содержит значение типа T, переменная с получит значение null. Если же в приведенном выше фраг­менте с проверкой убрать проверку и написать
var c = (double) a;
то при невозможности приведения типа происходит исключение.
3.8. Оператор switch
Оператор предназначен для выбора одного условия из многих. Начи­ная с 9.0 версии языка, он имеет вид
21
switch(<выражение>)
{
case <шаблон соответствия >:<один или несколько операторов><оператор
завершения case> …
default: <один или несколько операторов><оператор завершения case>
};
Сase-блоков может быть несколько, но не менее одного, default-блок мо­жет отсутствовать. Если по нескольким шаблонам соответствия требуется выполнить одинаковые действия, то такие шаблоны соответствия могут просто следовать друг за другом, как в примере ниже. Оператор switch вы­бирает первый шаблон соответствия, подходящий для заданного выраже­ния, и выполняет операторы, соответствующего шаблону case-блока. Последним оператором блока, оператором завершения case, обязательно должен быть оператор передачи управления, это либо оператор break (выйти из оператора switch), либо оператор goto case <шаблон соответ­ствия>, который позволяет выполнить операторы другого блока, либо лю­бой другой оператор, передающий управление вовне оператора switch (например, оператор return выхода из функции). Если ни один шаблон со­ответствия не выполнился для выражения, то выполняется default-блок.
Шаблоны соответствия могут быть следующих видов.
1. Константа или выражение со значением, вычисляемым на этапе компи-
ляции. Шаблон выполняется при равенстве значения проверяемого вы­ражения этой константе. Например, «case 12:».
2. Реляционный шаблон для сравнения результата выражения с констан-
той. В реляционном шаблоне можно использовать операции ‘<’, ‘>’, ‘<=’, ‘>=’. Например, «case <12:».
3. Логический шаблон для комбинации других шаблонов. Может исполь-
зовать ключевые слова and, or, not для булевых операций конъюнкции, дизъюнкции и отрицания. Например, «case <12 or 15:».
4. Шаблон соответствия типу. Полностью эквивалентен оператору is, но
без ключевого слова is, например, «case int:» или «case int c:». Объяв­ленную переменную можно использовать сразу после объявления в этом же блоке.
5. Шаблон с охранными условиями. К любой разновидности шаблона,
если он не однозначно определяет соответствие проверяемому выраже­нию, можно добавить любое дополнительное логическое условие с
22
обычным синтаксисом через ключевое слово when, например, «case int c when IsPrime(c):».
Обратим внимание, что оператор switch всегда не менее эффективен, чем серия проверок оператором if, а при константных шаблонах выполня­ется существенно быстрее.
Например:
switch (a)
{ case 1: case 10: Console.WriteLine(1); break; case < 11: Console.WriteLine(2); break;
case double and (< 10.0 or > 100):
Console.WriteLine(3); break; default: Console.WriteLine(4); break; }
В тех ситуациях, когда в зависимости от наборов условий нужно по­лучить всего одно значение, а не набор действий, можно использовать вме­сто оператора switch выражение switch с чуть более простым синтаксисом. Выражение switch имеет вид: <проверяемое выражение> switch {<шаблон
соответствия> => <результат>, <шаблон соответствия> => <резуль­тат>, _ => <результат>}, где <шаблон соответствия> может быть та-
ким же, как в операторе switch (но не должно быть ключевого слова case, а вместо двоеточия – ‘=>’), результат – выражение со значением одинако­вого типа для всех шаблонов соответствия (этот тип и будет типом выра­жения switch), символ подчеркивания играет роль блока default (и, в отли­чие от оператора switch, он должен быть обязательно последним). Напри­мер:
var s = a switch { < 11 => 10, > 12 and < 15 => 15, > 15 when a <= 100 => 17, _ => 0, };
23
Заметим, что компилятор и для оператора, и для выражения switch проверяет, что все возможные значения проверяемого выражения по­крыты, и, если это не так, выдает предупреждение при отсутствии блока default для оператора и ошибку для выражения.
3.9. Оператор безусловного перехода
Оператор перехода похож на аналогичные операторы других языков, имеет вид goto <метка>, где метка – идентификатор, которым помечен какой-либо оператор, находящийся в том же блоке операторов или в одном из охватывающих блоков внутри того же метода. Метка отделяется от опе­ратора символом ‘:’.
Оператор вызывает переход управления на помеченный оператор, при этом все покидаемые блоки корректно заканчиваются (т. е. если при вы­ходе из блока должны были выполниться какие-то специальные назначен­ные действия, например закрытие файлов при выходе из оператора using, который мы рассмотрим в разделе 10.2, то эти действия выполнятся до по­меченного оператора).
Заметим, что классики структурного программирования вообще не ре­комендуют использовать этот оператор, но, на наш взгляд, выйти, напри­мер, из вложенных циклов оператором перехода вполне корректно.
3.10. Исключения
В каком-то смысле еще одним способом передачи управления явля­ется механизм исключений. Механизм предназначен для обработки ис­ключительных ситуаций. Он состоит из нескольких операторов. Оператор throw инициирует исключение. Этот оператор имеет вид
throw <exception>;
где <exception> – выражение, приводимое к типу Exception. Оператор throw немедленно прерывает выполнение программы и начинает сворачи-
вать стек вызовов до тех пор, пока не встретит ближайший оператор try-catch-finally.
24
Оператор try-catch-finally имеет вид
try { <блок операторов с возможным вызовом подпрограмм, в которых может быть throw> } <несколько (возможно, ни одного) блоков catch> <необязательный (если есть хоть один catch) блок finally>
Блок catch имеет вид
catch(<тип> <переменная>) when <условие> { <блок операторов> }
Если возникает исключение внутри блока try (при выполнении опера­тора throw), то управление передается в блок операторов первого из бло­ков catch, к типу которого можно привести тип экземпляра, использован­ного в throw, и для которого выполняется условие when. Если это необхо­димо, в блоке операторов catch можно использовать оператор throw без параметров, который означает немедленное завершение текущей обра­ботки исключения и продолжение обработки выше по стеку вызовов.
Условие when может отсутствовать вместе с ключевым словом when. Переменная в catch также может отсутствовать, если она не используется в блоке операторов. Допустимо даже catch{ }, если нужно перехватить лю- бое исключение.
Блок finally выполняется в любом случае, независимо от того, проис­ходило исключение в блоке try или нет. Обычно в блоке finally програм­мист освобождает ресурсы, выделенные в блоке try.
Заметим, что throw может быть не только оператором, но и выраже­нием, что позволяет использовать его, например, так:
var c = a > 0 ? a : throw new Exception("отрицательное значение");
4. МЕТОДЫ (ПОДПРОГРАММЫ)
Идея разбивать программы на подпрограммы присутствует почти во всех языках программирования. В C# подпрограммы больше принято называть методами, но суть от этого не меняется: при вызове метода ему передается управление, по окончании выполнения метода управление обычно возвращается в точку вызова (если не произошло исключение). Метод может иметь параметры, которые подставляются при вызове, и мо­жет возвращать значение, как функция.
Описание метода (без модификаторов, которые будут описаны чуть позже) имеет вид: <тип возвращаемого значения> <идентифика- тор> (<список параметров метода>) {<тело метода>}.
Если тело метода состоит из одного оператора или возвращает значе­ние выражения, то может использоваться сокращенная форма: <тип воз-
вращаемого значения> <идентификатор> (<список параметров ме­тода>) => <оператор или выражение>.
Обратим внимание, что тело метода действительно почти всегда при­сутствует в описании метода (за редкими исключениями, когда, например, описываются интерфейсы или абстрактные методы, о которых читатель узнает немного позже). Дело в том, что в C# метод необязательно должен быть описан до его использования, как во многих других языках, компи­лятор «видит» все методы, описанные в любом файле проекта, одновре­менно.
Если метод не планируется никогда вызывать как функцию в выраже­ниях и он не должен возвращать значение, то тип возвращаемого значения должен описываться ключевым словом void.
Список параметров метода – разделенный запятыми список иденти­фикаторов с указанием их типов, с возможным указанием значений по умолчанию и способов передачи.
Обратим внимание, что язык допускает объявление методов с одина­ковым именем и разным количеством или типами параметров. В этом слу­чае при вызове метода компилятор выберет тот, которому список указан­ных при вызове параметров подходит больше всего. При этом методы не могут отличаться только типом возвращаемого значения.
4.1. Ключевые слова out и ref
В отличие от С++, в котором параметры всегда передаются по значе­нию (по значению того типа, который указан в описании параметра; если
26
параметр имеет тип «ссылка», то по значению ссылки), в C# есть дополни­тельные механизмы принудительной передачи параметра в метод по ссылке, эти механизмы включаются специальными ключевыми словами ref и out. Причем эти ключевые слова требуется указать не только при опи- сании параметров метода, но и при его вызове, что позволяет программи­сту не перепутать способ передачи параметров (в С++ при передаче пара­метров-ссылок то, что передается ссылка, никак не указывается при вы­зове, что затрудняет программисту понимание того, что значение переда­ваемого параметра может измениться в подпрограмме). Например, метод
void Mult2(int a, out int b){ b = a * 2;}
удваивает значение параметра a и возвращает результат через параметр b, передаваемый по ссылке. Вызов такого метода может иметь вид
Mult2(x, out y);
если переменные x и у были описаны ранее как int. В принципе, возвраща­емый через ссылку out параметр можно описать и непосредственно при вызове, например, так:
Mult2(x, out int y);
или даже так:
Mult2(x, out var y);
Обратим внимание, что компилятор отслеживает присваивание всем параметрам out перед выходом из метода, и если есть пути, при которых значение не будет установлено, сообщает об ошибке.
Ключевое слово ref отличается от out тем, что передаваемый параметр должен быть инициализирован до вызова метода, и внутри метода к нему можно обращаться для получения значения (при попытке получить значе­ния параметра out внутри метода компилятор выдаст сообщение об ошибке). При этом метод может, но не обязан присваивать значение пара­метру ref.
4.2. Ключевое слово in
Есть еще один достаточно редко используемый способ передачи па­раметров по ссылке, с использованием ключевого слова in. Этот способ
27
используется, если программист хочет указать компилятору, что время ко­пирования параметра при передаче по значению слишком велико и сле­дует передать объект по ссылке, при этом гарантируется, что содержимое передаваемого объекта внутри метода изменено не будет. При этом при вызове метода ключевое слово можно не указывать, но тогда если факти­чески передаваемый параметр не будет иметь строго тот тип, который ука­зан в описании метода, то произойдет неявное преобразование типа (если оно возможно) и по ссылке будет передана копия объекта. Если же при вызове ключевое слово in указано, то компилятор при попытке передачи не точно соответствующего типа выдаст сообщение об ошибке.
4.3. Параметры со значениями по умолчанию
У любого входного параметра (т. е. не помеченного ref или out) может быть задано значение по умолчанию. Значение такого параметра можно не задавать при вызове метода. Например, если метод имеет вид
int Sum(int a, int b = 10) => a + b;
то обратиться к нему можно как Sum (10), так и Sum (5, 7).
Значения по умолчанию могут иметь только последние параметры ме­тода, т. е. если у какого-то параметра задано значение по умолчанию, то у всех последующих параметров тоже должны быть заданы значения по умолчанию.
4.4. Позиционные и ключевые параметры
При вызове метода параметры можно задавать в той последователь­ности, в которой они перечислены при описании метода. Но есть возмож­ность менять порядок задания параметров или не задавать какие-то пара­метры, если у них есть значения по умолчанию. Для этого указывается имя параметра и через двоеточие – его значение. Например, Sum (b:7, a:5). Можно смешивать позиционное и ключевое задание параметров, т. е. не­сколько первых параметров задать позиционно, а остальные – по ключу.
28
4.5. Переменное число параметров
В С++ при описании функции можно указать переменное число пара­метров символом «многоточие» и потом «добывать» их внутри функции достаточно сложным механизмом. C# – чуть более строго типизированный и более удобный язык, поэтому переменное число параметров организу­ется следующим образом. Последним в списке параметров метода указы­вается ключевое слово params, и далее описывается одномерный массив произвольного типа (подробнее организацию массивов в языке мы пояс­ним в разделе 6). В этом случае при обращении к методу можно указать либо массив того же типа, либо перечислить произвольное число парамет­ров (даже ноль), конвертируемых неявно к этому типу. Внутри метода число реально переданных параметров определяется длиной массива (в C# можно узнать длину массива через его свойство Length). Например, для вычисления минимума из произвольного количества целых чисел можно написать метод:
int Min(params int[] array)
{ int min = int.MaxValue; for (int i = 0; i < array.Length; i++) { if (array[i] < min) min = array[i]; } return min; }
Обратиться к этому методу можно, например, так:
var c = Min(5, 4, -1);
в этом случае с примет значение «–1». Заметим, что константа
int.MaxValue имеет значение максимально представимого числа типа int, а
что она означает синтаксически, станет ясно чуть позже, после раздела 5.5.
29
4.6. Оператор return
Оператор имеет вид return <выражение>, где выражение должно быть преобразуемым к типу возвращаемого значения метода. Если это тип void, то выражения быть не должно. Оператор фактически является разно­видностью оператора безусловного перехода, осуществляющего переход на конец метода. Если метод не возвращает значений, то оператор return не является обязательным. Если метод возвращает значение, то компиля­тор проверяет, что при всех путях выполнения метода (если в методе не произошло исключений) это значение будет сформировано оператором re- turn.
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]