Menangkap pengecualian dengan "catch, when"

95

Saya menemukan fitur baru ini di C # yang memungkinkan penangan tangkapan untuk mengeksekusi ketika kondisi tertentu terpenuhi.

int i = 0;
try
{
    throw new ArgumentNullException(nameof(i));
}
catch (ArgumentNullException e)
when (i == 1)
{
    Console.WriteLine("Caught Argument Null Exception");
}

Saya mencoba untuk memahami kapan ini mungkin berguna.

Satu skenario bisa jadi seperti ini:

try
{
    DatabaseUpdate()
}
catch (SQLException e)
when (driver == "MySQL")
{
    //MySQL specific error handling and wrapping up the exception
}
catch (SQLException e)
when (driver == "Oracle")
{
    //Oracle specific error handling and wrapping up of exception
}
..

tetapi sekali lagi ini adalah sesuatu yang dapat saya lakukan dalam penangan yang sama dan mendelegasikan ke metode yang berbeda tergantung pada jenis driver. Apakah ini membuat kode lebih mudah dipahami? Bisa dibilang tidak.

Skenario lain yang dapat saya pikirkan adalah seperti:

try
{
    SomeOperation();
}
catch(SomeException e)
when (Condition == true)
{
    //some specific error handling that this layer can handle
}
catch (Exception e) //catchall
{
    throw;
}

Sekali lagi ini adalah sesuatu yang dapat saya lakukan seperti:

try
{
    SomeOperation();
}
catch(SomeException e)
{
    if (condition == true)
    {
        //some specific error handling that this layer can handle
    }
    else
        throw;
}

Apakah menggunakan fitur 'catch, when' membuat penanganan exception lebih cepat karena handler dilewati seperti itu dan stack unwinding bisa terjadi jauh lebih awal dibandingkan dengan menangani kasus penggunaan spesifik dalam handler? Adakah kasus penggunaan khusus yang lebih cocok dengan fitur ini yang kemudian dapat diadopsi sebagai praktik yang baik?

MS Srikkanth
sumber
9
Ini berguna jika whenkebutuhan untuk mengakses pengecualian itu sendiri
Tim Schmelter
1
Tapi itu adalah sesuatu yang dapat kita lakukan di dalam blok penangan itu sendiri juga dengan benar. Apakah ada manfaat selain 'kode yang sedikit lebih teratur'?
MS Srikkanth
3
Tapi kemudian Anda telah menangani pengecualian yang tidak Anda inginkan. Bagaimana jika Anda ingin menangkapnya di tempat lain dalam hal ini try..catch...catch..catch..finally?
Tim Schmelter
4
@ user3493289: Mengikuti argumen itu, kita juga tidak memerlukan pemeriksaan jenis otomatis di penangan pengecualian: Kita hanya dapat mengizinkan catch (Exception ex), memeriksa jenis, dan throwsebaliknya. Kode yang sedikit lebih terorganisir (alias menghindari gangguan kode) adalah alasan mengapa fitur ini ada. (Ini sebenarnya berlaku untuk banyak fitur.)
Heinzi
2
@Timmelter Terima kasih. Posting sebagai jawaban dan saya akan menerimanya. Jadi skenario sebenarnya adalah 'jika kondisi penanganan bergantung pada pengecualian', kemudian gunakan fitur ini /
MS Srikkanth

Jawaban:

120

Catch block sudah memungkinkan Anda untuk memfilter jenis pengecualian:

catch (SomeSpecificExceptionType e) {...}

The whenklausul memungkinkan Anda untuk memperpanjang filter ini untuk ekspresi generik.

Dengan demikian, Anda menggunakan whenklausa untuk kasus di mana jenis pengecualian tidak cukup berbeda untuk menentukan apakah pengecualian harus ditangani di sini atau tidak.


Kasus penggunaan yang umum adalah jenis pengecualian yang sebenarnya merupakan pembungkus untuk beberapa jenis kesalahan yang berbeda.

Inilah kasus yang sebenarnya saya gunakan (di VB, yang sudah memiliki fitur ini untuk beberapa waktu):

try
{
    SomeLegacyComOperation();
}
catch (COMException e) when (e.ErrorCode == 0x1234)
{
    // Handle the *specific* error I was expecting. 
}

Sama untuk SqlException, yang juga memiliki ErrorCodeproperti. Alternatifnya akan seperti itu:

try
{
    SomeLegacyComOperation();
}
catch (COMException e)
{
    if (e.ErrorCode == 0x1234)
    {
        // Handle error
    }
    else
    {
        throw;
    }
}

yang bisa dibilang kurang elegan dan sedikit merusak stack trace .

Selain itu, Anda dapat menyebutkan jenis pengecualian yang sama dua kali dalam blok coba-tangkap yang sama:

try
{
    SomeLegacyComOperation();
}
catch (COMException e) when (e.ErrorCode == 0x1234)
{
    ...
}
catch (COMException e) when (e.ErrorCode == 0x5678)
{
    ...
}

yang tidak akan mungkin terjadi tanpa whenkondisi tersebut.

Heinzi
sumber
2
Pendekatan kedua juga tidak memungkinkan untuk menangkapnya dengan cara yang berbeda catch, bukan?
Tim Schmelter
@Tim. Benar. Anda harus menangani semua COMExceptions di blok yang sama.
Heinzi
Sementara whenmemungkinkan Anda menangani jenis pengecualian yang sama beberapa kali. Anda harus menyebutkannya juga karena ini adalah perbedaan yang sangat penting. Tanpa whenAnda akan mendapatkan kesalahan kompiler.
Tim Schmelter
1
Sejauh yang saya ketahui, bagian setelah "Singkatnya:" harus menjadi baris pertama dari jawaban.
CompuChip
1
@ user3493289: itu sering terjadi pada kode jelek. Anda berpikir "Saya seharusnya tidak berada dalam kekacauan ini sejak awal, mendesain ulang kode", dan Anda juga berpikir "mungkin ada cara untuk mendukung desain ini dengan elegan, mendesain ulang bahasanya". Dalam hal ini ada semacam ambang batas untuk seberapa jelek Anda ingin klausa tangkapan Anda menjadi, jadi sesuatu yang membuat situasi tertentu tidak terlalu jelek memungkinkan Anda menyelesaikan lebih banyak dalam ambang Anda :-)
Steve Jessop
38

Dari wiki Roslyn (penekanan milik saya):

Filter pengecualian lebih disukai daripada menangkap dan melempar ulang karena mereka membiarkan tumpukan tidak terganggu . Jika pengecualian nanti menyebabkan tumpukan dibuang, Anda dapat melihat dari mana asalnya, bukan hanya tempat terakhir dimunculkan kembali.

Ini juga merupakan bentuk umum dan diterima dari "penyalahgunaan" untuk menggunakan filter pengecualian untuk efek samping; misalnya penebangan. Mereka dapat memeriksa pengecualian "terbang lewat" tanpa menghalangi jalurnya . Dalam kasus tersebut, filter akan sering menjadi panggilan ke fungsi helper kembali-palsu yang mengeksekusi efek samping:

private static bool Log(Exception e) { /* log it */ ; return false; }

 try {  } catch (Exception e) when (Log(e)) { }

Poin pertama patut didemonstrasikan.

static class Program
{
    static void Main(string[] args)
    {
        A(1);
    }

    private static void A(int i)
    {
        try
        {
            B(i + 1);
        }
        catch (Exception ex)
        {
            if (ex.Message != "!")
                Console.WriteLine(ex);
            else throw;
        }
    }

    private static void B(int i)
    {
        throw new Exception("!");
    }
}

Jika kita menjalankan ini di WinDbg hingga pengecualian tercapai, dan mencetak tumpukan menggunakan !clrstack -i -akita akan melihat bingkai dari A:

003eef10 00a7050d [DEFAULT] Void App.Program.A(I4)

PARAMETERS:
  + int i  = 1

LOCALS:
  + System.Exception ex @ 0x23e3178
  + (Error 0x80004005 retrieving local variable 'local_1')

Namun, jika kita mengubah program untuk menggunakan when:

catch (Exception ex) when (ex.Message != "!")
{
    Console.WriteLine(ex);
}

Kami akan melihat tumpukan juga berisi Bbingkai:

001af2b4 01fb05aa [DEFAULT] Void App.Program.B(I4)

PARAMETERS:
  + int i  = 2

LOCALS: (none)

001af2c8 01fb04c1 [DEFAULT] Void App.Program.A(I4)

PARAMETERS:
  + int i  = 1

