Ganti pernyataan kesalahan dalam C #?

365

Beralih pernyataan fallthrough adalah salah satu alasan utama pribadi saya untuk cinta switchvs if/else ifkonstruksi. Contohnya ada di sini:

static string NumberToWords(int number)
{
    string[] numbers = new string[] 
        { "", "one", "two", "three", "four", "five", 
          "six", "seven", "eight", "nine" };
    string[] tens = new string[] 
        { "", "", "twenty", "thirty", "forty", "fifty", 
          "sixty", "seventy", "eighty", "ninety" };
    string[] teens = new string[]
        { "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
          "sixteen", "seventeen", "eighteen", "nineteen" };

    string ans = "";
    switch (number.ToString().Length)
    {
        case 3:
            ans += string.Format("{0} hundred and ", numbers[number / 100]);
        case 2:
            int t = (number / 10) % 10;
            if (t == 1)
            {
                ans += teens[number % 10];
                break;
            }
            else if (t > 1)
                ans += string.Format("{0}-", tens[t]);
        case 1:
            int o = number % 10;
            ans += numbers[o];

            break;
        default:
            throw new ArgumentException("number");
    }
    return ans;
}

Orang pintar merasa ngeri karena string[]harus dinyatakan di luar fungsi: well, mereka, ini hanya sebuah contoh.

Kompiler gagal dengan kesalahan berikut:

Kontrol tidak dapat dialihkan dari satu label case ('case 3:') ke yang lain
Kontrol tidak dapat dialihkan dari satu label case ('case 2:') ke yang lain

Mengapa? Dan adakah cara untuk mendapatkan perilaku semacam ini tanpa memiliki tiga ifs?

Matthew Scharley
sumber

Jawaban:

648

(Salin / tempel jawaban yang saya berikan di tempat lain )

Falling through switch- cases dapat dicapai dengan tidak memiliki kode dalam case(lihat case 0), atau menggunakan bentuk khusus goto case(lihat case 1) atau goto default(lihat case 2):

switch (/*...*/) {
    case 0: // shares the exact same code as case 1
    case 1:
        // do something
        goto case 2;
    case 2:
        // do something else
        goto default;
    default:
        // do something entirely different
        break;
}
Alex Lyman
sumber
121
Saya pikir, dalam contoh khusus ini, kebotakan tidak dianggap berbahaya.
Thomas Owens
78
Sial - Saya sudah pemrograman dengan C # sejak awal 1.0, dan saya belum pernah melihat ini sampai sekarang. Hanya untuk menunjukkan, Anda belajar hal-hal baru setiap hari.
Erik Forbes
37
Semua baik-baik saja, Erik. Satu-satunya alasan / saya / saya tahu tentang hal itu adalah bahwa saya adalah pembuat teori nerd yang membaca spesifikasi ECMA-334 dengan kaca pembesar.
Alex Lyman
13
@Dancrumb: Pada saat fitur itu ditulis, C # belum menambahkan kata kunci "lunak" (seperti 'hasil', 'var', 'dari', dan 'pilih'), sehingga mereka memiliki tiga opsi nyata: 1 ) membuat 'fallthrough' kata kunci yang sulit (Anda tidak dapat menggunakannya sebagai nama variabel), 2) menulis kode yang diperlukan untuk mendukung kata kunci lunak, 3) menggunakan kata kunci yang sudah dipesan. # 1 adalah masalah besar bagi kode porting; # 2 adalah tugas teknik yang cukup besar, dari apa yang saya mengerti; dan opsi yang mereka gunakan, # 3 memiliki manfaat tambahan: pengembang lain yang membaca kode setelah fakta dapat mempelajari fitur dari konsep dasar goto
Alex Lyman
10
Semua ini berbicara tentang kata kunci baru / khusus untuk terobosan eksplisit. Tidak bisakah mereka menggunakan kata kunci 'lanjutkan'? Dengan kata lain. Keluar dari sakelar atau lanjutkan ke kasing berikutnya (jatuh).
Tal Even-Tov
44

"Mengapa" adalah untuk menghindari kejatuhan yang tidak disengaja, yang untuk itu saya berterima kasih. Ini bukan sumber bug yang umum di C dan Java.

Solusinya adalah menggunakan goto, mis

