Общий стиль написания кода на SourcePawn


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


Фрагмент кода.png

Рис. 1. Пример кода SourcePawn в Visual Code.

Тезисы

1. На данном этапе развития, мы имеем два различных синтаксиса, первый - это до версии 1.7 языка SourcePawn, а второй - после.
* на сам деле, совместимость двух синтаксисов это хорошо, но с другой стороны вносит определенную путаницу и непонимание у новичков, а иногда и профессионалов своего дела *

2. Язык SourcePawn, как и многие другие: С++, D, Java, Objective C, C#, PHP, Perl, Nemerle - является Си-подобным, а значит наследует их особенности, такие как:
- Чувствительность к регистру.
- Количество пробелов, табуляция и переводы строки не влияют на семантику нигде, кроме строковых литералов. Это, в частности, значит, что возможен перевод строки в любом удобном месте.
- Бинарные операторы имеют инфиксную форму.
- и т.д.
* набор данные постулатов, говорит о том, что следует придерживаться четкому "Си" правилу написания кода, о котором поговорим дальше *

3. SourcePawn - процедурный скриптовый язык, не стоит опираться на него, как на объектно-ориентированный используя methodmap и размышлять объектами (это клево, если у вас есть способность представлять и мыслить ими, но в данном случае лучше абстрагироваться), пишите простой и четкий, одним словом - лаконичный код.

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

5. Структура! Уделите внимание структуре исходника, где лучше находиться глобальным переменным, директивам и различным функциям.
* не мало важный критерий, который в разы облегчит, как чтение кода, так и обрадует ваши глаза *

6. Модульность. Уметь писать красивый код - это еще полбеды, но код в 10 тыс. строчек в скриптовом языке очень тяжел для чтения, лучше разбейте функционал вашего скрипта в несколько файлов и аккуратно раскидайте в соответствующие директивы. Это хороший показатель вашей компетенции и подчеркивает, как вы трепетно относитесь к своей работе.
* для более любознательных советую прочитать такую книгу, как "Clean Architecture", Robert C. Martin - безумно классная, даю свое слово, что она вам понравится, если вы интересуетесь разработкой ПО (вы же интересуетесь ей 😉) *

Опираясь на данные тезисы (да, я знаю, что звучат они многословно), давайте перейдем к сути дела и по ходу будем обращаться к выше стоящим 6 аксиомам.


Стиль кода

1. Директивы препроцессора.


Чаще всего встречаются такие директивы, как:
- #include
- #define
- #pragma
-
#if / #else

Обычно, это типичная схема, она очень простая: сначала мы включаем в исходный файл основные библиотеки и прочие вторичные файлы, потом обзаводимся "константами" и только после - устанавливаем свои правила чтения данного файла, чтобы правила подключенного другого исходника не мешали вашему. 4 пункт с #if / #else он более спонтанный и может присутствовать совершенно в разных местах, но главное, чтобы все было по делу.

1. #include - ничего сложного тут нет, для обычных библиотек, находящихся в папке .inc используйте, исключительно, только угловые скобочки <name> (без формата файла) и двойные кавычки "directory/name.sp", если файл находится непосредственно в другой папке.
SourcePawn:
// основные библиотеки SourceMod
#include <sourcemod>
#include <cstrike>
#include <sdktools>

// Подмечу, что между основными файлами и дополнительными, можно, а порой даже
// нужно использовать такую директиву, как #define, чтобы она распространялась, как
// на основной код вашего исходника, так и непосредственно на ваши включаемые дополнительные файлы.

// дополнительные файлы
#include "DENFER/AutoBalance/others.sp"
#include "DENFER/AutoBalance/algorithms.sp"
#include "DENFER/AutoBalance/forwards.sp"
* пути к файлам не должны содержать пробелов *

2. #define - определяет идентификатор и последовательность символов на которые он будет заменяться, в простонародье их еще называют константами препроцессора или макросами. Обычно, идентификатор, то бишь название стоит писать заглавными буквами, чтобы ярко выделить в коде.
SourcePawn:
#define PI                    3.141592653589793238462643 // что по памяти вспомнил
#define EXP                   2.71828182
#define PLUGIN_VERSION        "1.0.0"
#define AUTHOR                "DENFER"

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

4. #if / #else - условия препроцессора. Тут стоит обратить внимание на ветвление, в принципе ничего больше не добавить.
SourcePawn:
#if (константное выражение)
    ...
    #elif (константное выражение)
        ...
        #elif (константное выражение)
            ...
       #else
            ...
    #endif
        ...
С дополнительной информацию о директивах препроцессора, вы сможете ознакомиться тут.
- Википедия
- Microsoft

2. Глобальные переменные.

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

Глобальная переменная должна иметь:
- Яркое, самоговорящее название.
- Иметь префикс g_ (что расшифровывается, как global_).
- Содержать тип самой переменной (вы же помните, что хоть мы и пользуемся новым синтаксисом, указанный тип переменной в её название, облегчит вашу совместную жизнь с ней в разы).

Приведу примеры и тем самым сформулирую некоторое правило для написания глобальных переменных.
SourcePawn:
// StringMaps
StringMap gh_smTree;

// ArrayLists
ArrayList gh_alPlayers;

// DataPacks
DataPack gh_dpDetails;

// ConVars
ConVar gc_bPlugin;

// Handles
Handle g_hTimer;

// Strings
char g_szPrefix[32];

// Floats
float g_flSpeed;

// Integers
int g_iFrags;

// Booleans
bool g_bIsAlive;
Как вы можете заметить, во-первых, переменные имеют префикс, указывающий, что переменная находится в глобальной области видимости и вы ее уже никогда не спутаете с локальной,
во-вторых, ее тип и при использование переменной, вы четко знаете ее тип и сможете осуществлять операции куда более разумнее, ну и конечно, простое, но понятное название, которое говорит само за себя.
Хотелось бы выделить пару особенностей, таких как:
- Консольные переменные или еще их обзывают кварами - имеют особый вид наименования, так как любую консольную переменную можно представить разными типами, то лучше всего использовать в название - основной тип представления.
- Производным от типа Handle лучше к префиксу приписывать h, тем самым получая gh_ и указывать соответствующее наименования производного типа, лично по мне, лучше использовать 1-2 буквы от производного типа в начале, чтобы на всякий случай спастись от пересечения имен.

И вот мы получили пару исключений и строгий набор правил для написания глобальных переменных, скажите же, что так читается намного проще и приятнее?

Ссылка по данной теме.
- Википедия

3. Локальные переменные

Теперь думаю, будет куда разумнее перейти от глобальных к локальным переменным.
С локальными переменными дела обстоят еще хуже, как их только не называют и в каком только стиле не пишут, думаю определенный строгий стиль написания им не помешал.

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

Вот вам и пример с использованием локальных переменных:
SourcePawn:
// обычная функция, которая содержит вашу реализацию
void Foo()
{
    // используем верблюжий регистр, но заметьте, что если переменная содержит одно слово,
    // то мы пишем, его строчными буквами и выглядит все по феншую, все информативно и понятно.
    int amountHealth;
    float coordinatesPlayer[3];
    char name[MAX_NAME_LENGHT];
    // дальше идет ваша реализация функции
    ...
}
Ссылка по данной теме.
- Википедия

4. Функции.

В прошлом примере мы использовали функцию, они тоже содержат свои особенности в наименование, давайте пробежимся по ним.
- Название функций в SourcePawn содержит такой стиль, как ПаскальныйРегистр.
- Название должно быть, как и любая переменная - самоговорящая за свое назначение.
- Если функция является колбеком, то лучше добавить соответствующий префикс, пробежимся по некоторым из них:
SourcePawn:
// Наименование функции имеет префикс - означающий, что функция является колбеком
// определенного ивента, и громкое название - сообщающее, что событие является началом раунда.
Action Event_RoundStart (Event event, const char[] name, bool dontBroadcast)
{
    ...
}

