Mengapa C # tidak memiliki cakupan lokal di blok kasus?

38

Saya sedang menulis kode ini:

private static Expression<Func<Binding, bool>> ToExpression(BindingCriterion criterion)
{
    switch (criterion.ChangeAction)
    {
        case BindingType.Inherited:
            var action = (byte)ChangeAction.Inherit;
            return (x => x.Action == action);
        case BindingType.ExplicitValue:
            var action = (byte)ChangeAction.SetValue;
            return (x => x.Action == action);
        default:
            // TODO: Localize errors
            throw new InvalidOperationException("Invalid criterion.");
    }
}

Dan terkejut menemukan kesalahan kompilasi:

Variabel lokal bernama 'tindakan' sudah didefinisikan dalam lingkup ini

Itu adalah masalah yang cukup mudah untuk diselesaikan; menyingkirkan yang kedua varsudah cukup.

Jelas variabel yang dideklarasikan dalam caseblok memiliki ruang lingkup induk switch, tapi saya ingin tahu mengapa ini. Mengingat bahwa C # tidak memungkinkan eksekusi jatuh melalui kasus-kasus lain (itu membutuhkan break , return, throw, atau goto casepernyataan pada akhir setiap caseblok), tampaknya cukup aneh bahwa hal itu akan memungkinkan deklarasi variabel di dalam salah satu caseuntuk digunakan atau konflik dengan variabel di lain case. Dengan kata lain variabel tampaknya jatuh melalui casepernyataan meskipun eksekusi tidak bisa. C # bersusah payah untuk mempromosikan keterbacaan dengan melarang beberapa konstruksi bahasa lain yang membingungkan atau mudah disalahgunakan. Tapi ini sepertinya hanya menyebabkan kebingungan. Pertimbangkan skenario berikut:

  1. Jika ingin mengubahnya menjadi ini:

    case BindingType.Inherited:
        var action = (byte)ChangeAction.Inherit;
        return (x => x.Action == action);
    case BindingType.ExplicitValue:
        return (x => x.Action == action);

    Saya mendapatkan " Penggunaan 'tindakan' variabel lokal yang belum ditetapkan ". Ini membingungkan karena dalam setiap konstruksi lain di C # yang dapat saya pikirkan var action = ...akan menginisialisasi variabel, tetapi di sini hanya menyatakannya.

  2. Jika saya menukar kasus seperti ini:

    case BindingType.ExplicitValue:
        action = (byte)ChangeAction.SetValue;
        return (x => x.Action == action);
    case BindingType.Inherited:
        var action = (byte)ChangeAction.Inherit;
        return (x => x.Action == action);

    Saya mendapatkan " Tidak dapat menggunakan 'tindakan' variabel lokal sebelum dideklarasikan ". Jadi urutan blok kasus tampaknya penting di sini dengan cara yang tidak sepenuhnya jelas - Biasanya saya bisa menulis ini dalam urutan apa pun yang saya inginkan, tetapi karena varharus muncul di blok pertama di mana actiondigunakan, saya harus mengubah caseblok demikian.

  3. Jika ingin mengubahnya menjadi ini:

    case BindingType.Inherited:
        var action = (byte)ChangeAction.Inherit;
        return (x => x.Action == action);
    case BindingType.ExplicitValue:
        action = (byte)ChangeAction.SetValue;
        goto case BindingType.Inherited;

    Maka saya tidak mendapatkan kesalahan, tetapi dalam arti, sepertinya variabel diberi nilai sebelum dideklarasikan.
    (Meskipun saya tidak dapat memikirkan kapan pun Anda benar-benar ingin melakukan ini - saya bahkan tidak tahu goto caseada sebelum hari ini)

Jadi pertanyaan saya adalah, mengapa para desainer C # tidak memberikan caseruang lingkup lokal mereka sendiri? Apakah ada alasan historis atau teknis untuk ini?

pswg
sumber
4
Deklarasikan actionvariabel Anda sebelum switchpernyataan, atau masukkan setiap case ke dalam kurungannya sendiri, dan Anda akan mendapatkan perilaku yang masuk akal.
Robert Harvey
3
@ RobertTarvey ya, dan saya bisa melangkah lebih jauh dan menulis ini tanpa menggunakan switchsama sekali - Saya hanya ingin tahu tentang alasan di balik desain ini.
pswg
jika c # perlu istirahat setelah setiap kasus maka saya kira itu harus karena alasan historis seperti di c / java yang tidak terjadi!
tgkprog
@ tgkprog Saya percaya ini bukan karena alasan historis dan bahwa para perancang C # melakukan itu dengan sengaja untuk memastikan kesalahan umum dari melupakan a breaktidak mungkin dalam C #.
svick
12
Lihat jawaban Eric Lippert pada pertanyaan SO terkait ini. stackoverflow.com/a/1076642/414076 Anda mungkin atau mungkin tidak puas. Bunyinya "karena itulah yang mereka pilih untuk melakukannya pada 1999."
Anthony Pegram

