Introdução aos avisos de corte
Conceitualmente, o corte é simples: quando você publica um aplicativo, o SDK do .NET analisa todo o aplicativo e remove todo o código não utilizado. No entanto, pode ser difícil determinar o que não é utilizado ou, mais precisamente, o que é utilizado.
Para evitar alterações no comportamento ao cortar aplicativos, o SDK do .NET fornece análise estática da compatibilidade de corte por meio de avisos de corte. O aparador produz avisos de corte quando encontra código que pode não ser compatível com o corte. O código que não é compatível com corte pode produzir alterações comportamentais, ou até mesmo falhas, em um aplicativo depois que ele é cortado. Um aplicativo que usa corte não deve produzir nenhum aviso de corte. Se houver algum aviso de corte, o aplicativo deve ser cuidadosamente testado após o corte para garantir que não haja alterações de comportamento.
Este artigo ajuda você a entender por que alguns padrões produzem avisos de corte e como esses avisos podem ser abordados.
Exemplos de avisos de corte
Para a maioria dos códigos C#, é simples determinar qual código é usado e qual código não é usado — o trimmer pode percorrer chamadas de método, referências de campo e propriedade, e assim por diante, e determinar qual código é acessado. Infelizmente, algumas características, como a reflexão, apresentam um problema significativo. Considere o seguinte código:
string s = Console.ReadLine();
Type type = Type.GetType(s);
foreach (var m in type.GetMethods())
{
Console.WriteLine(m.Name);
}
Neste exemplo, GetType() solicita dinamicamente um tipo com um nome desconhecido e, em seguida, imprime os nomes de todos os seus métodos. Como não há como saber no momento da publicação qual nome de tipo será usado, não há como o aparador saber qual tipo preservar na saída. É provável que esse código possa ter funcionado antes do corte (desde que a entrada seja algo conhecido por existir na estrutura de destino), mas provavelmente produziria uma exceção de referência nula após o corte, pois Type.GetType
retorna null quando o tipo não é encontrado.
Nesse caso, o aparador emite um aviso na chamada para Type.GetType
, indicando que não pode determinar qual tipo será usado pelo aplicativo.
Reagir para aparar avisos
Os avisos de corte destinam-se a trazer previsibilidade ao corte. Há duas grandes categorias de avisos que você provavelmente verá:
- A funcionalidade não é compatível com o corte
- A funcionalidade tem certos requisitos na entrada para ser compatível com o trim
Funcionalidade incompatível com corte
Normalmente, esses são métodos que não funcionam ou podem ser quebrados em alguns casos se forem usados em um aplicativo cortado. Um bom exemplo é o Type.GetType
método do exemplo anterior. Em um aplicativo cortado, pode funcionar, mas não há garantia. Essas APIs são marcadas com RequiresUnreferencedCodeAttribute.
RequiresUnreferencedCodeAttribute é simples e amplo: é um atributo que significa que o membro foi anotado incompatível com o corte. Esse atributo é usado quando o código não é fundamentalmente compatível com trim ou a dependência trim é muito complexa para explicar ao aparador. Isso geralmente seria verdadeiro para métodos que carregam código dinamicamente, por exemplo, via LoadFrom(String), enumeram ou pesquisam todos os tipos em um aplicativo ou assembly, por exemplo, via GetType(), usam a palavra-chave C# dynamic
ou usam outras tecnologias de geração de código de tempo de execução. Um exemplo seria:
[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();
}
Não há muitas soluções alternativas para RequiresUnreferencedCode
o . A melhor solução é evitar chamar o método ao cortar e usar outra coisa que seja compatível com o corte.
Marcar funcionalidade como incompatível com o corte
Se estiver a escrever uma biblioteca e não estiver sob o seu controlo se deve ou não utilizar funcionalidades incompatíveis, pode marcá-la com RequiresUnreferencedCode
. Isso anota seu método como incompatível com o corte. O uso RequiresUnreferencedCode
silencia todos os avisos de corte no método dado, mas produz um aviso sempre que alguém o chama.
O RequiresUnreferencedCodeAttribute requer que você especifique um Message
arquivo . A mensagem é mostrada como parte de um aviso relatado ao desenvolvedor que chama o método marcado. Por exemplo:
IL2026: Using member <incompatible method> which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. <The message value>
Com o exemplo acima, um aviso para um método específico pode ter esta aparência:
IL2026: Using member 'MethodWithAssemblyLoad()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead.
Os desenvolvedores que chamam essas APIs geralmente não estarão interessados nas particularidades da API afetada ou nas especificidades relacionadas ao corte.
Uma boa mensagem deve indicar qual funcionalidade não é compatível com o corte e, em seguida, orientar o desenvolvedor sobre quais são seus possíveis próximos passos. Pode sugerir o uso de uma funcionalidade diferente ou alterar a forma como a funcionalidade é usada. Também pode simplesmente afirmar que a funcionalidade ainda não é compatível com o corte sem uma substituição clara.
Se a orientação para o desenvolvedor se tornar muito longa para ser incluída em uma mensagem de aviso, você pode adicionar um opcional Url
para apontar RequiresUnreferencedCodeAttribute o desenvolvedor para uma página da Web descrevendo o problema e possíveis soluções com mais detalhes.
Por exemplo:
[RequiresUnreferencedCode("This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead", Url = "https://site/trimming-and-method")]
void MethodWithAssemblyLoad() { ... }
Isto produz um aviso:
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
Usar RequiresUnreferencedCode
muitas vezes leva a marcar mais métodos com ele, devido ao mesmo motivo. Isso é comum quando um método de alto nível se torna incompatível com o corte porque chama um método de baixo nível que não é compatível com o corte. Você "borbulha" o aviso para uma API pública. Cada uso de precisa de RequiresUnreferencedCode
uma mensagem e, nesses casos, as mensagens são provavelmente as mesmas. Para evitar a duplicação de cadeias de caracteres e facilitar a manutenção, use um campo de cadeia de caracteres constante para armazenar a mensagem:
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();
}
}
Funcionalidade com requisitos na sua entrada
O Trimming fornece APIs para especificar mais requisitos de entrada para métodos e outros membros que levam a código compatível com trim. Esses requisitos geralmente são sobre reflexão e a capacidade de acessar certos membros ou operações em um tipo. Tais requisitos são especificados utilizando o DynamicallyAccessedMembersAttribute.
Ao contrário RequiresUnreferencedCode
do , a reflexão às vezes pode ser entendida pelo aparador, desde que seja anotada corretamente. Vamos dar outra olhada no exemplo original:
string s = Console.ReadLine();
Type type = Type.GetType(s);
foreach (var m in type.GetMethods())
{
Console.WriteLine(m.Name);
}
No exemplo anterior, o verdadeiro problema é Console.ReadLine()
. Como qualquer tipo pode ser lido, o aparador não tem como saber se você precisa de métodos em System.DateTime
ou System.Guid
qualquer outro tipo. Por outro lado, o seguinte código seria bom:
Type type = typeof(System.DateTime);
foreach (var m in type.GetMethods())
{
Console.WriteLine(m.Name);
}
Aqui o aparador pode ver o tipo exato que está sendo referenciado: System.DateTime
. Agora ele pode usar a análise de fluxo para determinar que precisa manter todos os métodos públicos em System.DateTime
. Então, onde entra DynamicallyAccessMembers
? Quando a reflexão é dividida em vários métodos. No código a seguir, podemos ver que o tipo System.DateTime
flui para Method3
onde a reflexão é usada para acessar System.DateTime
os métodos do ,
void Method1()
{
Method2<System.DateTime>();
}
void Method2<T>()
{
Type t = typeof(T);
Method3(t);
}
void Method3(Type type)
{
var methods = type.GetMethods();
...
}
Se você compilar o código anterior, o seguinte aviso será produzido:
IL2070: Program.Method3(Type): o argumento 'this' não satisfaz 'DynamicallyAccessedMemberTypes.PublicMethods' na chamada para 'System.Type.GetMethods()'. O parâmetro 'type' do método 'Program.Method3(Type)' não tem anotações correspondentes. O valor de origem deve declarar pelo menos os mesmos requisitos que os declarados no local de destino ao qual está atribuído.
Para desempenho e estabilidade, a análise de fluxo não é realizada entre métodos, portanto, uma anotação é necessária para passar informações entre métodos, da chamada de reflexão (GetMethods
) para a fonte do Type
. No exemplo anterior, o aviso do trimmer está dizendo que requer a instância do Type
objeto em que GetMethods
é chamado para ter a PublicMethods
anotação, mas a type
variável não tem o mesmo requisito. Em outras palavras, precisamos passar os requisitos de GetMethods
até o chamador:
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();
...
}
Depois de anotar o parâmetro type
, o aviso original desaparece, mas outro aparece:
IL2087: O argumento 'type' não satisfaz 'DynamicallyAccessedMemberTypes.PublicMethods' na chamada para 'Program.Method3(Type)'. O parâmetro genérico 'T' de 'Program.Method2<T>()' não tem anotações correspondentes.
Nós propagamos anotações até o parâmetro type
de Method3
, em Method2
que temos um problema semelhante. O trimmer é capaz de controlar o valor T
à medida que flui através da chamada para typeof
, é atribuído à variável t
local e passado para Method3
. Nesse ponto, ele vê que o parâmetro type
exige PublicMethods
, mas não há requisitos sobre T
, e produz um novo aviso. Para corrigir isso, devemos "anotar e propagar" aplicando anotações em toda a cadeia de chamadas até chegarmos a um tipo conhecido estaticamente (como System.DateTime
ou System.Tuple
), ou outro valor anotado. Neste caso, precisamos anotar o parâmetro T
type de 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();
...
}
Agora não há avisos porque o trimmer sabe quais membros podem ser acessados via reflexão de tempo de execução (métodos públicos) e em quais tipos (System.DateTime
), e os preserva. É uma boa prática adicionar anotações para que o aparador saiba o que preservar.
Os avisos produzidos por esses requisitos extras são automaticamente suprimidos se o código afetado estiver em um método com RequiresUnreferencedCode
.
Ao contrário RequiresUnreferencedCode
do , que simplesmente relata a incompatibilidade, a adição DynamicallyAccessedMembers
torna o código compatível com o corte.
Nota
Usando DynamicallyAccessedMembersAttribute
irá enraizar todos os membros especificados DynamicallyAccessedMemberTypes
do tipo. Isto significa que manterá os membros, bem como quaisquer metadados referenciados por esses membros. Isso pode levar a aplicativos muito maiores do que o esperado. Tenha cuidado para usar o mínimo DynamicallyAccessedMemberTypes
necessário.
Supressão de avisos de aparador
Se você puder de alguma forma determinar que a chamada é segura e todo o código necessário não será cortado, você também pode suprimir o aviso usando UnconditionalSuppressMessageAttribute. Por exemplo:
[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();
}
Aviso
Tenha muito cuidado ao suprimir avisos de corte. É possível que a chamada seja compatível com corte agora, mas à medida que você altera seu código, isso pode mudar e você pode esquecer de revisar todas as supressões.
UnconditionalSuppressMessage
é como SuppressMessage
, mas pode ser visto por publish
e outras ferramentas pós-construção.
Importante
Não utilize SuppressMessage
nem #pragma warning disable
suprima as advertências do aparador. Estes só funcionam para o compilador, mas não são preservados no assembly compilado. Trimmer opera em montagens compiladas e não veria a supressão.
A supressão aplica-se a todo o corpo do método. Então, em nosso exemplo acima, ele suprime todos os IL2026
avisos do método. Isso torna mais difícil de entender, pois não está claro qual método é o problemático, a menos que você adicione um comentário. Mais importante ainda, se o código mudar no futuro, como se ReportResults
se tornar incompatível com trim também, nenhum aviso será relatado para essa chamada de método.
Você pode resolver isso refatorando a chamada de método problemática em um método separado ou função local e, em seguida, aplicando a supressão apenas a esse método:
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
}
}