C # 8 referensi yang tidak dapat dibatalkan dan pola Coba

23

Ada pola dalam kelas C # yang dicontohkan oleh Dictionary.TryGetValuedan int.TryParse: metode yang mengembalikan boolean yang menunjukkan keberhasilan operasi dan parameter keluar yang berisi hasil aktual; jika operasi gagal, parameter keluar diatur ke nol.

Mari kita asumsikan saya menggunakan referensi non-nullable C # 8 dan ingin menulis metode TryParse untuk kelas saya sendiri. Tanda tangan yang benar adalah ini:

public static bool TryParse(string s, out MyClass? result);

Karena hasilnya nol dalam kasus palsu, variabel keluar harus ditandai sebagai nullable.

Namun, pola Coba umumnya digunakan seperti ini:

if (MyClass.TryParse(s, out var result))
{
  // use result here
}

Karena saya hanya memasuki cabang ketika operasi berhasil, hasilnya tidak boleh nol di cabang itu. Tetapi karena saya menandainya sebagai nullable, sekarang saya harus memeriksa atau menggunakannya !untuk mengganti:

if (MyClass.TryParse(s, out var result))
{
  Console.WriteLine("Look: {0}", result.SomeProperty); // compiler warning, could be null
  Console.WriteLine("Look: {0}", result!.SomeProperty); // need override
}

Ini jelek dan agak tidak ekonomis.

Karena pola penggunaan yang umum, saya memiliki pilihan lain: berbohong tentang jenis hasil:

public static bool TryParse(string s, out MyClass result) // not nullable
{
   // Happy path sets result to non-null and returns true.
   // Error path does this:
   result = null!; // override compiler complaint
   return false;
}

Sekarang penggunaan tipikal menjadi lebih baik:

if (MyClass.TryParse(s, out var result))
{
  Console.WriteLine("Look: {0}", result.SomeProperty); // no warning
}

tetapi penggunaan yang tidak biasa tidak mendapatkan peringatan yang seharusnya:

else
{
  Console.WriteLine("Fail: {0}", result.SomeProperty);
  // Yes, result is in scope here. No, it will never be non-null.
  // Yes, it will throw. No, the compiler won't warn about it.
}

Sekarang saya tidak yakin ke mana harus pergi ke sini. Apakah ada rekomendasi resmi dari tim bahasa C #? Apakah ada kode CoreFX yang telah dikonversi menjadi referensi yang tidak dapat dibatalkan yang dapat menunjukkan kepada saya bagaimana melakukan ini? (Saya pergi mencari TryParsemetode. IPAddressAdalah kelas yang memiliki satu, tetapi belum dikonversi pada cabang utama corefx.)

Dan bagaimana kode generik suka Dictionary.TryGetValuemenangani ini? (Mungkin dengan MaybeNullatribut khusus dari apa yang saya temukan.) Apa yang terjadi ketika saya instantiate Dictionarydengan tipe nilai yang tidak dapat dibatalkan?

Sebastian Redl
sumber
Tidak mencobanya (itulah sebabnya saya tidak menulis ini sebagai jawaban), tetapi dengan fitur pencocokan pola baru dari pernyataan peralihan, saya menduga satu opsi adalah mengembalikan referensi nullable (tanpa Try-pattern, kembali MyClass?), dan lakukan sakelar dengan a case MyClass myObj:dan (sebuah opsi semua) case null:.
Filip Milovanović
+1 Saya benar-benar menyukai pertanyaan ini, dan ketika saya harus bekerja dengan ini, saya selalu menggunakan cek nol ekstra alih-alih menimpa - yang selalu terasa sedikit tidak perlu dan tidak elegan, tetapi tidak pernah dalam kode kritis kinerja jadi saya biarkan saja. Akan menyenangkan untuk melihat apakah ada cara yang lebih bersih untuk menanganinya!
BrianH

Jawaban:

10

Pola bool / out-var tidak berfungsi dengan baik dengan tipe referensi yang dapat dibatalkan, seperti yang Anda gambarkan. Jadi alih-alih melawan kompiler, gunakan fitur untuk menyederhanakan banyak hal. Lempar fitur pencocokan pola yang ditingkatkan dari C # 8 dan Anda dapat memperlakukan referensi nullable sebagai "tipe orang miskin mungkin":

public static MyClass? TryParse(string s) => 



if (TryParse(someString) is {} myClass)
{
    // myClass wasn't null, we are good to use it
}

Dengan begitu, Anda menghindari bermain-main dengan outparameter dan Anda tidak perlu berkelahi dengan kompiler untuk mencampur nulldengan referensi yang tidak dapat dibatalkan.

Dan bagaimana kode generik suka Dictionary.TryGetValuemenangani ini?