// Наименование функции имеет префикс - означающий, что функция является обработчиком,
// а само название, говорит о том, что обрабатывает основное меню плагина.
int Handler_MainMenu (Menu menu, MenuAction action, int param1, int param2)
{
    ...
}

// Наименование функции, так же содержит префикс, который сообщает, что это обратный вызов,
// а само название, предупреждает, что данный колбек вызывается при вводе консольной команды.
Action CallBack_ConsoleCmd (int client, int args)
И по таком принципу вы можете наименовывать свои функции. Согласны, что так выглядит намного проще и приятнее для чтения, не нужно гадать, что за обратный вызов или зачем та или иная функция - экономия времени, залог успеха!

5. Перечисления.

В редких случаях, но они все-таки бывают, используют перечисления, с ними иногда удобно и они имеют место быть:
SourcePawn:
// ПаскальРегистр
enum ArmorType
{
    // Наименование полей строго капсом
    ARMOR_NONE,
    ARMOR_KEVLAR,
    ARMOR_VESTHELM
};
Тут только добавлю, что бывают ситуации, когда перечислители стоит называть в ПаскальРегистре, вполне, допустимо, а яркий пример - MenuAction.

Ссылка по данной теме.
- Википедия

6. Структуры.

Структуры
строятся по аналогии с перечислениями, только поля структуры (переменные внутри нее), работают по принципу локальных переменных, а методы, как функции.
SourcePawn:
// структура
struct Statistics
{
    // поля
    char name[MAX_NAME_LENGTH];
    int frags;
    int deaths;
    float headshotAccuracy; 

    // метод
    int GetFrags()
    {
        return frags;
    }

    ...
};
Ссылка по данной теме.
- Википедия

Закончив с базой, можно переходить к мелочам и деталям.

7. Пробелы.


Из маленьких штришков и мазков складывается картина, точно самое и с кодом. Небольшая деталь, но показывает на сколько разработчик аккуратен и следит за своей разработкой.
SourcePawn:
// отделяйте условия пробелами, тем самым это выглядит куда более гармонично, чем если
// вы лепили все с друг другом
if (param1)
{
    for (int i = 1; i <= MaxClients; ++i)
    {
        ...
    }
}
else if (param2)
{
    while (flag)
    {
        ...
    }
}
else
{
    // главное не отделяйте пробелом передачу аргументов во функцию!
    Foo();
}
Отделяйте литералы и операнды - используйте пробелы и не забывайте про них, вы обязательно заметите, как ваш код преобразуется не внеся никаких изменений в его логику.
SourcePawn:
bool Foo(int param1, int param2)
{
    result = param1 + param2;
    result += 2;
    result -= 2;
    // старайтесь брать в круглые скобочки условия или операции, превышающие одного слова,
    // тем самым вы подчеркиваете важность данного условия и оно в первую очередь бросится в глаза.
    return (result > 0) ? true : false;
}

8. Комментарии.


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

Я выделяю 3 вида комментарий в SourcePawn:
- Строчный комментарий.
- Блочный комментарий.
- JSDoc комментарий.

Пробежимся по ним.

1. Строчный комментарий - обычно используется, если нужно внести небольшую пометку, которая уместится в одну строчку.
SourcePawn:
// пункты меню
#define ITEAM_1             1
#define ITEAM_2             2
#define ITEAM_3             3
#define ITEAM_BACKBUTTON    9 // кнопка назад

void Foo()
{
    // пробегаемся по клиентам с четными индексами
    for (int i = 1; i <= MaxClients; ++i)
    {
        if(IsClientInGame(i) && i % 2)
        {
            ...
        }
    }
}
Совершенно очевидный и простой код, не советую использовать на практике, написан исключительно в образовательных целях. Обратите внимание, что строчные комментарии я еще поделил на два типа, первый - комментарий находится над строчкой, тогда я выделяю целый блок, и второй - комментарий, который непосредственно относится к строчке кода, которую я комментирую. Это личное мое, но мне кажется, так даже удобнее.

