Поделиться через


Прочие атрибуты, интерпретируемые компилятором C#

Существует несколько атрибутов, которые можно применить к элементам в коде, которые добавляют семантические значения к этим элементам:

  • Conditional: выполнение метода зависит от идентификатора препроцессора.
  • Obsolete: помечайте тип или член для (потенциального) удаления.
  • AttributeUsage: объявите языковые элементы, в которых можно применить атрибут.
  • AsyncMethodBuilder: объявление типа асинхронного построителя методов.
  • InterpolatedStringHandler: определите интерполированный построитель строк для известного сценария.
  • ModuleInitializer: объявите метод, который инициализирует модуль.
  • SkipLocalsInit: выделяйте код, который инициализирует локальное хранилище переменных до 0.
  • UnscopedRef: объявите, что ref переменная обычно интерпретируется как scoped неуправляемая.
  • OverloadResolutionPriority: добавьте атрибут тай-брейка, чтобы повлиять на разрешение перегрузки для возможно неоднозначных перегрузок.
  • Experimental: пометить тип или член как экспериментальный.

Компилятор использует эти семантические значения для изменения выходных данных и сообщения о возможных ошибках разработчиков, использующих код.

Атрибут Conditional

Атрибут Conditional определяет зависимость выполнения метода от идентификатора предварительной обработки. Атрибут Conditional является псевдонимом для ConditionalAttribute и может применяться к методу или классу атрибута.

В следующем примере атрибут Conditional применяется к методу для включения и отключения отображения диагностической информации для конкретной программы.

#define TRACE_ON
using System.Diagnostics;

namespace AttributeExamples;

public class Trace
{
    [Conditional("TRACE_ON")]
    public static void Msg(string msg)
    {
        Console.WriteLine(msg);
    }
}

public class TraceExample
{
    public static void Main()
    {
        Trace.Msg("Now in Main...");
        Console.WriteLine("Done.");
    }
}

Если идентификатор TRACE_ON не определен, выходные данные трассировки не отображаются. Поэкспериментируйте в интерактивном окне.

Атрибут Conditional часто используется с идентификатором DEBUG для включения функций трассировки и ведения журнала для отладочных сборок (но не сборок выпуска), как показано в следующем примере.

[Conditional("DEBUG")]
static void DebugMethod()
{
}

Когда вызывается метод, помеченный как условный, наличие или отсутствие заданного символа предварительной обработки определяет, включает ли компилятор вызов в метод или опускает его. Если этот символ определен, вызов включается; в противном случае вызов пропускается. Условный метод должен быть методом в объявлении класса или структуры и должен иметь тип возвращаемого значения void. Использование атрибута Conditional — это более понятная, элегантная и менее подверженная ошибкам альтернатива вложению методов в блоки #if…#endif.

Если метод содержит несколько атрибутов Conditional, компилятор включает вызовы в метод, если определен один или несколько условных символов (символы логически связаны с помощью оператора ИЛИ). В следующем примере наличие A или B приведет к вызову метода.

[Conditional("A"), Conditional("B")]
static void DoIfAorB()
{
    // ...
}

Использование Conditional с классами атрибутов

Атрибут Conditional также может применяться к определению класса атрибута. В следующем примере настраиваемый атрибут Documentation добавляет сведения в метаданные, если DEBUG он определен.

[Conditional("DEBUG")]
public class DocumentationAttribute : System.Attribute
{
    string text;

    public DocumentationAttribute(string text)
    {
        this.text = text;
    }
}

class SampleClass
{
    // This attribute will only be included if DEBUG is defined.
    [Documentation("This method displays an integer.")]
    static void DoWork(int i)
    {
        System.Console.WriteLine(i.ToString());
    }
}

Атрибут Obsolete

Атрибут Obsolete помечает элемент кода как больше не рекомендуемый для использования. Использование сущности, помеченной как устаревшая, приводит к возникновению предупреждения или ошибки. Атрибут Obsolete является атрибутом однократного использования и может применяться к любой сущности, допускающей использование атрибутов. Obsolete является псевдонимом для ObsoleteAttribute.

В следующем примере атрибут Obsolete применяется к классу A и к методу B.OldMethod. Так как для второго аргумента конструктора B.OldMethod атрибута задано trueзначение , этот метод вызывает ошибку компилятора, в то время как при использовании класса A возникает предупреждение. При этом вызов B.NewMethod не создает предупреждений или ошибок. Например, при использовании с предыдущими определениями следующий код создает два предупреждения и одну ошибку:


namespace AttributeExamples
{
    [Obsolete("use class B")]
    public class A
    {
        public void Method() { }
    }

    public class B
    {
        [Obsolete("use NewMethod", true)]
        public void OldMethod() { }

        public void NewMethod() { }
    }

    public static class ObsoleteProgram
    {
        public static void Main()
        {
            // Generates 2 warnings:
            A a = new A();

            // Generate no errors or warnings:
            B b = new B();
            b.NewMethod();

            // Generates an error, compilation fails.
            // b.OldMethod();
        }
    }
}

Строка, указанная в качестве первого аргумента конструктору атрибутов, отображается как часть предупреждения или ошибки. Создается два предупреждения для класса A: одно для объявления ссылки на класс, а второе — для конструктора класса. Атрибут Obsolete может использоваться без аргументов, однако рекомендуется включать пояснение о том, что следует использовать вместо него.

В C# 10 можно использовать интерполяцию константной строки и оператор nameof, чтобы гарантировать совпадение имен:

public class B
{
    [Obsolete($"use {nameof(NewMethod)} instead", true)]
    public void OldMethod() { }

    public void NewMethod() { }
}

Атрибут Experimental

Начиная с C# 12, типы, методы и сборки можно пометить как System.Diagnostics.CodeAnalysis.ExperimentalAttribute экспериментальную функцию. Компилятор выдает предупреждение при доступе к методу или типу, аннотированному ExperimentalAttributeс помощью . Все типы, объявленные в сборке или модуле Experimental , помеченные атрибутом, являются экспериментальными. Компилятор выдает предупреждение, если вы обращаетесь к любому из них. Эти предупреждения можно отключить для пилотного экспериментального компонента.

Предупреждение

Экспериментальные функции подвержены изменениям. API могут измениться или удалить их в будущих обновлениях. Включение экспериментальных функций — это способ получения отзывов авторов библиотек о идеях и концепциях для будущего развития. Используйте крайнюю осторожность при использовании любой функции, помеченной как экспериментальная.

Дополнительные сведения об Experimental атрибуте см. в спецификации компонентов.

Атрибут SetsRequiredMembers

Атрибут SetsRequiredMembers сообщает компилятору, что конструктор задает все required члены этого класса или структуры. Компилятор предполагает, что любой конструктор с атрибутом System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute инициализирует все required элементы. Любой код, вызывающий такой конструктор, не требует инициализаторов объектов для задания обязательных элементов. Добавление атрибута SetsRequiredMembers в первую очередь полезно для позиционных записей и основных конструкторов.

Атрибут AttributeUsage

Атрибут AttributeUsage определяет, как можно использовать пользовательский класс атрибутов. AttributeUsageAttribute — это атрибут, примененный к определениям настраиваемого атрибута. С помощью атрибута AttributeUsage можно контролировать:

  • К каким элементам программы можно применить атрибут. Если вы не ограничиваете его использование, атрибут можно применить к любому из следующих элементов программы:
    • Сборка
    • Модуль
    • Поле
    • Мероприятие
    • Способ
    • Параметр
    • Свойство
    • Возврат
    • Тип
  • Можно ли применить атрибут к одному элементу программы несколько раз.
  • Наследуют ли производные классы атрибуты.

При явном применении параметры по умолчанию выглядят следующим образом:

[AttributeUsage(AttributeTargets.All,
                   AllowMultiple = false,
                   Inherited = true)]
class NewAttribute : Attribute { }

В этом примере класс NewAttribute можно применить к любому поддерживаемому элементу программы. Но он применяется к каждому объекту только один раз. Производные классы наследуют атрибут, применяемый к базовому классу.

Аргументы AllowMultiple и Inherited являются необязательными, так что следующий код имеет тот же результат:

[AttributeUsage(AttributeTargets.All)]
class NewAttribute : Attribute { }

Первый аргумент AttributeUsageAttribute должен состоять из одного или нескольких элементов перечисления AttributeTargets. Несколько типов целевого объекта можно связать с помощью оператора OR, как показано в следующем примере:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
class NewPropertyOrFieldAttribute : Attribute { }

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

class MyClass
{
    // Attribute attached to property:
    [NewPropertyOrField]
    public string Name { get; set; } = string.Empty;

