Menghindari voodoo `goto`?

47

Saya memiliki switchstruktur yang memiliki beberapa kasus untuk ditangani. Ini switchberoperasi di atas enumyang menimbulkan masalah kode duplikat melalui nilai gabungan:

// All possible combinations of One - Eight.
public enum ExampleEnum {
    One,
    Two, TwoOne,
    Three, ThreeOne, ThreeTwo, ThreeOneTwo,
    Four, FourOne, FourTwo, FourThree, FourOneTwo, FourOneThree,
          FourTwoThree, FourOneTwoThree
    // ETC.
}

Saat ini switchstruktur menangani setiap nilai secara terpisah:

// All possible combinations of One - Eight.
switch (enumValue) {
    case One: DrawOne; break;
    case Two: DrawTwo; break;
    case TwoOne:
        DrawOne;
        DrawTwo;
        break;
     case Three: DrawThree; break;
     ...
}

Anda mendapatkan idenya di sana. Saat ini saya memecahnya menjadi ifstruktur bertumpuk untuk menangani kombinasi dengan satu baris saja:

// All possible combinations of One - Eight.
if (One || TwoOne || ThreeOne || ThreeOneTwo)
    DrawOne;
if (Two || TwoOne || ThreeTwo || ThreeOneTwo)
    DrawTwo;
if (Three || ThreeOne || ThreeTwo || ThreeOneTwo)
    DrawThree;

Ini menimbulkan masalah evaluasi logis yang sangat panjang yang membingungkan untuk dibaca dan sulit dipertahankan. Setelah mengulangi hal ini, saya mulai berpikir tentang alternatif dan memikirkan gagasan tentang switchstruktur dengan kejatuhan di antara kasus-kasus.

Saya harus menggunakan gotodalam hal itu karena C#tidak memungkinkan jatuh. Namun, itu mencegah rantai logika yang sangat panjang meskipun ia melompat di dalam switchstruktur, dan itu tetap membawa duplikasi kode.

switch (enumVal) {
    case ThreeOneTwo: DrawThree; goto case TwoOne;
    case ThreeTwo: DrawThree; goto case Two;
    case ThreeOne: DrawThree; goto default;
    case TwoOne: DrawTwo; goto default;
    case Two: DrawTwo; break;
    default: DrawOne; break;
}

Ini masih bukan solusi yang cukup bersih dan ada stigma yang terkait dengan gotokata kunci yang ingin saya hindari. Saya yakin harus ada cara yang lebih baik untuk membersihkan ini.


Pertanyaan saya

Apakah ada cara yang lebih baik untuk menangani kasus khusus ini tanpa mempengaruhi keterbacaan dan pemeliharaan?

SelamanyaJ
sumber
28
sepertinya Anda ingin berdebat besar. Tetapi Anda membutuhkan contoh kasus yang baik untuk menggunakan goto. flag enum dengan rapi menyelesaikan yang ini, bahkan jika contoh pernyataan if Anda tidak dapat diterima dan itu terlihat baik bagi saya
Ewan
5
@ Ewan Tidak ada yang menggunakan goto. Kapan terakhir kali Anda melihat kode sumber kernel Linux?
John Douma
6
Anda menggunakan gotoketika struktur tingkat tinggi tidak ada dalam bahasa Anda. Saya terkadang berharap ada fallthru; kata kunci untuk menyingkirkan penggunaan tertentu gototapi oh well.
Yosua
25
Secara umum, jika Anda merasa perlu menggunakan gotobahasa tingkat tinggi seperti C #, maka Anda mungkin mengabaikan banyak alternatif desain dan / atau implementasi lainnya (dan lebih baik). Sangat berkecil hati.
code_dredd
11
Menggunakan "goto case" di C # berbeda dari menggunakan "goto" yang lebih umum, karena itulah cara Anda mencapai fallthrough eksplisit.
Hammerite

Jawaban:

175

Saya menemukan kode sulit dibaca dengan gotopernyataan. Saya akan merekomendasikan penataan yang enumberbeda. Misalnya, jika Anda enumadalah bitfield di mana setiap bit mewakili salah satu pilihan, itu bisa terlihat seperti ini:

[Flags]
public enum ExampleEnum {
    One = 0b0001,
    Two = 0b0010,
    Three = 0b0100
};

Atribut Flags memberi tahu kompiler bahwa Anda sedang menyiapkan nilai yang tidak tumpang tindih. Kode yang memanggil kode ini dapat mengatur bit yang sesuai. Anda kemudian dapat melakukan sesuatu seperti ini untuk memperjelas apa yang terjadi:

if (myEnum.HasFlag(ExampleEnum.One))
{
    CallOne();
}
if (myEnum.HasFlag(ExampleEnum.Two))
{
    CallTwo();
}
if (myEnum.HasFlag(ExampleEnum.Three))
{
    CallThree();
}

Ini membutuhkan kode yang mengatur myEnumuntuk mengatur bitfields dengan benar dan ditandai dengan atribut Flags. Tapi Anda bisa melakukannya dengan mengubah nilai enum dalam contoh Anda menjadi:

[Flags]
public enum ExampleEnum {
    One = 0b0001,
    Two = 0b0010,
    Three = 0b0100,
    OneAndTwo = One | Two,
    OneAndThree = One | Three,
    TwoAndThree = Two | Three
};

Saat Anda menulis angka dalam formulir 0bxxxx, Anda menetapkannya dalam bentuk biner. Jadi Anda dapat melihat bahwa kami menetapkan bit 1, 2, atau 3 (well, secara teknis 0, 1, atau 2, tetapi Anda mendapatkan idenya). Anda juga dapat memberi nama kombinasi dengan menggunakan bitwise ATAU jika kombinasi mungkin sering disatukan.

pengguna1118321
sumber
5
Ini muncul begitu ! Tapi itu pasti C # 7.0 atau lebih baru, kurasa.
user1118321
31
Pokoknya, kita juga bisa melakukannya seperti ini: public enum ExampleEnum { One = 1 << 0, Two = 1 << 1, Three = 1 << 2, OneAndTwo = One | Two, OneAndThree = One | Three, TwoAndThree = Two | Three };. Tidak perlu bersikeras C # 7 +.
Deduplicator
8
Anda dapat menggunakan Enum.HasFlag
IMil
13
The [Flags]atribut tidak sinyal apa pun untuk compiler. Inilah sebabnya mengapa Anda masih harus secara eksplisit mendeklarasikan nilai enum sebagai kekuatan 2.
Joe Sewell
19
@UKMonkey saya sangat tidak setuju. HasFlagadalah istilah deskriptif dan eksplisit untuk operasi yang dilakukan dan abstrak implementasi dari fungsi. Menggunakan &karena umum di semua bahasa lain tidak masuk akal daripada menggunakan bytejenis alih-alih mendeklarasikan enum.
BJ Myers
136

IMO akar masalahnya adalah potongan kode ini seharusnya tidak ada.

Anda tampaknya memiliki tiga kondisi independen, dan tiga tindakan independen yang harus diambil jika kondisi itu benar. Jadi mengapa semua yang disalurkan menjadi satu bagian kode yang membutuhkan tiga bendera Boolean untuk mengatakan apa yang harus dilakukan (apakah Anda mengaburkannya menjadi enum) dan kemudian melakukan kombinasi dari tiga hal independen? Prinsip tanggung jawab tunggal tampaknya memiliki hari libur di sini.

Masukkan panggilan ke tiga fungsi di mana milik (yaitu di mana Anda menemukan perlunya melakukan tindakan) dan mengirimkan kode dalam contoh ini ke tempat sampah.

Jika ada sepuluh flag dan tindakan bukan tiga, apakah Anda akan memperpanjang kode semacam ini untuk menangani 1024 kombinasi berbeda? Saya harap tidak! Jika 1024 terlalu banyak, 8 juga terlalu banyak, untuk alasan yang sama.

alephzero
sumber
10
Faktanya, ada banyak API yang dirancang dengan baik yang memiliki semua jenis flag, opsi, dan berbagai macam parameter lainnya. Beberapa mungkin diteruskan, beberapa memiliki efek langsung, dan yang lain berpotensi diabaikan, tanpa melanggar SRP. Lagi pula, fungsinya bahkan bisa menjadi decoding beberapa input eksternal, siapa tahu? Juga, reductio ad absurdum sering mengarah pada hasil yang absurd, terutama jika titik awalnya bahkan tidak terlalu solid.
Deduplicator
9
Terpilih jawaban ini. Jika Anda hanya ingin berdebat GOTO, itu pertanyaan yang berbeda dari yang Anda tanyakan. Berdasarkan bukti yang disajikan, Anda memiliki tiga persyaratan terpisah, yang tidak boleh digabungkan. Dari sudut pandang SE, saya sampaikan bahwa alephzero adalah jawaban yang benar.
8
@Dupuplikator Satu kali melihat mishmash ini dari beberapa tetapi tidak semua kombinasi 1,2 & 3 menunjukkan bahwa ini bukan "API yang dirancang dengan baik".
user949300
4
Begitu banyak ini. Jawaban lain tidak benar-benar membaik pada apa yang ada dalam pertanyaan. Kode ini perlu ditulis ulang. Tidak ada yang salah dengan menulis lebih banyak fungsi.
only_pro
3
Saya suka jawaban ini, terutama "Jika 1024 terlalu banyak, 8 juga terlalu banyak". Tetapi pada dasarnya "gunakan polimorfisme", bukan? Dan jawaban saya (lebih lemah) semakin banyak omong kosong untuk mengatakan itu. Bisakah saya mempekerjakan orang PR Anda? :-)
user949300
29

Tidak pernah menggunakan gotos adalah salah satu konsep "kebohongan bagi anak-anak" dari ilmu komputer. Ini adalah saran yang tepat 99% dari waktu, dan waktu tidak begitu jarang dan terspesialisasi bahwa itu jauh lebih baik untuk semua orang jika itu hanya dijelaskan kepada coders baru sebagai "jangan menggunakannya".

Jadi ketika harus mereka digunakan? Ada beberapa skenario , tetapi yang mendasar yang tampaknya Anda pukul adalah: ketika Anda mengkode mesin negara . Jika tidak ada ekspresi yang lebih terorganisir dan terstruktur dari algoritma Anda daripada mesin negara, maka ekspresi alami dalam kode melibatkan cabang yang tidak terstruktur, dan tidak ada banyak yang dapat dilakukan tentang apa yang tidak bisa dibilang membuat struktur nyatakan mesin itu sendiri lebih tidak jelas, daripada kurang.

Penulis kompiler mengetahui hal ini, itulah sebabnya mengapa kode sumber untuk sebagian besar kompiler yang mengimplementasikan parser LALR * berisi gotos. Namun, sangat sedikit orang yang akan benar-benar membuat kode analisis parser dan parser sendiri.

* - IIRC, dimungkinkan untuk menerapkan tata bahasa LALL yang sepenuhnya bersifat rekursif tanpa menggunakan tabel lompatan atau pernyataan kontrol tidak terstruktur lainnya, jadi jika Anda benar-benar anti-goto, ini adalah salah satu jalan keluar.


Sekarang pertanyaan selanjutnya adalah, "Apakah ini contoh salah satu dari kasus itu?"

Apa yang saya lihat ketika melihatnya adalah bahwa Anda memiliki tiga kemungkinan kondisi berikutnya tergantung pada pemrosesan kondisi saat ini. Karena salah satu dari mereka ("default") hanyalah sebaris kode, secara teknis Anda bisa menyingkirkannya dengan menempelkan baris kode itu di akhir status yang berlaku. Itu akan membuat Anda turun ke 2 kemungkinan negara bagian berikutnya.

Salah satu yang tersisa ("Tiga") hanya bercabang dari satu tempat yang bisa saya lihat. Jadi Anda bisa menyingkirkannya dengan cara yang sama. Anda akan berakhir dengan kode yang terlihat seperti ini:

switch (exampleValue) {
    case OneAndTwo: i += 3 break;
    case OneAndThree: i += 4 break;
    case Two: i += 2 break;
    case TwoAndThree: i += 5 break;
    case Three: i += 3 break;
    default: i++ break;
}

Namun, sekali lagi ini adalah contoh mainan yang Anda berikan. Dalam kasus di mana "default" memiliki jumlah kode non-sepele di dalamnya, "tiga" ditransisikan ke dari beberapa negara, atau (yang paling penting) pemeliharaan lebih lanjut cenderung menambah atau mempersulit negara , Anda benar-benar akan lebih baik menggunakan gotos (dan mungkin bahkan menyingkirkan struktur enum-case yang menyembunyikan sifat mesin negara dari hal-hal, kecuali ada beberapa alasan yang baik perlu tetap).

