修复剪裁警告

在应用程序中启用修整时,.NET SDK 会执行静态分析来检测可能与剪裁不兼容的代码模式。 修剪警告表示在修剪后可能导致程序行为改变或崩溃的潜在问题。

使用剪裁的应用不应生成任何剪裁警告。 如果有任何修剪警告,请彻底测试修剪后的应用程序,以确保没有行为发生变化。

本文提供了解决剪裁警告的实际工作流。 若要更深入地了解这些警告发生的原因以及剪裁的工作原理,请参阅 “了解剪裁分析”。

了解警告类别

剪裁警告分为两个主要类别:

  • 与剪裁不兼容的代码 - 标记了 RequiresUnreferencedCodeAttribute。 根本无法对代码进行分析(例如,动态程序集加载或复杂反射模式)。 该方法标记为不兼容,调用方会收到警告。

  • 具有要求的代码 - 注释为 DynamicallyAccessedMembersAttribute。 使用反射,但在编译时已知类型。 要求满足后,代码将完全裁剪兼容。

工作流:确定正确的方法

遇到剪裁警告时,请按顺序执行以下步骤:

  1. 消除反射 - 如果可能,这始终是最佳选择。
  2. 使用动态访问成员 - 如果已知类型,请使代码适应剪裁兼容。
  3. 使用 RequiresUnreferencedCode - 如果确实为动态代码,请记录其不兼容性。
  4. 将警告禁止为最后手段 - 仅当确定代码安全时。

方法 1:消除反射

最佳解决方案是尽可能完全避免反射。 这使得你的代码运行更快,并且完全与优化兼容。

使用编译时泛型

将运行时类型操作替换为编译时泛型参数。

// ❌ Before: Uses reflection
void CreateAndProcess(Type type)
{
    var instance = Activator.CreateInstance(type);
    // Process instance...
}

// ✅ After: Uses generics
void CreateAndProcess<T>() where T : new()
{
    var instance = new T();
    // Process instance...
}

使用源生成器

新式 .NET 为常见反射方案提供源生成器:

有关详细信息,请参阅已知剪裁不兼容性

方法 2:使代码剪裁与动态AccessedMembers 兼容

当需要反射并且类型在编译时已知时,可以使用 DynamicallyAccessedMembersAttribute 来使您的代码兼容剪裁。

分步说明:批注反射用法

请考虑生成警告的此示例:

void PrintMethodNames(Type type)
{
    // ⚠️ IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods'
    foreach (var method in type.GetMethods())
    {
        Console.WriteLine(method.Name);
    }
}

步骤 1:确定执行了什么反射操作

代码调用 GetMethods(),并要求保留 PublicMethods

步骤 2:批注参数

添加 DynamicallyAccessedMembers 以告知剪裁器需要什么:

void PrintMethodNames(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    // ✅ No warning - trimmer knows to preserve public methods
    foreach (var method in type.GetMethods())
    {
        Console.WriteLine(method.Name);
    }
}

步骤 3:确保呼叫方满足要求

使用已知类型调用此方法时,typeof会自动满足要求:

// ✅ OK - DateTime's public methods will be preserved
PrintMethodNames(typeof(DateTime));

分步:通过调用链传播要求

当类型流经多个方法时,需要传递需求:

void Method1()
{
    Method2<DateTime>();  // ⚠️ Warning: Generic parameter needs annotation
}

void Method2<T>()
{
    Type t = typeof(T);
    Method3(t);  // ⚠️ Warning: Argument doesn't satisfy requirements
}

void Method3(Type type)
{
    var methods = type.GetMethods();  // ⚠️ Warning: Reflection usage
}

步骤 1:从反射用法开始

批注实际使用反射的位置:

void Method3(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    var methods = type.GetMethods();  // ✅ Fixed
}

步骤 2:向上传递调用链

通过调用链向后工作:

void Method2<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>()
{
    Type t = typeof(T);
    Method3(t);  // ✅ Fixed - T is annotated
}

步骤 3:在呼叫站点进行验证

void Method1()
{
    Method2<DateTime>();  // ✅ Fixed - DateTime's public methods preserved
}

