Freigeben über


Grundlegendes zur Kürzungsanalyse

In diesem Artikel werden die grundlegenden Konzepte der Trim-Analyse erläutert, um zu verstehen, warum bestimmte Codemuster Warnungen erzeugen und wie Sie Ihren Code trim-kompatibel machen können. Wenn Sie diese Konzepte verstehen, können Sie fundierte Entscheidungen treffen, wenn Sie Trimwarnungen angehen, anstatt einfach Attribute zu verstreuen, um die Warnungen der Tools zu unterdrücken.

Wie der Trimmer Code analysiert

Der Trimmer führt zur Veröffentlichungszeit statische Analysen durch, um zu bestimmen, welcher Code von Ihrer Anwendung verwendet wird. Sie beginnt mit bekannten Einstiegspunkten (z. B. Ihrer Main Methode) und folgt den Codepfaden durch Ihre Anwendung.

Was der Trimmer erkennen kann

Der Trimmer zeichnet sich durch die Analyse direkter, kompilierungszeit-sichtbarer Codemuster aus.

// The trimmer CAN understand these patterns:
var date = new DateTime();
date.AddDays(1);  // Direct method call - trimmer knows AddDays is used

var list = new List<string>();
list.Add("hello");  // Generic method call - trimmer knows List<string>.Add is used

string result = MyUtility.Process("data");  // Direct static method call

In diesen Beispielen kann der Trimmer dem Codepfad folgen und DateTime.AddDays, List<string>.Add und MyUtility.Process als verwendeten Code markieren, der in der endgültigen Anwendung beibehalten werden soll.

Was der Trimmer nicht verstehen kann

Der Trimmer hat Schwierigkeiten mit dynamischen Vorgängen, bei denen das Ziel einer Operation erst zur Laufzeit bekannt ist.

// The trimmer CANNOT fully understand these patterns:
Type type = Type.GetType(Console.ReadLine());  // Type name from user input
type.GetMethod("SomeMethod");  // Which method? On which type?

object obj = GetSomeObject();
obj.GetType().GetProperties();  // What type will obj be at runtime?

Assembly asm = Assembly.LoadFrom(pluginPath);  // What's in this assembly?

In diesen Beispielen hat der Trimmer keine Möglichkeit zu wissen:

  • Welche Art von Typ der Benutzer eingeben wird
  • Welchen Typ GetSomeObject() zurückgibt?
  • Welcher Code in der dynamisch geladenen Assembly vorhanden ist

Dies ist das grundlegende Problem, das durch Trim-Warnungen angesprochen wird.

Das Spiegelungsproblem

Reflection ermöglicht Code, Typen und Mitglieder dynamisch zur Laufzeit zu prüfen und aufzurufen. Dies ist leistungsfähig, schafft aber eine Herausforderung für statische Analysen.

Warum Reflektionstäuschungen das Trimmen beeinträchtigen

Betrachten Sie dieses Beispiel:

void PrintMethodNames(Type type)
{
    foreach (var method in type.GetMethods())
    {
        Console.WriteLine(method.Name);
    }
}

// Called somewhere in the app
PrintMethodNames(typeof(DateTime));

Aus Sicht des Trimmers:

  • Es wird festgestellt, dass type.GetMethods() aufgerufen wird.
  • Es weiß nicht, was type sein wird (es ist ein Parameter).
  • Es kann nicht bestimmt werden, welche Methoden der Typen beibehalten werden müssen.
  • Ohne Anleitung kann es Methoden entfernen DateTime, um den Code zu unterbrechen.

Folglich erzeugt der Trimmer eine Warnung bei diesem Code.

Grundlegendes über "DynamicallyAccessedMembers"

DynamicallyAccessedMembersAttribute löst das Spiegelungsproblem, indem ein expliziter Vertrag zwischen dem Aufrufer und der aufgerufenen Methode erstellt wird.

Der grundlegende Zweck

DynamicallyAccessedMembers teilt dem Trimmer mit: "Dieser Parameter (oder Feld oder Rückgabewert) wird eine Type enthalten, deren bestimmte Mitglieder beibehalten werden müssen, da Reflexion verwendet wird, um darauf zuzugreifen."

Ein konkretes Beispiel

Lassen Sie uns das vorherige Beispiel beheben:

void PrintMethodNames(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    foreach (var method in type.GetMethods())
    {
        Console.WriteLine(method.Name);
    }
}

// When this is called...
PrintMethodNames(typeof(DateTime));

