Операторы отношения и логические операторы. Переменные, операции, выражения

ГЛАВА 10. Выражения и операторы

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

Операторы

Оператор - это символ, указывающий операцию, выполняемую над одним или несколькими аргументами. При выполнении оператора получается результат. Синтаксис применения операторов несколько отличен от вызова методов, и формат выражений, содержащих операторы в С#, вы должны знать как свои пять пальцев. Как и в большинстве других языков, семантика операторов в С# соответствует правилам и нотациям, знакомым нам со школьной скамьи. Базовые операторы в С# включают умножение (*), деление (/), сложение и унарный плюс (+), вычитание и унарный минус (-), модуль (%) и присваивание (=).

Операторы служат для того, чтобы получить новое значение из значений, над которыми производится операция. Эти исходные значения называются операндами. Результат операции должен быть сохранен в памяти. Иногда он сохраняется в переменной, содержащей один из исходных операндов. Компилятор С# генерирует сообщение об ошибке, если при использовании оператора не определяется или не сохраняется новое значение. Приведенный ниже код значений не меняет. Компилятор выдаст сообщение об ошибке, так как арифметическое выражение, не приводящее к изменению хоть одного значения, обычно рассматривается как ошибочное.

class NoResultApp

{

public static void Main()

{

int i; int j;

i + j; // Ошибка, поскольку результат ничему не присваивается. } >

Большинство операторов работает только с числовыми типами данных, такими как Byte, Short, Long, Integer, Single, Double и Decimal. Исключение - операторы сравнения (== и!=). Кроме того, в С# можно применять операторы + и - для класса String и даже применять операторы инкремента (++) и (-) для таких необычных языковых конструкций как делегаты. О последних я расскажу в главе 14.

Старшинство операторов

Когда в одном выражении несколько операторов, компилятор должен определить порядок их выполнения. При этом компилятор руководствуется правилами, которые называются старшинством операторов. Понимание старшинства операторов нужно для правильного написания выражений - иногда результат может не соответствовать ожидаемому.

Рассмотрим выражение 42 + 6 * 10. Если сложить 42 и 6, а затем сумму умножить на 10, получится 480. Если же умножить 6 на 10 и к результату прибавить 42, получится 102. При компиляции кода специальный компонент компилятора - лексический анализатор - отвечает за порядок чтения этого кода. Именно лексический анализатор определяет относительное старшинство разнородных операторов в одном выражении. Для этого он использует некоторое значение - приоритет - каждого поддерживаемого оператора. Более приоритетные операторы разрешаются в первую очередь. В нашем примере оператор * имеет старшинство над оператором +, так как * поглощает (сейчас поясню этот термин) свои операнды до того, как это сделает +. Объяснение кроется в общих арифметических правилах: умножение и деление всегда имеют более высокий приоритет, чем сложение и вычитание. Вернемся к примеру: говорят, что число 6 поглощено оператором * и в 42 + 6 * 10 и в 42 * 6 + 10, так что эти выражения эквивалентны 42 + (6 * 10) и (42 * 6) + 10.

Как старшинство определяется в С#

Теперь посмотрим, как старшинство операторов определяется в С#. Ниже операторы перечислены в порядке убывания приоритета (табл. 10-1). Дальше я подробней расскажу о различных категориях операторов, поддерживаемых в С#.

Табл. 10-1. Старшинство операторов в С#.

Категория оператора Операторы
Простой (х), х.у, f(x), а[х], х++, х - , new, typeof, sizeof, checked, unchecked
Унарный + , -, !, ++x, - х, (Т)х
Мультипликативный *,/, %
Аддитивный +, -
Сдвиг «, »
Отношение <, >, <=, >=, is
Равенство ==
Логическое И (AND) &
Логическое исключающее ИЛИ (XOR) ^
Логическое ИЛИ (OR) 1
Условное И (AND) &&
Условное ИЛ И (OR) II
Условие 9-
Присваивание = *= /= % = , + = , -= « = , » = , &=, ^ = , =

Левая и правая ассоциативность

Ассоциативность определяет, какая часть выражения должна быть вычислена первой. Например, результатом приведенного выражения может быть 21 или 33 в зависимости от того, какая ассоциативность будет применяться для оператора «-»: левая или правая.

Оператор - имеет левую ассоциативность, т. е. сначала вычисляется 42-15, а затем из результата вычитается 6. Если бы он имел правую ассоциативность, сначала вычислялась бы правая часть выражения (15-6), а затем результат вычитался бы из 42.

Все бинарные операторы (операторы с двумя операндами), кроме операторов присваивания, - лево-ассоциативные, т. е. они обрабатывают выражения слева направо. Таким образом, а + Ь + с - то же, что и (а + Ь) + с, где сначала вычисляется а + Ъ, а затем к сумме прибавляется с. Операторы присваивания и условные операторы - право-ассоциативные, т. е. обрабатывают выражения справа налево. Иначе говоря, а=Ъ=с эквивалентно а = (Ь = с). Многие на этом спотыкаются, когда хотят поместить в одну строку несколько операторов присваивания, так что давайте рассмотрим такой код:

using System;

class RightAssocApp {

public static void Main() {

int a = 1; int b = 2; int с = 3;

Console.WriteLine("a={0} b={1} c={2>", a, b, c); a = b = c;

Console.WriteLine("После "a=b=c - : a={0} b={1} c={2}", a, b, c); > >

Результат выполнения этого примера таков:

а=1 ь=2 с=3

После "а=Ь=с": а=3 Ь=3 о=3

Поначалу вычисление выражений справа налево может сбивать с толку, но давайте подойдем к этому так: если бы оператор присваивания был лево-ассоциативным, компилятор сначала должен был бы вычислить а = Ь, после чего а было бы равно 2, а затем b = с и в результате b было бы равно 3. Конечный результат был бы а=2 Ь=3 с=3. Очевидно, что мы ожидаем не этого, когда пишем а = b = с, и именно поэтому операторы присваивания и условные операторы право-ассоциативные.

Практическое применение

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

а = b*c + d;

Такой подход в корне неверен: компилятор не может проводить корректный синтаксический анализ кода, если не определен конкретный синтаксис. Компилятор анализирует код согласно правилам, определенным разработчиками компилятора. С другой стороны, существуют круглые скобки, используемые для явного указания старшинства и ассоциативности. Например, выражение а = b * с + d можно переписать как а =(b * с) + d или как а = b * (с + d) и компилятор будет сначала вычислять выражение в скобках. Если есть несколько пар скобок, компилятор сначала вычислит выражения в скобках, а затем все выражение, основываясь на описанных правилах старшинства и ассоциативности.

Я твердо убежден, что следует всегда применять скобки, если в выражении несколько операторов. Рекомендую делать это, даже если вам понятен порядок вычислений, ведь люди, которые будут сопровождать ваш код, могут быть не такими грамотными.

Операторы С#

Правильней всего рассматривать операторы по старшинству. Ниже я опишу наиболее распространенные операторы.

Простые операторы

  • (х) Это разновидность оператора «скобки» для управления порядком вычислений как в математических операциях, так и при вызове методов.
  • х.у Оператор «точка» используется для указания члена класса или структуры. Здесь х представляет сущность, содержащую в себе член у.
  • f(x) Такая разновидность оператора «скобки» применяется для перечисления аргументов методов.
  • а[х] Квадратные скобки используются для индексации массива. Эти скобки также применяются совместно с индексаторами, когда объекты могут рассматриваться как массив. Об индексаторах см. главу 7.
  • х++ Об операторе инкремента мы поговорим отдельно в разделе «Операторы инкремента и декремента».
  • х- Оператор декремента мы тоже рассмотрим позднее.
  • new Этот оператор используется для создания экземпляров объектов на основании определения класса.

typeof

Отражение (reflection) - это способность получать информацию о типе в период исполнения. Эта информация включает имена типов, классов и элементы структур. В.NET Framework эта функциональность связана с классом System. Type. Этот класс - корень всех операторов отражения и может быть получен с помощью оператора typeof. Сейчас мы не будем вникать в подробности отражения (этим мы займемся в главе 16), но вот простой пример, иллюстрирующий простоту применения оператора typeof "для получения практически любой информации о типе или объекте во время исполнения программы:

using System;

using System.Reflection;

public class Apple {

public int nSeeds;

public void Ripen()

{

> >

public class TypeOfApp {

public static void Main() {

Type t = typeof(Apple);

string className = t.ToStringO;

Console.ИгШипе("\пИнформация 0 классе {О}", className);

Console.WriteLine("\nMeroflH {0}", className); Console. WriteLine("--------"); Methodlnfo methods = t.GetMethodsO;

foreach (Methodlnfo method in methods)

Console.WriteLine(method.ToSt ring());

}

Console.WriteLine("\nBce члены {О}", className); Console. Writel_ine("--------"); Memberlnfo allMembers = t.GetMembersO; foreach (Memberlnfo member in allMembers)

{

Console. WriteLine(member.ToStringO);

} > }

В этой программе содержится класс Apple, у которого всего два члена: поле nSeeds и метод Ripen. Сначала, используя оператор typeof и имя класса, я получаю объект System. Type, который затем сохраняется в переменной t. С этого момента я могу использовать объект System. Type для получения всех методов и членов класса Apple. Это делается с помощью методов GetMethods и GetMembers соответственно. Результаты выполнения этих методов выводятся на стандартное устройство вывода следующим образом:

Информация о классе Apple Методы Apple

Int32 GetHashCodeQ

System.String ToStringQ

Void RipenO

System.Type GetTypeO

Все члены Apple

Int32 nSeeds

Int32 GetHashCodeO

Boolean Equals(System.Object)

System.String ToStringO

Void RipenO

System.Type GetTypeO

Прежде чем двигаться дальше, хочу сделать два замечания. Во-первых, обратите внимание, что выводятся и унаследованные члены класса. Так как класс не порожден явно из другого класса, мы знаем, что все члены, не определенные в классе Apple наследуются от неявного базового класса System.Object. Во-вторых, объект System.Type можно получить методом GetType. Этот наследуемый от System.Object метод позволяет работать с объектами, а не с классами. Любой из двух приведенных далее фрагментов можно использовать для получения объекта System. Type.

II Получение объекта System.Type на основе определения класса. Type t1 = typeof(Apple);

// Получение объекта System.Type из объекта. Apple apple = new AppleQ; Type t2 = apple.GetTypeO;

sizeof

Оператор sizeof применяется для получения размера указанного типа в байтах. При этом помните о двух исключительно важных факторах. Во-первых, sizeof можно применять только к размерным типам. Следовательно, хотя его можно использовать для членов классов, для классов как таковых его применять нельзя. Во-вторых, sizeof можно применять только в методах или блоках кода, помеченных как unsafe. С кодом такого рода мы познакомимся в главе 17. Вот пример использования оператора sizeof в методе класса, помеченного как unsafe:

using System;

class BasicTypes {

// Примечание: Код, использующий оператор sizeof, // должен быть помечен как unsafe, static unsafe public void ShowSizesQ {

Console.WriteLine("\nPa3Mephi основных типов"); Console.WriteLine("Pa3Mep short = {0}", sizeof(short)); Console.WriteLine("Pa3Mep int = {0}", sizeof(int)); Console.Writel_ine("Pa3Mep long = {0}", sizeof(long)); Console.WriteLine("Pa3Mep bool = {0}", sizeof(bool)); } }

class UnsafeUpp

{

unsafe public static void MainQ

{

BasicTypes.ShowSizes();

} }

Вот результат выполнения этого приложения:

Размеры основных типов Размер short = 2 Размер int = 4 Размер long = 8 Размер bool = 1

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

// Использование оператора sizeof. using System;

struct StructWithNoMembers

struct StructWithMembers

{

short s;

int i;

long 1;

bool b; }

struct CompositeStruct

{

StructWithNoMembers a; StructWithMembers b;

StructWithNoMembers c; }

class UnSafe2App {

unsafe public static void Main() { Console.WriteLine("\nPa3Mep StructWithNoMembers structure = {0}",

sizeof(StructWithNoMembers)); Console.WriteLine("\nPa3Mep StructWithMembers structure = {0}",

sizeof(StructWithMembers)); Console.WriteLine("\nPa3Mep CompositeStruct structure = {0}",

sizeof(CompositeStruct)); } }

Хотя можно предположить, что это приложение выведет 0 для структуры без членов (StructWithNoMembers), 15 для структуры с четырьмя членами базовых типов (StructWithMembers) и 15 для структуры, агрегирующей две предыдущие (CompositeStruct), в действительности результат будет таким:

Размер StructWithNoMembers structure = 1 Размер StructWithMembers structure = 16

Размер CompositeStruct structure = 24

Объяснение этому - способ сохранения конструкции struct компилятором в выходном файле, при котором компилятор применяет выравнивание и дополнение пробелами. Например, если структура имеет размер 3 байта и установлено выравнивание по 4-байтным границам, компилятор автоматически добавит в структуру 1 байт, и оператор sizeof укажет, что размер структуры - 4 байта. Не забывайте это учитывать при определении размера структур в С#.

checked и unchecked

Эти два оператора управляют проверкой переполнения при выполнении математических операций.

Математические операторы

С#, как и большинство других языков, поддерживает основные математические операторы: умножение (*), деление (/), сложение (+), вычитание (-) и модуль (%). Назначение первых четырех операторов понятно из их названий; оператор модуля формирует остаток от целочисленного деления. Вот код, иллюстрирующий применение математических операторов:

using System;

class MathOpsApp

{

public static void MainQ

{

// Класс System.Random является частью библиотеки классов // .NET Framework. В его конструкторе по умолчанию // метод Next использует текущую дату/время в качестве // начального значения. Random rand = new RandomO; int a, b, c;

a = rand.Next() % 100; // Предельное значение 99. b = rand.NextO % 100; // Предельное значение 99.

Console.WriteLine("a={0} b={1}", a, b);

с = a * b;

Console.WriteLineC"a * b = {0}", c);

// Заметьте, что здесь используются целые числа. // Следовательно, если а меньше Ь, результат всегда // будет 0. Для получения более точного результата // нужно применять переменные типа double или float, с = а / b; Console.WriteLineC"a / b = {0}", с);

Console.WriteLineC"a + b = {0}", c);

Console.WriteLineC"a - b = {0}", c);

Console.WriteLineC"a X b = {0}", c); > >

Унарные операторы

Унарных операторов два: плюс и минус. Оператор унарного минуса указывает компилятору, что число отрицательное. Таким образом, в следующем коде а будет равно -42:

using System; using System;

class UnarylApp {

public static void Main()

{

int a = 0;

a = -42;

Console.WriteLine("{0}", a); } }

Однако в этом коде появляется неопределенность: using System;

class Unary2App <

public static void Main() {

int a; int b = 2; int с = 42;

a = b * -с;

Console.WriteLine("{0}", a); > >

Выражение a = b * -с не совсем понятно. Снова повторю, что использование скобок прояснит это выражение:

// При использовании скобок очевидно, что мы // умножаем b на отрицательное число с. а = b * (-с);

Если унарный минус возвращает отрицательное значение операнда, можно подумать, что унарный плюс возвращает положительное. Однако унарный плюс лишь возвращает операнд в его первоначальной форме и больше ничего не делает, т. е. не влияет на операнд. Например, выполнение этого кода приведет к выводу значения -84:

using System;

class UnarySApp {

public static void MainQ {

a = b * (+c);

Console.WriteLine("{0}", a); } }

Для получения положительного значения служит функция Math.Abs. Этот код выведет значение 84:

using System;

class Unary4App

{

public static void Main()

int a; int b = 2; int с = -42;

a = b * Math.Abs(c); Console.Writel_ine("{0}", a); } }

Последний унарный оператор, который я упоминал, - это Т(х). Это разновидность оператора «скобки», позволяющая приводить один тип к другому. Поскольку его можно перегрузить посредством создания пользовательского преобразования, мы обсудим его в главе 13.

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

Составной оператор присваивания - это комбинация бинарного оператора и оператора присваивания (=). Синтаксис этих операторов таков:

хор=у

где ор - это оператор. Заметьте, что при этом левое значение (lvalue) не заменяется правым (rvalue), составной оператор оказывает такой же эффект, как:

х = х ор у

и lvalue используется как база для результата операции.

Заметьте, я сказал «оказывает такой же эффект». Компилятор не переводит выражение наподобие х += 5 в х = х + 5, он действует логически. С особым вниманием нужно отнестись к случаям, когда lvalue является методом. Рассмотрим код:

using System;

class CompoundAssignmentlApp {

protected lnt elements;

public int GetArrayElementO

{

return elements;

}

CompoundAssignment1App() {

elements = new int;

elements = 42;

}

public static void Main() {

CompoundAssignmentlApp app = new CompoundAsslgnment1App();

Console.WrlteLine("{0>", app.GetArrayElement());

app.GetArrayElement() = app.GetArrayElement() + 5; Console.WriteLine("{0}", app.GetArrayElement()); }. }

Обратите внимание на выделенную строку - вызов метода Compound-AssignmentlApp.GetArrayElement и последующее изменение первого элемента - здесь я использовал синтаксис:

х = х ор у

Вот какой MSIL-код будет сгенерирован: // Неэффективная методика: х = х ор у.

Method public hldebyslg static void Main() 11 managed

Entrypolnt

// Размер кода 79 (Ox4f) .maxstack 4

Locals (class CompoundAssignmentlApp V_0)

IL_0000: newobj instance void CompoundAssignmentlApp::.ctor() IL_0005: stloc.O IL_0006: Idstr "{ОГ IL_OOOb: ldloc.0 ILJJOOc: call instance int32

CompoundAssignmentlApp:: GetArrayElementO

IL_0011: ldc.14.0

IL_0012: Idelema ["mscorlib"]System.Int32

IL_0017: box [ 1 mscorlib"]System.Int32

IL_001c: call void ["mscorlib 1 ]System.Console::WriteLine (class System.String, class System.Object)

IL_0021: ldloc.0

IL_0022: call instance int32 CompoundAssignmentlApp::GetArrayElementO

IL_0027: Idc.i4.0

IL_0028: ldloc.0

IL_0029: call instance int32 CompoundAssignmentlApp: :GetArrayElementO

IL_002e: Idc.i4.0

IL_002f: ldelem.14

IL_0030: ldc.14.5

IL_0031: add

IL_0032: stelem.14

IL_0033: Idstr "{0}"

IL_0038: ldloc.0

IL_0039: call

instance int32 CompoundAssignmentlApp::GetArrayElement() IL_003e: ldc.14.0

IL_003f: Idelema ["mscorlib"]Systera.Int32 IL_0044: box ["msoorlib"]System.Int32 IL_0049: call void ["mscorlib"]System.Console::WriteLine

(class System.String, class System.Object) IL_004e: ret

} // конец метода "CompoundAssignmentlApp::Main" !

Посмотрите на вьщеленные строки: метод CompoundAssignmentlApp.Get-ArrayElement на самом деле вызывается дважды! Это по меньшей мере неэффективно, а возможно, и пагубно в зависимости от того, что еще делает этот метод.

Теперь рассмотрим другой код, в котором применен синтаксис составного оператора присваивания:

using System;

class CompoundAssignment2App {

protected int elements;

public int GetArrayElementO

return elements;

}

CompoundAssignment2App() {

elements = new int;

elements = 42;

}

public static void Main() {

CompoundAssignment2App app = new CompoundAssignment2App();

Console.WriteLine("{0}", app.GetArrayElement());

app.GetArrayElement() += 5; Console.WriteLine("{0}", app.GetArrayElement()); } }

Использование составного оператора присваивания приведет к созданию гораздо более эффективного MSIL-кода:

// Более эффективная методика: х ор= у.

Method public hidebysig static void Main() il managed

\ {

\ .entrypoint

I // Размер кода 76 (Ox4c) \ .maxstack 4

Locals (class CompoundAssignmentlApp V_0, int32 V_1) \ IL_0000: newobj instance void CompoundAssignmentlApp::.ctor() \ IL_0005: stloc.O 1 IL_0006: Idstr "{0}" 1 IL_OOOb: ldloc.0 IL_OOOc: call instance int32

CompoundAssignmentlApp:: GetArrayElementO IL_0011: Idc.i4.0

IL_0012: Idelema [ mscorlib"]System.Int32 IL_0017: box [ > mscorlib - ]System.Int32 lL_001c: call void ["mscorlib"]System.Console::WriteLine

(class System.String, class System.Object) IL_0021: ldloc.0 IL_0022: call instance int32

CompoundAssignmentlApp::GetArrayElement()

IL_0027: dup

IL_0028: stloc.1

IL_0029: Idc.i4.0

IL_002a: ldloc.1

IL_002b: Idc.i4.0

IL_002c: ldelem.14

IL_002d: ldc.14.5

IL_002e: add

IL_002f: stelem.14

IL_0030: Idstr "{0}"

IL_0035: ldloc.0

IL_0036: call instance int32

CompoundAssignmentlApp:: GetArrayElementO IL_003b: Idc.i4.0

IL_003c: Idelema ["mscorlib"]System.Int32

IL_0041: box [ mscorlib"]System.Int32

IL_0046: call void ["mscorlib"]System.Console::WriteLine

(class System.String, class System.Object)

IL_004b: ret } // конец метода "CompoundAssignmentlApp::Main"

Вы видите, что использована команда MSIL dup. Она дублирует верхний элемент в стеке, создавая таким образом копию значения, возвра-щенного методом CompoundAssignmentlApp.Get Array Element. I

Из этого видно, что хотя по сути х +=у эквивалентно х = х + у, MSIL- код в обоих случаях разный. Это отличие должно заставить вас задуматься, какой синтаксис использовать в каждом отдельном случае. Эмпирическое правило и моя рекомендация: всегда и везде, где можно, применять составные операторы присваивания.

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

Появившиеся в языке С и перенесенные в C++ и Java операторы инк 1 -ремента и декремента позволяют лаконично выразить, что вы хотите увеличить или уменьшить числовое значение на 1. То есть /++ равносильно добавлению 1 к текущему значению /".

Наличие двух форм операторов инкремента и декремента иногда приводит к путанице. Префиксный и постфиксный типы этих операторов отличаются тем, в какой момент производится изменение значения. В префиксной версии операторов инкремента и декремента (++аи - а соответственно) сначала выполняется операция, а затем создается значение. В постфиксной версии (а++ и а-) сначала создается значение, а затем выполняется операция. Рассмотрим пример:

using System;

class IncDecApp {

public static void Foo(int j)

{

Console.WriteLine("IncDecApp.Foo j = {0}", j);

>

public static void Main() {

int i = 1;

Console.WriteLineC"flo обращения к Foo(i++) = {0}", i);

Console.WriteLine("После обращения к Foo(i++) = {0}", i);

Console.WriteLine("\n");

\ Console.WriteLineC"flo обращения к Foo(++i) = {0}", i);

\ Foo(++l);

\ Console.WrlteLine("После обращения к Foo(++i) = {0}", i);

l Результат выполнения будет таким:

До обращения к Foo(i++) = 1

IncDecApp.Foo j = 1

После обращения к Foo(i++) = 2

До обращения к Foo(-n-i) = 2

IncDecApp.Foo j = 3

После обращения к Foo(++i) = 3

Разница в том, когда создается значение и модифицируется операнд. При вызове Foo(i++) значение /" передается (без изменений) в метод Foo и после возврата из метода / увеличивается на 1. Посмотрите на приведенный MSIL-код: команда add выполняется после помещения значения в стек.

IL.0013: ldloc.0

IL.0014: dup

IL_0015: Idc.i4.1

IL_0016: add

IL_0017: stloc.O

IL_0018: call void IncDecApp::Foo(int32)

Теперь посмотрим на префиксную форму оператора, используемую в вызове Foo(++a). В этом случае MSIL-код будет другой. При этом команда add выполняется до того, как значение помещается в стек для последующего вызова метода Foo.

IL.0049: ldloc.0

IL_004a: Idc.i4.1

IL_004b: add

IL_004c: dup

IL_004d: stloc.O

IL_004e: call void IncDecApp::Foo(int32)

Операторы отношения

Большинство операторов возвращает числовые значения. Что касается операторов отношения, они генерируют булевский результат. Вместо / того чтобы выполнять математические операции с набором операндов, / операторы отношения анализируют соотношение между операндами и возвращают значение true, если соотношение истинно, false - если/ ложно.

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

К операторам отношения, называемым операторами сравнения, относят)-ся «меньше» (<), «меньше или равно» (<=), «больше» (>), «больше или равно» (>=), «равно» (==) и «не равно» (!=). Применение этих операторов к числам понятно, но при использовании с объектами их выполнение не так очевидно. Вот пример:

using System;

class NumericTest {

public NumericTest(int 1)

{

this.i = i;

>

protected int 1; }

class RelationalOpslApp {

public static void Main() {

NumericTest testl = new NumericTest(42); NumericTest test2 = new NumericTest(42);

Console.WriteLine("{0}", testl == test2); > }

Если вы программируете на Java, вы знаете что здесь должно произойти. Однако С++-программисты будут скорей всего удивлены, увидев результат false. Напомню: создавая экземпляр объекта, вы получаете ссылку на него. Значит, встречая оператор отношения, сравнивающий два объекта, компилятор С# сравнивает не содержимое объектов, а их адреса. Чтобы лучше в этом разобраться, рассмотрим MSIL-код:

\ .method public hldebysig static void MainQ il managed

\ .entrypoint

\ // Размер кода 39 (0x27)

1 .maxstack 3

Locals (class NumericTest V_0, \ class NumericTest V_1, 1 bool V_2)

ILJJOOO: Idc.i4.s 42

1L_0002: newobj instance void NumericTest::.ctor(int32)

IL_0007: stloc.O

IL_0008: Idc.i4.s 42

IL_OOOa: newobj instance void NumericTest::.ctor(int32)

IL_OOOf: stloc.1

IL_0010: Idstr "{0}"

IL_0015: ldloc.0

IL_0016: ldloc.1

IL_0017: eeq

IL_0019: stloc.2

IL_001a: Idloca.s V_2

IL_001c: box ["mscorlib"]System.Boolean

IL_0021: call void ["mscorlib"]System.Console::WriteLine

(class System.String,class System.Object)

IL_0026: ret } // конец метода "RelationalOpslApp::Main"

Посмотрите на строку .locals. Компилятор указывает, что у метода Main три локальных переменных. Первые две - объекты NumericTest, a третья - переменная булевского типа. Теперь перейдем к строкам IL_0002 и IL_0007. Здесь создается экземпляр объекта testl, и ссылка на него с помощью stloc сохраняется в первой локальной переменной. При этом важно, что MSIL сохраняет адрес вновь созданного объекта. Затем в строках IL_OOOa и IL_OOOf вы видите коды MSIL для создания объекта test2 и сохранения возвращаемой ссылки во второй локальной переменной. Наконец, в строках 1LJ)015 и IL_0016 локальные переменные помещаются в стек командой Idloc, а в строке IL_0017 команда сед сравнивает два значения в вершине стека (т. е. ссылки на объекты testl и testl). Возвращаемое значение сохраняется в третьей локальной переменной и далее выводится методом System.Console. WriteLine.

Но как же сравнивать члены двух объектов? Ответ - в использовании неявного базового класса всех объектов.NET Framework. Именно, для этих целей у класса System.Object есть метод Equals. Например, еле- / дующий код выполняет сравнение содержимого объектов и выводит, как / и следовало ожидать, true: I

using System; /

class RelationalOps2App

{ / public static void Main() {

Console.WriteLine("{0}", testl.Equals(test2));

В примере RelationalOpslApp используется «самодельный» класс (Nu-mericTest), а во втором примере - .NET-класс (Decimal). Дело в том, что метод System.Object.Equals нужно переопределить, чтобы выполнить реальное сравнение членов. Следовательно, метод Equals с классом Nume-ricTest работать не будет, так как мы не переопределили метод. А вот класс Decimal переопределяет наследуемый им метод Equals, и в этом случае все будет работать.

Другой способ сравнения объектов - использование перегрузки операторов (operator overloading). Перегрузка операторов определяет операции, выполняемые над объектами конкретного типа. Например, для объектов string оператор + не выполняет сложение, а конкатенирует строки. Перегрузку операторов мы рассмотрим в главе 13.

Простые операторы присваивания

Значение в левой части оператора присваивания называется lvalue, а в правой части - rvalue. В качестве rvalue может быть любая константа, переменная, число или выражение, результат которого совместим с lvalue. Между тем lvalue должно быть переменной определенного типа. Дело в том, что значение копируется из правой части в левую. Таким образом, для нового значения должно быть выделено физическое адресное пространство. Например, можно написать /" = 4, поскольку для / есть место в памяти - в стеке или в куче - в зависимости от типа переменной /. А вот оператор 4 = 1 выполнить нельзя, так как 4 - это значение, а не переменная, содержимое которой в памяти можно изменить. Замечу кстати, что в С# в качестве lvalue может быть переменная, свойство или индексатор. Подробнее о свойствах и индексаторах см. главу 7. В этой главе я для простоты использую переменные. Если с присваиванием числовых значений все достаточно понятно, с объектами дело сложнее. Напомню, что когда вы имеете дело с объектами, вы манипулируете не элементами стека, которые легко копировать и перемещать. В случае объектов у вас на самом деле есть лишь ссылки на некоторые сущности, для которых динамически выделена память. Следовательно, когда вы пытаетесь присвоить переменной объект (или любой ссылочный тип) копируются не данные, как это происходит в случае размерных типов, а ссылки.

Скажем, у вас два объекта: testl и test2. Если вы укажете testl = test2, testl не будет копией test2. Они будут совпадать! Объект testl указывает на ту же память, что и test2, и любые изменения объекта testl приведут к изменениям test2. Вот программа, которая это иллюстрирует:

using System;

class Foo {

public int i; }

class RefTestlApp {

public static void MainO {

Foo testl = new Foo(); testl.i = 1;

Foo test2 = new Foo(); test2.i = 2;

Console.WriteLine("До назначения объектов"); Console.WriteLine("test1.i={0>", testl.i); Console.WriteLine("test2.i={0}", test2.i); Console.WriteLine("\n");

testl = test2;

Console.Writel_ine("После назначения объектов");

Console.WriteLine("test1.i={0}", testl.i); Console.WriteLine("test2.i={0}", test2.i); Console.WriteLine("\n");

testl.i = 42; ;"

Console.WriteLine("Пocлe изменения только члена TEST1"); Console.WriteLine("test1.i={0}", testl.i); Console.WriteLine("test2.i={0}", test2.i); Console.WriteLine("\n"); } }

Выполнив этот код, вы увидите:

До назначения объекта

test1.i=1

test2.i=2

После назначения объекта

testt.i=2

test2.i=2

После изменения только члена TEST1

test1.i=42

test2.i=42

Посмотрим, что происходит на каждом этапе выполнения этого примера. Foo - это простой к класс с единственным членом, /. В методе Main создаются два экземпляра этого класса: testl и test2 - и их члены i устанавливаются в 1 и 2 соответственно. Затем эти значения выводятся, и, как и ожидалось, testl.i равен 1, a test2.i - 2. И тут начинается самое интересное! В следующей строке объекту testl присваивается test2. Читатели, программирующие на Java, знают, что будет дальше. Однако большинство программистов на C++ будут ожидать, что член / объекта testl теперь равен члену объекта test2 (если исходить из предположения, что при компиляции такого приложения будет выполнена некая разновидность оператора копирования членов объектов). Выводимый результат это вроде подтверждает. Однако на самом деле связь между объектами теперь гораздо глубже. Присвоим значение 42 члену testl.i и снова выведем результат. И?! При изменении объекта testl изменился и testZ Это произошло из-за того, что объекта testl больше нет. После присваивания ему test2 объект testl утерян, так как приложение на него больше не ссылается и в результате он «вычищается» сборщиком мусора (garbage collector, GC). Теперь testl и test2 указывают на одну и ту же память в куче. Следовательно, при изменении одной переменной пользователь увидит изменение и другой.

Обратите внимание на две последние выводимые строки: хотя в коде изменялось только значение testl.i, значение test2.i также изменилось. Еще раз: обе переменные теперь указывают на одно место в памяти - такое поведение и ожидали программисты на Java. Однако это совершенно не соответствует ожиданиям разработчиков на C++, поскольку в этом языке производится именно копирование объектов: каждая переменная имеет свою уникальную копию членов и изменения одного объекта не влияют на другой. Поскольку это ключ к пониманию работы объектов в С#, сделаем небольшое отступление и посмотрим, что будет происходить при передаче объекта методу:

using System;

class Foo {

public int i; }

class RefTest2App {

public void ChangeValue(Foo f)

{

f.i = 42;

}

public static void Main() {

RefTest2App app = new RefTest2App();

Foo test = new Foo(); test.i = 6;

Console.WriteLine("До вызова метода"); Console.WriteLine("test.i={0}", test.i); Console.WriteLine("\n");

app.ChangeValue(test);

Console.WriteLine("После вызова метода"); Console.WriteLine("test.i={0}", test.i); Console.WriteLine("\n"); > }

В большинстве языков, кроме Java, этот код будет копировать созданный объект test в локальный стек метода RefTest2App.ChangeValue. В таком случае объект test, созданный в методе Main, никогда не увидит изменений объекта/, производимых в методе ChangeValue. Однако еще раз повторю, что метод Main передает ссылку на выделенный в куче объект test. Когда метод ChangeValue манипулирует своей локальной переменной //, он так же напрямую манипулирует объектом test метода Main.

Подведем итоги

Главное в любом языке программирования - способ выполнения присваивания, математических, логических операций и операций отношения - всего, что требуется для работы реальных приложений. В коде эти операции представлены операторами. К факторам, влияющим на выполнение операторов, относятся старшинство и ассоциативность (правая и левая) операторов. Мощный набор предопределенных операторов в С# можно расширять реализациями, определенными пользователем, о чем мы поговорим в главе 13.

Последнее обновление: 19.06.2017

Отдельный набор операций представляет условные выражения. Такие операции возвращают логическое значение, то есть значение типа bool : true , если выражение истинно, и false , если выражение ложно. К подобным операциям относятся операции сравнения и логические операции.

Операции сравнения

В операциях сравнения сравниваются два операнда и возвращается значение типа bool - true , если выражение верно, и false , если выражение неверно.

    Сравнивает два операнда на равенство. Если они равны, то операция возвращает true , если не равны, то возвращается false :

    B; // false

    Сравнивает два операнда и возвращает true, если операнды не равны, и false, если они равны.

    Int a = 10; int b = 4; bool c = a != b; // true bool d = a!=10; // false

    Операция "меньше чем". Возвращает true, если первый операнд меньше второго, и false, если первый операнд больше второго:

    Int a = 10; int b = 4; bool c = a < b; // false

    Операция "больше чем". Сравнивает два операнда и возвращает true, если первый операнд больше второго, иначе возвращает false:

    Int a = 10; int b = 4; bool c = a > b; // true bool d = a > 25; // false

    Операция "меньше или равно". Сравнивает два операнда и возвращает true, если первый операнд меньше или равен второму. Иначе возвращает false.

    Int a = 10; int b = 4; bool c = a <= b; // false bool d = a <= 25; // true

    Операция "больше или равно". Сравнивает два операнда и возвращает true, если первый операнд больше или равен второму, иначе возвращается false:

    Int a = 10; int b = 4; bool c = a >= b; // true bool d = a >= 25; // false

Операции <, > <=, >= имеют больший приоритет, чем == и!=.

Логические операции

Также в C# определены логические операторы, которые также возвращают значение типа bool . В качестве операндов они принимают значения типа bool . Как правило, применяются к отношениям и объединяют несколько операций сравнения.

    Операция логического сложения или логическое ИЛИ. Возвращает true, если хотя бы один из операндов возвращает true.

    Bool x1 = (5 > 6) | (4 < 6); // 5 > 6 - false, 4 < 6 - true, поэтому возвращается true bool x2 = (5 > 6) | (4 > 6); // 5 > 6 - false, 4 >

    Операция логического умножения или логическое И. Возвращает true, если оба операнда одновременно равны true.

    Bool x1 = (5 > 6) & (4 < 6); // 5 > 6 - false, 4 < 6 - true, поэтому возвращается false bool x2 = (5 < 6) & (4 < 6); // 5 < 6 - true, 4 < 6 - true, поэтому возвращается true

    Операция логического сложения. Возвращает true, если хотя бы один из операндов возвращает true.

    Bool x1 = (5 > 6) || (4 < 6); // 5 > 6 - false, 4 < 6 - true, поэтому возвращается true bool x2 = (5 > 6) || (4 > 6); // 5 > 6 - false, 4 > 6 - false, поэтому возвращается false

    Операция логического умножения. Возвращает true, если оба операнда одновременно равны true.

    Bool x1 = (5 > 6) && (4 < 6); // 5 > 6 - false, 4 < 6 - true, поэтому возвращается false bool x2 = (5 < 6) && (4 < 6); // 5 < 6 - true, 4 > 6 - true, поэтому возвращается true

    Операция логического отрицания. Производится над одним операндом и возвращает true, если операнд равен false. Если операнд равен true, то операция возвращает false:

    Bool a = true; bool b = !a; // false

    Операция исключающего ИЛИ. Возвращает true, если либо первый, либо второй операнд (но не одновременно) равны true, иначе возвращает false

    Bool x5 = (5 > 6) ^ (4 < 6); // 5 > 6 - false, 4 < 6 - true, поэтому возвращается true bool x6 = (50 > 6) ^ (4 / 2 < 3); // 50 > 6 - true, 4/2 < 3 - true, поэтому возвращается false

Здесь у нас две пары операций | и || (а также & и &&) выполняют похожие действия, однако же они не равнозначны.

В выражении z=x|y; будут вычисляться оба значения - x и y.

В выражении же z=x||y; сначала будет вычисляться значение x, и если оно равно true , то вычисление значения y уже смысла не имеет, так как у нас в любом случае уже z будет равно true . Значение y будет вычисляться только в том случае, если x равно false

То же самое касается пары операций &/&& . В выражении z=x&y; будут вычисляться оба значения - x и y.

В выражении же z=x&&y; сначала будет вычисляться значение x, и если оно равно false , то вычисление значения y уже смысла не имеет, так как у нас в любом случае уже z будет равно false . Значение y будет вычисляться только в том случае, если x равно true

Поэтому операции || и && более удобны в вычислениях, так как позволяют сократить время на вычисление значения выражения, и тем самым повышают производительность. А операции | и & больше подходят для выполнения поразрядных операций над числами.

Операция && имеет больший приоритет, чем операция ||. Так, в выражении true || true && false сначала выполняется подвыражение true && false .

В языке C# неявных преобразований к логическому типу нет даже для целых арифметических типов. Поэтому вполне корректная в языке C++ запись:

int k 1 = 7;
if (k 1) Console . WriteLine (" ok !");

незаконна в программах на C#. На этапе трансляции возникнет ошибка, поскольку вычисляемое условие имеет типint , а неявное преобразование этого типа к типуbool отсутствует.

В языке C# более строгие правила действуют и для логических операций. Так, записьif (k 1 && (x > y )), корректная в языке C++, приводит к ошибке в

программах на C#, поскольку операция && определена только для операндов типаbool , а в данном выражении один из операндов имеет типint . В языке C# в данных ситуациях следует использовать записи:

if (k 1>0)
if ((k 1>0) && (x > y ))

Логические операции делятся на две категории: одни выполняются над логическими значениями операндов, другие осуществляют выполнение логической операции над битами операндов. По этой причине в C# существуют две унарные операции отрицания - логическое отрицание, заданное операцией «!», и побитовое отрицание, заданное операцией «~». Первая из них определена над операндом типаbool , вторая - над операндом целочисленного типа, начиная с типаint и выше(int , uint , long , ulong ). Результатом операции во втором случае является операнд, в котором каждый бит заменен его дополнением. Приведем пример:

/// < summary >
/// Логические выражения
/// summary >
public void Logic () {
//операции отрицания ~, !
bool b1, b2;
b1 = 2*2 == 4;
b2 = !b1;
//b2= ~b1;
uint j1 = 7, j2;
j2 = ~j1;
//j2 = !j1;
int j4 = 7, j5;
j5 = ~j4;
Console.WriteLine("uint j2 = " + j2 + " int j5 = " + j5);
} // Logic

В этом фрагменте закомментированы операторы, приводящие к ошибкам. В первом случае была сделана попытка применения операции побитового отрицания к выражению типаbool , во втором - логическое отрицание применялось к целочисленным данным. И то, и другое в C# незаконно. Обратите внимание на разную интерпретацию побитового отрицания для беззнаковых и знаковых целочисленных типов. Для переменныхj 5 иj 2 строка битов, задающая значение - одна и та же, но интерпретируется по-разному. Соответствующий вывод таков:

uint j 2 = 4294967288
int j 5 = -8.

Бинарные логические операции «&& - условное И» и «|| - условное ИЛИ» определены только над данными типаbool . Операции называются условными или краткими, поскольку, вычисление второго операнда зависит от уже вычисленного значения первого операнда. Ценность условных логических операций заключается в их эффективности по времени выполнения. Часто они позволяют вычислить логическое выражение, имеющее смысл, но в котором второй операнд не определен. Приведем в качестве примера классическую задачу поиска по образцу в массиве, когда разыскивается элемент с заданным значением (образец). Такой элемент в массиве может быть, а может и не быть. Вот типичное решение этой задачи в упрощенном виде, но передающем суть дела:

// Условное And - &&
int[ ] ar = { 1, 2, 3 };
int search = 7;
int i = 0;
while ((i < ar.Length) && (ar[i] != search)) {
i++;
}
if (i < ar.Length) Console.WriteLine(" Образец найден ");
else Console.WriteLine(" Образец не найден ");

Если значение переменнойsearch (образца) не совпадает ни с одним из значений элементов массиваar , то последняя проверка условия циклаwhile будет выполняться при значенииi , равномar . Length . В этом случае первый операнд получит значениеfalse , и, хотя второй операнд при этом не определен, цикл нормально завершит свою работу. Второй операнд не определен в последней проверке, поскольку индекс элемента массива выходит за допустимые пределы (в C# индексация элементов начинается с нуля).

Три бинарные побитовые операции - «& - AND» , « | - OR», «^ - XOR» используются двояко. Они определены как над целыми типами вышеint , так и над булевыми типами. В первом случае они используются как побитовые операции, во втором - как обычные логические операции. Иногда необходимо, чтобы оба операнда вычислялись в любом случае, тогда без этих операций не обойтись. Вот пример первого их использования:

//Логические побитовые операции And , Or , XOR (&,|,^)
int k2 = 7, k3 = 5, k4, k5, k6;
k4 = k2 & k3;
k5 = k2 | k3;
k6 = k2 ^ k3;
Console.WriteLine("k4 = " + k4 + " k5 = " + k5 + " k6 = " + k6);

Результаты вывода:

k 4 = 5 k 5 = 7 k 6 =2

Приведем пример поиска по образцу с использованием логического AND: i = 0;

search = ar;
while ((i < ar.Length) & (ar[i] != search)) i++;
if (i < ar.Length) Console.WriteLine(" Образец найден ");
else c Console.WriteLine(" Образец не найден ");

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

Последнее обновление: 19.06.2017

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

Бинарные арифметические операции:

    Операция сложения двух чисел:

    Int x = 10; int z = x + 12; // 22

    Операция вычитания двух чисел:

    Int x = 10; int z = x - 6; // 4

    Операция умножения двух чисел:

    Int x = 10; int z = x * 5; // 50

    операция деления двух чисел:

    Int x = 10; int z = x / 5; // 2 double a = 10; double b = 3; double c = a / b; // 3.33333333

    При делении стоит учитывать, что если оба операнда представляют целые числа, то результат также будет округляться до целого числа:

    Double z = 10 / 4; //результат равен 2

    Несмотря на то, что результат операции в итоге помещается в переменную типа double, которая позволяет сохранить дробную часть, но в самой опеации участвуют два литерала, которые по умолчанию рассматриваются как объекты int, то есть целые числа, и результат то же будет целочисленный.

    Для выхода из этой ситуации необходимо определять литералы или переменные, участвующие в операции, именно как типы double или float:

    Double z = 10.0 / 4.0; //результат равен 2.5

    Операция получение остатка от целочисленного деления двух чисел:

    Double x = 10.0; double z = x % 4.0; //результат равен 2

Также есть ряд унарных операций, в которых принимает участие один операнд:

    Операция инкремента

    Инкремент бывает префиксным: ++x - сначала значение переменной x увеличивается на 1, а потом ее значение возвращается в качестве результата операции.

    И также существует постфиксный инкремент: x++ - сначала значение переменной x возвращается в качестве результата операции, а затем к нему прибавляется 1.

int x1 = 5; int z1 = ++x1; // z1=6; x1=6 Console.WriteLine($"{x1} - {z1}"); int x2 = 5; int z2 = x2++; // z2=5; x2=6 Console.WriteLine($"{x2} - {z2}");

Операция декремента или уменьшения значения на единицу. Также существует префиксная форма декремента (--x) и постфиксная (x--).

Int x1 = 5; int z1 = --x1; // z1=4; x1=4 Console.WriteLine($"{x1} - {z1}"); int x2 = 5; int z2 = x2--; // z2=5; x2=4 Console.WriteLine($"{x2} - {z2}");

При выполнении сразу нескольких арифметических операций следует учитывать порядок их выполнения. Приоритет операций от наивысшего к низшему:

    Инкремент, декремент

    Умножение, деление, получение остатка

    Сложение, вычитание

Для изменения порядка следования операций применяются скобки.

Рассмотрим набор операций:

Int a = 3; int b = 5; int c = 40; int d = c---b*a; // a=3 b=5 c=39 d=25 Console.WriteLine($"a={a} b={b} c={c} d={d}");

Здесь мы имеем дело с тремя операциями: декремент, вычитание и умножение. Сначала выполняется декремент переменной c, затем умножение b*a, и в конце вычитание. То есть фактически набор операций выглядел так:

Int d = (c--)-(b*a);

Но с помощью скобок мы могли бы изменить порядок операций, например, следующим образом:

Int a = 3; int b = 5; int c = 40; int d = (c-(--b))*a; // a=3 b=4 c=40 d=108 Console.WriteLine($"a={a} b={b} c={c} d={d}");

Ассоциативность операторов

Как выше было отмечено, операции умножения и деления имеют один и тот же приоритет, но какой тогда результат будет в выражении:

Int x = 10 / 5 * 2;

Стоит нам трактовать это выражение как (10 / 5) * 2 или как 10 / (5 * 2) ? Ведь в зависимости от трактовки мы получим разные результаты.

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

    Левоассоциативные операторы, которые выполняются слева направо

    Правоассоциативные операторы, которые выполняются справа налево

Все арифметические операторы (кроме префиксного инкремента и декремента) являются левоассоциативными, то есть выполняются слева направо. Поэтому выражение 10 / 5 * 2 необходимо трактовать как (10 / 5) * 2 , то есть результатом будет 4.

В этом разделе кратко описаны синтаксис и применение всех операций C#, кроме некоторых первичных, которые рассматриваются позже при изучении соответствующего материала.

Инкремент и декремент

Операции инкремента (++) и декремента (--) увеличивают и уменьшают операнд на единицу. Они имеют две формы записи - префиксную , когда знак операции записывается перед операндом, и постфиксную . В префиксной форме сначала изменяется операнд, а затем его значение становится результирующим значением выражения, а в постфиксной форме значением выражения является исходное значение операнда, после чего он изменяется.

Стандартные операции инкремента существуют для целых, символьных, вещественных и финансовых величин.

Операция new

Операция new служит для создания нового объекта. Формат операции:

new тип ([ аргументы ])

С помощью этой операции можно создавать объекты как ссылочных, так и значимых типов, например:

object z = new object(); int i = new int(); // то же самое, что int i = 0;

При выполнении операции new сначала выделяется необходимый объем памяти (для ссылочных типов в хипе, для значимых - в стеке), а затем вызывается так называемый конструктор по умолчанию , то есть метод, с помощью которого инициализируется объект. Переменной значимого типа присваивается значение по умолчанию , которое равно нулю соответствующего типа.

Операции отрицания

Арифметическое отрицание (унарный минус – ) меняет знак операнда на противоположный. Стандартная операция отрицания определена для типов int , long , float , double и decimal . К величинам других типов ее можно применять, если для них возможно неявное преобразование к этим типам.

Логическое отрицание (!) определено для типа bool . Результат операции - значение false , если операнд равен true , и значение true , если операнд равен false .

Поразрядное отрицание (~ ), часто называемое побитовым, инвертирует каждый разряд в двоичном представлении операнда типа int , uint , long или ulong .

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

Операция используется для явного преобразования величины из одного типа в другой. Это требуется в том случае, когда неявного преобразования не существует. При преобразовании из более длинного типа в более короткий возможна потеря информации. Формат операции:

(тип) выражение

Здесь тип - это имя того типа, в который осуществляется преобразование, а выражение чаще всего представляет собой имя переменной, например:

long b = 300; int a = (int) b; // данные не теряются int d = (byte) a; // данные теряются

Умножение, деление и остаток от деления

Операция умножения (* ) возвращает результат перемножения двух операндов. Стандартная операция умножения определена для типов int , uint , long , ulong , float , double и decimal . К величинам других типов ее можно применять, если для них возможно неявное преобразование к этим типам. Тип результата операции равен "наибольшему" из типов операндов, но не менее int .

таблице 3.2 . Символами х и y обозначены конечные положительные значения, символом z - результат операции вещественного умножения. Если результат слишком велик для представления с помощью заданного типа, он принимается равным значению "бесконечность", если слишком мал, он принимается за 0. NaN (not a number) означает, что результат не является числом.

Таблица 3.2. Результаты вещественного умножения
* +y -y +0 -0 + - NaN
+x +z -z +0 -0 + - NaN
-x -z +z -0 +0 - + NaN
+0 +0 -0 +0 -0 NaN NaN NaN
-0 -0 +0 -0 +0 NaN NaN NaN
+ + - NaN NaN + - NaN
- - + NaN NaN - + NaN
NaN NaN NaN NaN NaN NaN NaN NaN

Операция деления (/ ) вычисляет частное от деления первого операнда на второй. Стандартная операция деления определена для типов int , uint , long , ulong , float , double и decimal . К величинам других типов ее можно применять, если для них существует неявное преобразование к этим типам. Тип результата определяется правилами преобразования, но не меньше int .

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

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

Таблица 3.3. Результаты вещественного деления
/ +y -y +0 -0 + - NaN
+x +z -z + - +0 -0 NaN
-x -z +z - + -0 +0 NaN
+0 +0 -0 NaN NaN +0 -0 NaN
-0 -0 +0 NaN NaN -0 +0 NaN
+ + - + - NaN NaN NaN
- - + - + NaN NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN

Для финансовых величин (тип decimal ) при делении на 0 и переполнении генерируются соответствующие исключения, при исчезновении порядка результат равен 0.

Операция остатка от деления (% ) также интерпретируется по-разному для целых, вещественных и финансовых величин. Если оба операнда целочисленные, результат операции вычисляется по формуле x - (x / y) * y . Если делитель равен нулю, генерируется исключение System.DivideByZeroException .

Если хотя бы один из операндов вещественный, результат операции вычисляется по формуле x – n * y , где n - наибольшее целое, меньшее или равное результату деления х на y . Все возможные комбинации значений операндов приведены в таблице 3.4 .

Таблица 3.4. Результаты вещественного остатка от деления
% +y -y +0 -0 + - NaN
+x +z z NaN NaN x x NaN
-x -z -z NaN NaN -x -x NaN
+0 +0 +0 NaN NaN +0 +0 NaN
-0 -0 -0 NaN NaN -0 -0 NaN
+ NaN NaN NaN NaN NaN NaN NaN
- NaN NaN NaN NaN NaN NaN NaN
NaN NaN NaN NaN NaN NaN NaN NaN

Для финансовых величин (тип decimal ) при получении остатка от деления на 0 и при переполнении генерируются соответствующие исключения, при исчезновении порядка результат равен 0. Знак результата равен знаку первого операнда.

Сложение и вычитание

Операция сложения (+ ) возвращает сумму двух операндов. Стандартная операция сложения определена для типов int , uint , long , ulong , float , double и decimal . К величинам других типов ее можно применять, если для них существует неявное преобразование к этим типам. Тип результата операции равен "наибольшему" из типов операндов, но не менее int .

Все возможные значения для вещественных операндов приведены в таблице 3.5 .

Таблица 3.5. Результаты вещественного сложения
+ y +0 -0 + - NaN
x z x x + - NaN
+0 y +0 +0 + - NaN
-0 y +0 -0 + - NaN
+ + + + + NaN NaN
- - - - NaN - NaN
NaN NaN NaN NaN NaN NaN NaN

Операция вычитания (- ) возвращает разность двух операндов. Стандартная операция вычитания определена для типов int , uint , long , ulong , float , double и decimal . К величинам других типов ее можно применять, если для них существует неявное преобразование к этим типам. Тип результата операции равен "наибольшему" из типов операндов, но не менее int .

Если оба операнда целочисленные или типа decimal и результат операции слишком велик для представления с помощью заданного типа, генерируется исключение System.OverflowException .

Все возможные значения результата вычитания для вещественных операндов приведены в таблице 3.6 . Символами х и y обозначены конечные положительные значения, символом z - результат операции вещественного вычитания. Если х и y равны, результат равен положительному нулю. Если результат слишком велик для представления с помощью заданного типа, он принимается равным значению "бесконечность" с тем же знаком, что х - y , если слишком мал, он принимается за 0 с тем же знаком, что х - y .

Таблица 3.6. Результаты вещественного вычитания
- y +0 -0 + - NaN
x z x x - + NaN
+0 -y +0 +0 - + NaN
-0 -y -0 +0 - + NaN
+ + + + NaN + NaN
- - - - - NaN NaN
NaN NaN NaN NaN NaN NaN NaN

Операции сдвига

Операции сдвига (<< и >> ) применяются к целочисленным операндам. Они сдвигают двоичное представление первого операнда влево или вправо на количество двоичных разрядов, заданное вторым операндом.

При сдвиге влево (<< ) освободившиеся разряды обнуляются. При сдвиге вправо (>> ) освободившиеся биты заполняются нулями, если первый операнд беззнакового типа, и знаковым разрядом в противном случае. Стандартные операции сдвига определены для типов int , uint , long и ulong .

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

Операции отношения (< , <= , > , >= , == , != ) сравнивают первый операнд со вторым. Операнды должны быть арифметического типа. Результат операции - логического типа, равен true или false . Правила вычисления результатов приведены в > y

true , если x больше y , иначе false x <= y true , если x меньше или равно y , иначе false x >= y true , если x больше или равно y , иначе false

Поразрядные логические операции

Поразрядные логические операции (& , | , ^ ) применяются к целочисленным операндам и работают с их двоичными представлениями. При выполнении операций операнды сопоставляются побитно (первый бит первого операнда с первым битом второго, второй бит первого операнда со вторым битом второго, и т д.). Стандартные операции определены для типов int , uint , long и ulong .

При поразрядной конъюнкции (& ), бит результата равен 1 только тогда, когда соответствующие биты обоих операндов равны 1.

При поразрядной дизъюнкции (| ), бит результата равен 1 тогда, когда соответствующий бит хотя бы одного из операндов равен 1.

При поразрядном исключающем ИЛИ () бит результата равен 1 только тогда, когда соответствующий бит только одного из операндов равен 1.

Условные логические операции

Условные логические операции И (&& ) и ИЛИ (|| ) чаще всего используются с операндами логического типа. Результатом логической операции является true true , то результатом условной операции будет значение второго операнда, иначе - третьего операнда. Вычисляется всегда либо второй операнд, либо третий. Их тип может различаться.

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

Операции присваивания

Операции присваивания (= , += , -= , *= и т. д.) задают новое значение переменной. Эти операции могут использоваться в программе как законченные операторы.

Формат операции простого присваивания (= ):

переменная = выражение

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

сложении с присваиванием ко второму операнду прибавляется первый, и результат записывается в первый операнд, то есть выражение a += b является более компактной записью выражения a = a + b .

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

Операции присваивания правоассоциативны , то есть выполняются справа налево, в отличие от большинства других операций (a = b = c означает a = (b = c) ).

Вопросы и задания для самостоятельной работы студента

  1. Где можно описывать переменные? Что входит в описание переменной?
  2. Что происходит при использовании в выражении операндов различных типов? Приведите примеры.
  3. Перечислите операции языка C#, сгруппировав их по приоритетам.
  4. Что такое NaN? В каких операциях NaN является результатом?
  5. К операндам какого типа применимы операции сдвига?
  6. Что такое исключительные ситуации?
  7. Опишите принципы обработки исключительных ситуаций.