共用方式為


定義和讀取自訂屬性

屬性提供以宣告方式將資訊與程式碼相關聯的方法。 這些屬性也可以提供可重複使用的元素,可以套用至各種目標。 以 ObsoleteAttribute 為例。 它可以套用至類別、結構、方法及建構函式等。 它宣告元素已過時。 然後是由 C# 編譯器來尋找此屬性,並執行一些動作做為回應。

在本教學課程中,您了解如何將屬性新增至您的程式碼、如何建立並使用您自己的屬性,以及如何使用一些 .NET 內建的屬性。

必要條件

您將必須設定您的機器才能執行 .NET。 您可以在 .NET 下載頁面上找到安裝指示。 您可以在 Windows、Ubuntu Linux、macOS 或在 Docker 容器中執行此應用程式。 您必須安裝您的慣用程式碼編輯器。 以下說明使用 Visual Studio Code,這是開放原始碼的跨平台編輯器。 不過,您可以使用您熟悉的任何工具。

建立 應用程式

現在您已安裝完所有工具,請建立新的 .NET 主控台應用程式。 若要使用命令列產生器,請在您慣用的殼層中執行下列命令︰

dotnet new console

此命令會建立相當基本的 .NET 專案檔。 您執行 dotnet restore 可以還原編譯此專案所需的相依性。

您不必執行 dotnet restore,因其是以隱含方式由需要還原的所有命令執行,例如 dotnet newdotnet builddotnet rundotnet testdotnet publishdotnet pack。 若要停用隱含還原,請使用 --no-restore 選項。

dotnet restore 命令在適合進行明確還原的特定案例中仍可派上用場,例如 Azure DevOps Services 中的持續整合組建,或在需要明確控制何時進行還原的組建系統中。

如需如何管理 NuGet 摘要的詳細資訊,請參閱 dotnet restore 文件

若要執行這個程式,請使用 dotnet run。 您應該會在主控台看到 "Hello, World" 輸出。

將屬性新增至程式碼

在 C# 中,屬性是繼承自 Attribute 基底類別的類別。 繼承自 Attribute 的任何類別都能在其他程式碼片段上做為一種「標記」。 舉例來說,有一個名為 ObsoleteAttribute 的屬性。 這個屬性發出訊號表示程式碼已過時,而且不應該再使用。 您將此屬性放置在類別上,例如透過使用方括弧。

[Obsolete]
public class MyClass
{
}

雖然此類別名為 ObsoleteAttribute,但在程式碼中只需要使用 [Obsolete]。 大部分的 C# 程式碼都遵循此慣例。 您也可以選擇使用完整名稱 [ObsoleteAttribute]

將類別標示為過時的時候,最好提供一些像是它為什麼過時及/或應改用什麼的資訊。 您將字串參數包含在 Obsolete 屬性中可以提供這類的說明。

[Obsolete("ThisClass is obsolete. Use ThisClass2 instead.")]
public class ThisClass
{
}

字串會以引數的方式傳遞至 ObsoleteAttribute 建構函式,如同您撰寫 var attr = new ObsoleteAttribute("some string") 一般。

傳遞到屬性建構函式的參數僅限於簡單型別/常值︰bool, int, double, string, Type, enums, etc 和這些型別的陣列。 您不能使用運算式或變數。 您可以自由使用位置或具名參數。

建立您自己的屬性

您透過定義繼承自 Attribute 基底類別的新類別來建立屬性。

public class MySpecialAttribute : Attribute
{
}

使用上述程式碼時,您可以在程式碼基底的其他位置使用 [MySpecial] (或 [MySpecialAttribute]) 做為屬性。

[MySpecial]
public class SomeOtherClass
{
}

.NET 基底類別庫中的屬性 (如 ObsoleteAttribute) 會觸發編譯器中的特定行為。 不過,您所建立的任何屬性僅做為中繼資料,不會在執行的屬性類別內產生任何程式碼。 您可以選擇在程式碼的其他位置對中繼資料採取動作。

此處有「陷阱」要注意。 如上所述,使用屬性時,只有特定型別可做為引數傳遞。 不過,在建立屬性型別時,C# 編譯器不會阻止您建立這些參數。 在下列範例中,您使用正確編譯的建構函式建立屬性。

public class GotchaAttribute : Attribute
{
    public GotchaAttribute(Foo myClass, string str)
    {
    }
}

不過,您無法將此建構函式與屬性語法搭配使用。

[Gotcha(new Foo(), "test")] // does not compile
public class AttributeFail
{
}

上述程式碼會導致編譯器錯誤,如 Attribute constructor parameter 'myClass' has type 'Foo', which is not a valid attribute parameter type

如何限制屬性使用方式