TED
sumber
2
@PerpetualJ - Yah, saya tentu senang Anda menemukan sesuatu yang lebih bersih. Mungkin masih menarik, jika itu benar-benar situasi Anda, untuk mencobanya dengan gotos (tanpa case atau enum. Hanya diberi label blok dan gotos) dan lihat bagaimana mereka membandingkannya dalam keterbacaan. Yang sedang berkata, opsi "goto" tidak hanya harus lebih mudah dibaca, tetapi cukup lebih mudah dibaca untuk mencegah argumen dan "memperbaiki" upaya dari pengembang lain, jadi sepertinya Anda menemukan opsi terbaik.
TED
4
Saya tidak percaya bahwa Anda akan "lebih baik menggunakan goto" jika "pemeliharaan lebih lanjut kemungkinan akan menambah atau mempersulit keadaan". Apakah Anda benar-benar mengklaim bahwa kode spaghetti lebih mudah dipelihara daripada kode terstruktur? Ini bertentangan dengan ~ 40 tahun latihan.
user949300
5
@ user949300 - Tidak berlatih menentang pembangunan mesin negara, tidak. Anda cukup membaca komentar saya sebelumnya tentang jawaban ini untuk contoh dunia nyata. Jika Anda mencoba menggunakan kasus C fall-throughs, yang memaksakan struktur artifisial (urutan liniernya) pada algoritma mesin keadaan yang tidak memiliki batasan seperti itu secara inheren, Anda akan menemukan diri Anda dengan kompleksitas yang meningkat secara geometris setiap kali Anda harus mengatur ulang semua pemesanan untuk mengakomodasi negara baru. Jika Anda hanya menyatakan kode dan transisi, seperti yang dibutuhkan algoritma, itu tidak terjadi. Status baru mudah ditambahkan.
TED
8
@ user949300 - Sekali lagi, "~ 40 tahun latihan" kompiler bangunan menunjukkan bahwa goto sebenarnya adalah cara terbaik untuk bagian-bagian dari domain masalah, itulah sebabnya jika Anda melihat kode sumber kompiler, Anda akan hampir selalu menemukan goto.
TED
3
@ user949300 Kode Spaghetti tidak hanya muncul secara inheren ketika menggunakan fungsi atau konvensi tertentu. Itu dapat muncul dalam bahasa apa pun, fungsi, atau konvensi kapan saja. Penyebabnya adalah menggunakan bahasa, fungsi, atau konvensi dalam aplikasi yang tidak dimaksudkan untuk itu dan membuatnya membungkuk ke belakang untuk menariknya. Selalu gunakan alat yang tepat untuk pekerjaan itu, dan ya, terkadang itu berarti menggunakan goto.
Abion47
26

Jawaban terbaik adalah menggunakan polimorfisme .

Jawaban lain, yang, IMO, membuat hal-hal itu lebih jelas dan bisa dibilang lebih pendek :

if (One || OneAndTwo || OneAndThree)
  CallOne();
if (Two || OneAndTwo || TwoAndThree)
  CallTwo();
if (Three || OneAndThree || TwoAndThree)
  CallThree();

goto mungkin pilihan saya yang ke 58 di sini ...

pengguna949300
sumber
5
+1 untuk menyarankan polimorfisme, meskipun idealnya yang mungkin menyederhanakan kode klien menjadi hanya "Panggil ();", menggunakan kirim jangan tanya - untuk mendorong logika keputusan menjauh dari klien yang mengonsumsi dan masuk ke mekanisme kelas dan hierarki.
Erik Eidt
12
Memproklamirkan sesuatu yang pemandangan terbaik yang tak terlihat tentu sangat berani. Tetapi tanpa info tambahan, saya menyarankan sedikit skeptis dan tetap berpikiran terbuka. Mungkin menderita ledakan kombinatorial?
Deduplicator
Wow, saya pikir ini adalah pertama kalinya saya pernah diturunkan karena menyarankan polimorfisme. :-)
user949300
25
Beberapa orang, ketika dihadapkan dengan masalah, mengatakan "Saya hanya akan menggunakan polimorfisme". Sekarang mereka memiliki dua masalah, dan polimorfisme.
Lightness Races dengan Monica
8
Saya benar-benar melakukan tesis Master saya tentang penggunaan polimorfisme runtime untuk menghilangkan gotos di mesin kompiler negara. Ini cukup bisa dilakukan, tetapi sejak itu saya menemukan dalam prakteknya tidak sepadan dengan usaha dalam banyak kasus. Ini membutuhkan satu ton kode pengaturan OO, yang semuanya tentu saja dapat berakhir dengan bug (dan yang jawaban ini dihilangkan).
TED
10

