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. Aplikacja korzystająca z przycinania nie powinna tworzyć ż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:
- Funkcjonalność nie jest zgodna z przycinaniem
- 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ą RequiresUnreferencedCode
polecenia . 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 Url
RequiresUnreferencedCodeAttribute , 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 RequiresUnreferencedCode
elementu 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.DateTime
do 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 Type
elementu . 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 type
oryginalne 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 type
Method3
, w Method2
pliku mamy podobny problem. Trimmer jest w stanie śledzić wartość T
, gdy przepływa przez wywołanie metody , typeof
jest przypisywany do zmiennej t
lokalnej i przekazywany do Method3
. W tym momencie widzi, że parametr type
wymaga PublicMethods
, ale nie ma żadnych wymagań dotyczących T
parametru 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 T
Method2
typu .
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 RequiresUnreferencedCode
elementu , który po prostu zgłasza niezgodność, dodanie DynamicallyAccessedMembers
sprawia, że kod jest zgodny z przycinaniem.
Uwaga
Użycie DynamicallyAccessedMembersAttribute
spowoduje root wszystkich określonych DynamicallyAccessedMemberTypes
elementów członkowskich typu. Oznacza to, że zachowa członków, a także wszelkie metadane, do których odwołuje się te elementy członkowskie. Może to prowadzić do znacznie większych aplikacji niż oczekiwano. Należy zachować ostrożność, aby użyć minimalnej DynamicallyAccessedMemberTypes
wymaganej wartości.
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
}
}