修剪警告簡介

修剪在概念上很簡單:當您發佈應用程式時,.NET SDK 會分析整個應用程式,並移除所有未使用的程式碼。 不過,很難判斷未使用的程式碼,或更精確地說,判斷使用的程式碼。

為了防止在修剪應用程式時的行為變更,.NET SDK 會透過修剪警告來提供修剪相容性的靜態分析。 修剪器會在找到可能與修剪不相容的程式碼時產生修剪警告。 與修剪不相容的程式碼在經修剪之後,可能會在應用程式中產生行為變更,甚至損毀。 在理想情況下,所有使用修剪的應用程式都不應產生任何修剪警告。。 如果有任何修剪警告,應在修剪之後徹底測試應用程式,確保沒有任何行為變更。

本文會協助您了解為何某些模式會產生修剪警告,以及如何解決這些警告問題。

修剪警告的範例

對於大部分的 C# 程式碼而言,判斷使用的程式碼和未使用的程式碼很簡單,修剪器可以逐步執行方法呼叫、欄位和屬性參考等,並判斷要存取哪些程式碼。 不幸的是,反映等部分功能有一個重大問題。 請考慮下列程式碼:

string s = Console.ReadLine();
Type type = Type.GetType(s);
foreach (var m in type.GetMethods())
{
    Console.WriteLine(m.Name);
}

在此範例中,GetType() 以動態方式要求名稱未知的型別,然後列印其所有方法的名稱。 由於無法在發佈時知道要使用的型別名稱,因此修剪器無法知道要在輸出中保留的型別。 此程式碼可能在修剪之前就可以運作 (只要輸入是已知存在於目標架構中的項目),但在修剪之後可能會產生 Null 參考例外狀況 (因為 Type.GetType 若未找到型別便會傳回 Null)。

在此情況下,修剪器會在呼叫 Type.GetType 時發出警告,表示無法判斷應用程式將使用哪一種型別。

回應修剪警告

修剪警告旨在讓修剪可預測。 您可能會看到兩大類別的警告:

  1. 功能與修剪不相容
  2. 功能對輸入要符合修剪相容性有特定的要求

功能與修剪不相容

這些方法通常是:要麼是完全無效,或者是如果在修剪過的應用程式中使用它們,在某些情況下可能會出現問題。 好的範例是上一個範例中的 Type.GetType 方法。 在修剪過的應用程式中,它可能有效,但不能保證。 這類的 API 會標示為 RequiresUnreferencedCodeAttribute

RequiresUnreferencedCodeAttribute 簡單且廣泛:它是一個屬性,表示成員已被標註為與修剪不相容。 程式碼根本上與修剪不相容,或修剪相依性太複雜而無法向修剪器解釋時,就會使用這個屬性。 對於動態載入程式碼 (例如透過 LoadFrom(String))、列舉或搜尋應用程式或組件中的所有類型 (例如透過 GetType())、使用 C# dynamic 關鍵字或使用其他執行階段程式碼產生技術的方法,通常都是如此。 有一個範例如下:

[RequiresUnreferencedCode("This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead")]
void MethodWithAssemblyLoad()
{
    ...
    Assembly.LoadFrom(...);
    ...
}

void TestMethod()
{
    // IL2026: Using method 'MethodWithAssemblyLoad' which has 'RequiresUnreferencedCodeAttribute'
    // can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead.
    MethodWithAssemblyLoad();
}

RequiresUnreferencedCode 並不多。 最佳修正方式是完全避免在修剪時呼叫該方法,並使用與修剪相容的其他方法。

將功能標示為與修剪不相容

如果您正在撰寫一個程式庫,而且您無法控制是否要使用不相容的功能,則可以使用 RequiresUnreferencedCode 來標記它。 這會將您的方法標註為與修剪不相容。 使用 RequiresUnreferencedCode 會在指定的方法中抑制所有修剪警告,但每當其他人呼叫它時都會產生警告。

RequiresUnreferencedCodeAttribute 會要求您指定一個 Message。 該訊息會顯示為向呼叫該標記方法的開發人員報告的警告的一部分。 例如:

IL2026: Using member <incompatible method> which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. <The message value>

