Dela via


Introduktion till trimningsvarningar

Konceptuellt är trimning enkelt: när du publicerar ett program analyserar .NET SDK hela programmet och tar bort all oanvänd kod. Det kan dock vara svårt att avgöra vad som används, eller mer exakt, vad som används.

För att förhindra ändringar i beteendet vid trimning av program tillhandahåller .NET SDK statisk analys av trimkompatibilitet via trimvarningar. Trimmern skapar trimvarningar när den hittar kod som kanske inte är kompatibel med trimning. Kod som inte är trimkompatibel kan skapa beteendeändringar, eller till och med krascher, i ett program efter att det har trimmats. Helst bör alla program som använder trimning inte ge några trimvarningar. Om det finns några trimvarningar bör appen testas noggrant efter trimningen för att säkerställa att det inte finns några beteendeändringar.

Den här artikeln hjälper dig att förstå varför vissa mönster ger trimvarningar och hur dessa varningar kan åtgärdas.

Exempel på trimvarningar

För de flesta C#-kod är det enkelt att avgöra vilken kod som används och vilken kod som inte används– trimmern kan gå igenom metodanrop, fält- och egenskapsreferenser och så vidare och avgöra vilken kod som används. Tyvärr utgör vissa funktioner, till exempel reflektion, ett betydande problem. Ta följande kod som exempel:

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

I det här exemplet GetType() begär dynamiskt en typ med ett okänt namn och skriver sedan ut namnen på alla dess metoder. Eftersom det inte finns något sätt att veta vid publiceringstillfället vilket typnamn som ska användas finns det inget sätt för trimmern att veta vilken typ som ska bevaras i utdata. Det är troligt att den här koden kunde ha fungerat innan du trimmade (så länge indata är något som är känt för att finnas i målramverket), men förmodligen skulle generera ett null-referensfel efter trimning, eftersom Type.GetType returnerar null när typen inte hittas.

I det här fallet utfärdar trimmern en varning vid anropet till Type.GetType, vilket indikerar att den inte kan avgöra vilken typ som ska användas av programmet.

Reagera på trimvarningar

Trimvarningar är avsedda att ge förutsägbarhet till trimning. Det finns två stora varningskategorier som du förmodligen kommer att se:

  1. Funktioner är inte kompatibla med trimning
  2. Funktionen har vissa krav på indata för att vara trimkompatibel

Funktioner som inte är kompatibla med trimning

Det här är vanligtvis metoder som antingen inte fungerar alls eller som kan brytas i vissa fall om de används i ett trimmat program. Ett bra exempel är Type.GetType metoden från föregående exempel. I en trimmad app kan det fungera, men det finns ingen garanti. Sådana API:er är markerade med RequiresUnreferencedCodeAttribute.

RequiresUnreferencedCodeAttribute är enkelt och brett: det är ett attribut som innebär att medlemmen har kommenterats som inte är kompatibel med trimning. Det här attributet används när koden i grunden inte är trimkompatibel, eller om trimningsberoendet är för komplext för att förklara för trimmern. Detta gäller ofta för metoder som dynamiskt läser in kod, till exempel via LoadFrom(String), räkna upp eller söka igenom alla typer i ett program eller en sammansättning, till exempel via GetType(), använda nyckelordet C# dynamic eller använda andra tekniker för körningskodgenerering. Ett exempel är:

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

Det finns inte många lösningar för RequiresUnreferencedCode. Den bästa lösningen är att undvika att anropa metoden alls när du trimmar och använder något annat som är trimkompatibelt.

Markera funktioner som inkompatibla med trimning

Om du skriver ett bibliotek och det inte finns i din kontroll om du vill använda inkompatibla funktioner kan du markera det med RequiresUnreferencedCode. Detta kommenterar din metod som inkompatibel med trimning. Om du använder RequiresUnreferencedCode tystar du alla trimvarningar i den angivna metoden, men skapar en varning när någon annan anropar den.

Kräver RequiresUnreferencedCodeAttribute att du anger en Message. Meddelandet visas som en del av en varning som rapporteras till utvecklaren som anropar den markerade metoden. Till exempel:

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

Med exemplet ovan kan en varning för en specifik metod se ut så här:

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

Utvecklare som anropar sådana API:er kommer i allmänhet inte att vara intresserade av detaljerna i det berörda API:et eller detaljerna när det gäller trimning.

Ett bra meddelande bör ange vilka funktioner som inte är kompatibla med trimning och sedan vägleda utvecklaren vad som är deras potentiella nästa steg. Det kan föreslå att du använder en annan funktion eller ändrar hur funktionen används. Det kan också helt enkelt ange att funktionerna ännu inte är kompatibla med trimning utan en tydlig ersättning.

Om vägledningen till utvecklaren blir för lång för att inkluderas i ett varningsmeddelande kan du lägga till ett valfritt Url till RequiresUnreferencedCodeAttribute för att peka utvecklaren på en webbsida som beskriver problemet och möjliga lösningar i detalj.

Till exempel:

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

Detta ger en varning:

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

Att använda RequiresUnreferencedCode leder ofta till att markera fler metoder med det, på grund av samma anledning. Detta är vanligt när en högnivåmetod blir inkompatibel med trimning eftersom den anropar en lågnivåmetod som inte är trimkompatibel. Du "bubblar upp" varningen till ett offentligt API. Varje användning av RequiresUnreferencedCode behöver ett meddelande, och i dessa fall är meddelandena sannolikt desamma. Om du vill undvika att duplicera strängar och göra det enklare att underhålla använder du ett konstant strängfält för att lagra meddelandet:

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

