เหตุใดคำสั่งสวิตช์จึงถูกออกแบบมาเพื่อให้หยุดพัก


139

รับงบสวิตช์ง่าย

switch (int)
{
    case 1 :
    {
        printf("1\n");
        break;
    }

    case 2 : 
    {
        printf("2\n");
    }

    case 3 : 
    {
        printf("3\n");
    }
}

การขาดคำสั่ง break ในกรณีที่ 2 หมายความว่าการดำเนินการจะดำเนินการต่อภายในรหัสสำหรับกรณีที่ 3 นี่ไม่ใช่อุบัติเหตุ มันถูกออกแบบมาอย่างนั้น เหตุใดการตัดสินใจนี้จึงเกิดขึ้น สิ่งนี้มีประโยชน์อะไรเมื่อเทียบกับการมี semantic break อัตโนมัติสำหรับบล็อก เหตุผลคืออะไร

คำตอบ:


150

ดูเหมือนว่าคำตอบหลายข้อจะมุ่งเน้นไปที่ความสามารถในการผ่านเป็นเหตุผลในการกำหนดbreakคำสั่ง

ฉันเชื่อว่ามันเป็นเพียงความผิดพลาดเนื่องจากส่วนใหญ่เนื่องจากเมื่อ C ถูกออกแบบไม่มีประสบการณ์มากพอที่จะใช้โครงสร้างเหล่านี้

Peter Van der Linden ทำคดีนี้ในหนังสือของเขา "Expert C Programming":

เราวิเคราะห์แหล่งคอมไพเลอร์ของ Sun C เพื่อดูว่ามีการใช้ค่าเริ่มต้นในการล่มสลายบ่อยแค่ไหน ส่วนหน้าคอมไพเลอร์ Sun ANSI C มีคำสั่งสวิตช์ 244 รายการแต่ละรายการมีค่าเฉลี่ยเจ็ดกรณี ล้มลงเกิดขึ้นเพียง 3% ของทุกกรณี

กล่าวอีกนัยหนึ่งพฤติกรรมสวิตช์ปกติคือ ผิด 97% ของเวลา ไม่ใช่แค่ในคอมไพเลอร์ - ในทางกลับกันที่การล่มสลายถูกนำมาใช้ในการวิเคราะห์นี้มันมักจะเกิดขึ้นสำหรับสถานการณ์ที่เกิดขึ้นบ่อยครั้งในคอมไพเลอร์มากกว่าในซอฟต์แวร์อื่น ๆ เช่นเมื่อรวบรวมตัวดำเนินการที่อาจมีหนึ่งหรือสองตัวถูกดำเนินการ :

switch (operator->num_of_operands) {
    case 2: process_operand( operator->operand_2);
              /* FALLTHRU */

    case 1: process_operand( operator->operand_1);
    break;
}

คดีที่ผ่านมาได้รับการยอมรับอย่างกว้างขวางว่าเป็นข้อบกพร่องที่มีแม้กระทั่งการประชุมความคิดเห็นพิเศษดังที่แสดงไว้ด้านบนซึ่งบอกว่าเป็นผ้าสำลี "นี่เป็นหนึ่งใน 3% ของกรณีที่ผู้คนต้องการผ่าน"

ฉันคิดว่ามันเป็นความคิดที่ดีสำหรับ C # ที่จะต้องใช้คำสั่ง jump อย่างชัดเจนในตอนท้ายของแต่ละ case case (ในขณะที่ยังอนุญาตให้ซ้อน case case หลาย ๆ ตัวได้ ใน C # คุณยังสามารถมีกรณีหนึ่งล้มลงไปอีก - คุณเพียงแค่ต้องทำให้ผ่านชัดเจนโดยการกระโดดไปยังกรณีถัดไปโดยใช้gotoคุณเพียงแค่ต้องทำให้ฤดูใบไม้ร่วงผ่านอย่างชัดเจนโดยการกระโดดกรณีต่อไปโดยใช้

มันแย่มากที่ Java ไม่ได้มีโอกาสหยุดพักจากซีแมนทิกส์ซี


แน่นอนฉันคิดว่าพวกเขาดำเนินการอย่างเรียบง่าย ภาษาอื่นบางภาษารองรับกรณีที่ซับซ้อนมากขึ้น (ช่วง, ค่าหลายค่า, สตริง ... ) ในราคาที่อาจมีประสิทธิภาพ
PhiLho

Java อาจไม่ต้องการทำลายนิสัยและทำให้เกิดความสับสน สำหรับพฤติกรรมที่แตกต่างพวกเขาจะต้องใช้ความหมายที่แตกต่างกัน นักออกแบบ Java เสียโอกาสในการแยกจาก C อย่างไรก็ตาม
PhiLho

@PhiLho - ฉันคิดว่าคุณน่าจะใกล้เคียงกับความจริงด้วย "ความเรียบง่ายของการใช้งาน"
Michael Burr

หากคุณกำลังใช้การกระโดดอย่างชัดเจนผ่าน GOTO มันไม่ได้เป็นเพียงแค่การใช้คำสั่ง IF แบบต่างๆหรือไม่
DevinB

3
แม้ปาสกาลใช้สวิตช์โดยไม่หยุด คอมไพเลอร์ -Coder C ไม่คิดอย่างไรเกี่ยวกับมัน @@
Chan Le

30

ในหลาย ๆ วิธี c เป็นเพียงส่วนต่อประสานที่สะอาดกับสำนวนประกอบมาตรฐาน เมื่อเขียนการควบคุมการไหลของการขับเคลื่อนตารางกระโดดโปรแกรมเมอร์มีทางเลือกในการล้มหรือกระโดดออกจาก "โครงสร้างการควบคุม" และการกระโดดออกต้องมีคำสั่งที่ชัดเจน

ดังนั้น c ทำสิ่งเดียวกัน ...


1
ฉันรู้ว่าหลายคนพูดแบบนั้น แต่ฉันคิดว่ามันไม่ใช่ภาพที่สมบูรณ์ C มักจะเป็นอินเทอร์เฟซที่น่าเกลียดในแอสเซมบลี antipatterns 1. น่าเกลียด: ข้อความแทนนิพจน์ "ประกาศสะท้อนถึงการใช้งาน" สรรเสริญ copy-paste เป็นกลไกในการสร้างต้นแบบและพกพา พิมพ์ระบบทางที่ซับซ้อนเกินไป; ส่งเสริมข้อบกพร่อง . (ต่อในความคิดเห็นต่อไป)NULL
แฮร์ริสัน

2. Antipatterns: ไม่ได้ให้สหภาพที่ติดแท็ก "สะอาด" ซึ่งเป็นหนึ่งในสองสำนวนการแสดงข้อมูลพื้นฐาน (Knuth, vol 1, ch 2) แทน "แฮ็กสหภาพแรงงานที่ไม่ได้ติดแท็ก" ตัวเลือกนี้ทำให้ผู้คนคิดถึงข้อมูลมาหลายทศวรรษแล้ว และNULสตริงที่ผ่านการทำลายล้างเป็นเพียงความคิดที่เลวร้ายที่สุดเท่าที่เคยมีมา
แฮร์ริสัน

@HarrisonKlaperman: ไม่มีวิธีการจัดเก็บสตริงที่สมบูรณ์แบบ ปัญหาส่วนใหญ่ที่เกี่ยวข้องกับสตริงที่สิ้นสุดด้วยค่า null จะไม่เกิดขึ้นหากรูทีนที่ยอมรับสตริงนั้นยอมรับพารามิเตอร์ที่มีความยาวสูงสุดและจะเกิดขึ้นกับรูทีนที่เก็บสตริงที่ทำเครื่องหมายความยาวไว้ในบัฟเฟอร์ขนาดคงที่โดยไม่ต้องยอมรับพารามิเตอร์ขนาดบัฟเฟอร์ . การออกแบบคำสั่งสวิตช์ที่กรณีเป็นเพียงฉลากอาจดูแปลกตาทันสมัย ​​แต่ก็ไม่ได้เลวร้ายยิ่งกว่าการออกแบบของDOห่วงFortran
supercat

ถ้าฉันตัดสินใจที่จะเขียนตารางกระโดดในการชุมนุมฉันจะใช้ค่ากรณีและอย่างน่าอัศจรรย์เปลี่ยนให้เป็นตัวห้อยตารางกระโดดข้ามไปยังตำแหน่งนั้นและรันรหัส หลังจากนั้นฉันจะไม่กระโดดเข้าไปในคดีถัดไป ฉันจะข้ามไปยังที่อยู่ที่เหมือนกันซึ่งเป็นทางออกของทุกกรณี ความคิดที่ว่าฉันจะกระโดดหรือตกลงไปในร่างของคดีต่อไปก็โง่ ไม่มีกรณีใช้งานสำหรับรูปแบบนั้น
EvilTeach

2
ในขณะที่ผู้คนในปัจจุบันมีความคุ้นเคยกับการทำความสะอาดและปกป้องตัวเองของไอดอลและฟีเจอร์ภาษาที่ป้องกันไม่ให้ตนเองยิงเข้าไปในเท้านี่เป็นสิ่งที่เหลืออยู่จากพื้นที่ที่ไบต์มีราคาแพง (C เริ่มย้อนกลับไปก่อนปี 1970) หากรหัสของคุณต้องมีขนาดไม่เกิน 1024 ไบต์คุณจะได้รับความพึงพอใจล่วงหน้าอย่างมากในการใช้ชิ้นส่วนของรหัสซ้ำ การนำรหัสมาใช้ใหม่โดยเริ่มต้นที่จุดเข้าใช้งานที่ต่างกันที่ใช้จุดสิ้นสุดเดียวกันเป็นกลไกหนึ่งในการบรรลุเป้าหมายนี้
rpy

23

ในการใช้อุปกรณ์ของดัฟฟ์เห็นได้ชัดว่า:

dsend(to, from, count)
char *to, *from;
int count;
{
    int n = (count + 7) / 8;
    switch (count % 8) {
    case 0: do { *to = *from++;
    case 7:      *to = *from++;
    case 6:      *to = *from++;
    case 5:      *to = *from++;
    case 4:      *to = *from++;
    case 3:      *to = *from++;
    case 2:      *to = *from++;
    case 1:      *to = *from++;
               } while (--n > 0);
    }
}

3
ฉันชอบอุปกรณ์ของดัฟฟ์ สง่างามและชั่วร้ายอย่างรวดเร็ว
dicroce

7
ใช่ แต่ต้องแสดงทุกครั้งที่มีคำสั่ง switch บน SO หรือไม่
billjamesdev

คุณพลาดวงเล็บปีกกาปิดสองอัน ;-)
Toon Krijthe

18

หากกรณีถูกออกแบบมาเพื่อทำลายโดยปริยายแล้วคุณจะไม่ได้เข้าใจผิด

case 0:
case 1:
case 2:
    // all do the same thing.
    break;
case 3:
case 4:
    // do something different.
    break;
default:
    // something else entirely.

หากสวิตช์ถูกออกแบบมาให้แยกออกโดยปริยายหลังจากทุกกรณีคุณจะไม่มีทางเลือก โครงสร้างตัวเรือนสวิตช์ได้รับการออกแบบในลักษณะที่ยืดหยุ่นได้มากขึ้น


12
คุณสามารถสร้างภาพสวิตช์ที่แตกโดยปริยาย แต่มีคำหลักที่ "ผิดพลาด" อึดอัดใจ แต่สามารถทำงานได้
dmckee --- ผู้ดูแลอดีตลูกแมว

มันจะดีกว่านี้ได้อย่างไร ฉันนึกภาพกรณีคำสั่งให้ทำงานด้วยวิธีนี้บ่อยกว่า "บล็อกของรหัสต่อกรณี" ... นั่นคือบล็อก if / then / else
billjamesdev

ฉันจะเพิ่มที่ในคำถาม, ล้อมกรอบขอบเขต {} ในแต่ละกรณีบล็อกจะเพิ่มความสับสนเนื่องจากดูเหมือนว่ารูปแบบของคำสั่ง "ในขณะที่"
Matthew Smith

1
@ การเรียกเก็บเงิน: มันจะแย่กว่านั้นฉันคิดว่า แต่มันจะจัดการกับข้อร้องเรียนที่เกิดขึ้นโดย Mike B: การตกถึงแม้ว่า (นอกเหนือจากหลายกรณีเดียวกัน) เป็นเหตุการณ์ที่หายากและไม่ควรเป็นพฤติกรรมเริ่มต้น
dmckee --- ผู้ดูแลอดีตลูกแมว

1
ฉันทิ้งวงเล็บปีกกาให้สั้นกระชับด้วยหลายงบพวกเขาควรจะอยู่ที่นั่น ฉันเห็นด้วยว่าการตกหล่นควรใช้เฉพาะเมื่อมีกรณีเหมือนกันทุกประการ มันสร้างความสับสนว่าเป็นนรกเมื่อคดีสร้างขึ้นในกรณีก่อนหน้านี้โดยใช้ข้อผิดพลาด
Bill the Lizard

15

คำสั่ง case ในคำสั่ง switch เป็นเพียงเลเบล

เมื่อคุณเปลี่ยนค่าข้อความสั่ง switch จะข้ามไปที่ป้ายกำกับโดยมีค่าที่ตรงกัน

ซึ่งหมายความว่าจำเป็นต้องมีการหยุดพักเพื่อหลีกเลี่ยงการส่งผ่านไปยังรหัสใต้ป้ายกำกับถัดไป

สำหรับเหตุผลที่ว่าทำไมมันจึงถูกนำไปใช้ด้วยวิธีนี้ลักษณะการล้มของคำสั่งสวิตช์อาจมีประโยชน์ในบางสถานการณ์ ตัวอย่างเช่น:

case optionA:
    // optionA needs to do its own thing, and also B's thing.
    // Fall-through to optionB afterwards.
    // Its behaviour is a superset of B's.
case optionB:
    // optionB needs to do its own thing
    // Its behaviour is a subset of A's.
    break;
case optionC:
    // optionC is quite independent so it does its own thing.
    break;

2
มีสองปัญหาใหญ่: 1) ลืมbreakที่มันต้องการ 2) หากคำสั่ง case มีการเปลี่ยนแปลงคำสั่งของพวกเขา fallthrough อาจส่งผลให้เกิดการเรียกใช้กรณีที่ไม่ถูกต้อง ดังนั้นฉันพบว่าการจัดการของ C # ดีขึ้นมาก (ชัดเจนgoto caseสำหรับการฝ่าฝืนยกเว้นป้ายกำกับกรณีที่ว่างเปล่า)
แดเนียลโรส

@DanielRose: 1) มีวิธีที่จะลืมbreakใน C # ด้วย - วิธีที่ง่ายที่สุดคือเมื่อคุณไม่ต้องการcaseทำอะไร แต่ลืมที่จะเพิ่มbreak(บางทีคุณอาจถูกห่อหุ้มด้วยความคิดเห็นที่อธิบายหรือถูกเรียกออกไปบ้าง งานอื่น ๆ ): การดำเนินการจะล้มลงไปcaseด้านล่าง 2) การสนับสนุนgoto caseคือการให้กำลังใจการเข้ารหัส "สปาเก็ตตี้" ที่ไม่มีโครงสร้าง - คุณสามารถจบลงด้วยวงจรที่ไม่ได้ตั้งใจ ( case A: ... goto case B; case B: ... ; goto case A;) โดยเฉพาะอย่างยิ่งเมื่อกรณีถูกแยกในไฟล์และยากที่จะรวมกัน ใน C ++ fall-through ถูกแปลเป็นภาษาท้องถิ่น
Tony Delroy

8

เพื่อให้สิ่งต่าง ๆ เช่น:

switch(foo) {
case 1:
    /* stuff for case 1 only */
    if (0) {
case 2:
    /* stuff for case 2 only */
    }
    /* stuff for cases 1 and 2 */
case 3:
    /* stuff for cases 1, 2, and 3 */
}

คิดว่าcaseคำหลักเป็นgotoป้ายกำกับและมีความเป็นธรรมชาติมากขึ้น


8
ที่if(0)ในตอนท้ายของกรณีแรกคือความชั่วร้ายและควรใช้ในกรณีที่มีวัตถุประสงค์คือการทำให้งงงวยรหัส
แดเนียลโรส

11
ฉันคิดว่าคำตอบทั้งหมดนี้เป็นการออกกำลังกายในความชั่วร้าย :-)
R .. GitHub หยุดช่วยน้ำแข็ง

3

มันกำจัดการทำสำเนารหัสเมื่อหลายกรณีจำเป็นต้องเรียกใช้รหัสเดียวกัน (หรือรหัสเดียวกันตามลำดับ)

เนื่องจากในระดับภาษาแอสเซมบลีมันไม่สนใจว่าคุณแบ่งระหว่างแต่ละคนหรือไม่มีค่าใช้จ่ายเป็นศูนย์สำหรับตกผ่านกรณีใด ๆ ดังนั้นทำไมไม่อนุญาตให้พวกเขาเนื่องจากพวกเขามีข้อได้เปรียบที่สำคัญในบางกรณี


2

ฉันบังเอิญทำงานในกรณีของการกำหนดค่าในเวกเตอร์ให้กับ structs: มันต้องทำในลักษณะที่ถ้าเวกเตอร์ข้อมูลสั้นกว่าจำนวนสมาชิกข้อมูลใน struct สมาชิกที่เหลือจะยังคงอยู่ใน ค่าเริ่มต้นของพวกเขา ในกรณีดังกล่าวการละเว้นbreakมีประโยชน์มาก

switch (nShorts)
{
case 4: frame.leadV1    = shortArray[3];
case 3: frame.leadIII   = shortArray[2];
case 2: frame.leadII    = shortArray[1];
case 1: frame.leadI     = shortArray[0]; break;
default: TS_ASSERT(false);
}

0

ตามที่ระบุไว้จำนวนมากที่นี่อนุญาตให้บล็อกรหัสเดียวทำงานได้หลายกรณี สิ่งนี้ควรจะเป็นเหตุการณ์ที่เกิดขึ้นโดยทั่วไปสำหรับคำสั่งสวิตช์ของคุณมากกว่า "บล็อกของรหัสต่อกรณี" ที่คุณระบุในตัวอย่างของคุณ

หากคุณมีบล็อกของรหัสต่อกรณีโดยไม่มี fall-through บางทีคุณควรลองใช้ if-elseif-else block เพราะดูจะเหมาะสมกว่า

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.