switch (number.ToString().Length)
{
    case 3:
        ans += string.Format("{0} hundred and ", numbers[number / 100]);
        goto case 2;
    case 2:
    // Etc
}

Secara umum desain switch / casing agak disayangkan menurut saya. Ini terjebak terlalu dekat dengan C - ada beberapa perubahan berguna yang dapat dibuat dalam hal pelingkupan dll. Bisa dibilang sebuah saklar yang lebih cerdas yang dapat melakukan pencocokan pola dll akan sangat membantu, tapi itu benar-benar berubah dari sakelar menjadi "memeriksa urutan kondisi" - pada titik mana nama yang berbeda mungkin akan dipanggil.

Jon Skeet
sumber
1
Inilah perbedaan antara sakelar dan if / elseif dalam pikiranku. Switch adalah untuk memeriksa berbagai keadaan variabel tunggal, sedangkan jika / elseif dapat digunakan untuk memeriksa sejumlah hal yang terhubung, tetapi tidak harus tunggal, atau variabel yang sama.
Matthew Scharley
2
Jika itu untuk mencegah jatuh secara tidak sengaja maka saya merasa peringatan kompiler akan lebih baik. Sama seperti Anda memiliki satu jika pernyataan if Anda memiliki tugas:if (result = true) { }
Tal Even-Tov
3
@ TalEven-Tov: Peringatan kompiler harus benar-benar untuk kasus di mana Anda hampir selalu dapat memperbaiki kode menjadi lebih baik. Secara pribadi saya lebih suka melanggar implisit, jadi tidak akan menjadi masalah untuk memulai, tapi itu masalah yang berbeda.
Jon Skeet
26

Switch fallthrough secara historis merupakan salah satu sumber utama bug dalam perangkat lunak modern. Perancang bahasa memutuskan untuk mewajibkan melompat di akhir kasing, kecuali jika Anda langsung ke kasing langsung tanpa proses.

switch(value)
{
    case 1:// this is still legal
    case 2:
}
Coincoin
sumber
23
Saya juga tidak pernah mengerti mengapa itu bukan "kasus 1, 2:"
BCS
11
@ David Pfeffer: Ya itu, dan begitu juga case 1, 2:dalam bahasa yang memungkinkan itu. Apa yang saya tidak akan pernah mengerti adalah mengapa bahasa modern tidak akan memilih untuk membiarkan itu terjadi.
BCS
@ HCS dengan pernyataan goto, beberapa opsi yang dipisahkan koma bisa sulit untuk ditangani?
penguat
@pengut: mungkin lebih akurat untuk mengatakan bahwa itu case 1, 2:adalah label tunggal tetapi dengan beberapa nama. - FWIW, saya pikir sebagian besar bahasa yang melarang jatuh bukan kasus khusus "label kasus berurutan" indium melainkan memperlakukan label kasus sebagai anotasi pada pernyataan berikutnya dan memerlukan pernyataan terakhir sebelum pernyataan dilabeli dengan (satu atau lebih banyak) label case menjadi lompatan.
BCS
22

Untuk menambah jawaban di sini, saya pikir ada baiknya mempertimbangkan pertanyaan yang berlawanan dalam hubungannya dengan ini, yaitu. mengapa C membiarkan jatuh-di tempat pertama?

Setiap bahasa pemrograman tentu saja melayani dua tujuan:

  1. Berikan instruksi ke komputer.
  2. Tinggalkan catatan niat si programmer.

Karena itu, penciptaan bahasa pemrograman apa pun merupakan keseimbangan antara cara terbaik untuk melayani dua tujuan ini. Di satu sisi, semakin mudah untuk berubah menjadi instruksi komputer (apakah itu kode mesin, bytecode seperti IL, atau instruksi ditafsirkan pada eksekusi) maka lebih mampu bahwa proses kompilasi atau interpretasi akan menjadi efisien, dapat diandalkan dan kompak dalam keluaran. Diambil secara ekstrem, tujuan ini menghasilkan penulisan kami yang tepat di assembly, IL, atau bahkan op-code mentah, karena kompilasi termudah adalah di mana tidak ada kompilasi sama sekali.

