Sonstige vom C#-Compiler interpretierte Attribute auf Assemblyebene

Die Attribute Conditional, Obsolete, AttributeUsage, AsyncMethodBuilder, InterpolatedStringHandler, ModuleInitializer und Experimental können auf Elemente in Ihrem Code angewendet werden. Sie fügen diesen Elementen eine semantische Bedeutung hinzu. Der Compiler verwendet diese semantischen Bedeutungen, um seine Ausgabe zu ändern und mögliche Entwicklerfehler im Code zu melden.

Conditional-Attribut

Das Conditional-Attribut macht die Ausführung einer Methode abhängig von einem Vorverarbeitungsbezeichner. Das Conditional-Attribut ist ein Alias für ConditionalAttribute und kann auf eine Methode oder Attributklasse angewendet werden.

Im folgenden Beispiel wird Conditional auf eine Methode angewendet, um die Anzeige programmspezifischer Diagnoseinformationen zu aktivieren oder zu deaktivieren:

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

Wenn der TRACE_ON-Bezeichner nicht definiert ist, wird die Ausgabe der Ablaufverfolgung nicht angezeigt. Untersuchen Sie den Code im interaktiven Fenster.

Das Conditional-Attribut wird oft zusammen mit dem DEBUG-Bezeichner verwendet, um die Ablaufverfolgung und Protokollierung für Debugbuilds – nicht jedoch in Releasebuilds – wie im folgenden Beispiel gezeigt zu aktivieren:

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

Wird eine als bedingt gekennzeichnete Methode aufgerufen, bestimmt das Vorhandensein oder Fehlen des angegebenen Vorverarbeitungssymbols, ob Aufrufe der Methode vom Compiler eingeschlossen oder ausgelassen werden. Wenn das Symbol definiert ist, wird der Aufruf einbezogen; andernfalls wird der Aufruf ausgelassen. Eine bedingte Methode muss eine Methode in einer Klassen- oder Strukturdeklaration sein und einen void-Rückgabetyp aufweisen. Die Verwendung von Conditional ist eine sauberere, elegantere und auch weniger fehleranfällige Alternative zum Einschließen von Methoden innerhalb von #if…#endif-Blöcken.

Besitzt eine Methode mehrere Conditional-Attribute, schließt der Compiler Aufrufe der Methode ein, wenn mindestens eines der bedingten Symbole definiert ist (und die Symbole durch den OR-Operator logisch miteinander verknüpft sind). Im folgenden Beispiel führt das Vorhandensein von A oder B zu einem Methodenaufruf:

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

Verwenden von Conditional mit Attributklassen

Das Conditional-Attribut kann auch auf die Definition einer Attributklasse angewendet werden. Im folgenden Beispiel fügt das benutzerdefinierte Attribut Documentation Informationen zu den Metadaten hinzu, wenn DEBUG definiert ist.

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

Das Obsolete-Attribut markiert ein Codeelement als nicht länger zur Verwendung empfohlen. Die Verwendung einer als veraltet markierten Entität führt zu einer Warnung oder einem Fehler. Das Obsolete-Attribut ist ein Attribut zur einmaligen Nutzung und kann auf jede Entität angewendet werden, die Attribute zulässt. Obsolete ist ein Alias für ObsoleteAttribute.

Das folgende Beispiel zeigt, wie das Obsolete-Attribut auf die Klasse A und die Methode B.OldMethod angewendet wird. Da das zweite Argument des Attributkonstruktors, das auf B.OldMethod angewendet wurde, auf true festgelegt wird, verursacht diese Methode einen Compilerfehler, wobei die Verwendung der A-Klasse hingegen nur eine Warnung erzeugt. Wenn Sie B.NewMethod aufrufen, werden weder Warnungen noch Fehler erzeugt. Wenn Sie es mit den vorherigen Definitionen verwenden, generiert der folgende Code zwei Warnungen und einen Fehler:


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

Die Zeichenfolge, die als erstes Argument für den Attributkonstruktor bereitgestellt wurde, wird als Teil der Warnung oder des Fehlers angezeigt. Es werden zwei Warnungen für die Klasse A generiert: eine für die Deklaration des Klassenverweises und eine für den Klassenkonstruktor. Das Obsolete-Attribut kann ohne Argumente verwendet werden. Es wird jedoch empfohlen, in einer Erläuterung anzugeben, was stattdessen verwendet werden soll.

In C# 10 können Sie konstante Zeichenfolgeninterpolation und den nameof-Operator verwenden, um sicherzustellen, dass die Namen übereinstimmen:

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

    public void NewMethod() { }
}

Experimental-Attribut

Ab C# 12 können Typen, Methoden und Assemblys mit System.Diagnostics.CodeAnalysis.ExperimentalAttribute gekennzeichnet werden, um eine experimentelle Funktion anzugeben. Der Compiler gibt eine Warnung aus, wenn Sie auf eine Methode zugreifen oder Anmerkungen mit ExperimentalAttribute eingeben. Alle Typen, die in einer Assembly oder einem Modul deklariert werden, die bzw. das mit dem Experimental-Attribut gekennzeichnet ist, sind experimentell. Der Compiler gibt eine Warnung aus, wenn Sie darauf zugreifen. Sie können diese Warnungen deaktivieren, um eine experimentelle Funktion zu testen.

Warnung

Experimentelle Funktionen unterliegen Änderungen. Die APIs können sich ändern oder in zukünftigen Aktualisierungen entfernt werden. Das Einschließen experimenteller Funktionen ist eine Möglichkeit für Bibliotheksautoren, Feedback zu Ideen und Konzepten für die zukünftige Entwicklung zu erhalten. Seien Sie äußerst vorsichtig, wenn Sie eine als experimentell gekennzeichnete Funktion verwenden.

Weitere Details zum Experimental-Attribut finden Sie in der Featurespezifikation.

SetsRequiredMembers-Attribut

Das SetsRequiredMembers-Attribut informiert den Compiler darüber, dass ein Konstruktor alle required-Member in dieser Klasse oder Struktur festlegt. Der Compiler geht davon aus, dass jeder Konstruktor mit dem System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute-Attribut alle required-Member initialisiert. Jeder Code, der einen solchen Konstruktor aufruft, benötigt keine Objektinitialisierer, um die erforderlichen Member festzulegen. Das Hinzufügen des SetsRequiredMembers-Attributs ist vor allem für Positionsdatensätze und primäre Konstrukteure nützlich.

AttributeUsage-Attribut

Das AttributeUsage-Attribut bestimmt, wie eine benutzerdefinierte Attributklasse verwendet werden kann. Bei AttributeUsageAttribute handelt es sich um ein Attribut, das Sie auf benutzerdefinierte Attributdefinitionen anwenden. Mithilfe des AttributeUsage-Attribut können Sie Folgendes steuern:

  • Auf welche Programmelemente das Attribut angewendet werden kann. Sofern Sie die Verwendung nicht einschränken, kann ein Attribut auf jedes der folgenden Programmelemente angewendet werden:
    • Assembly
    • Modul
    • Feld
    • Ereignis
    • Methode
    • Parameter
    • Eigenschaft
    • Rückgabewert
    • type
  • Ob ein Attribut mehrfach auf ein einzelnes Programmelement angewendet werden kann.
  • Gibt an, ob abgeleitete Klassen Attribute erben.

Die Standardeinstellungen ähneln folgendem Beispiel, wenn Sie explizit angewendet werden:

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

In diesem Beispiel kann die NewAttribute-Klasse auf jedes unterstützte Programmelement angewendet werden. Es kann jedoch nur einmal auf jede Entität angewendet werden. Abgeleitete Klassen erben das Attribut, das auf eine Basisklasse angewendet wird.

Die Argumente AllowMultiple und Inherited sind optional, sodass folgender Code die gleiche Wirkung hat:

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

Das erste AttributeUsageAttribute-Argument muss mindestens ein Element der AttributeTargets-Enumeration sein. Mehrere Zieltypen können wie im folgenden Beispiel dargestellt mithilfe des OR-Operators verknüpft werden:

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

Attribute können entweder auf die Eigenschaft oder das Hintergrundfeld für eine automatisch implementierte Eigenschaft angewendet werden. Das Attribut wird auf die Eigenschaft angewendet, sofern Sie den field-Bezeichner des Attributs nicht angeben. Beides wird im folgenden Beispiel veranschaulicht:

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

Wenn das AllowMultiple-Argument auf true festgelegt ist, kann das daraus entstehende Attribut wie im folgenden Beispiel dargestellt mehr als einmal auf eine einzelne Entität angewendet werden:

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

[MultiUse]
[MultiUse]
class Class1 { }

[MultiUse, MultiUse]
class Class2 { }

In diesem Fall kann MultiUseAttribute wiederholt angewendet werden, da AllowMultiple auf true festgelegt wurde. Beide gezeigten Formate für das Anwenden von mehreren Attributen sind gültig.

Wenn Inherited ja false, erben abgeleitete Klassen das Attribut nicht von einer attributierten Basisklasse. Beispiel:

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

[NonInherited]
class BClass { }

class DClass : BClass { }

In diesem Fall wird NonInheritedAttribute nicht durch Vererbung auf DClass angewendet.

Sie können diese Schlüsselwörter auch verwenden, um anzugeben, wo ein Attribut angewendet werden soll. Beispielsweise können Sie den field:-Bezeichner verwenden, um dem Unterstützungsfeld einer automatisch implementierten Eigenschaft ein Attribut hinzuzufügen. Sie können auch die Spezifizier field:, property: oder param: verwenden, um ein Attribut auf eines der Elemente anzuwenden, die aus einem Positionsdatensatz generiert wurden. Ein Beispiel finden Sie unter Positionssyntax für die Eigenschaftendefinition.

AsyncMethodBuilder-Attribut

Sie fügen das Attribut System.Runtime.CompilerServices.AsyncMethodBuilderAttribute einem Typ hinzu, der ein asynchroner Rückgabetyp sein kann. Das Attribut gibt den Typ an, mit dem die Implementierung der asynchronen Methode erstellt wird, wenn der angegebene Typ von einer asynchronen Methode zurückgegeben wird. Das Attribut AsyncMethodBuilder kann auf einen Typ angewendet werden, auf den Folgendes zutrifft:

Der Konstruktor des Attributs AsyncMethodBuilder gibt den Typ des zugeordneten Generators an. Der Generator muss die folgenden zugänglichen Member implementieren:

  • Eine statische Create()-Methode, die den Generatortyp zurückgibt.

  • Eine lesbare Task-Eigenschaft, die den asynchronen Rückgabetyp zurückgibt.

  • Eine void SetException(Exception)-Methode, die die Ausnahme für einen Fehler bei der Aufgabe festlegt.

  • Eine void SetResult()- oder void SetResult(T result)-Methode, die die Aufgabe als abgeschlossen kennzeichnet und optional deren Ergebnis festlegt.

  • Eine Start-Methode mit der folgenden API-Signatur:

    void Start<TStateMachine>(ref TStateMachine stateMachine)
              where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    
  • Eine AwaitOnCompleted-Methode mit der folgenden Signatur:

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : System.Runtime.CompilerServices.INotifyCompletion
        where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    
  • Eine AwaitUnsafeOnCompleted-Methode mit der folgenden Signatur:

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

Weitere Informationen zu Generatoren für asynchrone Methoden finden Sie in den Artikeln zu den folgenden von .NET bereitgestellten Generatoren:

Ab C# 10 kann das AsyncMethodBuilder-Attribut auf eine asynchrone Methode angewendet werden, um den Generator für diesen Typ zu überschreiben.

InterpolatedStringHandler- und InterpolatedStringHandlerArguments-Attribute

Ab C# 10 verwenden Sie diese Attribute, um anzugeben, dass es sich bei einem Typ um einen Handler für interpolierte Zeichenfolgen handelt. Die .NET 6-Bibliothek enthält System.Runtime.CompilerServices.DefaultInterpolatedStringHandler bereits für Szenarios, in denen Sie eine interpolierte Zeichenfolge als Argument für einen string-Parameter verwenden. Vielleicht möchten Sie auch in anderen Fällen steuern, wie interpolierte Zeichenfolgen verarbeitet werden. Sie wenden System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute auf den Typ an, der den Handler implementiert. Sie wenden System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute auf Parameter des Konstruktors dieses Typs an.

Weitere Informationen zum Erstellen eines Handlers für interpolierte Zeichenfolgen finden Sie in der C# 10-Featurespezifikation für Verbesserungen interpolierter Zeichenfolgen.

ModuleInitializer-Attribut

Das Attribut ModuleInitializer kennzeichnet eine Methode, die von der Laufzeit beim Laden der Assembly aufgerufen wird. ModuleInitializer ist ein Alias für ModuleInitializerAttribute.

Das Attribut ModuleInitializer kann nur auf eine Methode angewendet werden, die

  • statisch ist.
  • parameterlos ist.
  • Gibt voidzurück.
  • für das enthaltende Modul (internal oder public) zugänglich ist.
  • keine generische Methode ist.
  • in keiner generischen Klasse enthalten ist.
  • keine lokale Funktion ist.

Das Attribut ModuleInitializer kann auf mehrere Methoden angewendet werden. In diesem Fall ruft die Laufzeit die Methoden in einer deterministischen, jedoch nicht angegebenen Reihenfolge auf.

Das folgende Beispiel veranschaulicht, wie mehrere Modulinitialisierermethoden verwendet werden. Die Methoden Init1 und Init2 werden vor Main ausgeführt und fügen der Text-Eigenschaft jeweils eine Zeichenfolge hinzu. Bei der Ausführung von Main verfügt die Text-Eigenschaft also bereits über Zeichenfolgen von beiden Initialisierermethoden.

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

Quellcode-Generatoren müssen gelegentlich Initialisierungscode generieren. Modulinitialisierer stellen für diesen Code einen Standardspeicherort bereit. In den meisten übrigen Fällen sollten Sie einen statischen Konstruktor anstelle eines Modulinitialisierers schreiben.

SkipLocalsInit-Attribut

Das SkipLocalsInit-Attribut verhindert, dass der Compiler bei der Ausgabe an Metadaten das Flag .locals init festlegt. SkipLocalsInit ist ein einwertiges Attribut, das auf eine Methode, eine Eigenschaft, eine Klasse, eine Struktur, eine Schnittstelle oder ein Modul angewendet werden kann, jedoch nicht auf eine Assembly. SkipLocalsInit ist ein Alias für SkipLocalsInitAttribute.

Das Flag .locals init bewirkt, dass die CLR alle in einer Methode deklarierten lokalen Variablen mit ihrem Standardwert initialisiert. Da auch der Compiler dafür sorgt, dass eine Variable erst nach dem Zuweisen eines Werts verwendet werden kann, ist das .locals init-Attribut in der Regel nicht erforderlich. Allerdings kann die zusätzliche Null-Initialisierung in einigen Szenarien messbare Auswirkungen auf die Leistung haben, z. B. wenn Sie stackalloc verwenden, um ein Array auf dem Stack zuzuweisen. In diesen Fällen können Sie das Attribut SkipLocalsInit hinzufügen. Wird das Attribut direkt auf eine Methode angewendet, wirkt es sich auf die Methode und alle darin geschachtelten Funktionen, wie etwa Lambda- und lokale Funktionen, aus. Wird es auf einen Typ oder ein Modul angewendet, werden alle darin geschachtelten Methoden beeinflusst. Das Attribut hat keine Auswirkungen auf abstrakte Methoden, jedoch auf Code, der für die Implementierung generiert wurde.

Dieses Attribut erfordert die Compileroption AllowUnsafeBlocks. Diese Anforderung signalisiert, dass in einigen Fällen nicht zugewiesener Arbeitsspeicher vom Code angezeigt werden kann (z. B. durch das Lesen aus dem nicht initialisierten, im Stapel zugeordneten Arbeitsspeicher).

Das folgende Beispiel veranschaulicht, wie sich das SkipLocalsInit-Attribut auf eine Methode auswirkt, die stackalloc verwendet. Mit der Methode wird der gesamte Inhalt des Arbeitsspeichers zum Zeitpunkt angezeigt, als ein Array aus ganzen Zahlen zugeordnet wurde.

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

Wenn Sie diesen Code ausprobieren möchten, legen Sie in Ihrer CSPROJ-Datei die Compileroption AllowUnsafeBlocks fest:

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

Siehe auch