Funktioner med krav på indata

Trimning tillhandahåller API:er för att ange fler krav på indata till metoder och andra medlemmar som leder till trimkompatibel kod. Dessa krav handlar vanligtvis om reflektion och möjligheten att komma åt vissa medlemmar eller åtgärder av en typ. Sådana krav anges med hjälp av DynamicallyAccessedMembersAttribute.

Till skillnad från RequiresUnreferencedCodekan reflektion ibland förstås av trimmern så länge den kommenteras korrekt. Låt oss ta en ny titt på det ursprungliga exemplet:

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

I föregående exempel är Console.ReadLine()det verkliga problemet . Eftersom någon typ kan läsas, har trimmern inget sätt att veta om du behöver metoder på System.DateTime eller System.Guid någon annan typ. Å andra sidan skulle följande kod vara bra:

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

Här kan trimmern se den exakta typen som refereras: System.DateTime. Nu kan den använda flödesanalys för att fastställa att den behöver behålla alla offentliga metoder på System.DateTime. Så var DynamicallyAccessMembers kommer in? När reflektion delas mellan flera metoder. I följande kod kan vi se att typen System.DateTime flödar till Method3 där reflektion används för att komma åt System.DateTimemetoder,

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

Om du kompilerar den tidigare koden skapas följande varning:

IL2070: Program.Method3(Type): Argumentet "this" uppfyller inte "DynamicallyAccessedMemberTypes.PublicMethods" i anropet till "System.Type.GetMethods()". Parametern "type" för metoden "Program.Method3(Type)" har inte matchande anteckningar. Källvärdet måste deklarera minst samma krav som de som deklareras på den målplats som det tilldelas till.

För prestanda och stabilitet utförs inte flödesanalys mellan metoder, så en anteckning krävs för att skicka information mellan metoder, från reflektionsanropet (GetMethods) till källan Typetill . I föregående exempel säger trimmervarningen att det kräver att GetMethods objektinstansen Type som den anropas för har anteckningen PublicMethods , men variabeln type har inte samma krav. Med andra ord måste vi skicka kraven från GetMethods upp till anroparen:

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

När du har kommenterat parametern typeförsvinner den ursprungliga varningen, men en annan visas:

IL2087: argumentet "type" uppfyller inte "DynamicallyAccessedMemberTypes.PublicMethods" i anropet till "Program.Method3(Type)". Den generiska parametern "T" för "Program.Method2<T>()" har inte matchande anteckningar.

Vi har spridit anteckningar upp till parametern typeMethod3, i Method2 vi har ett liknande problem. Trimmern kan spåra värdet T när det flödar genom anropet till typeof, tilldelas till den lokala variabeln toch skickas till Method3. Då ser den att parametern type kräver PublicMethods men det finns inga krav på Toch skapar en ny varning. För att åtgärda detta måste vi "kommentera och sprida" genom att tillämpa anteckningar hela vägen upp i anropskedjan tills vi når en statiskt känd typ (t.ex System.DateTime . eller System.Tuple) eller ett annat kommenterat värde. I det här fallet måste vi kommentera typparametern TMethod2för .

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

Nu finns det inga varningar eftersom trimmern vet vilka medlemmar som kan kommas åt via körningsreflektion (offentliga metoder) och på vilka typer (System.DateTime), och det bevarar dem. Det är bästa praxis att lägga till anteckningar så att trimmern vet vad som ska bevaras.

Varningar som genereras av dessa extra krav ignoreras automatiskt om den berörda koden finns i en metod med RequiresUnreferencedCode.

Till skillnad från RequiresUnreferencedCode, som helt enkelt rapporterar inkompatibiliteten, gör tillägg DynamicallyAccessedMembers av koden kompatibel med trimning.

Förhindra trimmervarningar

Om du på något sätt kan fastställa att anropet är säkert och all kod som behövs inte kommer att trimmas bort, kan du också ignorera varningen med .UnconditionalSuppressMessageAttribute Till exempel:

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

Varning

Var mycket försiktig när du undertrycker trimvarningar. Det är möjligt att anropet kan vara trimkompatibelt nu, men när du ändrar din kod som kan ändras, och du kanske glömmer att granska alla undertryckningar.

UnconditionalSuppressMessage är som SuppressMessage men det kan ses av publish och andra verktyg efter bygget.

Viktigt!

Använd inte SuppressMessage eller #pragma warning disable för att förhindra trimmervarningar. Dessa fungerar bara för kompilatorn, men bevaras inte i den kompilerade sammansättningen. Trimmer fungerar på kompilerade sammansättningar och skulle inte se undertryckningen.

Undertryckningen gäller för hela metodtexten. Så i vårt exempel ovan utelämnas alla IL2026 varningar från metoden. Detta gör det svårare att förstå, eftersom det inte är klart vilken metod som är problematisk, såvida du inte lägger till en kommentar. Ännu viktigare är att om koden ändras i framtiden, till exempel om ReportResults den blir trim-inkompatibel också, rapporteras ingen varning för det här metodanropet.

Du kan lösa detta genom att omstrukturera det problematiska metodanropet till en separat metod eller lokal funktion och sedan tillämpa undertryckningen på just den metoden:

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