對於上面的範例,特定方法的警告可能如下所示:

IL2026: Using member 'MethodWithAssemblyLoad()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead.

呼叫此類 API 的開發人員通常不會對受影響的 API 的細節或它與修剪相關的細節感興趣。

一則好的訊息應該說明哪個功能與修剪不相容,然後引導開發人員後續可能採取的步驟。 它可能建議使用不同的功能或更改功能的使用方式。 它也可能只是簡單地指出,如果沒有明確的替代方案,該功能尚未與修剪相容。

如果給開發人員的指引太長而無法包含在警告訊息中,您可以對 RequiresUnreferencedCodeAttribute 新增一個選用的 Url,以將開發人員指向一個更詳細描述問題和可能的解決方案的網頁。

例如:

[RequiresUnreferencedCode("This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead", Url = "https://site/trimming-and-method")]
void MethodWithAssemblyLoad() { ... }

這會產生一則警告:

IL2026: Using member 'MethodWithAssemblyLoad()' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. This functionality is not compatible with trimming. Use 'MethodFriendlyToTrimming' instead. https://site/trimming-and-method

由於相同的原因,使用 RequiresUnreferencedCode 通常會導致用它來標記更多方法。 當高階方法因呼叫與修剪不相容的低階方法而變得與修剪不相容的情況,這是很常見的。 您可以將警告像「冒泡」一樣向上傳遞到一個公用 API。 每次使用 RequiresUnreferencedCode 都需要一則訊息,在這些情況下,訊息可能都是相同的。 為了避免重複撰寫字串並使其更易於維護,可使用常數字串欄位來儲存該訊息:

class Functionality
{
    const string IncompatibleWithTrimmingMessage = "This functionality is not compatible with trimming. Use 'FunctionalityFriendlyToTrimming' instead";

    [RequiresUnreferencedCode(IncompatibleWithTrimmingMessage)]
    private void ImplementationOfAssemblyLoading()
    {
        ...
    }

    [RequiresUnreferencedCode(IncompatibleWithTrimmingMessage)]
    public void MethodWithAssemblyLoad()
    {
        ImplementationOfAssemblyLoading();
    }
}

對其輸入有要求的功能

修剪會提供 API 來指定對方法和其他成員的輸入的更多要求,從而產生與修剪相容的程式碼。 這些要求通常與反射以及存取類型上的某些成員或作業的能力有關。 這類要求是使用 DynamicallyAccessedMembersAttribute 指定的。

不同於 RequiresUnreferencedCode,只要正確標註修剪器,修剪器有時可理解反映。 來看看原始範例:

string s = Console.ReadLine();
Type type = Type.GetType(s);
foreach (var m in type.GetMethods())
{
    Console.WriteLine(m.Name);
}

在前面的範例中,真正的問題是 Console.ReadLine()。 因為可能讀取「任何」型別,修剪器無法知道您是否需要 System.DateTimeSystem.Guid 或任何其他型別的方法。 另一方面,下列程式碼正常:

Type type = typeof(System.DateTime);
foreach (var m in type.GetMethods())
{
    Console.WriteLine(m.Name);
}

修剪器可在此看到參考的確切型別:System.DateTime。 修剪器現在可以使用流程分析,判斷需要在 System.DateTime 上保留的所有公用方法。 那麼 DynamicallyAccessMembers 用於何處? 反映分於多個方法時。 在下面的程式碼中,我們可以看到類型 System.DateTime 流向 Method3,其中使用反射來存取 System.DateTime 的方法,

void Method1()
{
    Method2<System.DateTime>();
}
void Method2<T>()
{
    Type t = typeof(T);
    Method3(t);
}
void Method3(Type type)
{
    var methods = type.GetMethods();
    ...
}

如果您編譯上面的程式碼,則會產生下列警告:

IL2070:Program.Method3(Type):'this' 引數不符合呼叫 'System.Type.GetMethods()' 中的 'DynamicallyAccessedMemberTypes.PublicMethods'。 'Program.Method3(Type)' 方法的 'type' 參數沒有相符的註釋。 來源值宣告的需求,至少須與其獲指派目標位置上所宣告的需求相符。