    // Attribute attached to backing field:
    [field: NewPropertyOrField]
    public string Description { get; set; } = string.Empty;
}

Если аргументу AllowMultiple присвоено значение true, то результирующий атрибут можно применить несколько раз к одной сущности, как показано в следующем примере:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
class MultiUse : Attribute { }

[MultiUse]
[MultiUse]
class Class1 { }

[MultiUse, MultiUse]
class Class2 { }

В этом случае MultiUseAttribute можно применять несколько раз, так как AllowMultiple имеет значение true. Для применения нескольких атрибутов допускаются оба показанных формата.

Если Inherited это falseтак, производные классы не наследуют атрибут от атрибута базового класса. Например:

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
class NonInheritedAttribute : Attribute { }

[NonInherited]
class BClass { }

class DClass : BClass { }

В этом случае NonInheritedAttribute не применяется к DClass наследованию.

Эти ключевые слова также позволяют указать, где нужно применить атрибут. Например, можно использовать field: описатель для добавления атрибута в резервное поле автоматически реализованного свойства. Вы также можете использовать описатели field:, property: или param:, чтобы применить атрибут к любому из элементов, созданных из позиционной записи. Пример см. в разделе Позиционный синтаксис для определения свойств.

Атрибут AsyncMethodBuilder

Атрибут добавляется System.Runtime.CompilerServices.AsyncMethodBuilderAttribute в тип, который может быть асинхронным типом возвращаемого значения. Атрибут задает тип, который создает реализацию асинхронного метода, когда указанный тип возвращается из асинхронного метода. Атрибут AsyncMethodBuilder можно применить к типу, который:

Конструктор для атрибута AsyncMethodBuilder указывает тип связанного построителя. В построителе должны быть реализованы следующие доступные члены:

  • статический метод Create(), возвращающий тип построителя;

  • доступное для чтения свойство Task, возвращающее асинхронный тип возвращаемого значения;

  • метод void SetException(Exception), который задает исключение при сбое задачи;

  • метод void SetResult() или void SetResult(T result), который помечает задачу как завершенную и при необходимости задает результат задачи;

  • метод Start со следующей сигнатурой API:

    void Start<TStateMachine>(ref TStateMachine stateMachine)
              where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    
  • метод AwaitOnCompleted со следующей сигнатурой:

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : System.Runtime.CompilerServices.INotifyCompletion
        where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    
  • метод AwaitUnsafeOnCompleted со следующей сигнатурой:

          public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
              where TAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion
              where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    

Дополнительные сведения о построителях асинхронных методов см. в статьях, посвященных следующим сборщикам в .NET:

В C# 10 и более поздних версиях атрибут AsyncMethodBuilder может применяться к асинхронному методу для переопределения построителя для этого типа.

Атрибуты InterpolatedStringHandler и InterpolatedStringHandlerArguments

Начиная с версии C# 10 эти атрибуты используются для уточнения того, что тип указывает на обработчика интерполированных строк. Библиотека .NET 6 уже включает в себя System.Runtime.CompilerServices.DefaultInterpolatedStringHandler для сценариев, в которых интерполированная строка используется в качестве аргумента для параметра string. У вас могут быть другие экземпляры, в которых требуется управлять обработкой интерполированных строк. Вам потребуется применить System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute к типу, реализующему обработчик, а также применить System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute для параметров конструктора этого типа.

Дополнительные сведения о создании обработчика интерполированных строк см. в спецификации функций C# 10 для улучшения интерполяции строк.

Атрибут ModuleInitializer

Атрибут ModuleInitializer помечает метод, который вызывает среда выполнения при загрузке сборки. ModuleInitializer является псевдонимом для ModuleInitializerAttribute.

Атрибут ModuleInitializer может применяться только к методу, который:

  • является статическим;
  • не имеет параметров;
  • Возвращает void.
  • доступен из содержащего модуля, а именно internal или public;
  • не является универсальным методом;
  • не содержится в универсальном классе;
  • не является локальной функцией.

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

В следующем примере показано использование нескольких методов инициализатора модуля. Методы Init1 и Init2 выполняются до Main, и каждый добавляет строку в свойство Text. Поэтому при выполнении Main свойство Text уже имеет строки из обоих методов инициализатора.

using System;

