Справочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией
Скачать 5.05 Mb.
|
Операторы Глава 4. Операторы 81 C# предусмотрен широкий набор операторов, которые дают в руки программисту мощные рычаги управления при создании разнообразнейших выражений и их вычислении. В C# имеется четыре общих класса операторов: арифметические, поразрядные, логические и операторы отношений. Помимо них в этой главе рассматриваются оператор присвоения и оператор ? . В C# определены также операторы для обработки специальных ситуаций, но их мы рассмотрим после изучения средств, к которым они применяются. Арифметические операторы В C# определены следующие арифметические операторы. Оператор Действие + Сложение - Вычитание, унарный минус * Умножение / Деление % Деление по модулю -- Декремент ++ Инкремент Действие C#-операторов + , - , * и / совпадает с действием аналогичных операторов в любом другом языке программирования (да и в алгебре, если уж на то пошло). Их можно применять к данным любого встроенного числового типа. Хотя действия арифметических операторов хорошо известны всем читателям, существуют ситуации, которые наверняка потребуют специальных разъяснений. Прежде всего хочу напомнить, что после применения оператора деления ( / ) к целому числу остаток будет отброшен. Например, результат целочисленного деления 10/3 будет равен 3 Остаток от деления можно получить с помощью оператора деления по модулю ( % ). Этот оператор работает практически так же, как в других языках программирования: возвращает остаток от деления нацело. Например, 10 % 3 равно 1 . В C# оператор % можно применить как к целочисленным типам, так и типам с плавающей точкой. Например, 10,0 % 3,0 также равно 1 . (В языках C/C++ операции деления по модулю применимы только к целочисленным типам.) Использование оператора деления по модулю демонстрируется в следующей программе. // Демонстрация использования оператора %. using System; class ModDemo { public static void Main() { int iresult, irem; double dresult, drem; iresult =10/3; irem = 10 % 3; dresult = 10.0 / 3.0; drem = 10.0 % 3.0; В 82 Часть I. Язык C# Console.WriteLine( "Результат и остаток от деления 10/3: " + iresult + " " + irem); Console.WriteLine( "Результат и остаток от деления 10.0 / 3.0: " + dresult + " " + drem); } } Результат выполнения этой программы таков: Результат и остаток от деления 10 / 3: 3 1 Результат и остаток от деления 10.0 / 3.0: 3,33333333333333 1 Как видите, оператор % генерирует остаток, равный 1 , как при делении целочисленных значений, так и значений с плавающей точкой. Инкремент и декремент Операторы инкремента ( ++ ) и декремента ( -- ) увеличивают и уменьшают значение операнда на единицу, соответственно. Как будет показано ниже, эти операторы обладают специальными свойствами, которые делают их весьма интересными для рассмотрения. Итак, оператор инкремента выполняет сложение операнда с числом 1, а оператор декремента вычитает 1 из своего операнда. Это значит, что инструкция x = x + 1; аналогична такой инструкции: x++; Точно так же инструкция x = x - 1; аналогична такой инструкции: x--; Операторы инкремента и декремента могут стоять как перед своим операндом, так и после него. Например, инструкцию x = x + 1; можно переписать в виде префиксной формы ++x; // Префиксная форма оператора инкремента. или в виде постфиксной формы: x++; // Постфиксная форма оператора инкремента. В предыдущем примере не имело значения, в какой форме был применен оператор инкремента: префиксной или постфиксной. Но если оператор инкремента или декремента используется как часть большего выражения, то форма его применения имеет важное значение. Если такой оператор применен в префиксной форме, то C# сначала выполнит эту операцию, чтобы операнд получил новое значение, которое затем будет использовано остальной частью выражения. Если же оператор применен в постфиксной форме, то C# использует в выражении его старое значение, а затем выполнит операцию, в результате которой операнд обретет новое значение. Рассмотрим следующий фрагмент кода: x = 10; y = ++x; Глава 4. Операторы 83 В этом случае переменная y будет установлена равной 11. Но если в этом коде префиксную форму записи заменить постфиксной, переменная y будет установлена равной 10: x = 10; y = x++; В обоих случаях переменная x получит значение 11. Разница состоит лишь в том, в какой момент она станет равной 11 (до присвоения ее значения переменной y или после). Для программиста очень важно иметь возможность управлять временем выполнения операции инкремента или декремента. Рассмотрим следующую программу, которая генерирует ряд чисел: /* Демонстрация различия между префиксной и постфиксной формами оператора ++. */ using System; class PrePostDemo { public static void Main() { int x, y; int i; x = 1; Console.WriteLine( "Ряд, построенный с помощью инструкции y = x + x++;"); for(i = 0; i < 10; i++) { y = x + x++; // постфиксная форма оператора ++ Console.WriteLine(y + " "); } Console.WriteLine(); x = 1; Console.WriteLine( "Ряд, построенный с помощью инструкции y = x + ++x;"); for(i = 0; i < 10; i++) { y = x + ++x; // префиксная форма оператора ++ Console.WriteLine(y + " "); } Console.WriteLine(); } } Вот как выглядит результат выполнения этой программы: Ряд, построенный с помощью инструкции y = x + x++; 2 4 6 8 10 12 14 84 Часть I. Язык C# 16 18 20 Ряд, построенный с помощью инструкции y = x + ++x; 3 5 7 9 11 13 15 17 19 21 Как видно из результатов работы этой программы, инструкция y = x + x++; сначала суммирует значения x и x , после чего присваивает результат переменной y . Только затем она инкрементирует переменную x . Но инструкция y = x + ++x; выполняется по-другому. Сначала она получает (и запоминает) исходное значение переменной x , затем инкрементирует его, суммирует новое значение с исходным, а результат суммирования присваивает переменной y . Нетрудно заметить, что простая замена элемента x++ элементом ++x меняет числовой ряд, генерируемый программой, с четного на нечетный. И еще. Выражение x + ++x; на первый взгляд может показаться странным, но только не компилятору. Несмотря на стоящие рядом два оператора, компилятор позаботится о правильной последовательности их выполнения. Достаточно понимать, что в этом выражении значение переменной x суммируется с инкрементированным значением той же переменной x Операторы отношения и логические операторы Операторы отношений оценивают по “двубальной системе” (ИСТИНА/ЛОЖЬ) отношения между двумя значениями, а логические определяют различные способы сочетания истинных и ложных значений. Поскольку операторы отношений генерируют ИСТИНА/ЛОЖЬ - результаты, то они часто выполняются с логическими операторами. Поэтому мы и рассматриваем их в одном разделе. Итак, перечислим операторы отношений. Оператор Значение == Равно != Не равно > Больше < Меньше >= Больше или равно <= Меньше или равно Глава 4. Операторы 85 Приведем список логических операторов. Оператор Значение & И | ИЛИ ^ Исключающее ИЛИ && Сокращенное И || Сокращенное ИЛИ ! НЕ Результат выполнения операторов отношений и логических операторов имеет тип bool В C# на равенство или неравенство можно сравнивать (соответственно, с помощью операторов == и != ) все объекты. Но такие операторы сравнения, как < , > , <= или >= , можно применять только к типам, которые поддерживают отношения упорядочения. Это значит, что все операторы отношений можно применять ко всем числовым типам. Однако значения типа bool можно сравнивать только на равенство или неравенство, поскольку значения true и false не упорядочиваются. Например, в C# сравнение true > false не имеет смысла. Что касается логических операторов, то их операнды должны иметь тип bool , и результат логической операции всегда будет иметь тип bool . Логические операторы & , | , ^ и ! выполняют базовые логические операции И, ИЛИ, исключающее ИЛИ и НЕ в соответствии со следующей таблицей истинности. p q p & q p | q p ^ q !p false false false false false true true false false true true false false true false true true true true true true true false false Как видно из этой таблицы, операция “исключающее ИЛИ” сгенерирует результат ИСТИНА лишь в случае, если истинен только один из ее операндов. Рассмотрим программу, которая демонстрирует использование операторов отношений совместно с логическими операторами. // Демонстрация использования операторов отношений // и логических операторов. using System; class RelLogOps { public static void Main(){ int i, j; bool b1, b2; i = 10; j = 11; if(i < j) Console.WriteLine("i < j"); if(i <= j) Console.WriteLine("i <= j"); if(i != j) Console.WriteLine("i !- j"); if(i == j) Console.WriteLine("Это не будет выполнено."); 86 Часть I. Язык C# if(i >= j) Console.WriteLine("Это не будет выполнено."); if(i > j) Console.WriteLine("Это не будет выполнено."); b1 = true; b2 = false; if(b1 & b2) Console.WriteLine("Это не будет выполнено."); if(!(b1 & b2)) Console.WriteLine("! (b1 & b2) -- ИСТИНА"); if(b1 | b2) Console.WriteLine("b1 | b2 -- ИСТИНА"); if(b1 ^ b2) Console.WriteLine("b1 ^ b2 -- ИСТИНА"); } } Результат выполнения этой программы таков: i < j i <= j i != j !(b1 & b2) -- ИСТИНА b1 | b2 -- ИСТИНА b1 ^ b2 -- ИСТИНА Рассмотренные выше логические операторы предназначены для выполнения самых распространенных логических операций. Однако существует ряд других операций, которые определяются правилами формальной логики. Их также можно выполнить с помощью логических операторов C#. Так, C# поддерживает набор логических операторов, на базе которых можно построить любую другую логическую операцию, например операцию импликации. Импликация — это логическая операция, результат которой будет ложным только в случае, когда левый операнд имеет значение ИСТИНА, а правый — ЛОЖЬ. (Операция импликации отражает идею о том, что истина не может подразумевать ложь.) Вот как выглядит таблица истинности для оператора импликации: p q Результат импликации p и q true true true true false false false false true false true true Операцию импликации можно создать, используя комбинацию операторов ! и | !p | q Использование импликации демонстрируется в следующей программе: // Создание оператора импликации в языке C#. using System; class Implication { public static void Main() { bool p = false, q = false; int i, j; for(i =0; i < 2; i++) { for(j =0; j < 2; j++) { if(i==0) p = true; if(i==1) p = false; if(j==0) q = true; if(j==1) q = false; Глава 4. Операторы 87 Console.WriteLine( "p равно " + p + ", q равно " + q); if(!p | q) Console.WriteLine("Результат импликации " + p + " и " + q + " равен " + true); Console.WriteLine(); } } } } Результат выполнения этой программы выглядит так: p равно True, q равно True Результат импликации True и True равен True p равно True, q равно False p равно False, q равно True Результат импликации False и True равен True p равно False, q равно False Результат импликации False и False равен True Сокращенные логические операторы C# поддерживает специальные сокращенные (short-circuit) версии логических операторов И и ИЛИ, которые можно использовать для создания более эффективного кода. Вспомним, что, если в операции И один операнд имеет значение ЛОЖЬ, результат будет ложным независимо от того, какое значение имеет второй операнд. А если в операции ИЛИ один операнд имеет значение ИСТИНА, результат будет истинным независимо от того, какое значение имеет второй операнд. Таким образом, в этих двух случаях вычислять второй операнд не имеет смысла. Если не вычисляется один из операндов, тем самым экономится время и создается более эффективный код. Сокращенный оператор И обозначается символом && , а сокращенный оператор ИЛИ — символом || (их обычные версии обозначаются одинарными символами & и | , соответственно). Единственное различие между обычной и сокращенной версиями этих операторов состоит в том, что при использовании обычной операции всегда вычисляются оба операнда, в случае же сокращенной версии второй операнд вычисляется только при необходимости. Рассмотрим программу, в которой демонстрируется использование сокращенного оператора И. Программа определяет, является ли значение переменной d множителем числа n . Здесь используется операция деления по модулю. Если остаток от деления d / n равен нулю, значит, d — множитель числа n . Чтобы не допустить ошибки деления на нуль, используется сокращенная форма оператора И. // Демонстрация использования сокращенных операторов using System; class SCops { public static void Main() { int n, d; n = 10; d = 2; if(d != 0 && (n % d) == 0) 88 Часть I. Язык C# Console.WriteLine(d + " — множитель числа " + n); d = 0; // Теперь установим d равным нулю. // Поскольку d равно нулю, // второй операнд не вычисляется. if(d != 0 && (n % d) == 0) Console.WriteLine(d + " — множитель числа " + n); /* Теперь попробуем проделать то же самое без сокращенного оператора. Такая попытка приведет к ошибке (деление на нуль). */ if(d != 0 & (n % d) == 0) Console.WriteLine(d + " -- множитель числа " + n); } } Чтобы не допустить деления на нуль, в инструкции if сначала проверяется значение переменной d на равенство нулю. Если наши опасения окажутся ненапрасными, выполнение сокращенного оператора И на этом прекратится. В первой проверке, когда переменная d содержит число 2, операция деления по модулю выполняется. Вторая проверка (а ей предшествует принудительная установка переменной d нулем) показывает, что второй операнд вычислять не имеет смысла, поэтому деление на нуль опускается. Попытка заменить сокращенный оператор И обычным заставит вычислить оба оператора и, как следствие, приведет к ошибке деления на нуль. Поскольку сокращенные формы операторов И и ИЛИ в некоторых случаях работают эффективнее своих обычных “коллег”, читатель мог бы задать вполне резонный вопрос: “Почему бы компилятору C# вообще не отказаться от обычных форм этих операторов?”. Дело в том, что иногда необходимо, чтобы вычислялись оба операнда, поскольку вас могут интересовать побочные эффекты вычислений. Чтобы прояснить ситуацию, рассмотрим следующую программу: // Демонстрация важности побочных эффектов. using System; class SideEffects { public static void Main() { int i; i = 0; /* Здесь значение i инкрементируется, несмотря на то, что инструкция выполнена не будет. */ if(false & (++i < 100)) Console.WriteLine("Этот текст не будет выведен."); Console.WriteLine( "Инструкция if выполнена: " + i); // Отображает: 1 /* В этом случае значение i не инкрементируется, поскольку сокращенный оператор И опускает инкрементирование. */ if(false && (++i < 100)) Console.WriteLine("Этот текст не будет выведен."); Console.WriteLine( "Инструкция if выполнена: " + i); // По-прежнему 1 !! } } Глава 4. Операторы 89 Как поясняется в комментариях, в первой if -инструкции значение переменной i инкрементируется независимо от результата выполнения самой if -инструкции. Но при использовании сокращенной версии оператора И во второй if -инструкции значение переменной i не инкрементируется, поскольку первый операнд имеет значение false . Из этого примера вы должны извлечь следующий урок. Если в программе предполагается обязательное выполнение правого операнда операции И/ИЛИ, вы должны использовать полную, или обычную, форму этих операторов, а не сокращенную. И еще одна терминологическая деталь. Сокращенный оператор И также называется условным И, а сокращенный ИЛИ — условным ИЛИ. Оператор присваивания С оператором присваивания мы “шапочно” познакомились в главе 2. Теперь пришло время для более официального знакомства. Оператор присваивания обозначается одинарным знаком равенства ( = ). Его роль в языке C# во многом такая же, как и в других языках программирования. Общая форма записи оператора присваивания имеет следующий вид. переменная = выражение ; Здесь тип элемента переменная должен быть совместим с типом элемента выражение . Оператор присваивания интересен тем, что позволяет создавать целую цепочку присвоений. Рассмотрим, например, следующий фрагмент кода. int x, y, z; x = y = z = 100;//Устанавливаем переменные x, y // и z равными 100. В этом фрагменте значения переменных x , y и z устанавливаются равными 100 в одной инструкции. Эта инструкция успешно работает благодаря тому, что оператор присваивания генерирует значение правостороннего выражения. Это значит, что значение выражения z = 100 равно числу 100 , которое затем присваивается переменной y , после чего в свою очередь присваивается переменной x . Использование цепочки присвоений — простой способ установить группу переменных равными одному (общему для всех) значению. Составные операторы присваивания В C# предусмотрены специальные составные операторы присваивания, которые упрощают программирование определенных инструкций присваивания. Лучше всего начать с примера. Рассмотрим следующую инструкцию: x = x + 10; Используя составной оператор присваивания, ее можно переписать в таком виде: x += 10; Пара операторов += служит указанием компилятору присвоить переменной x сумму текущего значения переменной x и числа 10 . А вот еще один пример. Инструкция x = x - 100; аналогична такой: x -= 100; Обе эти инструкции присваивают переменной x ее прежнее значение, уменьшенное на 100 90 Часть I. Язык C# Составные версии операторов присваивания существуют для всех бинарных операторов (т.е. для всех операторов, которые работают с двумя операндами). Общая форма их записи такова: переменная op = выражение ; Здесь элемент op означает конкретный арифметический или логический оператор, объединяемый с оператором присваивания. Возможны следующие варианты объединения операторов. += -= *= /= %= &= |= ^= Поскольку составные операторы присваивания выглядят короче своих несоставных эквивалентов, то составные версии часто называют укороченными операторами присваивания. Составные операторы присваивания обладают двумя заметными достоинствами. Во- первых, они компактнее своих “длинных” эквивалентов. Во-вторых, их наличие приводит к созданию более эффективного кода (поскольку операнд в этом случае вычисляется только один раз). Поэтому в профессионально написанных Сопрограммах вы часто встретите именно составные операторы присваивания. Поразрядные операторы В C# предусмотрен набор поразрядных операторов, которые расширяют области приложения языка C#. Поразрядные операторы действуют непосредственно на разряды своих операндов. Они определены только для целочисленных операндов и не могут быть использованы для операндов типа bool , float или double Поразрядные операторы предназначены для тестирования, установки или сдвига битов (разрядов), из которых состоит целочисленное значение. Поразрядные операторы очень часто используются для решения широкого круга задач программирования системного уровня, например, при опросе информации о состоянии устройства или ее формировании. Поразрядные операторы перечислены в табл. 4.1. Таблица 4.1. Поразрядные операторы Оператор Значение & Поразрядное И | Поразрядное ИЛИ ^ Поразрядное исключающее ИЛИ >> Сдвиг вправо << Сдвиг влево Дополнение до 1 (унарный оператор НЕ) Поразрядные операторы И, ИЛИ, исключающее ИЛИ и НЕ Поразрядные операторы И, ИЛИ, исключающее ИЛИ и НЕ обозначаются символами & , | , ^ и , соответственно. Они выполняют те же операции, что и их логические эквиваленты, описанные выше. Различие состоит лишь в том, что поразрядные операции работают на побитовой основе. В следующей таблице показан результат выполнения каждой поразрядной операции для всех возможных сочетаний операндов (нулей и единиц). Глава 4. Операторы 91 p q p & q p | q p ^ q p 0 0 0 0 0 1 1 0 0 1 1 0 0 1 0 1 1 1 1 1 1 1 0 0 Поразрядный оператор И можно представить как способ подавления битовой информации. Это значит, что 0 в любом операнде обеспечит установку в 0 соответствующего бита результата. Вот пример: 1101 0011 1010 1010 & ________ 1000 0010 В следующей программе демонстрируется использование поразрядного оператора & для получения четных чисел из нечетных. Это реализуется посредством подавления (установки в нуль) младшего разряда числа. Например, число 9 в двоичном коде представляется числом 0000 1001. После обнуления младшего разряда получается число 8 (0000 1000 в двоичном коде). // использование поразрядного оператора И для // "превращения" любого числа в четное. using System; class MakeEven { public static void Main() { ushort num; ushort i; for(i = 1; i <= 10; i++) { num = i; Console.WriteLine("num: " + num); num = (ushort) (num & 0xFFFE); // num & 1111 1110 Console.WriteLine("num после сброса младшего бита: " + num + "\n"); } } } Результат выполнения этой программы имеет следующий вид: num: 1 num после сброса младшего бита: 0 num: 2 num после сброса младшего бита: 2 num: 3 num после сброса младшего бита: 2 num: 4 num после сброса младшего бита: 4 num: 5 92 Часть I. Язык C# num после сброса младшего бита: 4 num: 6 num после сброса младшего бита: 6 num: 7 num после сброса младшего бита: 6 num: 8 num после сброса младшего бита: 8 num: 9 num после сброса младшего бита: 8 num: 10 num после сброса младшего бита: 10 Значение 0xFFFE , используемое в этой программе, в двоичном коде представляется числом 1111 1111 1111 1110 . Таким образом, операция num & 0xFFFE оставляет все биты неизменными за исключением младшего, который устанавливается в нуль. Поэтому любое четное число, пройдя через это “чистилище”, остается четным, а любое нечетное “выходит” из него уже четным (за счет уменьшения на единицу). Оператор И также используется для определения значения разряда. Например, следующая программа определяет, является ли заданное число нечетным. // Использование поразрядного оператора И для // определения, является ли число нечетным. using System; class IsOdd { public static void Main() { ushort num; num = 10; if((num & 1) == 1) Console.WriteLine("Этот текст не будет отображен."); num = 11; if((num & 1) == 1) Console.WriteLine(num + " -- нечетное число."); } } Результат выполнения этой программы выглядит так: 11 -- нечетное число. В обеих инструкциях if выполняется операция И для значения переменной num и числа 1 . Если младший бит переменной num установлен (т.е. равен единице), результат операции num & 1 также будет равен единице. В противном случае результат будет равен нулю. Условие инструкции if выполнится только в случае, если анализируемое число окажется нечетным. Возможности поразрядного тестирования, которые предоставляет поразрядный оператор & , можно использовать для создания программы, которая отображает значение типа byte в двоичном формате. Рассмотрим один из возможных вариантов решения этой задачи. Глава 4. Операторы 93 // Отображение значений битов, составляющих байт. using System; class ShowBits { public static void Main() { int t; byte val; val = 123; for(t = 128; t > 0; t = t/2) { if((val & t) != 0) Console.Write("1 "); if((val & t) == 0) Console.Write("0 "); } } } Вот как выглядит результат выполнения этой программы: 01111011 В цикле for с помощью поразрядного оператора И последовательно тестируется каждый бит переменной val . Если оказывается, что этот бит установлен, отображается цифра 1 , в противном случае — цифра 0 Поразрядный оператор ИЛИ, в противоположность поразрядному И, удобно использовать для установки нужных битов в единицу. При выполнении операции ИЛИ наличие в операнде бита, равного 1 , означает, что в результате соответствующий бит также будет равен единице. Вот пример: 1101 0011 1010 1010 | ________ 1111 1011 С помощью поразрядного оператора ИЛИ рассмотренную выше программу получения четных чисел легко превратить в программу получения нечетных чисел. // Использование поразрядного оператора ИЛИ для // "превращения" любого числа в нечетное. using System; class MakeOdd { public static void Main() { ushort num; ushort i; for(i = 1; i <= 10; i++) { num = i; Console.WriteLine("num: " + num); num = (ushort) (num | 1); // num | 0000 0001 Console.WriteLine( "num после установки младшего бита: " + num + "\n"); } } } 94 Часть I. Язык C# Результат выполнения этого варианта программы таков: num: 1 num после установки младшего бита: 1 num: 2 num после установки младшего бита: 3 num: 3 num после установки младшего бита: 3 num: 4 num после установки младшего бита: 5 num: 5 num после установки младшего бита: 5 num: б num после установки младшего бита: 7 num: 7 num после установки младшего бита: 7 num: 8 num после установки младшего бита: 9 num: 9 num после установки младшего бита: 9 num: 10 num после установки младшего бита: 11 Работа этой программы основана на выполнении поразрядной операции ИЛИ между каждым числом, генерируемым в цикле for , и числом 1 , которое в двоичном коде представляется как 0000 0001 . Таким образом, 1 — это значение, у которого установлен только один младший разряд. Если это значение является одним из операндов операции ИЛИ, то результат выполнения этой операции совпадет со вторым операндом за исключением его младшего разряда, который станет равным единице (а все остальные при этом не изменятся). Следовательно, любое четное число, “пройдя” через операцию ИЛИ, увеличится на единицу, т.е. станет нечетным. Поразрядное исключающее ИЛИ (XOR) устанавливает в единицу бит результата только в том случае, если соответствующие биты операндов отличаются один от другого, т.е. не равны. Вот пример: 0111 1111 1011 1001 ^ ________ 1100 0110 Оператор XOR обладает одним интересным свойством, которое позволяет использовать его для кодирования сообщений. Если выполнить операцию XOR между значением X и значением Y , а затем снова выполнить операцию XOR между результатом первой операции и тем же значением Y , получим исходное значение X . Это значит, что после выполнения двух операций R1 = X ^ Y; R2 = R1 ^ Y; значение R2 совпадет со значением X . Таким образом, в результате выполнения двух последовательных операций XOR, использующих одно и то же значение ( Y ), получается исходное значение ( X ). Этот принцип можно использовать для создания простой Глава 4. Операторы 95 программы шифрования, в которой некоторое целочисленное значение — ключ — служит для кодирования и декодирования сообщения, состоящего из символов. Для шифрования сообщения операция исключающего ИЛИ применяется первый раз, а для его дешифровки — второй. Реализуем этот простой способ шифрования в следующей программе: // Использование оператора XOR для шифрования //и дешифрирования сообщения. using System; class Encode { public static void Main() { char ch1 = 'H'; char ch2 = 'i'; char ch3 = '!'; int key = 88; Console.WriteLine("Исходное сообщение: " + ch1 + ch2 + ch3); // Шифруем сообщение. ch1 = (char) (ch1 ^ key); ch2 = (char) (ch2 ^ key); ch3 = (char) (ch3 ^ key); Console.WriteLine("Зашифрованное сообщение: " + ch1 + ch2 + ch3); // Дешифрируем сообщение. ch1 = (char) (ch1 ^ key); ch2 = (char) (ch2 ^ key); ch3 = (char) (ch3 ^ key); Console.WriteLine("Дешифрованное сообщение: " + ch1 + ch2 + ch3); } } Вот как выглядит результат выполнения этой программы: Исходное сообщение: Hi! Зашифрованное сообщение: >1у Дешифрованное сообщение: Hi! Как видите, в результате выполнения двух операций XOR, использующих одно и то же значение ключа, получается исходное (дешифрованное) сообщение. Унарный оператор НЕ (или оператор дополнения до 1 ) инвертирует состояние всех битов своего операнда. Например, если целочисленное значение (хранимое в переменной А ), представляет собой двоичный код 1001 0110 , то в результате операции а получим двоичный код 0110 1001 В следующей программе демонстрируется использование оператора НЕ посредством отображения некоторого числа и его дополнения до 1 в двоичном коде. // Демонстрация поразрядного оператора НЕ. using System; class NotDemo { public static void Main() { 96 Часть I. Язык C# sbyte b = -34; int t; for(t = 128; t > 0; t = t/2) { if((b & t) != 0) Console.Write("1 "); if((b & t) == 0) Console.Write("0 "); } Console.WriteLine(); // Инвертируем все биты. b = (sbyte) b; for(t = 128; t > 0; t = t/2) { if((b & t) != 0) Console.Write("1 "); if((b & t) == 0) Console.Write("0 "); } } } Выполнение этой программы дает такой результат: 11011110 00100001 Операторы сдвига В C# можно сдвигать значение влево или вправо на заданное число разрядов. Для это в C# определены следующие операторы поразрядного сдвига: << сдвиг влево; >> сдвиг вправо. Общий формат записи этих операторов такой: значение << число_битов ; значение >> число_битов Здесь значение — это объект операции сдвига, а элемент число_битов указывает, на сколько разрядов должно быть сдвинуто значение . При сдвиге влево на один разряд все биты, составляющее значение, сдвигаются влево на одну позицию, а в младший разряд записывается нуль. При сдвиге вправо все биты сдвигаются, соответственно, вправо. Если сдвигу вправо подвергается значение без знака, в старший разряд записывается нуль. Если же сдвигу вправо подвергается значение со знаком, значение знакового разряда сохраняется. Вспомните: отрицательные целые числа представляются установкой старшего разряда числа равным единице. Таким образом, если сдвигаемое значение отрицательно, при каждом сдвиге вправо в старший разряд записывается единица, а если положительно — нуль. При сдвиге как вправо, так и влево крайние биты теряются. Следовательно, при этом выполняется нециклический сдвиг, и содержимое потерянного бита узнать невозможно. Ниже приводится программа, которая наглядно иллюстрирует результат сдвигов влево и вправо. Значение, которое будет сдвигаться, устанавливается сначала равным единице, т.е. только младший разряд этого значения “на старте” равен 1, все остальные равны 0. После выполнения каждого из восьми сдвигов влево программа отображает младшие восемь разрядов нашего “подопытного” значения. Затем описанный процесс повторяется, но в зеркальном отображении. На этот раз перед началом сдвига в переменную val заносится не 1, а 128, что в двоичном коде представляется как 1000 0000. И, конечно же, теперь сдвиг выполняется не влево, а вправо. Глава 4. Операторы 97 // Демонстрация использования операторов сдвига << и >>. using System; class ShiftDemo { public static void Main() { int val = 1; int t; int i; for(i = 0; i < 8; i++) { for(t = 128; t > 0; t = t/2) { if((val & t) != 0) Console.Write("1 "); if((val & t) == 0) Console.Write("0 "); } Console.WriteLine(); val = val << 1; // Сдвиг влево. } Console.WriteLine(); val = 128; for(i = 0; i < 8; i++) { for(t = 128; t > 0; t = t/2) { if((val & t) != 0) Console.Write("1 "); if((val & t) == 0) Console.Write("0 "); } Console.WriteLine(); val = val >> 1; // Сдвиг вправо. } } } Вот результаты выполнения этой программы: 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 Поскольку разряды представления двоичных чисел представляют собой степени числа 2, то операторы сдвига можно использовать в качестве быстрого способа умножения или деления чисел на 2. При сдвиге влево число удваивается. При сдвиге вправо число делится пополам. Конечно же, это будет справедливо до тех пор, пока с одного или другого конца не выдвинутся (и потеряются) значимые биты. Вот пример: // Использование операторов сдвига для // умножения и деления на 2. 98 Часть I. Язык C# using System; class MultDiv { public static void Main() { int n; n = 10; Console.WriteLine("Значение переменной n: " + n); // Умножаем на 2. n = n << 1; Console.WriteLine( "Значение переменной n после n = n * 2: " + n); // Умножаем на 4. n = n << 2; Console.WriteLine( "Значение переменной n после n = n * 4: " + n); // Делим на 2. n = n >> 1; Console.WriteLine( "Значение переменной n после n = n / 2: " + n); // Делим на 4. n = n >> 2; Console.WriteLine( "Значение переменной n после n = n / 4: " + n); Console.WriteLine(); // Устанавливаем n в исходное состояние. n = 10; Console.WriteLine("Значение переменной n: " + n); // Умножаем на 2, причем 30 раз. n = n << 30; // Увы; данные потеряны. Console.WriteLine( "Значение n после сдвига влево на 30 разрядов: " + n); } } Вот как выглядят результаты выполнения этой программы: Значение переменной n: 10 Значение переменной n после n = n * 2: 20 Значение переменной n после n = n * 4: 80 Значение переменной n после n = n / 2: 40 Значение переменной n после n = n / 4: 10 Значение переменной n: 10 Значение n после сдвига влево на 30 разрядов: -2147483648 Обратите внимание на последнюю строку результатов выполнения программы. После сдвига числа 10 влево на 30 разрядов (т.е. после умножения на 2 30 ) информация будет потеряна, поскольку значение исходного числа было “выдвинуто” за пределы диапазона представления чисел, соответствующего типу int . В данном случае вы видите странное отрицательное значение, которое получилось в результате попадания Глава 4. Операторы 99 единицы в старший разряд числа, который для типа int используется в качестве знакового. Вследствие этого число стало интерпретироваться как отрицательное. Этот пример показывает, как необходима осторожность при использовании операторов сдвига для умножения или деления чисел на 2. (Чтобы вспомнить, чем отличается представление значений со знаком от представления значений без знака, обратитесь к главе 3.) Поразрядные составные операторы присваивания Все бинарные поразрядные операторы можно успешно объединять с оператором присваивания, образуя поразрядные составные операторы присваивания. Например, следующие две инструкции присваивают переменной x результат выполнения операции исключающего ИЛИ (XOR) с операндами x и 127 x = x ^ 127; x ^= 127; Оператор ? Одним из самых замечательных операторов C# является тернарный оператор ?. Оператор ? часто используется для замены определенных типов конструкций if - then - else . Оператор ? называется тернарным, поскольку он работает с тремя операторами. Его общий формат записи имеет такой вид: Выражение1 ? Выражение2 : Выражение3 ; Здесь Выражение1 должно иметь тип bool . Типы элементов Выражение2 и Выражение3 должны быть одинаковы. Обратите внимание на использование и расположение двоеточия. Значение ? -выражения определяется следующим образом. Вычисляется Выражение1 . Если оно оказывается истинным, вычисляется Выражение2 , и результат его вычисления становится значением всего ? -выражения. Если результат вычисления элемента Выражение1 оказывается ложным, значением всего ? -выражения становится результат вычисления элемента Выражение3 . Рассмотрим пример, в котором переменной absval присваивается абсолютное значение переменной val absval = val < 0 ? -val : val; // Получаем абсолютное // значение val. Здесь переменной absval присваивается значение переменной val , если оно больше или равно нулю. Если же значение переменной val отрицательно, переменной absval присваивается результат применения к ней операции “унарный минус”, который будет представлять собой положительное значение. Вот еще один пример использования оператора ? . В следующей программе выполняется деление числа 100 на разные числа, но попытка деления на нуль реализована не будет. // способ обойти деление на нуль с помощью оператора ? using System; class NoZeroDiv { public static void Main() { int result; int i; 100 Часть I. Язык C# for(i = -5; i < 6; i++) { result = i != 0 ? 100 / i : 0; if(i != 0) Console.WriteLine("100 / " + i + " равно " + result); } } } Посмотрите на результаты выполнения этой программы. 100 / -5 равно -20 100 / -4 равно -25 100 / -3 равно -33 100 / -2 равно -50 100 / -1 равно -100 100 / 1 равно 100 100 / 2 равно 50 100 / 3 равно 33 100 / 4 равно 25 100 / 5 равно 20 Обратите внимание на следующую строку из этой программы: result - i != 0 ? 100 / i : 0; Здесь переменной result присваивается результат деления числа 100 на значение переменной i . Однако это деление выполнится только в том случае, если i не равно нулю. В противном случае (при i = 0 ) переменной result будет присвоено нулевое значение. В действительности совсем не обязательно присваивать переменной значение, генерируемое оператором ? . Например, вы могли бы использовать это значение в качестве аргумента, передаваемого методу. Или возможен еще такой вариант. Если все выражения, принимаемые оператором ? , имеют тип bool , то результат выполнения этого оператора можно использовать в качестве условного выражения в цикле или инструкции if Рассмотрим, например, предыдущую программу, переписанную в более эффективном виде (результат ее выполнения аналогичен предыдущему). // Способ обойти деление на нуль с помощью ?-оператора. using System; class NoZeroDiv2 { public static void Main() { int i; for(i = -5; i < 6; i++) if(i != 0 ? true : false) Console.WriteLine("100 / " + i + " равно " + 100 / i); } } Обратите внимание на инструкцию if . Если значение переменной i равно нулю, результат проверки if -условия будет равен значению false , которое не допустит выполнения инструкции вывода, а значит, и деления на нуль. В противном случае деление (с выводом результата) будет иметь место. Глава 4. Операторы 101 Использование пробелов и круглых скобок Любое выражение в C# для повышения читабельности может включать пробелы (или символы табуляции). Например, следующие два выражения совершенно одинаковы, но второе прочитать гораздо легче: х=10/y*(127/x); x = 10 / y * (127/x); Круглые скобки (так же, как в алгебре) повышают приоритет операций, содержащихся внутри них. Использование избыточных или дополнительных круглых скобок не приведет к ошибке или замедлению вычисления выражения. Другими словами, от них не будет никакого вреда, но зато сколько пользы! Ведь они помогут прояснить (для вас самих в первую очередь, не говоря уже о тех, кому придется разбираться в этом без вас) точный порядок вычислений. Скажите, например, какое из следующих двух выражений легче понять? x = y/3-34*temp+127; X = (y/3) - (34*temp) + 127; Приоритет операторов В табл. 4.2 показан порядок выполнения C#-операторов (от высшего до самого низкого). Эта таблица включает несколько операторов, которые описаны далее. Таблица 4.2. Приоритет C#-операторов Наивысший ( ) [ ] . ++ (постфиксный) -- (постфиксный) checked new sizeof typeof unchecked ! Операторы приведения типа + (унарный) - (унарный) ++ (префиксный) -- (префиксный) * / % + - << >> < <= > >= is == != & ^ | && || ?: = op= Низший |