2. Блочный комментарий - используется, когда вы хотите вылить все свои мысли на определенный участок кода, обычно использую перед функциями или когда нужно оставить заметку непосредственно в процессе написания кода.
SourcePawn:
/*
    Совершенно бесполезная функция, возвращает переданное число.
    TODO: Хорошо бы переписать её, чтобы она несла пользу.
*/
int Foo(int param)
{
    ...   

    return param;
}

3. JSDoc комментарий - один из самых интересных стилей введения комментариев, так как он подсвечивает ваш комментарий при написание той или иной функции (в зависимости от среды разработки). Тут советую почитать дополнительную информацию в интернете, хотя в пределах SourcePawn такой вид комментария используется только при объявление нативных функций или форвардов.
SourcePawn:
/**
* Проверяет игрока на нахождение в игре.
*
* @param     client - индекс игрока.
* @return             true, если игрок в игре,
*                    иначе false
*/
native bool IsClientInGame(int client);
Советую прочитать про теги и в целом про вид данного комментария.
- Википедия

9. Кодовые метки и особые комментарии.

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


1. NOTE - описание того, как работает код, ремарка.
SourcePawn:
// NOTE: Проверяет на валидность клиентов.
public bool IsValidClient(int client)
{
    if (1 <= i <= MaxClients && IsClientInGame(i))
    {
        return true;
    }
 
    return false;
}
Данная кодовая метка эквивалентна простому блочному/строчному комментарию, но в конце этого раздела я подытожу, почему ее стоит использовать.

2. XXX - предупреждение о возможных подводных камнях, частенько используется заодно с NOTE:
SourcePawn:
// XXX: Выбросит исключение, если индекс не будет проверен на валидность!
native bool IsPlayerAlive(int client);

// XXX:NOTE:
...

3. FIXME - это вроде как работает, но можно было бы сделать лучше. (обычно код, написанный в спешке, нуждается в переписывании).
4. HACK - не очень хорошо написанный или искаженный код для обхода проблемы/ошибки. Следует использовать как HACK:FIXME:.
5. TODO- нет проблем, но нужно написать дополнительный код, обычно когда вы что-то пропускаете.
6. BUG - тут проблема.

Использование TODO в коде.png

Рис.2. Пример с использованием тега TODO в коде.

Плюсы данных кодовых меток в том, что они подсвечиваются специальными плагинами IDE, бросаются в глаза и всегда на виду. Я частенько использую их в своих частных разработках, так как спустя продолжительный промежуток времени я всегда могу вернуться к проекту и не забывать над чем работал до.


Примеры тегов.png

Рис.3. Выделение тегов в IDE Visual Code.

А на рисунке 3 вы можете наблюдать, как хорошо выделяются теги при помощи плагина Todo Tree и вы никогда не забудете, на чем остановились прошлый раз и какая работа еще впереди.

Думаю, базовую концепцию стиля написания на языке SourcePawn я смог разъяснить, вы будете исключительно правы - выдвигая свои способы и методы написания кода, так как в большинстве - это индивидуально и сравнимо с почерком, каждый пишет код так, как считает нужным, НО люди, которые только начинают основывать язык и в большинстве случаев даже не являются программистами или скриптерами (два разных понятия, подмечу, пожалуй), пишут совершенно абсурдные вещи, мешают старый синтаксис с новым, теряются сами в своем коде и просят помощи, подытожу. Данная статья была написана, исключительно для ребят, которые мечутся и не могут выбрать точный стиль написания. Это был урок по стилю написания кода на SourcePawn - пишите свой код чище, уделяйте внимание мелочам и не забывайте про комментарии, всем спасибо и всем хорошего кодирования!

Пару ссылок:
Венгерская нотация
Гайд по оформлению кода на C++
Введение в SourcePawn 1.7
Переходный синтаксис SourcePawn
Комментирование кода: хороший, плохой, злой