Рекомендации по написанию кода — MRTK2

В этом документе описываются принципы написания кода и соглашения, которые следует соблюдать при участии в MRTK.


Философия

Будьте краткими и стремиться к простоте

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

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

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

Создание согласованного, удобочитаемого кода

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

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

Поддержка настройки компонентов в редакторе и во время выполнения

MRTK поддерживает разнообразный набор пользователей— пользователей, которые предпочитают настраивать компоненты в редакторе Unity и загружать префабы, а также людей, которым необходимо создавать экземпляры и настраивать объекты во время выполнения.

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

Play-in-editor — это ваша первая и основная целевая платформа

Play-In-Editor — это самый быстрый способ итерации в Unity. Предоставление клиентам способов быстрой итерации позволяет им быстрее разрабатывать решения и пробовать больше идей. Другими словами, максимизация скорости итерации позволяет нашим клиентам достичь большего.

Сделайте все, что работает в редакторе, а затем сделайте его работой на любой другой платформе. Продолжайте работать в редакторе. Добавить новую платформу в play-In-Editor легко. Очень трудно работать с play-In-Editor, если ваше приложение работает только на устройстве.

Добавление новых открытых полей, свойств, методов и сериализованных частных полей с осторожностью

Каждый раз, когда вы добавляете открытый метод, поле, свойство, оно становится частью общедоступной области API MRTK. Закрытые поля, [SerializeField] помеченные также как открытые для редактора, и являются частью общедоступной области API. Другие пользователи могут использовать этот открытый метод, настраивать настраиваемые префабы с общедоступным полем и зависеть от него.

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

Определение приоритета при написании тестов

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

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

C# соглашения о написании кода

Заголовки с информацией о лицензии сценария

Все сотрудники Корпорации Майкрософт, которые вносят новые файлы, должны добавить следующий стандартный заголовок лицензии в верхней части всех новых файлов, как показано ниже:

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

Заголовки сводки по функциям и методу

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

/// <summary>
/// The Controller definition defines the Controller as defined by the SDK / Unity.
/// </summary>
public struct Controller
{
    /// <summary>
    /// The ID assigned to the Controller
    /// </summary>
    public string ID;
}

Это гарантирует правильное создание и распространение документации для всех классов, методов и свойств.

Все файлы сценариев, отправленные без соответствующих тегов сводных сведений, будут отклонены.

Правила пространства имен MRTK

Набор средств Смешанная реальность использует модель пространства имен на основе функций, где все базовые пространства имен начинаются с Microsoft.MixedReality.Toolkit. Как правило, вам не нужно указывать уровень набора средств (например, Core, Providers, Services) в пространствах имен.

В настоящее время определенные пространства имен:

  • Microsoft.MixedReality.Toolkit
  • Microsoft.MixedReality.Toolkit.Boundary
  • Microsoft.MixedReality.Toolkit.Diagnostics
  • Microsoft.MixedReality.Toolkit.Editor
  • Microsoft.MixedReality.Toolkit.Input
  • Microsoft.MixedReality.Toolkit.SpatialAwareness
  • Microsoft.MixedReality.Toolkit.Teleport
  • Microsoft.MixedReality.Toolkit.Utilities

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

Если пропустить пространство имен для интерфейса, класса или типа данных, изменение будет заблокировано.

Добавление новых скриптов MonoBehaviour

При добавлении новых скриптов MonoBehaviour с запросом на вытягивание убедитесь, что AddComponentMenu атрибут применяется ко всем применимым файлам. Это гарантирует, что компонент легко обнаруживается в редакторе под кнопкой "Добавить компонент ". Флаг атрибута не является обязательным, если компонент не может отображаться в редакторе, например абстрактном классе.

В приведенном ниже примере пакет должен быть заполнен расположением пакета компонента. Если элемент размещается в папке MRTK или SDK , пакет будет пакетом SDK.

[AddComponentMenu("Scripts/MRTK/{Package here}/MyNewComponent")]
public class MyNewComponent : MonoBehaviour

