3. Code convention

Архитектура и файлы

  1. Каждый отдельный объект, например класс, интерфейс или структура, должен находиться в отдельном файле.
  2. По возможности функционал выносится в отдельные модули или ООП-классы.
  3. Архитектура проекта строится в data-oriented подходе: данные, логика обработки и представление должны быть разделены там, где это снижает связность и упрощает обработку данных.
  4. Runtime-данные, конфигурации и визуальное представление должны быть разделены.
  5. В проекте обязательно использование namespace верхнего уровня, сформированного на основании имени проекта.
  6. Каждый объект должен иметь модификатор доступа с минимально возможной видимостью.

Работа с данными

  1. Явно описывать владельца данных и жизненный цикл данных: кто создает, кто изменяет, кто очищает.
  2. В runtime-коде избегать прямого обращения к глобальному состоянию.
  3. Зависимости передавать явно через контекст, конструктор, метод инициализации или выделенный сервис.
  4. Для систем обработки данных фиксировать порядок выполнения явно, если результат зависит от порядка.
  5. Стараться не выполнять повторный поиск или сбор рабочих наборов данных внутри горячих участков кода.
  6. Если набор данных требуется многократно, использовать заранее подготовленный буфер, кеш или явный контекст обработки.
  7. Стараться читать значение данных или объекта один раз в пределах одной операции обработки, если повторное чтение не требуется логикой.
  8. Для часто обрабатываемых массивов данных предпочитать List<T> и NativeArray<T>.
  9. Стараться не использовать ссылочные типы в структурах данных, предназначенных для обработки в runtime.
  10. Для больших структур данных избегать копирования по значению без необходимости.
  11. Не обновлять данные, если их значение фактически не изменилось.
  12. Для value-типов, которые сравниваются по значению, требуется корректно определять Equals и GetHashCode или использовать явно заданный способ сравнения.
  13. Поля, которые инициализируются в методе, должны быть в том же порядке, в котором они объявлены в классе или структуре.

Unity и gameplay

  1. Не смешивать изменение данных и применение Unity-side effects в одном проходе.
  2. По возможности сначала считать или рассчитать изменения, затем отдельно применить их к Unity-объектам.
  3. Не использовать реактивные или event-driven механики как основной способ реализации gameplay-логики, если порядок выполнения, стоимость вызовов или жизненный цикл подписок становятся неочевидными.
  4. События допускаются для связи между модулями и уведомления о результатах выполнения логики.
  5. По возможности минимизировать проверку на null для наследников класса UnityEngine.Object.
  6. Запрещено использовать операторы ?? и ?. для наследников класса UnityEngine.Object.
  7. Запрещено в игровой логике использовать механизм перехвата исключений (try-catch), однако разрешается бросать исключения.
  8. Запрещено добавлять тихие сценарии ошибки.
  9. Рекомендуется редактировать префабы с отключенным Auto Save, если изменения затрагивают общие или часто используемые префабы.
  10. По возможности минимизировать сериализацию сложных объектов через инспектор.
  11. По возможности минимизировать наследование от класса MonoBehaviour.
  12. Подписка и отписка на ивенты должны располагаться в методах OnEnable и OnDisable.
  13. Каждому подписанию на ивент должна соответствовать отписка от ивента.

Производительность

  1. Для часто вызываемых участков кода избегать неявных аллокаций: LINQ, boxing, замыканий, временных коллекций и форматирования строк.
  2. Запрещено использовать Enum.HasFlag при проверке битовых флагов.
  3. Вместо Enum.HasFlag нужно использовать:
    • ==, если нужно совпадение с каждым битом из маски;
    • != Enum.None, если нужно совпадение с любым битом из маски.
  4. Метод Enum.HasFlag выполняет упаковку (boxing) входного значения.

Общие правила кода

  1. Запрещено использование любых магических чисел и строк в коде.
  2. Не злоупотреблять операторами break, return и continue в циклах и методах.
  3. Перед вызовом события всегда проверять наличие подписчиков или использовать безопасный вызов события.
  4. По умолчанию при объявлении класса выставлять ключевое слово sealed.
  5. Запрещается для события создавать свой собственный делегат.
  6. Предпочтительно использовать expression body вместо однострочных тел.
  7. Запрещается оставлять пустые тела методов, кроме случаев, когда метод предназначен для перегрузки в наследниках.
  8. При явном переопределении метода всегда вначале вызывается базовый функционал.
  9. В редких исключениях базовый функционал вызывается в конце метода.
  10. Запрещается использовать публичные изменяемые поля.
  11. Для внешнего доступа использовать свойства с ограниченной записью или методы изменения состояния.
  12. Перегружать методы необходимо только тогда, когда они совпадают по назначению и различаются только входными параметрами.
  13. Метод или конструктор не должен принимать более 6 параметров.
  14. Запрещено в одной строке объявлять более одной переменной.
  15. Для каждого значения enum необходимо явно указывать числовое значение.
  16. Любое перечисление должно содержать None.
  17. Если это int-перечисление, то None должен соответствовать 0.
  18. Битовые enum с [Flags] использовать только для масок и наборов независимых признаков, которые могут комбинироваться.
  19. Классы с методами расширения допускается объявлять partial.
  20. Нужно избегать разделения объявления и инициализации локальных переменных.
  21. Локальные методы рекомендуется объявлять внизу метода.
  22. Запрещено использовать ключевое слово unsafe без явного технического обоснования.
  23. Следует отдавать предпочтение перегрузке метода или конструктора для аргументов метода, а не использованию параметров по умолчанию.
  24. Импортируйте все пространства имен, которые используете в коде.

Float

  1. Следует избегать проверки float-переменных на неравенство.
  2. При необходимости использовать функцию Mathf.Approximately().
  3. Оператор != для сравнения float использовать запрещено.

Switch

Оператор switch обязательно должен содержать default case, который:

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

  1. Рекомендуется использовать IDE Rider.
  2. Использовать пробелы вместо табуляции.
  3. Размер отступа - 4 пробела.
  4. Форматирование должно быть закреплено в .editorconfig и применяться через Rider.
  5. Все регионы пишутся в формате PascalCase.
  6. В конструкции if/else пишем else на новой строке без пропуска на пустую строку.
  7. Запрещено использовать expression-body для реализации методов, если получившаяся строка кода занимает более одной строки или более 120 символов.

Пустые строки

  1. Между методами - 1 пустая строка.
  2. Между подключенными пространствами имен и объявлением класса или namespace - 1 пустая строка.
  3. Между методами и полями, свойствами или конструктором - 2 пустые строки.
  4. Между регионами - 2 пустые строки.
  5. Внутри региона отступ от директивы region - 1 пустая строка.
  6. Между методами, свойствами или классами внутри региона - 1 пустая строка.
  7. Между смысловыми блоками кода внутри метода или в полях - 1 пустая строка.

Перенос длинных строк

Длинные строки кода, более 120 символов, необходимо разносить на несколько строк.

При переносе придерживаться правил:

  1. Перенос строки должен быть после оператора.
  2. Перенос строки должен быть после запятой.
  3. Каждая строка, начиная со второй, должна находиться на следующем уровне табуляции по сравнению с первой строкой.

Комментарии

  1. Комментарии, как и весь код, должны быть написаны исключительно на английском языке.
  2. Размещайте комментарии на отдельной строке, а не в конце строки кода.
  3. Начинайте текст комментария с заглавной буквы.
  4. Между // и текстом должен быть один пробел.
  5. Не создавайте форматированные блоки звездочек вокруг комментариев.

Пример:

// The following declaration creates a query. It does not run the query.