Pada titik ini, "tipe orang miskin mungkin" jatuh. Tantangan yang akan Anda hadapi adalah ketika menggunakan nullable reference types (NRT), kompiler akan memperlakukan Foo<T>sebagai tidak dapat dibatalkan. Tetapi cobalah dan ubahlah Foo<T?>dan itu akan menginginkan bahwa Tdibatasi ke kelas atau struct sebagai tipe nilai nullable adalah hal yang sangat berbeda dari sudut pandang CLR. Ada berbagai cara untuk mengatasi ini:

  1. Jangan aktifkan fitur NRT,
  2. Mulai gunakan default(bersama dengan !) untuk outparameter meskipun kode Anda mendaftar tanpa nol,
  3. Gunakan Maybe<T>tipe nyata sebagai nilai kembali, yang kemudian tidak pernah nulldan membungkus itu booldan out Tke dalam HasValuedan Valueproperti atau semacamnya,
  4. Gunakan tuple:
public static (bool success, T result) TryParse<T>(string s) => 


if (TryParse<MyClass>(someString) is (true, var result))
{
    // result is valid here, as success is true
}

Saya pribadi lebih suka menggunakan Maybe<T>tetapi memilikinya mendukung dekonstruksi sehingga dapat dicocokkan dengan pola sebagai tuple seperti pada 4, di atas.

David Arno
sumber
2
TryParse(someString) is {} myClass- Sintaks ini akan membiasakan diri, tapi saya suka idenya.
Sebastian Redl
TryParse(someString) is var myClassterlihat lebih mudah bagiku.
Olivier Jacot-Descombes
2
@ OlivierJacot-Descombes itu Mungkin terlihat lebih mudah ... tetapi tidak akan berhasil. Pola mobil selalu cocok, jadi x is var yakan selalu benar, apakah xitu nol atau tidak.
David Arno
15

Jika Anda sampai pada hal ini agak terlambat, seperti saya, ternyata tim .NET mengatasinya melalui sekelompok atribut parameter seperti MaybeNullWhen(returnValue: true)di System.Diagnostics.CodeAnalysisruang yang dapat Anda gunakan untuk pola coba.

Sebagai contoh:

bagaimana kode generik seperti Dictionary.TryGetValue menangani ini?

bool TryGetValue(TKey key, [MaybeNullWhen(returnValue: false)] out TValue value);

yang berarti Anda dimarahi jika Anda tidak memeriksa true

// This is okay:
if(myDictionary.TryGetValue("cheese", out var result))
{
  var more = result * 42;
}

// But this is not:
_ = myDictionary.TryGetValue("cheese", out var result);
var more = result * 42;
// "CS8602: Dereference of a potentially null reference"

Keterangan lebih lanjut:

Nick Darvey
sumber
3

Saya tidak berpikir ada konflik di sini.

keberatan Anda

public static bool TryParse(string s, out MyClass? result);

aku s

Karena saya hanya memasuki cabang ketika operasi berhasil, hasilnya tidak boleh nol di cabang itu.

Namun, pada kenyataannya tidak ada yang mencegah penugasan null ke parameter keluar dalam fungsi TryParse gaya lama.

misalnya.

MyJsonObject.TryParse("null", out obj) //sets obj to a null MyJsonObject and returns true

Peringatan yang diberikan kepada programmer ketika mereka menggunakan parameter keluar tanpa memeriksa sudah benar. Anda harus memeriksa!

Akan ada banyak kasus di mana Anda akan dipaksa untuk mengembalikan tipe nullable di mana cabang utama dari kode mengembalikan tipe non-nullable. Peringatan itu ada di sana untuk membantu Anda membuat ini eksplisit. yaitu.

MyClass? x = (new List<MyClass>()).FirstOrDefault(i=>i==1);

Cara non-nullable untuk kode itu akan melemparkan pengecualian di mana akan ada null. Apakah Anda menguraikan, mendapatkan atau memprioritaskan

MyClass x = (new List<MyClass>()).First(i=>i==1);
Ewan
sumber
Saya tidak berpikir FirstOrDefaultdapat dibandingkan, karena nolnya nilai pengembaliannya adalah sinyal utama. Dalam TryParsemetode, parameter keluar tidak menjadi nol jika nilai kembali benar adalah bagian dari kontrak metode.
Sebastian Redl
itu bukan bagian dari kontrak. satu-satunya hal yang meyakinkan adalah bahwa sesuatu ditugaskan ke parameter out
Ewan
Itu perilaku yang saya harapkan dari suatu TryParsemetode. Jika IPAddress.TryParsepernah kembali benar tetapi tidak menetapkan non-null ke parameternya, saya akan melaporkannya sebagai bug.
Sebastian Redl
Harapan Anda bisa dimengerti tetapi tidak dipaksakan oleh kompiler. Jadi yakin, spek untuk IpAddress mungkin mengatakan tidak pernah mengembalikan nilai true dan null, tetapi contoh JsonObject saya menunjukkan kasus di mana pengembalian null mungkin benar
Ewan
"Harapanmu bisa dimengerti tetapi tidak dipaksakan oleh kompiler." - Saya tahu, tetapi pertanyaan saya adalah bagaimana cara terbaik menulis kode saya untuk mengekspresikan kontrak yang ada dalam pikiran saya, bukan apa kontrak dari beberapa kode lain.
Sebastian Redl