Condividi tramite


Correzione degli avvisi di taglio

Quando si abilita il taglio nell'applicazione, .NET SDK esegue l'analisi statica per rilevare i modelli di codice che potrebbero non essere compatibili con il taglio. Gli avvisi di taglio indicano potenziali problemi che potrebbero causare modifiche al comportamento o arresti anomali dopo il taglio.

Un'app che usa il taglio non dovrebbe generare avvisi di taglio. Se sono presenti avvisi di riduzione, testare accuratamente l'app dopo la riduzione per accertarsi che non vi siano cambiamenti nel comportamento.

Questo articolo fornisce flussi di lavoro pratici per risolvere gli avvisi di taglio. Per una comprensione più approfondita dei motivi per cui si verificano questi avvisi e del funzionamento del taglio, vedere Informazioni sull'analisi del taglio.

Informazioni sulle categorie di avviso

Gli avvisi di taglio rientrano in due categorie principali:

  • Codice incompatibile con il taglio - Contrassegnato con RequiresUnreferencedCodeAttribute. Fondamentalmente, il codice non può essere reso analizzabile (ad esempio, caricamento dinamico di assembly o schemi di riflessione complessi). Il metodo è contrassegnato come incompatibile e i chiamanti ricevono avvisi.

  • Codice con i requisiti : annotato con DynamicallyAccessedMembersAttribute. Viene usata la Reflection, ma i tipi sono noti in fase di compilazione. Quando vengono soddisfatti i requisiti, il codice diventa completamente compatibile con il trim.

Flusso di lavoro: determinare l'approccio corretto

Quando viene visualizzato un avviso di taglio, seguire questa procedura nell'ordine seguente:

  1. Elimina riflessione - questa è sempre l'opzione migliore, se possibile.
  2. Usare DynamicallyAccessedMembers - Se i tipi sono noti, rendere il codice compatibile con il trimming.
  3. Usare RequiresUnreferencedCode : se è veramente dinamico, documentare l'incompatibilità.
  4. Eliminare gli avvisi come ultima risorsa : solo se si è certi che il codice sia sicuro.

Approccio 1: Eliminare la riflessione

La soluzione migliore consiste nell'evitare completamente la riflessione quando possibile. In questo modo il tuo codice diventa più veloce e completamente compatibile con le operazioni di ottimizzazione.

Utilizzare generics in fase di compilazione

Sostituire le operazioni del tipo di runtime con parametri generici in fase di compilazione:

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

// ✅ After: Uses generics
void CreateAndProcess<T>() where T : new()
{
    var instance = new T();
    // Process instance...
}

Usare generatori di origine

.NET moderno offre generatori di codice sorgente per scenari comuni di reflection:

Per altre informazioni, vedere Incompatibilità di riduzione note.

Approccio 2: Rendere il codice compatibile con DynamicallyAccessedMembers

Quando i tipi sono noti in fase di compilazione ma è necessaria la reflection, usa DynamicallyAccessedMembersAttribute per rendere il codice compatibile con il trimming.

Procedura dettagliata: Annotare l'utilizzo della riflessione

Si consideri questo esempio che genera un avviso:

void PrintMethodNames(Type type)
{
    // ⚠️ IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods'
    foreach (var method in type.GetMethods())
    {
        Console.WriteLine(method.Name);
    }
}

Passaggio 1: Identificare l'operazione di riflessione eseguita

Il codice chiama GetMethods(), che deve PublicMethods essere mantenuto.

Passaggio 2: Annotare il parametro

Aggiungere DynamicallyAccessedMembers per indicare al trimmer cosa è necessario:

void PrintMethodNames(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    // ✅ No warning - trimmer knows to preserve public methods
    foreach (var method in type.GetMethods())
    {
        Console.WriteLine(method.Name);
    }
}

Passaggio 3: Assicurarsi che i chiamanti soddisfino il requisito

Quando si chiama questo metodo con un tipo noto (typeof), il requisito viene soddisfatto automaticamente:

// ✅ OK - DateTime's public methods will be preserved
PrintMethodNames(typeof(DateTime));

Procedura dettagliata: Propagare i requisiti tramite catene di chiamate

Quando i tipi passano attraverso più metodi, è necessario propagare i requisiti:

void Method1()
{
    Method2<DateTime>();  // ⚠️ Warning: Generic parameter needs annotation
}

void Method2<T>()
{
    Type t = typeof(T);
    Method3(t);  // ⚠️ Warning: Argument doesn't satisfy requirements
}

void Method3(Type type)
{
    var methods = type.GetMethods();  // ⚠️ Warning: Reflection usage
}

Passaggio 1: Iniziare a utilizzare la reflection

Annotare dove viene effettivamente usata la reflection:

void Method3(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    var methods = type.GetMethods();  // ✅ Fixed
}

Passaggio 2: Propagare verso l'alto la catena di chiamate

Ripercorri la catena di chiamate al contrario.