Добавление новых скриптов инспектора Unity

Как правило, старайтесь не создавать пользовательские скрипты инспектора для компонентов MRTK. Он добавляет дополнительные издержки и управление базой кода, которая может обрабатываться подсистемой Unity.

Если требуется класс инспектора, попробуйте использовать Unity DrawDefaultInspector(). Это снова упрощает класс инспектора и оставляет большую часть работы в Unity.

public override void OnInspectorGUI()
{
    // Do some custom calculations or checks
    // ....
    DrawDefaultInspector();
}

Если в классе инспектора требуется пользовательская отрисовка, попробуйте использовать SerializedProperty и EditorGUILayout.PropertyField. Это обеспечит правильную обработку вложенных префабов и измененных значений в Unity.

Если EditorGUILayout.PropertyField не удается использовать из-за требования в пользовательской логике, убедитесь, что все использование упаковывается вокруг EditorGUI.PropertyScope. Это обеспечит правильную отрисовку инспектора для вложенных префабов и измененных значений с заданным свойством.

Кроме того, попробуйте украсить класс настраиваемого инспектора с помощью .CanEditMultipleObjects Этот тег гарантирует, что несколько объектов с этим компонентом в сцене могут быть выбраны и изменены вместе. Все новые классы инспекторов должны проверить, работает ли их код в этой ситуации на сцене.

    // Example inspector class demonstrating usage of SerializedProperty & EditorGUILayout.PropertyField
    // as well as use of EditorGUI.PropertyScope for custom property logic
    [CustomEditor(typeof(MyComponent))]
    public class MyComponentInspector : UnityEditor.Editor
    {
        private SerializedProperty myProperty;
        private SerializedProperty handedness;

        protected virtual void OnEnable()
        {
            myProperty = serializedObject.FindProperty("myProperty");
            handedness = serializedObject.FindProperty("handedness");
        }

        public override void OnInspectorGUI()
        {
            EditorGUILayout.PropertyField(destroyOnSourceLost);

            Rect position = EditorGUILayout.GetControlRect();
            var label = new GUIContent(handedness.displayName);
            using (new EditorGUI.PropertyScope(position, label, handedness))
            {
                var currentHandedness = (Handedness)handedness.enumValueIndex;

                handedness.enumValueIndex = (int)(Handedness)EditorGUI.EnumPopup(
                    position,
                    label,
                    currentHandedness,
                    (value) => {
                        // This function is executed by Unity to determine if a possible enum value
                        // is valid for selection in the editor view
                        // In this case, only Handedness.Left and Handedness.Right can be selected
                        return (Handedness)value == Handedness.Left
                        || (Handedness)value == Handedness.Right;
                    });
            }
        }
    }

Добавление новых объектов ScriptableObjects

При добавлении новых скриптов ScriptableObject убедитесь, что CreateAssetMenu атрибут применяется ко всем применимым файлам. Это гарантирует, что компонент легко обнаруживается в редакторе с помощью меню создания ресурса. Флаг атрибута не является обязательным, если компонент не может отображаться в редакторе, например абстрактном классе.

В приведенном ниже примере вложенная папка должна быть заполнена вложенной папкой MRTK, если применимо. При размещении элемента в папке MRTK/Providers пакет будет поставщиком. Если элемент размещается в папке MRTK/Core , задайте для этого параметра значение "Профили".

В приведенном ниже примере | MyNewService MyNewProvider должен быть заполнен именем нового класса, если применимо. При размещении элемента в папке MixedRealityToolkit оставьте эту строку.

[CreateAssetMenu(fileName = "MyNewProfile", menuName = "Mixed Reality Toolkit/{Subfolder}/{MyNewService | MyNewProvider}/MyNewProfile")]
public class MyNewProfile : ScriptableObject

Ведение журнала

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

Интересный пример использования ведения журнала (вместе с интересными полезными данными):

DebugUtilities.LogVerboseFormat("RaiseSourceDetected: Source ID: {0}, Source Type: {1}", source.SourceId, source.SourceType);

