Definiowanie i odczytywanie atrybutów niestandardowych

Atrybuty umożliwiają kojarzenie informacji z kodem w sposób deklaratywny. Mogą również udostępniać element wielokrotnego użytku, który można zastosować do różnych elementów docelowych. Rozważmy element ObsoleteAttribute. Można go stosować do klas, struktur, metod, konstruktorów i nie tylko. Deklaruje, że element jest przestarzały. Następnie do kompilatora języka C# należy wyszukać ten atrybut i wykonać jakąś akcję w odpowiedzi.

Z tego samouczka dowiesz się, jak dodawać atrybuty do kodu, jak tworzyć i używać własnych atrybutów oraz jak używać niektórych atrybutów wbudowanych w platformę .NET.

Wymagania wstępne

Aby uruchomić platformę .NET, musisz skonfigurować maszynę. Instrukcje instalacji można znaleźć na stronie Pliki do pobrania platformy .NET. Tę aplikację można uruchomić w systemach Windows, Ubuntu Linux, macOS lub w kontenerze platformy Docker. Musisz zainstalować ulubiony edytor kodu. Poniższe opisy używają programu Visual Studio Code, który jest edytorem międzyplatformowym typu open source. Można jednak użyć dowolnych narzędzi, z których korzystasz.

Tworzenie aplikacji

Po zainstalowaniu wszystkich narzędzi utwórz nową aplikację konsolową platformy .NET. Aby użyć generatora wiersza polecenia, wykonaj następujące polecenie w ulubionej powłoce:

dotnet new console

To polecenie tworzy pliki projektów .NET bez kości. Uruchom polecenie dotnet restore , aby przywrócić zależności potrzebne do skompilowania tego projektu.

Nie trzeba uruchamiaćdotnet restore, ponieważ jest ona uruchamiana niejawnie przez wszystkie polecenia, które wymagają przywrócenia, takie jak dotnet new, , dotnet build, dotnet rundotnet test, , dotnet publish, i dotnet pack. Aby wyłączyć niejawne przywracanie, użyj --no-restore opcji .

Polecenie dotnet restore jest nadal przydatne w niektórych scenariuszach, w których jawne przywracanie ma sens, takie jak kompilacje ciągłej integracji w usługach Azure DevOps Services lub w systemach kompilacji, które muszą jawnie kontrolować, kiedy nastąpi przywracanie.

Aby uzyskać informacje na temat zarządzania kanałami informacyjnymi NuGet, zobacz dokumentacjędotnet restore.

Aby wykonać program, użyj polecenia dotnet run. Powinny zostać wyświetlone dane wyjściowe "Hello, World" w konsoli.

Dodawanie atrybutów do kodu

W języku C# atrybuty to klasy dziedziczone z klasy bazowej Attribute . Każda klasa dziedziczona z Attribute klasy może służyć jako rodzaj "tagu" w innych fragmentach kodu. Na przykład istnieje atrybut o nazwie ObsoleteAttribute. Ten atrybut sygnalizuje, że kod jest przestarzały i nie powinien już być używany. Ten atrybut należy umieścić na klasie, na przykład przy użyciu nawiasów kwadratowych.

[Obsolete]
public class MyClass
{
}

Chociaż klasa jest wywoływana ObsoleteAttribute, jest wymagana tylko do użycia [Obsolete] w kodzie. Większość kodu w języku C# jest zgodna z tą konwencją. Jeśli wybierzesz, możesz użyć pełnej nazwy [ObsoleteAttribute] .

W przypadku oznaczania klasy przestarzałą warto podać pewne informacje, dlaczego są przestarzałe i/lub co zamiast tego należy użyć. Aby podać to wyjaśnienie, dołącz parametr ciągu do atrybutu Przestarzałe.

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

Ciąg jest przekazywany jako argument do konstruktora ObsoleteAttribute , tak jakby pisać var attr = new ObsoleteAttribute("some string").

Parametry konstruktora atrybutu są ograniczone do prostych typów/literałów: bool, int, double, string, Type, enums, etc i tablic tych typów. Nie można użyć wyrażenia ani zmiennej. Możesz używać parametrów pozycyjnych lub nazwanych.

Tworzenie własnego atrybutu

Atrybut można utworzyć, definiując nową klasę dziedziczą po klasie bazowej Attribute .

public class MySpecialAttribute : Attribute
{
}

W poprzednim kodzie można użyć [MySpecial] (lub [MySpecialAttribute]) jako atrybutu w innym miejscu w bazie kodu.

[MySpecial]
public class SomeOtherClass
{
}

Atrybuty w bibliotece klas bazowych platformy .NET, takie jak ObsoleteAttribute wyzwalanie niektórych zachowań w kompilatorze. Jednak każdy utworzony atrybut działa tylko jako metadane i nie powoduje wykonania żadnego kodu w klasie atrybutów. To ty musisz wykonywać działania na tych metadanych w innym miejscu w kodzie.

Jest tu "gotcha", aby uważać. Jak wspomniano wcześniej, tylko niektóre typy mogą być przekazywane jako argumenty podczas używania atrybutów. Jednak podczas tworzenia typu atrybutu kompilator języka C# nie powstrzymuje tworzenia tych parametrów. W poniższym przykładzie utworzono atrybut z konstruktorem, który kompiluje się poprawnie.

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

