Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Nota
Artikel ini adalah spesifikasi fitur. Spesifikasi berfungsi sebagai dokumen desain untuk fitur tersebut. Ini termasuk perubahan spesifikasi yang diusulkan, bersama dengan informasi yang diperlukan selama desain dan pengembangan fitur. Artikel ini diterbitkan sampai perubahan spesifikasi yang diusulkan diselesaikan dan dimasukkan dalam spesifikasi ECMA saat ini.
Mungkin ada beberapa perbedaan antara spesifikasi fitur dan implementasi yang selesai. Perbedaan tersebut dicatat dalam catatan terkait rapat desain bahasa (LDM) .
Anda dapat mempelajari lebih lanjut tentang proses untuk mengadopsi speklet fitur ke dalam standar bahasa C# dalam artikel tentang spesifikasi .
Masalah utama: https://github.com/dotnet/csharplang/issues/287
Ringkasan
Izinkan pengembang untuk mengambil ekspresi yang diteruskan ke metode , untuk mengaktifkan pesan kesalahan yang lebih baik dalam API diagnostik/pengujian dan mengurangi penekanan kunci.
Motivasi
Ketika pernyataan atau validasi argumen gagal, pengembang ingin tahu sebanyak mungkin tentang di mana dan mengapa gagal. Namun, API diagnostik hari ini tidak sepenuhnya memfasilitasi hal ini. Pertimbangkan metode berikut:
T Single<T>(this T[] array)
{
Debug.Assert(array != null);
Debug.Assert(array.Length == 1);
return array[0];
}
Ketika salah satu assert gagal, hanya nama file, nomor baris, dan nama metode yang akan disediakan dalam stack trace. Pengembang tidak akan dapat mengetahui pernyataan mana yang gagal dari informasi ini - mereka harus membuka file dan menavigasi ke nomor baris yang disediakan untuk melihat apa yang salah.
Ini juga merupakan alasan kerangka kerja pengujian harus menyediakan berbagai metode pernyataan. Dengan xUnit, Assert.True dan Assert.False tidak sering digunakan karena tidak memberikan konteks yang cukup tentang apa yang gagal.
Meskipun situasinya sedikit lebih baik untuk validasi argumen karena nama argumen yang tidak valid ditunjukkan kepada pengembang, pengembang harus meneruskan nama-nama ini ke pengecualian secara manual. Jika contoh di atas ditulis ulang untuk menggunakan validasi argumen tradisional alih-alih Debug.Assert, akan terlihat seperti
T Single<T>(this T[] array)
{
if (array == null)
{
throw new ArgumentNullException(nameof(array));
}
if (array.Length != 1)
{
throw new ArgumentException("Array must contain a single element.", nameof(array));
}
return array[0];
}
Perhatikan bahwa nameof(array) harus diteruskan ke setiap pengecualian, meskipun sudah jelas dari konteks argumen mana yang tidak valid.
Desain terperinci
Dalam contoh di atas, termasuk string "array != null" atau "array.Length == 1" dalam pesan pernyataan akan membantu pengembang menentukan apa yang gagal. Masukkan CallerArgumentExpression: ini adalah atribut yang dapat digunakan kerangka kerja untuk mendapatkan string yang terkait dengan argumen metode tertentu. Kami akan menambahkannya ke Debug.Assert seperti berikut
public static class Debug
{
public static void Assert(bool condition, [CallerArgumentExpression("condition")] string message = null);
}
Kode sumber dalam contoh di atas akan tetap sama. Namun, kode yang sebenarnya dikeluarkan kompilator akan sesuai dengan
T Single<T>(this T[] array)
{
Debug.Assert(array != null, "array != null");
Debug.Assert(array.Length == 1, "array.Length == 1");
return array[0];
}
Pengkompilasi secara khusus mengenali atribut pada Debug.Assert. Ini meneruskan string yang terkait dengan argumen yang dirujuk dalam konstruktor atribut (dalam hal ini, condition) pada titik panggilan. Ketika salah satu pernyataan gagal, pengembang akan ditampilkan kondisi yang salah dan akan tahu mana yang gagal.
Untuk validasi argumen, atribut tidak dapat digunakan secara langsung, tetapi dapat digunakan melalui kelas pembantu:
public static class Verify
{
public static void Argument(bool condition, string message, [CallerArgumentExpression("condition")] string conditionExpression = null)
{
if (!condition) throw new ArgumentException(message: message, paramName: conditionExpression);
}
public static void InRange(int argument, int low, int high,
[CallerArgumentExpression("argument")] string argumentExpression = null,
[CallerArgumentExpression("low")] string lowExpression = null,
[CallerArgumentExpression("high")] string highExpression = null)
{
if (argument < low)
{
throw new ArgumentOutOfRangeException(paramName: argumentExpression,
message: $"{argumentExpression} ({argument}) cannot be less than {lowExpression} ({low}).");
}
if (argument > high)
{
throw new ArgumentOutOfRangeException(paramName: argumentExpression,
message: $"{argumentExpression} ({argument}) cannot be greater than {highExpression} ({high}).");
}
}
public static void NotNull<T>(T argument, [CallerArgumentExpression("argument")] string argumentExpression = null)
where T : class
{
if (argument == null) throw new ArgumentNullException(paramName: argumentExpression);
}
}
static T Single<T>(this T[] array)
{
Verify.NotNull(array); // paramName: "array"
Verify.Argument(array.Length == 1, "Array must contain a single element."); // paramName: "array.Length == 1"
return array[0];
}
static T ElementAt<T>(this T[] array, int index)
{
Verify.NotNull(array); // paramName: "array"
// paramName: "index"
// message: "index (-1) cannot be less than 0 (0).", or
// "index (6) cannot be greater than array.Length - 1 (5)."
Verify.InRange(index, 0, array.Length - 1);
return array[index];
}
Sebuah proposal untuk menambahkan kelas pembantu tersebut ke kerangka kerja sedang dipertimbangkan di https://github.com/dotnet/corefx/issues/17068. Jika fitur bahasa ini diimplementasikan, proposal dapat diperbarui untuk memanfaatkan fitur ini.
Metode ekstensi
Parameter this dalam metode ekstensi dapat dirujuk oleh CallerArgumentExpression. Misalnya:
public static void ShouldBe<T>(this T @this, T expected, [CallerArgumentExpression("this")] string thisExpression = null) {}
contestant.Points.ShouldBe(1337); // thisExpression: "contestant.Points"
Ekspresi yang sesuai dengan objek sebelum titik akan diterima oleh thisExpression. Jika dipanggil dengan sintaks metode statis, misalnya Ext.ShouldBe(contestant.Points, 1337), itu akan bereaksi seolah-olah parameter pertama tidak ditandai this.
Harus selalu ada ekspresi yang sesuai dengan parameter this. Bahkan jika instans kelas memanggil metode ekstensi pada dirinya sendiri, misalnya this.Single() dari dalam jenis koleksi, this diamanatkan oleh pengkompilasi sehingga "this" akan diteruskan. Jika aturan ini diubah di masa mendatang, kita dapat mempertimbangkan untuk mengirimkan null atau string kosong.
Detail tambahan
- Seperti atribut
Caller*lainnya, sepertiCallerMemberName, atribut ini hanya dapat digunakan pada parameter dengan nilai default. - Beberapa parameter yang ditandai dengan
CallerArgumentExpressiondiizinkan, seperti yang ditunjukkan di atas. - Namespace atributnya akan dijadikan
System.Runtime.CompilerServices. - Jika
nullatau string yang bukan nama parameter (misalnya"notAParameterName") disediakan, pengkompilasi akan meneruskan string kosong. - Jenis parameter tempat
CallerArgumentExpressionAttributediterapkan harus memiliki konversi standar daristring. Ini berarti tidak ada konversi yang ditentukan pengguna daristringyang diizinkan, dan dalam praktiknya berarti jenis parameter tersebut harusstring,object, atau antarmuka yang diterapkan olehstring.
Kekurangan
Orang yang tahu cara menggunakan dekompilasi akan dapat melihat beberapa kode sumber di situs panggilan untuk metode yang ditandai dengan atribut ini. Ini mungkin tidak diinginkan/tidak terduga untuk perangkat lunak sumber tertutup.
Meskipun ini bukan kelemahan dalam fitur itu sendiri, ada kemungkinan sumber kekhawatiran bahwa saat ini ada API
Debug.Assertyang hanya mengambilbool. Bahkan jika kelebihan beban mengambil pesan memiliki parameter kedua yang ditandai dengan atribut ini dan dibuat opsional, pengkompilasi masih akan memilih yang tidak memiliki pesan dalam resolusi kelebihan beban. Oleh karena itu, kelebihan pesan no-message harus dihapus untuk memanfaatkan fitur ini, yang akan menjadi perubahan pemecah biner (meskipun bukan sumber).
Alternatif
- Jika melihat kode sumber di lokasi pemanggilan untuk metode yang menggunakan atribut ini terbukti menjadi masalah, kita dapat membuat efek atribut menjadi pilihan aktif. Pengembang akan mengaktifkannya melalui atribut
[assembly: EnableCallerArgumentExpression]yang diterapkan pada seluruh perakitan dan mereka tambahkan ke dalamAssemblyInfo.cs.- Jika efek atribut tidak diaktifkan, metode panggilan yang ditandai dengan atribut tidak akan menjadi kesalahan, untuk memungkinkan metode yang ada menggunakan atribut dan mempertahankan kompatibilitas sumber. Namun, atribut akan diabaikan dan metode akan dipanggil dengan nilai default apa pun yang disediakan.
// Assembly1
void Foo(string bar); // V1
void Foo(string bar, string barExpression = "not provided"); // V2
void Foo(string bar, [CallerArgumentExpression("bar")] string barExpression = "not provided"); // V3
// Assembly2
Foo(a); // V1: Compiles to Foo(a), V2, V3: Compiles to Foo(a, "not provided")
Foo(a, "provided"); // V2, V3: Compiles to Foo(a, "provided")
// Assembly3
[assembly: EnableCallerArgumentExpression]
Foo(a); // V1: Compiles to Foo(a), V2: Compiles to Foo(a, "not provided"), V3: Compiles to Foo(a, "a")
Foo(a, "provided"); // V2, V3: Compiles to Foo(a, "provided")
- Untuk mencegah masalah kompatibilitas biner terjadi setiap kali kami ingin menambahkan info pemanggil baru ke
Debug.Assert, solusi alternatifnya adalah menambahkanCallerInfostruct ke kerangka kerja yang berisi semua informasi yang diperlukan tentang pemanggil.
struct CallerInfo
{
public string MemberName { get; set; }
public string TypeName { get; set; }
public string Namespace { get; set; }
public string FullTypeName { get; set; }
public string FilePath { get; set; }
public int LineNumber { get; set; }
public int ColumnNumber { get; set; }
public Type Type { get; set; }
public MethodBase Method { get; set; }
public string[] ArgumentExpressions { get; set; }
}
[Flags]
enum CallerInfoOptions
{
MemberName = 1, TypeName = 2, ...
}
public static class Debug
{
public static void Assert(bool condition,
// If a flag is not set here, the corresponding CallerInfo member is not populated by the caller, so it's
// pay-for-play friendly.
[CallerInfo(CallerInfoOptions.FilePath | CallerInfoOptions.Method | CallerInfoOptions.ArgumentExpressions)] CallerInfo callerInfo = default(CallerInfo))
{
string filePath = callerInfo.FilePath;
MethodBase method = callerInfo.Method;
string conditionExpression = callerInfo.ArgumentExpressions[0];
//...
}
}
class Bar
{
void Foo()
{
Debug.Assert(false);
// Translates to:
var callerInfo = new CallerInfo();
callerInfo.FilePath = @"C:\Bar.cs";
callerInfo.Method = MethodBase.GetCurrentMethod();
callerInfo.ArgumentExpressions = new string[] { "false" };
Debug.Assert(false, callerInfo);
}
}
Pada awalnya, ini diusulkan di https://github.com/dotnet/csharplang/issues/87.
Ada beberapa kerugian dari pendekatan ini:
Meskipun ramah bayar-untuk-main dengan memungkinkan Anda menentukan properti mana yang Anda butuhkan, itu masih bisa menyakiti perf secara signifikan dengan mengalokasikan array untuk ekspresi / panggilan
MethodBase.GetCurrentMethodbahkan ketika pernyataan lolos.Selain itu, meskipun meneruskan flag baru ke atribut
CallerInfotidak akan menjadi perubahan yang mengganggu kompatibilitas,Debug.Asserttidak dijamin akan benar-benar menerima parameter baru tersebut dari tempat pemanggilan yang menggunakan versi lama metode tersebut.
Pertanyaan yang belum terselesaikan
TBD
Rapat Desain
N/A
C# feature specifications