屬性可用於下列「目標」。 上述範例示範屬性用於類別,但屬性也可以用於:

  • 組件
  • 類別
  • 建構函式
  • Delegate
  • 列舉
  • Event
  • 欄位
  • GenericParameter
  • 介面
  • 方法
  • 模組
  • 參數
  • 屬性
  • ReturnValue
  • 結構

當您建立屬性類別時,根據預設,C# 允許您將該屬性用於任何可能的屬性目標。 如果您想要將您的屬性限制在特定目標,您可以在屬性類別中使用 AttributeUsageAttribute 來完成。 沒錯,屬性中的屬性!

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class MyAttributeForClassAndStructOnly : Attribute
{
}

如果您嘗試將上述屬性放在類別或結構以外的項目,您會收到如下的編譯器錯誤 Attribute 'MyAttributeForClassAndStructOnly' is not valid on this declaration type. It is only valid on 'class, struct' declarations

public class Foo
{
    // if the below attribute was uncommented, it would cause a compiler error
    // [MyAttributeForClassAndStructOnly]
    public Foo()
    { }
}

如何使用附加至程式碼元素的屬性

屬性是做為中繼資料。 在無外力介入的情況下,屬性不會實際執行任何動作。

若要尋找屬性並對屬性採取動作,您需要反映。 反映可讓您以 C# 撰寫程式碼來檢查其他程式碼。 比方說,您可以使用 Reflection 取得類別的相關資訊 (在程式碼的前端新增 using System.Reflection;):

TypeInfo typeInfo = typeof(MyClass).GetTypeInfo();
Console.WriteLine("The assembly qualified name of MyClass is " + typeInfo.AssemblyQualifiedName);

這會列印如下的內容:The assembly qualified name of MyClass is ConsoleApplication.MyClass, attributes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

一旦您擁有 TypeInfo 物件 (或是 MemberInfoFieldInfo 或其他物件),您就可以使用 GetCustomAttributes 方法。 這個方法會傳回 Attribute 物件的集合。 您也可以使用 GetCustomAttribute 並指定一個「屬性」型別。

以下是在 MyClassMemberInfo 執行個體上使用 GetCustomAttributes 的範例 (我們之前已看到其中有一個 [Obsolete] 屬性)。

var attrs = typeInfo.GetCustomAttributes();
foreach(var attr in attrs)
    Console.WriteLine("Attribute on MyClass: " + attr.GetType().Name);

這會列印至主控台:Attribute on MyClass: ObsoleteAttribute。 嘗試將其他屬性加入 MyClass

務必注意的是,這些 Attribute 物件會延遲具現化。 也就是說,在您使用 GetCustomAttributeGetCustomAttributes 之前,這些物件不會具現化。 這些物件也會每次都具現化。 在一個資料列中呼叫 GetCustomAttributes 兩次,會傳回兩個不同的 ObsoleteAttribute 執行個體。

執行階段中的常見屬性

許多工具和架構都會使用屬性。 NUnit 使用 NUnit 測試執行器使用的屬性 (例如 [Test][TestFixture])。 ASP.NET MVC 使用 [Authorize] 之類的屬性,並提供動作篩選架構來執行關於 MVC 動作的交叉關注。 PostSharp 使用屬性語法以允許 C# 中的層面導向程式設計。

以下是幾個值得注意的屬性,內建於 .NET Core 基底類別庫中︰

  • [Obsolete]. 此屬性使用於上述範例中,且存在於 System 命名空間。 若提供有關變更程式碼基底的宣告式文件會很有用。 訊息可以字串的形式提供,使用另一個布林值參數可以從編譯器警告提升至編譯器錯誤。
  • [Conditional]. 此屬性位於 System.Diagnostics 命名空間。 這個屬性可以套用至方法 (或屬性類別)。 您必須傳遞字串給建構函式。 如果該字串不符合 #define 指示詞,則 C# 編譯器會移除對該方法的任何呼叫 (但非方法本身)。 這項技巧通常用於偵錯 (診斷) 用途。
  • [CallerMemberName]. 這個屬性可以用於參數,並存在 System.Runtime.CompilerServices 命名空間中。 CallerMemberName 屬性可以用來插入呼叫其他方法的方法名稱。 在各種 UI 架構中實作 INotifyPropertyChanged 時,這是一個消除 'magic strings' 的方法。 例如:
public class MyUIClass : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    public void RaisePropertyChanged([CallerMemberName] string propertyName = default!)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private string? _name;
    public string? Name
    {
        get { return _name;}
        set
        {
            if (value != _name)
            {
                _name = value;
                RaisePropertyChanged();   // notice that "Name" is not needed here explicitly
            }
        }
    }
}

在上述程式碼中,您不必使用常值 "Name" 字串。 使用 CallerMemberName 有助於避免打字錯誤相關的問題,也可使重構/重新命名更順暢。 屬性為 C# 帶來宣告式能力,但屬性是程式碼的中繼資料形式,無法自行運作。