Этот тип ведения журнала может помочь перехватывать такие проблемы, как https://github.com/microsoft/MixedRealityToolkit-Unity/issues/8016, которые были вызваны несовпадениями обнаруженных и потерянных событий источника.

Избегайте добавления журналов для данных и событий, происходящих на каждом кадре. В идеале ведение журнала должно охватывать "интересные" события, управляемые различными входными данными пользователя (т. е. "щелчок" пользователем и набор изменений и событий, которые поступают из них интересны для ведения журнала). Текущее состояние "пользователь по-прежнему держит жест", регистрируемый каждый кадр, не является интересным и приведет к перегрузке журналов.

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

Пробелы и вкладки

Не забудьте использовать 4 пробела вместо вкладок при участии в этом проекте.

Интервал

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

Не рекомендуется

private Foo()
{
    int[ ] var = new int [ 9 ];
    Vector2 vector = new Vector2 ( 0f, 10f );
}

Рекомендуется

private Foo()
{
    int[] var = new int[9];
    Vector2 vector = new Vector2(0f, 10f);
}

Соглашения об именах

Всегда используйте PascalCase для свойств. Используется camelCase для большинства полей, за исключением использования PascalCase и static readonlyconst полей. Единственным исключением из этого является структура данных, требующая сериализации полей с помощью JsonUtility.

Не рекомендуется

public string myProperty; // <- Starts with a lowercase letter
private string MyField; // <- Starts with an uppercase letter

Рекомендуется

public string MyProperty;
protected string MyProperty;
private static readonly string MyField;
private string myField;

Модификаторы доступа

Всегда объявляйте модификатор доступа для всех полей, свойств и методов.

  • Все методы API Unity должны быть по умолчанию равны private, если их не нужно переопределять в производном классе. В этом случае следует использовать protected.

  • Поля всегда должны иметь значение private, с методами доступа к свойствам public или protected.

  • По возможности используйте элементы, воплощаемые в выражениях, и автоматические свойства

Не рекомендуется

// protected field should be private
protected int myVariable = 0;

// property should have protected setter
public int MyVariable => myVariable;

// No public / private access modifiers
void Foo() { }
void Bar() { }

Рекомендуется

public int MyVariable { get; protected set; } = 0;

private void Foo() { }
public void Bar() { }
protected virtual void FooBar() { }

Использование фигурных скобок

Всегда используйте фигурные скобки после каждого блока операторов и поместите их на следующую строку.

Не рекомендуется

private Foo()
{
    if (Bar==null) // <- missing braces surrounding if action
        DoThing();
    else
        DoTheOtherThing();
}

Не рекомендуется

private Foo() { // <- Open bracket on same line
    if (Bar==null) DoThing(); <- if action on same line with no surrounding brackets
    else DoTheOtherThing();
}

Рекомендуется

private Foo()
{
    if (Bar==true)
    {
        DoThing();
    }
    else
    {
        DoTheOtherThing();
    }
}

Открытые классы, структуры и перечисления должны находиться в собственных файлах

Если класс, структуру или перечисление можно сделать закрытым, можно включить в тот же файл. Это позволяет избежать проблем с компиляцией в Unity и обеспечить правильную абстракцию кода, а также снижает конфликты и критические изменения при необходимости изменения кода.

Не рекомендуется

public class MyClass
{
    public struct MyStruct() { }
    public enum MyEnumType() { }
    public class MyNestedClass() { }
}

Рекомендуется

 // Private references for use inside the class only
public class MyClass
{
    private struct MyStruct() { }
    private enum MyEnumType() { }
    private class MyNestedClass() { }
}

Рекомендуется

MyStruct.cs

// Public Struct / Enum definitions for use in your class.  Try to make them generic for reuse.
public struct MyStruct
{
    public string Var1;
    public string Var2;
}

MyEnumType.cs

public enum MuEnumType
{
    Value1,
    Value2 // <- note, no "," on last value to denote end of list.
}