Sebaliknya, semakin banyak bahasa yang mengekspresikan niat programmer, daripada sarana yang digunakan untuk tujuan itu, semakin dimengerti program baik saat menulis maupun selama pemeliharaan.

Sekarang, switchselalu bisa dikompilasi dengan mengubahnya menjadi rantai if-elseblok yang setara atau serupa, tetapi ia dirancang sebagai memungkinkan kompilasi menjadi pola perakitan umum tertentu di mana seseorang mengambil nilai, menghitung offset dari itu (apakah dengan melihat sebuah tabel diindeks oleh hash sempurna dari nilai, atau dengan aritmatika aktual pada nilai *). Perlu dicatat pada titik ini bahwa hari ini, kompilasi C # kadang-kadang akan berubah switchmenjadi setara if-else, dan kadang-kadang menggunakan pendekatan lompat berbasis hash (dan juga dengan C, C ++, dan bahasa lain dengan sintaks yang sebanding).

Dalam hal ini ada dua alasan bagus untuk membiarkan jatuh:

  1. Itu terjadi secara alami: jika Anda membangun tabel lompatan ke dalam satu set instruksi, dan salah satu dari batch instruksi sebelumnya tidak mengandung semacam lompatan atau pengembalian, maka eksekusi akan secara alami berkembang ke batch berikutnya. Mengizinkan fall-through adalah apa yang akan "terjadi begitu saja" jika Anda mengubah switch-menggunakan C-tabel-menggunakan kode mesin.

  2. Coder yang menulis dalam assembly sudah terbiasa dengan yang setara: ketika menulis tabel lompat dengan tangan di assembly, mereka harus mempertimbangkan apakah suatu blok kode tertentu akan berakhir dengan return, lompatan di luar tabel, atau hanya melanjutkan ke blok berikutnya. Karena itu, meminta pembuat kode untuk menambahkan secara eksplisit breakjika diperlukan juga "alami" untuk pembuat kode.

Oleh karena itu, pada saat itu, merupakan upaya yang masuk akal untuk menyeimbangkan dua tujuan bahasa komputer karena berkaitan dengan kode mesin yang diproduksi, dan ekspresi kode sumber.

Namun, empat dekade kemudian, segalanya tidak persis sama, karena beberapa alasan:

  1. Coder di C hari ini mungkin memiliki sedikit atau tidak ada pengalaman perakitan. Coder dalam banyak bahasa gaya C lainnya bahkan lebih kecil kemungkinannya (terutama Javascript!). Konsep "apa yang digunakan orang dari majelis" tidak lagi relevan.
  2. Peningkatan dalam optimisasi berarti bahwa kemungkinan switchmenjadi berubah if-elsekarena dianggap pendekatan yang paling efisien, atau berubah menjadi varian esoterik khusus dari pendekatan jump-table yang lebih tinggi. Pemetaan antara pendekatan tingkat tinggi dan rendah tidak sekuat dulu.
  3. Pengalaman telah menunjukkan bahwa fall-through cenderung menjadi kasus minoritas daripada norma (sebuah studi kompiler Sun menemukan 3% dari switchblok menggunakan fall-through selain beberapa label pada blok yang sama, dan dianggap bahwa penggunaan kasus di sini berarti bahwa 3% ini sebenarnya jauh lebih tinggi dari biasanya). Jadi bahasa yang dipelajari membuat yang tidak biasa lebih siap melayani daripada yang umum.
  4. Pengalaman menunjukkan bahwa fall-through cenderung menjadi sumber masalah baik dalam kasus-kasus di mana hal itu secara tidak sengaja dilakukan, dan juga dalam kasus-kasus di mana fall-through yang benar terlewatkan oleh seseorang yang menjaga kode. Yang terakhir ini merupakan tambahan halus untuk bug yang terkait dengan fall-through, karena meskipun kode Anda bebas bug, fall-through Anda masih dapat menyebabkan masalah.

Terkait dengan dua poin terakhir, pertimbangkan kutipan berikut dari edisi K&R saat ini:

Jatuh dari satu kasus ke kasus lain tidak kuat, rentan terhadap disintegrasi ketika program dimodifikasi. Dengan pengecualian beberapa label untuk perhitungan tunggal, fall-throughs harus digunakan dengan hemat, dan dikomentari.

