トリミングの警告の概要

概念的には、トリミングはシンプルです。アプリケーションを発行すると、.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. 機能がトリミングと互換性がありません
  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 を使用すると、指定されたメソッド内のすべてのトリミング警告が抑制されますが、他のユーザーが呼び出すと警告が生成されます。

RequiresUnreferencedCodeAttributeMessage を指定することが必要です。 メッセージは、マークされたメソッドを呼び出す開発者に報告される警告の一部として表示されます。 次に例を示します。

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 の詳細や詳細には関心がありません。

適切なメッセージは、トリミングと互換性のない機能であることを示し、次に取るべきステップの選択肢を開発者に案内する必要があります。 別の機能を使用するか、機能の使用方法を変更することを推奨するかもしれません。 または、単純に、機能がまだトリミングと互換性がなく、明らかに置き換えられる機能がないと示す場合もあります。

開発者へのガイダンスが長すぎて警告メッセージに含まれない場合は、問題を説明し、解決策の選択肢をより詳細に説明する Web ページに開発者を導くように、オプションの UrlRequiresUnreferencedCodeAttribute に追加することもできます。

次に例を示します。

[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): 'System.Type.GetMethods()' への呼び出しで 'this' 引数が 'DynamicallyAccessedMemberTypes.PublicMethods' を満たしていません。 メソッド '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' 引数は 'Program.Method3(Type)' の呼び出しで 'DynamicallyAccessedMemberTypes.PublicMethods' を満たしません。 'Program.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) がトリマーで認識され、保存されるため、警告はこれでなくなります。 トリマーが保持する内容を認識できるように、注釈を追加することをお勧めします。

これらの追加の要件によって生成される警告は、影響を受けるコードが 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();
}

警告

トリミングの警告を抑制するときは十分に注意してください。 その呼び出しが現在はトリミング対応でも、コードを変更したときにトリミング対応ではなくなり、さらにすべての抑制を確認し忘れる可能性があります。

UnconditionalSuppressMessageSuppressMessage と似たものですが、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
    }
}