準備 .NET 程式庫以進行修剪

.NET SDK 可讓您藉由修剪來減少獨立式應用程式的大小。 修剪會從應用程式及其相依性中移除未使用的程式碼。 並非所有程式碼都能與修剪相容。 .NET 會提供修剪分析警告,以偵測可能會中斷所修剪應用程式的模式。 本文:

必要條件

.NET 6 SDK 或更新版本。

若要取得最新的修剪警告和分析器涵蓋範圍:

  • 安裝並使用 .NET 8 SDK 或更新版本。
  • 目標 net8.0 或更新版本。

.NET 7 SDK 或更新版本。

若要取得最新的修剪警告和分析器涵蓋範圍:

  • 安裝並使用 .NET 8 SDK 或更新版本。
  • 目標 net8.0 或更新版本。

.NET 8 SDK 或更新版本。

啟用程式庫修剪警告

您可以透過下列任一方法找到程式庫中的修剪警告:

  • 使用 IsTrimmable 屬性啟用專案特定的修剪。
  • 建立會使用程式庫的修剪測試應用程式,並為測試應用程式啟用修剪。 不需要參考程式庫中的所有 API。

我們的建議是兩種方法都使用。 專案特定的修剪很方便,並顯示一個專案的修剪警告,但依賴標示為修剪相容的參考來查看所有警告。 修剪測試應用程式需要進行更多工作,但會顯示所有警告。

啟用專案專用的修剪

在專案檔中設定 <IsTrimmable>true</IsTrimmable>

<PropertyGroup>
    <IsTrimmable>true</IsTrimmable>
</PropertyGroup>

將 MSBuild 屬性 IsTrimmable 設定為 true 會將組件標示為「可修剪」,並啟用修剪警告。 「可修剪」表示專案:

  • 會視為與修剪相容。
  • 不應在建置時產生修剪相關的警告。 在已修剪的應用程式中使用時,組件會在最終輸出中修剪其未使用的成員。

使用 <IsAotCompatible>true</IsAotCompatible> 將專案設定為 AOT 相容時,IsTrimmable 屬性會預設為 true。 如需詳細資訊,請參閱 AOT 相容性分析器

若要在不將專案標示為修剪相容的情況下產生修剪警告,請使用 <EnableTrimAnalyzer>true</EnableTrimAnalyzer> 而非 <IsTrimmable>true</IsTrimmable>

使用測試應用程式顯示所有警告

若要顯示程式庫的所有分析警告,修剪器必須分析程式庫的實作,以及程式庫所用一切相依性的實作。

在建置和發佈程式庫時:

  • 相依性的實作無法使用。
  • 可用的參考組件沒有足夠的資訊可讓修剪器判斷其是否與修剪相容。

由於相依性的限制,因此必須建立使用程式庫及其相依性的獨立式測試應用程式。 測試應用程式包含修剪器在遇到修剪不相容時要發出警告所需的一切資訊:

  • 程式庫程式碼。
  • 程式庫從其相依性參考的程式碼。

注意

如果程式庫會根據目標 Framework 而有不同的行為,請針對支援修剪的每個目標 Framework 建立修剪測試應用程式。 例如,如果程式庫會使用 #if NET7_0 之類的條件式編譯來變更行為的話。

若要建立修剪測試應用程式:

  • 建立個別的主控台應用程式專案。
  • 新增程式庫的參考。
  • 針對下面所示的專案,使用下列清單修改與其類似的專案:

如果程式庫以無法修剪的 TFM 為目標 (例如 net472netstandard2.0),則建立修剪測試應用程式沒有任何好處。 只有 .NET 6 和更新版本才支援修剪。

  • <TrimmerDefaultAction> 設定為 link
  • 加入 <PublishTrimmed>true</PublishTrimmed>
  • 使用 <ProjectReference Include="/Path/To/YourLibrary.csproj" /> 新增程式庫專案的參考。
  • 使用 <TrimmerRootAssembly Include="YourLibraryName" /> 將程式庫指定為修剪器根組件。
    • TrimmerRootAssembly 可確保程式庫的每個部分都有經過分析。 其會告訴修剪器這個組件是「根」組件。 「根」組件表示修剪器會分析程式庫中的每個呼叫,並周遊源自該組件的所有程式碼路徑。
  • 加入 <PublishTrimmed>true</PublishTrimmed>
  • 使用 <ProjectReference Include="/Path/To/YourLibrary.csproj" /> 新增程式庫專案的參考。
  • 使用 <TrimmerRootAssembly Include="YourLibraryName" /> 將程式庫指定為修剪器根組件。
    • TrimmerRootAssembly 可確保程式庫的每個部分都有經過分析。 其會告訴修剪器這個組件是「根」組件。 「根」組件表示修剪器會分析程式庫中的每個呼叫,並周遊源自該組件的所有程式碼路徑。
  • 加入 <PublishTrimmed>true</PublishTrimmed>
  • 使用 <ProjectReference Include="/Path/To/YourLibrary.csproj" /> 新增程式庫專案的參考。
  • 使用 <TrimmerRootAssembly Include="YourLibraryName" /> 將程式庫指定為修剪器根組件。
    • TrimmerRootAssembly 可確保程式庫的每個部分都有經過分析。 其會告訴修剪器這個組件是「根」組件。 「根」組件表示修剪器會分析程式庫中的每個呼叫,並周遊源自該組件的所有程式碼路徑。

.csproj 檔案

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <PublishTrimmed>true</PublishTrimmed>
    <!-- Prevent warnings from unused code in dependencies -->
    <TrimmerDefaultAction>link</TrimmerDefaultAction>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="path/to/MyLibrary.csproj" />
    <!-- Analyze the whole library, even if attributed with "IsTrimmable" -->
    <TrimmerRootAssembly Include="MyLibrary" />
  </ItemGroup>

</Project>
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <PublishTrimmed>true</PublishTrimmed>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
    <TrimmerRootAssembly Include="MyLibrary" />
  </ItemGroup>

</Project>

注意:在上述專案檔中使用 .NET 7 時,請將 <TargetFramework>net8.0</TargetFramework> 取代為 <TargetFramework>net7.0</TargetFramework>

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <PublishTrimmed>true</PublishTrimmed>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
    <TrimmerRootAssembly Include="MyLibrary" />
  </ItemGroup>

</Project>

專案檔更新後,請使用目標執行階段識別碼 (RID) 執行 dotnet publish

dotnet publish -c Release -r <RID>

針對多個程式庫遵循上述模式。 若要查看一次修剪多個程式庫的分析警告,請將全部新增至與 ProjectReferenceTrimmerRootAssembly 項目相同的專案。 如果有任何根程式庫在相依性中使用不利於進行修剪的 API,則使用 ProjectReferenceTrimmerRootAssembly 項目將所有程式庫新增至相同專案時會發出關於相依性的警告。 若要查看只有特定程式庫相關的警告,請只參考該程式庫。

注意:分析結果取決於相依性的實作詳細資料。 更新為新版相依性可能會引入分析警告:

  • 如果新版本新增了無法理解的反映模式的話。
  • 即使未變更 API 也一樣。
  • 當程式庫與 PublishTrimmed 搭配使用時,引入修剪分析警告屬於中斷性變更。

解決修剪警告

上述步驟會產生有關程式碼的警告,這些警告可能會在修剪的應用程式中使用時造成問題。 下列範例顯示最常見的警告,以及用於修正這些警告的建議。

RequiresUnreferencedCode

請考慮下列程式碼,其使用 [RequiresUnreferencedCode] 來表示所指定方法需要動態存取不是以靜態方式參考的程式碼,例如透過 System.Reflection

