在应用程序中启用修整时,.NET SDK 会执行静态分析来检测可能与剪裁不兼容的代码模式。 修剪警告表示在修剪后可能导致程序行为改变或崩溃的潜在问题。
使用剪裁的应用不应生成任何剪裁警告。 如果有任何修剪警告,请彻底测试修剪后的应用程序,以确保没有行为发生变化。
本文提供了解决剪裁警告的实际工作流。 若要更深入地了解这些警告发生的原因以及剪裁的工作原理,请参阅 “了解剪裁分析”。
了解警告类别
剪裁警告分为两个主要类别:
与剪裁不兼容的代码 - 标记了 RequiresUnreferencedCodeAttribute。 根本无法对代码进行分析(例如,动态程序集加载或复杂反射模式)。 该方法标记为不兼容,调用方会收到警告。
具有要求的代码 - 注释为 DynamicallyAccessedMembersAttribute。 使用反射,但在编译时已知类型。 要求满足后,代码将完全裁剪兼容。
工作流:确定正确的方法
遇到剪裁警告时,请按顺序执行以下步骤:
- 消除反射 - 如果可能,这始终是最佳选择。
- 使用动态访问成员 - 如果已知类型,请使代码适应剪裁兼容。
- 使用 RequiresUnreferencedCode - 如果确实为动态代码,请记录其不兼容性。
- 将警告禁止为最后手段 - 仅当确定代码安全时。
方法 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 为常见反射方案提供源生成器:
- 序列化:使用 System.Text.Json 源生成 ,而不是基于反射的序列化程序
- 配置:使用 配置绑定源代码生成器
有关详细信息,请参阅已知剪裁不兼容性。
方法 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.Emit 或
dynamic关键字。
分步说明:标记不兼容的方法
步骤 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 。
当抑制合适时
仅在抑制警告的情况下:
- 你已手动确保保留所有必需的代码(通过
DynamicDependency或其他机制)。 - 代码路径在裁剪场景中永远不会被执行。
- 你已彻底测试过精简版应用程序。
如何禁止显示警告
[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的源。
- 查找使用反射的位置(如
GetMethods())。 - 批注该方法的参数。
- 追溯
Type值,逆向遍历所有方法调用。 - 批注链中的每个参数、字段或泛型类型参数。
警告过多,难以处理
- 从你自己的代码开始 - 修复你首先控制的代码中的警告。
- 使用
TrimmerSingleWarn查看包中的单个警告。 - 考虑剪裁是否适合应用程序。
- 检查 已知剪裁 不兼容的框架级问题。
不确定要使用哪个DynamicallyAccessedMemberTypes
查看正在调用的反射 API:
-
GetMethod()/GetMethods()→PublicMethods -
GetProperty()/GetProperties()→PublicProperties -
GetField()/GetFields()→PublicFields -
GetConstructor()/Activator.CreateInstance()PublicParameterlessConstructor→或PublicConstructors -
GetEvent()/GetEvents()→PublicEvents
使用最窄的类型可以最小化应用大小。