Редактиране

Споделяне чрез


Miscellaneous attributes interpreted by the C# compiler

There are several attributes that can be applied to elements in your code that add semantic meaning to those elements:

  • Conditional: Make execution of a method dependent on a preprocessor identifier.
  • Obsolete: Mark a type or member for (potential) future removal.
  • AttributeUsage: Declare the language elements where an attribute can be applied.
  • AsyncMethodBuilder: Declare an async method builder type.
  • InterpolatedStringHandler: Define an interpolated string builder for a known scenario.
  • ModuleInitializer: Declare a method that initializes a module.
  • SkipLocalsInit: Elide the code that initializes local variable storage to 0.
  • UnscopedRef: Declare that a ref variable normally interpreted as scoped should be treated as unscoped.
  • OverloadResolutionPriority: Add a tiebreaker attribute to influence overload resolution for possibly ambiguous overloads.
  • Experimental: Mark a type or member as experimental.

The compiler uses those semantic meanings to alter its output and report possible mistakes by developers using your code.

Conditional attribute

The Conditional attribute makes the execution of a method dependent on a preprocessing identifier. The Conditional attribute is an alias for ConditionalAttribute, and can be applied to a method or an attribute class.

In the following example, Conditional is applied to a method to enable or disable the display of program-specific diagnostic information:

#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.");
    }
}

If the TRACE_ON identifier isn't defined, the trace output isn't displayed. Explore for yourself in the interactive window.

The Conditional attribute is often used with the DEBUG identifier to enable trace and logging features for debug builds but not in release builds, as shown in the following example:

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

When a method marked conditional is called, the presence or absence of the specified preprocessing symbol determines whether the compiler includes or omits calls to the method. If the symbol is defined, the call is included; otherwise, the call is omitted. A conditional method must be a method in a class or struct declaration and must have a void return type. Using Conditional is cleaner, more elegant, and less error-prone than enclosing methods inside #if…#endif blocks.

If a method has multiple Conditional attributes, compiler includes calls to the method if one or more conditional symbols are defined (the symbols are logically linked together by using the OR operator). In the following example, the presence of either A or B results in a method call:

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

Using Conditional with attribute classes

The Conditional attribute can also be applied to an attribute class definition. In the following example, the custom attribute Documentation adds information to the metadata if DEBUG is defined.

[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 attribute

The Obsolete attribute marks a code element as no longer recommended for use. Use of an entity marked obsolete generates a warning or an error. The Obsolete attribute is a single-use attribute and can be applied to any entity that allows attributes. Obsolete is an alias for ObsoleteAttribute.

In the following example, the Obsolete attribute is applied to class A and to method B.OldMethod. Because the second argument of the attribute constructor applied to B.OldMethod is set to true, this method causes a compiler error, whereas using class A produces a warning. Calling B.NewMethod, however, produces no warning or error. For example, when you use it with the previous definitions, the following code generates two warnings and one error:


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();
        }
    }
}

The string provided as the first argument to the attribute constructor is displayed as part of the warning or error. Two warnings for class A are generated: one for the declaration of the class reference, and one for the class constructor. The Obsolete attribute can be used without arguments, but including an explanation what to use instead is recommended.

In C# 10, you can use constant string interpolation and the nameof operator to ensure the names match:

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

    public void NewMethod() { }
}

Experimental attribute

Beginning in C# 12, types, methods, and assemblies can be marked with the System.Diagnostics.CodeAnalysis.ExperimentalAttribute to indicate an experimental feature. The compiler issues a warning if you access a method or type annotated with the ExperimentalAttribute. All types declared in an assembly or module marked with the Experimental attribute are experimental. The compiler issues a warning if you access any of them. You can disable these warnings to pilot an experimental feature.

Warning

Experimental features are subject to changes. The APIs may change, or they may be removed in future updates. Including experimental features is a way for library authors to get feedback on ideas and concepts for future development. Use extreme caution when using any feature marked as experimental.