Sebagai bentuk yang baik, beri istirahat setelah kasus terakhir (default di sini) meskipun secara logis tidak perlu. Suatu hari ketika kasus lain ditambahkan di akhir, program defensif ini akan menyelamatkan Anda.

Jadi, dari mulut kuda, jatuh dalam C adalah masalah. Ini dianggap praktik yang baik untuk selalu mendokumentasikan kegagalan dengan komentar, yang merupakan penerapan prinsip umum bahwa seseorang harus mendokumentasikan di mana seseorang melakukan sesuatu yang tidak biasa, karena itulah yang akan menyebabkan pemeriksaan kode kemudian dan / atau membuat kode Anda terlihat seperti itu. memiliki bug pemula di dalamnya padahal sebenarnya benar.

Dan ketika Anda memikirkannya, kode seperti ini:

switch(x)
{
  case 1:
   foo();
   /* FALLTHRU */
  case 2:
    bar();
    break;
}

Adalah menambahkan sesuatu untuk membuat fall-through eksplisit dalam kode, itu bukan sesuatu yang dapat dideteksi (atau yang ketidakhadirannya dapat dideteksi) oleh kompiler.

Dengan demikian, fakta bahwa on harus eksplisit dengan fall-through dalam C # tidak menambah hukuman bagi orang yang menulis dengan baik dalam bahasa gaya C lainnya, karena mereka sudah akan eksplisit dalam fall-throughs mereka. †

Akhirnya, penggunaan di gotosini sudah menjadi norma dari C dan bahasa lain seperti:

switch(x)
{
  case 0:
  case 1:
  case 2:
    foo();
    goto below_six;
  case 3:
    bar();
    goto below_six;
  case 4:
    baz();
    /* FALLTHRU */
  case 5:
  below_six:
    qux();
    break;
  default:
    quux();
}

Dalam kasus seperti ini di mana kita ingin sebuah blok dimasukkan dalam kode yang dieksekusi untuk nilai selain hanya yang membawa satu ke blok sebelumnya, maka kita sudah harus menggunakan goto. (Tentu saja, ada cara dan cara untuk menghindarinya dengan persyaratan yang berbeda tetapi itu berlaku untuk hampir semua yang berkaitan dengan pertanyaan ini). Karena itu, C # dibangun dengan cara yang sudah biasa untuk menangani satu situasi di mana kami ingin menekan lebih dari satu blok kode dalam a switch, dan hanya menggeneralisasikannya untuk mencakup fall-through juga. Itu juga membuat kedua kasus lebih nyaman dan mendokumentasikan diri sendiri, karena kita harus menambahkan label baru di C tetapi dapat menggunakan casesebagai label di C #. Di C # kita bisa menghilangkan below_sixlabel dan menggunakan goto case 5yang lebih jelas tentang apa yang kita lakukan. (Kami juga harus menambahkanbreakuntuk default, yang saya tinggalkan hanya untuk membuat kode C di atas jelas bukan kode C #).

Singkatnya karena itu:

  1. C # tidak lagi berkaitan dengan output kompiler yang tidak dioptimalkan secara langsung seperti kode C lakukan 40 tahun yang lalu (juga tidak C hari ini), yang membuat salah satu inspirasi dari jatuh-melalui menjadi tidak relevan.
  2. C # tetap kompatibel dengan C tidak hanya memiliki implisit break, untuk lebih mudah belajar bahasa oleh mereka yang akrab dengan bahasa yang sama, dan porting lebih mudah.
  3. C # menghapus kemungkinan sumber bug atau kode disalahpahami yang telah didokumentasikan dengan baik sebagai penyebab masalah selama empat dekade terakhir.
  4. C # membuat praktik terbaik yang ada dengan C (dokumen jatuh melalui) dapat diberlakukan oleh kompiler.
  5. C # membuat kasus yang tidak biasa satu dengan kode yang lebih eksplisit, kasus yang biasa satu dengan kode yang baru saja ditulis secara otomatis.
  6. C # menggunakan gotopendekatan berbasis yang sama untuk memukul blok yang sama dari yang berbedacase label yang seperti yang digunakan dalam C. Itu hanya menggeneralisasikannya untuk beberapa kasus lain.
  7. C # membuat gotopendekatan berbasis itu lebih nyaman, dan lebih jelas, daripada di C, dengan memungkinkan casepernyataan untuk bertindak sebagai label.

