Добавлен , опубликован

Основы программирования Корсаров

Содержание:

Оператор if

Самыми простыми условными ветвлениями являются стейтменты if/else. Они выглядят следующим образом:
if (выражение)
{
    стейтмент1;
}
Либо так:
if (выражение)
{
    стейтмент1;
}
else
{
    стейтмент2;
}
выражение называется условием. Если результатом условия является true, то выполняться будет стейтмент1, находящийся в блоке кода после if (между фигурными скобками). Если же результатом выражения является false, то выполняться будет стейтмент2. Например:
string myFunc (int a)       // ф-ция принимает число
{
    if (a > 0)              // проверяем, что оно больше нуля
    {
        return "positive";  // возвращаем ответ, что число положительное
    }
    else                    // если наше условие не выполнилось (число равно или меньше нуля)
    {
        return "negative";  // возвращаем ответ, что число отрицательное
    }
}
В каждом условии, в примере выше, я писал лишь по одному стейтменту return. Но блок кода может быть любого размера.
Кстати, ситуация, когда в блоке if находится return, называется ранним возвратом (так как функция возвращает управление в caller не выполняясь до конца).
В условии может находиться что угодно, что имеет значение. К примеру результат вычисления функции:
int sum (int a, int b)
{
    return a + b;
}

string myFunc ()
{
    int a = 1;
    int b = rand(99);
    
    if (sum(a, b) == 100)
    {
        return "b is 99";
    }
    else
    {
        return "try again";
    }
}
Это был самый простой пример - мы проверяли всего одно условие. Но можно писать и более сложные конструкции. Например, мы хотим проверить больше возможных состояний a. Для этого стейтменты if/else можно использовать в связке:
string myFunc (int a)
{
    if (a > 0)              // если a больше нуля
    {
        return "positive";
    }
    else if (a == 0)        // если a равно нулю
    {
        return "zero";
    }
    else                    // иначе
    {
        return "negative";
    }
}
Таких проверок может быть сколько угодно много. И они не обязательно должны заканчиваться именно стейтментом else - все ваши блоки вполне могут быть с проверкой условия, то есть else if.
Давайте ещё немного усложним задачу. Что если этапов проверки несколько? В таких случаях можно использовать вложенные ветвления if/else (то есть один стейтмент if находится внутри другого):
string myFunc (int a)
{
    if (a > 0)
    {
        if (a > 100)
        {
            return "hundreds";
        }
        else if (a > 1000)
        {
            return "thousands";
        }
        else
        {
            return "tens";
        }
    }
    else
    {
        return "negative";
    }
}
Как вы, наверное, догадались, уровней вложенности тоже может быть бесконечно много (один if, в нём второй if, а там третий - как матрёшка).
Это имеет и обратную сторону медали - все эти уровни вложенности сильно ухудшают читаемость кода. А ситуаций, где нужно проверить несколько условий одновременно у вас будет едва ли не больше, чем ситуаций где условие проверяется всего одно.
И здесь нам на помощь приходят логические операторы, которые можно использовать в условии:
string myFunc (int a, int b)
{
    string s = "";
    
    if (a > 0 && b > 0)         // && - это логическое "И". Условие вернёт истину, если оба числа больше нуля
    {
        s = "both numbers are positive";
    }
    else if (a > 0 || b > 0)    // || - это логическое "ИЛИ". Условие вернёт истину, если одно из чисел больше нуля
    {
       s = "one of the numbers is positive";
    }
    
    return s;
}
Опять таки, объединить в одном выражении можно много условий. Но здесь затесалась очередная ОСОБЕННОСТЬ Корсаров - использование логических И и ИЛИ в одном выражении вызывает ошибку.
То есть выражение вида:
if (a && b && (d || c))
Непременно вызовет ошибку. Причём такая конструкция не поддерживается не только в if'ах, а вообще в любых bool-значениях (циклах и переменных).
Вариантов решения этой проблемы есть три:
// Вариант 1:
if (a && b)
{
	if (d || c)
	{
	    // ...
	}
}

// Вариант 2:
bool ok;

ok = (d) || (c);

if (a && b && ok)
{
    // ...
}

// Вариант 3:
// создаём ф-ции конъюнкции и дизъюнкции
bool Con (bool a, bool b)
{
	return a && b;
}

bool Dis (bool a, bool b)
{
	return a || b;
}

