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

C# для чайников

.pdf
Скачиваний:
183
Добавлен:
27.03.2015
Размер:
15.52 Mб
Скачать

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

i n t

n l

=

5;

 

i n t

n2

=

- nl ; // Теперь значение п2 равно -5

 

Значение -п представляет собой отрицательное значение п.

 

Оператор деления по модулю может быть вам незнаком. Деление по модулю анало-

1

гично получению остатка после деления. Так, 5%3 равно 2 (5/3 = 1, остаток 2), а^25%3

I

равен 1 (25/3 = 8, остаток 1).

 

 

 

Строгое определение оператора % выглядит как х= (х/у) *у + (х%у).

 

Арифметические операторы (кроме деления по модулю) определены для всех типов

 

переменных. Оператор же деления по модулю не определен для чисел с плавающей точ-

I

кой, поскольку при делении значений с плавающей точкой не существует остатка.

 

П о р я д ок в ы п о л н е н и я о п е р а т о р о в

 

Значение некоторых выражений может оказаться непонятным. Например, рассмот-

|

рим выражение:

 

i n t П = 5 * 3 + 2 ;

 

Что имел в виду написавший такую строку программист? Что надо умножить 5 на 3,

|

а затем прибавить 2? Или сначала сложить 3 и 2, а результат умножить на 5?

 

 

 

С# обычно выполняет операторы слева направо, так что результатом приведен-

I

 

 

ного примера будет значение, равное 17.

 

С# вычисляет значение п в представленном далее выражении, сначала деля 24 на 6, а затем деля получившееся значение на 2 :

i n t п = 2 4 / 6 / 2 ;

Однако у операторов есть своя иерархия, приоритеты, или проще — свой порядок выполнения. С# считывает все выражение и определяет, какие операторы имеют наи­ высший приоритет и должны быть выполнены до операторов с меньшим приоритетом. I Например, приоритет умножения выше, чем сложения. Во многих книгах изложению этого вопроса посвящены целые главы, но сейчас не стоит забивать этим ни главу, ни вашу голову.

Никогда не полагайтесь на то, что вы (или кто-то иной) помните приоритеты I операторов. Явно указывайте подразумеваемый порядок выполнения выраже­ ния посредством скобок.

Значение следующего выражения совершенно очевидно и не зависит от приоритета операторов:

i n t п = (7 % 3) * ( 4 + ( 6 / 3 ) ) ;

74

Часть //. Основы программирования в С# [

Скобки перекрывают приоритеты операторов, явно указывая, как именно компилятор должен интерпретировать выражение. С# ищет наиболее вложенную пару скобок и вы­ числяет выражение в ней — в данном случае это 6/3, что дает значение 2. В результате получается:

int п = (7 % 3) * (4 + 2) ; // 2 = 6 / 3

Затем С# продолжает поиск скобок и вычисляет значения в них, что приводит к выражению:

int

n =

1 * 6; / / 6 = 4 + 2, 1 = 7 % 3

Так что в конечном счете получается:

int

П =

6;

Правило "всегда используйте скобки" имеет, пожалуй, одно исключение. Лично мне с этим сложно примириться, но многие программисты опускают скобки в выражениях наподобие приведенного ниже, поскольку очевидно, что умножение имеет более высо­

кий приоритет, чем сложение:

 

int п = 7 + 2 * 3 ;

// То же, что и 7 + ( 2 * 3 )

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

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

Этот новый взгляд на присваивание никак не влияет на выражения, с которыми вы уже сталкивались:

П= 5 * 3 ;

Вданном примере 5*3 = 15 и имеет тип i n t . Оператор присваивания сохраняет это int-значение справа в int-переменной слева и возвращает значение 15. То, что он воз­ вращает значение, позволяет, например, сохранить это значение еще в одной перемен­ ной, т.е. написать:

m = п = 5 * 3 ;

