Опубликован: 02.12.2009 | Уровень: для всех | Доступ: свободно | ВУЗ: Тверской государственный университет
Лекция 4:

Операторы языка C#

Аннотация: Рассмотрен весь набор операторов языка С#. Обсуждаются их достоинства и недостатки. Операторы присваивания выбора и циклов составляют основу процесса алгоритмизации. Рассмотрены специальные операторы языка try - catch – finally, позволяющие организовать обработку исключительных ситуаций. Рассмотрен оператор yield, используемый в итераторах. Обсуждается тема математической бесконечности и конечности вычислений. Рассматриваются классические алгоритмы и даются задачи на эту тему.

Проект к данной лекции Вы можете скачать здесь.

Состав операторов языка C#, их синтаксис и семантика унаследованы от языка С++. Как и положено, потомок частично дополнил состав, переопределил синтаксис и семантику отдельных операторов, постарался улучшить характеристики языка во благо программиста. Посмотрим, насколько это удалось языку C#.

Оператор присваивания

В "Выражения и операции" подробно рассматривались операция и выражение присваивания

X = expr

и многочисленные вариации, позволяющие строить выражения вида:

X1 += X2 *= … = Xk = expr

Синтаксически присваивание состоит из левой и правой частей, разделенных знаком операции присваивания. Правая часть - это выражение, в том числе выражение присваивания, как в последнем примере. Левая часть - это переменная; более точно: левая часть представляет собой lvalue - выражение левой части, которому можно присвоить значение. Переменная является наиболее распространенным частным случаем lvalue.

Выражение присваивания представляет собой пример выражения с побочным эффектом. Прямым эффектом вычисления такого выражения является вычисленное значение и тип выражения expr. Побочным эффектом является присваивание вычисленного значения переменной левой части.

Выражение с побочным эффектом в языке C# можно легко преобразовать в соответствующий оператор. Стоит такое выражение закончить символом точка с запятой, как получится оператор, который можно использовать всюду, где синтаксически допустимы операторы языка. Так что синтаксически оператор присваивания выглядит так:

X = expr;

Допустимы и многочисленные вариации:

X1 += X2 *= … = Xk = expr;

К операторам присваивания можно отнести и такие операторы, как:

X++;	X--;	++X;	--X;

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

Семантика присваивания

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

Будем называть целью левую часть оператора присваивания, а источником - правую часть оператора присваивания. Источник и цель могут быть как значимого, так и ссылочного типа. Присваивание будем называть ссылочным, если цель ссылочного типа. В этом случае источник должен быть ссылочного типа или быть приведенным к этому типу. Присваивание будем называть значимым, если цель значимого типа. В этом случае источник должен быть значимого типа или быть приведенным к этому типу.

Операции "упаковать" и "распаковать" - boxing и unboxing

Возникает естественный вопрос: можно ли ссылочным переменным, связанным с объектами, хранимыми в куче, присваивать значимые переменные, хранимые в стеке? Можно ли выполнять обратную операцию? В C# такие возможности преобразования типов предусмотрены. Операция "упаковать" ( boxing ) позволяет переменную значимого типа "упаковать в одежды класса", создавая объект в динамической памяти. Такое преобразование выполняется автоматически всякий раз, когда цель принадлежит классу object, а источником может быть переменная любого из значимых типов. Операция "распаковать" ( unboxing ) позволяет переменную типа object "распаковать и извлечь хранимое значение". Такое преобразование выполняется автоматически. Извлеченное значение не сохраняет информацию о своем типе. Поэтому, прежде чем присвоить это значение цели, его необходимо привести к нужному типу. Ответственность за это приведение лежит на программисте.

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

Цель и источник значимого типа. Здесь речь идет о семантике значимого присваивания. В этом случае источник и цель имеют собственную память для хранения значений. Если типы цели и источника совпадают, то никаких проблем нет. Значения источника копируются и заменяют значения соответствующих полей цели. Источник и цель после этого продолжают жить независимо. У них своя память, хранящая после присваивания одинаковые значения. Если типы разные, то необходимо преобразование типов. Оно может быть безопасным и тогда выполняется автоматически. В противном случае оно должно явно задаваться программистом. Явные и неявные преобразования внутри арифметического типа, кастинг, метод Parse и методы класса Convert подробно рассматривались в "Типы и классы. Переменные и объекты" .

Цель и источник ссылочного типа. Здесь имеет место семантика ссылочного присваивания - присваивание ссылок. В этом случае значениями источника и цели являются ссылки на объекты, хранящиеся в динамической памяти ("куче"). Если типы источника и цели совпадают, то никаких проблем нет. Цель разрывает связь с тем объектом, на который она ссылалась до присваивания, и становится ссылкой на объект, связанный с источником. Результат ссылочного присваивания двоякий. Объект, на который ссылалась цель, теряет одну из своих ссылок и может стать "висячим" - бесполезным объектом, на который никто не ссылается, так что его дальнейшую судьбу определит сборщик мусора.

