Pengantar peringatan pemangkasan

Secara konseptual, pemangkasan sederhana: ketika Anda menerbitkan aplikasi, .NET SDK menganalisis seluruh aplikasi dan menghapus semua kode yang tidak digunakan. Namun, mungkin sulit untuk menentukan apa yang tidak digunakan, atau lebih tepatnya, apa yang digunakan.

Untuk mencegah perubahan perilaku saat memangkas aplikasi, .NET SDK menyediakan analisis statis kompatibilitas pemangkasan melalui peringatan pemangkasan. Pemangkas menghasilkan peringatan pemangkasan ketika menemukan kode yang mungkin tidak kompatibel dengan pemangkasan. Kode yang tidak kompatibel dengan pemangkasan dapat menghasilkan perubahan perilaku, atau bahkan crash, dalam aplikasi setelah dipangkas. Idealnya, semua aplikasi yang menggunakan pemangkasan tidak boleh menghasilkan peringatan pemangkasan apa pun. Jika ada peringatan pemangkasan, aplikasi harus diuji secara menyeluruh setelah pemangkasan untuk memastikan bahwa tidak ada perubahan perilaku.

Artikel ini membantu Anda memahami mengapa beberapa pola menghasilkan peringatan pemangkasan, dan bagaimana peringatan ini dapat diatasi.

Contoh peringatan pemangkasan

Untuk sebagian besar kode C#, sangat mudah untuk menentukan kode apa yang digunakan dan kode apa yang tidak digunakan—pemangkas dapat memandu panggilan metode, referensi bidang dan properti, dan sebagainya, dan menentukan kode apa yang diakses. Sayangnya, beberapa fitur, seperti refleksi, menghadirkan masalah yang signifikan. Pertimbangkan gambar berikut:

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

Dalam contoh ini, GetType() secara dinamis meminta jenis dengan nama yang tidak diketahui, lalu mencetak nama semua metodenya. Karena tidak ada cara untuk mengetahui pada waktu publikasi nama jenis apa yang akan digunakan, tidak ada cara bagi pemangkas untuk mengetahui jenis mana yang harus dipertahankan dalam output. Kemungkinan kode ini dapat berfungsi sebelum pemangkasan (selama input adalah sesuatu yang diketahui ada dalam kerangka kerja target), tetapi mungkin akan menghasilkan pengecualian referensi null setelah pemangkasan, karena Type.GetType mengembalikan null ketika jenis tidak ditemukan.

Dalam hal ini, pemangkas mengeluarkan peringatan pada panggilan ke Type.GetType, yang menunjukkan bahwa itu tidak dapat menentukan jenis mana yang akan digunakan oleh aplikasi.

Bereaksi terhadap peringatan pemangkasan

Memangkas peringatan dimaksudkan untuk membawa prediksi ke pemangkasan. Ada dua kategori peringatan besar yang kemungkinan akan Anda lihat:

  1. Fungsionalitas tidak kompatibel dengan pemangkasan
  2. Fungsionalitas memiliki persyaratan tertentu pada input agar kompatibel dengan pemangkasan

Fungsionalitas tidak kompatibel dengan pemangkasan

Ini biasanya adalah metode yang tidak berfungsi sama sekali, atau mungkin rusak dalam beberapa kasus jika digunakan dalam aplikasi yang dipangkas. Contoh yang baik adalah Type.GetType metode dari contoh sebelumnya. Dalam aplikasi yang dipangkas mungkin berfungsi, tetapi tidak ada jaminan. API tersebut ditandai dengan RequiresUnreferencedCodeAttribute.

RequiresUnreferencedCodeAttribute sederhana dan luas: ini adalah atribut yang berarti anggota telah dianotasi tidak kompatibel dengan pemangkasan. Atribut ini digunakan ketika kode pada dasarnya tidak kompatibel dengan pemangkasan, atau dependensi pemangkasan terlalu kompleks untuk dijelaskan ke pemangkas. Ini sering kali berlaku untuk metode yang secara dinamis memuat kode misalnya melalui LoadFrom(String), menghitung atau mencari melalui semua jenis dalam aplikasi atau rakitan misalnya melalui GetType(), gunakan kata kunci C# dynamic , atau gunakan teknologi pembuatan kode runtime lainnya. Contohnya seperti:

[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();
}

Tidak ada banyak solusi untuk RequiresUnreferencedCode. Perbaikan terbaik adalah menghindari pemanggilan metode sama sekali saat memangkas dan menggunakan sesuatu yang lain yang kompatibel dengan pemangkasan.

