การใช้ goto อย่างถูกต้องสำหรับการจัดการข้อผิดพลาดใน C?


95

คำถามนี้เป็นผลมาจากการสนทนาที่น่าสนใจที่ programming.reddit.com เมื่อไม่นานมานี้ โดยทั่วไปแล้วจะเดือดตามรหัสต่อไปนี้:

int foo(int bar)
{
    int return_value = 0;
    if (!do_something( bar )) {
        goto error_1;
    }
    if (!init_stuff( bar )) {
        goto error_2;
    }
    if (!prepare_stuff( bar )) {
        goto error_3;
    }
    return_value = do_the_thing( bar );
error_3:
    cleanup_3();
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

การใช้gotoที่นี่ดูเหมือนจะเป็นวิธีที่ดีที่สุดส่งผลให้รหัสที่สะอาดและมีประสิทธิภาพที่สุดในบรรดาความเป็นไปได้ทั้งหมดหรืออย่างน้อยก็ดูเหมือนว่าสำหรับฉัน อ้างถึง Steve McConnell ในCode Complete :

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

การสนับสนุนอื่นสำหรับแนวทางนี้มาจากหนังสือLinux Device Driversในส่วนนี้

คุณคิดอย่างไร? กรณีนี้ใช้ได้กับgotoใน C หรือไม่? คุณต้องการวิธีการอื่นซึ่งสร้างรหัสที่ซับซ้อนและ / หรือมีประสิทธิภาพน้อยกว่า แต่หลีกเลี่ยงgotoหรือไม่?



@Eli: ทำไมคุณไม่ลบแท็กและวางฟังก์ชัน (cleanup_3 ();) ไว้ในวงเล็บของ if?

@ อากิโตะ: หมายความว่าไง? คุณช่วยโพสต์ข้อเสนอแนะของคุณเป็นคำตอบพร้อมตัวอย่างโค้ดที่สมบูรณ์ได้ไหม
Eli Bendersky

@EliBendersky: โปรดดูคำตอบของฉัน

สิ่งหนึ่งที่ฉันเกลียดที่สุดจาก Visual Basic (รวม VBS และ VB6) คือon error goto errorระบบจัดการข้อผิดพลาด :)
Manu343726

คำตอบ:


64

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


1
เขาไม่สามารถลบแท็กเหล่านั้นและใส่ฟังก์ชั่นโดยตรงลงใน if ได้หรือไม่มันจะไม่สามารถอ่านได้มากขึ้น?

8
@StartupCrazy ฉันรู้ว่านี่อายุหลายปีแล้ว แต่เพื่อความถูกต้องของโพสต์ในเว็บไซต์นี้ฉันจะชี้ให้เห็นว่าเขาไม่สามารถทำไม่ได้ หากเขาได้รับข้อผิดพลาดที่ goto error3 ในโค้ดของเขาเขาจะเรียกใช้ clean up 1 2 และ 3 ในโซลูชัน sugeested ของคุณเขาจะเรียกใช้ cleanup 3 เท่านั้นเขาสามารถซ้อนสิ่งต่าง ๆ ได้ แต่นั่นจะเป็นเพียงแอนติแพตเทิร์นของลูกศรสิ่งที่โปรแกรมเมอร์ควรหลีกเลี่ยง .
gbtimmon

18

ตามกฎทั่วไปการหลีกเลี่ยง goto เป็นความคิดที่ดี แต่การละเมิดที่แพร่หลายเมื่อ Dijkstra เขียนเป็นครั้งแรกว่า 'GOTO ถือว่าเป็นอันตราย' ไม่ได้ข้ามความคิดของคนส่วนใหญ่เป็นตัวเลือกในทุกวันนี้

สิ่งที่คุณร่างไว้เป็นวิธีแก้ปัญหาโดยทั่วไปสำหรับการจัดการข้อผิดพลาด - เป็นเรื่องปกติสำหรับฉันตราบใดที่ใช้อย่างระมัดระวัง

ตัวอย่างเฉพาะของคุณสามารถทำให้ง่ายขึ้นได้ดังนี้ (ขั้นตอนที่ 1):

int foo(int bar)
{
    int return_value = 0;
    if (!do_something(bar)) {
        goto error_1;
    }
    if (!init_stuff(bar)) {
        goto error_2;
    }
    if (prepare_stuff(bar))
    {
        return_value = do_the_thing(bar);
        cleanup_3();
    }
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

ดำเนินกระบวนการต่อไป:

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar))
    {   
        if (init_stuff(bar))
        {
            if (prepare_stuff(bar))
            {
                return_value = do_the_thing(bar);
                cleanup_3();
            }
            cleanup_2();
        }
        cleanup_1();
    }
    return return_value;
}

นี่คือฉันเชื่อว่าเทียบเท่ากับรหัสเดิม สิ่งนี้ดูสะอาดเป็นพิเศษเนื่องจากโค้ดดั้งเดิมนั้นสะอาดและจัดระเบียบได้ดี บ่อยครั้งที่ส่วนของรหัสไม่เป็นระเบียบเรียบร้อย (แม้ว่าฉันจะยอมรับข้อโต้แย้งที่ควรจะเป็น) ตัวอย่างเช่นมักมีสถานะที่จะส่งผ่านไปยังรูทีนการเริ่มต้น (การตั้งค่า) มากกว่าที่แสดงดังนั้นจึงมีสถานะที่จะส่งผ่านไปยังรูทีนการล้างข้อมูลมากขึ้นด้วย


24
ใช่โซลูชันที่ซ้อนกันเป็นหนึ่งในทางเลือกที่ทำงานได้ น่าเสียดายที่มันทำงานได้น้อยลงเมื่อระดับการซ้อนลึกลงไป
Eli Bendersky

4
@eliben: เห็นด้วย - แต่การซ้อนที่ลึกขึ้นอาจเป็น (อาจเป็น) ข้อบ่งชี้ว่าคุณต้องแนะนำฟังก์ชันเพิ่มเติมหรือมีขั้นตอนการเตรียมการเพิ่มเติมหรือปรับรหัสของคุณใหม่ ฉันสามารถโต้แย้งได้ว่าฟังก์ชันจัดเตรียมแต่ละฟังก์ชันควรทำการตั้งค่าเรียกสิ่งต่อไปในเครือข่ายและทำการล้างข้อมูลของตนเอง มันแปลงานนั้น - คุณอาจบันทึกฟังก์ชันการล้างข้อมูลสามอย่าง นอกจากนี้บางส่วนขึ้นอยู่กับว่ามีการใช้ฟังก์ชันการตั้งค่าหรือการล้างข้อมูล (ใช้งานได้) ในลำดับการโทรอื่น ๆ หรือไม่
Jonathan Leffler

6
น่าเสียดายที่สิ่งนี้ใช้ไม่ได้กับลูป - หากข้อผิดพลาดเกิดขึ้นภายในลูปแสดงว่า goto นั้นสะอาดกว่าทางเลือกในการตั้งค่าและตรวจสอบแฟล็กและคำสั่ง 'break' (ซึ่งเป็นเพียง gotos ที่ปลอมตัวมาอย่างชาญฉลาด)
Adam Rosenfield

1
@ สมิ ธ เหมือนขับรถโดยไม่มีเสื้อเกราะกันกระสุนมากกว่า
strager

6
ฉันรู้ว่าฉันกำลังร่ายมนต์อยู่ที่นี่ แต่ฉันพบว่าคำแนะนำนี้ค่อนข้างแย่ - ควรหลีกเลี่ยงรูปแบบการต่อต้านลูกศร
KingRadical

16

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

แนวคิดพื้นฐานคือสำหรับการดำเนินการล้างข้อมูลทุกครั้งที่อาจต้องดำเนินการมีตัวแปรจากค่าที่เราสามารถบอกได้ว่าการล้างข้อมูลจำเป็นต้องทำหรือไม่

ฉันจะแสดงgotoเวอร์ชันก่อนเพราะใกล้เคียงกับรหัสในคำถามเดิมมากกว่า

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;


    /*
     * Prepare
     */
    if (do_something(bar)) {
        something_done = 1;
    } else {
        goto cleanup;
    }

    if (init_stuff(bar)) {
        stuff_inited = 1;
    } else {
        goto cleanup;
    }

    if (prepare_stuff(bar)) {
        stufF_prepared = 1;
    } else {
        goto cleanup;
    }

    /*
     * Do the thing
     */
    return_value = do_the_thing(bar);

    /*
     * Clean up
     */
cleanup:
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

ข้อดีอย่างหนึ่งของสิ่งนี้จากเทคนิคอื่น ๆ ก็คือหากลำดับของฟังก์ชันการเริ่มต้นมีการเปลี่ยนแปลงการล้างข้อมูลที่ถูกต้องจะยังคงเกิดขึ้นตัวอย่างเช่นใช้switchวิธีการที่อธิบายไว้ในคำตอบอื่นหากลำดับของการเริ่มต้นเปลี่ยนแปลงไปดังนั้นswitchจะต้องได้รับการแก้ไขอย่างรอบคอบเพื่อหลีกเลี่ยงการพยายามล้างบางสิ่งที่ไม่ได้เริ่มต้นจริงตั้งแต่แรก

ตอนนี้บางคนอาจโต้แย้งว่าวิธีนี้เพิ่มตัวแปรพิเศษจำนวนมาก - และในกรณีนี้ก็เป็นจริง - แต่ในทางปฏิบัติมักเป็นตัวแปรที่มีอยู่แล้วติดตามหรือสามารถทำการติดตามสถานะที่ต้องการได้ ตัวอย่างเช่นหากprepare_stuff()เป็นการเรียกไปที่malloc()หรือถึงopen()จริงตัวแปรที่ถือตัวชี้ที่ส่งคืนหรือตัวบอกไฟล์สามารถใช้ได้ตัวอย่างเช่น:

int fd = -1;

....

fd = open(...);
if (fd == -1) {
    goto cleanup;
}

...

cleanup:

if (fd != -1) {
    close(fd);
}

ตอนนี้หากเราติดตามสถานะข้อผิดพลาดเพิ่มเติมด้วยตัวแปรเราสามารถหลีกเลี่ยงได้gotoทั้งหมดและยังคงล้างข้อมูลได้อย่างถูกต้องโดยไม่ต้องมีการเยื้องที่ลึกและลึกยิ่งเราต้องการการเริ่มต้นมากขึ้น:

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;
    int oksofar = 1;


    /*
     * Prepare
     */
    if (oksofar) {  /* NB This "if" statement is optional (it always executes) but included for consistency */
        if (do_something(bar)) {
            something_done = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (init_stuff(bar)) {
            stuff_inited = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (prepare_stuff(bar)) {
            stuff_prepared = 1;
        } else {
            oksofar = 0;
        }
    }

    /*
     * Do the thing
     */
    if (oksofar) {
        return_value = do_the_thing(bar);
    }

    /*
     * Clean up
     */
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

อีกครั้งมีการวิพากษ์วิจารณ์ที่อาจเกิดขึ้นนี้:

  • ประสิทธิภาพการทำงาน "ถ้า" เหล่านั้นไม่เจ็บใช่หรือไม่? ไม่ - เพราะในกรณีที่ประสบความสำเร็จคุณต้องทำการตรวจสอบทั้งหมดอยู่ดี (มิฉะนั้นคุณจะไม่ได้ตรวจสอบกรณีข้อผิดพลาดทั้งหมด) และในกรณีความล้มเหลวคอมไพเลอร์ส่วนใหญ่จะปรับลำดับของการif (oksofar)ตรวจสอบที่ล้มเหลวให้เหมาะสมกับการข้ามไปยังรหัสการล้างข้อมูลเพียงครั้งเดียว (GCC ทำได้แน่นอน) - และในกรณีใด ๆ ข้อผิดพลาดมักจะไม่สำคัญต่อประสิทธิภาพ
  • นี่ยังไม่เพิ่มตัวแปรอื่นอีกหรือ ในกรณีนี้ใช่ แต่บ่อยครั้งreturn_valueตัวแปรสามารถใช้เพื่อแสดงบทบาทที่oksofarกำลังเล่นอยู่ที่นี่ หากคุณจัดโครงสร้างฟังก์ชันเพื่อส่งกลับข้อผิดพลาดในลักษณะที่สอดคล้องกันคุณสามารถหลีกเลี่ยงข้อผิดพลาดที่สองได้ifในแต่ละกรณี:

    int return_value = 0;
    
    if (!return_value) {
        return_value = do_something(bar);
    }
    
    if (!return_value) {
        return_value = init_stuff(bar);
    }
    
    if (!return_value) {
        return_value = prepare_stuff(bar);
    }
    

    ข้อดีอย่างหนึ่งของการเขียนโค้ดแบบนั้นคือความสม่ำเสมอหมายความว่าสถานที่ใด ๆ ที่โปรแกรมเมอร์ดั้งเดิมลืมตรวจสอบค่าส่งคืนจะยื่นออกมาเหมือนนิ้วหัวแม่มือที่เจ็บทำให้ง่ายต่อการค้นหาจุดบกพร่อง (ระดับหนึ่ง)

นี่คืออีกหนึ่งสไตล์ที่สามารถใช้แก้ปัญหานี้ได้ ใช้อย่างถูกต้องจะช่วยให้ได้รหัสที่สะอาดและสอดคล้องกันมาก - และเช่นเดียวกับเทคนิคใด ๆ ในมือที่ไม่ถูกต้องอาจทำให้เกิดรหัสที่ยืดยาวและสับสนได้ :-)


2
ดูเหมือนว่าคุณจะสายไปงานปาร์ตี้ แต่ฉันชอบคำตอบอย่างแน่นอน!

Linus อาจจะปฏิเสธรหัสของคุณblogs.oracle.com/oswald/entry/is_goto_the_root_of
Fizz

1
@ user3588161: ถ้าเขาเป็นเช่นนั้นนั่นคือสิทธิพิเศษของเขา - แต่ฉันไม่แน่ใจจากบทความที่คุณเชื่อมโยงว่าเป็นเช่นนั้นโปรดทราบว่าในรูปแบบที่ฉันกำลังอธิบาย (1) เงื่อนไขจะไม่ซ้อนกัน โดยพลการอย่างลึกซึ้งและ (2) ไม่มีคำสั่ง "if" เพิ่มเติมเมื่อเทียบกับสิ่งที่คุณต้องการ (สมมติว่าคุณกำลังจะตรวจสอบรหัสส่งคืนทั้งหมด)
psmears

มากสิ่งนี้แทนที่จะเป็น goto ที่น่ากลัวและยิ่งแย่กว่านั้นก็คือการแก้ปัญหา arrow-antipattern!
ครัวซองต์ Paramagnetic

8

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

FWIW หากคุณค้นหาบทช่วยสอน developer.apple.com พวกเขาใช้วิธี goto ในการจัดการข้อผิดพลาด

เราไม่ใช้ gotos ความสำคัญที่สูงกว่าคือการกำหนดมูลค่าผลตอบแทน การจัดการข้อยกเว้นทำได้โดยsetjmp/longjmpไม่ว่าคุณจะทำได้เพียงใด


8
ในขณะที่ฉันใช้ setjmp / longjmp อย่างแน่นอนในบางกรณีที่มีการเรียกใช้ฉันคิดว่ามัน "แย่กว่า" กว่า goto ด้วยซ้ำ (ซึ่งฉันก็ใช้ค่อนข้างสงวนไว้น้อยกว่าเมื่อถูกเรียกใช้) ครั้งเดียวที่ฉันใช้ setjmp / longjmp คือเมื่อ (1) เป้าหมายกำลังจะปิดทุกอย่างในลักษณะที่จะไม่ได้รับผลกระทบจากสถานะปัจจุบันหรือ (2) เป้าหมายกำลังจะเริ่มต้นทุกอย่างที่ควบคุมภายใน setjmp / longjmp-guarded บล็อกในลักษณะที่ไม่ขึ้นกับสถานะปัจจุบัน
supercat

4

ไม่มีอะไรผิดทางศีลธรรมเกี่ยวกับคำสั่ง goto มากไปกว่ามีบางอย่างผิดศีลธรรมกับตัวชี้ (โมฆะ) *

ทั้งหมดนี้อยู่ที่วิธีการใช้เครื่องมือของคุณ ในกรณี (เล็กน้อย) ที่คุณนำเสนอคำสั่ง case สามารถบรรลุตรรกะเดียวกันได้แม้ว่าจะมีค่าใช้จ่ายมากกว่าก็ตาม คำถามที่แท้จริงคือ "ข้อกำหนดด้านความเร็วของฉันคืออะไร"

goto นั้นเร็วมากโดยเฉพาะอย่างยิ่งถ้าคุณระมัดระวังเพื่อให้แน่ใจว่ามันรวมเข้ากับการกระโดดสั้น ๆ เหมาะสำหรับการใช้งานที่ความเร็วเป็นระดับพรีเมียม สำหรับแอปพลิเคชันอื่น ๆ อาจเป็นเรื่องที่สมเหตุสมผลที่จะใช้ค่าใช้จ่ายในการโจมตีด้วย if / else + case เพื่อการบำรุงรักษา

ข้อควรจำ: goto ไม่ฆ่าแอปพลิเคชันนักพัฒนาฆ่าแอปพลิเคชัน

UPDATE: นี่คือตัวอย่างกรณี

int foo(int bar) { 
     int return_value = 0 ; 
     int failure_value = 0 ;

     if (!do_something(bar)) { 
          failure_value = 1; 
      } else if (!init_stuff(bar)) { 
          failure_value = 2; 
      } else if (prepare_stuff(bar)) { 
          return_value = do_the_thing(bar); 
          cleanup_3(); 
      } 

      switch (failure_value) { 
          case 2: cleanup_2(); 
          case 1: cleanup_1(); 
          default: break ; 
      } 
} 

1
คุณสามารถนำเสนอทางเลือก 'กรณี' ได้หรือไม่? นอกจากนี้ฉันขอยืนยันว่านี่เป็นวิธีที่แตกต่างจาก void * ซึ่งจำเป็นสำหรับสิ่งที่เป็นนามธรรมโครงสร้างข้อมูลที่ร้ายแรงใน C ฉันไม่คิดว่าจะมีใครต่อต้านโมฆะ * อย่างจริงจังและคุณจะไม่พบฐานรหัสขนาดใหญ่เพียงฐานเดียวหากไม่มี มัน.
Eli Bendersky

Re: void * นั่นคือประเด็นของฉันไม่มีอะไรผิดศีลธรรมด้วยเช่นกัน ตัวอย่างสวิตช์ / กรณีด้านล่าง int foo (แถบ int) {int return_value = 0; int failure_value = 0; ถ้า (! do_something (bar)) {failure_value = 1; } else if (! init_stuff (bar)) {failure_value = 2; } else if (เตรียม _stuff (บาร์)) {{return_value = do_the_thing (bar); cleanup_3 (); } สวิตช์ (failure_value) {กรณีที่ 2: cleanup_2 (); หยุดพัก ; กรณีที่ 1: cleanup_1 (); หยุดพัก ; ค่าเริ่มต้น: ทำลาย; }}
webmarc

5
@webmarc ขอโทษนะ แต่มันน่ากลัว! คุณเพิ่งจำลอง goto ทั้งหมดที่มีป้ายกำกับ - สร้างค่าที่ไม่สามารถอธิบายได้ของคุณเองสำหรับป้ายกำกับและใช้ goto ด้วยสวิตช์ / ตัวพิมพ์ Failure_value = 1 สะอาดกว่า "goto cleanup_something"?
Eli Bendersky

4
ฉันรู้สึกเหมือนคุณตั้งฉันไว้ที่นี่ ... คำถามของคุณมีไว้เพื่อแสดงความคิดเห็นและฉันต้องการอะไร แต่เมื่อฉันเสนอคำตอบมันแย่มาก :-( สำหรับการร้องเรียนของคุณเกี่ยวกับชื่อป้ายกำกับนั้นเป็นเพียงคำอธิบายเช่นเดียวกับตัวอย่างที่เหลือ: cleanup_1, foo, bar เหตุใดคุณจึงโจมตีชื่อป้ายกำกับในเมื่อนั่นไม่ได้เป็นสาเหตุของคำถาม
webmarc

1
ฉันไม่ได้ตั้งใจที่จะ "ตั้งค่า" และทำให้เกิดความรู้สึกเชิงลบใด ๆ ขออภัยด้วย! รู้สึกเหมือนว่าแนวทางใหม่ของคุณมีเป้าหมายที่ 'goto removal' เพียงอย่างเดียวโดยไม่ต้องเพิ่มความชัดเจนใด ๆ เหมือนกับว่าคุณได้นำสิ่งที่ goto ไปใช้งานอีกครั้งเพียงแค่ใช้โค้ดมากขึ้นซึ่งมีความชัดเจนน้อยลง IMHO นี้ไม่ใช่สิ่งที่ดีที่จะทำ - การกำจัด goto เพียงเพื่อประโยชน์ของมัน
Eli Bendersky

2

GOTO มีประโยชน์ เป็นสิ่งที่โปรเซสเซอร์ของคุณสามารถทำได้และนี่คือเหตุผลที่คุณควรเข้าถึงได้

บางครั้งคุณต้องการเพิ่มบางอย่างลงในฟังก์ชันของคุณและไปที่หน้าเดียวให้คุณทำอย่างนั้น ประหยัดเวลาได้ ..


3
คุณไม่จำเป็นต้องเข้าถึงทุกสิ่งที่โปรเซสเซอร์ของคุณทำได้ goto ส่วนใหญ่จะสับสนมากกว่าทางเลือกอื่น
David Thornley

@DavidThornley: ใช่คุณทำเข้าถึงจำเป็นต้องทุกสิ่งเดียวที่ประมวลผลของคุณสามารถทำมิฉะนั้นคุณจะเสียประมวลผลของคุณ Goto เป็นคำสั่งที่ดีที่สุดในการเขียนโปรแกรม
Ron Maimon

1

โดยทั่วไปฉันจะถือว่าความจริงที่ว่าชิ้นส่วนของโค้ดสามารถเขียนได้อย่างชัดเจนที่สุดโดยใช้gotoเป็นอาการที่ผังของโปรแกรมมีความซับซ้อนมากกว่าที่ต้องการโดยทั่วไป การรวมโครงสร้างโปรแกรมอื่น ๆ ด้วยวิธีแปลก ๆ เพื่อหลีกเลี่ยงการใช้gotoจะพยายามรักษาอาการแทนที่จะเป็นโรค ตัวอย่างเฉพาะของคุณอาจไม่ยากเกินไปที่จะนำไปใช้หากไม่มีgoto:

  ทำ {
    .. ตั้งค่า thing1 ที่จะต้องล้างเฉพาะในกรณีออกก่อน
    ถ้า (ข้อผิดพลาด) แตก;
    ทำ
    {
      .. ตั้งค่า thing2 ที่จะต้องล้างข้อมูลในกรณีที่ออกก่อนเวลา
      ถ้า (ข้อผิดพลาด) แตก;
      // ***** ดูข้อความที่เกี่ยวข้องกับบรรทัดนี้
    } ในขณะที่ (0);
    .. สะสาง thing2;
  } ในขณะที่ (0);
  .. เรื่องล้าง 1;

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

ในสถานการณ์จำลอง "การล้างข้อมูลแม้ในกรณีปกติ" ฉันจะถือว่าการใช้งานgotoนั้นชัดเจนกว่าdo/ การwhile(0)สร้างและอื่น ๆ เนื่องจากป้ายกำกับเป้าหมายมักจะร้องว่า "LOOK AT ME" มากกว่าbreakและdo/ while(0)โครงสร้าง สำหรับกรณี "ล้างข้อมูลเฉพาะเมื่อเกิดข้อผิดพลาด" returnคำสั่งจะต้องอยู่ในจุดที่แย่ที่สุดเท่าที่จะเป็นไปได้จากมุมมองที่สามารถอ่านได้ (โดยทั่วไปคำสั่ง return ควรอยู่ที่จุดเริ่มต้นของฟังก์ชันหรืออื่น ๆ ที่ "ดูเหมือน" ตอนจบ); การมีreturnป้ายกำกับเป้าหมายก่อนที่จะตรงตามคุณสมบัตินั้นง่ายกว่าการมีป้ายกำกับก่อนสิ้นสุด "ลูป"

BTW สถานการณ์หนึ่งที่บางครั้งฉันใช้gotoสำหรับการจัดการข้อผิดพลาดอยู่ในswitchคำสั่งเมื่อรหัสสำหรับหลายกรณีใช้รหัสข้อผิดพลาดเดียวกัน แม้ว่าคอมไพเลอร์ของฉันมักจะฉลาดพอที่จะรับรู้ว่าหลายกรณีลงท้ายด้วยรหัสเดียวกันฉันคิดว่ามันชัดเจนกว่าที่จะพูดว่า:

 REPARSE_PACKET:
  สวิตช์ (แพ็คเก็ต [0])
  {
    กรณี PKT_THIS_OPERATION:
      ถ้า (สภาพปัญหา)
        ไปที่ PACKET_ERROR;
      ... จัดการ THIS_OPERATION
      หยุดพัก;
    กรณี PKT_THAT_OPERATION:
      ถ้า (สภาพปัญหา)
        ไปที่ PACKET_ERROR;
      ... จัดการ THAT_OPERATION
      หยุดพัก;
    ...
    กรณี PKT_PROCESS_CONDITIONALLY
      ถ้า (packet_length <9)
        ไปที่ PACKET_ERROR;
      ถ้า (packet_condition เกี่ยวข้องกับแพ็คเก็ต [4])
      {
        packet_length - = 5;
        memmove (แพ็คเก็ต, แพ็คเก็ต + 5, packet_length);
        ไปที่ REPARSE_PACKET;
      }
      อื่น
      {
        แพ็คเก็ต [0] = PKT_CONDITION_SKIPPED;
        แพ็คเก็ต [4] = packet_length;
        packet_length = 5;
        packet_status = READY_TO_SEND;
      }
      หยุดพัก;
    ...
    ค่าเริ่มต้น:
    {
     PACKET_ERROR:
      packet_error_count ++;
      packet_length = 4;
      แพ็คเก็ต [0] = PKT_ERROR;
      packet_status = READY_TO_SEND;
      หยุดพัก;
    }
  }   

แม้ว่าเราจะสามารถแทนที่gotoคำสั่งได้{handle_error(); break;}และถึงแม้ว่าจะสามารถใช้ a do/ while(0)loop ร่วมกับcontinueเพื่อประมวลผลแพ็กเก็ตที่มีเงื่อนไขในการดำเนินการที่ห่อหุ้มไว้ได้ แต่ฉันไม่คิดว่าจะชัดเจนไปกว่าการใช้ไฟล์goto. นอกจากนี้ในขณะที่อาจเป็นไปได้ที่จะคัดลอกรหัสจากPACKET_ERRORทุกที่ที่goto PACKET_ERRORใช้และในขณะที่คอมไพเลอร์อาจเขียนโค้ดที่ซ้ำกันเพียงครั้งเดียวและแทนที่เหตุการณ์ส่วนใหญ่ด้วยการข้ามไปยังสำเนาที่แชร์นั้นการใช้gotoจะทำให้สังเกตเห็นสถานที่ได้ง่ายขึ้น ซึ่งตั้งค่าแพ็กเก็ตแตกต่างกันเล็กน้อย (เช่นหากคำสั่ง "ดำเนินการตามเงื่อนไข" ตัดสินใจที่จะไม่ดำเนินการ)


1

ผมเองเป็นสาวกของ"พลังแห่งสิบ - 10 กฎสำหรับการเขียนรหัสความปลอดภัยที่สำคัญ"

ฉันจะรวมตัวอย่างเล็ก ๆ จากข้อความนั้นเพื่อแสดงให้เห็นถึงสิ่งที่ฉันเชื่อว่าเป็นความคิดที่ดีเกี่ยวกับ goto


กฎ: จำกัด โค้ดทั้งหมดให้อยู่ในโครงสร้างโฟลว์การควบคุมที่ง่ายมาก - ห้ามใช้คำสั่ง goto, โครงสร้าง setjmp หรือ longjmp และการเรียกซ้ำโดยตรงหรือโดยอ้อม

เหตุผล: ขั้นตอนการควบคุมที่ง่ายขึ้นแปลเป็นความสามารถที่แข็งแกร่งขึ้นสำหรับการตรวจสอบและมักจะส่งผลให้โค้ดมีความชัดเจนมากขึ้น การเนรเทศการเรียกซ้ำอาจเป็นเรื่องน่าประหลาดใจที่สุดที่นี่ อย่างไรก็ตามหากไม่มีการเรียกซ้ำเรารับประกันได้ว่าจะมีกราฟการเรียกใช้ฟังก์ชันแบบ acyclic ซึ่งสามารถใช้ประโยชน์ได้โดยเครื่องวิเคราะห์โค้ดและสามารถช่วยพิสูจน์ได้โดยตรงว่าการประหารชีวิตทั้งหมดที่ควรถูกกำหนดขอบเขตนั้นมีขอบเขตตามความเป็นจริง (โปรดทราบว่ากฎนี้ไม่ได้กำหนดให้ฟังก์ชันทั้งหมดมีจุดส่งคืนเพียงจุดเดียว - แม้ว่าสิ่งนี้มักจะทำให้ขั้นตอนการควบคุมง่ายขึ้นอย่างไรก็ตามมีหลายกรณีที่การส่งคืนข้อผิดพลาดในช่วงต้นเป็นวิธีแก้ปัญหาที่ง่ายกว่า)


การยกเลิกการใช้ goto ดูเหมือนจะไม่ดีแต่:

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


22
ปัญหาของเรื่องนี้คือวิธีปกติในการกำจัดgotoบูลีนโดยสิ้นเชิงคือการใช้บูลีนที่ "ฉลาด" บางชุดใน ifs หรือลูปที่ซ้อนกันอย่างลึกซึ้ง นั่นช่วยไม่ได้จริงๆ บางทีเครื่องมือของคุณอาจจะดีขึ้น แต่คุณทำไม่ได้และคุณสำคัญกว่า
Donal Fellows

1

ฉันยอมรับว่าการล้างข้อมูล goto ตามลำดับย้อนกลับที่ระบุในคำถามเป็นวิธีที่สะอาดที่สุดในการทำความสะอาดสิ่งต่างๆในฟังก์ชันส่วนใหญ่ แต่ฉันก็อยากจะชี้ให้เห็นว่าบางครั้งคุณต้องการให้ฟังก์ชันของคุณสะอาดขึ้นอยู่ดี ในกรณีเหล่านี้ฉันใช้ตัวแปรต่อไปนี้ if (0) {label:} idiom เพื่อไปยังจุดที่ถูกต้องของกระบวนการล้างข้อมูล:

int decode ( char * path_in , char * path_out )
{
  FILE * in , * out ;
  code c ;
  int len ;
  int res = 0  ;
  if ( path_in == NULL )
    in = stdin ;
  else
    {
      if ( ( in = fopen ( path_in , "r" ) ) == NULL )
        goto error_open_file_in ;
    }
  if ( path_out == NULL )
    out = stdout ;
  else
    {
      if ( ( out = fopen ( path_out , "w" ) ) == NULL )
        goto error_open_file_out ;
    }

  if( read_code ( in , & c , & longueur ) )
    goto error_code_construction ;

  if ( decode_h ( in , c , out , longueur ) )
  goto error_decode ;

  if ( 0 ) { error_decode: res = 1 ;}
  free_code ( c ) ;
  if ( 0 ) { error_code_construction: res = 1 ; }
  if ( out != stdout ) fclose ( stdout ) ;
  if ( 0 ) { error_open_file_out: res = 1 ; }
  if ( in != stdin ) fclose ( in ) ;
  if ( 0 ) { error_open_file_in: res = 1 ; }
  return res ;
 }

0

ดูเหมือนว่าฉันcleanup_3ควรจะทำความสะอาดแล้วโทรcleanup_2. ในทำนองเดียวกันcleanup_2ควรทำการล้างข้อมูลแล้วเรียก cleanup_1 ดูเหมือนว่าเมื่อใดก็ตามที่คุณทำcleanup_[n]นั่นcleanup_[n-1]เป็นสิ่งที่จำเป็นดังนั้นจึงควรเป็นความรับผิดชอบของวิธีการนี้ (ตัวอย่างเช่นcleanup_3ไม่สามารถเรียกได้โดยไม่ต้องโทรcleanup_2และอาจทำให้เกิดการรั่วไหล)

ด้วยวิธีการดังกล่าวแทนที่จะเป็น gotos คุณจะเรียกขั้นตอนการล้างข้อมูลแล้วย้อนกลับมา

อย่างไรก็ตามgotoแนวทางดังกล่าวไม่ได้ผิดหรือไม่ดีแต่ก็น่าสังเกตว่าไม่จำเป็นต้องเป็นแนวทางที่ "สะอาดที่สุด" (IMHO)

หากคุณกำลังมองหาประสิทธิภาพที่ดีที่สุดฉันคิดว่าgotoวิธีนี้ดีที่สุด ฉันคาดหวังว่ามันจะเกี่ยวข้องเท่านั้นอย่างไรก็ตามประสิทธิภาพที่สำคัญในบางแอปพลิเคชัน (เช่นไดรเวอร์อุปกรณ์อุปกรณ์ฝังตัว ฯลฯ ) มิฉะนั้นจะเป็นการเพิ่มประสิทธิภาพระดับไมโครที่มีลำดับความสำคัญต่ำกว่าความชัดเจนของโค้ด


4
สิ่งนี้จะไม่ตัด - การล้างข้อมูลจะเฉพาะสำหรับทรัพยากรซึ่งจัดสรรตามลำดับนี้ในกิจวัตรนี้เท่านั้น ในที่อื่นไม่เกี่ยวข้องกันดังนั้นการเรียกจากที่อื่นจึงไม่สมเหตุสมผล
Eli Bendersky

0

ฉันคิดว่าคำถามที่นี่ผิดเกี่ยวกับรหัสที่ระบุ

พิจารณา:

  1. do_something (), init_stuff () และ prep_stuff () ดูเหมือนจะรู้ว่าล้มเหลวหรือไม่เนื่องจากส่งคืนเท็จหรือศูนย์ในกรณีนั้น
  2. ความรับผิดชอบในการตั้งค่าสถานะดูเหมือนจะเป็นความรับผิดชอบของฟังก์ชันเหล่านั้นเนื่องจากไม่มีการตั้งค่าสถานะโดยตรงใน foo ()

ดังนั้น: do_something () init_stuff () และ prepare_stuff () ควรจะทำเช่นการทำความสะอาดตัวเอง การมีฟังก์ชัน cleanup_1 () แยกต่างหากที่ทำความสะอาดหลังจาก do_something () ทำลายปรัชญาของการห่อหุ้ม มันออกแบบไม่ดี

หากพวกเขาทำการล้างข้อมูลด้วยตัวเอง foo () จะค่อนข้างง่าย

ในทางกลับกัน. ถ้า foo () สร้างสถานะของตัวเองที่จำเป็นต้องถูกทำลายลง goto ก็จะเหมาะสม


0

นี่คือสิ่งที่ฉันต้องการ:

bool do_something(void **ptr1, void **ptr2)
{
    if (!ptr1 || !ptr2) {
        err("Missing arguments");
        return false;
    }
    bool ret = false;

    //Pointers must be initialized as NULL
    void *some_pointer = NULL, *another_pointer = NULL;

    if (allocate_some_stuff(&some_pointer) != STUFF_OK) {
        err("allocate_some_stuff step1 failed, abort");
        goto out;
    }
    if (allocate_some_stuff(&another_pointer) != STUFF_OK) {
        err("allocate_some_stuff step 2 failed, abort");
        goto out;
    }

    void *some_temporary_malloc = malloc(1000);

    //Do something with the data here
    info("do_something OK");

    ret = true;

    // Assign outputs only on success so we don't end up with
    // dangling pointers
    *ptr1 = some_pointer;
    *ptr2 = another_pointer;
out:
    if (!ret) {
        //We are returning an error, clean up everything
        //deallocate_some_stuff is a NO-OP if pointer is NULL
        deallocate_some_stuff(some_pointer);
        deallocate_some_stuff(another_pointer);
    }
    //this needs to be freed every time
    free(some_temporary_malloc);
    return ret;
}

0

อย่างไรก็ตามการสนทนาเก่า ๆ .... แล้วการใช้ "arrow anti pattern" แล้วห่อหุ้มทุกระดับที่ซ้อนกันในฟังก์ชันอินไลน์แบบคงที่ในภายหลังล่ะ รหัสดูสะอาดเป็นสิ่งที่ดีที่สุด (เมื่อเปิดใช้งานการเพิ่มประสิทธิภาพ) และไม่มีการใช้ goto ในระยะสั้นแบ่งและพิชิต ด้านล่างตัวอย่าง:

static inline int foo_2(int bar)
{
    int return_value = 0;
    if ( prepare_stuff( bar ) ) {
        return_value = do_the_thing( bar );
    }
    cleanup_3();
    return return_value;
}

static inline int foo_1(int bar)
{
    int return_value = 0;
    if ( init_stuff( bar ) ) {
        return_value = foo_2(bar);
    }
    cleanup_2();
    return return_value;
}

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar)) {
        return_value = foo_1(bar);
    }
    cleanup_1();
    return return_value;
}

ในแง่ของพื้นที่เรากำลังสร้างตัวแปรในสแต็กสามเท่าซึ่งไม่ดี แต่สิ่งนี้จะหายไปเมื่อคอมไพล์ด้วย -O2 ลบตัวแปรออกจากสแต็กและใช้รีจิสเตอร์ในตัวอย่างง่ายๆนี้ สิ่งที่ฉันได้รับจากบล็อกด้านบนgcc -S -O2 test.cคือด้านล่าง:

    .section    __TEXT,__text,regular,pure_instructions
    .macosx_version_min 10, 13
    .globl  _foo                    ## -- Begin function foo
    .p2align    4, 0x90
_foo:                                   ## @foo
    .cfi_startproc
## %bb.0:
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset %rbp, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register %rbp
    pushq   %r14
    pushq   %rbx
    .cfi_offset %rbx, -32
    .cfi_offset %r14, -24
    movl    %edi, %ebx
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    callq   _do_something
    testl   %eax, %eax
    je  LBB0_6
## %bb.1:
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _init_stuff
    testl   %eax, %eax
    je  LBB0_5
## %bb.2:
    xorl    %r14d, %r14d
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _prepare_stuff
    testl   %eax, %eax
    je  LBB0_4
## %bb.3:
    xorl    %eax, %eax
    movl    %ebx, %edi
    callq   _do_the_thing
    movl    %eax, %r14d