После присваивания с объектом в памяти, на который ссылался источник, теперь связываются, по меньшей мере, две ссылки, рассматриваемые как различные имена одного объекта. Ссылочное присваивание приводит к созданию псевдонимов - к появлению разных имен у одного объекта. Особо следует учитывать ситуацию, когда цель и/или источник имеет значение null - нулевой ссылки, не указывающей ни на какой объект. Если такое значение имеет источник, то в результате присваивания цель получает это значение и более не ссылается ни на какой объект. Если же цель имела значение null, а источник - нет, то в результате присваивания ранее "висячая" цель становится ссылкой на объект, связанный с источником.

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

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

Цель ссылочного типа, источник значимого типа. В этом случае "на лету" значимый тип преобразуется в ссылочный. Как обеспечивается двойственность существования значимого и ссылочного типа - переменной и объекта? Ответ прост: за счет эффективно реализованной операции "упаковать" ( boxing ), выполняемой автоматически.

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

Цель значимого типа, источник ссылочного типа. В этом случае "на лету" ссылочный тип преобразуется в значимый. Операция "распаковать" ( unboxing ) выполняет обратную операцию - она "сдирает" объектную упаковку и извлекает хранимое значение. Заметьте, операция "распаковать" не является обратной к операции "упаковать" в строгом смысле этого слова. Оператор object obj = x корректен, но выполняемый следом оператор x = obj приведет к ошибке. Недостаточно, чтобы хранимое значение в упакованном объекте точно совпадало по типу с переменной, которой присваивается объект. Необходимо явно заданное преобразование к нужному типу.

Блок, или составной оператор

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