При наличии нескольких присваиваний они выполняются справа налево. В приведен­ ном выше выражении правый оператор присваивания сохраняет значение 15 в перемен­ ной п и возвращает 15, после чего левый оператор присваивания сохраняет значение 15 в переменной m и возвращает 15 (это возвращенное значение в данном примере больше никак не используется).

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

int n; int m;

П = m = 2 ;

Старайтесь избегать цепочек присваиваний, поскольку они менее понятны человеку, читающему исходный текст программы. Всего, что может запутать человека, читающего исходный текст вашей программы (включая и лично вас), следует избегать. Любые неяс­ ности ведут к ошибкам. Огромная часть программирования — от правил языка и его конструкций до соглашений по именованию переменных и рекомендаций, основанных на опыте программистов — нацелены на одно: устранение ошибок в программах.

[лава 4. Операторы

75

С# добавляет ко множеству простейших операторов небольшое подмножество опера­ торов, построенных на основе других бинарных операторов. Например, выражение

П += 1;

эквивалентно следующему:

П = П + 1 ;

Такие операторы присваивания существуют почти для всех бинарных операторов. I В табл. 4.2 показаны наиболее распространенные составные операторы присваивания.

Т а б л и ц а

4.2. Составные операторы присваивания

Оператор

Значение

В табл. 4.2 опущено два более сложных составных оператора присваивания, <<= и >>=. Операторы побитового сдвига, на которых они основаны, будут рассмотрены позже в этой главе.

О п е р а т о р инкремента

Среди всех сложений, выполняемых в программах, добавление 1 к переменной — наиболее распространенная операция:

n = n + 1; // Увеличение п на 1

С# позволяет записать такую операцию сокращенно: n += 1; // Увеличение п на 1

Но, оказывается, и это недостаточно кратко, и в С# имеется еще более краткое обозначе­ ние этого действия — оператор инкремента:

++п ; // Увеличение п на 1

Все три приведенных выражения функционально эквивалентны, т.е. все они увеличи­ вают значение п на 1.

Оператор инкремента достаточно странен, но еще больше странности добавляет ему то, что на самом деле имеется два оператора инкремента: ++п и п++. Первый, ++п, на­ зывается префиксным, а второй, п++, — постфиксным. Разница между ними достаточно тонкая, но очень важная.

Вспомните, что каждое выражение имеет тип и значение. В следующем фрагменте

исходного текста и ++п, и п++ имеют тип int:

int П;

 

П = 1;

 

int р = ++П;

 

76

Часть II. Основы программирования в С#

n = 1;

int m = n + + ;

Чему равны значения p и m после выполнения этого фрагмента? (Подсказка: можно выбирать 1 или 2.) Оказывается, значение р равно 2, а значение m — 1. То есть значение выражения + + п — это значение п после увеличения, а значение п++ равно значению п до увеличения. Значение самой переменной п в обоих вариантах равно 2.

Эквивалентные операторы декремента— п- - и — п— используются для замены выражения n = n - 1 . Они работают точно так же, как и операторы инкремента.

Откуда взялся оператор инкремента?

Причина появления оператора инкремента лежит в туманном прошлом — в наличии в 1970-х годах в машине PDP-8 машинной команды инкремента.

Язык С, прямой предок С#, в свое время создавался для применения именно на этих машинах. Наличие соответствующей машинной команды позволяло уменьшить ко­ личество машинных команд при использовании п++ вместо п=пч-1. В то время эко­ номия даже нескольких машинных команд давала существенный выигрыш во вре­ мени работы.

В настоящее время компиляторы гораздо интеллектуальнее, и нет никакой разницы, написать ли в программе п++ или п=п +1 . Однако программисты — люди привычки, так что оператор инкремента благополучно дожил до сегодняшних дней, и увидеть в программе на С или С++ выражение п=п +1 практически нереально.

Кроме того, чаще всего программистами используется постфиксная версия оператора. Впрочем, это дело вкуса.. ?

С# предоставляет к услугам программиста также целый ряд логических операторов сравнения, показанных в табл. 4.3. Эти операторы называются логическими сравнениями (logical comparisons), поскольку они возвращают результат сравнения в виде значения true или f a l s e , имеющего тип b o o l .

• Вот примеры использования логических сравнений:

int m = 5; int n = 6;

bool b = m > n;

В этом примере переменной b присваивается значение f a l s e , поскольку 5 не боль­ ше, чем 6.

2 Использование префиксного оператора инкремента дает определенный выигрыш (компилятору не приходится дополнительно хранить значение переменной до инкремента), так что лучше приобретать привычку применять префиксную форму там, где выбор вида оператора инкремента не принципиален. — Примеч. ред.

Глава 4. Операторы

77

Т а б л и ц а 4.3.

Логические операторы сравнения

Оператор...

 

...возвращает true, если ...

 

 

 

 

а

=.= Ь

а

имеет то же значение, что и ь

а

> ь

а

больше ь

а

>= b

а больше или равно ь

а

<

ь

а

меньше ь

а

<=

ь

а меньше или равно ь

а

! = Ь

а

не равно ь

 

 

 

 

 

Логические сравнения определены для всех числовых типов, включая f l o a t , dou­ b l e , d e c i m a l и c h a r . Все приведенные ниже выражения корректны:

b o o l

 

b ;

 

 

b

=

3

>

2;

 

b

=

3

. 0

> 2 . 0 ;

b

=

'a1

>

'b ' ;

b

=

1 A'

<

' a ' ;

b

=•

' A1

<

'b ' ;

b =

10M

>

12M;

 

 

 

 

 

/ /

t r u e

 

 

 

 

/ /

t r u e

 

 

 

 

/ /

f a l s e

-

позже в алфавитном

порядке

/ /

означае т

"больше"

 

 

/ /

t r u e

-

прописное 'А'

меньше

/ /

строчного ' а '

 

 

/ /

t r u e

-

вс е прописные

буквы

меньше все х

/ /

строчных

 

 

 

/ /

f a l s e

 

 

 

 

Операторы сравнения всегда дают в качестве результата величину типа b o o l . Опера­ торы сравнения, отличные от ==, неприменимы к переменным тира s t r i n g (не волнуй­ тесь, С# предлагает другие способы сравнения строк).

С р а в н е н ие ч и с е л с п л а в а ю щ е й т о ч к о й

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

f l o a t

 

f l ;

 

 

 

 

 

f l o a t

f 2 ;

 

 

 

 

 

f l

=

1 0 ;

 

 

 

 

 

f2

=

f l

/

3 ;

 

 

 

 

b o o l

Ы

=

(3

*

f2)

==

f l ;

f l

=

9;

 

 

 

 

 

 

f2

=

f l

/

3 ;

 

 

 

 

b o o l

b2

=

(3

*

f2)

==

f l ;

Обратите внимание, что в пятой и восьмой строках примера сначала содержится опе­ ратор присваивания =, а затем оператор сравнения ==. Это — разные операторы. С# сначала выполняет логическое сравнение, а затем присваивает его результат пере­ менной слева от оператора присваивания.

Единственное отличие между вычислениями Ы и Ь2 состоит в исходном значении f 1. Так чему же равны значения Ы и Ь2? Очевидно, что значение Ы равно t r u e : 9/3 равно 3, 3*3 равно 9, 9 равно 9. Никаких проблем!

Значение Ы не столь очевидно:

10/3 равно 3.3333.... 3.3333...*3 равно 9.9999.... Но

равны ли числа 9.9999... и 10? Это

зависит от того, насколько сообразительны ваши

78

Часть II. Основы программирования в С#

компилятор и процессор. При использовании процессора типа Pentium С# недостаточно умен для того, чтобы понять, что Ы надо присвоить значение t r u e .

Для сравнения f 1 и f 2 можно воспользоваться функцией для вычисления аб­ солютного значения следующим образом:

M a t h . a b s ( f l - f 2 * 3 . 0 ) < . 0 0 0 1 ; // . . . и л и другая степень точ­ ности

Такая функция вернет значение t r u e в обоих случаях. Вместо . 0 0 0 1 можно исполь­ зовать константу D o u b l e . E p s i l o n , чтобы получить максимальную точность. Эта кон­ станта представляет собой наименьшую возможную разницу между двумя неравными значениями типа double .

Чтобы узнать, какие еще возможности скрывает в себе класс System . Math, вос­ пользуйтесь командой меню Help1 ^Index и введите Math в поле Look For.

Составные л о г и ч е с к и е о п е р а т о р ы

Для переменных типа b o o l имеются специфичные для них операторы, показанные в табл. 4.4.

Оператор ! представляет собой логический эквивалент знака "минус". Например, ! а истинно, если а ложно, и ложно, если а истинно.

Следующие два оператора тоже вполне просты и понятны. а&Ь истинно тогда и толь­ ко тогда, когда и а, и b одновременно равны t r u e ; а | b истинно тогда и только тогда, когда или а, или Ь, или оба они одновременно равны t r u e . Оператор Л (исключающее или) возвращает значение t r u e тогда и только тогда, когда значения а и b различны — т.е. когда одно из значений t r u e , а второе — f a l s e .

Все перечисленные операторы возвращают в качестве результата значение типа b o o l .

Операторы &, | и имеют версии, называющиеся побитовыми (bitwise). При применении к переменным типа i n t эти операторы выполняют действия с каж­ дым битом отдельно. Таким образом, 6&3 равно 2 (01102&00112 равно 00102), 6 | 3 равно 7 (011021 00112 равно 01112), а 6Л3 равно 5 (01102 Л 00112 равно

01012). Бинарная арифметика — очень интересная вещь, но она выходит за рам­ ки настоящей книги.

Последние два оператора очень похожи на предыдущие, но имеют одно едва уловимое от­ личие. В чем оно заключается, вы сейчас поймете. Рассмотрим следующий пример:

bool b = (ЛогическоеВыражение!) & (ЛогическоеВьгражение2) ;

Глава 4. Операторы

79

В этом случае С# вычисляет ЛогическоеВыражение1 и ЛогическоеВыражение2,

а затем смотрит, равны они оба t r u e или нет, чтобы найти, какое значение следует при­ своить переменной Ь. Но может оказаться, что С# выполняет лишнюю работу — ведь если одно из выражений равно f a l s e , то каким бы ни было второе, результат не может быть t r u e в любом случае.

Оператор && позволяет избежать вычисления второго выражения, если после вычисле­ ния первого конечный результат становится очевиден:

b o o l Ь

= (ЛогическоеВыражение1)

&& (ЛогическоеВыражение2);

В этой

ситуации С# вычисляет значение

ЛогическоеВыражение1, и если оно равно

f a l s e , то переменной Ь присваивается значение f a l s e и ЛогическоеВыражение2 не

вычисляется. Если же ЛогическоеВыражение1

равно t r u e , то С# вычисляет Логиче-

скоеВыражение2 и после этого определяет, какое значение присвоить переменной Ь.

Оператор

| | работает аналогично, как видно из следующего выражения:

b o o l b =

(ЛогическоеВыражение1)

(ЛогическоеВыражение2);

В этой ситуации С# вычисляет значение ЛогическоеВыражение1, и если оно равно t r u e , то переменной b присваивается значение t r u e и ЛогическоеВыражение2 не вычисляет­ ся. Если же ЛогическоеВыражение1 равно f a l s e , то С# вычисляет ЛогическоеВыражение2 и после этого определяет, какое значение присвоить переменной Ь.

Вы можете называть эти операторы "сокращенным и" и "сокращенным или".

В вычислениях тип результата важен не менее, чем сам результат. Рассмотрим сле­ дующее выражение:

int П ;

П = 5 * 5 + 7;

Калькулятор утверждает, что результат вычислений равен 32. Но это выражение име­ ет не только значение, но и тип.

Будучи записано на "языке типов", оно принимает следующий вид: int [=] int * int + int;

Для выяснения типа выражения нужно следовать тому же шаблону, что и при вычис­ лении его значения. Умножение имеет более высокий приоритет, чем сложение. Умно­ жение int на int дает int. Далее идет сложение int и int, что в результате тоже да­ ет int. Итак, вычисление типа приведенного выражения происходит таким образом:

int * int + int int + int

int

В ы ч и с л е н и е т и п а о п е р а ц и и

Выяснение типа выражения происходит в нисходящем направлении посредством вы­ яснения типов подвыражений. Каждое выражение имеет тип, и типы левых и правых ар­ гументов оператора должны соответствовать самому оператору:

t y p e l <ор> t y p e 2 •=> t y p e 3

(Здесь стрелка означает "дает".) Типы t y p e l и t y p e 2 должны быть совместимы с опе­ ратором ор.

80

Часть II. Основы программирования в С#

Большинство операторов могут иметь несколько вариантов. Например, оператор ум­ ножения может быть следующих видов:

int

*

i n t

uint

* u i n t

long

* l o n g

float

* f l o a t

decimal

*

decima l

double

*

doubl e

Таким образом, 2*3 int 6.

•=> i n t

<=> u i n t •=> l o n g •=> f l o a t

<=> d e c i m a l doubl e

использует i n t * i n t версию оператора * и дает в результате

Неявное преобразование типов

Все хорошо, просто и понятно, если умножать две переменные типа i n t или две пе­ ременные типа f l o a t . Но что получится, если типы аргументов слева и справа будут различны? Что, например, произойдет в следующей ситуации:

int nl = 10 ; double d2 = 5 . 0 ;

double dResul t = nl * d2 ;

Во-первых, в C# нет оператора умножения i n t * d o u b l e . C# может просто сгенери­ ровать сообщение об ошибке и предоставить разбираться с проблемой программисту. Однако он пытается понять намерения программиста и помочь ему. У С# есть операторы умножения i n t * i n t и double*double . С# мог бы преобразовать d2 в значение i n t , но такое преобразование привело бы к потере дробной части числа (цифр после десятич­ ной точки). Поэтому вместо этого С# преобразует nl в значение типа doubl e и исполь­ зует оператор умножения double*double . Это действие известно как неявное повы­ шение типа (implicit promotion).

Такое повышение называется неявным, поскольку С# выполняет его автоматически, и является повышением, так как включает естественную концепцию высоты типа. Список

операторов умножения был приведен в порядке повышения— от i n t до double, или от int до decima l — от типа меньшего размера к типу большего размера. Между ти­ пами с плавающей точкой и d e c i m a l неявное преобразование не выполняется. Преоб­ разование из более емкого типа, такого как double, в менее емкий, такой как i n t , на­ зывается понижением (demotion).

Повышение иногда называют преобразованием вверх (up conversion), а понижение —

преобразованием вниз (down conversion).

Неявные понижения запрещены. В таких случаях С# генерирует сообщение об ошибке.

Явное преобразование типа

Но что, если С# ошибается? Если на самом деле программист хотел выполнить целое умножение?

Вы можете изменить тип любой переменной с типом-значением с помощью оператора приведения типа (cast), который представляет собой требуемый тип, заключенный в скобки,

и располагаемый непосредственно перед приводимой переменной или выражением.

Глава 4. Операторы

81

Таким образом, в следующем выражении используется оператор умножения i n t * i n t :

i n t n l

= 1 0

;

d o u b l e

d2 =

5 . 0 ;

d o u b l e n R e s u l t = n l * ( i n t ) d 2 ;

Приведение d2 к типу i n t известно как явное понижение (explicit demotion) или noi нижающее приведение (downcast). Понижение является явным, поскольку программю! явно объявил о своих намерениях.

Вы можете осуществить приведение между двумя любыми типами-значениями, нем висимо от их взаимной высоты.

Избегайте неявного преобразования типов. Делайте все изменения типов! значений явными с помощью оператора приведения.

Оставьте логику в покое

С# не позволяет преобразовывать другие типы в тип b o o l или выполнять преобразо­ вание типа b o o l в другие типы.

Т и п ы при присваивании

Все сказанное о типах выражений применимо и к оператору присваивания.

Случайные несоответствия типов, приводящие к генерации сообщений об ошибках, обычно происходят в операторах присваивания, а не в точке действи­ тельного несоответствия.

Рассмотрим следующий пример умножения:

i n t

n l = 1 0 ;

i n t

П2 = 5 . 0 * n l ;

Вторая строка этого примера приведет к генерации сообщения об ошибке, связанной с несоответствием типов, но ошибка произошла при присваивании, а не при умножении. Вот что произошло: для того чтобы выполнить умножение, С# неявно преобразовал nl в тип d o u b l e . Затем С# выполнил умножение двух значений типа d o u b l e , получив в результате значение того же типа d o u b l e .

Типы левого и правого аргументов оператора присваивания должны совпадать, но тип левого аргумента не может быть изменен. Поскольку С# не может неявно понизить тип выражения, компилятор генерирует сообщение о том, что он не может неявно пре­ образовать тип d o u b l e в i n t .

При использовании явного приведения никаких проблем не возникнет:

i n t n l = 1 0 ;

i n t n2 = ( i n t ) ( 5 . 0 * n l ) ;

(Скобки необходимы, потому что оператор приведения имеет очень высокий приоритет.) Такой исходный текст вполне работоспособен, так как явное понижение разрешено. Здесь значение nl будет повышено до d o u b l e , выполнено умножение, а результат типа d o u b l e будет понижен до i n t . Однако в этой ситуации надо подумать о душевном здо­ ровье программиста, поскольку написать просто 5*п1 было бы проще как для програм­ миста, так и для С#.

82

Часть II. Основы программирования в С#

Большинство операторов имеют два аргумента, меньшинство —- один. И только один оператор — тернарный — имеет три аргумента. Лично я считаю, что это ненужная экзо­ тика. Вот формат этого оператора:

Выражение типа boo l ? Выражение1 : Выражение2

А это пример его применения: int а = 1;

int Ь = 2 ;

int nMax = (a>b) ? а : Ь;

Если а больше Ь (условие в скобках), значение выражения равно а. Если а не больше Ь, значение выражения равно Ь.

Выражения 1 и 2 могут быть любой сложности, но это должны быть истинные выра­ жения— они не могут содержать объявлений или других инструкций, не являющихся выражениями.

Тернарный оператор непопулярен3 по следующим причинам.

Он не является необходимым. Использование оператора if, описанного в гла­ ве 5, "Управление потоком выполнения", дает тот же эффект, и его легче понять.

На тернарный оператор накладываются дополнительные ограничения. На­ пример, выражения 1 и 2 должны быть одного и того же типа. Это приводит к следующему:

in t

а =

1 ;

double b = 0 . 0 ;

in t

nMax

= (a>b) ? а : b;

Такой исходный текст не будет компилироваться, несмотря на то что в конечном итоге nMax будет иметь значение а. Поскольку а и b должны быть одного и того же типа, а будет повышено до double, чтобы соответствовать Ь. Тип результи­

рующего значения оператора

?:

оказывается double, и этот тип должен быть

понижен до i n t перед присваиванием:

in t

а =

1 ;

 

 

 

double b

= 0 . 0 ;

 

 

 

i n t

nMax;

 

 

 

//

Можно поступить

так:

 

 

nMax =

( i n t ) ( (a>b)

? а

:

b) ;

//. . . или так:

nMax = (a>b) ? а : ( i n t ) b ;

Увидеть тернарный оператор в реальной программе — большая редкость.

3 Непопулярность этого оператора относится к С#, программистами на С и С++ он употребля­ ется достаточно часто и не вызывает никаких отрицательных эмоций. — Примеч. ред.

Глава 4. Операторы

83

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]