トリミング用に .NET ライブラリを準備する
.NET SDK を使用すると、トリミングによって自己完結型アプリのサイズを縮小できます。 トリミングにより、使用されていないコードをアプリとその依存関係から削除できます。 すべてのコードにトリミングとの互換性があるわけではありません。 .NET には、トリミングされたアプリを中断する可能性があるパターンを検出するためのトリミング分析の警告が表示されます。 この記事の内容は次のとおりです。
- トリミング用にライブラリを準備する方法について説明します。
- 一般的なトリミングに関する警告を解決するための推奨事項を指定します。
前提条件
.NET 8 SDK 以降。
ライブラリのトリミングの警告を有効にする
ライブラリのトリミング警告は、以下のいずれかの方法で見つけることができます。
IsTrimmable
プロパティを使用してプロジェクト固有のトリミングを有効にします。- ライブラリを使用するトリミング テスト アプリを作成し、テスト アプリのトリミングを有効にします。 ライブラリのすべての API を参照する必要はありません。
両方のアプローチの使用をお勧めします。 プロジェクト固有のトリミングは便利で、1 つのプロジェクトに対するトリミングの警告を表示できますが、すべての警告を表示するためにトリミング対応とマークされている参照に依存します。 テスト アプリのトリミングには手間がかかりますが、すべての警告が表示されます。
プロジェクト固有のトリミングを有効にする
プロジェクト ファイルで <IsTrimmable>true</IsTrimmable>
を設定します。
<PropertyGroup>
<IsTrimmable>true</IsTrimmable>
</PropertyGroup>
MSBuild プロパティ IsTrimmable
を true
に設定すると、アセンブリが "トリミング可能" とマークされ、トリミング警告が有効になります。 "トリミング可能" は、以下のプロジェクトを意味します。
- トリミングとの互換性があると見なされます。
- ビルド時にトリミング関連の警告が生成されないようにしてください。 トリミングされたアプリで使用されたとき、最終的な出力において、アセンブリではその未使用のメンバーがトリミングされます。
IsTrimmable
プロパティは、AOT が <IsAotCompatible>true</IsAotCompatible>
と互換性があるとしてプロジェクトを構成するときに、既定で true
になります。 詳細については、「AOT 互換性アナライザー」を参照してください。
プロジェクトをトリミング互換としてマークせずにトリミング警告を生成するには、<IsTrimmable>true</IsTrimmable>
ではなく <EnableTrimAnalyzer>true</EnableTrimAnalyzer>
を使用します。
テスト アプリですべての警告を表示する
ライブラリのすべての解析警告を表示するには、ライブラリの実装と、ライブラリで使用するすべての依存関係の実装をトリマーで解析する必要があります。
以下のライブラリのビルドおよび公開時。
- 依存関係の実装は使用できません。
- 使用可能な参照アセンブリには、トリマーがトリミングに互換性があるかどうかを判断するのに十分な情報がありません。
依存関係に制限があるため、ライブラリとその依存関係を使用する自己完結型のテスト アプリを作成する必要があります。 テスト アプリには、トリマーがトリミングの非互換性について警告を発するために必要なすべての情報が含まれています。
- ライブラリ コード。
- ライブラリで依存関係から参照されるコード。
Note
ターゲット フレームワークに依存してライブラリの動作が異なる場合は、トリミングをサポートするターゲット フレームワークごとにトリミング テスト アプリを作成します。 たとえば、ライブラリが #if NET7_0
などの条件付きコンパイルを使用して動作を変える場合があります。
トリミング テスト アプリを作成するには、次のようにします。
- 分離コンソール アプリケーション プロジェクトを作成します。
- ライブラリへの参照を追加します。
- 以下のリストを使用して、以下の図に示すようなプロジェクトと同様のプロジェクトを変更します。
net472
や netstandard2.0
のように、ライブラリでトリミングできない TFM がターゲットである場合、トリミング テスト アプリを作成するメリットはありません。 トリミングは .NET 6 以降でのみサポートされています。
<PublishTrimmed>true</PublishTrimmed>
を追加します。<ProjectReference Include="/Path/To/YourLibrary.csproj" />
を使用してライブラリ プロジェクトに参照を追加します。<TrimmerRootAssembly Include="YourLibraryName" />
を使ってトリマー ルート アセンブリとしてライブラリを指定します。TrimmerRootAssembly
では、ライブラリのすべての部分が分析されるようになります。 これにより、トリマーにこのアセンブリが "ルート" であることを伝達します。 "ルート" アセンブリとは、トリマーがライブラリ内のすべての呼び出しを分析し、そのアセンブリを起点とするすべてのコード パスを横断することを意味します。
.csproj ファイル
<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>
複数のライブラリに対して前述のパターンに従ってください。 一度に複数のライブラリのトリミング分析の警告を表示するには、ProjectReference
項目および TrimmerRootAssembly
項目と同じプロジェクトにそのすべてを追加します。 すべてのライブラリを ProjectReference
および TrimmerRootAssembly
の項目で同じプロジェクトに追加すると、ルート ライブラリのいずれかで依存関係にトリミングに対応していない API が使用されている場合に、依存関係に関する警告が表示されます。 特定のライブラリだけに関係する警告を表示するには、そのライブラリのみを参照します。
Note
分析結果は、依存関係の実装の詳細によって異なります。 依存関係の新しいバージョンに更新すると、分析の警告が発生する可能性があります。
- 新しいバージョンに理解できない反射パターンが追加された場合。
- 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 で永続化され、トリミング分析中に考慮されます。
警告
警告を抑制する場合は、検査とテストによって true であることがわかっている不変条件に基づいて、コードのトリミングの互換性を保証する責任があります。 これらの注釈は、正しくない場合、またはコードの不変条件が変更された場合に、誤ったコードがわからなくなる可能性があるため、注意してください。
次に例を示します。
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
の呼び出しで警告が表示されなくなります。 インデクサー セッターの注釈により、Type[]
に格納されているすべての型にコンストラクターが含まれるようになります。 ただし、分析ではこれを確認できず、ゲッターの警告が生成されます。戻り値の型のコンストラクターが保持されていることがわからないためです。
要件が満たされていることが確実な場合は、ゲッターに [UnconditionalSuppressMessage]
を追加することで、この警告をサイレント状態にすることができます。
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
がないと、トリミングによって、Helper
が MyAssembly
から削除されたり、MyAssembly
が他の場所で参照されていない場合に完全に削除されたりして、実行時に障害が発生する可能性があることを示す警告が表示される場合があります。 この属性を使用することで、Helper
が確実に保持されます。
この属性では、string
または DynamicallyAccessedMemberTypes
を介して保持するメンバーを指定します。 型とアセンブリは、属性コンテキスト内で暗黙的に指定されるか、または属性内で明示的に指定されます (Type
によって、または型とアセンブリ名の場合は string
によって)。
型とメンバーの文字列では、C# ドキュメントのコメント ID 文字列の形式 のバリエーションを、メンバー プレフィックスなしで使用します。 メンバー文字列には宣言する型の名前を含めず、指定した名前のすべてのメンバーを保持するためにパラメーターを省略する場合があります。 形式の例を次のコードに示します。
[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
を使用しても分析できないリフレクション パターンがメソッドに含まれている場合に使用するように設計されています。
.NET