MyClass.cs

public class MyClass
{
    private MyStruct myStructReference;
    private MyEnumType myEnumReference;
}

Инициализация перечислений

Чтобы убедиться, что все перечисления инициализируются правильно, начиная с 0, .NET предоставляет аккуратный ярлык для автоматической инициализации перечисления, просто добавив первое (начальное) значение. (Например, значение 1 = 0 оставшихся значений не требуется)

Не рекомендуется

public enum Value
{
    Value1, <- no initializer
    Value2,
    Value3
}

Рекомендуется

public enum ValueType
{
    Value1 = 0,
    Value2,
    Value3
}

Порядок перечислений для соответствующего расширения

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

Не рекомендуется

public enum SDKType
{
    WindowsMR,
    OpenVR,
    OpenXR,
    None, <- default value not at start
    Other <- anonymous value left to end of enum
}

Рекомендуется

/// <summary>
/// The SDKType lists the VR SDKs that are supported by the MRTK
/// Initially, this lists proposed SDKs, not all may be implemented at this time (please see ReleaseNotes for more details)
/// </summary>
public enum SDKType
{
    /// <summary>
    /// No specified type or Standalone / non-VR type
    /// </summary>
    None = 0,
    /// <summary>
    /// Undefined SDK.
    /// </summary>
    Other,
    /// <summary>
    /// The Windows 10 Mixed reality SDK provided by the Universal Windows Platform (UWP), for Immersive MR headsets and HoloLens.
    /// </summary>
    WindowsMR,
    /// <summary>
    /// The OpenVR platform provided by Unity (does not support the downloadable SteamVR SDK).
    /// </summary>
    OpenVR,
    /// <summary>
    /// The OpenXR platform. SDK to be determined once released.
    /// </summary>
    OpenXR
}

Проверка использования перечисления для битовых полей

Если существует возможность перечисления требовать несколько состояний в качестве значения, например Handedness = Left & Right. Затем перечисление должно быть правильно украшено BitFlags, чтобы обеспечить его правильное использование.

Файл Handedness.cs имеет конкретную реализацию для этого.

Не рекомендуется

public enum Handedness
{
    None,
    Left,
    Right
}

Рекомендуется

[Flags]
public enum Handedness
{
    None = 0 << 0,
    Left = 1 << 0,
    Right = 1 << 1,
    Both = Left | Right
}

Жестко заданные пути к файлам

При создании путей к строковым файлам и, в частности, записи жестко заданных строковых путей выполните следующие действия.

  1. Используйте API-интерфейсы C#Path, если это возможно, напримерPath.Combine.Path.GetFullPath
  2. Используйте / или Path.DirectorySeparatorChar вместо \ или \\.

Эти действия гарантируют, что MRTK работает как в Windows, так и в системах под управлением Unix.

Не рекомендуется

private const string FilePath = "MyPath\\to\\a\\file.txt";
private const string OtherFilePath = "MyPath\to\a\file.txt";

string filePath = myVarRootPath + myRelativePath;

Рекомендуется

private const string FilePath = "MyPath/to/a/file.txt";
private const string OtherFilePath = "folder{Path.DirectorySeparatorChar}file.txt";

string filePath = Path.Combine(myVarRootPath,myRelativePath);

// Path.GetFullPath() will return the full length path of provided with correct system directory separators
string cleanedFilePath = Path.GetFullPath(unknownSourceFilePath);

Рекомендации, включая рекомендации по Unity

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

Инкапсуляция

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

Примечание

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

Не рекомендуется

private float myValue1;
private float myValue2;

public float MyValue1
{
    get{ return myValue1; }
    set{ myValue1 = value }
}

public float MyValue2
{
    get{ return myValue2; }
    set{ myValue2 = value }
}

Рекомендуется

// Enable field to be configurable in the editor and available externally to other scripts (field is correctly serialized in Unity)
[SerializeField]
[ToolTip("If using a tooltip, the text should match the public property's summary documentation, if appropriate.")]
private float myValue; // <- Notice we co-located the backing field above our corresponding property.