public class MyLibrary
{
    public static void MyMethod()
    {
        // warning IL2026 :
        // MyLibrary.MyMethod: Using 'MyLibrary.DynamicBehavior'
        // which has [RequiresUnreferencedCode] can break functionality
        // when trimming app code.
        DynamicBehavior();
    }

    [RequiresUnreferencedCode(
        "DynamicBehavior is incompatible with trimming.")]
    static void DynamicBehavior()
    {
    }
}

上面醒目提示的程式碼表示程式庫會呼叫明確註釋為與修剪不相容的方法。 若要移除警告,請考慮 MyMethod 是否需要呼叫 DynamicBehavior。 如果是的話,請為呼叫端 MyMethod 加上 [RequiresUnreferencedCode] 註釋,這會傳播警告,反而讓 MyMethod 的呼叫端收到警告:

public class MyLibrary
{
    [RequiresUnreferencedCode("Calls DynamicBehavior.")]
    public static void MyMethod()
    {
        DynamicBehavior();
    }

    [RequiresUnreferencedCode(
        "DynamicBehavior is incompatible with trimming.")]
    static void DynamicBehavior()
    {
    }
}

一路將屬性傳播到公用 API 後,呼叫程式庫的應用程式:

  • 只會針對無法修剪的公用方法收到警告。
  • 不會收到類似 IL2104: Assembly 'MyLibrary' produced trim warnings 的警告。

DynamicallyAccessedMembers

public class MyLibrary3
{
    static void UseMethods(Type type)
    {
        // warning IL2070: MyLibrary.UseMethods(Type): 'this' argument does not satisfy
        // 'DynamicallyAccessedMemberTypes.PublicMethods' in call to
        // 'System.Type.GetMethods()'.
        // The parameter 't' of method 'MyLibrary.UseMethods(Type)' doesn't have
        // matching annotations.
        foreach (var method in type.GetMethods())
        {
            // ...
        }
    }
}

在上述程式碼中,UseMethods 會呼叫具有 [DynamicallyAccessedMembers] 需求的反映方法。 需求指出該類型的公用方法可供使用。 請將相同的需求新增至 UseMethods 的參數,以滿足需求。

static void UseMethods(
   // State the requirement in the UseMethods parameter.
   [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
    // ...
}

現在,只要 UseMethods 的呼叫所傳入的值未滿足 PublicMethods 需求,便會產生警告。 與 [RequiresUnreferencedCode] 類似,將這類警告傳播至公用 API 後,便大功告成。

在下列範例中,未知的 Type 會流入已註釋的方法參數。 未知的 Type 來自欄位:

static Type type;
static void UseMethodsHelper()
{
    // warning IL2077: MyLibrary.UseMethodsHelper(Type): 'type' argument does not satisfy
    // 'DynamicallyAccessedMemberTypes.PublicMethods' in call to
    // 'MyLibrary.UseMethods(Type)'.
    // The field 'System.Type MyLibrary::type' does not have matching annotations.
    UseMethods(type);
}

同樣地,此處的問題在於欄位 type 會傳遞至具有這些需求的參數。 將 [DynamicallyAccessedMembers] 新增至該欄位,即可修正此問題。 [DynamicallyAccessedMembers] 會針對有關將不相容值指派給欄位的程式碼發出警告。 有時候,此程序會繼續,直到公用 API 加上註釋為止,而其他時候,當具象類型流向具有這些需求的位置時,此程序便會結束。 例如:

[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)]
static Type type;

static void UseMethodsHelper()
{
    MyLibrary.type = typeof(System.Tuple);
}

在此案例中,修剪分析會保留 Tuple 的公用方法,並產生進一步的警告。

