Wprowadzenie do ostrzeżeń dotyczących przycinania

Koncepcyjnie przycinanie jest proste: podczas publikowania aplikacji zestaw SDK platformy .NET analizuje całą aplikację i usuwa cały nieużywany kod. Jednak może być trudno określić, co jest nieużywane, lub dokładniej, co jest używane.

Aby zapobiec zmianom zachowania podczas przycinania aplikacji, zestaw .NET SDK zapewnia statyczną analizę zgodności przycinania za pomocą ostrzeżeń dotyczących przycinania. Trimmer generuje ostrzeżenia dotyczące przycinania, gdy znajdzie kod, który może nie być zgodny z przycinaniem. Kod, który nie jest zgodny z przycinanie, może generować zmiany behawioralne, a nawet ulegać awarii, w aplikacji po jego przycięciu. Najlepiej, aby wszystkie aplikacje korzystające z przycinania nie tworzyły żadnych ostrzeżeń dotyczących przycinania. Jeśli istnieją ostrzeżenia dotyczące przycinania, aplikacja powinna być dokładnie przetestowana po przycinaniu, aby upewnić się, że nie ma żadnych zmian w zachowaniu.

Ten artykuł pomaga zrozumieć, dlaczego niektóre wzorce generują ostrzeżenia dotyczące przycinania i jak można rozwiązać te ostrzeżenia.

Przykłady ostrzeżeń dotyczących przycinania

W przypadku większości kodu w języku C# łatwo jest określić, który kod jest używany i jaki kod jest nieużywany — trymer może służyć do chodzenia wywołań metod, odwołań do pól i właściwości itd., a także określać, do jakiego kodu uzyskuje się dostęp. Niestety, niektóre funkcje, takie jak odbicie, stanowią znaczący problem. Spójrzmy na poniższy kod:

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

W tym przykładzie GetType() dynamicznie żąda typu o nieznanej nazwie, a następnie wyświetla nazwy wszystkich jego metod. Ponieważ nie ma możliwości określenia w czasie publikowania, jakiej nazwy typu ma zostać użyta, nie ma możliwości, aby trymer wiedział, jakiego typu należy zachować w danych wyjściowych. Prawdopodobnie ten kod mógł działać przed przycinaniem (o ile dane wejściowe istnieją w strukturze docelowej), ale prawdopodobnie utworzy wyjątek odwołania o wartości null po przycinaniu, ponieważ Type.GetType zwraca wartość null, gdy typ nie zostanie znaleziony.

W takim przypadku program trymer wyświetla ostrzeżenie dotyczące wywołania metody Type.GetType, co oznacza, że nie może określić, który typ będzie używany przez aplikację.

Reagowanie na ostrzeżenia dotyczące przycinania

Ostrzeżenia dotyczące przycinania mają na celu przewidywalność przycinania. Istnieją dwie duże kategorie ostrzeżeń, które prawdopodobnie zobaczysz:

  1. Funkcjonalność nie jest zgodna z przycinaniem
  2. Funkcjonalność ma pewne wymagania dotyczące danych wejściowych do przycinania zgodnego

Funkcjonalność niezgodna z przycinaniem

Są to zazwyczaj metody, które w ogóle nie działają lub mogą być uszkodzone w niektórych przypadkach, jeśli są używane w przyciętej aplikacji. Dobrym przykładem jest Type.GetType metoda z poprzedniego przykładu. W przyciętej aplikacji może działać, ale nie ma gwarancji. Takie interfejsy API są oznaczone znakiem RequiresUnreferencedCodeAttribute.

RequiresUnreferencedCodeAttribute jest prosty i szeroki: jest to atrybut, który oznacza, że element członkowski został oznaczony adnotacją niezgodną z przycinaniem. Ten atrybut jest używany, gdy kod nie jest zasadniczo zgodny z przycinanie lub zależność przycinania jest zbyt złożona, aby wyjaśnić trymer. Jest to często prawdziwe w przypadku metod, które dynamicznie ładują kod, na przykład za pomocą LoadFrom(String)metody , wyliczają lub wyszukują wszystkie typy w aplikacji lub zestawie, na przykład za pomocą GetType()słowa kluczowego języka C# dynamic lub używają innych technologii generowania kodu środowiska uruchomieniowego. Przykładem może być:

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

Nie ma wielu obejść dla programu RequiresUnreferencedCode. Najlepszym rozwiązaniem jest unikanie wywoływania metody w ogóle podczas przycinania i używania innego elementu zgodnego z przycinaniem.

