Úvod k upozorněním oříznutí

Koncepčně je oříznutí jednoduché: když publikujete aplikaci, sada .NET SDK analyzuje celou aplikaci a odebere veškerý nepoužívaný kód. Může ale být obtížné určit, co se nepoužívá, nebo přesněji řečeno, co se používá.

Aby se zabránilo změnám chování při oříznutí aplikací, sada .NET SDK poskytuje statickou analýzu kompatibility oříznutí prostřednictvím upozornění oříznutí. Při nalezení kódu, který nemusí být kompatibilní s ořezáváním, vystřihuje oříznutí. Kód, který není kompatibilní s oříznutím, může v aplikaci po oříznutí způsobit změny chování nebo dokonce chybové ukončení. V ideálním případě by všechny aplikace, které používají oříznutí, neměly vygenerovat žádná upozornění na oříznutí. Pokud existují nějaká upozornění na oříznutí, měla by být aplikace po oříznutí důkladně otestována, aby se zajistilo, že nedošlo ke změnám chování.

Tento článek vám pomůže pochopit, proč některé vzory vytvářejí upozornění pro oříznutí a jak se tato upozornění dají řešit.

Příklady upozornění oříznutí

U většiny kódu jazyka C# je jednoduché určit, jaký kód se používá a jaký kód se nepoužívá – zastřihovací metodou může procházet volání metod, odkazy na pole a vlastnosti atd. a určit, k jakému kódu se přistupuje. Některé funkce, jako je reflexe, bohužel představují významný problém. Uvažujte následující kód:

string s = Console.ReadLine();
Type type = Type.GetType(s);
foreach (var m in type.GetMethods())
{
    Console.WriteLine(m.Name);
}

V tomto příkladu GetType() dynamicky požaduje typ s neznámým názvem a potom vytiskne názvy všech jeho metod. Vzhledem k tomu, že neexistuje způsob, jak zjistit v době publikování, jaký název typu se použije, neexistuje způsob, jak zjistit, jaký typ se má ve výstupu zachovat. Je pravděpodobné, že tento kód by mohl před oříznutím fungovat (pokud vstup existuje v cílovém rozhraní), ale pravděpodobně by po oříznutí vytvořil výjimku nulového odkazu, protože Type.GetType pokud se typ nenajde, vrátí hodnotu null.

V tomto případě trimmer vydá upozornění na volání Type.GetType, které indikuje, že nemůže určit, který typ bude aplikace používat.

Reakce na upozornění oříznutí

Upozornění oříznutí mají přinést předvídatelnost oříznutí. Pravděpodobně uvidíte dvě velké kategorie upozornění:

  1. Funkce nejsou kompatibilní s oříznutím.
  2. Funkce mají určité požadavky na vstup, aby byly kompatibilní se střihem.

Funkce nekompatibilní s oříznutím

Obvykle se jedná o metody, které nefungují vůbec, nebo se v některých případech můžou porušovat, pokud se používají v oříznuté aplikaci. Dobrým příkladem je Type.GetType metoda z předchozího příkladu. V oříznuté aplikaci to může fungovat, ale neexistuje žádná záruka. Tato rozhraní API jsou označena značkou RequiresUnreferencedCodeAttribute.

RequiresUnreferencedCodeAttribute je jednoduchý a široký: je to atribut, který znamená, že člen byl opatřen poznámkami nekompatibilní s oříznutím. Tento atribut se používá v případě, že kód není v zásadě kompatibilní nebo závislost oříznutí je příliš složitá, aby bylo vysvětleno oříznutí. To by často platilo pro metody, které dynamicky načítají kód, například prostřednictvím LoadFrom(String), výčet nebo vyhledávání ve všech typech v aplikaci nebo sestavení, například pomocí GetType()klíčového slova jazyka C# dynamic nebo použít jiné technologie generování kódu modulu runtime. Příkladem může být:

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

Neexistuje mnoho alternativních řešení pro RequiresUnreferencedCode. Nejlepší opravou je vyhnout se volání metody vůbec při oříznutí a použití něčeho jiného, co je kompatibilní s oříznutím.

Označení funkcí jako nekompatibilní s oříznutím

