Объектно-ориентированное программирование.-6
.pdfВыбора |
?: ?? |
|
|
Создания объектов |
new stackalloc |
|
|
Рефлективные |
sizeof is typeof |
|
|
Управления переполнением |
checked unchecked |
|
|
Работа с указателями |
* -> & [] |
|
|
Лямбда-выражение |
=> |
|
|
Данное деление на категории несколько условно. Например, арифметический оператор «+» также применяется для конкатенации строк. Оператор «()» используется не только для преобразования типов, но и для вызова методов, а также как обычные скобки в выражении, указывающие порядок выполнения операций. А оператор «–>» можно отнести как к операторам работы с указателями, так и к операторам доступа. И т.д.
3.3.2. Приоритет и порядок выполнения
Когда в одном выражении несколько операторов, компилятор должен определить порядок их выполнения. При этом компилятор руководствуется правилами, которые называются старшинством операторов. Понимание старшинства операторов нужно для правильного написания выражений – иногда результат может не соответствовать ожидаемому.
Посмотрим, как старшинство операторов определяется в C# (табл.
3.18).
Табл. 3.18 – Операторы языка C# (по приоритету)
Прио- |
Группа |
|
Операции |
Поря- |
ритет |
|
|
|
док |
|
|
|
|
|
0 |
Первичные |
|
()(1) . :: ()(2) [] ++(3) |
→ |
|
|
|
––(3) ->(4) new typeof |
|
|
|
|
checked unchecked |
|
|
|
|
default delegate |
|
|
|
|
stackalloc(4) |
|
1 |
Унарные |
|
+ – ! ~ ++(5) ––(5) ()(6) |
→ |
|
|
|
&(4, 7) *(4, 8) sizeof(4) |
|
|
|
|
true false |
|
|
|
|
|
|
2 |
Мультипликативные |
|
* / % |
→ |
|
|
|
|
|
3 |
Аддитивные |
|
+ – |
→ |
|
|
|
|
|
4 |
Сдвига |
|
<< >> |
→ |
|
|
|
|
|
|
|
171 |
|
5 |
Отношения и проверки типов |
< > <= >= is as |
→ |
|
|
|
|
6 |
Эквивалентности |
== != |
→ |
|
|
|
|
7 |
Побитовые |
& |
→ |
|
|
|
|
8 |
|
^ |
→ |
|
|
|
|
9 |
|
| |
→ |
|
|
|
|
10 |
Булевы |
&& |
→ |
|
|
|
|
11 |
|
|| |
→ |
|
|
|
|
12 |
Поддержка null |
?? |
← |
|
|
|
|
13 |
Условный |
?: |
← |
|
|
|
|
14 |
Присваивания |
= *= /= %= += –= <<= |
← |
|
|
>>= &= ^= |= |
|
|
|
|
|
|
Лямбда-выражение |
=> |
← |
|
|
|
|
Примечания:
(1)скобки;
(2)оператор вызова функции;
(3)постфиксные;
(4)только в небезопасном коде;
(5)префиксные;
(6)преобразование типа;
(7)получение адреса;
(8)разыменование указателя.
При конструировании выражений важно учитывать приоритет операций. Например, результат выражения
false && false || true
зависит от того, у какого оператора выше приоритет – логической конъюнкции или дизъюнкции. Если у конъюнкции, то получим
(false && false) || true = false || true = true
А если у дизъюнкции, то
false && (false || true) = false && true = false
Если нет уверенности в очередности выполнения операций, то лучше задать ее явно, используя скобки. Важно, что у операторов присваивания самый низкий приоритет (14). Иначе в записи
<переменная> = <выражение>
172
выражение практически всегда приходилось бы брать в скобки.
По порядку выполнения операторы разделяются на операторы с левой и правой ассоциативностью. Ассоциативность определяет, какая часть выражения должна быть вычислена первой. Например, результатом приведенного выражения может быть 21 или 33 в зависимости от того, какая ассоциативность будет применяться для оператора «–»: левая или правая:
42 - 15 - 6
Данный оператор имеет левую ассоциативность, т.е. сначала вычисляется 42–15, а затем из результата вычитается 6, результат – 21. Если бы он имел правую ассоциативность, сначала вычислялась бы правая часть выражения (15–6 = 9), а затем результат вычитался бы из: 42 – 9 = 33.
Все бинарные операторы (операторы с двумя операндами), кроме операторов присваивания, – лево-ассоциативные, т.е. они обрабатывают выражения слева направо. Таким образом, «а + b + c» – то же, что и «(а + b) + c», где сначала вычисляется «а + b», а затем к сумме прибавляется «c». Операторы присваивания и условные операторы – право-ассоциативные, т.е. обрабатывают выражения справа налево. Иначе говоря, «a = b = c» эквивалентно «a = (b = c)». Например, рассмотрим следующий код:
int a = 1; int b = 2; int с = 3; a = b = c;
В результате все три переменные будут иметь значение 3. Если бы оператор присваивания был лево-ассоциативным, то сначала выполнился бы оператор «a = b», а затем – «b = c». В итоге переменная «a» имела бы значение 2, а переменные «b» и «c» – 3. Очевидно, что мы ожидаем не этого, когда пишем «a = b = c», и именно поэтому операторы присваивания и условные операторы право-ассоциативные.
3.3.3. Описание операторов
Рассмотрим перечисленные выше операторы по категориям. Но сначала выделим некоторые первичные операторы, которые нам уже знакомы или которые будут изучены позже.
Пример: Samples\3.3\3_3_3_oper.
173
3.3.3.1. Первичные операторы
Рассмотрим сначала некоторые простые первичные операторы, с которыми мы уже сталкивались ранее или рассмотрим в последующих разделах:
•(x) – Это разновидность оператора «скобки» для управления порядком вычислений в математических, логических и других операциях;
•x.y – Оператор «точка» используется для указания члена пространства имен, класса, структуры или интерфейса. Здесь «x» представляет сущность, содержащую в себе член «y»;
•x::y – Квалификатор псевдонима пространства имен. Здесь «x» представляет псевдоним пространства имен, содержащего в себе член «y» (§ 4.1);
•f(x) – Такая разновидность оператора «скобки» применяется для вызова методов;
•a[x] – Квадратные скобки используются для индексации массива;
•new – Этот оператор используется для создания экземпляров объектов (классов, структур, делегатов и т.д.);
•stackalloc – Выделение блока памяти на стеке (п. 5.5.2.4);
•default – Возвращает значение типа по умолчанию (§ 3.1);
•delegate – Оператор объявления анонимной функции (§ 4.8). Остальные первичные операторы рассмотрены далее, по категориям.
Кроме условного тернарного оператора, остальные операторы являются унарными или бинарными (оператора). Синтаксис операторов будем записывать в следующей форме:
1) Для префиксных или постфиксных унарных операторов:
<тип результата> operator op (<тип аргумента> x)
В префиксных операторах знак операции (op) стоит перед аргументом (op x), в постфиксных операциях – после аргумента (x op).
2) Для инфиксных бинарных операторов:
<тип результата> operator op (<тип аргумента1> x, <тип аргумента2> y)
В инфиксных операторах знак операции (op) стоит между аргументами
(x op y).
Если оператор применим для какого-либо типа по значению <тип>, то он применим и для соответствующего обнуляемого типа <тип>?. При этом, если хотя бы один аргумент имеет неопределенное значение (null), то и результатом также будет null. Исключения будут оговариваться отдельно.
174
3.3.3.2. Арифметические операторы
Язык C#, как и большинство других языков, поддерживает основные математические операторы – бинарные (умножение, деление, сложение, вычитание и модуль) и унарные (плюс и минус). Синтаксис бинарных операто-
ров (*, /, +, -, %):
<тип> operator op (<тип> x, <тип> y)
Здесь <тип> – один из типов int, uint, long, ulong, float, double, decimal. Операторы «+» и «–» также определены для делегатов (см. § 4.8). Для выполнения арифметических операций с другими типами данных используется неявное приведение типов. А если оно не определено, компилятор генерирует ошибку:
byte b = 1;
b = b + b; // Ошибка
В данном примере используется оператор сложения с аргументами типа int, т.к. это ближайший тип, перекрывающий диапазон типа byte. Полученный результат также имеет тип int, но нет неявного преобразования из int в byte, поэтому компилятор покажет ошибку.
Унарных арифметических операторов четыре: плюс, минус, инкремент и декремент. Первые два являются префиксными, последние два имеют префиксную и постфиксную форму.
Оператор унарного минуса указывает компилятору, что результатом операции будет значение аргумента с обратным знаком. Синтаксис:
<тип> operator - (<тип> x)
Допустимые типы – int, long, float, double, decimal. В операторе унарного плюса результатом будет просто значение аргумента. Синтаксис:
<тип> operator + (<тип> x)
Допустимые типы – int, uint, long, ulong, float, double, decimal.
Операторы инкремента (++) и декремента (--) позволяют лаконично выразить, что мы хотим увеличить или уменьшить числовое значение на 1. В префиксной версии операторов сначала выполняется операция, а затем создается значение. В постфиксной версии сначала создается значение, а затем выполняется операция. Синтаксис операторов:
<тип> operator ++ (<тип> x) <тип> operator –- (<тип> x)
175
Допустимые типы – sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal и перечисления. При этом аргумент «x» обязательно должен попадать в категорию l-value (см. п. 3.3.3.6).
Пример:
int v1 = 1; uint v2 = 2; byte v3 = 1; sbyte v4 = 2; long v5 = 1; ulong v6 = 2; double v7 = 1; float v8 = 2; decimal v9 = 3; char v10 = 'A';
bool v11 = false;
Console.WriteLine("int + uint = " + (v1 + v2).GetType()); Console.WriteLine("byte - sbyte = " + (v3 - v4).GetType()); Console.WriteLine("uint * ulong = " + (v2 * v6).GetType()); Console.WriteLine("uint / byte = " + (v2 / v3).GetType()); Console.WriteLine("byte % byte = " + (v3 % v3).GetType()); Console.WriteLine("long + ulong = " + (v5 + v6).GetType()); // Ошибка Console.WriteLine("double * int = " + (v7 * v1).GetType()); Console.WriteLine("double - decimal = " + (v7 – v9).GetType()); //
Ошибка
Console.WriteLine("float - float = " + (v8 - v8).GetType()); Console.WriteLine("char * char = " + (v10 * v10).GetType()); Console.WriteLine("bool * bool = " + (v11 * v11).GetType()); // Ошибка
DayOfWeek v12 = DayOfWeek.Friday;
int a1 |
= 1; |
||
int b1 |
= a1++; |
||
int |
a2 |
= |
1; |
int |
b2 |
= |
++a2; |
Console.WriteLine("{0} {1}", a1, b1);
Console.WriteLine("{0} {1}", a2, b2);
int b3 = 1++; // Ошибка
Console.WriteLine(++v10);
Console.WriteLine(++v12);
Первая ошибка связана с тем, что нет типа данных с диапазоном значений, перекрывающим диапазоны значений типов long и ulong. Вторая – с тем, что не определены неявные преобразования между типами double и decimal. Третья – с тем, что тип bool нельзя неявно преобразовать ни к одному из типов, для которых определены арифметические операторы. Четвертая – аргумент инкремента не является l-value. Результаты работы программы:
int + uint = System.Int64 byte - sbyte = System.Int32 uint * ulong = System.UInt64
176
uint / byte = System.UInt32 byte + byte = System.Int32 double * int = System.Double float - float = System.Single char * char = System.Int32
2 1
2 2 B
Saturday
3.3.3.3.Логические операторы
Клогическим операторам относятся условные логические операторы конъюнкция (&&) и дизъюнкция (||), а также логические операторы отрицания (!) и пользовательские условные логические операторы true и false.
Условные логические операторы являются бинарными:
bool operator && (bool x, bool y) bool operator || (bool x, bool y)
Данные операторы применимы только к аргументам логического типа, а также к типам, определяющим преобразование к типу bool (как определить операции преобразования типа, читайте в § 4.7). Для обнуляемого типа bool? они не применимы.
Операторы «&&» и «||» являются условными версиями операторов «&»
и«|» для типа bool:
•Операция «x && y» соответствует операции «x & y», за исключением того, что «y» вычисляется, только если «x» не равен false.
•Операция «x || y» соответствует операции «x | y», за исключением того, что «y» вычисляется, только если «x» не равен true.
Синтаксис унарных логических операторов:
bool operator ! (bool x) bool operator true (<тип> x)
bool operator false (<тип> x)
Первый оператор вычисляет логическое отрицание операнда. Операторы true и false определяются в классе пользователя для того, чтобы экземпляры класса могли:
1)использоваться в качестве условия в операторах if, for, while, а также
вусловном операторе (?:) (см. пример);
2)являться аргументами логических операторов (см. § 4.7).
Пример:
CInt v16 = new CInt(1);
177
v1 = 1;
if (v1 && v11) Console.WriteLine(v1); // Ошибка v11 = false;
if (v11 && (v1++) != 0) Console.WriteLine(v1); v11 = true;
if (v11 || (v1++) != 0) Console.WriteLine(v1);
if (true || (v1++) != 0) Console.WriteLine(v1); // Предупреждение Console.WriteLine(v1);
if (v16) Console.WriteLine(v16.Value + " == true");
Вывод на консоль:
1
1
1
1 == true
Видно, что код «v1++» ни разу не был выполнен (данное выражение не обязательно заключать в скобки, т.к. приоритет инкремента выше, чем у операций сравнения; это сделано для повышения читабельности кода). В первом случае потому, что «false && x» всегда равно false, поэтому вычисление аргумента «x» не требуется. Во втором случае – потому что «true || x» всегда равно true, и вычисление аргумента «x» также не требуется. В третьем случае в логическом операторе использована не переменная, а константа, поэтому код «v1++» не будет выполнен ни при каких условиях. Компилятор генерирует предупреждение «Обнаружен недостижимый код в выражении».
Ошибка связана с тем, что оператор «int && bool» не определен, и нет неявного преобразования типа int в тип bool.
В последней строке переменная v16 является аргументом условия. Это возможно, т.к. класс CInt перегружает операторы true и false (см. исходный код решения к данному пункту). Видно, что данные операторы вызываются неявно. Запись true(v16) или false(v16) привела бы к ошибке компиляции.
3.3.3.4.Побитовые операторы
Кпобитовым операторам относятся побитовые логические операторы конъюнкция (&), дизъюнкция (|), исключающая дизъюнкция (^) и дополнение (~), а также операторы побитового сдвига (<< и >>).
Синтаксис бинарных побитовых логических операторов:
<тип> operator op (<тип> x, <тип> y)
Данные операторы применимы к аргументам следующих типов: int, uint, long, ulong, bool и перечислений.
178
Синтаксис побитового дополнения:
<тип> operator ~ (<тип> x)
Допустимые типы – те же самые за исключением bool.
Также побитовая конъюнкция и дизъюнкция применимы к обнуляемому типу bool?. При этом, если один из операндов имеет неопределенное значение (null), правила вычисления результата таковы:
false && null = null && false = false true || null = null || true = true
Во всех остальных случаях результатом будет null. Синтаксис операторов сдвига:
<тип> operator op (<тип> x, int y)
Допустимые типы аргумента – int, uint, long и ulong, а величина сдвига всегда имеет тип int. Сдвиг влево на n позиций эквивалентен умножению целого числа на 2n, сдвиг вправо – целочисленному делению на 2n. При сдвиге вправо чисел со знаком бит знака сохраняется. Во всех остальных случаях освободившиеся позиции заполняются нулями.
Пример:
bool? v13 = null;
Console.WriteLine("null & false = " + (v13 & false));
Console.WriteLine("true | null = " + (true | v13));
Console.WriteLine("null & false = " + (v13 & null));
//0111 0111 0011 0101 1001 0100 0000 0000
v1 = 2000000000;
//0011 1011 1001 1010 1100 1010 0000 0000 Console.WriteLine(v1 >> 1);
//1110 1110 0110 1011 0010 1000 0000 0000 Console.WriteLine(v1 << 1);
//1000 1000 1100 1010 0110 1100 0000 0000
v1 = -2000000000;
//1100 0100 0110 0101 0011 0110 0000 0000 Console.WriteLine(v1 >> 1);
//0001 0001 1001 0100 1101 1000 0000 0000 Console.WriteLine(v1 << 1);
Вывод на консоль:
null & false = False true | null = True null & false = 1000000000 -294967296 -1000000000 294967296
В третьей строке результат null выводится на консоль в виде пустой
179
строки.
3.3.3.5. Операторы сравнения
Большинство операторов возвращает числовые значения. Что касается операторов сравнения, они генерируют булевский результат. Операторы сравнения анализируют соотношение между операндами и возвращают значение true, если соотношение истинно, false – если ложно.
К операторам сравнения относятся операторы: меньше (<), меньше или равно (<=), больше (>), больше или равно (>=), равно (==) и не равно (!=). Синтаксис:
bool operator op (<тип> x, <тип> y)
Допустимые типы – uint, int, long, ulong, float, double, decimal и пере-
числения. Также операторы «==» и «!=» определены для типов bool, object, string и делегатов, а для обнуляемых типов они допускают сравнение с null.
На операциях со строками мы еще остановимся в п. 3.3.4, с делегатами
– в § 4.8. Здесь лишь еще раз отметим, что при сравнении типов по значению сравнивается содержимое объектов, а при сравнении ссылочных типов – ссылки, хотя некоторые классы (типа System.String) эти операторы перегружают. Операторы «==» и «!=» используют метод Object.Equals, который для ссылочных типов эквивалентен методу Object.ReferenceEquals.
Пример:
int? v14 = null; int val1 = 1; int val2 = 1;
object obj1 = val1; object obj2 = val2;
Console.WriteLine("v14 == null? " + (v14 == null));
Console.WriteLine("v13 == v11? " + (v13 == v11));
Console.WriteLine("v13 < v11? " + (v13 < v11)); // Ошибка
Console.WriteLine("val1 == val2? " + (val1 == val2));
Console.WriteLine("obj1 == obj2? " + (obj1 == obj2));
Вывод на консоль:
v14 == null? True
v13 == v11? False val1 == val2? True obj1 == obj2? False
180