有关要求如何流经代码的更多详细信息,请参阅 “了解剪裁分析”。

常见 DynamicallyAccessedMemberTypes 值

选择所需的最低访问级别:

成员类型 何时使用
PublicConstructors 使用 Activator.CreateInstance()GetConstructor()
PublicMethods 使用 GetMethod()GetMethods()
PublicFields 使用 GetField()GetFields()
PublicProperties 使用 GetProperty()GetProperties() (序列化)
PublicEvents 使用 GetEvent()GetEvents()

警告

使用 DynamicallyAccessedMemberTypes.All 保留目标类型及其嵌套类型上的所有成员,但不包括属性返回类型上的成员等传递依赖项。 这大大增加了应用大小。 更重要的是,保留的成员变得可访问,这意味着他们可能包含自己的有问题的代码。 例如,如果一个被保留的成员调用一个被标记为 RequiresUnreferencedCode 的方法,则无法解析该警告,因为该成员是通过反射机制而不是直接调用来被保留。 使用所需的最小成员类型来避免这些级联问题。

方法 3:将代码标记为与 RequiresUnreferencedCode 不兼容

当代码根本无法进行分析时,请使用 RequiresUnreferencedCodeAttribute 来记录不兼容性。

何时使用 RequiresUnreferencedCode

在以下情况下使用此属性:

  • 类型是动态加载的:与运行时确定的字符串一起使用 GetType()
  • 程序集在运行时加载:使用 LoadFrom(String)
  • 复杂反射模式:反射使用过于复杂,无法批注。
  • 运行时代码生成:使用 System.Reflection.Emitdynamic 关键字。

分步说明:标记不兼容的方法

步骤 1:识别真正不兼容的代码

无法进行精简兼容的代码示例:

void LoadPluginByName(string pluginName)
{
    // Type name comes from runtime input - trimmer cannot know what types are needed
    Type pluginType = Type.GetType(pluginName);
    var plugin = Activator.CreateInstance(pluginType);
    // Use plugin...
}

步骤 2:添加 RequiresUnreferencedCode 属性

[RequiresUnreferencedCode("Plugin loading by name is not compatible with trimming. Consider using compile-time plugin registration instead.")]
void LoadPluginByName(string pluginName)
{
    Type pluginType = Type.GetType(pluginName);
    var plugin = Activator.CreateInstance(pluginType);
    // ✅ No warnings inside this method - it's marked as incompatible
}

步骤 3:呼叫者收到警告

void InitializePlugins()
{
    // ⚠️ IL2026: Using member 'LoadPluginByName' which has 'RequiresUnreferencedCodeAttribute'
    // can break functionality when trimming application code. Plugin loading by name is not
    // compatible with trimming. Consider using compile-time plugin registration instead.
    LoadPluginByName("MyPlugin");
}

编写有效的警告消息

良好的 RequiresUnreferencedCode 消息应:

  • 指出哪些功能不兼容:具体说明哪些功能与剪裁不兼容。
  • 建议替代方法:指导开发人员使用与剪裁兼容的解决方案。
  • 简洁:使消息保持简短且可作。
// ❌ Not helpful
[RequiresUnreferencedCode("Uses reflection")]

// ✅ Helpful - explains problem and suggests alternative
[RequiresUnreferencedCode("Dynamic type loading is not compatible with trimming. Use generic type parameters or source generators instead.")]

对于较长的指导,请添加参数 Url

[RequiresUnreferencedCode(
    "Plugin system is not compatible with trimming. See documentation for alternatives.",
    Url = "https://docs.example.com/plugin-trimming")]

传播 RequiresUnreferencedCode

当方法调用标记 RequiresUnreferencedCode的另一个方法时,通常需要传播属性:

class PluginSystem
{
    // Use a constant for consistent messaging
    const string PluginMessage = "Plugin system is not compatible with trimming. Use compile-time registration instead.";

    [RequiresUnreferencedCode(PluginMessage)]
    private void LoadPluginImplementation(string name)
    {
        // Low-level plugin loading
    }