Kenapa tidak ini:

public enum ExampleEnum {
    One = 0, // Why not?
    OneAndTwo,
    OneAndThree,
    Two,
    TwoAndThree,
    Three
}
int[] COUNTS = { 1, 3, 4, 2, 5, 3 }; // Whatever

int ComputeExampleValue(int i, ExampleEnum exampleValue) {
    return i + COUNTS[(int)exampleValue];
}

OK, saya setuju, ini peretasan (saya bukan pengembang C # btw, jadi maafkan saya untuk kodenya), tetapi dari sudut pandang efisiensi, ini harus dilakukan? Menggunakan enum sebagai indeks array valid C #.

Laurent Grégoire
sumber
3
Iya. Contoh lain dari mengganti kode dengan data (yang biasanya diinginkan, untuk kecepatan, struktur dan perawatan).
Peter - Pasang kembali Monica
2
Itu ide yang bagus untuk edisi pertanyaan terbaru, tidak diragukan lagi. Dan itu dapat digunakan untuk beralih dari enum pertama (kisaran nilai yang padat) ke bendera tindakan yang kurang lebih independen yang harus dilakukan secara umum.
Deduplicator
Saya tidak memiliki akses ke C # compiler, tapi mungkin kode dapat dibuat lebih aman menggunakan kode semacam ini: int[ExempleEnum.length] COUNTS = { 1, 3, 4, 2, 5, 3 };?
Laurent Grégoire
7

Jika Anda tidak bisa atau tidak ingin menggunakan flag, gunakan fungsi rekursif ekor. Dalam mode rilis 64bit, kompiler akan menghasilkan kode yang sangat mirip dengan gotopernyataan Anda . Anda hanya tidak harus menghadapinya.

int ComputeExampleValue(int i, ExampleEnum exampleValue) {
    switch (exampleValue) {
        case One: return i + 1;
        case OneAndTwo: return ComputeExampleValue(i + 2, ExampleEnum.One);
        case OneAndThree: return ComputeExampleValue(i + 3, ExampleEnum.One);
        case Two: return i + 2;
        case TwoAndThree: return ComputeExampleValue(i + 2, ExampleEnum.Three);
        case Three: return i + 3;
   }
}
Philipp
sumber
Itu solusi unik! +1 Saya pikir saya tidak perlu melakukannya dengan cara ini, tetapi bagus untuk pembaca masa depan!
Abadi
2

Solusi yang diterima baik-baik saja dan merupakan solusi konkret untuk masalah Anda. Namun, saya ingin menempatkan alternatif, solusi yang lebih abstrak.

Dalam pengalaman saya, penggunaan enum untuk menentukan aliran logika adalah bau kode karena sering merupakan tanda desain kelas yang buruk.

Saya bertemu dengan contoh dunia nyata dari hal ini terjadi dalam kode yang saya kerjakan tahun lalu. Pengembang asli telah menciptakan kelas tunggal yang melakukan logika impor dan ekspor, dan beralih di antara keduanya berdasarkan enum. Sekarang kodenya mirip dan memiliki beberapa kode duplikat, tetapi cukup berbeda sehingga hal itu membuat kodenya secara signifikan lebih sulit dibaca dan hampir tidak mungkin untuk diuji. Saya akhirnya refactoring itu menjadi dua kelas yang terpisah, yang menyederhanakan keduanya dan benar-benar membiarkan saya melihat dan menghilangkan sejumlah bug yang tidak dilaporkan.

Sekali lagi, saya harus menyatakan bahwa menggunakan enum untuk mengontrol aliran logika sering merupakan masalah desain. Dalam kasus umum, Enums harus digunakan sebagian besar untuk memberikan nilai-nilai aman-ramah-konsumen di mana nilai-nilai yang mungkin didefinisikan dengan jelas. Mereka lebih baik digunakan sebagai properti (misalnya, sebagai ID kolom dalam tabel) daripada sebagai mekanisme kontrol logika.

Mari kita perhatikan masalah yang disajikan dalam pertanyaan. Saya tidak benar-benar tahu konteksnya di sini, atau apa yang diwakili oleh enum ini. Apakah itu kartu gambar? Menggambar? Menggambar darah? Apakah pesanan itu penting? Saya juga tidak tahu betapa pentingnya kinerja. Jika kinerja atau memori sangat penting, maka solusi ini mungkin tidak akan menjadi yang Anda inginkan.

Bagaimanapun, mari kita pertimbangkan enum:

// All possible combinations of One - Eight.
public enum ExampleEnum {
    One,
    Two,
    TwoOne,
    Three,
    ThreeOne,
    ThreeTwo,
    ThreeOneTwo
}

Apa yang kami miliki di sini adalah sejumlah nilai enum yang berbeda yang mewakili konsep bisnis yang berbeda.

Apa yang bisa kita gunakan adalah abstraksi untuk menyederhanakan banyak hal.

Mari kita pertimbangkan antarmuka berikut:

public interface IExample
{
  void Draw();
}

Kami kemudian dapat mengimplementasikan ini sebagai kelas abstrak:

public abstract class ExampleClassBase : IExample
{
  public abstract void Draw();
  // other common functionality defined here
}

Kita dapat memiliki kelas konkret untuk mewakili Menggambar Satu, dua dan tiga (yang demi argumen memiliki logika yang berbeda). Ini berpotensi menggunakan kelas dasar yang didefinisikan di atas, tapi saya berasumsi bahwa konsep DrawOne berbeda dari konsep yang diwakili oleh enum:

public class DrawOne
{
  public void Draw()
  {
    // Drawing logic here
  }
}

public class DrawTwo
{
  public void Draw()
  {
    // Drawing two logic here
  }
}

public class DrawThree
{
  public void Draw()
  {
    // Drawing three logic here
  }
}

Dan sekarang kami memiliki tiga kelas terpisah yang dapat disusun untuk menyediakan logika untuk kelas-kelas lain.

public class One : ExampleClassBase
{
  private DrawOne drawOne;

  public One(DrawOne drawOne)
  {
    this.drawOne = drawOne;
  }

  public void Draw()
  {
    this.drawOne.Draw();
  }
}

public class TwoOne : ExampleClassBase
{
  private DrawOne drawOne;
  private DrawTwo drawTwo;

  public One(DrawOne drawOne, DrawTwo drawTwo)
  {
    this.drawOne = drawOne;
    this.drawTwo = drawTwo;
  }

  public void Draw()
  {
    this.drawOne.Draw();
    this.drawTwo.Draw();
  }
}

// the other six classes here

Pendekatan ini jauh lebih bertele-tele. Tetapi memang memiliki kelebihan.

Pertimbangkan kelas berikut, yang berisi bug:

public class ThreeTwoOne : ExampleClassBase
{
  private DrawOne drawOne;
  private DrawTwo drawTwo;
  private DrawThree drawThree;

  public One(DrawOne drawOne, DrawTwo drawTwo, DrawThree drawThree)
  {
    this.drawOne = drawOne;
    this.drawTwo = drawTwo;
    this.drawThree = drawThree;
  }

  public void Draw()
  {
    this.drawOne.Draw();
    this.drawTwo.Draw();
  }
}

Seberapa mudah untuk menemukan panggilan drawThree.Draw () yang hilang? Dan jika pesanan penting, urutan panggilan undian juga sangat mudah dilihat dan diikuti.

Kerugian dari pendekatan ini:

  • Setiap satu dari delapan opsi yang disajikan memerlukan kelas terpisah
  • Ini akan menggunakan lebih banyak memori
  • Ini akan membuat kode Anda lebih dangkal
  • Kadang-kadang pendekatan ini tidak mungkin, meskipun beberapa variasi di antaranya mungkin

Keuntungan dari pendekatan ini:

  • Setiap kelas ini sepenuhnya dapat diuji; karena
  • Kompleksitas siklomatik dari metode draw rendah (dan secara teori saya bisa mengejek kelas DrawOne, DrawTwo atau DrawThree jika perlu)
  • Metode menggambar dapat dipahami - pengembang tidak harus mengikat otak mereka dalam simpul untuk mengetahui apa yang dilakukan metode tersebut
  • Bug mudah dikenali dan sulit ditulis
  • Kelas disusun menjadi kelas yang lebih tinggi, artinya mendefinisikan kelas ThreeThreeThree mudah dilakukan

Pertimbangkan pendekatan ini (atau yang serupa) setiap kali Anda merasa perlu memiliki kode kontrol logika kompleks yang dituliskan dalam pernyataan kasus. Masa depan kamu akan senang kamu lakukan.

Stephen
sumber
Ini adalah jawaban yang ditulis dengan sangat baik dan saya setuju sepenuhnya dengan pendekatan tersebut. Dalam kasus khusus saya, sesuatu yang bertele-tele ini akan berlebihan sampai tingkat yang tak terbatas mengingat kode sepele di belakang masing-masing. Untuk meletakkannya dalam perspektif, bayangkan kode hanya melakukan sebuah Console.WriteLine (n). Itu praktis setara dengan apa yang saya kerjakan dengan kode. Dalam 90% dari semua kasus lain, solusi Anda jelas merupakan jawaban terbaik saat mengikuti OOP.
PerpetualJ
Ya titik adil, pendekatan ini tidak berguna dalam setiap situasi. Saya ingin meletakkannya di sini karena itu umum bagi pengembang untuk terpaku pada satu pendekatan khusus untuk memecahkan masalah dan kadang-kadang membayar untuk mundur dan mencari alternatif.
Stephen
Saya sepenuhnya setuju dengan itu, itu sebenarnya alasan saya berakhir di sini karena saya mencoba untuk memecahkan masalah skalabilitas dengan sesuatu yang pengembang lain tulis karena kebiasaan.
PerpetualJ
0

Jika Anda bermaksud menggunakan sakelar di sini, kode Anda sebenarnya akan lebih cepat jika Anda menangani setiap kasing secara terpisah

switch(exampleValue)
{
    case One:
        i++;
        break;
    case Two:
        i += 2;
        break;
    case OneAndTwo:
    case Three:
        i+=3;
        break;
    case OneAndThree:
        i+=4;
        break;
    case TwoAndThree:
        i+=5;
        break;
}

hanya operasi aritmatika tunggal dilakukan dalam setiap kasus

juga karena orang lain telah menyatakan jika Anda mempertimbangkan untuk menggunakan gotos Anda mungkin harus memikirkan kembali algoritma Anda (meskipun saya akan mengakui bahwa kurangnya kasus C # s meskipun bisa menjadi alasan untuk menggunakan goto). Lihat karya terkenal Edgar Dijkstra "Go To Statement Dianggap Berbahaya"

Captian Jelas
sumber
3
Saya akan mengatakan ini adalah ide bagus untuk seseorang yang dihadapkan dengan masalah yang lebih sederhana jadi terima kasih telah mempostingnya. Dalam kasus saya ini tidak begitu sederhana.
PerpetualJ
Juga, kita tidak memiliki masalah saat ini yang dialami Edgar pada saat menulis makalahnya; kebanyakan orang saat ini bahkan tidak memiliki alasan yang sah untuk membenci goto selain itu membuat kode sulit dibaca. Nah, dalam semua kejujuran, jika itu disalahgunakan maka ya, kalau tidak itu terperangkap ke metode yang mengandung sehingga Anda tidak bisa benar-benar membuat kode spaghetti. Mengapa menghabiskan upaya untuk memperbaiki fitur bahasa? Saya akan mengatakan bahwa goto kuno dan Anda harus memikirkan solusi lain dalam 99% kasus penggunaan; tetapi jika Anda membutuhkannya, itulah gunanya.
PerpetualJ
Ini tidak akan skala dengan baik ketika ada banyak boolean.
user949300
0

Untuk contoh khusus Anda, karena semua yang benar-benar Anda inginkan dari penghitungan adalah indikator do / do-not untuk masing-masing langkah, solusi yang menulis ulang tiga ifpernyataan Anda lebih disukai daripada switch, dan ada baiknya Anda menjadikannya sebagai jawaban yang diterima .

Tetapi jika Anda memiliki beberapa logika yang lebih kompleks yang tidak berjalan dengan sangat bersih, maka saya masih menemukan gotos dalam switchpernyataan itu membingungkan. Saya lebih suka melihat sesuatu seperti ini:

switch (enumVal) {
    case ThreeOneTwo: DrawThree; DrawTwo; DrawOne; break;
    case ThreeTwo:    DrawThree; DrawTwo; break;
    case ThreeOne:    DrawThree; DrawOne; break;
    case TwoOne:      DrawTwo; DrawOne; break;
    case Two:         DrawTwo; break;
    default:          DrawOne; break;
}

Ini tidak sempurna tapi saya pikir lebih baik begini daripada dengan gotos. Jika urutan peristiwa sangat panjang dan saling menduplikasi sehingga tidak masuk akal untuk menguraikan urutan lengkap untuk setiap kasus, saya lebih suka subrutin daripada gotountuk mengurangi duplikasi kode.

David K
sumber
0

Saya tidak yakin ada orang yang benar-benar memiliki alasan untuk membenci kata kunci goto hari ini. Ini jelas kuno dan tidak diperlukan dalam 99% kasus penggunaan, tetapi itu adalah fitur bahasa karena suatu alasan.

Alasan membenci gotokata kunci adalah kode suka

if (someCondition) {
    goto label;
}

string message = "Hello World!";

label:
Console.WriteLine(message);

Ups! Jelas itu tidak akan berhasil. The messagevariabel tidak didefinisikan dalam kode jalan. Jadi C # tidak akan lulus itu. Tapi itu mungkin implisit. Mempertimbangkan

object.setMessage("Hello World!");

label:
object.display();

Dan anggap itu displaykemudian memiliki WriteLinepernyataan di dalamnya.

Jenis bug ini mungkin sulit ditemukan karena gotomengaburkan jalur kode.

Ini adalah contoh yang disederhanakan. Asumsikan bahwa contoh nyata tidak akan terlalu jelas. Mungkin ada lima puluh baris kode antara label:dan penggunaan message.

Bahasa dapat membantu memperbaikinya dengan membatasi bagaimana gotodapat digunakan, hanya turun dari blok. Namun C # gototidak terbatas seperti itu. Itu dapat melompati kode. Lebih lanjut, jika Anda akan membatasi goto, itu juga baik untuk mengubah nama. Bahasa lain digunakan breakuntuk turun dari blok, baik dengan nomor (blok untuk keluar) atau label (di salah satu blok).

Konsep gotoadalah instruksi bahasa mesin tingkat rendah. Tetapi seluruh alasan mengapa kita memiliki bahasa tingkat yang lebih tinggi adalah agar kita terbatas pada abstraksi tingkat yang lebih tinggi, misalnya ruang lingkup variabel.

Semua yang dikatakan, jika Anda menggunakan C # gotodi dalam switchpernyataan untuk melompat dari kasus ke kasus, itu cukup tidak berbahaya. Setiap kasing sudah menjadi titik masuk. Saya masih berpikir bahwa menyebutnya gotokonyol dalam situasi itu, karena mengonfigurasikan penggunaan tidak berbahaya ini gotodengan bentuk yang lebih berbahaya. Saya lebih suka mereka menggunakan sesuatu seperti continueitu. Tetapi karena suatu alasan, mereka lupa bertanya kepada saya sebelum mereka menulis bahasanya.

mdfst13
sumber
2
Dalam C#bahasa ini Anda tidak dapat melompati deklarasi variabel. Sebagai bukti, luncurkan aplikasi konsol dan masukkan kode yang Anda berikan di pos Anda. Anda akan mendapatkan kesalahan kompiler di label Anda yang menyatakan bahwa kesalahan tersebut menggunakan 'pesan' variabel lokal yang belum ditetapkan . Dalam bahasa yang diizinkan, ini merupakan masalah yang valid, tetapi tidak dalam bahasa C #.
PerpetualJ
0

Ketika Anda memiliki banyak pilihan ini (dan bahkan lebih, seperti yang Anda katakan), maka mungkin itu bukan kode tetapi data.

Buat pemetaan kamus nilai-nilai enum untuk tindakan, dinyatakan baik sebagai fungsi atau sebagai tipe enum sederhana yang mewakili tindakan. Kemudian kode Anda dapat dirubah menjadi pencarian kamus sederhana, diikuti dengan memanggil nilai fungsi atau beralih pada pilihan yang disederhanakan.

alexis
sumber
-7

Gunakan lingkaran dan perbedaan antara istirahat dan melanjutkan.

do {
  switch (exampleValue) {
    case OneAndTwo: i += 2; break;
    case OneAndThree: i += 3; break;
    case Two: i += 2; continue;
    case TwoAndThree: i += 2;
    // dropthrough
    case Three: i += 3; continue;
    default: break;
  }
  i++;
} while(0);
QuentinUK
sumber