Introduzione agli avvisi di taglio

Concettualmente, il taglio è semplice: quando si pubblica l'applicazione, .NET SDK analizza l'intera applicazione e rimuove tutto il codice inutilizzato. Tuttavia, può essere difficile determinare ciò che è inutilizzato, o più precisamente, ciò che viene usato.

Per evitare modifiche nel comportamento durante il taglio delle applicazioni, .NET SDK fornisce un'analisi statica della compatibilità di taglio tramite "avvisi di taglio". Gli avvisi di taglio vengono generati dal trimmer quando trova codice che potrebbe non essere compatibile con il taglio. Il codice non compatibile con il taglio può produrre modifiche comportamentali o addirittura arresti anomali, in un'applicazione dopo che è stato tagliato. Idealmente, tutte le applicazioni che usano il taglio non devono avere avvisi di taglio. Se sono presenti avvisi di taglio, l'app deve essere testata accuratamente dopo il taglio per garantire che non siano presenti modifiche di comportamento.

Questo articolo aiuterà gli sviluppatori a comprendere perché alcuni modelli generano avvisi di taglio e come questi avvisi possono essere risolti.

Esempi di avvisi di taglio

Per la maggior parte del codice C#, è semplice determinare il codice usato e il codice inutilizzato. Il trimmer può eseguire chiamate ai metodi, ai riferimenti di campo e proprietà e così via e determinare il codice a cui si accede. Purtroppo alcune funzionalità, come la reflection, presentano un problema significativo. Osservare il codice seguente:

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

In questo esempio richiede GetType() dinamicamente un tipo con un nome sconosciuto e quindi stampa i nomi di tutti i relativi metodi. Poiché non è possibile sapere in fase di pubblicazione quale nome del tipo verrà usato, non è possibile che il trimmer sappia quale tipo conservare nell'output. È probabile che questo codice abbia funzionato prima di tagliare (purché l'input esista nel framework di destinazione), ma probabilmente produrrebbe un'eccezione di riferimento Null dopo il taglio (a causa della Type.GetType restituzione di null).

In questo caso, il trimmer genera un avviso sulla chiamata a Type.GetType, a indicare che non è in grado di determinare quale tipo verrà usato dall'applicazione.

Reazione agli avvisi di taglio

Gli avvisi di taglio sono progettati per portare la prevedibilità al taglio. Esistono due grandi categorie di avvisi che è probabile che vengano visualizzati:

  1. RequiresUnreferencedCode
  2. DynamicallyAccessedMembers

RequiresUnreferencedCode

RequiresUnreferencedCodeAttribute è semplice e ampio: si tratta di un attributo che significa che il membro è stato annotato incompatibile con il taglio, il che significa che potrebbe usare la reflection o un altro meccanismo per accedere al codice che potrebbe essere eliminato. Questo attributo viene usato quando il codice non è fondamentalmente compatibile con il taglio o la dipendenza trim è troppo complessa da spiegare al trimmer. Questo vale spesso per i metodi che usano la parola chiave C# dynamic , l'accesso ai tipi da LoadFrom(String)o altre tecnologie di generazione del codice di runtime. Un esempio è:

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

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

Non sono disponibili molte soluzioni alternative per RequiresUnreferencedCode. La correzione migliore consiste nell'evitare di chiamare il metodo quando si taglia e si usa qualcos'altro compatibile con il taglio. Se si sta scrivendo una libreria e non si trova nel controllo se chiamare o meno il metodo, è anche possibile aggiungere RequiresUnreferencedCode al proprio metodo. In questo modo il metodo verrà annotato come non compatibile con trim. L'aggiunta RequiresUnreferencedCode disattiva tutti gli avvisi di taglio nel metodo specificato, ma genererà un avviso ogni volta che qualcun altro lo chiama. Per questo motivo, è principalmente utile per gli autori di librerie "far saltare" l'avviso a un'API pubblica.

Se in qualche modo è possibile determinare che la chiamata è sicura e tutto il codice necessario non verrà eliminato, è anche possibile eliminare l'avviso usando UnconditionalSuppressMessageAttribute. Ad esempio:

[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()
{
    MethodWithAssemblyLoad(); // Warning suppressed
}

UnconditionalSuppressMessage è simile SuppressMessage , ma può essere visto da publish e altri strumenti post-compilazione. SuppressMessage Le direttive e #pragma sono presenti solo nell'origine in modo che non possano essere usate per disattivare gli avvisi del trimmer. Prestare molta attenzione quando si eliminano gli avvisi di taglio: è possibile che la chiamata sia compatibile con il taglio ora, ma quando si modifica il codice che può cambiare e si può dimenticare di esaminare tutte le eliminazioni.

DynamicallyAccessedMembers

DynamicallyAccessedMembersAttribute si tratta in genere di reflection. A differenza di RequiresUnreferencedCode, la reflection può talvolta essere compresa dal trimmer purché sia annotata correttamente. Diamo un'altra occhiata all'esempio originale:

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

Nell'esempio precedente il problema reale è Console.ReadLine(). Poiché qualsiasi tipo può essere letto, il trimmer non è in grado di sapere se sono necessari metodi su System.DateTime o System.Guid qualsiasi altro tipo. D'altra parte, il codice seguente sarebbe corretto:

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

Qui il trimmer può vedere il tipo esatto a cui si fa riferimento: System.DateTime. È ora possibile usare l'analisi del flusso per determinare che deve mantenere tutti i metodi pubblici in System.DateTime. Allora, dove DynamicallyAccessMembers entra? Quando la reflection viene suddivisa tra più metodi. Nel codice seguente è possibile osservare che il tipo System.DateTime passa a Method3 dove viene usata la reflection per accedere System.DateTimeai metodi di ,

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

Se si compila il codice precedente, viene visualizzato un avviso:

Avviso di analisi trim IL2070: net6. Program.Method3(Type): 'this' argomento non soddisfa 'DynamicallyAccessedMemberTypes.PublicMethods' nella chiamata a 'System.Type.GetMethods()'. Parametro 'type' del metodo 'net6. Program.Method3(Type)' non ha annotazioni corrispondenti. Il valore di origine deve dichiarare almeno gli stessi requisiti dichiarati nel percorso di destinazione a cui è assegnato.

Per prestazioni e stabilità, l'analisi del flusso non viene eseguita tra i metodi, quindi è necessaria un'annotazione per passare informazioni tra i metodi, dalla chiamata di reflection (GetMethods) all'origine dell'oggetto Type. Nell'esempio precedente l'avviso trimmer indica che richiede che GetMethods l'istanza dell'oggetto Type su cui venga chiamata per avere l'annotazione PublicMethods , ma la type variabile non ha lo stesso requisito. In altre parole, è necessario passare i requisiti dal GetMethods chiamante al chiamante:

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

Dopo l'annotazione del parametro type, l'avviso originale scompare, ma viene visualizzato un altro elemento:

IL2087: l'argomento 'type' non soddisfa 'DynamicallyAccessedMemberTypes.PublicMethods' nella chiamata a 'C.Method3(Type)'. Il parametro generico 'T' di 'C.Method2<T>()' non ha annotazioni corrispondenti.

Sono state propagate annotazioni fino al parametro type di Method3, in Method2 si è verificato un problema simile. Il trimmer è in grado di tenere traccia del valore T mentre scorre la chiamata a typeof, viene assegnato alla variabile tlocale e passato a Method3. A questo punto si nota che il parametro type richiede PublicMethods ma non sono previsti requisiti per Te genera un nuovo avviso. Per risolvere questo problema, è necessario "annotare e propagare" applicando annotazioni fino all'alto della catena di chiamate fino a raggiungere un tipo noto staticamente (ad esempio System.DateTime o System.Tuple) o un altro valore con annotazioni. In questo caso, è necessario annotare il parametro T di tipo di Method2.

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

A questo punto non sono presenti avvisi perché il trimmer sa esattamente quali membri possono essere accessibili tramite reflection di runtime (metodi pubblici) e su quali tipi (System.DateTime) e li manterrà. In generale, questo è il modo migliore per gestire DynamicallyAccessedMembers gli avvisi: aggiungere annotazioni in modo che il trimmer sappia cosa conservare.

Come per RequiresUnreferencedCode gli avvisi, l'aggiunta RequiresUnreferencedCode di attributi o UnconditionalSuppressMessage elimina anche gli avvisi, ma non rende il codice compatibile con il taglio, mentre l'aggiunta DynamicallyAccessedMembers lo rende compatibile.