You can read more details about the Experimental attribute in the feature specification.

SetsRequiredMembers attribute

The SetsRequiredMembers attribute informs the compiler that a constructor sets all required members in that class or struct. The compiler assumes any constructor with the System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute attribute initializes all required members. Any code that invokes such a constructor doesn't need object initializers to set required members. Adding the SetsRequiredMembers attribute is primarily useful for positional records and primary constructors.

AttributeUsage attribute

The AttributeUsage attribute determines how a custom attribute class can be used. AttributeUsageAttribute is an attribute you apply to custom attribute definitions. The AttributeUsage attribute enables you to control:

  • Which program elements the attribute can be applied to. Unless you restrict its usage, an attribute can be applied to any of the following program elements:
    • Assembly
    • Module
    • Field
    • Event
    • Method
    • Parameter
    • Property
    • Return
    • Type
  • Whether an attribute can be applied to a single program element multiple times.
  • Whether derived classes inherit attributes.

The default settings look like the following example when applied explicitly:

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

In this example, the NewAttribute class can be applied to any supported program element. But it can be applied only once to each entity. Derived classes inherit the attribute applied to a base class.

The AllowMultiple and Inherited arguments are optional, so the following code has the same effect:

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

The first AttributeUsageAttribute argument must be one or more elements of the AttributeTargets enumeration. Multiple target types can be linked together with the OR operator, like the following example shows:

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

Attributes can be applied to either the property or the backing field for an autoimplemented property. The attribute applies to the property, unless you specify the field specifier on the attribute. Both are shown in the following example:

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;
}

If the AllowMultiple argument is true, then the resulting attribute can be applied more than once to a single entity, as shown in the following example:

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

[MultiUse]
[MultiUse]
class Class1 { }

[MultiUse, MultiUse]
class Class2 { }

In this case, MultiUseAttribute can be applied repeatedly because AllowMultiple is set to true. Both formats shown for applying multiple attributes are valid.

If Inherited is false, then derived classes don't inherit the attribute from an attributed base class. For example:

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

[NonInherited]
class BClass { }

class DClass : BClass { }

In this case, NonInheritedAttribute isn't applied to DClass via inheritance.

You can also use these keywords to specify where an attribute should be applied. For example, you can use the field: specifier to add an attribute to the backing field of an automatically implemented property. Or you can use the field:, property: or param: specifier to apply an attribute to any of the elements generated from a positional record. For an example, see Positional syntax for property definition.

AsyncMethodBuilder attribute

You add the System.Runtime.CompilerServices.AsyncMethodBuilderAttribute attribute to a type that can be an async return type. The attribute specifies the type that builds the async method implementation when the specified type is returned from an async method. The AsyncMethodBuilder attribute can be applied to a type that:

The constructor to the AsyncMethodBuilder attribute specifies the type of the associated builder. The builder must implement the following accessible members:

  • A static Create() method that returns the type of the builder.

  • A readable Task property that returns the async return type.

  • A void SetException(Exception) method that sets the exception when a task faults.

  • A void SetResult() or void SetResult(T result) method that marks the task as completed and optionally sets the task's result

  • A Start method with the following API signature:

    void Start<TStateMachine>(ref TStateMachine stateMachine)
              where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    
  • An AwaitOnCompleted method with the following signature:

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : System.Runtime.CompilerServices.INotifyCompletion
        where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    
  • An AwaitUnsafeOnCompleted method with the following signature:

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

You can learn about async method builders by reading about the following builders supplied by .NET:

In C# 10 and later, the AsyncMethodBuilder attribute can be applied to an async method to override the builder for that type.

InterpolatedStringHandler and InterpolatedStringHandlerArguments attributes