Semua dalam semua, keputusan desain yang cukup masuk akal


* Beberapa bentuk BASIC akan memungkinkan seseorang untuk melakukan hal-hal seperti GOTO (x AND 7) * 50 + 240yang sementara rapuh dan karenanya kasus persuasif khusus untuk pelarangan goto, memang berfungsi untuk menunjukkan setara bahasa yang lebih tinggi dari jenis cara kode tingkat yang lebih rendah dapat membuat lompatan berdasarkan aritmatika pada nilai, yang jauh lebih masuk akal ketika itu adalah hasil kompilasi daripada sesuatu yang harus dipertahankan secara manual. Implementasi Perangkat Duff khususnya cocok untuk kode mesin yang setara atau IL karena setiap blok instruksi akan sering memiliki panjang yang sama tanpa perlu penambahan noppengisi.

† Perangkat Duff muncul lagi di sini, sebagai pengecualian yang masuk akal. Fakta bahwa dengan pola itu dan yang serupa ada pengulangan operasi berfungsi untuk membuat penggunaan fall-through relatif jelas bahkan tanpa komentar eksplisit untuk efek itu.

Jon Hanna
sumber
17

Anda dapat 'membuka label kasus' http://www.blackwasp.co.uk/CSharpGoto.aspx

Pernyataan goto adalah perintah sederhana yang tanpa syarat mentransfer kendali program ke pernyataan lain. Perintah ini sering dikritik dengan beberapa pengembang menganjurkan penghapusan dari semua bahasa pemrograman tingkat tinggi karena dapat menyebabkan kode spageti . Ini terjadi ketika ada begitu banyak pernyataan goto atau pernyataan lompatan serupa sehingga kode menjadi sulit dibaca dan dipelihara. Namun, ada programmer yang menunjukkan bahwa pernyataan goto, ketika digunakan dengan hati-hati, memberikan solusi yang elegan untuk beberapa masalah ...

kenny
sumber
8

Mereka meninggalkan perilaku ini dengan desain untuk menghindari ketika itu tidak digunakan oleh kehendak tetapi menyebabkan masalah.

Ini dapat digunakan hanya jika tidak ada pernyataan di bagian case, seperti:

switch (whatever)
{
    case 1:
    case 2:
    case 3: boo; break;
}
Biri
sumber
4

Mereka mengubah pernyataan beralih (dari C / Java / C ++) perilaku untuk c #. Saya kira alasannya adalah bahwa orang lupa tentang kejatuhan dan kesalahan terjadi. Satu buku yang saya baca dikatakan menggunakan goto untuk mensimulasikan, tetapi ini kedengarannya bukan solusi yang baik bagi saya.

Ken
sumber
2
C # mendukung goto, tetapi tidak membuat terobosan? Wow. Dan bukan hanya itu saja. C # adalah satu-satunya bahasa yang saya tahu berperilaku seperti ini.
Matthew Scharley
Awalnya saya tidak suka, tapi "fall-thru" benar-benar resep untuk bencana (terutama di antara para programmer junior.) Seperti yang telah ditunjukkan banyak orang, C # masih memungkinkan fall-thru untuk jalur kosong (yang merupakan mayoritas dari case.) "Kenny" memposting tautan yang menyoroti penggunaan Goto yang elegan dengan switch-case.
Pretzel
Menurut saya itu bukan masalah besar. 99% dari waktu saya tidak ingin jatuh dan saya telah dibakar oleh bug di masa lalu.
Ken
1
"ini kedengarannya bukan solusi yang bagus untukku" - maaf mendengarnya tentangmu, karena itu untuk apa goto case. Keuntungannya daripada fallthrough adalah eksplisit. Bahwa beberapa orang di sini keberatan goto casehanya menunjukkan bahwa mereka diindoktrinasi melawan "kebohongan" tanpa pemahaman tentang masalah ini dan tidak mampu berpikir sendiri. Ketika Dijkstra menulis "GOTO Dianggap Berbahaya", dia berbicara bahasa yang tidak memiliki cara lain untuk mengubah aliran kontrol.
Jim Balter
@ JimBalter dan kemudian berapa banyak orang yang mengutip Dijkstra tentang hal itu akan mengutip Knuth bahwa "optimisasi prematur adalah akar dari semua kejahatan" meskipun kutipan itu adalah ketika Knuth secara eksplisit menulis tentang seberapa berguna gotodapat ketika mengoptimalkan kode?
Jon Hanna
1