Menandai fungsionalitas sebagai tidak kompatibel dengan pemangkasan

Jika Anda menulis pustaka dan tidak berada dalam kontrol Anda apakah menggunakan fungsionalitas yang tidak kompatibel atau tidak, Anda dapat menandainya dengan RequiresUnreferencedCode. Ini membuat anotasi metode Anda sebagai tidak kompatibel dengan pemangkasan. Menggunakan RequiresUnreferencedCode keheningan semua peringatan pemangkasan dalam metode tertentu, tetapi menghasilkan peringatan setiap kali orang lain memanggilnya.

RequiresUnreferencedCodeAttribute mengharuskan Anda menentukan Message. Pesan ditampilkan sebagai bagian dari peringatan yang dilaporkan kepada pengembang yang memanggil metode yang ditandai. Misalnya:

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

Dengan contoh di atas, peringatan untuk metode tertentu mungkin terlihat seperti ini:

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

Pengembang yang memanggil API tersebut umumnya tidak akan tertarik pada khususnya API yang terpengaruh atau spesifik karena berkaitan dengan pemangkasan.

Pesan yang baik harus menyatakan fungsionalitas apa yang tidak kompatibel dengan pemangkasan lalu memandu pengembang apa langkah berikutnya potensial mereka. Mungkin disarankan untuk menggunakan fungsionalitas yang berbeda atau mengubah cara fungsionalitas digunakan. Mungkin juga hanya menyatakan bahwa fungsionalitas belum kompatibel dengan pemangkasan tanpa penggantian yang jelas.

Jika panduan kepada pengembang menjadi terlalu lama untuk disertakan dalam pesan peringatan, Anda dapat menambahkan opsional Url ke RequiresUnreferencedCodeAttribute untuk mengarahkan pengembang ke halaman web yang menjelaskan masalah dan kemungkinan solusi secara lebih rinci.

Misalnya:

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

Ini menghasilkan peringatan:

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

Penggunaan RequiresUnreferencedCode sering menyebabkan menandai lebih banyak metode dengannya, karena alasan yang sama. Ini umum ketika metode tingkat tinggi menjadi tidak kompatibel dengan pemangkasan karena memanggil metode tingkat rendah yang tidak kompatibel dengan pemangkasan. Anda "menggelegak" peringatan ke API publik. Setiap penggunaan RequiresUnreferencedCode membutuhkan pesan, dan dalam kasus ini pesan kemungkinan sama. Untuk menghindari string duplikat dan membuatnya lebih mudah dipertahankan, gunakan bidang string konstanta untuk menyimpan pesan:

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();
    }
}

Fungsionalitas dengan persyaratan pada inputnya

Pemangkasan menyediakan API untuk menentukan lebih banyak persyaratan tentang input ke metode dan anggota lain yang mengarah ke kode yang kompatibel dengan pemangkasan. Persyaratan ini biasanya tentang refleksi dan kemampuan untuk mengakses anggota atau operasi tertentu pada jenis. Persyaratan tersebut DynamicallyAccessedMembersAttributeditentukan menggunakan .

Tidak seperti RequiresUnreferencedCode, refleksi terkadang dapat dipahami oleh pemangkas selama diannotasi dengan benar. Mari kita lihat contoh aslinya:

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

Dalam contoh sebelumnya, masalah sebenarnya adalah Console.ReadLine(). Karena jenis apa pun dapat dibaca, pemangkas tidak memiliki cara untuk mengetahui apakah Anda memerlukan metode pada System.DateTime atau System.Guid atau jenis lainnya. Di sisi lain, kode berikut akan baik-baik saja:

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

Di sini pemangkas dapat melihat jenis yang tepat yang dirujuk: System.DateTime. Sekarang dapat menggunakan analisis alur untuk menentukan bahwa ia perlu menjaga semua metode publik pada System.DateTime. Jadi di mana datang DynamicallyAccessMembers ? Ketika refleksi dibagi di beberapa metode. Dalam kode berikut, kita dapat melihat bahwa jenis System.DateTime mengalir ke Method3 tempat pantulan digunakan untuk mengakses System.DateTimemetode,

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

Jika Anda mengkompilasi kode sebelumnya, peringatan berikut akan dihasilkan:

IL2070: Program.Method3(Type): Argumen 'ini' tidak memenuhi 'DynamicallyAccessedMemberTypes.PublicMethods' dalam panggilan ke 'System.Type.GetMethods()'. Parameter 'type' metode 'Program.Method3(Type)' tidak memiliki anotasi yang cocok. Nilai sumber harus menyatakan setidaknya persyaratan yang sama dengan yang dinyatakan pada lokasi target tempatnya ditetapkan.