Nie można jednak użyć tego konstruktora ze składnią atrybutów.

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

Powyższy kod powoduje błąd kompilatora, taki jak Attribute constructor parameter 'myClass' has type 'Foo', which is not a valid attribute parameter type

Jak ograniczyć użycie atrybutów

Atrybuty mogą być używane w następujących elementach docelowych. Powyższe przykłady pokazują je w klasach, ale mogą być również używane w następujących klasie:

  • Zestaw
  • Klasa
  • Konstruktor
  • Delegat
  • Wyliczenie
  • Zdarzenie
  • Pole
  • Parametr ogólny
  • Interfejs
  • Method
  • Moduł
  • Parametr
  • Właściwości
  • Returnvalue
  • Struktura

Podczas tworzenia klasy atrybutów domyślnie język C# umożliwia używanie tego atrybutu na dowolnym z możliwych obiektów docelowych atrybutów. Jeśli chcesz ograniczyć atrybut do określonych obiektów docelowych, możesz to zrobić przy użyciu klasy atrybutów AttributeUsageAttribute . To prawda, atrybut atrybutu atrybutu!

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

Jeśli próbujesz umieścić powyższy atrybut na czymś, co nie jest klasą lub strukturą, zostanie wyświetlony błąd kompilatora, taki jak 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()
    { }
}

Jak używać atrybutów dołączonych do elementu kodu

Atrybuty działają jako metadane. Bez jakiejś siły zewnętrznej, nie robią nic.

Aby znaleźć atrybuty i działać na ich podstawie, potrzebne jest odbicie. Emocje ion umożliwia pisanie kodu w języku C#, który analizuje inny kod. Na przykład możesz użyć Emocje ion, aby uzyskać informacje o klasie (dodać using System.Reflection; na początku kodu):

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

To drukuje coś takiego jak: The assembly qualified name of MyClass is ConsoleApplication.MyClass, attributes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

Po utworzeniu TypeInfo obiektu (lub MemberInfoobiektu , FieldInfolub innego obiektu) możesz użyć GetCustomAttributes metody . Ta metoda zwraca kolekcję Attribute obiektów. Można również użyć GetCustomAttribute i określić typ atrybutu.

Oto przykład użycia GetCustomAttributes w wystąpieniu MemberInfo ( MyClass które widzieliśmy wcześniej ma [Obsolete] na nim atrybut).

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

To drukuje do konsoli: Attribute on MyClass: ObsoleteAttribute. Spróbuj dodać inne atrybuty do MyClasselementu .

Należy pamiętać, że te Attribute obiekty są tworzone leniwie. Oznacza to, że nie są tworzone, dopóki nie zostanie użyta funkcja GetCustomAttribute lub GetCustomAttributes. Są one również tworzone za każdym razem. Wywołanie GetCustomAttributes dwukrotnie w wierszu zwraca dwa różne wystąpienia ObsoleteAttributeklasy .

Typowe atrybuty w środowisku uruchomieniowym

Atrybuty są używane przez wiele narzędzi i struktur. NUnit używa atrybutów takich jak [Test] i [TestFixture] , które są używane przez moduł uruchamiający testy NUnit. ASP.NET MVC używa atrybutów, takich jak [Authorize] i udostępnia strukturę filtru akcji do wykonywania problemów krzyżowych w akcjach MVC. PostSharp używa składni atrybutów, aby umożliwić programowanie zorientowane na aspekty w języku C#.

Oto kilka godnych uwagi atrybutów wbudowanych w biblioteki klas bazowych platformy .NET Core:

  • [Obsolete]. Ten został użyty w powyższych przykładach i znajduje się w System przestrzeni nazw. Warto podać dokumentację deklaratywną dotyczącą zmieniającej się bazy kodu. Komunikat można podać w postaci ciągu, a inny parametr logiczny może służyć do eskalacji z ostrzeżenia kompilatora do błędu kompilatora.
  • [Conditional]. Ten atrybut znajduje się w System.Diagnostics przestrzeni nazw. Ten atrybut można zastosować do metod (lub klas atrybutów). Należy przekazać ciąg do konstruktora. Jeśli ten ciąg nie jest zgodny z dyrektywą #define , kompilator języka C# usuwa wszystkie wywołania tej metody (ale nie samej metody). Zazwyczaj ta technika jest używana do debugowania (diagnostyki).
  • [CallerMemberName]. Ten atrybut może być używany w parametrach i znajduje się w System.Runtime.CompilerServices przestrzeni nazw. CallerMemberName jest atrybutem używanym do wstrzykiwania nazwy metody wywołującej inną metodę. Jest to sposób na wyeliminowanie "ciągów magicznych" podczas implementowania funkcji INotifyPropertyChanged w różnych strukturach interfejsu użytkownika. Przykład:
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
            }
        }
    }
}

W powyższym kodzie nie trzeba mieć ciągu literału "Name" . Użycie CallerMemberName zapobiega błędom związanym z literówkami, a także zapewnia płynniejszą refaktoryzację/zmianę nazwy. Atrybuty zapewniają deklaratywną moc w języku C#, ale są one meta-data formą kodu i nie działają samodzielnie.