LBB0_4:
    xorl    %eax, %eax
    callq   _cleanup_3
LBB0_5:
    xorl    %eax, %eax
    callq   _cleanup_2
LBB0_6:
    xorl    %eax, %eax
    callq   _cleanup_1
    movl    %r14d, %eax
    popq    %rbx
    popq    %r14
    popq    %rbp
    retq
    .cfi_endproc
                                        ## -- End function

.subsections_via_symbols

-1

ฉันชอบใช้เทคนิคที่อธิบายไว้ในตัวอย่างต่อไปนี้ ...

struct lnode *insert(char *data, int len, struct lnode *list) {
    struct lnode *p, *q;
    uint8_t good;
    struct {
            uint8_t alloc_node : 1;
            uint8_t alloc_str : 1;
    } cleanup = { 0, 0 };

    // allocate node.
    p = (struct lnode *)malloc(sizeof(struct lnode));
    good = cleanup.alloc_node = (p != NULL);

    // good? then allocate str
    if (good) {
            p->str = (char *)malloc(sizeof(char)*len);
            good = cleanup.alloc_str = (p->str != NULL);
    }

    // good? copy data
    if(good) {
            memcpy ( p->str, data, len );
    }

    // still good? insert in list
    if(good) {
            if(NULL == list) {
                    p->next = NULL;
                    list = p;
            } else {
                    q = list;
                    while(q->next != NULL && good) {
                            // duplicate found--not good
                            good = (strcmp(q->str,p->str) != 0);
                            q = q->next;
                    }
                    if (good) {
                            p->next = q->next;
                            q->next = p;
                    }
            }
    }

    // not-good? cleanup.
    if(!good) {
            if(cleanup.alloc_str)   free(p->str);
            if(cleanup.alloc_node)  free(p);
    }

    // good? return list or else return NULL
    return (good? list: NULL);

}

แหล่งที่มา: http://blog.staila.com/?p=114


2
รหัสแฟล็กกี้และรูปแบบการต่อต้านลูกศร (ทั้งสองอย่างที่แสดงในตัวอย่างของคุณ) เป็นทั้งสองสิ่งที่ทำให้โค้ดซับซ้อนโดยไม่จำเป็น ไม่มีเหตุผลใด ๆ นอกจาก "goto is evil" ที่จะใช้มัน
KingRadical

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