Untuk performa dan stabilitas, analisis alur tidak dilakukan di antara metode, sehingga anotasi diperlukan untuk meneruskan informasi antar metode, dari panggilan pantulan (GetMethods) ke sumber Type. Dalam contoh sebelumnya, peringatan pemangkas mengatakan bahwa GetMethods memerlukan instans Type objek yang dipanggil untuk memiliki PublicMethods anotasi, tetapi type variabel tidak memiliki persyaratan yang sama. Dengan kata lain, kita perlu meneruskan persyaratan dari GetMethods hingga pemanggil:

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();
  ...
}

Setelah menganotasi parameter type, peringatan asli menghilang, tetapi yang lain muncul:

IL2087: Argumen 'type' tidak memenuhi 'DynamicallyAccessedMemberTypes.PublicMethods' dalam panggilan ke 'Program.Method3(Type)'. Parameter generik 'T' dari 'Program.Method2<T>()' tidak memiliki anotasi yang cocok.

Kami menyebarkan anotasi hingga parameter typeMethod3, di Method2 kami memiliki masalah serupa. Pemangkas dapat melacak nilai T saat mengalir melalui panggilan ke typeof, ditetapkan ke variabel tlokal , dan diteruskan ke Method3. Pada saat itu melihat bahwa parameter type memerlukan PublicMethods tetapi tidak ada persyaratan pada T, dan menghasilkan peringatan baru. Untuk memperbaiki ini, kita harus "membuat anotasi dan menyebarkan" dengan menerapkan anotasi hingga rantai panggilan sampai kita mencapai jenis yang diketahui secara statis (seperti System.DateTime atau System.Tuple), atau nilai anotasi lainnya. Dalam hal ini, kita perlu membuat anotasi parameter TMethod2jenis .

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();
  ...
}

Sekarang tidak ada peringatan karena pemangkas tahu anggota mana yang mungkin diakses melalui refleksi runtime (metode publik) dan pada jenis mana (System.DateTime), dan mempertahankannya. Ini adalah praktik terbaik untuk menambahkan anotasi sehingga pemangkas tahu apa yang harus dipertahankan.

Peringatan yang dihasilkan oleh persyaratan tambahan ini secara otomatis ditekan jika kode yang terpengaruh berada dalam metode dengan RequiresUnreferencedCode.

Tidak seperti RequiresUnreferencedCode, yang hanya melaporkan ketidakcocokan, menambahkan DynamicallyAccessedMembers membuat kode kompatibel dengan pemangkasan.

Menyembunyikan peringatan pemangkas

Jika Anda entah bagaimana dapat menentukan bahwa panggilan aman, dan semua kode yang diperlukan tidak akan dipangkas, Anda juga dapat menekan peringatan menggunakan UnconditionalSuppressMessageAttribute. Misalnya:

[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();
}

Peringatan

Berhati-hatilah saat menekan peringatan pemangkasan. Ada kemungkinan bahwa panggilan mungkin kompatibel dengan trim sekarang, tetapi saat Anda mengubah kode Yang mungkin berubah, dan Anda mungkin lupa meninjau semua supresi.

UnconditionalSuppressMessage seperti SuppressMessage tetapi dapat dilihat oleh publish dan alat pasca-build lainnya.

Penting

Jangan gunakan SuppressMessage atau #pragma warning disable untuk menekan peringatan pemangkas. Ini hanya berfungsi untuk pengkompilasi, tetapi tidak dipertahankan dalam rakitan yang dikompilasi. Pemangkas beroperasi pada rakitan yang dikompilasi dan tidak akan melihat penindasan.

Penindasan berlaku untuk seluruh isi metode. Jadi dalam sampel kami di atas itu menekan semua IL2026 peringatan dari metode . Ini membuatnya lebih sulit untuk dipahami, karena tidak jelas metode mana yang bermasalah, kecuali Anda menambahkan komentar. Lebih penting lagi, jika kode berubah di masa depan, seperti jika ReportResults menjadi tidak kompatibel dengan trim juga, tidak ada peringatan yang dilaporkan untuk panggilan metode ini.

Anda dapat mengatasinya dengan merefaktor panggilan metode bermasalah ke dalam metode terpisah atau fungsi lokal dan kemudian menerapkan penindasan hanya untuk metode itu:

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
    }
}