Partager via


Présentation de l’analyse de découpage

Cet article explique les concepts fondamentaux de l’analyse de réduction pour vous aider à comprendre pourquoi certains modèles de code produisent des avertissements et comment rendre votre code compatible avec la réduction. La compréhension de ces concepts vous aidera à prendre des décisions éclairées lors du traitement des avertissements de découpage, plutôt que de simplement distribuer les attributs pour faire taire les outils.

Comment l'analyseur de code optimise le code

Le trimmer effectue une analyse statique au moment de la publication pour identifier le code utilisé par votre application. Il commence par des points d’entrée connus (comme votre Main méthode) et suit les chemins de code de votre application.

Ce que la tondeuse peut comprendre

La tondeuse excelle dans l’analyse des modèles de code directement visibles à la compilation.

// 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

Dans ces exemples, le trimmer peut suivre le chemin d'accès du code et marquer DateTime.AddDays, List<string>.Add, et MyUtility.Process comme du code utilisé qui doit être conservé dans l'application finale.

Ce que la tondeuse ne peut pas comprendre

La tondeuse rencontre des difficultés avec les opérations dynamiques où la cible d'une opération n'est pas connue jusqu'à ce que l'opération soit en cours d'exécution.

// 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?

Dans ces exemples, la tondeuse ne peut pas savoir :

  • Quel type l’utilisateur entrera
  • Quel type GetSomeObject() retourne
  • Quel code existe dans l’assembly chargé dynamiquement

Il s’agit du problème fondamental que les avertissements de trim abordent.

Le problème de réflexion

La réflexion permet au code d’inspecter et d’appeler dynamiquement des types et des membres au moment de l’exécution. Cela est puissant, mais crée un défi pour l’analyse statique.

Pourquoi la réflexion empêche l'élagage

Prenons cet exemple :

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

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

Du point de vue de la tondeuse :

  • On remarque que type.GetMethods() est appelé.
  • Il ne sait pas ce qui type sera (il s’agit d’un paramètre).
  • Il ne peut pas déterminer quelles méthodes des différents types doivent être préservées.
  • Sans guidance, cela pourrait supprimer des méthodes de DateTime, endommageant le code.

Par conséquent, l'outil d'élagage génère un avertissement sur ce code.

Comprendre DynamicallyAccessedMembers

DynamicallyAccessedMembersAttribute résout le problème de réflexion en créant un contrat explicite entre l’appelant et la méthode appelée.

L’objectif fondamental

DynamicallyAccessedMembers informe le trimmer : « Ce paramètre (ou champ, ou valeur de retour) contiendra un Type dont il faudra conserver des membres spécifiques, car l'accès à ceux-ci se fera par réflexion. »

Exemple concret

Nous allons corriger l’exemple précédent :

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

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

Maintenant, le découpage comprend :

  1. PrintMethodNames nécessite que son paramètre ait PublicMethods été conservé.
  2. Le site d’appel passe typeof(DateTime).
  3. Par conséquent, DateTimeles méthodes publiques doivent être conservées.

L’attribut crée une exigence qui s'écoule en sens inverse de l'utilisation de la réflexion vers la source de la Type valeur.

C’est un contrat, pas un conseil

Il est essentiel de comprendre : DynamicallyAccessedMembers ce n’est pas seulement de la documentation. La tondeuse assure ce contrat.

Analogie avec les contraintes de type générique

Si vous êtes familiarisé avec les contraintes de type générique, DynamicallyAccessedMembers fonctionne de la même façon. Tout comme les contraintes génériques transitent par votre code :

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 crée des exigences similaires qui transitent par votre code :

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
}

Les deux créent des contrats qui doivent être remplis et produisent des erreurs ou des avertissements lorsque le contrat ne peut pas être satisfait.

Application du contrat

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

Si vous ne pouvez pas remplir le contrat (comme dans le deuxième exemple), vous recevez un avertissement.

Présentation de RequiresUnreferencedCode

Certains modèles de code ne peuvent simplement pas être analysés statiquement. Pour ces cas, utilisez RequiresUnreferencedCodeAttribute.

Quand utiliser RequiresUnreferencedCode

Utilisez l’attribut RequiresUnreferencedCodeAttribute quand :

  • Le modèle de réflexion est fondamentalement dynamique : chargement d’assemblys ou de types par noms de chaîne à partir de sources externes.
  • La complexité est trop élevée pour annoter : le code qui utilise la réflexion de manière complexe et pilotée par les données.
  • Vous utilisez la génération de code runtime : technologies telles que System.Reflection.Emit ou le dynamic mot clé.

Exemple :

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

Objectif de l’attribut

RequiresUnreferencedCode sert à deux fins :

  1. Supprime les avertissements à l’intérieur de la méthode : le trimmer n’analyse ni n’avertit l’utilisation de la réflexion.
  2. Crée des avertissements sur les sites d’appel : tout code appelant cette méthode obtient un avertissement.

Cela « bulle » l’avertissement pour donner aux développeurs une visibilité sur les chemins de code incompatibles.

Écriture de bons messages

Le message doit aider les développeurs à comprendre leurs options :

// ❌ 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")]

Comment les exigences transitent par le code

Comprendre comment les exigences se propagent vous permet de savoir où ajouter des attributs.

Les exigences remontent

Flux des exigences de l’endroit où la réflexion est utilisée jusqu’à l’origine du 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
}

Pour rendre ce découpage compatible, vous devez annoter la chaîne :

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

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

Maintenant, les flux d'exigences : GetMethods() nécessite PublicMethodstype paramètre nécessite PublicMethodsT générique nécessite PublicMethodsDateTime nécessite que PublicMethods soit conservé.

Flux d’exigences via le stockage

Les exigences sont aussi transmises par les champs et les propriétés :

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

Choix de l’approche appropriée

Lorsque vous rencontrez du code qui a besoin de réflexion, suivez cet arbre de décision :

1. Pouvez-vous éviter la réflexion ?

La meilleure solution consiste à éviter la réflexion lorsque cela est possible :

// ❌ 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. Le type est-il connu au moment de la compilation ?

Si la réflexion est nécessaire, mais que les types sont connus, utilisez DynamicallyAccessedMembers:

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

3. Le modèle est-il fondamentalement dynamique ?

Si les types ne sont pas connus jusqu’à l’exécution, utilisez 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
}

Modèles et solutions courants

Modèle : méthodes de fabrique

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

Modèle : Systèmes de plug-in

// 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

Points clés à prendre

  • Le découpage utilise l’analyse statique : il ne peut comprendre que les chemins de code visibles au moment de la compilation.
  • La réflexion compromet l'analyse statique : l'outil de réduction ne peut pas voir ce que la réflexion accède au moment de l'exécution.
  • DynamicallyAccessedMembers crée des contrats : il indique au trimmer ce qui doit être conservé.
  • Les exigences sont descendantes , de l’utilisation de la réflexion à la source de la Type valeur.
  • RequiresUnreferencedCode documents incompatibility : utilisez-le quand le code ne peut pas être analysé.
  • Les attributs ne sont pas seulement des indicateurs : le découpage applique des contrats et produit des avertissements lorsqu’ils ne peuvent pas être respectés.

Étapes suivantes