Oznacz funkcjonalność jako niezgodną z przycinaniem

Jeśli piszesz bibliotekę i nie znajduje się ona w kontrolce, czy używać niezgodnych funkcji, możesz oznaczyć ją za pomocą RequiresUnreferencedCodepolecenia . Spowoduje to dodawanie adnotacji do metody jako niezgodnej z przycinaniem. Użycie RequiresUnreferencedCode funkcji wycisza wszystkie ostrzeżenia przycinania w danej metodzie, ale generuje ostrzeżenie za każdym razem, gdy ktoś inny go wywoła.

Parametr RequiresUnreferencedCodeAttribute wymaga określenia wartości Message. Komunikat jest wyświetlany jako część ostrzeżenia zgłoszonego deweloperowi, który wywołuje oznaczoną metodę. Na przykład:

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

W powyższym przykładzie ostrzeżenie dla określonej metody może wyglądać następująco:

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

Deweloperzy wywołujący takie interfejsy API zazwyczaj nie będą zainteresowani konkretnymi elementami interfejsu API lub specyfikami, które odnoszą się do przycinania.

Dobry komunikat powinien określać, jakie funkcje nie są zgodne z przycinaniem, a następnie kierować deweloperem, jakie są ich potencjalne następne kroki. Może to sugerować użycie innej funkcji lub zmianę sposobu używania funkcji. Może również po prostu stwierdzić, że funkcjonalność nie jest jeszcze zgodna z przycinaniem bez wyraźnego zastąpienia.

Jeśli wskazówki dla dewelopera staną się zbyt długie, aby zostały uwzględnione w komunikacie ostrzegawczym, możesz dodać opcjonalny element UrlRequiresUnreferencedCodeAttribute , aby wskazać deweloperowi stronę internetową opisującą problem i możliwe rozwiązania bardziej szczegółowo.

Na przykład:

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

Spowoduje to wygenerowanie ostrzeżenia:

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

Użycie RequiresUnreferencedCode często prowadzi do oznaczania większej liczby metod z tą samą przyczyną. Jest to powszechne, gdy metoda wysokiego poziomu staje się niezgodna z przycinaniem, ponieważ wywołuje metodę niskiego poziomu, która nie jest zgodna z przycinaniem. Ostrzeżenie jest "bąbelkowe" dla publicznego interfejsu API. Każde użycie komunikatu wymaga komunikatu RequiresUnreferencedCode , a w takich przypadkach komunikaty są prawdopodobnie takie same. Aby uniknąć duplikowania ciągów i ułatwiać konserwację, użyj pola ciągów stałych do przechowywania komunikatu:

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

Funkcjonalność z wymaganiami dotyczącymi danych wejściowych

Przycinanie zapewnia interfejsy API, aby określić więcej wymagań dotyczących danych wejściowych do metod i innych elementów członkowskich, które prowadzą do przycinania kodu zgodnego. Te wymagania dotyczą zwykle odbicia i możliwości uzyskania dostępu do niektórych elementów członkowskich lub operacji na typie. Takie wymagania są określane przy użyciu elementu DynamicallyAccessedMembersAttribute.

W przeciwieństwie do RequiresUnreferencedCodeelementu odbicie może być czasami zrozumiałe przez trymer tak długo, jak jest poprawnie oznaczone. Przyjrzyjmy się innemu przykładowi:

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

W poprzednim przykładzie prawdziwym problemem jest Console.ReadLine(). Ponieważ można odczytać dowolny typ, trymer nie ma sposobu, aby wiedzieć, czy potrzebujesz metod na System.DateTime lub System.Guid w jakimkolwiek innym typie. Z drugiej strony następujący kod byłby odpowiedni:

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

W tym miejscu trimmer może zobaczyć dokładny typ, do których odwołuje się odwołanie: System.DateTime. Teraz można użyć analizy przepływu, aby określić, że musi zachować wszystkie metody publiczne w systemie System.DateTime. Więc gdzie DynamicallyAccessMembers przychodzi? Gdy odbicie jest podzielone na wiele metod. W poniższym kodzie widać, że typ System.DateTime przepływa do Method3 miejsca, w którym odbicie jest używane do uzyskiwania dostępu System.DateTimedo metod ,

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

Jeśli skompilujesz poprzedni kod, zostanie wygenerowane następujące ostrzeżenie:

IL2070: Program.Method3(Type): argument "this" nie spełnia parametru "DynamicallyAccessedMemberTypes.PublicMethods" w wywołaniu metody "System.Type.GetMethods()". Parametr "type" metody "Program.Method3(Type)" nie ma pasujących adnotacji. Wartość źródłowa musi zadeklarować co najmniej te same wymagania co zadeklarowane w lokalizacji docelowej, do której jest przypisana.

W celu zapewnienia wydajności i stabilności analiza przepływu nie jest wykonywana między metodami, dlatego adnotacja jest potrzebna do przekazywania informacji między metodami z wywołania odbicia (GetMethods) do źródła Typeelementu . W poprzednim przykładzie ostrzeżenie trymeru oznacza, że GetMethods wymaga Type wystąpienia obiektu, na PublicMethods które jest wywoływana adnotacja, ale type zmienna nie ma tego samego wymagania. Innymi słowy, musimy przekazać wymagania od GetMethods obiektu wywołującego:

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 dodaniu adnotacji do parametru typeoryginalne ostrzeżenie zniknie, ale zostanie wyświetlone inne:

IL2087: argument "type" nie spełnia parametru "DynamicallyAccessedMemberTypes.PublicMethods" w wywołaniu metody "Program.Method3(Type)". Ogólny parametr "T" elementu "Program.Method2<T>()" nie ma pasujących adnotacji.

Rozpropagowaliśmy adnotacje do parametru typeMethod3, w Method2 pliku mamy podobny problem. Trimmer jest w stanie śledzić wartość T , gdy przepływa przez wywołanie metody , typeofjest przypisywany do zmiennej tlokalnej i przekazywany do Method3. W tym momencie widzi, że parametr type wymaga PublicMethods , ale nie ma żadnych wymagań dotyczących Tparametru i generuje nowe ostrzeżenie. Aby rozwiązać ten problem, musimy "dodawać adnotacje i propagować", stosując adnotacje aż do momentu osiągnięcia statycznie znanego typu (na System.DateTime przykład lub System.Tuple) lub innej wartości adnotacji. W tym przypadku musimy dodać adnotację do parametru TMethod2typu .

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

Teraz nie ma żadnych ostrzeżeń, ponieważ program trymer wie, do których elementów członkowskich można uzyskać dostęp za pośrednictwem odbicia środowiska uruchomieniowego (metod publicznych) i których typów (System.DateTime) i zachowuje je. Najlepszym rozwiązaniem jest dodanie adnotacji, dzięki czemu trymer wie, co należy zachować.

Ostrzeżenia generowane przez te dodatkowe wymagania są automatycznie pomijane, jeśli kod, którego dotyczy problem, znajduje się w metodzie .RequiresUnreferencedCode

W przeciwieństwie do RequiresUnreferencedCodeelementu , który po prostu zgłasza niezgodność, dodanie DynamicallyAccessedMembers sprawia, że kod jest zgodny z przycinaniem.

Pomijanie ostrzeżeń trymeru

Jeśli możesz w jakiś sposób określić, że wywołanie jest bezpieczne, a cały potrzebny kod nie zostanie przycięty, możesz również pominąć ostrzeżenie przy użyciu polecenia UnconditionalSuppressMessageAttribute. Na przykład:

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

Ostrzeżenie

Należy zachować ostrożność podczas pomijania ostrzeżeń dotyczących przycinania. Istnieje możliwość, że wywołanie może być teraz zgodne z przycinaniem, ale w miarę zmiany kodu, który może ulec zmianie, i możesz zapomnieć o przejrzeniu wszystkich pomijań.

UnconditionalSuppressMessage jest jak SuppressMessage , ale można go zobaczyć za pomocą publish innych narzędzi po kompilacji.

Ważne

Nie należy używać SuppressMessage ani #pragma warning disable pomijać ostrzeżeń trymeru. Działają one tylko dla kompilatora, ale nie są zachowywane w skompilowanym zestawie. Program Trimmer działa na skompilowanych zestawach i nie widziałby pomijania.

Pomijanie dotyczy całej treści metody. W naszym przykładzie powyżej pomija wszystkie IL2026 ostrzeżenia z metody . Utrudnia to zrozumienie, ponieważ nie jest jasne, która metoda jest problematyczna, chyba że dodasz komentarz. Co ważniejsze, jeśli kod zmieni się w przyszłości, na przykład jeśli ReportResults stanie się niezgodny z przycinaniem, żadne ostrzeżenie nie zostanie zgłoszone dla tego wywołania metody.

Można rozwiązać ten problem, refaktoryzując problematyczne wywołanie metody do oddzielnej metody lub funkcji lokalnej, a następnie stosując pomijanie tylko do tej metody:

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