void Method2<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>()
{
    Type t = typeof(T);
    Method3(t);  // ✅ Fixed - T is annotated
}

Passaggio 3: Verificare nel sito di chiamata

void Method1()
{
    Method2<DateTime>();  // ✅ Fixed - DateTime's public methods preserved
}

Per altre informazioni sul flusso dei requisiti attraverso il codice, vedere Informazioni sull'analisi dei tagli.

Valori comuni di DynamicallyAccessedMemberTypes

Scegliere il livello di accesso minimo necessario:

Tipo di membro Quando utilizzare
PublicConstructors Uso di Activator.CreateInstance() o GetConstructor()
PublicMethods Uso di GetMethod() o GetMethods()
PublicFields Uso di GetField() o GetFields()
PublicProperties Uso di GetProperty() o GetProperties() (serializzazione)
PublicEvents Uso di GetEvent() o GetEvents()

Avviso

L'uso di DynamicallyAccessedMemberTypes.All conserva tutti i membri nel tipo di destinazione e tutti i membri nei relativi tipi annidati (ma non le dipendenze transitive come i membri del tipo di ritorno di una proprietà). Questo aumenta significativamente le dimensioni dell'app. Più importante, i membri mantenuti diventano raggiungibili, il che significa che possono contenere il proprio codice problematico. Ad esempio, se un membro preservato chiama un metodo contrassegnato con RequiresUnreferencedCode, tale avviso non può essere risolto perché il membro viene mantenuto tramite l'annotazione di reflection anziché una chiamata esplicita. Usare i tipi di membri minimi necessari per evitare questi problemi di propagazione.

Approccio 3: Contrassegnare il codice come incompatibile con RequiresUnreferencedCode

Quando il codice fondamentalmente non può essere reso analizzabile, usare RequiresUnreferencedCodeAttribute per documentare l'incompatibilità.

Quando usare RequiresUnreferencedCode

Usare questo attributo quando:

  • I tipi vengono caricati in modo dinamico: utilizzando GetType() con stringhe determinate a runtime.
  • Gli assembly vengono caricati in fase di esecuzione: usando LoadFrom(String).
  • Modelli di riflessione complessi: l'uso della riflessione troppo complesso da annotare.
  • Generazione del codice di runtime: utilizzare System.Reflection.Emit oppure la parola chiave dynamic.

Procedura dettagliata: Contrassegnare i metodi incompatibili

Passaggio 1: Identificare il codice veramente incompatibile

Esempio di codice che non può essere reso compatibile con trim:

void LoadPluginByName(string pluginName)
{
    // Type name comes from runtime input - trimmer cannot know what types are needed
    Type pluginType = Type.GetType(pluginName);
    var plugin = Activator.CreateInstance(pluginType);
    // Use plugin...
}

Passaggio 2: Aggiungere l'attributo RequiresUnreferencedCode

[RequiresUnreferencedCode("Plugin loading by name is not compatible with trimming. Consider using compile-time plugin registration instead.")]
void LoadPluginByName(string pluginName)
{
    Type pluginType = Type.GetType(pluginName);
    var plugin = Activator.CreateInstance(pluginType);
    // ✅ No warnings inside this method - it's marked as incompatible
}

Passaggio 3: I chiamanti ricevono avvisi

void InitializePlugins()
{
    // ⚠️ IL2026: Using member 'LoadPluginByName' which has 'RequiresUnreferencedCodeAttribute'
    // can break functionality when trimming application code. Plugin loading by name is not
    // compatible with trimming. Consider using compile-time plugin registration instead.
    LoadPluginByName("MyPlugin");
}

Scrittura di messaggi di avviso effettivi

Un buon RequiresUnreferencedCode messaggio dovrebbe:

  • Specificare le funzionalità non compatibili: sii specifico su ciò che non funziona con il ritaglio.
  • Suggerisci alternative: guida gli sviluppatori verso soluzioni compatibili con trim.
  • Concisa: mantenere brevi e interattivi i messaggi.
// ❌ Not helpful
[RequiresUnreferencedCode("Uses reflection")]

// ✅ Helpful - explains problem and suggests alternative
[RequiresUnreferencedCode("Dynamic type loading is not compatible with trimming. Use generic type parameters or source generators instead.")]

Per indicazioni più lunghe, aggiungere un Url parametro:

[RequiresUnreferencedCode(
    "Plugin system is not compatible with trimming. See documentation for alternatives.",
    Url = "https://docs.example.com/plugin-trimming")]

Propagazione di RequiresUnreferencedCode

Quando un metodo chiama un altro metodo contrassegnato con RequiresUnreferencedCode, in genere è necessario propagare l'attributo :

class PluginSystem
{
    // Use a constant for consistent messaging
    const string PluginMessage = "Plugin system is not compatible with trimming. Use compile-time registration instead.";

    [RequiresUnreferencedCode(PluginMessage)]
    private void LoadPluginImplementation(string name)
    {
        // Low-level plugin loading
    }

