Общие сведения о предупреждениях об обрезке

Концептуально обрезка проста: при публикации приложения пакет SDK для .NET анализирует все приложение и удаляет весь неиспользуемый код. Однако бывает трудно определить, что именно не используется.

Чтобы предотвратить изменения в поведении при обрезке приложений, пакет SDK для .NET предоставляет статический анализ совместимости обрезки с помощью предупреждений обрезки. Триммер выдает предупреждения об обрезки при поиске кода, который может быть несовместим с обрезкой. Код, который не совместим с обрезкой, может создавать изменения поведения или даже сбои в приложении после его усечения. В идеале все приложения, использующие обрезку, не должны создавать предупреждения об обрезки. При наличии предупреждений приложение нужно тщательно проверить после обрезки, чтобы гарантировать отсутствие изменений в поведении.

В этой статье показано, почему некоторые шаблоны создают предупреждения обрезки и как можно устранить эти предупреждения.

Примеры предупреждений об обрезке

Для большинства кода C# просто определить, какой код используется и какой код не используется, триммер может выполнять вызовы методов, ссылки на поля и свойства и т. д., а также определить, какой код доступен. К сожалению, некоторые функции, такие как отражение, представляют серьезную проблему. Рассмотрим следующий код:

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

В этом примере GetType() динамически запрашивает тип с неизвестным именем, а затем выводит имена всех его методов. Так как в публикации не существует способа узнать, какое имя типа будет использоваться, нет способа знать, какой тип следует сохранить в выходных данных. Скорее всего, этот код мог бы работать до обрезки (если входные данные являются чем-то известным в целевой платформе), но, вероятно, создайте исключение null ссылок после обрезки, как Type.GetType возвращает значение NULL, если тип не найден.

В этом случае триммер выдает предупреждение о вызове Type.GetType, указывающее, что он не может определить, какой тип будет использоваться приложением.

Реагирование на предупреждения об обрезке

Предупреждения об обрезке предназначены для того, чтобы сделать результаты обрезки предсказуемыми. Существует две большие категории предупреждений, которые, скорее всего, вы увидите:

  1. Функциональные возможности несовместимы с обрезкой
  2. Функциональные возможности имеют определенные требования к входным данным, совместимым с обрезкой

Функциональные возможности несовместимы с обрезкой

Обычно это методы, которые либо не работают вообще, либо могут быть нарушены в некоторых случаях, если они используются в обрезаемом приложении. Хорошим примером Type.GetType является метод из предыдущего примера. В обрезаном приложении он может работать, но нет гарантии. Такие API помечены как RequiresUnreferencedCodeAttribute.

RequiresUnreferencedCodeAttribute простой и широкий: это атрибут, который означает, что член был аннотирован несовместим с обрезкой. Этот атрибут используется в том случае, если код не является совместимым с обрезкой, а также если зависимость от обрезки слишком сложна, чтобы ее могло проанализировать средство обрезки. Это часто может быть верно для методов, динамически загружающих код, например с помощью LoadFrom(String)перечисления или поиска по всем типам в приложении или сборке, например с помощью GetType()ключевое слово C# dynamic или использования других технологий создания кода среды выполнения. Пример:

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

Решений для RequiresUnreferencedCode немного. Лучший вариант — не вызывать при обрезке метод вообще и использовать что-то другое, совместимое с обрезкой.

Пометка функциональных возможностей как несовместимых с обрезкой

Если вы пишете библиотеку, и она не находится в вашем элементе управления, следует ли использовать несовместимую функциональность, вы можете пометить ее с RequiresUnreferencedCodeпомощью . Это заметит метод как несовместимый с обрезкой. Использование RequiresUnreferencedCode молчания всех предупреждений обрезки в данном методе, но создает предупреждение всякий раз, когда кто-то другой вызывает его.

Требуется RequiresUnreferencedCodeAttribute указать Messageобъект . Сообщение отображается как часть предупреждения, сообщаемого разработчику, который вызывает помеченный метод. Например:

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

В приведенном выше примере предупреждение для определенного метода может выглядеть следующим образом:

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

Разработчики, вызывающие такие API, обычно не будут заинтересованы в особенности затронутых API или конкретных особенностей, так как это относится к обрезке.

Хорошее сообщение должно укажите, какие функциональные возможности несовместимы с обрезкой, а затем помогут разработчику, что такое их потенциальные дальнейшие шаги. Он может предложить использовать другую функциональность или изменить способ использования функциональных возможностей. Это также может просто указать, что функциональные возможности пока не совместимы с обрезкой без четкой замены.

Если руководство разработчика становится слишком длинным, чтобы быть включено в предупреждающее сообщение, можно добавить необязательный Url элемент для RequiresUnreferencedCodeAttribute указания разработчика на веб-страницу, описывающую проблему и возможные решения более подробно.

Например:

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

При этом создается предупреждение:

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

Использование RequiresUnreferencedCode часто приводит к пометке с ним дополнительных методов из-за той же причины. Это часто происходит, когда высокоуровневый метод становится несовместимым с обрезкой, так как вызывает низкоуровневый метод, который не совместим с обрезкой. Вы "пузырьк" предупреждение для общедоступного API. Каждое RequiresUnreferencedCode использование нужного сообщения, и в этих случаях сообщения, скорее всего, одинаковы. Чтобы избежать дедупликации строк и упростить обслуживание, используйте постоянное строковое поле для хранения сообщения:

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

