Introduzione agli avvisi di taglio
Concettualmente, il taglio è semplice: quando si pubblica un'applicazione, l’SDK .NET 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, l’SDK .NET fornisce un'analisi statica della compatibilità di taglio tramite avvisi di taglio. Il trimmer genera avvisi di taglio quando trova codice che potrebbe non essere compatibile con il taglio. Il codice non compatibile con il taglio può produrre modifiche funzionali, o addirittura arresti anomali, in un'applicazione dopo che è stato tagliato. Idealmente, tutte le applicazioni che usano il taglio non dovrebbero generare avvisi di taglio. Se sono presenti avvisi di taglio, l'app deve essere testata accuratamente dopo il taglio per assicurarsi che non siano presenti modifiche funzionali.
Questo articolo ti aiuta a comprendere perché alcuni modelli generano avvisi di taglio e come è possibile risolvere questi avvisi.
Esempi di avvisi di taglio
Per la maggior parte del codice C#, è semplice determinare il codice usato e il codice inutilizzato, ovvero il trimmer può analizzare le chiamate a un metodo, i riferimenti a campi e proprietà e così via, e determinare il codice a cui si accede. Sfortunatamente, 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, GetType() richiede dinamicamente un tipo con un nome sconosciuto, quindi stampa i nomi di tutti i relativi metodi. Poiché non è possibile sapere in fase di pubblicazione quale nome di tipo verrà usato, non è possibile che il trimmer sappia quale tipo conservare nell'output. Probabilmente questo codice funzionava prima del taglio (purché l'input fosse qualcosa di cui si conosceva l’esistenza nel framework di destinazione), ma con ogni probabilità produrrebbe un'eccezione di riferimento Null dopo il taglio, poiché Type.GetType
restituisce Null quando il tipo non viene trovato.
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 hanno lo scopo di rendere prevedibile il taglio. Esistono due grandi categorie di avvisi che probabilmente verranno visualizzati:
- La funzionalità non è compatibile con il taglio
- La funzionalità prevede determinati requisiti dell'input per essere compatibile con il taglio
Funzionalità incompatibili con il taglio
Questi sono in genere metodi che non funzionano affatto o che potrebbero essere interrotti in alcuni casi, se vengono usati in un'applicazione tagliata. Un buon esempio è il metodo Type.GetType
dell'esempio precedente. In un'app tagliata potrebbe funzionare, ma non esiste alcuna garanzia. Tali API sono contrassegnate con RequiresUnreferencedCodeAttribute.
RequiresUnreferencedCodeAttribute è semplice e ampio: si tratta di un attributo che indica che il membro è stato annotato come incompatibile con il taglio. Questo attributo viene usato quando il codice è fondamentalmente non compatibile o quando la dipendenza dal taglio è troppo complessa da essere spiegata al trimmer. Questo vale spesso per i metodi che caricano dinamicamente il codice tramite LoadFrom(String), che enumerano o cercano tutti i tipi in un'applicazione o un assembly, ad esempio tramite GetType(), che usano la parola chiave dynamic
di C# o altre tecnologie di generazione del codice di runtime. Un esempio sarebbe:
[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();
}
Non esistono molte soluzioni alternative per RequiresUnreferencedCode
. La soluzione migliore consiste nell'evitare di chiamare il metodo quando si taglia e si usa qualcos'altro che è compatibile con il taglio.
Contrassegnare la funzionalità come incompatibile con il taglio
Se stai scrivendo una libreria e non sai se usare o meno funzionalità incompatibili, puoi contrassegnarle con RequiresUnreferencedCode
. Questo annota il metodo come incompatibile con il taglio. L'uso di RequiresUnreferencedCode
disattiva tutti gli avvisi di taglio nel metodo specificato, ma genera un avviso ogni volta che qualcun altro lo chiama.
Il RequiresUnreferencedCodeAttribute richiede di specificare un Message
. Il messaggio viene mostrato come parte di un avviso segnalato allo sviluppatore che chiama il metodo contrassegnato. Ad esempio:
IL2026: Using member <incompatible method> which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. <The message value>
Nell'esempio precedente, un avviso per un metodo specifico potrebbe essere simile al seguente:
IL2026: Using member 'MethodWithAssemblyLoad()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead.
Gli sviluppatori che chiamano tali API in genere non sono interessati ai particolari dell'API interessata o alle specifiche in relazione al taglio.
Un buon messaggio deve indicare quali funzionalità non sono compatibili con il taglio e quindi indicare allo sviluppatore quali sono i potenziali passaggi successivi. Potrebbe suggerire di usare una funzionalità diversa o modificare il modo in cui viene usata la funzionalità. Potrebbe anche semplicemente dichiarare che la funzionalità non è ancora compatibile con il taglio, senza proporre una sostituzione chiara.
Se le indicazioni per lo sviluppatore diventano troppo lunghe da essere incluse in un messaggio di avviso, puoi aggiungere un Url
facoltativo a RequiresUnreferencedCodeAttribute per indirizzare lo sviluppatore a una pagina Web che descrive il problema e le possibili soluzioni in modo più dettagliato.
Ad esempio:
[RequiresUnreferencedCode("This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead", Url = "https://site/trimming-and-method")]
void MethodWithAssemblyLoad() { ... }
Questo genera un avviso:
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
L'uso di RequiresUnreferencedCode
spesso porta a contrassegnare più metodi relativi, a causa della stessa ragione. Questo è comune quando un metodo di alto livello diventa incompatibile con il taglio perché chiama un metodo di basso livello che non è compatibile con il taglio. L'avviso viene inviato a un'API pubblica. Ogni utilizzo di RequiresUnreferencedCode
richiede un messaggio e in questi casi i messaggi sono probabilmente uguali. Per evitare la duplicazione di stringhe e semplificare la gestione, usa un campo stringa costante per archiviare il messaggio:
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();
}
}
Funzionalità con requisiti per l'input
Il trimming fornisce API per specificare più requisiti sull'input ai metodi e agli altri membri che portano a un codice compatibile con il taglio. Questi requisiti in genere riguardano la reflection e la possibilità di accedere a determinati membri o operazioni su un tipo. Tali requisiti vengono specificati usando DynamicallyAccessedMembersAttribute.
A differenza di RequiresUnreferencedCode
, la reflection a volte può 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
o 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 può usare l'analisi del flusso per determinare che deve mantenere tutti i metodi pubblici in System.DateTime
. Allora, dove entra in gioco DynamicallyAccessMembers
? Quando la reflection viene suddivisa su più metodi. Nel codice seguente vediamo che il tipo System.DateTime
passa a Method3
, dove viene usata la reflection per accedere ai metodi di System.DateTime
.
void Method1()
{
Method2<System.DateTime>();
}
void Method2<T>()
{
Type t = typeof(T);
Method3(t);
}
void Method3(Type type)
{
var methods = type.GetMethods();
...
}
Se compili il codice precedente, viene generato l'avviso seguente:
IL2070: Program.Method3(Type): l’argomento “this” non soddisfa “DynamicallyAccessedMemberTypes.PublicMethods” nella chiamata a “System.Type.GetMethods()”. Il parametro “type'”del metodo “Program.Method3(Type)” non dispone di annotazioni corrispondenti. Il valore di origine deve dichiarare almeno gli stessi requisiti di quelli dichiarati nella posizione di destinazione a cui è assegnato.
Per le prestazioni e la stabilità, l'analisi del flusso non viene eseguita tra i metodi, pertanto è necessaria un'annotazione per passare informazioni tra i metodi, dalla chiamata di reflection (GetMethods
) all'origine del Type
. Nell'esempio precedente, l'avviso di taglio indica che GetMethods
richiede che l'istanza dell'oggetto Type
su cui viene chiamata per avere l'annotazione PublicMethods
, ma la variabile type
non ha lo stesso requisito. In altre parole, è necessario passare i requisiti da GetMethods
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 aver annotato il parametro type
, l'avviso originale scompare, ma ne appare un altro:
IL2087: l'argomento “type” non soddisfa “DynamicallyAccessedMemberTypes.PublicMethods” nella chiamata a “Program.Method3(Type)”. Il parametro generico “T” di “Program.Method2<T>()” non include annotazioni corrispondenti.
Sono state propagate annotazioni fino al parametro type
di Method3
, in Method2
è presente un problema simile. Il trimmer è in grado di tenere traccia del valore T
mentre passa attraverso la chiamata a typeof
, viene assegnato alla variabile locale t
e passato a Method3
. A questo punto viene rilevato che il parametro type
richiede PublicMethods
ma non sono previsti requisiti per T
perciò genera un nuovo avviso. Per risolvere questo problema, è necessario "annotare e propagare" applicando annotazioni in tutta la catena di chiamate fino a raggiungere un tipo noto staticamente (ad esempio System.DateTime
o System.Tuple
) o un altro valore annotato. In questo caso, è necessario annotare il parametro di tipo T
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();
...
}
Ora non sono presenti avvisi perché il trimmer sa quali membri possono essere accessibili tramite reflection di runtime (metodi pubblici) e su quali tipi (System.DateTime
) e li mantiene. È consigliabile aggiungere annotazioni in modo che il trimmer sappia cosa conservare.
Gli avvisi generati da questi requisiti aggiuntivi vengono eliminati automaticamente se il codice interessato si trova in un metodo con RequiresUnreferencedCode
.
A differenza di RequiresUnreferencedCode
, che segnala semplicemente l'incompatibilità, l'aggiunta di DynamicallyAccessedMembers
rende il codice compatibile con il taglio.
Nota
Se si usa DynamicallyAccessedMembersAttribute
, verranno reperiti tutti i membri specificati DynamicallyAccessedMemberTypes
del tipo. Questo significa che i membri verranno mantenuti, oltre ai metadati a cui fanno riferimento i membri. Questo può portare ad app molto più grandi del previsto. Prestare attenzione a usare il minimo DynamicallyAccessedMemberTypes
necessario.
Eliminazione degli avvisi di trimmer
Se in qualche modo è possibile determinare che la chiamata è sicura, e tutto il codice necessario non verrà eliminato, è possibile anche 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()
{
InitializeEverything();
MethodWithAssemblyLoad(); // Warning suppressed
ReportResults();
}
Avviso
Prestare molta attenzione quando si eliminano gli avvisi di taglio. È possibile che la chiamata ora sia compatibile con il taglio, ma con la modifica del codice la situazione potrebbe cambiare e ci si potrebbe dimenticare di esaminare tutte le eliminazioni.
UnconditionalSuppressMessage
è come SuppressMessage
ma può essere visto da publish
e da altri strumenti di post-compilazione.
Importante
Non usare SuppressMessage
o #pragma warning disable
per eliminare gli avvisi di trimmer. Questi funzionano solo per il compilatore, ma non vengono mantenuti nell'assembly compilato. Il trimmer opera su assembly compilati e non visualizza l'eliminazione.
L'eliminazione si applica all'intero corpo del metodo. Nell'esempio precedente, elimina quindi tutti gli avvisi IL2026
dal metodo. Ciò rende più difficile la comprensione, perché non è chiaro quale metodo è quello problematico, a meno che non si aggiunga un commento. Più importante, se il codice cambia in futuro, ad esempio se anche ReportResults
diventa anche incompatibile con il taglio, non viene segnalato alcun avviso per questa chiamata al metodo.
È possibile risolvere questo problema eseguendo il refactoring della chiamata al metodo problematico in un metodo separato o in una funzione locale e applicando l'eliminazione solo a tale metodo:
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
}
}