//используем по аналогии с вариантом 2
if ( Con(a, b) || c)
{
	// ...
}
Здесь же стоит рассказать об оптимизаторе выражений.
if (a && b)
Если в такой конструкции a вернёт false, то b проверяться вообще не будет.
Если b это функция, в которой лежит большой и сложный расчёт, то зачем его выполнять, когда и так понятно, что условие не выполняется. В этом оптимизация и заключается.
Другое дело, если в этой функции выполняются какие-то важные действия, которые обязательно должны быть выполнены, не зависимо от результата условия.
Это нужно иметь в виду. Если в вашей проверке есть код, выполнение которого не обязательно, то на первое место ставьте проверку переменных, чтобы этот код не выполнять в случае когда переменная проверку не пройдёт. И наоборот, если код должен быть выполнен всегда - ставьте его на первое место.
Аналогичная ситуация с логическим ИЛИ:
if (a || b)
Если a вернёт true - b проверяться не будет.

Следующая ОСОБЕННОСТЬ касается использования ветвлений else if на старых версиях Storm Engine (до вот этого коммита от kb31).
Цитата Rosarak:
else if можно использовать только в скриптах диалогов и интерфейса. В других местах синтаксис скриптов не воспринимает классическое ветвление if {} else if {} else if {}
Суть в том, что бесконечное множество операторов else if в рамках одной цепочки может использоваться только в файлах интерфейсов и диалогов (т.к. они вызываются из-под интерфейса диалога). В остальных же местах кода игра воспринимает только один else if на одну цепочку. Попытка прописать больше приводит к ошибке еще на этапе запуска игры.
// этот код отработает везде
if (a == 1)
{
    // ...
}
else if (a == 2)
{
    // ...
}
else
{
    // ...
}

// а этот только в интерфейсах/диалогах
if (a == 1)
{
    // ...
}
else if (a == 2)
{
    // ...
}
else if (a == 3)
{
    // ...
}
else
{
    // ...
}
Сразу напрашивается вопрос: как из этой ситуации выкручиваться?
Вариант 1:
Если дело происходит внутри switch-case (об операторе switch поговорим чуть ниже), то можно прерывать с помощью break:
case "name":
    if (условие)
    {
        // ...
		break;
	}
	if (условие)
	{
		// ...
		break;
	}
	// ...
break;
Вариант 2:
Если внутри функции, то можно воспользоваться оператором return:
void myFunc (int num)
{
	if (num < 0)
	{
		// ...
		return;
	}
	if ( num == 0)
	{
		// ...
		return;
	}
	// ...
}
Вариант 3:
Если прерывание не требуется, то подойдет более ортодоксальный очевидный вариант.
if(условие 1) {} else if(условие 2) {} else {} всегда можно превратить в:
if(условие 1 || условие 2)
{
	if(условие 1)
	{
		// ...
	}
	else
	{
		// ...
	}
}
else
{
	// ...
}

Оператор switch

switch - ещё один условный оператор ветвления (управления потоком выполнения программы).
Хоть мы и можем использовать сразу несколько операторов if/else вместе — читается и смотрится это не очень. Например:
string getColor (string color)
{
    string result;
    
    if (color == "red")
    {
        result = "#ff0000";
    }
    else if (color == "blue")
    {
        result = "#0000ff";
    }
    else if (color == "yellow")
    {
        result = "#ffff00";
    }
    else if (color == "green")
    {
        result = "#00ff00"
    }
    else if (color == "white")
    {
        result = "#ffffff";
    }
    else
    {
        result = "unknown color";
    }
    
    return result;
}
Использование ветвления if/else для проверки значения одной переменной — практика распространенная, но язык C++ предоставляет альтернативный и более эффективный оператор switch. С его использованием вышеприведенная функция выглядит так:
string getColor (string color)
{
    string result;
    
    switch (color)
    {
        case "red":
            result = "#ff0000";
            break;
        case "blue":
            result = "#0000ff";
            break;
        case "yellow":
            result = "#ffff00";
            break;
        case "green":
            result = "#00ff00";
            break;
        case "white":
            result = "#ffffff";
            break;
        default:
            result = "unknown color";
            break;
    }
    
    return result;
}
Общая идея операторов switch проста: выражение оператора switch (в данном случае switch (color)) должно производить значение, а каждый кейс (англ. «case») проверяет это значение на соответствие. Если кейс совпадает с выражением switch, то выполняются инструкции под соответствующим кейсом. Если ни один кейс не соответствует выражению switch, то выполняются инструкции после кейса default (о нём ниже).
Из-за своей реализации, оператор switch обычно более эффективен, чем цепочка if/else. Давайте рассмотрим его более подробно.
Синтаксис
Сначала пишем ключевое слово switch за которым в круглых скобках следует выражение, с которым мы хотим работать. Обычно это выражение представляет собой только одну переменную, но это может быть и нечто более сложное, например, nX + 2 или nX − nY. Единственное ограничение к этому выражению — оно должно быть интегрального типа данных (т.е. число или строка). Переменные типа с плавающей точкой или неинтегральные типы использоваться не могут.
После выражения switch мы объявляем блок кода (фигурные скобки). Внутри блока мы используем лейблы (англ. «labels») для определения всех значений, которые мы хотим проверять на соответствие выражению. Существуют два типа лейблов.
Первый тип лейбла — case, который имеет константное выражение. Константное выражение — это то, которое генерирует константное значение, другими словами: либо литерал (например, 5), либо константу (например, COLOR_RED).
Константное выражение, находящееся после ключевого слова case, проверяется на равенство с выражением, находящимся возле ключевого слова switch. Если они совпадают, то тогда выполняется код под соответствующим кейсом.
Стоит отметить, что все выражения case должны производить уникальные значения. То есть вы не сможете сделать следующее:
#define COLOR_YELLOW	1
#define COLOR_RED		2
#define COLOR_PURPLE	3