Jetzt versteht der Trimmer Folgendes:

  1. PrintMethodNames erfordert, dass sein Parameter PublicMethods erhalten bleibt.
  2. Der Aufrufpunkt übergibt typeof(DateTime).
  3. DateTimeDaher müssen die öffentlichen Methoden beibehalten werden.

Das Attribut erstellt eine Anforderung , die von der Spiegelungsverwendung in die Quelle des Type Werts rückwärts fließt.

Es ist ein Vertrag, kein Hinweis

Dies ist entscheidend zu verstehen: DynamicallyAccessedMembers ist nicht nur Dokumentation. Der Trimmer setzt diesen Vertrag durch.

Analogie zu generischen Typeinschränkungen

Wenn Sie mit generischen Typeinschränkungen vertraut sind, DynamicallyAccessedMembers funktioniert dies ähnlich. Genauso wie generische Einschränkungen durch Ihren Code fließen:

void Process<T>(T value) where T : IDisposable
{
    value.Dispose();  // OK because constraint guarantees IDisposable
}

void CallProcess<T>(T value) where T : IDisposable
{
    Process(value);  // OK - constraint satisfied
}

void CallProcessBroken<T>(T value)
{
    Process(value);  // ERROR - T doesn't have IDisposable constraint
}

DynamicallyAccessedMembers erstellt ähnliche Anforderungen, die über Ihren Code fließen:

void UseReflection([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    type.GetMethods();  // OK because annotation guarantees methods are preserved
}

void PassType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    UseReflection(type);  // OK - requirement satisfied
}

void PassTypeBroken(Type type)
{
    UseReflection(type);  // WARNING - type doesn't have required annotation
}

Beide erstellen Verträge, die erfüllt werden müssen, und beide erzeugen Fehler oder Warnungen, wenn der Vertrag nicht erfüllt werden kann.

Wie der Vertrag erzwungen wird

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
Type GetTypeForProcessing() 
{
    return typeof(DateTime);  // OK - trimmer will preserve DateTime's public methods
}

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
Type GetTypeFromInput()
{
    // WARNING: The trimmer can't verify that the type from GetType()
    // will have its public methods preserved
    return Type.GetType(Console.ReadLine());
}

Wenn Sie den Vertrag (wie im zweiten Beispiel) nicht erfüllen können, erhalten Sie eine Warnung.

Grundlegendes zu "RequiresUnreferencedCode"

Einige Codemuster können einfach nicht statisch analyzierbar gemacht werden. Verwenden Sie RequiresUnreferencedCodeAttributefür diese Fälle .

Wann man RequiresUnreferencedCode verwenden sollte

Verwenden Sie das RequiresUnreferencedCodeAttribute Attribut in folgenden Fällen:

  • Das Spiegelungsmuster ist grundlegend dynamisch: Laden von Assemblys oder Typen nach Zeichenfolgennamen aus externen Quellen.
  • Die Komplexität ist zu hoch zu kommentieren: Code, der Spiegelung auf komplexe, datengesteuerte Weise verwendet.
  • Sie verwenden Laufzeitcodegenerierung: Technologien wie System.Reflection.Emit oder das dynamic Schlüsselwort.

Beispiel:

[RequiresUnreferencedCode("Plugin loading is not compatible with trimming")]
void LoadPlugin(string pluginPath)
{
    Assembly pluginAssembly = Assembly.LoadFrom(pluginPath);
    // Plugin assemblies aren't known at publish time
    // This fundamentally cannot be made trim-compatible
}

Der Zweck des Attributs

RequiresUnreferencedCode dient zwei Zwecken:

  1. Unterdrückt Warnungen innerhalb der Methode: Der Trimmer analysiert oder gibt keine Warnungen bezüglich der Reflexionsnutzung.
  2. Erstellt Warnungen an Aufrufwebsites: Jeder Code, der diese Methode aufruft, ruft eine Warnung ab.

Diese Warnung leitet weiter, um Entwicklern Einblicke in trim-inkompatible Codepfade zu geben.

Schreiben guter Nachrichten

Die Meldung sollte Entwicklern helfen, ihre Optionen zu verstehen:

// ❌ Not helpful
[RequiresUnreferencedCode("Uses reflection")]

// ✅ Helpful - explains what's incompatible and suggests alternatives
[RequiresUnreferencedCode("Plugin loading is not compatible with trimming. Consider using a source generator for known plugins instead")]

Wie Anforderungen durch den Code fließen

Wenn Sie wissen, wie Anforderungen verteilt werden, wissen Sie, wo Attribute hinzugefügt werden sollen.

Anforderungen fließen rückwärts

