Управляющие конструкции
Привет! Мы продолжаем нашу серию статей по основам C#. Сегодня разберемся с приоритетом операций и управляющими конструкциями. Чего-то сильно уникального относительно других языков вы вряд ли для себя найдете, если C# для вас не первый язык, но можете обратить внимание на оператор switch
, он действительно устроен немного по-другому.
Операторы и выражения
При прочтении различных книжек на русском вы сможете заметить, что в разных работах даны свои определения операциям, операторам и выражениям в C#, и при этом они могут еще и противоречить друг другу, из-за чего возникает некоторая несущественная путаница в понятиях. Вероятно, это связано с некорректным переводом с английского. Я постараюсь дать определения в соответствии с тем, как это преподносит нам microsoft.
Итак, основной код программы на C# состоит из инструкций/команд(statements), операторов(operators) и выражений(expressions).
Инструкции могут состоять из одной строки кода, заканчивающейся точкой с запятой, или серии однострочных инструкций в блоке. Блок инструкций заклюается в скобки {}
и может содержать вложенные блоки.
//пример простой инструкции объявления переменной int a = 5;
Инструкции состоят из выражений, а те, в свою очередь – из операндов и операторов. Каждая инструкция заканчивается ;
.
Операция - это некоторое действие, производимое над операндами. А оператор определяет суть данного действия, которое будет произведено над операндами (например, оператор +
определяет действие сложения).
Выражение же обозначает по существу значение, которое мы можем получить в результате его выполнения. Простейшими выражениями являются литералы и переменные, которые можно объединять в сложные выражения с помощью операторов.
//инструкции объявления переменных int a; int b = 1; //правая часть rvalue является константным выражением //инструкция выражения, сохраняющая значение выражение в переменной a a = 1 + b + 2; //1 + b + 2 - выражение //1, b, 2 - операнды //+ = - операторы
Операторы в C# делятся на унарные, бинарные и тернарные в зависимости от количества операндов, с которыми они работают.
При вычислении выражения, в котором содержится несколько операторов, операции выполняются в порядке приоритета операторов.
int a = 5 + 4 * 3; // 5 + (4 * 3) = 17
Если в выражении операции имеют одинаковые приоритеты, то порядок их выполнения определяется ассоциативностью. Операторы делятся на левоассоциативные и правоассоциативные.
Левоассоциативные операции выполняются последовательно слева направо. Левоассоциативными операторами являются все бинарные операторы за исключением операторов присвоения, объединения с null.
1 + 2 + 3 //((1 + 2) + 3) = 3 + 3 = 6
Правоассоциативные операции выполняются справа налево. Правоасоциативными являются операторы присваивания, объединения с null
, лямбда-выражения и тернарный оператор?:
.
int a = 5; int b, c; b = c = a; //b = (c = a)
Управляющие конструкции
Программы не ограничиваются линейной последовательностью выполнения команд, что было показано до этого. Во время выполнения программа может повторять сегменты кода или разветвляться в зависимости от некоторого условия. Для этих целей в C# существует ряд конструкций для управления потоком выполнения программы, которые служат для указания, что наша программа должна сделать, когда и при каких обстоятельствах:
- условные конструкции (
if else
) - конструкция выбора (
switch
) - конструкции цикла (
while
,do while
,for
,foreach
) - условный оператор (?:)
- выражение (
switch
)
Вообще в документации и в оригинальной литературе конструкции if else
, switch,
for
, while, break, continue
выделяются от operators именно как инструкции statements, но во всех переводах мы увидим заветное слово "оператор" в независимости +
это или if
. Не знаю, только у меня такая шиза или еще кого-то от этих понятий триггерит. Наверно нужно это принять...
Условная конструкция if else
Конструкция if проверяет истинность некоторого условия и в зависимости от результатов проверки выполняет определенный код. Синтаксис выглядит следующим образом:
if (условие) { блок выполнения, если условие истинно } else { блок выполнения, если условие ложно }
После ключевого слова if
следует логическое выражение, на основе которого происходит выбор выполняющегося кода. Код в блоке if будет выполнен в том случае, если выражение дает значение true
, а блок else выполняться не будет, и наоборот.
int a = 1; int b = 5; if (a > b) { Console.WriteLine(quot;First number {a} is greater {b}"); } else { Console.WriteLine(quot;First number {a} is not greater {b}"); }
Часть else
является необязательной, и при ее отсутствии будет выполняться только блок if
, если условие истинно.
int a = 20; int b = 20; if (a == b) { Console.WriteLine(quot;Numbers are equal"); }
Но выполнении программы может возникнуть ситуация, когда нам необходимо выполнить более одного разветвления. Например, в первом примере при сравнении двух переменных может быть три варианта развития событий: когда первая больше второй, когда вторая больше и когда они равны; Чтобы поправить пример и корректно обработать все три случая мы можем в блоке else
прописать еще одно сравнение if
(что делать не нужно), а можем использовать конструкцию else if
:
int a = 1; int b = 5; /*if (a > b) { Console.WriteLine(quot;First number {a} is greater {b}"); } else { if (a == b) { Console.WriteLine(quot;Numbers are equal"); } else { Console.WriteLine(quot;First number {a} is less {b}"); } }*/ if (a > b) { Console.WriteLine(quot;First number {a} is greater {b}"); } else if (a == b) { Console.WriteLine(quot;Numbers are equal"); } else { Console.WriteLine(quot;First number {a} is less {b}"); }
И, да, когда в блоке конструкции одна команда, можно опустить скобки {}
, они необходимы, когда в блоке содержится более одной команды.
if (a > b) Console.WriteLine(quot;First number {a} is greater {b}");
Сравнение ссылочных типов
Как мы уже знаем, для сравнения двух значений на равенство используется оператор ==. И это действительно справедливо для типов значений, операнды типов значений равны, если равны их значения.
int a = 5; int b = 5; Console.WriteLine(a == b); //true
При сравнении же операндов ссылочного типа через оператор == по умолчанию будут сравниваться ссылки на данные объекты. Если два операнда ссылаются на один и тот же объект в памяти, будет true
.
class Cat { public int id; } Cat cat1 = new Cat{ id=1 }; Cat cat2 = new Cat{ id=1 }; Cat cat3 = cat1; Console.WriteLine(cat1 == cat2); //false Console.WriteLine(cat1 == cat3); //true
Для определения условия сравнения объектов у ссылочного типа используется метод Equals()
, который возвращает значение типа bool и наследуется от базового класса Object
.
Да-да, я еще не рассказывал про наследование и это будет далее. Пока просто нужно понимать, что все создаваемые пользовательские классы неявно наследуются от общего базового класса Object
, а вместе с ним его методы, которые мы можем переопределить в своих классах.
По умолчанию метод Equals также проверяет равенство ссылок объектов, но это поведение можно изменить перегрузив данный метод у класса.
class Cat { public int id; public bool Equals(Cat otherCat) { return id == otherCat.id; } } Cat cat1 = new Cat{ id=1 }; Cat cat2 = new Cat{ id=1 }; Cat cat3 = cat1; Console.WriteLine(cat1 == cat2); //false Console.WriteLine(cat1 == cat3); //true Console.WriteLine(cat1.Equals(cat2)); //true Console.WriteLine(cat1.Equals(cat3)); //true
У вас сразу же может возникнуть вопрос. Раз String - это класс и он, соответственно, является ссылочным типом, то почему же строки нормально сравниваются по значению также и через оператор == ?! А все просто, для строк == также перегружен для их сравнения не по ссылке. О перегрузке операторов мы также уже скоро поговорим)
Тернарный оператор
Условный оператор (тернарный) вычисляет логическое выражение и в зависимости от полученного значения true
или false
возвращает результат одного из двух соответствующих выражений. Он имеет следующий синтаксис:
[условие] ? [выражение при истинном условии] : [выражение при ложном условии]
В зависимости от значения логического(условного) выражения true
или false
, будет выполняться первое или второе выражение тернарного оператора.
int a = 1; int b = 5; int c = a < b ? a + b : a - b; Console.WriteLine(c); //6
Циклы
Циклы являются управляющими конструкциями, позволяющими выполнять набор инструкций множество раз. В C# 4 типа циклов: for
, foreach
, while
, do while
. В этой статье пойдет речь только о 3 штуках. О foreach
мы поговорим, когда будем разбирать коллекции.
Цикл for
имеет следующее формальное определение:
for ([действия_до_выполнения_цикла]; [условие]; [действия_после_выполнения]) { // блок команд }
Объявление цикла for состоит из трех частей. Первая часть объявления цикла - некоторые действия, которые выполняются один раз до выполнения цикла. Обычно здесь определяются переменные, которые будут использоваться в цикле.
Вторая часть - условие, при котором будет выполняться цикл. Цикл будет выполняться, пока условие не станет равно false
.
И третья часть - некоторые действия, которые выполняются после завершения блока цикла. Эти действия выполняются каждый раз при завершении блока цикла.
Но при этом все эти 3 части являются необязательными и их при надобности можно опустить)
int sum = 0; for (int i = 0; i < 5; i++) { sum += i; } Console.WriteLine(sum); //10 for (int i = 1, int j = 2; i < 10; i++, j++) { int mult = i * j; Console.WriteLine(quot;{mult}"); } int i = 1; for (; ;) //этот цикл будет бесконечным, что не гуд, нужно делать точку выхода из цикла { Console.WriteLine(quot;i = {i}"); i++; }
Все переменные, которые создаются при объявлении цикла (как i j в примере) являются локальными переменными в области видимости данного цикла, а переменные, которые создаются в теле цикла живут в течении одной итерации.
Цикл while
выполняет блок команд, пока определенное логическое выражение равно значению true
. Так как это выражение оценивается перед каждым выполнением цикла, цикл while
выполняется ноль или несколько раз. Это основное отличие от цикла do while
с постусловием, у которого будет выполнена хотябы одна итерация, после чего будет произведена проверка условия.
int i = 10; while (i > 0) { Console.WriteLine(i); i--; } do { Console.WriteLine(i); i--; } while(i > 0)
Цикл do while почти никогда не используют, так как почти всегда есть возможность заменить его на while. На практике у меня возникала одна ситуация, когда мне потребовался именно цикл do while, и не было возможности заменить его обычным while, и то я эту ситуацию уже даже не помню))
Инструкции перехода: continue и break
Для досрочного выхода из цикла, не дожидаясь его завершения, используется break
. Он завершает ближайшую итерацию цикла и передает поток управления на команду, которая расположена после завершения цикла.
int i = 10; while (true) { if (i < 0) break; Console.WriteLine(i); i--; }
А для досрочного перехода к следующей итерации цикла без его завершения используется инструкция continue
.
for (int i = 0; i < 9; i++) { if (i == 5) continue; Console.WriteLine(i); }
Конструкция switch case
позволяют организовать ветвление потока выполнения программы на основе выбора из заранее определенного набора вариантов значений, которые может принять выражение.
switch (выражение) { case значение1: набор команд, выполняемых, если выражение имеет значение1 break; case значение2: набор команд break; case значениеN: набор команд break; default: набор команд, который выполняется, если выражение не подходит ни под одно значение выше break; }
После ключевого слова switch
в скобках идет сравниваемое выражение. Значение этого выражения последовательно сравнивается со значениями, помещенными после оператора сase
. И если совпадение будет найдено, то будет выполняться определенный блок сase
. Если значение выражения не подходит ни под одно условие, будет выполнен блок default
, но он в конструкции необязателен.
В конце каждого блока case
следует указывать break
, с помощью которого произойдет выход из контекста switch после выполнения всех инструкций данного case блока. А иначе сравниваться будут последовательно все блоки case, даже если "совпадение" уже было найдено.
switch в C# способен сопоставлять все базовые типы данных char
, string
, bool
, int
, long
, float
, double
, decimal
и enum
.
int x = 4; switch (x) { case 1: Console.WriteLine("one"); break; case 2: Console.WriteLine ("two"); break; case 3: Console.WriteLine ("three"); break; case 4: Console.WriteLine ("four"); break; default: Console.Writeline("Wow, king in the castle"); break; }
До выхода версии C# 7 сопоставляющие выражения в операторах switch ограничивались сравнением переменной с константными значениями. Начиная с C# 7 появилась возможность указывать диапазон значений, с помощью ключевого слова when
.
int x = 6; switch (x) { case < 5: Console.WriteLine("< 6"); break; case int i when i > 5 && i < 6: Console.WriteLine("5 < x < 6"); break; case int i when i > 5 || i == 4: Console.WriteLine("> 5 || 4"); break; case 6: Console.WriteLine("6"); break; }
Привел глупый пример, но как работать when
должно стать понятно)
На этом вроде все) Не стал писать здесь про весьма приятное выражение switch
, о нем вероятно напишу в отдельном посте. И не стал писать про go to
, прости господи. Не используйте недоразумение в своем коде, тех, кто использует это недоразумение, бьют палками!!! Нет таких ситуаций, когда нельзя было бы не обойтись без go to.
В следующей статье снова поговорим о классах. Рассмотрим свойства, конструкторы и деструкторы классов, жизненный цикл объекта класса и немного поговорим про garbage collector и возможно еще о чем-нибудь)
Подписывайтесь на наш тг-канал, чтобы ничего не пропустить.