{
	оператор_1
	…
	оператор_N
}
В языках программирования нет общепринятой нормы для использования символа точки с запятой при записи последовательности операторов. Есть три различных подхода и их вариации. Категорические противники точек с запятой считают, что каждый оператор должен записываться на отдельной строке (для длинных операторов определяются правила переноса). В этом случае точки с запятой (или другие аналогичные разделители) не нужны. Горячие поклонники точек с запятой (к ним относятся авторы языков С++ и C#) считают, что точкой с запятой должен оканчиваться каждый оператор. В результате в операторе if перед else появляется точка с запятой. Третьи полагают, что точка с запятой не принадлежит оператору, а играет роль разделителя операторов. В выше приведенной записи блока, следуя синтаксису C#, каждый из операторов заканчивается символом точка с запятой. Но, заметьте, блок не заканчивается этим символом!

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

Пустой оператор

Пустой оператор - это "пусто", завершаемое точкой с запятой. Иногда полезно рассматривать отсутствие операторов как существующий пустой оператор. Вот пример:

if (a > b) ;
            else
            {
                int temp = a; a = b; b = temp;
            }

Это корректно работающий пример. А вот типичная для новичков ошибка:

for(int i = 0; i < n; i++);
{
	…
}

Здесь телом цикла является пустой оператор.

Операторы выбора

Как в С++ и других языках программирования, в языке C# для выбора одной из нескольких возможностей используются две конструкции - if и switch. Первую из них обычно называют альтернативным выбором, вторую - разбором случаев.

Оператор if

Начнем с синтаксиса оператора if:

if(выражение_1) оператор_1
else if(выражение_2) оператор_2
…
else if(выражение_K) оператор_K
else оператор_N

Какие особенности синтаксиса следует отметить? Логические выражения if заключаются в круглые скобки и имеют значения true или false. Каждый из операторов может быть блоком, в частности, if -оператором. Поэтому возможна и такая конструкция:

if(выражение1) if(выражение2) if(выражение3) …

Ветви else if, позволяющие организовать выбор из многих возможностей, могут отсутствовать. Может быть опущена и заключительная else -ветвь. В этом случае краткая форма оператора if задает альтернативный выбор - делать или не делать - выполнять или не выполнять then -оператор.

Семантика оператора if проста и понятна. Выражения if проверяются в порядке их написания. Как только получено значение true, проверка прекращается и выполняется оператор (это может быть блок), который следует за выражением, получившим значение true. С завершением этого оператора завершается и оператор if. Ветвь else, если она есть, относится к ближайшему открытому if.

Оператор switch

Частным, но важным случаем выбора из нескольких вариантов является ситуация, при которой выбор варианта определяется значениями некоторого выражения. Соответствующий оператор C#, унаследованный от C++, но с небольшими изменениями в синтаксисе, называется оператором switch. Вот его синтаксис:

switch(выражение)
{
	case константное_выражение_1: [операторы_1 оператор_перехода_1]
	…
	case константное_выражение_K: [операторы_K оператор_перехода_K]
	[default: операторы_N оператор_перехода_N]
}

Ветвь default может отсутствовать. Заметьте: по синтаксису допустимо, чтобы после двоеточия следовала пустая последовательность операторов, а не последовательность, заканчивающаяся оператором перехода. Константные выражения в case должны иметь тот же тип, что и switch -выражение.

Семантика оператора switch чуть запутана. Вначале вычисляется значение switch -выражения. Затем оно поочередно в порядке следования case сравнивается на совпадение с константными выражениями. Как только достигнуто совпадение, выполняется соответствующая последовательность операторов case -ветви. Поскольку последний оператор этой последовательности является оператором перехода (чаще всего это оператор break ), обычно он завершает выполнение оператора switch. Использование операторов перехода - это плохая идея. Таким оператором может быть оператор goto, передающий управление другой case -ветви, которая, в свою очередь, может передать управление еще куда-нибудь, получая блюдо "спагетти" вместо хорошо структурированной последовательности операторов. Семантика осложняется еще и тем, что case -ветвь может быть пустой последовательностью операторов. Тогда в случае совпадения константного выражения этой ветви со значением switch -выражения будет выполняться первая непустая последовательность очередной case -ветви. Если значение switch -выражения не совпадает ни с одним константным выражением, то выполняется последовательность операторов ветви default, если же таковой ветви нет, то оператор switch эквивалентен пустому оператору.

Полагаю, что оператор switch - это самый неудачный оператор языка C# как с точки зрения синтаксиса, так и семантики. Неудачный синтаксис порождает запутанную семантику, являющуюся источником плохого стиля программирования. Понять, почему авторов постигла неудача, можно, оправдать - нет. Дело в том, что оператор унаследован от С++, где его семантика и синтаксис еще хуже. В языке C# синтаксически каждая case-ветвь должна заканчиваться оператором перехода (забудем на минуту о пустой последовательности), иначе возникнет ошибка периода компиляции. В языке С++ это правило не является синтаксически обязательным, хотя на практике применяется та же конструкция с конечным оператором break. При его отсутствии управление "проваливается" в следующую case-ветвь. Конечно, профессионал может с успехом использовать этот трюк, но в целом ни к чему хорошему это не приводит. Борясь с этим, в C# потребовали обязательного включения оператора перехода, завершающего ветвь. Гораздо лучше было бы, если бы последним оператором мог быть только оператор break, как следствие, его можно было бы не писать, и семантика стала бы прозрачной - при совпадении значений двух выражений выполняются операторы соответствующей case-ветви, при завершении которой завершается и оператор switch.

Еще одна неудача в синтаксической конструкции switch связана с существенным ограничением, накладываемым на case-выражения, которые могут быть только константным выражением. Уж если изменять оператор, то гораздо лучше было бы использовать синтаксис и семантику Visual Basic, где в case-выражениях допускается список, каждое из выражений которого может задавать диапазон значений.

Разбор случаев - это часто встречающаяся ситуация в самых разных задачах. Применяя оператор switch, помните о недостатках его синтаксиса, используйте его в правильном стиле. Заканчивайте каждую case-ветвь оператором break, но не применяйте goto.

Содержательный пример применения оператора switch подробно рассмотрен в "Типы и классы. Переменные и объекты" . Рассмотрим еще один показательный пример, в котором вычисляется арифметическое выражение с двумя аргументами.

/// <summary>
/// Разбор случаев с использованием списков выражений
/// </summary>
/// <param name="operation">операция над аргументами</param>
/// <param name="arg1">первый аргумент бинарной операции</param>
/// <param name="arg2">второй аргумент бинарной операции</param>
/// <param name="result">результат бинарной операции</param>
public void ExprResult(string operation, double arg1, double arg2,
    ref double result)
{
    switch (operation)
    {
        case "+":
        case "Plus":
        case "Плюс":
            result = arg1 + arg2;
            break;
        case "-":
        case "Minus":
        case "Минус":
            result = arg1 - arg2;
            break;
        case "*":
        case "Mult":
        case "Умножить":
            result = arg1 * arg2;
            break;
        case "/":
        case "Divide":
        case "Div":
        case "разделить":
        case "Делить":
            result = arg1 / arg2;
            break;
        default:
            result = 0;
            break;
    }            
}//ExprResult

Обратите внимание: знак операции над аргументами можно задавать разными способами, что демонстрирует возможность задания списка константных выражений в ветвях оператора switch.

Гулжанат Ергалиева
Гулжанат Ергалиева
Федор Антонов
Федор Антонов

Здравствуйте!

Записался на ваш курс, но не понимаю как произвести оплату.

Надо ли писать заявление и, если да, то куда отправлять?

как я получу диплом о профессиональной переподготовке?