Anforderungen fließen von dem Punkt, an dem die Reflexion verwendet wird, zurück zu dem Ursprung der Type.

void CallChain()
{
    // Step 1: Source of the Type value
    ProcessData<DateTime>();  // ← Requirement ends here
}

void ProcessData<T>()
{
    // Step 2: Type flows through generic parameter
    var type = typeof(T);
    DisplayInfo(type);  // ← Requirement flows back through here
}

void DisplayInfo(Type type)
{
    // Step 3: Reflection creates the requirement
    type.GetMethods();  // ← Requirement starts here
}

Damit diese Trim-Kompatibilität gegeben ist, müssen Sie die Kette annotieren:

void ProcessData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>()
{
    var type = typeof(T);
    DisplayInfo(type);
}

void DisplayInfo(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    type.GetMethods();
}

Jetzt fließen die Anforderungen: GetMethods() erfordert PublicMethodstype Parameter benötigt PublicMethods → generischer T benötigt PublicMethodsDateTime benötigt PublicMethods beibehalten werden.

Anforderungen fließen durch Speicher

Anforderungen fließen auch durch Felder und Eigenschaften:

class TypeHolder
{
    // This field will hold Types that need PublicMethods preserved
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
    private Type _typeToProcess;

    public void SetType<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>()
    {
        _typeToProcess = typeof(T);  // OK - requirement satisfied
    }

    public void Process()
    {
        _typeToProcess.GetMethods();  // OK - field is annotated
    }
}

Auswählen des richtigen Ansatzes

Wenn Sie auf Code stoßen, der Spiegelung benötigt, folgen Sie dieser Entscheidungsstruktur:

1. Können Sie Reflexion vermeiden?

Die beste Lösung besteht darin, nach Möglichkeit Reflexionen zu vermeiden:

// ❌ Uses reflection
void Process(Type type)
{
    var instance = Activator.CreateInstance(type);
}

// ✅ Uses compile-time generics instead
void Process<T>() where T : new()
{
    var instance = new T();
}

2. Ist der Typ zur Kompilierungszeit bekannt?

Wenn die Reflexion erforderlich ist, aber die Typen bekannt sind, verwenden Sie DynamicallyAccessedMembersFolgendes:

// ✅ Trim-compatible
void Serialize<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T obj)
{
    foreach (var prop in typeof(T).GetProperties())
    {
        // Serialize property
    }
}

3. Ist das Muster grundlegend dynamisch?

Wenn die Typen wirklich erst zur Laufzeit bekannt sind, verwenden Sie RequiresUnreferencedCode:

// ✅ Documented as trim-incompatible
[RequiresUnreferencedCode("Dynamic type loading is not compatible with trimming")]
void ProcessTypeByName(string typeName)
{
    var type = Type.GetType(typeName);
    // Work with type
}

Allgemeine Muster und Lösungen

Entwurfsmuster: Fabrikmethoden

// Problem: Creating instances from Type parameter
object CreateInstance(Type type)
{
    return Activator.CreateInstance(type);
}

// Solution: Specify constructor requirements
object CreateInstance(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type)
{
    return Activator.CreateInstance(type);
}

Muster: Plug-In-Systeme

// Problem: Loading unknown assemblies at runtime
[RequiresUnreferencedCode("Plugin loading is not trim-compatible. Plugins must be known at compile time.")]
void LoadPlugins(string pluginDirectory)
{
    foreach (var file in Directory.GetFiles(pluginDirectory, "*.dll"))
    {
        Assembly.LoadFrom(file);
    }
}

// Better solution: Known plugins with source generation
// Use source generators to create plugin registration code at compile time

Wichtige Erkenntnisse

  • Der Trimmer verwendet statische Analysen – es kann nur Codepfade verstehen, die zur Kompilierungszeit sichtbar sind.
  • Reflection bricht die statische Analyse – der Trimmer kann nicht erkennen, was die Reflection zur Laufzeit zugreifen wird.
  • DynamicallyAccessedMembers erstellt Verträge – sie teilt dem Trimmer mit, was beibehalten werden muss.
  • Anforderungen fließen rückwärts – von der Spiegelungsverwendung zurück zur Quelle des Type Werts.
  • RequiresUnreferencedCode dokumentiert Inkompatibilitäten - verwenden Sie es, wenn der Code nicht analysierbar gemacht werden kann.
  • Attribute sind nicht nur Hinweise – der Trimmer erzwingt Verträge und erzeugt Warnungen, wenn sie nicht erfüllt werden können.

Nächste Schritte