程式碼撰寫指導方針 - MRTK2

本檔概述參與 MRTK 時要遵循的程式碼撰寫原則和慣例。


哲學

簡潔且致力於簡化

最簡單的解決方案通常是最好的解決方案。 這是這些指導方針的覆寫目標,應該是所有程式碼撰寫活動的目標。 簡單的一部分是簡潔且與現有的程式碼一致。 請嘗試讓您的程式碼保持簡單。

讀者應該只會遇到提供實用資訊的成品。 例如,重新指出明顯狀況的批註不會提供額外的資訊,並增加雜訊與訊號比率。

讓程式碼邏輯保持簡單。 請注意,這不是使用最少行數的語句、將識別碼名稱或大括弧樣式的大小降到最低,但關於減少概念數目,以及透過熟悉模式最大化這些概念的可見度。

產生一致且可讀取的程式碼

程式碼可讀性與低瑕疵率相互關聯。 致力於建立容易閱讀的程式碼。 致力於建立具有簡單邏輯並重複使用現有元件的程式碼,因為它也會協助確保正確性。

您產生之程式碼的所有詳細資料,從最基本的正確性詳細資料到一致的樣式和格式設定。 讓您的程式碼撰寫樣式與已經存在的專案保持一致,即使它不符合您的喜好設定也一樣。 這會增加整體程式碼基底的可讀性。

支援在編輯器和執行時間設定元件

MRTK 支援一組不同的使用者 – 偏好在 Unity 編輯器中設定元件並載入預製專案的人員,以及需要在執行時間具現化和設定物件的人員。

您的所有程式碼都應該透過將元件新增至已儲存場景中的 GameObject,以及在程式碼中具現化該元件來運作。 測試應該包含具現化預製專案和具現化的測試案例,並在執行時間設定元件。

播放編輯器是您的第一個主要目標平臺

Play-In-Editor 是逐一查看 Unity 最快的方式。 為客戶提供快速逐一查看的方式,可讓他們更快速地開發解決方案,並試用更多想法。 換句話說,最大化反復專案的速度可讓客戶達成更多目標。

讓所有專案在編輯器中運作,然後在任何其他平臺上運作。 讓它在編輯器中保持運作。 輕鬆地將新的平臺新增至 Play-In-Editor。 如果您的 app 只能在裝置上運作,則很難讓 Play-In-Editor 運作。

小心新增公用欄位、屬性、方法和序列化私用欄位

每次新增公用方法、欄位、屬性時,都會成為 MRTK 公用 API 介面的一部分。 標示為 [SerializeField] 的私人欄位也會向編輯器公開欄位,而且是公用 API 介面的一部分。 其他人可能會使用該公用方法、使用您的公用欄位設定自訂預製專案,並相依于它。

應仔細檢查新的公用成員。 未來必須維護任何公用欄位。 請記住,如果公用欄位的類型 (或序列化私用欄位) 變更或從 MonoBehaviour 中移除,這可能會中斷其他人。 欄位必須先針對版本淘汰,而且必須提供已取得相依性的人員移轉變更的程式碼。

設定撰寫測試的優先順序

MRTK 是一個社群專案,由各種參與者修改。 這些參與者可能不知道錯誤修正/功能的詳細資料,並意外中斷您的功能。 MRTK 會在 完成每個提取要求之前執行持續整合測試。 無法簽入中斷測試的變更。 因此,測試是確保其他人不會中斷功能的最佳方式。

當您修正錯誤時,請撰寫測試以確保它不會在未來回歸。 如果新增功能,請撰寫可驗證功能運作的測試。 除了實驗性功能以外,所有 UX 功能都需要這樣做。

C# 程式碼撰寫慣例

編寫授權資訊標頭的腳本

所有參與新檔案的 Microsoft 員工都應該在任何新檔案頂端新增下列標準授權標頭,如下所示:

// 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 命名空間規則

Mixed Reality Toolkit 使用功能型命名空間模型,其中所有基礎命名空間都是以 「Microsoft.MixedReality.Toolkit」 開頭。 一般而言,您不需要指定工具組層 (例如:命名空間中的核心、提供者、服務) 。

目前定義的命名空間如下:

  • 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 引擎可處理之程式碼基底的額外負荷和管理。

如果需要 Inspector 類別,請嘗試使用 Unity 的 DrawDefaultInspector() 。 這可再次簡化 Inspector 類別,並將大部分的工作保留給 Unity。

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

如果偵測器類別中需要自訂轉譯,請嘗試使用 SerializedPropertyEditorGUILayout.PropertyField 。 這可確保 Unity 正確地處理呈現巢狀預製專案和修改的值。

如果 EditorGUILayout.PropertyField 因為自訂邏輯的需求而無法使用,請確定所有使用方式都會包裝在 EditorGUI.PropertyScope 周圍。 這可確保 Unity 使用指定的 屬性正確呈現巢狀預製專案和修改值的偵測器。

此外,請嘗試使用 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 資料夾中放置專案,請將此設定為 「Profiles」。

在下列範例中, 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針對大部分欄位使用 ,但 用於 PascalCasestatic 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;

存取修飾詞

一律宣告所有欄位、屬性和方法的存取修飾詞。

  • 除非您需要在衍生類別中覆寫它們,否則所有 Unity API 方法都應該 private 預設為 。 在此情況下 protected ,應該使用 。

  • 欄位應一律為 private ,且具有 publicprotected 屬性存取子。

  • 盡可能使用運算式主體成員自動屬性

禁止事項

// 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
}

檢閱 bitfields 的列舉用法

如果列舉可能會要求多個狀態做為值,例如 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. 盡可能使用 C# 的Path API,例如 Path.CombinePath.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 以使用 UWP 特定的非 Unity API。 這可防止他們嘗試在編輯器中或在不支援的平臺上執行。 這相當於 UNITY_WSA && !UNITY_EDITOR 和 ,應該使用 。
  • 使用 UNITY_WSA 來使用 UWP 特定的 Unity API,例如 UnityEngine.XR.WSA 命名空間。 當平臺設定為 UWP,以及內建的 UWP app 時,這會在編輯器中執行。

此圖表可協助您根據使用案例和預期的組建設定,決定要使用哪 #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 False
NETFX_CORE False True False

偏好使用 DateTime.UtcNow over DateTime.Now

DateTime.UtcNow 比 DateTime.Now 快。 在先前的效能調查中,我們發現使用 DateTime.Now 會增加顯著的額外負荷,特別是在 Update () 迴圈中使用時。 其他人遇到相同的問題

建議您使用 DateTime.UtcNow,除非您實際需要當地語系化的時間 (合法原因可能是您想要在使用者的時區) 顯示目前時間。 如果您正在處理相對時間 (亦即,某些上次更新與現在) 之間的差異,最好使用 DateTime.UtcNow 來避免執行時區轉換的額外負荷。

PowerShell 編碼慣例

MRTK 程式碼基底的子集會針對管線基礎結構和各種腳本和公用程式使用 PowerShell。 新的 PowerShell 程式碼應遵循 PoshCode 樣式

另請參閱

MSDN 的 C# 編碼慣例