Руководство Google по стилю в C++. Часть 10

Часть 1. Вступление

Часть 9. Комментарии
Часть 10. Форматирование

Эта статья является переводом части руководства Google по стилю в C++ на русский язык.
Исходная статья (fork на github), обновляемый перевод.

Форматирование

Стиль кодирования и форматирования являются вещью произвольной, однако проект намного легче управляется, если все следуют одному стилю. Хотя кто-то может не соглашаться со всеми правилами (или пользоваться тем, чем привыкли), очень важно чтобы все следовали единым правилам, чтобы легко читать и понимать чужой код.
Для корректного форматирования мы создали файл настроек для emacs.

Длина строк

Желательно ограничивать длину строк кода 80-ю символами.
Это правило немного спорное, однако масса уже существющего кода придерживается этого принципа, и мы также поддерживаем его.

За
Приверженцы правила утверждают, что строки длиннее не нужны, а постоянно подгонять размеры окон утомительно. Кроме того, некоторые размещают окна с кодом рядом друг с другом и не могут произвольно увеличивать ширину окон. При этом ширина в 80 символов — исторический стандарт, зачем его менять?..

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

Вердикт
80 символов — максимум.

Строка можут превышать предел в 80 символов если:

  • комментарий при разделении потеряет в понятности или лёгкости копирования. Например, комментарий с примером команды или URL-ссылкой, длиннее 80 символов.
  • строковый литерал/имя, длиной более 80 символов. Исключением является тестовый код, который желдательно размещать в начале файла.
  • выражения с include.
  • Блокировка от повторного включения
  • using декларации

Не-ASCII символы

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

Кодировка hex также допустима, особенно если она улучшает читабельность. Например, "\xEF\xBB\xBF" или u8"\uFEFF" — неразрывный пробел нулевой длины в Юникоде, и который не должен отображаться в правильном UTF-8 тексте.

Используйте префикс u8 чтобы литералы вида \uXXXX кодировались в UTF-8. Не используйте его для строк, содержащих не-ASCII символы уже закодированные в UTF-8 — можете получить корявый текст если компилятор не распознает исходный код как UTF-8.

Избегайте использования символов C++11 char16_t и char32_t т.к. они нужны для не-UTF-8 строк. По тем же причинам не используйте wchar_t (кроме случаев работы с Windows API, использующий wchar_t).

Пробелы против Табуляции

Используйте только пробелы для отступов. 2 пробела на один отступ.
Мы используем пробелы для отступов. Не используйте табуляцию в своём коде — настройте свой редактор на вставку пробелов при нажатии клавиши Tab.

Объявления и определения функций

Старайтесь размещать тип возвращаемого значения, имя функции и её параметры на одной строке (если всё умещается). Разбейте слишком длинный список параметров на строки также как аргументы в вызове функции.

Пример правильного оформления функции:

ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) {   DoSomething();   ... } 

В случае если одной строки мало:

ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2,                                              Type par_name3) {   DoSomething();   ... } 

или, если первый параметр также не помещается:

ReturnType LongClassName::ReallyReallyReallyLongFunctionName(     Type par_name1,  // Отступ 4 пробела     Type par_name2,     Type par_name3) {   DoSomething();  // Отступ 2 пробела   ... } 

Несколько замечаний:

  • Выбирайте хорошие имена для параметров.
  • Имя параметра можно опустить, если он не используется в определении функции.
  • Если тип возвращаемого значения и имя функции не помещаются в одной строке, тип оставьте на одной строке, имя функции перенесите на следующую. В этом случае не делайте дополнительный отступ перед именем функции.
  • Открывающая круглая скобка всегда находится на одной строке с именем функции.
  • Не вставляйте пробелы между именем функции и открывающей круглой скобкой.
  • Не вставляйте пробелы между круглыми скобками и параметрами.
  • Открывающая фигурная скобка всегда в конце последней строки определения. Не переносите её на новую строку.
  • Закрывающая фигурная скобка располагается либо на отдельной строке, либо на той же строке, где и открывающая скобка.
  • Между закрывающей круглой скобкой и открывающей фигурной скобкой должен быть пробел.
  • Старайтесь выравнивать все параметры.
  • Стандартный отступ — 2 пробела.
  • При переносе параметров на другую строку используйте отступ 4 пробела.

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

class Foo {  public:   Foo(const Foo&) = delete;   Foo& operator=(const Foo&) = delete; }; 

Неиспользуемый параметры с неочевидным контекстом следует закомментировать в определении функции:

class Shape {  public:   virtual void Rotate(double radians) = 0; };  class Circle : public Shape {  public:   void Rotate(double radians) override; };  void Circle::Rotate(double /*radians*/) {} 
// Плохой стиль - если кто-то потом захочет изменить реализацию функции, // назначение параметра не ясно. void Circle::Rotate(double) {} 

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

ABSL_MUST_USE_RESULT bool IsOk(); 

Лямбды

Форматируйте параметры и тело выражения аналогично обычной функции, список захватываемых переменных — как обычный список.

Для захвата переменных по ссылке не ставьте пробел между амперсандом (&) и именем переменной.

int x = 0; auto x_plus_n = [&x](int n) -> int { return x + n; } 

Короткие лямбды можно использовать напрямую как аргумент функции.

std::set<int> blacklist = {7, 8, 9}; std::vector<int> digits = {3, 9, 1, 8, 4, 7, 1}; digits.erase(std::remove_if(digits.begin(), digits.end(), [&blacklist](int i) {                return blacklist.find(i) != blacklist.end();              }),              digits.end()); 

Числа с плавающей запятой

Числа с плавающей запятой всегда должны быть с десятичной точкой и числами по обе стороны от неё (даже в случае экспоненциальной нотации). Такой подход улучшить читабельность: все числа с плавающей запятой будут в одинаковом формате, не спутаешь с целым числом, и символы E e экспоненциальной нотации не примешь за шестнадцатеричные цифры. Помните, что число в экспоненциальной нотации не является целым числом.

float f = 1.f; long double ld = -.5L; double d = 1248e6; 
float f = 1.0f; float f2 = 1;   // Также правильно long double ld = -0.5L; double d = 1248.0e6; 

Вызов функции

Следует либо писать весь вызов функции одной строкой, либо размещать аргументы на новой строке. И отступ может быть либо по первому аргументу, либо 4 пробела. Старайтесь минимизировать количество строк, размещайте по несколько аргументов на каждой строке.

Формат вызова функции:

bool result = DoSomething(argument1, argument2, argument3); 

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

bool result = DoSomething(averyveryveryverylongargument1,                           argument2, argument3); 

Допускается размещать аргументы на нескольких строках с отступом в 4 пробела:

if (...) {   ...   ...   if (...) {     bool result = DoSomething(         argument1, argument2,  // Отступ 4 пробела         argument3, argument4);     ...   } 

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

Если несколько аргементов в одной строке ухудшают читабельность (из-за сложности или запутанности выражений), попробуйте создать для аргументов «говорящие» перемынные:

int my_heuristic = scores[x] * y + bases[x]; bool result = DoSomething(my_heuristic, x, y, z); 

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

bool result = DoSomething(scores[x] * y + bases[x],  // Небольшая эвристика                           x, y, z); 

Если в вызове функции ещё есть аргументы, которые желательно разместить на отдельной строке — размещайте. Решение должно основываться улучшении читабельность кода.

Иногда аргументы формируют структуру. В этом случае форматируйте аргументы согласно требуемой структуре:

// Преобразование с помощью матрицы 3x3 my_widget.Transform(x1, x2, x3,                     y1, y2, y3,                     z1, z2, z3); 

Форматирование списка инициализации

Форматируйте список инициализации аналогично вызову функции.
Если список в скобках следует за именем (например, имя типа или переменной), форматируйте {} как будто это вызов функции с этим именем. Даже если имени нет, считайте что оно есть, только пустое.

// Пример списка инициализации на одной строке. return {foo, bar}; functioncall({foo, bar}); std::pair<int, int> p{foo, bar};  // Когда хочется разделить на строки. SomeFunction(     {"assume a zero-length name before {"},     some_other_function_parameter); SomeType variable{     some, other, values,     {"assume a zero-length name before {"},     SomeOtherType{         "Very long string requiring the surrounding breaks.",         some, other values},     SomeOtherType{"Slightly shorter string",                   some, other, values}}; SomeType variable{     "This is too long to fit all in one line"}; MyType m = {  // Here, you could also break before {.     superlongvariablename1,     superlongvariablename2,     {short, interior, list},     {interiorwrappinglist,      interiorwrappinglist2}}; 

Условия

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

if (condition) {  // без пробелов внутри скобок   ...  // отступ 2 пробела } else if (...) {  // 'else' находится на строке с закрывающей скобкой   ... } else {   ... } 

Если используется формат с пробелами:

if ( condition ) {  // пробелы внутри скобок   ...  // отступ 2 пробела } else {  // 'else' находится на строке с закрывающей скобкой   ... } 

Заметьте, что в любом случае должен быть пробел между if и открывающей скобкой. Также нужен пробел между закрывающей скобкой и фигурной скобкой (если она есть).

if(condition) {   // Плохо - нет пробела после 'if' if (condition){   // Плохо - нет пробела перед { if(condition){    // Дважды плохо 
if (condition) {  // Хороший код - правильное количество пробелов после 'if' и перед { 

Короткие условия можно записать в одну строку, если это улучшит читабельность. Используйте этот вариант только если строка короткая и условие не содержит секцию else.

if (x == kFoo) return new Foo(); if (x == kBar) return new Bar(); 

Не используйте сокращённый вариант, если есть секция else:

// Плохо - условие в одну строку, хотя есть 'else' if (x) DoThis(); else DoThat(); 

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

if (condition)   DoSomething();  // отступ 2 пробела  if (condition) {   DoSomething();  // отступ 2 пробела } 

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

// Плохо - фигурные скобки у 'if', у 'else' - нет if (condition) {   foo; } else   bar;  // Плохо - фигурные скобки у 'else', у 'if' - нет if (condition)   foo; else {   bar; } 
// Хорошо - фигурные скобки и у 'if' и у 'else' if (condition) {   foo; } else {   bar; } 

Циклы и switch-и

Конструкция switch может использовать скобки для блоков. Описывайте нетривиальные переходы между вариантами. Скобки необязательны для циклов с одним выражением. Пустой цикл должен использовать либо пустое тело в скобках или continue.

Блоки case в switch могут как быть с фигурными скобками, так быть и без них (на ваш выбор). Если же скобки используются, используйте формат, описанный ниже.

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

switch (var) {   case 0: {  // Отступ 2 пробела     ...      // Отступ 4 пробела     break;   }   case 1: {     ...     break;   }   default: {     assert(false);   } } 

Переход с одной метки на следующую должен быть помечен макросом ABSL_FALLTHROUGH_INTENDED; (определён в absl/base/macros.h). Размещайте ABSL_FALLTHROUGH_INTENDED; в точке, где будет переход. Исключение из этого правила — последовательные метки без кода, в этом случае помечать ничего не нужно.

switch (x) {   case 41:  // Без пометок   case 43:     if (dont_be_picky) {       // Используйте макрос вместо (или совместно) с комментарием о переходе       ABSL_FALLTHROUGH_INTENDED;     } else {       CloseButNoCigar();       break;     }   case 42:     DoSomethingSpecial();     ABSL_FALLTHROUGH_INTENDED;   default:     DoSomethingGeneric();     break; } 

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

for (int i = 0; i < kSomeNumber; ++i)   printf("I love you\n");  for (int i = 0; i < kSomeNumber; ++i) {   printf("I take it back\n"); } 

Пустой цикл должен быть оформлен либо как пара скобок, либо как continue без скобок. Не используйте одиночную точку с запятой.

while (condition) {   // Повторять до получения false } for (int i = 0; i < kSomeNumber; ++i) {}  // Хорошо. Если разбить на две строки - тоже будет хорошо while (condition) continue;  // Хорошо - continue указывает на отсутствие дополнительной логики 
while (condition);  // Плохо - выглядит как часть цикла do/while 

Указатели и ссылки

Вокруг ‘.’ и ‘->’ не ставьте пробелы. Оператор разыменования или взятия адреса должен быть без пробелов.

Ниже приведены примеры правильного форматирования выражений с указателями и ссылками:

x = *p; p = &x; x = r.y; x = r->y; 

Отметим:

  • ‘.’ и ‘->’ используются без пробелов.
  • Операторы * или & не отделяются пробелами.

При объявлении переменной или аргумента можно размещать ‘*’ как к типу, так и к имени:

// Отлично, пробел до *, & char *c; const std::string &str;  // Отлично, пробел после *, & char* c; const std::string& str; 

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

Допускается объявлять несколько переменных одним выражением. Однако не используйте множественное объявление с указателями или ссылками — это может быть неправильно понято.

// Хорошо - читабельно int x, y; 

int x, *y;  // Плохо - не используйте множественное объявление с & или * char * c;  // Плохо - пробелы с обеих сторон * const std::string & str;  // Плохо - пробелы с обеих сторон & 

Логические выражения

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

Например, здесь при переносе оператор AND располагается в конце строки:

if (this_one_thing > this_other_thing &&     a_third_thing == a_fourth_thing &&     yet_another && last_one) {   ... } 

Отметим, что разбиение кода (согласно примеру) производится так, чтобы && и оператор AND завершали строку. Такой стиль чаще используется с коде Google, хотя расположение операторов в начале строки тоже допустимо. Также, можете добавлять дополнительные скобки для улучшения читабельности. Учтите, что использование операторов в виде пунктуации (такие как && и ~) более предпочтительно, что использование операторов в виде слов and и compl.

Возвращаемые значения

Не заключайте простые выражения return в скобки.

Используйте скобки в return expr; только если бы вы использовали их в выражении вида x = expr;.

return result;                  // Простое выражение - нет скобок // Скобки - Ок. Они улучшают читабельность выражения return (some_long_condition &&         another_condition); 
return (value);                // Плохо. Например, вы бы не стали писать var = (value); return(result);                // Плохо. return - это не функция! 

Инициализация переменных и массивов

Что использовать: =, () или
{} — это ваш выбор.

Вы можете выбирать между вариантами =,
() и {}. Следующие примеры кода корректны:

int x = 3; int x(3); int x{3}; std::string name = "Some Name"; std::string name("Some Name"); std::string name{"Some Name"}; 

Будьте внимательны при использовании списка инициализации {…} для типа, у которого есть конструктор с std::initializer_list.
Компилятор предпочтёт использовать конструктор std::initializer_list при наличии списка в фигурных скобках. Заметьте, что пустые фигурные скобки {} — это особый случай и будет вызван конструктор по-умолчанию (если он доступен). Для явного использования конструктора без std::initializer_list применяйте круглые скобки вместо фигурных.

std::vector<int> v(100, 1);  // Вектор из сотни единиц std::vector<int> v{100, 1};  // Вектор из 2-х элементов: 100 и 1 

Также конструирование с фигурными скобками запрещает ряд преобразований целых типов (преобразования с уменьшением точности). И можно получить ошибки компиляции.

int pi(3.14);  // Ок: pi == 3 int pi{3.14};  // Ошибка компиляции: "сужающее" преобразование 

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

Знак # (признак директивы препроцессора) должен быть в начале строки.

Даже если директива препроцессора относится к вложенному коду, директивы пишутся с начала строки.

// Хорошо - директивы с начала строки   if (lopsided_score) { #if DISASTER_PENDING      // Корректно - начинается с начала строки     DropEverything(); # if NOTIFY               // Пробелы после # - ок, но не обязательно     NotifyClient(); # endif #endif     BackToNormal();   } 
// Плохо - директивы с отступами   if (lopsided_score) {     #if DISASTER_PENDING  // Неправильно! "#if" должна быть в начале строки     DropEverything();     #endif                // Неправильно! Не делайте отступ для "#endif"     BackToNormal();   } 

Форматирование классов

Размещайте секции в следующем порядке: public, protected и private. Отступ — один пробел.

Ниже описан базовый формат для класса (за исключением комментариев, см. описание Комментирование класса):

class MyClass : public OtherClass {  public:      // Отступ 1 пробел   MyClass();  // Обычный 2-х пробельный отступ   explicit MyClass(int var);   ~MyClass() {}    void SomeFunction();   void SomeFunctionThatDoesNothing() {   }    void set_some_var(int var) { some_var_ = var; }   int some_var() const { return some_var_; }   private:   bool SomeInternalFunction();    int some_var_;   int some_other_var_; }; 

Замечания:

  • Имя базового класса пишется в той же строке, что и имя наследуемого класса (конечно, с учётом ограничения в 80 символов).
  • Ключевые слова public:, protected:, и private: должны быть с отступом в 1 пробел.
  • Перед каждым из этих ключевых слов должна быть пустая строка (за исключением первого упоминания). Также в маленьких классах пустые строки можно опустить.
  • Не добавляйте пустую строку после этих ключевых слов.
  • Секция public должна быть первой, за ней protected и в конце секция private.
  • См. Порядок объявления для выстраивания деклараций в каждой из этих секций.

Списки инициализации конструктора

Списки инициализации конструктора могут быть как в одну строку, так и на нескольких строках с 4-х пробельным отступом.

Ниже представлены правильные форматы для списков инициализации:

// Всё в одну строку MyClass::MyClass(int var) : some_var_(var) {   DoSomething(); }  // Если сигнатура и список инициализации не помещается на одной строке, // нужно перенести двоеточие и всё что после него на новую строку MyClass::MyClass(int var)     : some_var_(var), some_other_var_(var + 1) {   DoSomething(); }  // Если список занимает несколько строк, то размещайте каждый элемент на // отдельной строке и всё выравниваем MyClass::MyClass(int var)     : some_var_(var),             // Отступ 4 пробела       some_other_var_(var + 1) {  // Выравнивание по предыдущему   DoSomething(); }  // Как и в других случаях, фигурные скобки могут размещаться на одной строке MyClass::MyClass(int var)     : some_var_(var) {} 

Форматирование пространств имён

Содержимое в пространстве имён пишется без отступа.

Пространство имён не добавляет отступов. Например:

namespace {  void foo() {  // Хорошо. Без дополнительного отступа   ... }  }  // namespace 

Не делайте отступов в пространстве имён:

namespace {    // Плохо. Сделан отступ там, где не нужно   void foo() {     ...   }  }  // namespace 

При объявлении вложенных пространств имён, размещайте каждое объявление на отдельной строке.

namespace foo { namespace bar { 

Горизонтальная разбивка

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

Общие принципы

void f(bool b) {  // Перед открывающей фигурной скобкой всегда ставьте пробел   ... int i = 0;  // Обычно перед точкой с запятой нет пробела // Пробелы внутри фигурных скобок для списка инициализации можно добавлять на ваш выбор. // Если вы добавляете пробелы, то ставьте их с обеих сторон int x[] = { 0 }; int x[] = {0};  // Пробелы вокруг двоеточия в списках наследования и инициализации class Foo : public Bar {  public:   // Для inline-функции добавляйте    // пробелы внутри фигурных скобок (кроме пустого блока)   Foo(int b) : Bar(), baz_(b) {}  // Пустой блок без пробелов   void Reset() { baz_ = 0; }  // Пробелы разделяют фигурные скобки и реализацию   ... 

Добавление разделительных пробелов может мешать при слиянии кода. Поэтому: Не добавляйте разделительных пробелов в существующий код. Вы можете удалить пробелы, если уже модифицировали эту строку. Или сделайте это отдельной операцией (предпочтительно, чтобы с этим кодом при этом никто не работал).

Циклы и условия

if (b) {          // Пробел после ключевого слова в условии или цикле } else {          // Пробелы вокруг else } while (test) {}   // Внутри круглых скобок обычно не ставят пробел switch (i) { for (int i = 0; i < 5; ++i) { // Циклы и условия могут могут внутри быть с пробелам. Но это редкость. // В любом случае, будьте последовательны switch ( i ) { if ( test ) { for ( int i = 0; i < 5; ++i ) { // В циклах после точки с запятой всегда ставьте пробел // Также некоторые любят ставить пробел и перед точкой с запятой, но это редкость for ( ; i < 5 ; ++i) {   ...  // В циклы по диапазону всегда ставьте пробел до двоеточия и после for (auto x : counts) {   ... } switch (i) {   case 1:         // Перед двоеточнием в case нет пробела     ...   case 2: break;  // После двоеточия есть пробел, если дальше (на той же строке) идёт код 

Операторы

// Операторы присваивания всегда окружайте пробелами x = 0;  // Другие бинарные операторы обычно окружаются пробелами, // хотя допустимо умножение/деление записывать без пробелов. // Между выражением внутри скобок и самими скобками не вставляйте пробелы v = w * x + y / z; v = w*x + y/z; v = w * (x + z);  // Унарные операторы не отделяйте от их аргумента x = -5; ++x; if (x && !y)   ... 

Шаблоны и приведение типов

// Не ставьте пробелы внутри угловых скобок (< и >), // перед <, между >( в приведении std::vector<std::string> x; y = static_cast<char*>(x);  // Пробелы между типом и знаком указателя вполне допустимы. Но смотрите на уже используемый формат кода std::vector<char *> x; 

Вертикальная разбивка

Сведите к минимуму вертикальное разбиение.

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

Базовый принцип: чем больше кода поместится на одном экране, тем легче его понять и отследить последовательность выполнения. Используйте пустую строку исключительно с целью визуально разделить эту последовательность.

Несколько полезных замечаний о пустых строках:

  • Пустая строка в начале или в конце функции не улучшит читабельность.
  • Пустые строки в цепочке блоков if-else могут улучшить читабельность.
  • Пустая строка перед строкой с комментарием обычно помогает читабельности кода — новый комментарий обычно предполагает завершение старой мысли и начало новой идеи. И пустая строка явно на это намекает.

ссылка на оригинал статьи https://habr.com/ru/post/499944/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *