Einführung in Kürzungswarnungen

Konzeptionell ist das Kürzen einfach: Wenn Sie eine Anwendung veröffentlichen, analysiert das .NET-SDK die gesamte Anwendung und entfernt sämtlichen nicht verwendeten Code. Es kann jedoch schwierig sein zu ermitteln, was nicht verwendet und was verwendet wird.

Um beim Kürzen von Anwendungen Verhaltensänderungen zu verhindern, bietet das .NET-SDK eine statische Analyse der Kürzungskompatibilität mittels Kürzungswarnungen. Der Kürzer erzeugt Kürzungswarnungen, wenn er Code findet, der möglicherweise beim Kürzen nicht kompatibel ist. Code, der nicht kürzungskompatibel ist, kann bei einer Anwendung nach dem Kürzen Verhaltensänderungen oder sogar Abstürze verursachen. Im Idealfall sollten alle Anwendungen, die Kürzen verwenden, keine Kürzungswarnungen erzeugen. Wenn Kürzungswarnungen angezeigt werden, sollte die App nach dem Kürzen gründlich getestet werden, um sicherzustellen, dass keine Verhaltensänderungen auftreten.

Dieser Artikel hilft Ihnen zu verstehen, warum einige Muster Kürzungswarnungen erzeugen und wie diese Warnungen behoben werden können.

Beispiele für Kürzungswarnungen

Bei einem Großteil des C#-Codes lässt sich einfach feststellen, welcher Code verwendet und welcher Code nicht verwendet wird. Der Trimmer kann beispielsweise Methodenaufrufe, Feld- und Eigenschaftsverweise durchgehen und ermitteln, auf welchen Code zugegriffen wird. Leider stellen einige Features wie z. B. die Reflexion ein erhebliches Problem dar. Betrachten Sie folgenden Code:

string s = Console.ReadLine();
Type type = Type.GetType(s);
foreach (var m in type.GetMethods())
{
    Console.WriteLine(m.Name);
}

In diesem Beispiel fordert GetType() dynamisch einen Typ mit einem unbekannten Namen an und gibt dann die Namen aller zugehörigen Methoden aus. Da der Typname zum Zeitpunkt der Veröffentlichung immer unbekannt ist, hat der Kürzer keine Möglichkeit zu wissen, welcher Typ in der Ausgabe beibehalten werden soll. Es ist wahrscheinlich, dass dieser Code vor dem Kürzen funktioniert hat (sofern die Eingabe bekanntermaßen im Zielframework vorhanden ist), aber nach dem Kürzen würde er wahrscheinlich eine Ausnahme aufgrund eines Nullverweises verursachen (weil Type.GetType NULL zurückgibt, wenn der Typ nicht gefunden wird).

In diesem Fall gibt der Kürzer beim Aufruf von Type.GetType eine Warnung aus, die darauf hinweist, dass er nicht ermitteln kann, welcher Typ von der Anwendung verwendet werden wird.

Reagieren auf Kürzungswarnungen

Kürzungswarnungen sollen den Kürzungsvorgang vorhersagbar gestalten. Es gibt zwei große Kategorien von Warnungen, die Ihnen möglicherweise angezeigt werden:

  1. Die Funktionalität ist mit dem Kürzen nicht kompatibel
  2. Die Funktionalität hat bestimmte Anforderungen an die Eingabe, um mit dem Kürzen kompatibel zu sein

Die Funktionalität ist nicht kompatibel mit dem Kürzen

Dies sind in der Regel Methoden, die entweder überhaupt nicht funktionieren oder in einigen Fällen beschädigt werden, wenn sie in einer gekürzten Anwendung verwendet werden. Ein gutes Beispiel ist die Type.GetType-Methode aus dem vorherigen Beispiel. In einer gekürzten App funktioniert sie möglicherweise, aber es gibt keine Garantie. Solche APIs sind mit RequiresUnreferencedCodeAttribute markiert.

RequiresUnreferencedCodeAttribute ist einfach und allgemein: Es ist ein Attribut, das bedeutet, dass das Member als nicht kompatibel mit dem Kürzen kommentiert wurde. Dieses Attribut wird verwendet, wenn Code grundsätzlich nicht kürzungskompatibel ist oder wenn die Kürzungsabhängigkeit zu komplex ist, um sie dem Trimmer zu erklären. Dies gilt häufig für Methoden, die dynamisch Code laden, z. B. über LoadFrom(String), alle Typen in einer Anwendung oder Assembly aufzählen oder durchsuchen, z. B. über GetType(), das C#-Schlüsselwort dynamic verwenden oder andere Technologien zur Codegenerierung während der Laufzeit verwenden. Beispiel:

[RequiresUnreferencedCode("This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead")]
void MethodWithAssemblyLoad()
{
    ...
    Assembly.LoadFrom(...);
    ...
}

void TestMethod()
{
    // IL2026: Using method 'MethodWithAssemblyLoad' which has 'RequiresUnreferencedCodeAttribute'
    // can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead.
    MethodWithAssemblyLoad();
}

Es gibt nicht viele Problemumgehungen für RequiresUnreferencedCode. Die beste Lösung besteht darin, den Aufruf der Methode beim Kürzen vollständig zu vermeiden und stattdessen eine kürzungskompatible Methode zu verwenden.

Kennzeichnen der Funktionalität als nicht mit dem Kürzen kompatibel

Wenn Sie eine Bibliothek schreiben und Sie nicht die Kontrolle darüber haben, ob inkompatible Funktionalitäten verwendet werden sollen, können Sie diese mit RequiresUnreferencedCode markieren. Dadurch wird die Methode als nicht mit dem Kürzen kompatibel gekennzeichnet. Durch die Verwendung von RequiresUnreferencedCode werden alle Kürzungswarnungen in der angegebenen Methode stumm geschaltet, aber es wird eine Warnung erzeugt, wenn eine andere Person sie aufruft.

Dies RequiresUnreferencedCodeAttribute erfordert, dass Sie eine Message angeben. Die Meldung wird als Teil einer Warnung angezeigt, die dem Entwickler gemeldet wird, der die markierte Methode aufruft. Beispiel:

IL2026: Using member <incompatible method> which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. <The message value>

Im obigen Beispiel kann eine Warnung für eine bestimmte Methode wie folgt aussehen:

IL2026: Using member 'MethodWithAssemblyLoad()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead.

Entwickler, die solche APIs aufrufen, sind in der Regel nicht an den Einzelheiten der betroffenen API oder an den Besonderheiten in Bezug auf das Kürzen interessiert.

Eine gute Nachricht sollte angeben, welche Funktionalität nicht mit dem Kürzen kompatibel ist, und dann den Entwickler anleiten, was ihre möglichen nächsten Schritte sind. Möglicherweise wird empfohlen, eine andere Funktionalität zu verwenden oder zu ändern, wie die Funktionalität verwendet wird. Möglicherweise wird auch einfach angegeben, dass die Funktionalität noch nicht mit dem Kürzen kompatibel ist, ohne dass ein klarer Ersatz angegeben wird.

Wenn der Leitfaden für den Entwickler zu lang wird, um in eine Warnmeldung eingeschlossen zu werden, können Sie eine optionale Url zu RequiresUnreferencedCodeAttribute hinzufügen, um den Entwickler auf eine Webseite hinzuführen, die das Problem und mögliche Lösungen ausführlicher beschreibt.

Beispiel:

[RequiresUnreferencedCode("This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead", Url = "https://site/trimming-and-method")]
void MethodWithAssemblyLoad() { ... }

Dies erzeugt eine Warnung:

IL2026: Using member 'MethodWithAssemblyLoad()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead. https://site/trimming-and-method

Die Verwendung von RequiresUnreferencedCode führt häufig dazu, dass aus demselben Grund mehr Methoden mit ihr markiert werden. Dies ist häufig der Fall, wenn eine High-Level-Methode nicht mehr mit dem Kürzen kompatibel ist, weil sie eine Low-Level-Methode aufruft, die nicht kürzungskompatibel ist. Sie geben die Warnung an eine öffentliche API weiter. Jede Verwendung von RequiresUnreferencedCode benötigt eine Nachricht, und in diesen Fällen sind die Nachrichten wahrscheinlich identisch. Um die Duplizierung von Zeichenketten zu vermeiden und die Wartung zu erleichtern, verwenden Sie ein konstantes Zeichenfolgenfeld, um die Nachricht zu speichern:

class Functionality
{
    const string IncompatibleWithTrimmingMessage = "This functionality is not compatible with trimming. Use 'FunctionalityFriendlyToTrimming' instead";

    [RequiresUnreferencedCode(IncompatibleWithTrimmingMessage)]
    private void ImplementationOfAssemblyLoading()
    {
        ...
    }

    [RequiresUnreferencedCode(IncompatibleWithTrimmingMessage)]
    public void MethodWithAssemblyLoad()
    {
        ImplementationOfAssemblyLoading();
    }
}

Funktionalität mit Anforderungen an ihre Eingabe

Das Kürzen stellt APIs bereit, um weitere Anforderungen für Eingaben an Methoden und andere Member zu spezifizieren, die zu kürzungskompatiblem Code führen. Diese Anforderungen beziehen sich in der Regel auf Reflexion und die Fähigkeit, auf bestimmte Members oder Vorgänge eines Typs zuzugreifen. Solche Anforderungen werden mithilfe der DynamicallyAccessedMembersAttribute spezifiziert.

Im Gegensatz zu RequiresUnreferencedCode kann Reflexion manchmal vom Trimmer interpretiert werden, sofern die Anmerkungen korrekt sind. Sehen wir uns das ursprüngliche Beispiel noch einmal an:

string s = Console.ReadLine();
Type type = Type.GetType(s);
foreach (var m in type.GetMethods())
{
    Console.WriteLine(m.Name);
}

Im vorherigen Beispiel ist Console.ReadLine() das eigentliche Problem. Da jeder Typ gelesen werden könnte, kann der Trimmer nicht entscheiden, ob Sie Methoden für System.DateTime oder System.Guid oder einen anderen Typ benötigen. Andererseits wäre der folgende Code in Ordnung:

Type type = typeof(System.DateTime);
foreach (var m in type.GetMethods())
{
    Console.WriteLine(m.Name);
}

Hier kann der Trimmer den genauen Typ erkennen, auf den verwiesen wird: System.DateTime. Jetzt kann er die Flowanalyse verwenden, um zu bestimmen, dass alle öffentlichen Methoden für System.DateTime beibehalten werden müssen. Wo also kommt DynamicallyAccessMembers ins Spiel? Sobald die Reflexion auf mehrere Methoden aufgeteilt wird. Im folgenden Code können wir sehen, dass der Typ System.DateTime zu Method3 fließt, wo Reflexion für den Zugriff auf die Methoden von System.DateTime verwendet wird,

void Method1()
{
    Method2<System.DateTime>();
}
void Method2<T>()
{
    Type t = typeof(T);
    Method3(t);
}
void Method3(Type type)
{
    var methods = type.GetMethods();
    ...
}

Wenn Sie den vorherigen Code kompilieren, wird die folgende Warnung erstellt:

IL2070: Program.Method3(Type): Das „this“-Argument erfüllt nicht „DynamicallyAccessedMemberTypes.PublicMethods“ im Aufruf von „System.Type.GetMethods()“. Der Parameter „type“ der Methode „Program.Method3(Type)“ verfügt über keine übereinstimmenden Anmerkungen. Der Quellwert muss mindestens diejenigen Anforderungen deklarieren, die an dem ihm zugewiesenen Zielspeicherort deklariert sind.

Für Leistung und Stabilität wird die Flowanalyse nicht zwischen Methoden ausgeführt. Daher ist eine Anmerkung erforderlich, um Informationen zwischen Methoden vom Reflexionsaufruf (GetMethods) an die Quelle von Type zu übergeben. Im vorherigen Beispiel besagt die Warnung des Kürzers, dass GetMethods erfordert, dass die Type-Objektinstanz, für die sie aufgerufen wird, die PublicMethods-Anmerkung aufweist, aber die Variable type hat nicht dieselbe Anforderung. Anders ausgedrückt: Wir müssen die Anforderungen von GetMethods nach oben an den Aufrufer weitergeben:

void Method1()
{
    Method2<System.DateTime>();
}
void Method2<T>()
{
    Type t = typeof(T);
    Method3(t);
}
void Method3(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    var methods = type.GetMethods();
  ...
}

Nach dem Kommentieren des Parameters type wird die ursprüngliche Warnung nicht mehr angezeigt, aber es wird eine andere angezeigt:

IL2087: Das „type“-Argument erfüllt nicht „DynamicallyAccessedMemberTypes.PublicMethods“ im Aufruf von „Program.Method3(Type)“. Der generische Parameter „T“ von „Program.Method2<T>()‘ verfügt über keine übereinstimmenden Anmerkungen.

Wir haben Anmerkungen bis zum Parameter type von Method3 weitergegeben, in Method2 liegt ein ähnliches Problem vor. Der Trimmer kann den Wert T nachverfolgen, während er durch den Aufruf von typeof fließt, der lokalen Variablen t zugewiesen und an Method3 übergeben wird. An diesem Punkt stellt er fest, dass der Parameter typePublicMethods erfordert, aber keine Anforderungen für T vorhanden sind, und erzeugt eine neue Warnung. Um dies zu beheben, müssen wir „kommentieren und weitergeben“, indem wir Anmerkungen ganz nach oben in der Aufrufkette anwenden, bis wir einen statisch bekannten Typ (z. B. System.DateTime oder System.Tuple) oder einen anderen kommentierten Wert erreichen. In diesem Fall müssen wir den Typparameter T von Method2 kommentieren.

void Method1()
{
    Method2<System.DateTime>();
}
void Method2<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>()
{
    Type t = typeof(T);
    Method3(t);
}
void Method3(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    var methods = type.GetMethods();
  ...
}

Jetzt gibt es keine Warnungen mehr, da der Kürzer weiß, auf welche Member über die Runtimereflexion (öffentliche Methoden) und auf welchen Typen (System.DateTime) zugegriffen werden kann, und er bewahrt sie. Es ist eine bewährte Methode, Anmerkungen hinzuzufügen, damit der Kürzer weiß, was beibehalten werden soll.

Warnungen, die von diesen zusätzlichen Anforderungen erzeugt werden, werden automatisch unterdrückt, wenn sich der betroffene Code in einer Methode mit RequiresUnreferencedCode befindet.

Im Gegensatz zu RequiresUnreferencedCode, was einfach die Inkompatibilität meldet, macht das Hinzufügen von DynamicallyAccessedMembers den Code mit dem Kürzen kompatibel.

Unterdrücken von Warnungen des Kürzers

Wenn Sie feststellen können, dass der Aufruf sicher ist und der benötigte Code nicht entfernt wird, können Sie die Warnung auch durch UnconditionalSuppressMessageAttribute unterdrücken. Beispiel:

[RequiresUnreferencedCode("Use 'MethodFriendlyToTrimming' instead")]
void MethodWithAssemblyLoad() { ... }

[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
    Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
void TestMethod()
{
    InitializeEverything();

    MethodWithAssemblyLoad(); // Warning suppressed

    ReportResults();
}

Warnung

Seien Sie beim Unterdrücken von Kürzungswarnungen sehr vorsichtig. Es ist möglich, dass der Aufruf jetzt kürzungskompatibel ist, aber wenn Sie Ihren Code ändern, kann sich dies ändern, und möglicherweise vergessen Sie die Überprüfung sämtlicher Unterdrückungen.

UnconditionalSuppressMessage ist wie SuppressMessage, kann aber von publish und anderen Post-Build-Tools erkannt werden.

Wichtig

Verwenden Sie nicht SuppressMessage oder #pragma warning disable, um Warnungen des Kürzers zu unterdrücken. Diese funktionieren nur für den Compiler, werden aber in der kompilierten Assembly nicht beibehalten. Der Kürzer arbeitet auf kompilierten Assemblys und würde die Unterdrückung nicht sehen.

Die Unterdrückung gilt für den gesamten Methodenkörper. In unserem obigen Beispiel werden also alle IL2026-Warnungen aus der Methode unterdrückt. Dies macht es schwieriger zu verstehen, da es nicht klar ist, welche Methode die problematische ist, es sei denn, Sie fügen einen Kommentar hinzu. Noch wichtiger: Wenn sich der Code in Zukunft ändert, z. B. wenn ReportResults ebenfalls nicht mehr kürzungskompatibel wird, wird für diesen Methodenaufruf keine Warnung gemeldet.

Sie können dies beheben, indem Sie den problematischen Methodenaufruf in eine separate Methode oder lokale Funktion umgestalten und dann die Unterdrückung nur auf diese Methode anwenden:

void TestMethod()
{
    InitializeEverything();

    CallMethodWithAssemblyLoad();

    ReportResults();

    [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
        Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
    void CallMethodWithAssemblyLoad()
    {
        MethodWIthAssemblyLoad(); // Warning suppressed
    }
}