Compartilhar via


Noções básicas sobre a análise de corte

Este artigo explica os conceitos fundamentais por trás da análise de corte para ajudá-lo a entender por que determinados padrões de código produzem avisos e como tornar seu código compatível com o corte. Entender esses conceitos ajudará você a tomar decisões informadas ao lidar com avisos de corte, em vez de simplesmente "distribuir atributos para calar a ferramenta".

Como o analisador analisa o código

O analisador executa a análise estática durante a publicação para determinar qual código é usado pelo seu aplicativo. Ele começa a partir de pontos de entrada conhecidos (como seu Main método) e segue os caminhos de código por meio de seu aplicativo.

O que o aparador pode entender

O trimmer se destaca na análise de padrões de código visíveis diretamente em tempo de compilação.

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

Nestes exemplos, o trimmer pode seguir o caminho de código e marcar DateTime.AddDays, List<string>.Add e MyUtility.Process como código usado que deve ser mantido no aplicativo final.

O que o aparador não consegue entender

O aparador luta com operações dinâmicas em que o destino de uma operação não é conhecido até o runtime:

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

Nestes exemplos, o aparador não tem como saber:

  • Qual tipo o usuário inserirá
  • Que tipo GetSomeObject() retorna
  • Qual código existe no assembly carregado dinamicamente

Este é o problema fundamental que os avisos de redução procuram resolver.

O problema de reflexão

A reflexão permite que o código inspecione e invoque tipos e membros dinamicamente em runtime. Isso é poderoso, mas cria um desafio para análise estática.

Por que a reflexão interrompe o corte

Considere este exemplo:

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

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

Do ponto de vista do cortador:

  • Observa-se que type.GetMethods() é invocado.
  • Ele não sabe o que type será (é um parâmetro).
  • Ele não pode determinar quais métodos dos tipos precisam ser preservados.
  • Sem orientação, ele pode remover métodos de DateTime, quebrando o código.

Consequentemente, a ferramenta de ajuste gera um aviso sobre esse código.

Noções básicas sobre DynamicallyAccessedMembers

DynamicallyAccessedMembersAttribute resolve o problema de reflexão criando um contrato explícito entre o chamador e o método chamado.

A finalidade fundamental

DynamicallyAccessedMembers informa ao podador: "Esse parâmetro (ou campo ou valor de retorno) manterá um Type que precisa ter membros específicos preservados, pois a reflexão será utilizada para acessá-los."

Um exemplo concreto

Vamos corrigir o exemplo anterior:

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

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

Agora o aparador compreende:

  1. PrintMethodNames requer que seu parâmetro tenha PublicMethods sido preservado.
  2. O site de chamada passa typeof(DateTime).
  3. Portanto, DateTimeos métodos públicos devem ser mantidos.

O atributo cria um requisito que se propaga retroativamente do uso da reflexão para a origem do valor Type.

É um contrato, não uma dica.

Isso é crucial para entender: DynamicallyAccessedMembers não é apenas documentação. O aparador garante a execução deste contrato.

Analogia com restrições de tipo genérico

Se você estiver familiarizado com restrições de tipo genérico, DynamicallyAccessedMembers funcionará da mesma forma. Assim como as restrições genéricas se propagam pelo seu código:

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 cria requisitos semelhantes que fluem pelo código:

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
}

Ambos criam contratos que devem ser atendidos e produzem erros ou avisos quando o contrato não pode ser atendido.

Como o contrato é imposto

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

Se você não conseguir cumprir o contrato (como no segundo exemplo), receberá um aviso.

Noções básicas sobre RequiresUnreferencedCode

Alguns padrões de código simplesmente não podem ser tornados estaticamente analisáveis. Para esses casos, use RequiresUnreferencedCodeAttribute.

"Quando usar 'RequiresUnreferencedCode'"

Use o RequiresUnreferencedCodeAttribute atributo quando:

  • O padrão de reflexão é fundamentalmente dinâmico: carregar assemblies ou tipos por nomes de strings de fontes externas.
  • A complexidade é muito alta para anotar: código que usa reflexão de maneiras complexas e controladas por dados.
  • Você está usando geração de código em tempo de execução: tecnologias como System.Reflection.Emit ou a palavra-chave dynamic.

Exemplo:

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

A finalidade do atributo

RequiresUnreferencedCode serve a duas finalidades:

  1. Suprime avisos dentro do método: a ferramenta de análise de código não verificará nem alertará sobre o uso da reflexão.
  2. Cria avisos em sites de chamada: qualquer código que chame esse método recebe um aviso.

Isso "aumenta" o aviso para dar aos desenvolvedores visibilidade em caminhos de código incompatíveis com corte.

Escrevendo boas mensagens

A mensagem deve ajudar os desenvolvedores a entender suas opções:

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

Como os requisitos fluem por meio do código

Entender como os requisitos são propagados ajuda você a saber onde adicionar atributos.

O fluxo de requisitos retorna ao ponto de origem

Os requisitos fluem do ponto onde a reflexão é utilizada até a origem do 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
}

Para tornar esse corte compatível, você precisa anotar a cadeia:

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

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

Agora, o requisito flui: GetMethods() requer PublicMethodstype necessita do parâmetro PublicMethods → necessário genérico TPublicMethodsDateTime necessita que PublicMethods seja preservado.

Os requisitos fluem pelo armazenamento

Os requisitos também fluem por campos e propriedades:

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

Escolhendo a abordagem certa

Ao encontrar um código que precise de reflexão, siga esta árvore de decisão:

1. Você pode evitar reflexão?

A melhor solução é evitar a reflexão quando possível:

// ❌ 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. O tipo é conhecido no momento da compilação?

Se a reflexão for necessária, mas os tipos forem conhecidos, use DynamicallyAccessedMembers:

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

3. O padrão é fundamentalmente dinâmico?

Se os tipos de fato não forem conhecidos até o tempo de execução, use 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
}

Padrões e soluções comuns

Padrão: Métodos de fábrica

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

Padrão: sistemas de plugin

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

Principais conclusões

  • O aparador usa análise estática – ele só consegue entender os caminhos de código visíveis durante a compilação.
  • A reflexão quebra a análise estática – o otimizador não pode prever o que a reflexão acessará durante o tempo de execução.
  • DynamicallyAccessedMembers cria contratos - ele informa ao cortador o que precisa ser preservado.
  • Os requisitos fluem para trás – do uso de reflexão de volta à fonte do Type valor.
  • RequiresUnreferencedCode documenta incompatibilidade – use este atributo quando o código não puder ser tornado analisável.
  • Atributos não são apenas dicas - o cortador impõe contratos e produz avisos quando eles não podem ser atendidos.

Próximas etapas