Основы C#
October 19, 2022

Управляющие конструкции

Привет! Мы продолжаем нашу серию статей по основам 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# делятся на унарные, бинарные и тернарные в зависимости от количества операндов, с которыми они работают.

При вычислении выражения, в котором содержится несколько операторов, операции выполняются в порядке приоритета операторов.

В таблице перечислены все операторы 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 ([действия_до_выполнения_цикла]; [условие]; [действия_после_выполнения])
{
    // блок команд
}

Объявление цикла 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 и do while

Цикл 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 позволяют организовать ветвление потока выполнения программы на основе выбора из заранее определенного набора вариантов значений, которые может принять выражение.

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 и возможно еще о чем-нибудь)
Подписывайтесь на наш тг-канал, чтобы ничего не пропустить.