Pokud píšete knihovnu a není ve vašem ovládacím prvku, jestli se má používat nekompatibilní funkce, můžete ji RequiresUnreferencedCodeoznačit . Tím se vaše metoda označí jako nekompatibilní s oříznutím. Pomocí RequiresUnreferencedCode ticha se všechna upozornění v dané metodě oříznou, ale vygeneruje upozornění pokaždé, když ji někdo jiný zavolá.

Vyžaduje RequiresUnreferencedCodeAttribute , abyste zadali hodnotu Message. Zpráva se zobrazí jako součást upozornění hlášeného vývojáři, který volá označenou metodu. Příklad:

IL2026: Using member <incompatible method> which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. <The message value>

U výše uvedeného příkladu může upozornění pro konkrétní metodu vypadat takto:

IL2026: Using member 'MethodWithAssemblyLoad()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead.

Vývojáři, kteří volají taková rozhraní API, se obvykle nebudou zajímat o konkrétní údaje ovlivněného rozhraní API nebo specifika, protože souvisí s oříznutím.

Dobrá zpráva by měla uvést, jaké funkce nejsou kompatibilní s ořezáváním, a pak provádět vývojáře, co jsou jejich potenciální další kroky. Může navrhnout použití jiné funkce nebo změnit způsob použití této funkce. Může také jednoduše uvést, že funkce ještě není kompatibilní s oříznutím bez jasného nahrazení.

Pokud se pokyny pro vývojáře změní na příliš dlouhou dobu, než se do zprávy s upozorněním zahrnou, můžete přidat volitelný Url RequiresUnreferencedCodeAttribute odkaz na odkaz vývojáře na webovou stránku popisující problém a možná řešení podrobněji.

Příklad:

[RequiresUnreferencedCode("This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead", Url = "https://site/trimming-and-method")]
void MethodWithAssemblyLoad() { ... }

Výsledkem je upozornění:

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

Použití RequiresUnreferencedCode často vede k označení více metod s ním, a to z důvodu stejného důvodu. To je běžné, když se metoda vysoké úrovně stane nekompatibilní s oříznutím, protože volá metodu nízké úrovně, která není kompatibilní s oříznutím. Upozornění na veřejné rozhraní API "bublinová" bublina. Každé použití RequiresUnreferencedCode potřebuje zprávu a v těchto případech jsou zprávy pravděpodobně stejné. Pokud se chcete vyhnout duplikování řetězců a usnadnit údržbu, uložte zprávu pomocí pole s konstantním řetězcem:

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

Funkce s požadavky na vstup

Oříznutí poskytuje rozhraní API pro určení dalších požadavků na vstup do metod a dalších členů, které vedou k oříznutí kompatibilního kódu. Tyto požadavky se obvykle týkají reflexe a možnosti přístupu k určitým členům nebo operacím typu. Tyto požadavky jsou určeny pomocí .DynamicallyAccessedMembersAttribute

Na rozdíl od RequiresUnreferencedCodeodrazu může být někdy srozumitelný pomocí zatřižovače, pokud je správně opatřen poznámkami. Podívejme se na původní příklad:

string s = Console.ReadLine();
Type type = Type.GetType(s);
foreach (var m in type.GetMethods())
{
    Console.WriteLine(m.Name);
}

V předchozím příkladu je Console.ReadLine()skutečný problém . Vzhledem k tomu , že je možné číst jakýkoli typ, není možné zjistit, jestli potřebujete metody nebo System.DateTime System.Guid jiný typ. Na druhou stranu by následující kód byl v pořádku:

Type type = typeof(System.DateTime);
foreach (var m in type.GetMethods())
{
    Console.WriteLine(m.Name);
}

Zde může zatřižovač zobrazit přesný odkaz na typ: System.DateTime. Teď může použít analýzu toku k určení, že musí udržovat všechny veřejné metody zapnuté System.DateTime. Tak kam DynamicallyAccessMembers přijde? Když je reflexe rozdělena mezi více metod. V následujícím kódu vidíme, že typ toky do místa System.DateTime , kde Method3 se reflexe používá pro přístup k System.DateTimemetodám,

void Method1()
{
    Method2<System.DateTime>();
}
void Method2<T>()
{
    Type t = typeof(T);
    Method3(t);
}
void Method3(Type type)
{
    var methods = type.GetMethods();
    ...
}

Pokud zkompilujete předchozí kód, vytvoří se následující upozornění:

IL2070: Program.Method3(Type): 'this' argument nevyhovuje "DynamickyAccessedMemberTypes.PublicMethods" ve volání 'System.Type.GetMethods()'. Parametr type metody Program.Method3(Type) nemá odpovídající poznámky. Zdrojová hodnota musí deklarovat alespoň stejné požadavky jako požadavky deklarované v cílovém umístění, ke kterému je přiřazena.

Pro zajištění výkonu a stability se analýza toku neprovádí mezi metodami, takže je potřeba anotace předávat informace mezi metodami od volání reflexe (GetMethods) ke zdroji Type. V předchozím příkladu upozornění na zatřižovač říká, že GetMethods vyžaduje Type , aby instance objektu, ke které je volána, měla poznámku PublicMethods , ale type proměnná nemá stejný požadavek. Jinými slovy, musíme splnit požadavky až GetMethods do volajícího:

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

Po anotaci parametru typepůvodní upozornění zmizí, ale zobrazí se další:

IL2087: Argument type nevyhovuje dynamickyAccessedMemberTypes.PublicMethods ve volání "Program.Method3(Type)". Obecný parametr T metody Program.Method2<T>() nemá odpovídající poznámky.

Rozšířili jsme poznámky až do parametru type Method3, v Method2 máme podobný problém. Zatřižovač dokáže sledovat hodnotu T , když prochází voláním typeof, je přiřazen k místní proměnné ta předán do Method3. V tomto okamžiku vidí, že parametr type vyžaduje PublicMethods , ale neexistují žádné požadavky na Ta vytvoří nové upozornění. Abychom to vyřešili, musíme "anotovat a rozšířit" použitím poznámek až po celý řetězec volání, dokud nedosáhneme staticky známého typu (například System.DateTime nebo System.Tuple) nebo jiné anotované hodnoty. V tomto případě potřebujeme anotovat parametr T Method2typu .

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

Teď neexistují žádná upozornění, protože zatřižovač ví, ke kterým členům může přistupovat reflexe modulu runtime (veřejné metody) a ke kterým typům () a které typy (System.DateTime) je zachová. Osvědčeným postupem je přidat poznámky, aby trimmer věděl, co chcete zachovat.

Upozornění vytvořená těmito dodatečnými požadavky jsou automaticky potlačena, pokud je ovlivněný kód v metodě s RequiresUnreferencedCode.

Na rozdíl od RequiresUnreferencedCodetoho, který jednoduše hlásí nekompatibilitu, je přidání DynamicallyAccessedMembers kódu kompatibilní s oříznutím.

Poznámka:

Použití DynamicallyAccessedMembersAttribute bude root všechny zadané DynamicallyAccessedMemberTypes členy typu. To znamená, že členové zůstanou, stejně jako všechna metadata, na která odkazují tito členové. To může vést k mnohem větším aplikacím, než se čekalo. Dávejte pozor, abyste použili minimální DynamicallyAccessedMemberTypes požadovanou hodnotu.

Potlačení upozornění pro zatřihování

Pokud můžete nějakým způsobem zjistit, že volání je bezpečné a veškerý potřebný kód nebude oříznut, můžete také potlačit upozornění pomocí UnconditionalSuppressMessageAttribute. Příklad:

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

Upozorňující

Při potlačení upozornění oříznutí buďte velmi opatrní. Je možné, že volání je teď kompatibilní s oříznutím, ale když změníte kód, který se může změnit, a možná zapomenete zkontrolovat všechny potlačení.

UnconditionalSuppressMessage je to podobné SuppressMessage , ale můžete ho vidět pomocí publish dalších nástrojů po sestavení.

Důležité

Nepoužívejte SuppressMessage ani #pragma warning disable nepotlačujte upozornění pro zatřižování. Tyto funkce fungují pouze pro kompilátor, ale v kompilovaném sestavení se nezachovají. Trimmer pracuje s kompilovanými sestaveními a nezobrazuje potlačení.

Potlačení se vztahuje na celé tělo metody. V našem příkladu výše tedy potlačí všechna IL2026 upozornění z metody. To znesnadňuje pochopení, protože není jasné, která metoda je problematická, pokud nepřidáte komentář. Důležitější je, že pokud se kód v budoucnu změní, například pokud ReportResults se stane nekompatibilním s oříznutím, nebude pro volání této metody hlášeno žádné upozornění.

Můžete to vyřešit refaktorováním problematického volání metody do samostatné metody nebo místní funkce a následným použitím potlačení pouze na tuto metodu:

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