Jawaban:

24

Saya pikir alasan yang baik adalah bahwa dalam setiap kasus lain, ruang lingkup variabel lokal "normal" adalah blok yang dibatasi oleh kurung kurawal ( {}). Variabel lokal yang tidak normal muncul dalam konstruk khusus sebelum pernyataan (yang biasanya merupakan blok), seperti forvariabel loop atau variabel yang dideklarasikan dalamusing .

Satu lagi pengecualian adalah variabel lokal dalam ekspresi kueri LINQ, tetapi itu sama sekali berbeda dari deklarasi variabel lokal normal, jadi saya tidak berpikir ada kemungkinan kebingungan di sana.

Untuk referensi, aturannya ada di §3.7 Cakupan spesifikasi C #:

  • Cakupan variabel lokal yang dideklarasikan dalam deklarasi variabel lokal adalah blok tempat deklarasi terjadi.

  • Ruang lingkup variabel lokal dideklarasikan dalam switch-blok dari switchpernyataan adalah switch-blok .

  • Ruang lingkup variabel lokal dideklarasikan dalam untuk-initializer dari forpernyataan adalah untuk-initializer , yang untuk-kondisi , yang untuk-iterator , dan terdapat pernyataan dari forpernyataan.

  • Lingkup variabel yang dideklarasikan sebagai bagian dari foreach-statement , using-statement , lock-statement, atau query-expression ditentukan oleh perluasan konstruk yang diberikan.

(Meskipun saya tidak sepenuhnya yakin mengapa switchblok disebutkan secara eksplisit, karena tidak memiliki sintaks khusus untuk pernyataan variabel lokal, tidak seperti semua konstruksi yang disebutkan lainnya.)

