Compartir por


Comprensión del análisis de recorte

En este artículo se explican los conceptos básicos detrás del análisis de recorte para ayudarle a comprender por qué determinados patrones de código generan advertencias y cómo hacer que el código sea compatible con el recorte. Comprender estos conceptos le ayudará a tomar decisiones fundamentadas al abordar advertencias de recorte en lugar de simplemente "distribuir atributos alrededor para silenciar las herramientas".

Cómo analiza el optimizador el código

El recortador realiza análisis estático en tiempo de publicado para determinar qué código usa tu aplicación. Se inicia desde puntos de entrada conocidos (como el método Main) y sigue las rutas de código a través de la aplicación.

Lo que la cortadora puede entender

El optimizador se destaca al analizar patrones de código directos y visibles en tiempo de compilación:

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

En estos ejemplos, el recortador puede seguir la ruta de código y marcar DateTime.AddDays, List<string>.Add y MyUtility.Process como código utilizado que debe mantenerse en la aplicación final.

Lo que el recortador no puede entender

El recortador tiene dificultades con las operaciones dinámicas en las que el destino de una operación no se conoce hasta el tiempo de ejecución.

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

En estos ejemplos, el recortador no tiene forma de saber:

  • El tipo que introducirá el usuario
  • ¿Qué tipo devuelve GetSomeObject()?
  • ¿Qué código existe en el ensamblado cargado dinámicamente?

Este es el problema fundamental que abordan las advertencias.

Problema de reflexión

La reflexión permite que el código inspeccione e invoque tipos y miembros dinámicamente en tiempo de ejecución. Esto es eficaz, pero crea un desafío para el análisis estático.

¿Por qué la reflexión interrumpe la poda?

Considere este ejemplo:

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

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

Desde la perspectiva del recortador:

  • Se llama type.GetMethods().
  • No sabe qué type será (es un parámetro).
  • No puede determinar qué métodos de tipos deben conservarse.
  • Sin instrucciones, podría quitar métodos de DateTime, rompiendo el código.

Por lo tanto, el recortador genera una advertencia en este código.

Descripción de DynamicallyAccessedMembers

DynamicallyAccessedMembersAttribute resuelve el problema de reflexión mediante la creación de un contrato explícito entre el autor de la llamada y el método llamado.

El propósito fundamental

DynamicallyAccessedMembers indica al recortador: "Este parámetro (o campo o valor devuelto) contendrá un Type que necesita miembros específicos que deben conservarse porque la reflexión se usará para acceder a ellos".

Ejemplo concreto

Vamos a corregir el ejemplo 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));

Ahora el recortador entiende lo siguiente:

  1. PrintMethodNames requiere que se mantenga el parámetro PublicMethods.
  2. El punto de llamada pasa typeof(DateTime).
  3. Por lo tanto, se deben mantener los métodos públicos de DateTime.

El atributo crea un requisito que fluye hacia atrás desde el uso de la reflexión hasta el origen del Type valor.

Es un contrato, no una sugerencia

Esto es fundamental para comprender: DynamicallyAccessedMembers no es solo documentación. El recortador hace cumplir este contrato.

Analogía con restricciones de tipo genérico

Si está familiarizado con las restricciones de tipo genérico, DynamicallyAccessedMembers funciona de forma similar. Al igual que las restricciones genéricas fluyen a través del 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 crea requisitos similares que fluyen a través del 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 crean contratos que deben cumplirse y generan errores o advertencias cuando no se puede satisfacer el contrato.

Cómo se aplica el contrato

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

Si no puede cumplir el contrato (como en el segundo ejemplo), recibirá una advertencia.

Descripción de RequireUnreferencedCode

Algunos patrones de código simplemente no se pueden analizar estáticamente. En estos casos, use RequiresUnreferencedCodeAttribute.

Cuándo usar RequiresUnreferencedCode

Use el RequiresUnreferencedCodeAttribute atributo cuando:

  • El patrón de reflexión es fundamentalmente dinámico: cargar ensamblados o tipos mediante nombres de cadena desde orígenes externos.
  • La complejidad es demasiado alta para anotar: código que usa la reflexión en formas complejas controladas por datos.
  • Está usando la generación de código en tiempo de ejecución: tecnologías como System.Reflection.Emit o la dynamic palabra clave .

Ejemplo:

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

Propósito del atributo

RequiresUnreferencedCode tiene dos propósitos:

  1. Suprime los avisos dentro del método: el optimizador no analizará ni advertirá sobre el uso de la reflexión.
  2. Crea advertencias en los sitios de llamada: cualquier código que llama a este método obtiene una advertencia.

Esta "elevación" de la advertencia está diseñada para dar a los desarrolladores visibilidad de las rutas de acceso de código incompatibles con recorte.

Escribir mensajes buenos

El mensaje debe ayudar a los desarrolladores a comprender sus opciones:

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

Cómo fluyen los requisitos a través del código

Comprender cómo propagan los requisitos le ayuda a saber dónde agregar atributos.

Los requisitos fluyen hacia atrás

Los requisitos fluyen desde dónde se usa la reflexión hasta dónde Type se origina:

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 que este recorte sea compatible, debe anotar la cadena:

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

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

Ahora, los flujos de requisitos: GetMethods() requiere PublicMethodstype parámetro necesita PublicMethodsT genérico necesita PublicMethodsDateTime necesita PublicMethods conservado.

Los requisitos fluyen a través del almacenamiento

Los requisitos también fluyen a través de campos y propiedades:

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

Elección del enfoque adecuado

Cuando encuentre código que necesite reflexión, siga este árbol de decisión:

1. ¿Puedes evitar la reflexión?

La mejor solución es evitar la reflexión siempre que sea posible:

// ❌ 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. ¿Se conoce el tipo en tiempo de compilación?

Si la reflexión es necesaria, pero se conocen los tipos, use DynamicallyAccessedMembers:

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

3. ¿El patrón es fundamentalmente dinámico?

Si los tipos realmente no se conocen hasta el tiempo de ejecución, 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
}

Patrones y soluciones comunes

Patrón: 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);
}

Patrón: Sistemas de complementos

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

Conclusiones clave

  • El recortador usa el análisis estático - solo puede comprender las rutas de acceso de código visibles en el momento de la compilación.
  • La reflexión rompe el análisis estático: el recortador no puede determinar qué se accederá mediante reflexión en tiempo de ejecución.
  • DynamicallyAccessedMembers crea contratos : indica al recortador lo que se debe conservar.
  • Los requisitos fluyen hacia atrás — desde el uso de la reflexión hasta el origen del Type valor.
  • RequiresUnreferencedCode documenta incompatibilidades - úselo cuando el código no se pueda analizar.
  • Los atributos no son solo sugerencias : el recortador aplica contratos y genera advertencias cuando no se pueden cumplir.

Pasos siguientes