在專案開發上遇到這樣的問題:每個繼承類別要提供類似的 static 函式,功能差不多,但只有要取用的值不一樣。就在想能不能把 static 函式拉到基礎類別上,但又可以依照繼承類別給與不同的 static 成員值。

如果把 static 函式放在繼承類別上,就得每個繼承類別寫類似的函式。如果把 static 函式放在基礎類別,又不能在基礎類別上取得繼承類別的 static 成員值,因為 static 函式不能宣告為 abstract 或是 virtual,所以基礎類別無法知道繼承類別有那個 static 成員。最後想到的方法是透過 attribute 配合 reflection 來取得繼承類別上的不同 static 成員值。

實作

在專案中,每個 Unity 場景都有一個啟動的程式碼,程式碼會提供對應的場景名稱。在場景載入後,就要執行這個程式碼。所以載入場景跟執行啟動程式碼的功能放在基礎類別上,而對應的場景名稱就在繼承類別上指定。

首先建立一個 attribute 類別用來儲存場景名稱的資訊,並限制 TargetSceneAttribute 只能用在類別上:

[AttributeUsage(AttributeTargets.Class)]
public class TargetSceneAttribute : Attribute
{
  public readonly string SceneName;

  public TargetSceneAttribute(string sceneName)
  {
    SceneName = sceneName;
  }
}

接著在基礎類別上的 static 函式取得繼承類別上的 attribute 值。attribute 只是記錄資訊而已,要透過 reflection 來取得上面的值:

public abstract class EntryPoint<T> : MonoBehaviour
  where T : EntryPoint<T>
{
  public static string GetSceneName()
  {
    var attrs = Attribute.GetCustomAttributes(typeof(T));
    foreach (var attr in attrs) {
      if (attr is TargetSceneAttribute targetSceneAttr)
        return targetSceneAttr.SceneName;
    }

    throw new MissingMemberException(
      "The class which inherits from the 'EntryPoint' " +
      "must have the 'TargetScene' attribute");
  }
}

最後在繼承的類別上加上指定場景名稱的屬性:

[TargetScene("Home")]
public class HomeEntryPoint : EntryPoint<HomeEntryPoint>
{}

這樣基礎類別就可以透過它的 static 函式去取得繼承類別上的 static 成員了:

Debug.Log(HomeEntryPoint.GetSceneName()); // "Home"

如果有一系列的繼承類別擁有不同的 static 成員值,就可以透過這樣的方式,由基礎類別提供 static 函式來取得值或是做後續處理。

應用

例如在專案中,在載入場景後就要去執行起始的程式碼,所以就在 EntryPoint 提供這樣的功能:

public abstract class EntryPoint<T> : MonoBehaviour
  where T : EntryPoint<T>
{
  public static string GetSceneName()
  {
    var attrs = Attribute.GetCustomAttributes(typeof(T));
    foreach (var attr in attrs) {
      if (attr is TargetSceneAttribute targetSceneAttr)
        return targetSceneAttr.SceneName;
    }

    throw new MissingMemberException(
      "The class which inherits from the 'EntryPoint' " +
      "must have the 'TargetScene' attribute");
  }

  // Load the target scene and execute the entry point
  public static string LoadScene()
  {
    SceneManager.LoadScene(GetSceneName());
    var entryPoint = (T)FindObjectOfType(typeof(T));
    entryPoint.StartCoroutine(Execute());
  }

  public abstract IEnumerator Execute();
}

這樣就可以透過基礎類別提供的 static 函式來載入場景跟對應的程式碼,而不用為每一個類別寫 static 的載入函式了:

HomeEntryPoint.LoadScene();

參考資料