internal class ModuleInitializerExampleMain
{
    public static void Main()
    {
        Console.WriteLine(ModuleInitializerExampleModule.Text);
        //output: Hello from Init1! Hello from Init2!
    }
}
using System.Runtime.CompilerServices;

internal class ModuleInitializerExampleModule
{
    public static string? Text { get; set; }

    [ModuleInitializer]
    public static void Init1()
    {
        Text += "Hello from Init1! ";
    }

    [ModuleInitializer]
    public static void Init2()
    {
        Text += "Hello from Init2! ";
    }
}

Иногда генераторам исходного кода требуется создать код инициализации. Инициализаторы модулей предоставляют стандартное расположение для этого кода. В большинстве случаев необходимо написать статический конструктор, а не инициализатор модуля.

Атрибут SkipLocalsInit

Атрибут SkipLocalsInit запрещает компилятору .locals init задавать флаг при создании метаданных. Атрибут SkipLocalsInit является одноразовым и может применяться к методу, свойству, классу, структуре, интерфейсу или модулю, но не к сборке. SkipLocalsInit является псевдонимом для SkipLocalsInitAttribute.

Флаг .locals init заставляет среду CLR инициализировать все локальные переменные, объявленные в методе, со значениями по умолчанию. Так как компилятор также гарантирует, что вы никогда не используете переменную, прежде чем назначить ей какое-либо значение, .locals init обычно не требуется. Однако дополнительная инициализация нуля может оказать заметное влияние на производительность в некоторых сценариях, например при использовании stackalloc для выделения массива в стеке. В таких случаях можно добавить атрибут SkipLocalsInit. При применении напрямую к методу атрибут воздействует на него и все его вложенные функции, включая лямбда-выражения и локальные функции. При применении к типу или модулю он влияет на все вложенные внутрь методы. Этот атрибут не воздействует на абстрактные методы, но воздействует на код, созданный для реализации.

Для этого атрибута требуется параметр компилятора AllowUnsafeBlocks. Требование указывает на то, что в некоторых случаях код может иметь доступ к неназначенной памяти (например, осуществлять чтение из неинициализированной памяти, выделенной в стеке).

В следующем примере показано воздействие атрибута SkipLocalsInit на метод, использующий stackalloc. Метод отображает все, что было в памяти на момент выделения массива целых чисел.

[SkipLocalsInit]
static void ReadUninitializedMemory()
{
    Span<int> numbers = stackalloc int[120];
    for (int i = 0; i < 120; i++)
    {
        Console.WriteLine(numbers[i]);
    }
}
// output depends on initial contents of memory, for example:
//0
//0
//0
//168
//0
//-1271631451
//32767
//38
//0
//0
//0
//38
// Remaining rows omitted for brevity.

Чтобы выполнить этот код самостоятельно, задайте параметр компилятора AllowUnsafeBlocks в файле CSPROJ.

<PropertyGroup>
  ...
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

Атрибут UnscopedRef

Атрибут UnscopedRef помечает объявление переменной как неуправляемое, что означает, что ссылка может быть экранирована.

Этот атрибут добавляется, когда компилятор обрабатывает неявно ref scoped:

  • Параметр this для struct методов экземпляра.
  • ref параметры, ссылающиеся на ref struct типы.
  • out Параметры.

System.Diagnostics.CodeAnalysis.UnscopedRefAttribute Применение меток элемента как неуправляемого.

Атрибут OverloadResolutionPriority

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

Например, можно добавить новую перегрузку, которая используется ReadOnlySpan<T> для уменьшения выделения памяти:

[OverloadResolutionPriority(1)]
public void M(params ReadOnlySpan<int> s) => Console.WriteLine("Span");
// Default overload resolution priority of 0
public void M(params int[] a) => Console.WriteLine("Array");

Разрешение перегрузки учитывает два метода одинаково хорошо для некоторых типов аргументов. Для аргумента int[], он предпочитает первую перегрузку. Чтобы компилятор предпочел ReadOnlySpan версию, можно увеличить приоритет этой перегрузки. В следующем примере показан эффект добавления атрибута:

var d = new OverloadExample();
int[] arr = [1, 2, 3];
d.M(1, 2, 3, 4); // Prints "Span"
d.M(arr); // Prints "Span" when PriorityAttribute is applied
d.M([1, 2, 3, 4]); // Prints "Span"
d.M(1, 2, 3, 4); // Prints "Span"

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

См. также