Mengapa pengembalian hasil tidak dapat muncul di dalam blok percobaan dengan tangkapan?

95

Berikut ini tidak masalah:

try
{
    Console.WriteLine("Before");

    yield return 1;

    Console.WriteLine("After");
}
finally
{
    Console.WriteLine("Done");
}

The finallyblok berjalan ketika semuanya telah selesai mengeksekusi ( IEnumerator<T>mendukung IDisposableuntuk menyediakan cara untuk memastikan ini bahkan ketika pencacahan ditinggalkan sebelum selesai).

Tapi ini tidak baik:

try
{
    Console.WriteLine("Before");

    yield return 1;  // error CS1626: Cannot yield a value in the body of a try block with a catch clause

    Console.WriteLine("After");
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

Misalkan (demi argumen) bahwa pengecualian dilemparkan oleh salah satu WriteLinepanggilan di dalam blok percobaan. Apa masalah melanjutkan eksekusi di catchblok?

Tentu saja, bagian pengembalian hasil (saat ini) tidak dapat melempar apa pun, tetapi mengapa hal itu menghentikan kami dari memiliki penutup try/ catchuntuk menangani pengecualian yang dilemparkan sebelum atau sesudah yield return?

Pembaruan: Ada komentar menarik dari Eric Lippert di sini - tampaknya mereka sudah memiliki cukup masalah dalam menerapkan perilaku coba / akhirnya dengan benar!

EDIT: Halaman MSDN tentang kesalahan ini adalah: http://msdn.microsoft.com/en-us/library/cs1x15az.aspx . Itu tidak menjelaskan mengapa.

Daniel Earwicker
sumber
2
Tautan langsung ke komentar Eric Lippert: blogs.msdn.com/oldnewthing/archive/2008/08/14/…
Roman Starkov
catatan: Anda juga tidak bisa menyerah di blok tangkapan :-(
Simon_Weaver
2
Tautan lama tidak lagi berfungsi.
Sebastian Redl

Jawaban:

50

Saya menduga ini adalah masalah kepraktisan daripada kelayakan. Saya menduga ada sangat, sangat sedikit waktu di mana pembatasan ini sebenarnya merupakan masalah yang tidak dapat diatasi - tetapi kompleksitas tambahan dalam kompiler akan menjadi sangat signifikan.

Ada beberapa hal seperti ini yang pernah saya temui:

  • Atribut tidak boleh generik
  • Ketidakmampuan X untuk diturunkan dari XY (kelas bersarang di X)
  • Blok Iterator menggunakan bidang publik di kelas yang dihasilkan

Dalam setiap kasus ini, dimungkinkan untuk mendapatkan sedikit lebih banyak kebebasan, dengan biaya kerumitan ekstra dalam kompiler. Tim membuat pilihan pragmatis, yang saya tepuk tangani mereka - saya lebih suka memiliki bahasa yang sedikit lebih ketat dengan kompiler akurat 99,9% (ya, ada bug; saya menemukan satu di SO beberapa hari yang lalu) daripada lebih bahasa fleksibel yang tidak dapat dikompilasi dengan benar.

EDIT: Berikut adalah bukti semu tentang bagaimana mengapa itu layak.

Pertimbangkan bahwa:

  • Anda dapat memastikan bahwa bagian hasil pengembalian itu sendiri tidak memunculkan pengecualian (menghitung sebelumnya nilai, lalu Anda hanya menyetel bidang dan mengembalikan "benar")
  • Anda diizinkan mencoba / menangkap yang tidak menggunakan pengembalian hasil dalam blok iterator.
  • Semua variabel lokal di blok iterator adalah variabel instan dalam tipe yang dihasilkan, sehingga Anda dapat dengan bebas memindahkan kode ke metode baru

Sekarang ubah:

try
{
    Console.WriteLine("a");
    yield return 10;
    Console.WriteLine("b");
}
catch (Something e)
{
    Console.WriteLine("Catch block");
}
Console.WriteLine("Post");

menjadi (semacam pseudo-code):

case just_before_try_state:
    try
    {
        Console.WriteLine("a");
    }
    catch (Something e)
    {
        CatchBlock();
        goto case post;
    }
    __current = 10;
    return true;

case just_after_yield_return:
    try
    {
        Console.WriteLine("b");
    }
    catch (Something e)
    {
        CatchBlock();
    }
    goto case post;

case post;
    Console.WriteLine("Post");


void CatchBlock()
{
    Console.WriteLine("Catch block");
}

Satu-satunya duplikasi adalah dalam menyiapkan blok coba / tangkap - tetapi itu adalah sesuatu yang pasti dapat dilakukan oleh kompilator.

Saya mungkin melewatkan sesuatu di sini - jika demikian, beri tahu saya!

Jon Skeet
sumber
11
Bukti konsep yang bagus, tetapi strategi ini menjadi menyakitkan (mungkin lebih untuk programmer C # daripada untuk penulis kompiler C #) setelah Anda mulai membuat cakupan dengan hal-hal seperti usingdan foreach. Misalnya:try{foreach (string s in c){yield return s;}}catch(Exception){}
Brian
Semantik normal dari "coba / tangkap" menyiratkan bahwa jika ada bagian dari blok coba / tangkap yang dilewati karena pengecualian, kontrol akan ditransfer ke blok "tangkap" yang sesuai jika ada. Sayangnya, jika pengecualian terjadi "selama" pengembalian hasil, tidak ada cara bagi iterator untuk membedakan kasus-kasus di mana itu akan Dibuang karena pengecualian dari kasus-kasus di mana itu akan Dibuang karena pemilik mengambil semua data yang diinginkan.
supercat
7
"Saya menduga ada sangat, sangat sedikit waktu di mana pembatasan ini sebenarnya merupakan masalah yang tidak dapat diatasi" Itu seperti mengatakan Anda tidak memerlukan pengecualian karena Anda dapat menggunakan strategi pengembalian kode kesalahan yang biasa digunakan di C jadi bertahun-tahun yang lalu. Saya akui kesulitan teknis mungkin signifikan, tetapi ini masih sangat membatasi kegunaan yield, menurut saya, karena kode spageti yang harus Anda tulis untuk mengatasinya.
jpmc26
@ jpmc26: Tidak, sama sekali tidak seperti itu. Saya tidak ingat ini pernah menggigit saya, dan saya telah menggunakan blok iterator berkali-kali. Ini sedikit membatasi kegunaan yieldIMO - itu jauh dari sangat .
Jon Skeet
2
'Fitur' ini sebenarnya memerlukan beberapa kode yang agak jelek dalam beberapa kasus untuk mengatasinya, lihat stackoverflow.com/questions/5067188/…
namey
5

Semua yieldpernyataan dalam definisi iterator diubah menjadi keadaan dalam mesin keadaan yang secara efektif menggunakan switchpernyataan untuk memajukan keadaan. Jika itu benar - benar menghasilkan kode untuk yieldpernyataan dalam coba / tangkap itu harus menduplikasi semua yang ada di tryblok untuk setiap yield pernyataan sambil mengecualikan setiap yieldpernyataan lain untuk blok itu. Ini tidak selalu memungkinkan, terutama jika satu yieldpernyataan bergantung pada pernyataan sebelumnya.

Mark Cidade
sumber
2
Saya tidak berpikir saya membeli itu. Saya rasa itu akan sepenuhnya mungkin - tetapi sangat rumit.
Jon Skeet
2
Coba / tangkap blok di C # tidak dimaksudkan untuk menjadi peserta kembali. Jika Anda memisahkan mereka maka dimungkinkan untuk memanggil MoveNext () setelah pengecualian dan melanjutkan blok percobaan dengan keadaan yang mungkin tidak valid.
Mark Cidade
2

Saya akan berspekulasi bahwa karena cara tumpukan panggilan mendapat luka / dibatalkan ketika Anda menghasilkan kembali dari enumerator menjadi tidak mungkin untuk mencoba / menangkap blok untuk benar-benar "menangkap" pengecualian. (karena blok pengembalian hasil tidak ada di tumpukan, meskipun ia berasal dari blok iterasi)

Untuk mendapatkan gambaran tentang apa yang saya bicarakan menyiapkan blok iterator dan foreach menggunakan iterator itu. Periksa seperti apa Call Stack di dalam blok foreach dan kemudian periksa di dalam blok percobaan / akhirnya iterator.

Radu094
sumber
Saya terbiasa dengan stack unwinding di C ++, di mana destruktor dipanggil pada objek lokal yang berada di luar cakupan. Hal yang sesuai di C # akan dicoba / akhirnya. Tetapi pelepasan itu tidak terjadi ketika pengembalian hasil terjadi. Dan untuk try / catch tidak perlu berinteraksi dengan yield return.
Daniel Earwicker
Periksa apa yang terjadi pada tumpukan panggilan saat mengulang iterator dan Anda akan mengerti apa yang saya maksud
Radu094
@ Radu094: Tidak, saya yakin itu mungkin. Jangan lupa bahwa itu sudah menangani akhirnya, yang setidaknya agak mirip.
Jon Skeet
2

Saya telah menerima jawaban THE INVINCIBLE SKEET sampai seseorang dari Microsoft datang untuk menuangkan ide tersebut. Tetapi saya tidak setuju dengan bagian masalah pendapat - tentu saja kompilator yang benar lebih penting daripada yang lengkap, tetapi kompilator C # sudah sangat pintar dalam memilah transformasi ini untuk kita sejauh yang dilakukannya. Sedikit lebih banyak kelengkapan dalam hal ini akan membuat bahasa lebih mudah digunakan, diajarkan, dijelaskan, dengan kasus tepi atau gotcha yang lebih sedikit. Jadi saya pikir itu akan sepadan dengan usaha ekstra. Beberapa orang di Redmond menggaruk-garuk kepala selama dua minggu, dan sebagai hasilnya jutaan pembuat kode selama dekade berikutnya dapat sedikit lebih santai.

(Saya juga menyimpan keinginan kotor agar ada cara untuk membuat yield returnpengecualian yang telah dimasukkan ke dalam mesin negara "dari luar", dengan kode yang mendorong pengulangan. Tapi alasan saya menginginkan ini cukup kabur.)

Sebenarnya satu pertanyaan yang saya miliki tentang jawaban Jon adalah berkaitan dengan ekspresi pengembalian hasil yang dilempar.

Jelas hasil pengembalian 10 tidak terlalu buruk. Tapi ini buruk:

yield return File.ReadAllText("c:\\missing.txt").Length;

Jadi, bukankah lebih masuk akal untuk mengevaluasi ini di dalam blok coba / tangkap sebelumnya:

case just_before_try_state:
    try
    {
        Console.WriteLine("a");
        __current = File.ReadAllText("c:\\missing.txt").Length;
    }
    catch (Something e)
    {
        CatchBlock();
        goto case post;
    }
    return true;

Masalah selanjutnya adalah blok coba / tangkap bersarang dan pengecualian yang muncul kembali:

try
{
    Console.WriteLine("x");

    try
    {
        Console.WriteLine("a");
        yield return 10;
        Console.WriteLine("b");
    }
    catch (Something e)
    {
        Console.WriteLine("y");

        if ((DateTime.Now.Second % 2) == 0)
            throw;
    }
}
catch (Something e)
{
    Console.WriteLine("Catch block");
}
Console.WriteLine("Post");

Tapi saya yakin itu mungkin ...

Daniel Earwicker
sumber
1
Ya, Anda akan memasukkan evaluasi ke dalam coba / tangkap. Tidak masalah di mana Anda meletakkan pengaturan variabel. Poin utamanya adalah Anda dapat secara efektif memecah satu percobaan / tangkapan dengan pengembalian hasil di dalamnya menjadi dua percobaan / tangkapan dengan pengembalian hasil di antara mereka.
Jon Skeet