    [RequiresUnreferencedCode(PluginMessage)]
    public void LoadPlugin(string name)
    {
        LoadPluginImplementation(name);  // ✅ No warning - method is also marked
    }
}

Modelli e soluzioni comuni

Modello: metodi di fabbrica con Activator.CreateInstance

// ❌ Before: Produces warning
object CreateInstance(Type type)
{
    return Activator.CreateInstance(type);
}

// ✅ After: Trim-compatible
object CreateInstance(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type)
{
    return Activator.CreateInstance(type);
}

Modello: Sistemi plugin che caricano assembly

// This pattern is fundamentally incompatible with trimming
[RequiresUnreferencedCode("Plugin loading is not compatible with trimming. Consider compile-time plugin registration using source generators.")]
void LoadPluginsFromDirectory(string directory)
{
    foreach (var dll in Directory.GetFiles(directory, "*.dll"))
    {
        Assembly.LoadFrom(dll);
    }
}

Modello: contenitori di iniezione delle dipendenze

// Complex DI containers are often incompatible
class Container
{
    [RequiresUnreferencedCode("Service resolution uses complex reflection. Consider using source-generated DI or registering services explicitly.")]
    public object Resolve(Type serviceType)
    {
        // Complex reflection to resolve dependencies
    }
}

Approccio 4: Eliminare gli avvisi come ultima risorsa

Avviso

Eliminare gli avvisi di taglio solo se si è assolutamente certi che il codice sia sicuro. Le soppressioni errate possono causare errori di runtime dopo la riduzione.

Usare UnconditionalSuppressMessageAttribute quando si è verificato che il codice è trim-safe, ma il trimmer non può dimostrarlo staticamente.

Quando l'eliminazione è appropriata

Eliminare gli avvisi solo quando:

  1. Hai assicurato manualmente che tutto il codice richiesto sia preservato (tramite DynamicDependency o altri meccanismi).
  2. Il percorso del codice non viene mai eseguito in scenari di riduzione.
  3. L'applicazione ottimizzata è stata testata accuratamente.

Come eliminare gli avvisi

[RequiresUnreferencedCode("Uses reflection")]
void MethodWithReflection() { /* ... */ }

[UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode",
    Justification = "All referenced types are manually preserved via DynamicDependency attributes")]
void CallerMethod()
{
    MethodWithReflection();  // Warning suppressed
}

Importante

Non usare SuppressMessage o #pragma warning disable per gli avvisi di taglio. Queste operazioni funzionano solo per il compilatore, ma non vengono mantenute nell'assembly compilato. Il trimmer opera su assembly compilati e non visualizzerà queste eliminazioni. Usare sempre UnconditionalSuppressMessage.

Ridurre al minimo l'ambito di soppressione

Applicare eliminazioni al più piccolo ambito possibile. Estrarre la chiamata problematica in una funzione locale:

void ProcessData()
{
    InitializeData();

    CallReflectionMethod();  // Only this call is suppressed

    ProcessResults();

    [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode",
        Justification = "Types are preserved via DynamicDependency on ProcessData method")]
    void CallReflectionMethod()
    {
        MethodWithReflection();
    }
}

Questo approccio:

  • Rende chiaro quale chiamata specifica viene eliminata.
  • Impedisce l'eliminazione accidentale di altri avvisi se il codice cambia.
  • Mantiene la giustificazione vicina alla chiamata soppressa.

Suggerimenti per la risoluzione dei problemi

L'avviso persiste dopo l'aggiunta di DynamicallyAccessedMembers

Assicurarsi di aver annotato l'intera catena di chiamate dall'utilizzo della Reflection all'origine di Type:

  1. Trovare dove viene usata la reflection (ad esempio GetMethods()).
  2. Annotare il parametro del metodo.
  3. Seguire il valore all'indietro in tutte le chiamate al Type metodo.
  4. Annotare ogni parametro, campo o parametro di tipo generico nella catena.

Troppi avvisi da risolvere

  1. Inizia con il tuo codice: correggi prima gli avvisi nel codice che controlli.
  2. Usare TrimmerSingleWarn per visualizzare singoli avvisi dai pacchetti.
  3. Valutare se il taglio è appropriato per l'applicazione.
  4. Controllare le incompatibilità note di trimmatura per i problemi a livello di framework.

Non si è certi di quale oggetto DynamicallyAccessedMemberTypes usare

Esaminare l'API di riflessione chiamata:

  • GetMethod() / GetMethods()PublicMethods
  • GetProperty() / GetProperties()PublicProperties
  • GetField() / GetFields()PublicFields
  • GetConstructor() / Activator.CreateInstance()PublicParameterlessConstructor o PublicConstructors
  • GetEvent() / GetEvents()PublicEvents

Usare il tipo più stretto possibile per ridurre al minimo le dimensioni dell'app.

Passaggi successivi