共用方式為


修正邊框警告

當你在應用程式中啟用修剪功能時,.NET SDK 會進行靜態分析,以偵測可能不相容於修剪的程式碼模式。 修剪警告表示可能造成潛在問題,可能導致修剪後行為改變或當機。

使用修剪的應用程式不應該產生任何修剪警告。 如果有任何修剪警告,修剪後請仔細測試應用程式,確保沒有行為變化。

本文提供處理修剪警告的實用工作流程。 欲更深入了解這些警告為何會出現及修剪的運作方式,請參閱 「理解修剪分析」。

了解警示類別

修剪警告主要分為兩大類:

  • 程式碼與修剪不相容 - 標記為 RequiresUnreferencedCodeAttribute。 程式碼根本無法被分析(例如動態組合載入或複雜的反射模式)。 此方法被標記為不相容,來電者會收到警告。

  • 帶有需求的程式碼 - 附註。DynamicallyAccessedMembersAttribute 雖然使用了反射,但型別在編譯時已知。 當需求達成時,程式碼即完全符合trim-comp。

工作流程:找出正確的方法

遇到修剪警告時,請依序執行以下步驟:

  1. 消除反射 ——如果可能,這是最好的選擇。
  2. 使用 DynamicallyAccessedMembers - 如果已知型別,則讓程式碼具備縮減相容性。
  3. 使用 RequiresUnreferencedCode - 如果確實是動態的,請詳細記錄相容性問題。
  4. 僅在你確定程式碼安全時,才把警告抑制作為最後手段

方法一:消除反射

最好的解決方法是盡可能避免反思。 這樣你的程式碼會更快,且完全修剪相容。

使用編譯時通用碼

將執行時型態操作替換為編譯時的通用參數:

// ❌ 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 提供常見反射情境的原始碼產生器:

如需詳細資訊,請參閱已知修剪不相容性

方法二:讓程式碼與 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.Emitdynamic關鍵字。

詳細步驟:標記不相容的方法

步驟 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

何時適當壓制

僅在以下情況下才抑制警告:

  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. 檢查已知的修剪不相容性,以防框架層級問題。

不確定該使用哪個動態存取的成員類型

看看被呼叫的反射 API:

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

盡量用最窄的字體來減少應用程式大小。

後續步驟