/// <summary>
/// If using a tooltip, the text should match the public property's summary documentation, if appropriate.
/// </summary>
public float MyValue
{
    get => myValue;
    set => myValue = value;
}

/// <summary>
/// Getter/Setters not wrapping a value directly should contain documentation comments just as public functions would
/// </summary>
public float AbsMyValue
{
    get
    {
        if (MyValue < 0)
        {
            return -MyValue;
        }

        return MyValue
    }
}

Кэширование значений и их сериализация в сцене или заготовке по возможности

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

Не рекомендуется

void Update()
{
    gameObject.GetComponent<Renderer>().Foo(Bar);
}

Рекомендуется

[SerializeField] // To enable setting the reference in the inspector.
private Renderer myRenderer;

private void Awake()
{
    // If you didn't set it in the inspector, then we cache it on awake.
    if (myRenderer == null)
    {
        myRenderer = gameObject.GetComponent<Renderer>();
    }
}

private void Update()
{
    myRenderer.Foo(Bar);
}

Кэширование ссылок на материалы, не вызывайте ".material" каждый раз

Unity будет создавать новый материал каждый раз при использовании ".material", что приведет к утечке памяти при неправильной очистке.

Не рекомендуется

public class MyClass
{
    void Update()
    {
        Material myMaterial = GetComponent<Renderer>().material;
        myMaterial.SetColor("_Color", Color.White);
    }
}

Рекомендуется

// Private references for use inside the class only
public class MyClass
{
    private Material cachedMaterial;

    private void Awake()
    {
        cachedMaterial = GetComponent<Renderer>().material;
    }

    void Update()
    {
        cachedMaterial.SetColor("_Color", Color.White);
    }

    private void OnDestroy()
    {
        Destroy(cachedMaterial);
    }
}

Примечание

Кроме того, можно использовать свойство Unity под названием SharedMaterial, которое не создает новый материал при каждом обращении.

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

  • Используйте WINDOWS_UWP, чтобы использовать API, относящихся к UWP и не относящимся к Unity. Это не позволит им выполняться в редакторе или на неподдерживаемых платформах. Это эквивалентно UNITY_WSA && !UNITY_EDITOR и должно использоваться в пользу.
  • Используйте UNITY_WSA, чтобы использовать API Unity, зависящие от UWP, например пространство имен UnityEngine.XR.WSA. Это будет выполняться в редакторе, если для платформы задано значение UWP, а также во встроенных приложениях UWP.

Эта таблица поможет вам выбрать нужный вариант #if в зависимости от ваших сценариев использования и ожидаемых параметров сборки.

Платформа UWP IL2CPP UWP .NET Редактор
UNITY_EDITOR False False True
UNITY_WSA True True True
WINDOWS_UWP True True False
UNITY_WSA && !UNITY_EDITOR True True False
ENABLE_WINMD_SUPPORT True True Неверно
NETFX_CORE False True False

Предпочитайте использовать DateTime.UtcNow вместо DateTime.Now

DateTime.UtcNow работает быстрее, чем DateTime.Now. В предыдущем исследовании производительности мы обнаружили, что использование DateTime.Now значительно влияет на производительность, особенно при использовании в цикле Update(). Другие пользователи столкнулись с той же проблемой.

Используйте DateTime.UtcNow, если только вам не требуется локализованное время (согласно требованиям законодательства может понадобиться отобразить текущее время в часовом поясе пользователя). Если вы работаете с относительным временем (т. е. разница между последним обновлением и сейчас), рекомендуется использовать DateTime.UTCNow, чтобы избежать затрат на преобразование часового пояса.

Соглашения о написании кода PowerShell

Подмножество базы кода MRTK использует PowerShell для инфраструктуры конвейера и различных сценариев и служебных программ. Новый код PowerShell должен соответствовать стилю PoshCode.

См. также статью

C# соглашения о написании кода из MSDN