Starting with C# 10, you use these attributes to specify that a type is an interpolated string handler. The .NET 6 library already includes System.Runtime.CompilerServices.DefaultInterpolatedStringHandler for scenarios where you use an interpolated string as the argument for a string parameter. You might have other instances where you want to control how interpolated strings are processed. You apply the System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute to the type that implements your handler. You apply the System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute to parameters of that type's constructor.

You can learn more about building an interpolated string handler in the C# 10 feature specification for interpolated string improvements.

ModuleInitializer attribute

The ModuleInitializer attribute marks a method that the runtime calls when the assembly loads. ModuleInitializer is an alias for ModuleInitializerAttribute.

The ModuleInitializer attribute can only be applied to a method that:

  • Is static.
  • Is parameterless.
  • Returns void.
  • Is accessible from the containing module, that is, internal or public.
  • Isn't a generic method.
  • Isn't contained in a generic class.
  • Isn't a local function.

The ModuleInitializer attribute can be applied to multiple methods. In that case, the order in which the runtime calls them is deterministic but not specified.

The following example illustrates use of multiple module initializer methods. The Init1 and Init2 methods run before Main, and each adds a string to the Text property. So when Main runs, the Text property already has strings from both initializer methods.

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! ";
    }
}

Source code generators sometimes need to generate initialization code. Module initializers provide a standard place for that code. In most other cases, you should write a static constructor instead of a module initializer.

SkipLocalsInit attribute

The SkipLocalsInit attribute prevents the compiler from setting the .locals init flag when emitting to metadata. The SkipLocalsInit attribute is a single-use attribute and can be applied to a method, a property, a class, a struct, an interface, or a module, but not to an assembly. SkipLocalsInit is an alias for SkipLocalsInitAttribute.

The .locals init flag causes the CLR to initialize all of the local variables declared in a method to their default values. Since the compiler also makes sure that you never use a variable before assigning some value to it, .locals init is typically not necessary. However, the extra zero-initialization might have measurable performance impact in some scenarios, such as when you use stackalloc to allocate an array on the stack. In those cases, you can add the SkipLocalsInit attribute. If applied to a method directly, the attribute affects that method and all its nested functions, including lambdas and local functions. If applied to a type or module, it affects all methods nested inside. This attribute doesn't affect abstract methods, but it does affect code generated for the implementation.

This attribute requires the AllowUnsafeBlocks compiler option. This requirement signals that in some cases code could view unassigned memory (for example, reading from uninitialized stack-allocated memory).

The following example illustrates the effect of SkipLocalsInit attribute on a method that uses stackalloc. The method displays whatever was in memory when the array of integers was allocated.

[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.

To try this code yourself, set the AllowUnsafeBlocks compiler option in your .csproj file:

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

UnscopedRef attribute

The UnscopedRef attribute marks a variable declaration as unscoped, meaning the reference is allowed to escape.

You add this attribute where the compiler treats a ref as implicitly scoped:

  • The this parameter for struct instance methods.
  • ref parameters that refer to ref struct types.
  • out parameters.

Applying the System.Diagnostics.CodeAnalysis.UnscopedRefAttribute marks the element as unscoped.

OverloadResolutionPriority attribute

The OverloadResolutionPriorityAttribute enables library authors to prefer one overload over another when two overloads can be ambiguous. Its primary use case is for library authors to write better performing overloads while still supporting existing code without breaks.

For example, you might add a new overload that uses ReadOnlySpan<T> to reduce memory allocations:

[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");

Overload resolution considers the two methods equally good for some argument types. For an argument of int[], it prefers the first overload. To get the compiler to prefer the ReadOnlySpan version, you can increase the priority of that overload. The following example shows the effect of adding the attribute:

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"

All overloads with a lower priority than the highest overload priority are removed from the set of applicable methods. Methods without this attribute have the overload priority set to the default of zero. Library authors should use this attribute as a last resort when adding a new and better method overload. Library authors should have a deep understanding of how Overload resolution impacts choosing the better method. Otherwise, unexpected errors can result.

See also