Функциональные возможности с требованиями к входным данным

Обрезка предоставляет API для указания дополнительных требований к входным данным для методов и других элементов, которые приводят к обрезке совместимого кода. Эти требования обычно относятся к отражению и возможности доступа к определенным элементам или операциям с типом. Такие требования задаются с помощью .DynamicallyAccessedMembersAttribute

В отличие от RequiresUnreferencedCode, в некоторых случаях средство обрезки может понять отражение, если к нему не добавлена правильная заметка. Давайте рассмотрим исходный пример:

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

В предыдущем примере реальная проблема .Console.ReadLine() Так как любой тип может быть прочитан, триммер не имеет способа узнать, нужны ли методы или System.DateTimeSystem.Guid любой другой тип. С другой стороны, следующий код будет оштрафован:

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

Средство обрезки сможет определить, какой именно тип используется: System.DateTime. Теперь оно сможет использовать анализ потока, чтобы определить необходимость сохранения всех открытых методов для System.DateTime. Итак, когда используется DynamicallyAccessMembers? Когда отражение разделено между несколькими методами. В следующем коде мы видим, что потоки типов System.DateTime , в Method3 которые используется отражение для доступа к System.DateTimeметодам,

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

При компиляции предыдущего кода создается следующее предупреждение:

IL2070: Program.Method3(Type): аргумент "this" не удовлетворяет аргументу ДинамическиAccessedMemberTypes.PublicMethods в вызове System.Type.GetMethods(). Параметр "type" метода "Program.Method3(Type)" не содержит соответствующие заметки. The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Для обеспечения производительности и стабильности анализ потока не выполняется между методами, поэтому заметка необходима для передачи информации между методами из вызова отражения (GetMethods) в источник Type. В предыдущем примере предупреждение триммера говорит о том, что GetMethods требуется Type , чтобы экземпляр объекта, который он вызвал, чтобы иметь PublicMethods заметку, но type переменная не имеет того же требования. Иными словами, нам нужно передать требования от GetMethods к вызывающей функции:

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

После аннотирования параметра typeисходное предупреждение исчезает, но появится другое:

IL2087: аргумент type не удовлетворяет аргументу "ДинамическиaccessedMemberTypes.PublicMethods" при вызове Program.Method3(Type)". Универсальный параметр T "Program.Method2<T>()" не содержит соответствующие заметки.

Мы распространяли заметки вплоть до параметра typeMethod3, в том Method2 , что у нас есть аналогичная проблема. Триммер может отслеживать значение T по мере прохождения вызова typeof, присваивается локальной переменной tи передается в Method3. На этом этапе он видит, что параметр type требует PublicMethods , но нет требований к Tнему и создает новое предупреждение. Чтобы устранить эту проблему, мы должны "аннотировать и распространять", применяя заметки вплоть до цепочки вызовов, пока мы не достигаем статического известного типа (например System.DateTime , или System.Tupleдругого аннотированного значения). В этом случае нам нужно удлинить параметр TMethod2типа .

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

Теперь нет предупреждений, так как триммер знает, какие элементы могут быть доступны через отражение среды выполнения (общедоступные методы) и какие типы (System.DateTime) и сохраняют их. Рекомендуется добавить заметки, чтобы триммер знал, что сохранить.

Предупреждения, созданные этими дополнительными требованиями, автоматически подавляются, если затронутый код находится в методе.RequiresUnreferencedCode

В отличие RequiresUnreferencedCodeот того, что просто сообщает о несовместимости, добавление DynamicallyAccessedMembers делает код совместимым с обрезкой.

Подавление предупреждений триммера

Если вы можете каким-либо образом определить, что вызов работает надлежащим образом, и весь необходимый код не будет обрезан, предупреждение можно отключить с помощью UnconditionalSuppressMessageAttribute. Например:

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

Предупреждение

Будьте очень осторожны при подавлении предупреждений обрезки. Возможно, вызов может быть совместим с обрезкой сейчас, но при изменении кода, который может измениться, и вы можете забыть просмотреть все подавления.

UnconditionalSuppressMessage похож на SuppressMessage, но видим для publish и других средств после сборки.

Внимание

Не используйте SuppressMessage или #pragma warning disable не подавляйте предупреждения триммера. Они работают только для компилятора, но не сохраняются в скомпилированной сборке. Trimmer работает на скомпилированных сборках и не увидит подавление.

Подавление применяется ко всему телу метода. Таким образом, в нашем примере выше он подавляет все IL2026 предупреждения из метода. Это затрудняет понимание, так как не ясно, какой метод является проблематичным, если вы не добавите комментарий. Более важно, если код изменится в будущем, например, если ReportResults он становится несовместимым с обрезкой, предупреждение не сообщается для этого вызова метода.

Это можно устранить, рефакторируя вызов проблемного метода в отдельный метод или локальную функцию, а затем применяя подавление только к этому методу:

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