switch (i)
{
    case 3:
    case 3:             // нельзя, значение 3 уже используется!
    case COLOR_PURPLE:  // нельзя, COLOR_PURPLE вычисляется как 3!
}
Можно использовать сразу несколько кейсов для одного выражения. Следующая функция использует несколько кейсов для проверки, соответствует ли параметр p числу из ASCII-таблицы:
bool isDigit(char p)
{
    switch (p)
    {
        case '0': // если p = 0
        case '1': // если p = 1
        case '2': // если p = 2
        case '3': // если p = 3
        case '4': // если p = 4
        case '5': // если p = 5
        case '6': // если p = 6
        case '7': // если p = 7
        case '8': // если p = 8
        case '9': // если p = 9
            return true;    // возвращаем true
        default:            // в противном случае, возвращаем false
            return false;
    }
}
Второй тип лейбла — default. Код под этим лейблом выполняется, если ни один из кейсов не соответствует выражению switch (аналог оператора else в условной конструкции if/else).
В вышеприведенном примере, если p не является числом из ASCII-таблицы, то тогда выполняется лейбл по умолчанию и возвращается false.
Одна из самых каверзных вещей в switch — это последовательность выполнения кода. Когда кейс совпал (или выполняется default), то выполнение начинается с первого стейтмента, который находится после соответствующего кейса и продолжается до тех пор, пока не будет выполнено одно из следующих условий завершения:
  • Достигнут конец блока switch.
  • Выполняется оператор return.
  • Выполняется оператор goto.
  • Выполняется оператор break.
Обратите внимание, если ни одного из этих условий завершения не будет, то выполняться будут все кейсы после того кейса, который совпал с выражением switch. Например:
switch (2)
{
   case 1:              // Не совпадает!
       result = "one";  // пропускается
   case 2:              // Совпало!
       result = "two";  // выполнение кода начинается здесь
   case 3:
       result = "three";    // это также выполнится
   case 4:
       result = "four";     // и это
   default:
       result = "unknown";  // и это
}
А это точно не то, что нам нужно! Когда выполнение переходит из одного кейса в следующий, то это называется fall-through. Программисты почти никогда не используют fall-through, поэтому в редких случаях, когда это все-таки используется — принято оставлять комментарий, что fall-through является преднамеренным.
Оператор break сообщает компилятору, что мы уже сделали всё, что хотели с определенным switch (или цикломи while или for) и больше не намерены с ним работать. Когда компилятор встречает оператор break, то выполнение кода переходит из switch на следующую строку после блока switch.
ПРАВИЛО Не забывайте использовать оператор break в конце каждого кейса. Его отсутствие — одна из наиболее распространенных ошибок новичков!

Теперь об ОСОБЕННОСТЯХ. Во-первых, компилятор Корсаров не идентифицирует лейбл default как часть оператора switch. Он его идентифицирует как метку перехода goto. Что это значит? Что возможностями лейбла default мы воспользоваться не сможем. Но есть другие особенности, которые помогают эту проблему частично решить.
Так, оператор switch выполняет код находящийся вне конструкций case-break.
Рассмотрим несколько примеров.
switch (i)
{
	case 1:
		log_info("кейс 1");
	break;

	log_info("между кейсами 1 и 2");

	case 2:
		log_info("кейс 2");
	break;

	log_info("вместо default");
}
  • Если мы передадим значение 1, то выполнится первый кейс, мы получим сообщение кейс 1 и далее оператор break прервёт работу switch.
  • Если передать 2 - будет выполнено всё, что не находится внутри конструкций case-break, но находится выше целевого кейса. Плюс содержимое самого целевого кейса, разумеется. То есть на выходе мы получим две строчки: между кейсами 1 и 2 и кейс 2.
  • Если передать значение, для которого кейса не существует (в нашем случае - 3), компилятор пройдёт по всему блоку switch, выполняя код, находящийся вне конструкций case-break. Таким образом, мы получим две строчки: между кейсами 1 и 2 и вместо default.
