Определение и чтение настраиваемых атрибутов

Атрибуты предоставляют возможность декларативно связать информацию с кодом. Они также могут предоставлять многократно используемый элемент, который можно применять к различным целевым объектам. Рассмотрим ObsoleteAttribute. Его можно применять к классам, структурам, методам, конструкторам и т. д. Он объявляет, что соответствующий элемент является устаревшим. Компилятор C# проверят наличие этого атрибута и выполняет некоторые действия, если он присутствует.

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

Необходимые компоненты

Необходимо настроить компьютер для запуска .NET. Инструкции по установке можно найти на странице загрузки .NET. Это приложение можно запустить в Windows, Ubuntu Linux, macOS или в контейнере Docker. Вам нужно установить любимый редактор кода. В следующих описаниях используется Visual Studio Code, который является кроссплатформенным редактором с открытым кодом. Однако вы можете использовать все инструменты, с которыми вы комфортно.

Создание приложения

Теперь, когда вы установили все средства, создайте консольное приложение .NET. Чтобы использовать генератор из командной строки, выполните следующую команду в любой оболочке:

dotnet new console

Эта команда создает файлы проекта .NET без костей. Выполните восстановление dotnet restore зависимостей, необходимых для компиляции этого проекта.

Вам не нужно выполнять команду dotnet restore, так как она выполняется неявно всеми командами, которые требуют восстановления, например dotnet new, dotnet build, dotnet run, dotnet test, dotnet publish и dotnet 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("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

Как ограничить использование атрибута

Атрибуты можно использовать в следующих "целевых объектах". Приведенные выше примеры показывают их на классах, но их также можно использовать для:

  • Сборка
  • Класс
  • Конструктор
  • Делегирование
  • Перечисление
  • Событие
  • Поле
  • универсальный параметр;
  • Интерфейс
  • Способ
  • Модуль
  • Параметр
  • Свойство
  • Возвращаемое значение
  • Структура

При создании класса атрибутов по умолчанию 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()
    { }
}

Как использовать атрибуты, прикрепленные к элементу кода

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

Для поиска и действия с атрибутами требуется отражение. Рефлексия ion позволяет писать код в C#, который проверяет другой код. Например, с помощью отражения можно получить сведения о классе (добавьте 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, указав тип атрибута.

Ниже вы видите пример использования GetCustomAttributes для экземпляра MemberInfo класса MyClass (как мы продемонстрировали ранее, он имеет атрибут [Obsolete]).

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

Это выводится в консоль: Attribute on MyClass: ObsoleteAttribute Попробуйте добавить другие атрибуты для MyClass.

Обратите особое внимание, что к таким объектам Attribute применяется отложенное создание экземпляров. То есть они не создаются до тех пор, пока вы не используете или GetCustomAttributesне будете создавать GetCustomAttribute экземпляры. Они также создаются каждый раз. Вызов GetCustomAttributes дважды в строке возвращает два разных экземпляра ObsoleteAttribute.

Общие атрибуты во время выполнения

Атрибуты используются многими средствами и платформами. NUnit использует такие атрибуты, как [Test] и [TestFixture], которые нужны для средства тестового запуска NUnit. ASP.NET MVC использует такие атрибуты, как [Authorize], и предоставляет платформу фильтра действий, которая позволяет использовать перекрестные функции для действий MVC. PostSharp использует синтаксис атрибутов для реализации аспектно-ориентированного программирования на языке C#.

Ниже приведены несколько важных атрибутов, используемых в библиотеках базовых классов .NET Core.

  • [Obsolete]. Этот атрибут мы уже использовали в примерах выше. Он размещен в пространстве имен System. Полезно предоставить декларативную документацию по изменяющейся базе кода. К нему можно добавить строковое сообщение, а дополнительный логический параметр позволяет повысить уровень сообщений компилятора с предупреждения до ошибки.
  • [Conditional]. Этот атрибут находится в пространстве имен System.Diagnostics. Его может применять к методам или классам атрибутов. В его конструктор необходимо передать строку. Если эта строка не соответствует директиве #define , компилятор C# удаляет вызовы этого метода (но не сам метод). Обычно этот метод используется для отладки (диагностика) целей.
  • [CallerMemberName]. Этот атрибут можно применить для параметров. Он размещен в пространства имен System.Runtime.CompilerServices. CallerMemberName — это атрибут, используемый для внедрения имени метода, вызывающего другой метод. Это способ исключить "волшебные строки" при реализации INotifyPropertyChanged в различных платформах пользовательского интерфейса. Например:
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#, но они являются формой метаданных кода и не действуют сами по себе.