建議

  • 盡量避免反映。 使用反映時,請盡量縮小反映範圍,以便只能從程式庫的一小部分進行連線。
  • 為程式碼加上 DynamicallyAccessedMembers 註釋,盡量以靜態方式表達修剪需求。
  • 考慮重新組織程式碼,使其遵循能加上 DynamicallyAccessedMembers 註釋的可分析模式
  • 當程式碼與修剪不相容時,請為其加上 RequiresUnreferencedCode 註釋,並將此註釋傳播給呼叫端,直到相關的公用 API 加上註釋為止。
  • 所使用的程式碼應避免以靜態分析無法理解的方式使用反映。 例如,應避免在靜態建構函式中使用反映。 在靜態建構函式中使用以靜態方式無法分析的反映,會導致警告傳播給該類別的所有成員。
  • 避免為虛擬方法或介面方法加上註釋。 要為虛擬方法或介面方法加上註釋,就必須讓所有覆寫有相符的註釋。
  • 如果 API 大多與修剪不相容,則可能需要考慮使用替代的 API 編碼方法。 常見的範例是以反映為基礎的序列化程式。 在這些情況下,請考慮採用其他技術,例如來源產生器來產生更能容易靜態分析的程式碼。 例如,請參閱如何在 System.Text.Json 中使用來源產生

解決無法分析模式的警告

最好盡可能使用 [RequiresUnreferencedCode]DynamicallyAccessedMembers 表達程式碼的意圖來解決警告。 不過,在某些情況下,針對所使用的模式無法以這些屬性加以表達的程式庫,您可能會想要為其啟用修剪,或是不重構現有程式碼就為其啟用修剪。 本節說明解決修剪分析警告的一些進階方法。

警告

若未正確使用,這些技術可能會變更行為或您的程式碼,或導致執行階段例外狀況。

UnconditionalSuppressMessage

請考慮下列程式碼:

  • 無法以註釋表達意圖。
  • 產生警告,但不代表執行階段真的發生問題。

警告可以隱藏起來 UnconditionalSuppressMessageAttribute。 這與 SuppressMessageAttribute 類似,但會在 IL 中保存,並在修剪分析期間受到尊重。

警告

隱藏警告時,您必須負責根據在檢查和測試後知道為真的非變異值,保證程式碼的修剪相容性。 請謹慎使用這些註釋,因為如果這些註釋不正確,或是程式碼的非變異值出現變更,最後可能會隱藏不正確的程式碼。

例如:

class TypeCollection
{
    Type[] types;

    // Ensure that only types with preserved constructors are stored in the array
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
    public Type this[int i]
    {
        // warning IL2063: TypeCollection.Item.get: Value returned from method
        // 'TypeCollection.Item.get' can't be statically determined and may not meet
        // 'DynamicallyAccessedMembersAttribute' requirements.
        get => types[i];
        set => types[i] = value;
    }
}

class TypeCreator
{
    TypeCollection types;

    public void CreateType(int i)
    {
        types[i] = typeof(TypeWithConstructor);
        Activator.CreateInstance(types[i]); // No warning!
    }
}

class TypeWithConstructor
{
}

在上述程式碼中,索引子屬性已加上註釋,以便傳回的 Type 符合 CreateInstance 的需求。 這可確保 TypeWithConstructor 建構函式會保留下來,而且 CreateInstance 的呼叫不會發出警告。 索引子 setter 註釋可確保 Type[] 中儲存的任何類型都有建構函式。 不過,分析無法看到此狀況,並且會產生 getter 的警告,因為其不知道傳回的類型已保留其建構函式。

如果確定有符合需求,便可藉由將 [UnconditionalSuppressMessage] 新增至 getter 來消除此警告:

class TypeCollection
{
    Type[] types;

    // Ensure that only types with preserved constructors are stored in the array
    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)]
    public Type this[int i]
    {
        [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2063",
            Justification = "The list only contains types stored through the annotated setter.")]
        get => types[i];
        set => types[i] = value;
    }
}

class TypeCreator
{
    TypeCollection types;

    public void CreateType(int i)
    {
        types[i] = typeof(TypeWithConstructor);
        Activator.CreateInstance(types[i]); // No warning!
    }
}