svick
sumber
1
+1 Bisakah Anda memberikan tautan ke lingkup yang Anda kutip?
pswg
@ pswg Anda dapat menemukan tautan untuk mengunduh spesifikasi di MSDN . (Klik tautan yang mengatakan "Microsoft Developer Network (MSDN)" di halaman itu.)
svick
Jadi saya mencari spesifikasi Java, dan switchestampaknya berperilaku identik dalam hal ini (kecuali untuk memerlukan lompatan pada akhir case's). Tampaknya perilaku ini disalin dari sana. Jadi saya kira jawabannya adalah - casepernyataan tidak membuat blok, mereka hanya mendefinisikan pembagian switchblok - dan karena itu tidak memiliki cakupan sendiri.
pswg
3
Re: Saya tidak sepenuhnya yakin mengapa blok switch secara eksplisit disebutkan - penulis spec hanya pilih-pilih, secara implisit menunjukkan bahwa switch-blok memiliki tata bahasa yang berbeda dari blok biasa.
Eric Lippert
42

Tapi itu benar. Anda dapat membuat cakupan lokal di mana saja dengan membungkus garis dengan{}

switch (criterion.ChangeAction)
{
  case BindingType.Inherited:
    {
      var action = (byte)ChangeAction.Inherit;
      return (x => x.Action == action);
    }
  case BindingType.ExplicitValue:
    {
      var action = (byte)ChangeAction.SetValue;
      return (x => x.Action == action);
    }
  default:
    // TODO: Localize errors
    throw new InvalidOperationException("Invalid criterion.");
}
Mike Koder
sumber
Yup menggunakan ini dan berfungsi seperti yang dijelaskan
Mvision
1
+1 Ini adalah trik yang bagus, tetapi saya akan menerima jawaban svick untuk datang paling dekat dengan menjawab pertanyaan awal saya.
pswg
10

Saya akan mengutip Eric Lippert, yang jawabannya cukup jelas pada subjek:

Pertanyaan yang masuk akal adalah "mengapa ini tidak sah?" Jawaban yang masuk akal adalah "baik, mengapa harus"? Anda dapat memilikinya salah satu dari dua cara. Apakah ini legal:

switch(y) 
{ 
    case 1:  int x = 123; ...  break; 
    case 2:  int x = 456; ...  break; 
}

atau ini legal:

switch(y) 
{
    case 1:  int x = 123; ... break; 
    case 2:  x = 456; ... break; 
}

tetapi Anda tidak dapat memiliki keduanya. Para perancang C # memilih cara kedua sebagai cara yang lebih alami untuk melakukannya.

Keputusan ini dibuat pada 7 Juli 1999, hanya beberapa tahun yang lalu. Komentar dalam catatan dari hari itu sangat singkat, hanya menyatakan "Sebuah switch-case tidak membuat ruang deklarasi sendiri" dan kemudian memberikan beberapa contoh kode yang menunjukkan apa yang berhasil dan yang tidak.

Untuk mencari tahu lebih banyak tentang apa yang ada dalam pikiran para desainer pada hari tertentu, saya harus mengganggu banyak orang tentang apa yang mereka pikirkan sepuluh tahun lalu - dan mengganggunya tentang apa yang pada akhirnya merupakan masalah sepele; Saya tidak akan melakukan itu.

Singkatnya, tidak ada alasan kuat untuk memilih satu cara atau yang lain; keduanya memiliki kelebihan. Tim desain bahasa memilih satu cara karena mereka harus memilih satu; yang mereka pilih tampaknya masuk akal bagi saya.

Jadi, kecuali Anda lebih suka bergaul dengan tim pengembang C # tahun 1999 daripada Eric Lippert, Anda tidak akan pernah tahu alasan pastinya!

Cyril Gandon
sumber
Bukan downvoter, tapi ini komentar yang dibuat pada pertanyaan kemarin .
Jesse C. Slicer
4
Saya melihat itu, tetapi karena komentator tidak memposting jawaban, saya pikir itu bisa menjadi ide yang baik untuk secara eksplisit membuat server dengan mengutip konten dari tautan. Dan saya tidak peduli dengan downvotes! OP menanyakan alasan historis, bukan jawaban "Bagaimana melakukannya".
Cyril Gandon
5

Penjelasannya sederhana - itu karena seperti itu di C. Bahasa seperti C ++, Java dan C # telah menyalin sintaks dan pelingkupan dari pernyataan pergantian demi keakraban.

(Sebagaimana dinyatakan dalam jawaban lain, para pengembang C # tidak memiliki dokumentasi tentang bagaimana keputusan ini mengenai lingkup kasus dibuat. Tetapi prinsip tidak tertulis untuk sintaksis C # adalah bahwa kecuali mereka memiliki alasan kuat untuk melakukannya secara berbeda, mereka menyalin Jawa. )

Di C, pernyataan kasus mirip dengan label-goto. Pernyataan peralihan benar-benar sintaks yang lebih bagus untuk goto yang dikomputasi . Kasing menentukan titik masuk ke dalam blok sakelar. Secara default sisa kode akan dieksekusi, kecuali ada jalan keluar yang eksplisit. Jadi masuk akal jika mereka menggunakan cakupan yang sama.

(Lebih mendasar. Goto tidak terstruktur - mereka tidak mendefinisikan atau membatasi bagian kode, mereka hanya menentukan titik lompatan. Karenanya label goto tidak dapat memperkenalkan ruang lingkup.)

C # mempertahankan sintaksisnya, tetapi memperkenalkan perlindungan terhadap "fall through" dengan meminta jalan keluar setelah setiap klausa kasus (tidak kosong). Tetapi ini mengubah cara kita berpikir tentang peralihan! Kasing sekarang adalah cabang alternatif , seperti cabang di if-else. Ini berarti kita akan mengharapkan setiap cabang untuk mendefinisikan ruang lingkupnya sendiri seperti klausa if atau klausa iterasi.

Jadi singkatnya: Kasus memiliki cakupan yang sama karena ini adalah bagaimana di C. Tetapi tampaknya aneh dan tidak konsisten dalam C # karena kami menganggap kasus sebagai cabang alternatif daripada target goto.

JacquesB
sumber
1
" Kelihatannya aneh dan tidak konsisten dalam C # karena kami menganggap kasus sebagai cabang alternatif daripada target kebagian " Itulah kebingungan yang saya miliki. +1 untuk menjelaskan alasan estetika di balik mengapa rasanya salah di C #.
pswg
4

Cara yang disederhanakan untuk melihat ruang lingkup adalah dengan mempertimbangkan ruang lingkup dengan blok {}.

Karena switchtidak mengandung blok apa pun, itu tidak dapat memiliki cakupan yang berbeda.

Guvante
sumber
3
Bagaimana dengan for (int i = 0; i < n; i++) Write(i); /* legal */ Write(i); /* illegal */? Tidak ada blok, tetapi ada cakupan yang berbeda.
svick
@vick: Seperti yang saya katakan, disederhanakan, ada blok, dibuat oleh forpernyataan itu. switchtidak membuat blok untuk setiap kasus, hanya di tingkat atas. Kesamaannya adalah bahwa setiap pernyataan menciptakan satu blok (dihitung switchsebagai pernyataan).
Guvante
1
Lalu mengapa kamu tidak bisa menghitung casesaja?
svick
1
@vick: Karena casetidak mengandung blok, kecuali jika Anda memutuskan untuk menambahkannya secara opsional. forBerlangsung untuk pernyataan berikutnya kecuali Anda menambahkan {}sehingga selalu memiliki blok, tetapi caseberlangsung sampai sesuatu menyebabkan Anda meninggalkan blok nyata, switchpernyataan.
Guvante