Anda dapat mencapai jatuh melalui seperti c ++ dengan kata kunci goto.

EX:

switch(num)
{
   case 1:
      goto case 3;
   case 2:
      goto case 3;
   case 3:
      //do something
      break;
   case 4:
      //do something else
      break;
   case default:
      break;
}
Dai Tran
sumber
9
Andai saja ada yang memposting itu dua tahun sebelumnya!
0

Pernyataan lompatan seperti istirahat diperlukan setelah setiap blok kasus, termasuk blok terakhir apakah itu pernyataan kasus atau pernyataan default. Dengan satu pengecualian, (tidak seperti pernyataan saklar C ++), C # tidak mendukung penurunan implisit dari satu label kasus ke label kasus lainnya. Satu pengecualian adalah jika pernyataan kasus tidak memiliki kode.

- Dokumentasi C # switch ()

Powerlord
sumber
1
Saya menyadari perilaku ini didokumentasikan, saya ingin tahu MENGAPA seperti itu adanya, dan setiap alternatif untuk mendapatkan perilaku lama.
Matthew Scharley
0

Setelah setiap pernyataan kasus membutuhkan pernyataan break atau kebagian bahkan jika itu adalah kasus default.

Shilpa gulati
sumber
2
Andai saja ada yang memposting itu dua tahun sebelumnya!
1
@Poldie itu lucu pertama kali ... Shilpa Anda tidak perlu istirahat atau kebagian untuk setiap kasus, hanya untuk setiap kasus dengan kode itu sendiri. Anda dapat memiliki banyak kasus yang membagikan kode dengan baik.
Maverick
0

Hanya catatan singkat untuk menambahkan bahwa kompiler untuk Xamarin benar-benar salah dan ini memungkinkan fallthrough. Seharusnya sudah diperbaiki, tetapi belum dirilis. Menemukan ini dalam beberapa kode yang benar-benar jatuh dan kompiler tidak mengeluh.


sumber
0

switch (C # Referensi) mengatakan

C # membutuhkan akhir dari bagian switch, termasuk yang terakhir,

Jadi, Anda juga perlu menambahkan break;ke defaultbagian Anda , jika tidak maka akan ada kesalahan kompiler.

gm2008
sumber
Terima kasih, ini membantu saya;)
CareTaker22
-1

C # tidak mendukung jatuh dengan pernyataan switch / case. Tidak yakin mengapa, tetapi benar-benar tidak ada dukungan untuk itu. Tautan

BFree
sumber
Ini sangat salah. Lihat jawaban lain ... efek dari kesalahan implisit dapat diperoleh dengan eksplisit goto case . Ini adalah pilihan disengaja, desain yang bijaksana oleh desainer C #.
Jim Balter
-11

Anda lupa menambahkan "break;" pernyataan ke dalam case 3. Dalam kasus 2 Anda menulisnya ke dalam blok if. Karena itu coba ini:

case 3:            
{
    ans += string.Format("{0} hundred and ", numbers[number / 100]);
    break;
}


case 2:            
{
    int t = (number / 10) % 10;            
    if (t == 1)            
    {                
        ans += teens[number % 10];                
    }            
    else if (t > 1)                
    {
        ans += string.Format("{0}-", tens[t]);        
    }
    break;
}

case 1:            
{
    int o = number % 10;            
    ans += numbers[o];            
    break;        
}

default:            
{
    throw new ArgumentException("number");
}
Marcus
sumber
2
Ini menghasilkan output yang sangat salah. Saya meninggalkan pernyataan switch dengan desain. Pertanyaannya adalah mengapa kompiler C # melihat ini sebagai kesalahan ketika hampir tidak ada bahasa lain yang memiliki batasan ini.
Matthew Scharley
5
Sungguh kegagalan yang menakjubkan untuk dipahami. Dan Anda memiliki 5 tahun untuk menghapus ini dan masih belum melakukannya? Mindboggling.
Jim Balter