LOCALS:
  + System.Exception ex @ 0x2213178
  + (Error 0x80004005 retrieving local variable 'local_1')

Informasi tersebut bisa sangat berguna saat men-debug crash dump.

Eli Arbel
sumber
7
Itu mengejutkan saya. Tidak akan throw;(sebagai lawan throw ex;) membiarkan tumpukan tidak terluka juga? 1 untuk hal efek samping. Saya tidak yakin saya menyetujuinya, tetapi ada baiknya mengetahui tentang teknik itu.
Heinzi
13
Itu tidak salah - ini tidak mengacu pada pelacakan tumpukan - ini mengacu pada tumpukan itu sendiri. Jika Anda melihat tumpukan di debugger (WinDbg), dan bahkan jika Anda telah menggunakan throw;, tumpukan terlepas dan Anda kehilangan nilai parameter.
Eli Arbel
1
Ini bisa sangat berguna saat men-debug dump.
Eli Arbel
3
@Heinzi Lihat jawaban saya di utas lain di mana Anda dapat melihat bahwa throw;mengubah jejak tumpukan sedikit dan banyak throw ex;mengubahnya.
Jeppe Stig Nielsen
1
Penggunaan throwmemang sedikit mengganggu jejak tumpukan. Nomor baris berbeda saat digunakan throwsebagai lawan when.
Mike Zboray
7

Ketika sebuah eksepsi dilemparkan, lintasan pertama penanganan eksepsi mengidentifikasi di mana eksepsi akan ditangkap sebelum melepaskan tumpukan; jika / ketika lokasi "catch" diidentifikasi, semua blok "akhirnya" dijalankan (perhatikan bahwa jika pengecualian lolos dari blok "akhirnya", pemrosesan pengecualian sebelumnya dapat ditinggalkan). Setelah itu terjadi, kode akan melanjutkan eksekusi di "catch".

Jika ada breakpoint dalam fungsi yang dievaluasi sebagai bagian dari "when", breakpoint tersebut akan menangguhkan eksekusi sebelum stack unwinding terjadi; sebaliknya, breakpoint pada "catch" hanya akan menghentikan eksekusi setelah semua finallyhandler berjalan.

Akhirnya, jika baris 23 dan 27 foopanggilan bar, dan panggilan pada baris 23 melontarkan pengecualian yang tertangkap di dalam foodan ditarik kembali pada baris 57, maka jejak tumpukan akan menyarankan bahwa pengecualian terjadi saat memanggil bardari baris 57 [lokasi pelemparan ulang] , menghancurkan informasi apa pun tentang apakah pengecualian terjadi pada panggilan baris-23 atau baris-27. Menggunakan whenuntuk menghindari menangkap pengecualian di tempat pertama menghindari gangguan seperti itu.

BTW, pola berguna yang sangat aneh di C # dan VB.NET adalah menggunakan pemanggilan fungsi dalam whenklausa untuk menyetel variabel yang dapat digunakan dalam finallyklausa untuk menentukan apakah fungsi selesai secara normal, untuk menangani kasus di mana suatu fungsi tidak memiliki harapan untuk "menyelesaikan" pengecualian apa pun yang terjadi tetapi harus mengambil tindakan berdasarkan pengecualian tersebut. Misalnya, jika pengecualian dilemparkan dalam metode pabrik yang seharusnya mengembalikan objek yang merangkum sumber daya, sumber daya apa pun yang diperoleh harus dirilis, tetapi pengecualian yang mendasarinya harus meresap ke pemanggil. Cara terbersih untuk menanganinya secara semantik (meskipun tidak secara sintaksis) adalah dengan memiliki filefinallycekal apakah pengecualian terjadi dan, jika demikian, lepaskan semua sumber daya yang diperoleh atas nama objek yang tidak akan dikembalikan. Karena kode pembersihan tidak memiliki harapan untuk menyelesaikan kondisi apa pun yang menyebabkan pengecualian, seharusnya tidak catchdemikian, tetapi hanya perlu mengetahui apa yang terjadi. Memanggil fungsi seperti:

bool CopySecondArgumentToFirstAndReturnFalse<T>(ref T first, T second)
{
  first = second;
  return false;
}

dalam whenklausa akan memungkinkan fungsi pabrik untuk mengetahui bahwa sesuatu telah terjadi.

supercat
sumber