トリミングの警告の概要

概念的には、トリミングはシンプルです。アプリケーションを発行する際に、.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 の呼び出しで警告が発行され、アプリケーションによってどの型が使用されるかを判断できないことが示されます。

トリミング警告への対応

トリミング警告は、トリミングに予測可能性をもたらすためのものです。 表示される可能性のある警告には、次の 2 つの大きなカテゴリがあります。

  1. RequiresUnreferencedCode
  2. DynamicallyAccessedMembers

RequiresUnreferencedCode

RequiresUnreferencedCodeAttribute はシンプルかつ広範です。これは、メンバーにトリミング非対応という注釈が付けられたことを意味する属性です。つまり、リフレクションや他の何らかのメカニズムを使用して、トリミングされるかもしれないコードにアクセスする可能性があるということです。 この属性は、コードが根本的にトリミング対応でない場合、またはトリミングの依存関係が複雑すぎてトリマーによって説明できない場合に使用されます。 これは多くの場合、C# dynamic キーワードを使用したり、LoadFrom(String) から型にアクセスしたり、またはその他のランタイム コード生成テクノロジを使用したりするメソッドに当てはまります。 例を次に示します:

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

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

RequiresUnreferencedCode の回避策はあまり多くありません。 最善の解決策は、トリミング時にそのメソッドの呼び出しを完全に避け、トリミング対応の他のものを使用することです。 ライブラリを記述していて、そのメソッドが呼び出されるかどうかを制御できない場合は、独自のメソッドに RequiresUnreferencedCode を追加することもできます。 これにより、トリミング対応でないという注釈をメソッドに付けることができます。 RequiresUnreferencedCode を追加すると、指定されたメソッドですべてのトリミング警告が抑制されますが、誰かが呼び出すたびに警告が生成されます。 このため、大抵は、警告をパブリック API に "バブリング" するとライブラリの作成者にとって便利です。

その呼び出しが安全であり、必要なすべてのコードがトリミングされないことを何らかの方法によって判断できる場合は、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()
{
    MethodWithAssemblyLoad(); // Warning suppressed
}

UnconditionalSuppressMessageSuppressMessage と似たものですが、publish やその他のビルド後のツールによって確認できます。 SuppressMessage ディレクティブと #pragma ディレクティブはソースにのみ存在します。そのため、トリマーからの警告を抑制するために使用することはできません。 トリミング警告を抑制する場合は十分に注意してください。その呼び出しが現在はトリミング対応でも、コードを変更したときにそれが変更される可能性があり、すべての抑制を確認するのを忘れる可能性もあります。

DynamicallyAccessedMembers

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: net6.Program.Method3(Type): 'System.Type.GetMethods()' への呼び出しで 'this' 引数が 'DynamicallyAccessedMemberTypes.PublicMethods' を満たしていません。 メソッド 'net6.Program.Method3(Type)' のパラメーター 'type' に一致する注釈がありません。 ソース値では、それが割り当てられているターゲットの場所で宣言されている要件と少なくとも同じ要件が宣言される必要があります。

パフォーマンスと安定性のため、フロー分析はメソッド間で実行されません。そのため、リフレクション呼び出し (GetMethods) から Type のソースへと、メソッド間で情報を渡すには、注釈が必要です。 前の例では、トリマーの警告によって、GetMethodsPublicMethods 注釈を与えるには、それが呼び出される Type オブジェクト インスタンスが必要だが、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' 引数は 'C.Method3(Type)' の呼び出しで 'DynamicallyAccessedMemberTypes.PublicMethods' を満たしません。 'C.Method2<T>()' のジェネリック パラメーター 'T' に、一致する注釈がありません。

Method3 のパラメーター type まで注釈を伝達しました。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) がトリマーで正式に認識され、保存されるため、警告はこれでなくなります。 一般に、これが DynamicallyAccessedMembers 警告に対処する最善の方法です。つまり、維持すべきものがトリマーにわかるように注釈を追加することです。

RequiresUnreferencedCode 警告と同様に、RequiresUnreferencedCode 属性または UnconditionalSuppressMessage 属性を追加しても警告は抑制されますが、コードがトリミング対応になるわけではありません。一方、DynamicallyAccessedMembers を追加すると、トリミング対応になります。