Что это нам даёт и как это можно использовать?
Во-первых, между кейсами можно вставлять повторяющийся код, который должен выполняться для всех оставшихся кейсов.
Во-вторых, код в конце блока switch можно использовать вместо лейбла default.
Это не позволит нам сделать нечто такое:
#define COLOR_BLUE	1
#define COLOR_WHITE	2
#define COLOR_RED	3

int getColorId (int color)
{
	switch (color)
	{
		case COLOR_BLUE:
			return COLOR_BLUE;
		// Если color не является одним из значений COLOR, используем COLOR_WHITE в качестве значения по умолчанию.
		default:
        case COLOR_WHITE:
			return COLOR_WHITE;
        case COLOR_RED:
			return COLOR_RED;
	}
}
Придется быть немного более гибким и подстраиваться под существующие реалии. Если кейс, который мы хотим использовать по умолчанию, поставить в конце и без указания лейбла, то приведенный выше пример вполне себе работает.

`
ОЖИДАНИЕ РЕКЛАМЫ...
Этот комментарий удален
23
Спасибо за статью! С её помощью я наконец то врубился, как работает switch и case, и зачем нужны && и || в...cjass )
Ответы (1)
23
EugeAl, рад, что помогает.
Касательно switch/case - хороший наглядный пример применения в статье про диалоги
23
Вышла новая версия! Прокрутить к ресурсу
Внесены подсказанные Rosarak правки:
  • особенности ветвлений else if вне интерфейсов
  • выполнение кода оператором switch вне конструкций case-break
  • альтернатива лейблу default для последних версий Storm Engine
  • третий вариант обхода ограничения на логические И и ИЛИ в одном выражении
30
использование логических И и ИЛИ в одном выражении вызывает ошибку
Я надеюсь сложение и умножение разрешено в одном выражении?
Ответы (3)
23
nazarpunk, да.
Кстати, сложениями и умножениями можно закостылитьпобороть ограничение на И + ИЛИ
if(((a)*(b) + с) - (a)*(b)*(c))
где операция И - это a x b; операция ИЛИ - это a+b-(a x b)
5
побороть ограничение на И + ИЛИ
Ага, но через арифметику - это шуточный вариант. В Реконе и Far Horizons используют:
bool Con(bool a, bool b) {return a && b;} //КОНЪЮНКЦИЯ
bool Dis(bool a, bool b) {return a || b;} //ДИЗЪЮНКЦИЯ
То есть некорректное if( (a && b) || c) можно превратить в работающее if( Con(a, b) || c)
Злоупотреблять Con/Dis не стоит, код может стать неудобочитаемым, но в иных случаях всё понятно, можно не плодить лишние предваряющие bool'ы.
Также важно помнить про оптимизацию (сначала базовые чеки релевантности, потом большие функции), о ней в гайде ты написал, и, например, что проверка значений атрибута обычно идёт в связке с проверкой его наличия (если только заведомо не известно, что он есть).
30
Кстати, код swittch разруливается на этапе компиляции и соответсвтвенно выполнится за O(1) вместо O(n) как в случае elseif.
Ответы (4)
23
nazarpunk, не понял. Что за 0(1) и 0(n) ?
И компиляции, в том виде, к которому ты привык, у нас нет, бтв.
30
И компиляции, в том виде, к которому ты привык, у нас нет, бтв.
Если процесс компиляции скрыт от разработчика, то это не значит что его нет.
23
nazarpunk, он не скрыт, я ж кидал исходники движка. Я бы скорее назвал это интерпретация, чем компиляция.
38
Здесь же стоит упомянуть об ещё одной ОСОБЕННОСТИ - оптимизаторе выражений.
Это не особенность, так работают почти все ЯП
Ответы (1)
23
ScorpioT1000, ок. Плашка лишняя, получается)
30
Что насчёт расположения default?
enum MyEnum {A, B, C}

int GetMyEnumValue(int myInt){
    switch(myInt){
        case MyEnum::A:
           return MyEnum::A;
        // Если myInt не является одним из значений MyEnum, используем MyEnum::B в качестве значения по умолчанию.
        default:
        case MyEnum::B:
           return MyEnum::B;
        case MyEnum::C:
           return MyEnum::C;
    }
}
Ответы (3)
23
nazarpunk, по середине типа?
Интересно. Завтра обязательно проверю
30
Ну да. Нигде не написано, что default должен быть последним выражением.
23
nazarpunk, не работает. Хорошее исследование.
Теперь мы точно понимаем, почему начиная с ГПК движок вообще ругается на этот default как на дублирующуюся метку перехода GoTo.
Потому что он его не распознаёт как часть конструкции switch. А код в конце исполняет просто потому, что он вне кейса.
Надо поправить статью. Спасибо!
Чтобы оставить комментарий, пожалуйста, войдите на сайт.