class TypeWithConstructor
{
}

請務必強調,只有在有註釋或程式碼可確保反映的成員是可見的反映目標時,才能有效隱藏警告。 成員是呼叫、欄位或屬性存取的目標並不足夠。 有時可能看起來是這種情況,但隨中新增更多修剪最佳化,這類程式碼最終會中斷。 不是可見反映目標的屬性、欄位和方法可以內嵌、將其名稱移除、移至不同類型,或以會中斷其反映的方式進行最佳化。 隱藏警告時,只允許反映到其他地方修剪分析器可見的目標。

// Invalid justification and suppression: property being non-reflectively
// used by the app doesn't guarantee that the property will be available
// for reflection. Properties that are not visible targets of reflection
// are already optimized away with Native AOT trimming and may be
// optimized away for non-native deployment in the future as well.
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2063",
    Justification = "*INVALID* Only need to serialize properties that are used by"
                    + "the app. *INVALID*")]
public string Serialize(object o)
{
    StringBuilder sb = new StringBuilder();
    foreach (var property in o.GetType().GetProperties())
    {
        AppendProperty(sb, property, o);
    }
    return sb.ToString();
}

DynamicDependency

[DynamicDependency] 屬性可用來指出某個成員與其他成員具有動態相依性。 這會導致保留每當具有該屬性的成員時,都會保留參考的成員,但不會自行隱藏警告。 不同於會向修剪分析告知程式碼反映行為的其他屬性,[DynamicDependency] 只會保留其他成員。 這可以與 [UnconditionalSuppressMessage] 起使用來修正一些分析警告。

警告

只有當其他方法不可行時,才使用 [DynamicDependency] 屬性作為最後手段。 最好使用 [RequiresUnreferencedCode][DynamicallyAccessedMembers] 來表達反映行為。

[DynamicDependency("Helper", "MyType", "MyAssembly")]
static void RunHelper()
{
    var helper = Assembly.Load("MyAssembly").GetType("MyType").GetMethod("Helper");
    helper.Invoke(null, null);
}

如果沒有 DynamicDependency,如果沒有其他位置參照,修剪可能會從 MyAssembly 移除 Helper,或完全移除 MyAssembly,產生警告來指出執行時間可能發生失敗。 屬性可確保將保留 Helper

屬性會指定要透過 string 或透過 DynamicallyAccessedMemberTypes 保留的成員。 型別和組件在屬性內容中是隱含的,或在屬性中指定 (透過 Type 或透過 string 指定型別和組件名稱)。

型別和成員字串會使用 C# 檔註釋識別碼字串格式的變化,而不需要成員前置詞。 成員字串不應包含宣告類型的名稱,而且可能會省略參數來保留指定名稱的所有成員。 下列程式碼會顯示此格式的一些範例:

[DynamicDependency("MyMethod()")]
[DynamicDependency("MyMethod(System,Boolean,System.String)")]
[DynamicDependency("MethodOnDifferentType()", typeof(ContainingType))]
[DynamicDependency("MemberName")]
[DynamicDependency("MemberOnUnreferencedAssembly", "ContainingType"
                                                 , "UnreferencedAssembly")]
[DynamicDependency("MemberName", "Namespace.ContainingType.NestedType", "Assembly")]
// generics
[DynamicDependency("GenericMethodName``1")]
[DynamicDependency("GenericMethod``2(``0,``1)")]
[DynamicDependency(
    "MethodWithGenericParameterTypes(System.Collections.Generic.List{System.String})")]
[DynamicDependency("MethodOnGenericType(`0)", "GenericType`1", "UnreferencedAssembly")]
[DynamicDependency("MethodOnGenericType(`0)", typeof(GenericType<>))]

[DynamicDependency] 屬性的設計是要用在以下情況:方法所包含的反映模式無法分析,即使有 DynamicallyAccessedMembersAttribute 的協助也一樣。