了解剪裁分析

本文介绍了剪裁分析背后的基本概念,可帮助你了解为什么某些代码模式生成警告以及如何使代码剪裁兼容。 了解这些概念有助于你在解决裁剪警告时做出明智的决策,而不仅仅是使用分散的属性来使工具软件保持静默。

剪裁器如何分析代码

剪裁器在发布时执行 静态分析 以确定应用程序使用的代码。 它从已知的入口点(如 Main 方法)开始,并遵循应用程序的代码路径。

剪裁器可以理解的内容

剪裁器擅长分析直接编译时可见的代码模式:

// The trimmer CAN understand these patterns:
var date = new DateTime();
date.AddDays(1);  // Direct method call - trimmer knows AddDays is used

var list = new List<string>();
list.Add("hello");  // Generic method call - trimmer knows List<string>.Add is used

string result = MyUtility.Process("data");  // Direct static method call

在这些示例中,修整程序可以遵循代码路径和标记 DateTime.AddDaysList<string>.AddMyUtility.Process 作为应保留在最终应用程序中的已用代码。

剪裁器无法理解的内容

剪裁器在动态操作中表现较差,因为其操作对象在运行时才会确定。

// The trimmer CANNOT fully understand these patterns:
Type type = Type.GetType(Console.ReadLine());  // Type name from user input
type.GetMethod("SomeMethod");  // Which method? On which type?

object obj = GetSomeObject();
obj.GetType().GetProperties();  // What type will obj be at runtime?

Assembly asm = Assembly.LoadFrom(pluginPath);  // What's in this assembly?

在这些示例中,剪裁器无法知道:

  • 用户将输入的类型
  • 什么类型GetSomeObject()返回
  • 动态加载程序集中存在哪些代码

这是修剪警告所解决的根本问题。

反射问题

反射允许代码在运行时动态检查和调用类型和成员。 这是强大的,但为静态分析创造了一个挑战。

为什么反射中断剪裁

请看以下示例:

void PrintMethodNames(Type type)
{
    foreach (var method in type.GetMethods())
    {
        Console.WriteLine(method.Name);
    }
}

// Called somewhere in the app
PrintMethodNames(typeof(DateTime));

从剪裁器的角度来看:

  • 它检测到 type.GetMethods() 被调用。
  • 作为一个参数,它不知道 type 将是什么。
  • 它无法确定需要保留哪些类型的方法。
  • 如果没有指导,它可能会从 DateTime中删除方法,破坏代码。

因此,剪裁器对此代码生成警告。

了解动态访问成员

DynamicallyAccessedMembersAttribute 通过在调用方和被调用方法之间创建显式协定来解决反射问题。

基本用途

DynamicallyAccessedMembers 告知剪裁器:“此参数(或字段或返回值)将保留 Type 需要保留的特定成员,因为反射将用于访问它们。

具体示例

我们来修复前面的示例:

void PrintMethodNames(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    foreach (var method in type.GetMethods())
    {
        Console.WriteLine(method.Name);
    }
}

// When this is called...
PrintMethodNames(typeof(DateTime));

现在剪裁器理解:

  1. PrintMethodNames 要求其参数保留 PublicMethods
  2. 呼叫站点通过 typeof(DateTime)
  3. 因此,必须保留DateTime的公共方法。

该属性创建一个 要求 ,此要求从反射使用往后追溯到 Type 值的来源。

这是一个合同,而不是提示

这一点对于理解至关重要: DynamicallyAccessedMembers 不仅仅是文档。 剪裁器强制实施此协定。

具有泛型类型约束的类比

如果熟悉泛型类型约束, DynamicallyAccessedMembers 则同样有效。 就像泛型约束流经代码一样:

void Process<T>(T value) where T : IDisposable
{
    value.Dispose();  // OK because constraint guarantees IDisposable
}

void CallProcess<T>(T value) where T : IDisposable
{
    Process(value);  // OK - constraint satisfied
}

void CallProcessBroken<T>(T value)
{
    Process(value);  // ERROR - T doesn't have IDisposable constraint
}

DynamicallyAccessedMembers 在代码中应用类似的要求:

void UseReflection([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    type.GetMethods();  // OK because annotation guarantees methods are preserved
}

void PassType([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    UseReflection(type);  // OK - requirement satisfied
}

void PassTypeBroken(Type type)
{
    UseReflection(type);  // WARNING - type doesn't have required annotation
}

两者都创建必须满足的合同,并且当无法满足合同时都会生成错误或警告。

如何强制执行合同

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
Type GetTypeForProcessing() 
{
    return typeof(DateTime);  // OK - trimmer will preserve DateTime's public methods
}

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
Type GetTypeFromInput()
{
    // WARNING: The trimmer can't verify that the type from GetType()
    // will have its public methods preserved
    return Type.GetType(Console.ReadLine());
}

如果无法履行合同(如第二个示例中),将收到警告。

理解RequiresUnreferencedCode属性

某些代码模式根本无法静态分析。 对于这些情况,请使用 RequiresUnreferencedCodeAttribute

何时使用 RequiresUnreferencedCode

在以下情况下使用 RequiresUnreferencedCodeAttribute 特性:

  • 反射模式从根本上是动态的:按来自外部源的字符串名称加载程序集或类型。
  • 复杂性过高,无法批注:采用复杂数据驱动方式使用反射的代码。
  • 你正在使用运行时代码生成:技术(如 System.Reflection.Emitdynamic 关键字)。

示例:

[RequiresUnreferencedCode("Plugin loading is not compatible with trimming")]
void LoadPlugin(string pluginPath)
{
    Assembly pluginAssembly = Assembly.LoadFrom(pluginPath);
    // Plugin assemblies aren't known at publish time
    // This fundamentally cannot be made trim-compatible
}

特性的用途

RequiresUnreferencedCode 服务两个目的:

  1. 禁止在方法内显示警告:剪裁器不会分析或警告反射使用情况。
  2. 在调用站点创建警告:调用此方法的任何代码都会收到警告。

此警告会“上升”,以便为开发人员提供对剪裁不兼容代码路径的可见性。

编写好消息

该消息应帮助开发人员了解其选项:

// ❌ Not helpful
[RequiresUnreferencedCode("Uses reflection")]

// ✅ Helpful - explains what's incompatible and suggests alternatives
[RequiresUnreferencedCode("Plugin loading is not compatible with trimming. Consider using a source generator for known plugins instead")]

要求如何流经代码

了解需求传播方式有助于了解添加属性的位置。

需求反向流动

要求从使用反射的地方流动回 Type 的起源地。

void CallChain()
{
    // Step 1: Source of the Type value
    ProcessData<DateTime>();  // ← Requirement ends here
}

void ProcessData<T>()
{
    // Step 2: Type flows through generic parameter
    var type = typeof(T);
    DisplayInfo(type);  // ← Requirement flows back through here
}

void DisplayInfo(Type type)
{
    // Step 3: Reflection creates the requirement
    type.GetMethods();  // ← Requirement starts here
}

若要使此剪裁兼容,需要对链进行批注:

void ProcessData<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>()
{
    var type = typeof(T);
    DisplayInfo(type);
}

void DisplayInfo(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    type.GetMethods();
}

现在要求流:GetMethods()需要PublicMethodstype参数需要PublicMethods→通用T需要PublicMethodsDateTime需要保留PublicMethods

需求流经存储

要求还会传递到字段和属性中:

class TypeHolder
{
    // This field will hold Types that need PublicMethods preserved
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
    private Type _typeToProcess;

    public void SetType<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>()
    {
        _typeToProcess = typeof(T);  // OK - requirement satisfied
    }

    public void Process()
    {
        _typeToProcess.GetMethods();  // OK - field is annotated
    }
}

选择正确的方法

遇到需要反射的代码时,请遵循以下决策树:

1. 你能避免反射吗?

最佳解决方案是尽可能避免反射:

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

// ✅ Uses compile-time generics instead
void Process<T>() where T : new()
{
    var instance = new T();
}

2. 类型在编译时是否已知?

如果需要反射,但类型已知,请使用 DynamicallyAccessedMembers

// ✅ Trim-compatible
void Serialize<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T>(T obj)
{
    foreach (var prop in typeof(T).GetProperties())
    {
        // Serialize property
    }
}

3. 模式是否从根本上是动态的?

如果在运行时之前真正不知道这些类型,请使用 RequiresUnreferencedCode

// ✅ Documented as trim-incompatible
[RequiresUnreferencedCode("Dynamic type loading is not compatible with trimming")]
void ProcessTypeByName(string typeName)
{
    var type = Type.GetType(typeName);
    // Work with type
}

常见模式和解决方案

模式:工厂方法

// Problem: Creating instances from Type parameter
object CreateInstance(Type type)
{
    return Activator.CreateInstance(type);
}

// Solution: Specify constructor requirements
object CreateInstance(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type)
{
    return Activator.CreateInstance(type);
}

模式:插件系统

// Problem: Loading unknown assemblies at runtime
[RequiresUnreferencedCode("Plugin loading is not trim-compatible. Plugins must be known at compile time.")]
void LoadPlugins(string pluginDirectory)
{
    foreach (var file in Directory.GetFiles(pluginDirectory, "*.dll"))
    {
        Assembly.LoadFrom(file);
    }
}

// Better solution: Known plugins with source generation
// Use source generators to create plugin registration code at compile time

关键结论

  • 剪裁器使用静态分析 - 它只能了解编译时可见的代码路径。
  • 反射中断静态分析 - 缩减器无法预见反射在运行时的访问内容。
  • DynamicallyAccessedMembers 定义协议 - 它指示修剪器需要保留哪些内容。
  • 需求反向流动 - 从反射使用情况回到Type值的源头。
  • RequiresUnreferencedCode 用于记录不兼容性 - 当无法进行代码分析时使用它。
  • 属性不仅仅是提示 - 剪裁器会强制实施协定,并在无法满足它们时生成警告。

后续步骤