    [RequiresUnreferencedCode(PluginMessage)]
    public void LoadPlugin(string name)
    {
        LoadPluginImplementation(name);  // ✅ No warning - method is also marked
    }
}

常见模式和解决方案

模式:使用 Activator.CreateInstance 的工厂方法

// ❌ Before: Produces warning
object CreateInstance(Type type)
{
    return Activator.CreateInstance(type);
}

// ✅ After: Trim-compatible
object CreateInstance(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type)
{
    return Activator.CreateInstance(type);
}

模式:插件系统加载程序集

// This pattern is fundamentally incompatible with trimming
[RequiresUnreferencedCode("Plugin loading is not compatible with trimming. Consider compile-time plugin registration using source generators.")]
void LoadPluginsFromDirectory(string directory)
{
    foreach (var dll in Directory.GetFiles(directory, "*.dll"))
    {
        Assembly.LoadFrom(dll);
    }
}

模式:依赖注入容器

// Complex DI containers are often incompatible
class Container
{
    [RequiresUnreferencedCode("Service resolution uses complex reflection. Consider using source-generated DI or registering services explicitly.")]
    public object Resolve(Type serviceType)
    {
        // Complex reflection to resolve dependencies
    }
}

方法 4:将警告作为最后手段取消

警告

绝对确定代码安全时,才屏蔽修剪警告。 不正确的抑制操作可能导致裁剪后出现运行时失败。

验证代码是裁剪安全的,但代码裁剪器无法静态证明它时使用 UnconditionalSuppressMessageAttribute

当抑制合适时

仅在抑制警告的情况下:

  1. 你已手动确保保留所有必需的代码(通过 DynamicDependency 或其他机制)。
  2. 代码路径在裁剪场景中永远不会被执行。
  3. 你已彻底测试过精简版应用程序。

如何禁止显示警告

[RequiresUnreferencedCode("Uses reflection")]
void MethodWithReflection() { /* ... */ }

[UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode",
    Justification = "All referenced types are manually preserved via DynamicDependency attributes")]
void CallerMethod()
{
    MethodWithReflection();  // Warning suppressed
}

重要

请勿使用 SuppressMessage#pragma warning disable 在裁剪警告中。 它们仅适用于编译器,但不保留在已编译的程序集中。 裁剪器对已编译的程序集进行操作,不会识别这些抑制器。 请始终使用 UnconditionalSuppressMessage

最小化抑制范围

将抑制应用于可能的最小范围。 将有问题的调用提取到本地函数中:

void ProcessData()
{
    InitializeData();

    CallReflectionMethod();  // Only this call is suppressed

    ProcessResults();

    [UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode",
        Justification = "Types are preserved via DynamicDependency on ProcessData method")]
    void CallReflectionMethod()
    {
        MethodWithReflection();
    }
}

此方法:

  • 明确指出哪些特定调用被抑制。
  • 为了防止因代码更改而意外屏蔽其他警告。
  • 使理由与取消调用保持密切。

故障排除提示

添加"DynamicallyAccessedMembers"后,警告仍然存在。

确保已将整个调用链从反射使用中标注返回到Type的源。

  1. 查找使用反射的位置(如 GetMethods())。
  2. 批注该方法的参数。
  3. 追溯Type值,逆向遍历所有方法调用。
  4. 批注链中的每个参数、字段或泛型类型参数。

警告过多,难以处理

  1. 从你自己的代码开始 - 修复你首先控制的代码中的警告。
  2. 使用 TrimmerSingleWarn 查看包中的单个警告。
  3. 考虑剪裁是否适合应用程序。
  4. 检查 已知剪裁 不兼容的框架级问题。

不确定要使用哪个DynamicallyAccessedMemberTypes

查看正在调用的反射 API:

  • GetMethod() / GetMethods()PublicMethods
  • GetProperty() / GetProperties()PublicProperties
  • GetField() / GetFields()PublicFields
  • GetConstructor() / Activator.CreateInstance() PublicParameterlessConstructor→或PublicConstructors
  • GetEvent() / GetEvents()PublicEvents

使用最窄的类型可以最小化应用大小。

后续步骤