Pochopení analýzy oříznutí

Tento článek vysvětluje základní koncepty za analýzou trimování, které vám pomohou pochopit, proč určité vzory kódu generují varování, a jak zajistit, aby váš kód byl kompatibilní s procesem trimování. Porozumění těmto konceptům vám pomůže učinit informovaná rozhodnutí při řešení upozornění na zmenšení, nikoli jen „šířením atributů k potlačení varování nástroje“.

Jak ořezávač analyzuje kód

Zatřihovací modul provádí statickou analýzu v době publikování a zjišťuje, který kód používá vaše aplikace. Začíná ze známých vstupních bodů (jako je vaše Main metoda) a řídí se cestami kódu v aplikaci.

Co může zatřižovač pochopit

Ořezávač vyniká při analýze přímých vzorů kódu viditelných při kompilaci:

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

V těchto příkladech může trimmer následovat cestu kódu a označit DateTime.AddDays, List<string>.Add a MyUtility.Process jako použitý kód, který by měl být zachován v konečné aplikaci.

Co trimmer nedokáže pochopit

Zastřihovač má problémy s dynamickými operacemi, kde cíl operace není známý, dokud není spuštěný 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?

V těchto příkladech řezačka nemá žádný způsob, jak to vědět.

  • Který typ uživatel zadá
  • Jaký typ GetSomeObject() vrátí
  • Jaký kód existuje v dynamicky načteném sestavení

Toto je základní problém, který řeší upozornění.

Problém reflexe

Reflexe kódu umožňuje zkoumat a volat typy a členy dynamicky během běhu. To je výkonné, ale vytváří výzvu pro statickou analýzu.

Proč reflexe přeruší oříznutí

Podívejte se na tento příklad:

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

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

Z pohledu trimmeru:

  • Je vidět, že type.GetMethods() je zavolán.
  • Neví, co type bude (jedná se o parametr).
  • Nemůže určit, které metody typů je potřeba zachovat.
  • Bez pokynů může dojít k odebrání metod z DateTime, což přeruší kód.

V důsledku toho trimmer vygeneruje upozornění na tento kód.

Porozumění DynamicallyAccessedMembers

DynamicallyAccessedMembersAttribute řeší problém reflexe vytvořením explicitního kontraktu mezi volajícím a volanou metodou.

Základní účel

DynamicallyAccessedMembers řekne trimmeru: "Tento parametr (nebo pole nebo návratová hodnota) bude obsahovat Type, který potřebuje mít zachované konkrétní členy, protože k nim bude přistupováno pomocí reflexe."

Konkrétní příklad

Pojďme opravit předchozí příklad:

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

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

Teď zastřihovač rozumí:

  1. PrintMethodNames vyžaduje, aby jeho parametr PublicMethods byl zachován.
  2. Web volání předá typeof(DateTime).
  3. DateTimeProto musí být zachovány veřejné metody.

Atribut vytvoří požadavek , který přetéká zpět od použití reflexe ke zdroji Type hodnoty.

Jedná se o kontrakt, nikoli nápovědu.

Je to důležité pro pochopení: DynamicallyAccessedMembers není to jenom dokumentace. Trimmer vynucuje tento kontrakt.

Analogie s omezeními obecného typu

Pokud znáte omezení obecného typu, DynamicallyAccessedMembers funguje podobně. Stejně jako obecná omezení procházejí kódem:

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 vytvoří podobné požadavky, které procházejí vaším kódem:

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
}

Oba vytvářejí kontrakty, které musí být splněny, a obě generují chyby nebo upozornění, když kontrakt nemůže být splněn.

Jak se smlouva vynucuje

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

Pokud smlouvu nemůžete splnit (například v druhém příkladu), zobrazí se upozornění.

Porozumění RequiresUnreferencedCode

Některé vzory kódu nelze jednoduše analyzovat staticky. V těchto případech použijte RequiresUnreferencedCodeAttribute.

Kdy použít RequiresUnreferencedCode

Atribut použijte v následujících RequiresUnreferencedCodeAttribute případech:

  • Vzor reflexe je zásadně dynamický: Načítání sestavení nebo typů podle názvů řetězců z externích zdrojů.
  • Složitost je příliš vysoká na anotace: Kód, který používá reflexi složitými způsoby řízenými daty.
  • Používáte generování kódu prostředí runtime: Technologie jako System.Reflection.Emit nebo dynamic klíčové slovo.

Příklad:

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

Účel atributu

RequiresUnreferencedCode slouží ke dvěma účelům:

  1. Potlačí upozornění uvnitř metody: Optimalizátor nebude analyzovat ani upozorňovat na použití reflexe.
  2. Vytvoří upozornění na webech volání: Jakýkoli kód volající tuto metodu dostane upozornění.

Toto upozornění zvýrazní vývojářům viditelnost do nekompatibilních cest kódu.

Psaní dobrých zpráv

Zpráva by měla vývojářům pomoct pochopit jejich možnosti:

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

Jak požadavky protékají kódem

Pochopení způsobu šíření požadavků vám pomůže zjistit, kde přidat atributy.

Tok požadavků zpět

Požadavky se šíří z místa, kde se používá odraz, zpět tam, odkud pochází 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
}

Aby bylo toto kompatibilní s oříznutím, musíte řetěz označit:

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

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

Nyní tok požadavků: GetMethods() vyžaduje PublicMethodstype parametr vyžaduje PublicMethods → obecný T vyžaduje PublicMethodsDateTime vyžaduje zachování PublicMethods.

Požadavky procházející úložištěm

Požadavky také protékají poli a vlastnostmi:

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

Volba správného přístupu

Když narazíte na kód, který potřebuje reflexi, postupujte podle tohoto rozhodovacího stromu:

1. Můžete se vyhnout reflexi?

Nejlepším řešením je vyhnout se reflexi, pokud je to možné:

// ❌ 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. Je typ známý v době kompilace?

Pokud je reflexe nezbytná, ale typy jsou známé, použijte DynamicallyAccessedMembers:

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

3. Je model v podstatě dynamický?

Pokud typy skutečně nejsou známé až do spuštění, použijte 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
}

Běžné vzory a řešení

Model: Metody továrny

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

Vzor: Pluginové systémy

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

Klíčové poznatky

  • Zatřižovač používá statickou analýzu – dokáže pochopit pouze cesty kódu viditelné v době kompilace.
  • Reflexe přeruší statickou analýzu – zatřihovací modul nevidí, k čemu bude reflexe přistupovat za běhu.
  • DynamickyAccessedMembers vytváří kontrakty – říká trimmeru, co musí být zachováno.
  • Požadavky proudí zpět – od využití reflexe zpět ke zdroji Type hodnoty.
  • RequiresUnreferencedCode dokumentuje nekompatibilitu – použijte jej, když kód nelze analyzovat.
  • Atributy nejsou jen nápovědy – trimovač vynucuje kontrakty a generuje varování, když nelze tyto kontrakty splnit.

Další kroky