為了效能和穩定性,不會在方法之間執行流程分析,因此需要註釋,才能將方法之間的資訊從反映呼叫 (GetMethods) 傳遞至 Type。 在上一個範例中,修剪器警告指出 GetMethods 需要在其上遭呼叫的 Type 物件執行個體,才能有 PublicMethods 註釋,但 type 變數沒有相同需求。 換句話說,我們必須將需求從 GetMethods 傳遞給呼叫者:

void Method1()
{
    Method2<System.DateTime>();
}
void Method2<T>()
{
    Type t = typeof(T);
    Method3(t);
}
void Method3(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    var methods = type.GetMethods();
  ...
}

在標註 type 參數後,原始警告就會消失,但會顯示另一個警告:

IL2087:'type' 引數不符合 'Program.Method3(Type)' 呼叫中的 'DynamicallyAccessedMemberTypes.PublicMethods'。 'Program.Method2<T>()' 的泛型參數 'T' 沒有相符的註釋。

我們將註釋傳播至 Method3type 參數,Method2 中也有類似問題。 修剪器能夠在流經 typeof 呼叫、指派給區域變數 t,以及傳遞至 Method3 時,追蹤 T 值。 此時,它會看到 type 參數需要 PublicMethods,但對 T 沒有任何需求,並產生新的警告。 若要修正此問題,我們必須「標註並傳播」,方法是在整條呼叫鏈結上套用註釋,直到我們觸達靜態已知型別 (如 System.DateTimeSystem.Tuple) 或其他註解的值為止。 在此情況下,我們必須標註 Method2 的型別參數 T

void Method1()
{
    Method2<System.DateTime>();
}
void Method2<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] T>()
{
    Type t = typeof(T);
    Method3(t);
}
void Method3(
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    var methods = type.GetMethods();
  ...
}

現在沒有警告,因為修剪器知道可以透過執行階段反射 (公用方法) 及在哪些型別 (System.DateTime) 上存取哪些成員,並且加以保留。 最佳做法是新增註釋,以便修剪器知道要保留的項目。

如果受影響的程式碼位於具有 RequiresUnreferencedCode 的方法中,則由這些額外要求所產生的警告會自動加以隱藏。

不同於 RequiresUnreferencedCode (它只會報告不相容性),新增 DynamicallyAccessedMembers 可讓程式碼與修剪相容。

隱藏修剪器警告

如果您能以某種方式判斷該呼叫是安全的,而且不會修剪所有需要的程式碼,您也可以使用 UnconditionalSuppressMessageAttribute,來隱藏警告。 例如:

[RequiresUnreferencedCode("Use 'MethodFriendlyToTrimming' instead")]
void MethodWithAssemblyLoad() { ... }

[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
    Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
void TestMethod()
{
    InitializeEverything();

    MethodWithAssemblyLoad(); // Warning suppressed

    ReportResults();
}

警告

隱藏修剪警告時要非常小心。 呼叫現在可能與修剪相容,但您會變更可能變更的程式碼,且可能會忘記查看所有隱藏。

UnconditionalSuppressMessage 就像 SuppressMessage,但 publish 和其他建置後工具可以看到。

重要

請勿使用 SuppressMessage#pragma warning disable 來隱藏修剪器警告。 這些僅適用於編譯器,但不會保留在編譯過的組件中。 修剪器會在編譯過的組件上運作,不會看到隱藏。

隱藏會套用至整個方法主體中。 因此,在我們上面的範例中,它會隱藏來自該方法的所有 IL2026 警告。 這使得理解變得更加困難,因為除非您新增註解,否則不清楚哪個方法有問題。 更重要的是,如果程式碼將來發生變更 (例如若 ReportResults 也變得與修剪不相容的話),則不會針對此方法呼叫報告任何警告。

您可以透過將有問題的方法呼叫重構為個別的方法或區域函式,然後僅對該方法套用隱藏來解決此問題:

void TestMethod()
{
    InitializeEverything();

    CallMethodWithAssemblyLoad();

    ReportResults();

    [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode",
        Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
    void CallMethodWithAssemblyLoad()
    {
        MethodWIthAssemblyLoad(); // Warning suppressed
    }
}