當你在應用程式中啟用修剪功能時,.NET SDK 會進行靜態分析,以偵測可能不相容於修剪的程式碼模式。 修剪警告表示可能造成潛在問題,可能導致修剪後行為改變或當機。
使用修剪的應用程式不應該產生任何修剪警告。 如果有任何修剪警告,修剪後請仔細測試應用程式,確保沒有行為變化。
本文提供處理修剪警告的實用工作流程。 欲更深入了解這些警告為何會出現及修剪的運作方式,請參閱 「理解修剪分析」。
了解警示類別
修剪警告主要分為兩大類:
程式碼與修剪不相容 - 標記為 RequiresUnreferencedCodeAttribute。 程式碼根本無法被分析(例如動態組合載入或複雜的反射模式)。 此方法被標記為不相容,來電者會收到警告。
帶有需求的程式碼 - 附註。DynamicallyAccessedMembersAttribute 雖然使用了反射,但型別在編譯時已知。 當需求達成時,程式碼即完全符合trim-comp。
工作流程:找出正確的方法
遇到修剪警告時,請依序執行以下步驟:
- 消除反射 ——如果可能,這是最好的選擇。
- 使用 DynamicallyAccessedMembers - 如果已知型別,則讓程式碼具備縮減相容性。
- 使用 RequiresUnreferencedCode - 如果確實是動態的,請詳細記錄相容性問題。
- 僅在你確定程式碼安全時,才把警告抑制作為最後手段。
方法一:消除反射
最好的解決方法是盡可能避免反思。 這樣你的程式碼會更快,且完全修剪相容。
使用編譯時通用碼
將執行時型態操作替換為編譯時的通用參數:
// ❌ 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 原始碼產生 ,而非基於反射的序列化器
- 配置:使用 配置綁定來源產生器
如需詳細資訊,請參閱已知修剪不相容性。
方法二:讓程式碼與 DynamicallyAccessedMembers 相容 trim
當需要反射但編譯時已知型別時,請使用 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);
}
}
步驟三:確保來電者符合要求
當以已知型別()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
}
步驟二:向上推廣呼叫鏈
從後往前檢視呼叫鏈:
void Method2<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>()
{
Type t = typeof(T);
Method3(t); // ✅ Fixed - T is annotated
}
步驟三:在呼叫現場驗證
void Method1()
{
Method2<DateTime>(); // ✅ Fixed - DateTime's public methods preserved
}
欲了解更多需求如何透過程式碼流動的細節,請參閱 「理解修剪分析」。
常見的動態存取成員類型值
選擇最低存取權限:
| 會員類型 | 使用時機 |
|---|---|
PublicConstructors |
使用 Activator.CreateInstance() 或 GetConstructor() |
PublicMethods |
使用 GetMethod() 或 GetMethods() |
PublicFields |
使用 GetField() 或 GetFields() |
PublicProperties |
使用 GetProperty() 或 GetProperties() (序列化) |
PublicEvents |
使用 GetEvent() 或 GetEvents() |
警告
使用 DynamicallyAccessedMemberTypes.All 時會保留目標型別上的所有成員及其巢狀型別上的所有成員,但不保留傳遞依賴關係中的成員,例如屬性的回傳型別中的成員。 這大幅增加了應用程式的大小。 更重要的是,已保存的成員會變得可達,這意味著它們可能包含潛在的問題程式碼。 例如,若保留成員呼叫標記為 RequiresUnreferencedCode的方法,該警告無法解析,因為該成員是透過反射註解而非明確呼叫來保存。 使用最低要求的會員類型以避免這些連鎖問題。
方法三:將程式碼標記為與 RequiresUnreferencedCode 不相容
當程式碼根本無法被分析時,請用 RequiresUnreferencedCodeAttribute 來記錄不相容性。
何時使用 RequiresUnreferencedCode
使用此屬性時:
- 類型以動態方式載入:使用GetType()於執行期決定的字串。
- 組件檔在執行時載入:使用 LoadFrom(String)。
- 複雜的反射模式:反射的使用過於複雜,無法註解。
-
執行時程式碼產生:使用System.Reflection.Emit或
dynamic關鍵字。
詳細步驟:標記不相容的方法
步驟 1:辨識真正不相容的程式碼
無法使其與 trim 相容的程式碼範例:
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
}
步驟三:來電者收到警告
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
}
}
方法四:將警告抑制作為最後手段
警告
只有在你絕對確定該代碼安全時,才抑制截取警告。 錯誤的抑制可能導致修剪後的運行時失效。
當你確認程式碼是修剪安全但修剪器無法靜態證明時,請使用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來查看包裹上的個別警告。 - 考慮修剪是否適合您的應用。
- 檢查已知的修剪不相容性,以防框架層級問題。
不確定該使用哪個動態存取的成員類型
看看被呼叫的反射 API:
-
GetMethod()/GetMethods()→PublicMethods -
GetProperty()/GetProperties()→PublicProperties -
GetField()/GetFields()→PublicFields -
GetConstructor()/Activator.CreateInstance()→PublicParameterlessConstructor或PublicConstructors -
GetEvent()/GetEvents()→PublicEvents
盡量用最窄的字體來減少應用程式大小。