นี่เป็นกรณีการใช้งานที่ดีสำหรับ goto ใน C หรือไม่?


59

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

ฉันเพิ่งได้ยินความไม่พอใจgotoคำสั่งบางอย่างแต่เมื่อเร็ว ๆ นี้ฉันก็พบว่ามีกรณีการใช้งานที่ดีสำหรับมัน

รหัสเช่นนี้:

error = function_that_could_fail_1();
if (!error) {
    error = function_that_could_fail_2();
    if (!error) {
        error = function_that_could_fail_3();
        ...to the n-th tab level!
    } else {
        // deal with error, clean up, and return error code
    }
} else {
    // deal with error, clean up, and return error code
}

หากส่วนที่ทำความสะอาดคล้ายกันมาก ๆ ก็สามารถเขียนได้ดีกว่าเล็กน้อย (ความเห็นของฉัน?) เช่นนี้

error = function_that_could_fail_1();
if(error) {
    goto cleanup;
}
error = function_that_could_fail_2();
if(error) {
    goto cleanup;
}
error = function_that_could_fail_3();
if(error) {
    goto cleanup;
}
...
cleanup:
// deal with error if it exists, clean up
// return error code

นี่เป็นกรณีใช้งานทั่วไปหรือเป็นที่ยอมรับของgotoใน C หรือไม่ มีวิธีที่แตกต่าง / ดีกว่าในการทำเช่นนี้?



2
มากกว่าการเคลือบน้ำตาลคอมไพเลอร์รับประกันการทำความสะอาดและตรวจสอบข้อผิดพลาดใน C คุณต้องทำด้วยตนเองละเมิด DRY และรับประกันว่าในบางสถานที่ที่คุณขี้เกียจเกินกว่าที่จะตรวจสอบรหัสข้อผิดพลาดหรือไปผิดฉลากหรืออะไรทำนองนั้น
DeadMG

3
@Izkata: ไม่มีจุดที่จะมีฟังก์ชั่น (ชอบ destructors) ซึ่งโดยปริยายเรียกว่าคอมไพเลอร์เมื่อยกเว้นจะโยน
DeadMG

6
@DeadMG: การใช้ภาษาอื่นไม่ใช่ตัวเลือกเสมอไป
Benjamin Kloster

13
ฉันคิดว่าการตั้งชื่อป้ายกำกับที่เหมาะสมตัวอย่างอาจดูค่อนข้างดี:goto hell;
ริ้น

คำตอบ:


48

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

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

ถึงกระนั้น C ก็ไม่รวมถึงโครงสร้างการเขียนโปรแกรมที่เป็นไปได้ทั้งหมด (ยังไม่ชัดเจนสำหรับฉันว่ามีการค้นพบสิ่งที่เกี่ยวข้องทั้งหมดอัตราการค้นพบช้าในขณะนี้ แต่ฉันลังเลที่จะข้ามไปที่จะบอกว่าพบสิ่งนั้นทั้งหมดแล้ว) สิ่งที่เรารู้เกี่ยวกับ C ขาดแน่นอนtry/ catch/ finallyโครงสร้าง (และข้อยกเว้นด้วย) มันยังขาดหลายระดับbreak-from-loop นี่คือสิ่งต่าง ๆ ที่gotoสามารถนำไปใช้ในการปฏิบัติ เป็นไปได้ที่จะใช้รูปแบบอื่น ๆ เพื่อทำสิ่งเหล่านี้เช่นกัน - เรารู้ว่า C มีชุดที่ไม่เพียงพอgotoดั่งเดิม - แต่สิ่งเหล่านี้มักเกี่ยวข้องกับการสร้างตัวแปรแฟล็กและเงื่อนไขลูปหรือการป้องกันที่ซับซ้อนมากขึ้น การเพิ่มความพัวพันของการวิเคราะห์การควบคุมด้วยการวิเคราะห์ข้อมูลทำให้โปรแกรมเข้าใจยากโดยรวม นอกจากนี้ยังทำให้ยากขึ้นสำหรับคอมไพเลอร์ในการเพิ่มประสิทธิภาพและเพื่อให้ CPU ดำเนินการอย่างรวดเร็ว (ส่วนใหญ่สร้างการควบคุมการไหล - และแน่นอน goto - มีราคาถูกมาก)

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

int frobnicateTheThings() {
    char *workingBuffer = malloc(...);
    int i;

    for (i=0 ; i<numberOfThings ; i++) {
        if (giveMeThing(i, workingBuffer) != OK)
            goto error;
        if (processThing(workingBuffer) != OK)
            goto error;
        if (dispatchThing(i, workingBuffer) != OK)
            goto error;
    }

    free(workingBuffer);
    return OK;

  error:
    free(workingBuffer);
    return OOPS;
}

รหัสอาจสั้นกว่านี้ แต่ก็เพียงพอที่จะแสดงให้เห็นถึงประเด็น


4
+1: ใน C goto ไม่เคย "จำเป็น" ในทางเทคนิค - มีวิธีทำอยู่เสมอมันยุ่งเหยิง ..... สำหรับแนวทางที่แข็งแกร่งสำหรับการใช้ goto ดู MISRA C.
mattnz

1
คุณต้องการtry/catch/finallyที่จะgotoทั้งๆที่คล้ายกัน แต่ที่แพร่หลายมากขึ้น (เท่าที่จะสามารถแพร่กระจายไปทั่วหลายฟังก์ชั่น / modules) รูปแบบของรหัสปาเก็ตตี้ที่เป็นไปได้โดยใช้try/catch/finally?
ออทิสติก

65

ใช่.

มันถูกใช้ในตัวอย่างเช่นเคอร์เนลลินุกซ์ นี่คืออีเมลจากปลายเธรดจากเกือบหนึ่งทศวรรษที่ผ่านมาปั้นของฉัน:

จาก: Robert Love
หัวเรื่อง: Re: โอกาสใด ๆ ของการทดสอบ 2.6.0 *?
วันที่: 12 ม.ค. 2546 17:58:06 -0500

วันอาทิตย์ที่ 2003-01-12 เวลา 17:22, Rob Wilkens เขียนว่า:

ฉันพูดว่า "โปรดอย่าใช้ goto" และแทนที่จะมีฟังก์ชั่น "cleanup_lock" และเพิ่มเข้าไปก่อนที่จะส่งคืนข้อความทั้งหมด .. มันไม่ควรเป็นภาระ ใช่มันขอให้ผู้พัฒนาทำงานหนักขึ้นเล็กน้อย แต่ผลลัพธ์ที่ได้คือรหัสที่ดีกว่า

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

หรืออ่านง่ายกว่า

ในฐานะที่เป็นข้อโต้แย้งสุดท้ายมันไม่ได้ช่วยให้เราทำลมสแต็ก - เอสqueตามปกติและผ่อนคลายเช่น

        do A
        if (error)
            goto out_a;
        do B
        if (error)
            goto out_b;
        do C
        if (error)
            goto out_c;
        goto out;
        out_c:
        undo C
        out_b:
        undo B:
        out_a:
        undo A
        out:
        return ret;

ตอนนี้หยุด

โรเบิร์ตเลิฟ

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


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

11
ใช้ stack un-wind เพื่อจัดการกับข้อผิดพลาดโดยไม่ต้องกดลงบน stack! นี่คือการใช้ goto ที่ยอดเยี่ยม
mike30

1
@ user1249, ขยะคุณไม่สามารถโปรไฟล์ได้ทุกแอป {ในอดีตปัจจุบันและอนาคต} ที่ใช้รหัส {ไลบรารีเคอร์เนล} คุณต้องรวดเร็ว
Pacerier

1
ไม่เกี่ยวข้อง: ฉันประหลาดใจที่ผู้คนสามารถใช้รายชื่อรับเมลเพื่อทำสิ่งต่างๆให้สำเร็จโครงการขนาดใหญ่เช่นนี้ มันเป็นเช่นนั้น ... ดั้งเดิม ผู้คนเข้ามามีส่วนร่วมกับเรื่องราวของเตาผิงได้อย่างไร!
Alexander

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

14

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

อย่างไรก็ตามเนื่องจากการถกเถียงของ goto แบบเก่าโปรแกรมเมอร์จึงหลีกเลี่ยงgotoมา 40 ปีแล้วดังนั้นพวกเขาจึงไม่คุ้นเคยกับการอ่านรหัสกับ goto นี่คือเหตุผลที่ถูกต้องเพื่อหลีกเลี่ยงการข้ามไป: มันก็ไม่ได้มาตรฐาน

ฉันจะเขียนรหัสใหม่เป็นสิ่งที่อ่านได้ง่ายขึ้นโดยโปรแกรมเมอร์ C:

Error some_func (void)
{
  Error error;
  type_t* resource = malloc(...);

  error = some_other_func (resource);

  free (resource);

  /* error handling can be placed here, or it can be returned to the caller */

  return error;
}


Error some_other_func (type_t* resource)  // inline if needed
{
  error = function_that_could_fail_1();
  if(error)
  {
    return error;
  }

  /* ... */

  error = function_that_could_fail_2();
  if(error)
  {
    return error;
  }

  /* ... */

  return ok;
}

ข้อดีของการออกแบบนี้:

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

11

เอกสารที่มีชื่อเสียงที่อธิบายถึงกรณีการใช้งานที่ถูกต้องของการเขียนโปรแกรมแบบมีโครงสร้างกับคำชี้แจง GOTOโดย Donald E. Knuth (มหาวิทยาลัยสแตนฟอร์ด) บทความปรากฏในสมัยที่ใช้ GOTO ถือเป็นบาปโดยบางคนและเมื่อการเคลื่อนไหวสำหรับการเขียนโปรแกรมเชิงโครงสร้างเป็นจุดสูงสุด คุณอาจต้องการดูGoTo พิจารณาเป็นอันตราย


9

ใน Java คุณต้องการทำสิ่งนี้:

makeCalls:  {
    error = function_that_could_fail_1();
    if (error) {
        break makeCalls;
    }
    error = function_that_could_fail_2();
    if (error) {
        break makeCalls;
    }
    error = function_that_could_fail_3();
    if (error) {
        break makeCalls;
    }
    ...
    return 0;  // No error code.
}
// deal with error if it exists, clean up
// return error code

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


3
อ๊ะนั่นเป็นโครงสร้างการควบคุมที่เรียบร้อย
Bryan Boettcher

4
มันน่าสนใจจริงๆ ฉันมักจะคิดว่าการใช้โครงสร้างลอง / จับ / ในที่สุดสำหรับสิ่งนี้ใน java (โยนข้อยกเว้นแทนการทำลาย)
Robz

5
ที่อ่านไม่ได้จริงๆ (อย่างน้อยสำหรับฉัน) หากปัจจุบันข้อยกเว้นดีกว่ามาก
m3th0dman

1
@ m3th0dman ฉันเห็นด้วยกับคุณในตัวอย่างนี้ (การจัดการข้อผิดพลาด) แต่มีอีกหลายกรณี (ที่ไม่ธรรมดา) ซึ่งสำนวนนี้อาจมีประโยชน์
Konrad Rudolph

1
ข้อยกเว้นมีราคาแพงพวกเขาจำเป็นต้องสร้างข้อผิดพลาด stacktrace และขยะมากขึ้น ตัวแบ่งป้ายกำกับนี้ให้การออกที่สะอาดจากลูปการตรวจสอบ นอกจากจะไม่มีใครสนใจเรื่องความจำและความเร็วแล้วสำหรับทุกคนที่ฉันสนใจใช้ข้อยกเว้น
Tschallacka

8

ฉันคิดว่ามันเป็นกรณีการใช้งานที่ดี แต่ในกรณี "ข้อผิดพลาด" ไม่มีอะไรมากไปกว่าค่าบูลีนมีวิธีที่แตกต่างในการบรรลุสิ่งที่คุณต้องการ:

error = function_that_could_fail_1();
error = error || function_that_could_fail_2();
error = error || function_that_could_fail_3();
if(error)
{
     // do cleanup
}

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


1
ปัญหาของสิ่งนี้คือerrorคุณค่าของสิ่งนั้นอาจกลายเป็นสิ่งที่ไร้ความหมายกับสิ่งมีชีวิตทั้งหมด
James

@James: แก้ไขคำตอบของฉันเนื่องจากความคิดเห็นของคุณ
Doc Brown

1
สิ่งนี้ไม่เพียงพอ หากเกิดข้อผิดพลาดระหว่างฟังก์ชั่นแรกฉันไม่ต้องการเรียกใช้ฟังก์ชั่นที่สองหรือสาม
Robz

2
หากใช้การประเมินแบบสั้นคุณหมายถึงการประเมินผลแบบลัดวงจรนี่ไม่ใช่สิ่งที่ทำที่นี่เนื่องจากการใช้บิตหรือในระดับตรรกะแทนที่จะเป็นแบบตรรกะหรือ
Christian Rau

1
@ChristianRau: ขอบคุณแก้ไขคำตอบของฉันตามนั้น
Doc Brown

6

คู่มือสไตล์ linux ให้เหตุผลเฉพาะในการใช้งานgotoซึ่งสอดคล้องกับตัวอย่างของคุณ:

https://www.kernel.org/doc/Documentation/process/coding-style.rst

เหตุผลในการใช้ gotos คือ:

  • ข้อความที่ไม่มีเงื่อนไขนั้นง่ายต่อการเข้าใจและติดตาม
  • การทำรังจะลดลง
  • ข้อผิดพลาดโดยไม่อัปเดตจุดออกแต่ละจุดเมื่อมีการป้องกันแก้ไข
  • บันทึกการทำงานของคอมไพเลอร์เพื่อเพิ่มประสิทธิภาพรหัสซ้ำซ้อนออกไป;)

ข้อจำกัดความรับผิดชอบฉันไม่ควรแบ่งปันงานของฉัน ตัวอย่างที่นี่มีการประดิษฐ์เล็กน้อยดังนั้นโปรดอดทนกับฉัน

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

tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
    ...
    some statements, some involving dynamically allocated memory
    ...
    if ( check_this() ){
        free(var1);
        free(var2);
        ...
        free(varN);
        return 1;
    }
    ...
    some more stuff
    ...
    if(something()){
        if ( check_that() ){
            free(var1);
            free(var2);
            ...
            free(varN);
            return 1;
        } else {
            free(var1);
            free(var2);
            ...
            free(varN);
            return 0;
        }
    }
    token = strtok(NULL,delim);
}

free(var1);
free(var2);
...
free(varN);
return 1;

ตอนนี้สำหรับฉันรหัสต่อไปนี้ดีกว่าและง่ายกว่าถ้าคุณต้องการเพิ่มvarNplus1:

int retval = 1;
tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
    ...
    some statements, some involving dynamically allocated memory
    ...
    if ( check_this() ){
        retval = 1;
        goto out_free;
    }
    ...
    some more stuff
    ...
    if(something()){
        if ( check_that() ){
            retval = 1;
            goto out_free;
        } else {
            retval = 0;
            goto out_free;
        }
    }
    token = strtok(NULL,delim);
}

out_free:
free(var1);
free(var2);
...
free(varN);
return retval;

ตอนนี้โค้ดมีปัญหาอื่น ๆ อีกมากมายซึ่งก็คือ N อยู่ที่ใดที่หนึ่งที่สูงกว่า 10 และฟังก์ชั่นมีมากกว่า 450 บรรทัดโดยมีความซ้อนกัน 10 ระดับในบางสถานที่

แต่ฉันเสนอหัวหน้างานของฉันให้ refactor ซึ่งฉันทำและตอนนี้มันเป็นฟังก์ชั่นมากมายที่สั้นและพวกเขาทั้งหมดมีสไตล์ของ linux

int function(const char * param)
{
    int retval = 1;
    char * var1 = fcn_that_returns_dynamically_allocated_string(param);
    if( var1 == NULL ){
        retval = 0;
        goto out;
    }

    if( isValid(var1) ){
         retval = some_function(var1);
         goto out_free;
    }

    if( isGood(var1) ){
         retval = 0;
         goto out_free;
    }

out_free:
    free(var1);
out:
    return retval;
}

หากเราพิจารณาสิ่งที่เทียบเท่าโดยไม่มีgotos:

int function(const char * param)
{
    int retval = 1;
    char * var1 = fcn_that_returns_dynamically_allocated_string(param);
    if( var1 != NULL ){

       if( isValid(var1) ){
            retval = some_function(var1);
       } else {
          if( isGood(var1) ){
               retval = 0;
          }
       }
       free(var1);

    } else {
       retval = 0;
    }

    return retval;
}

ให้ฉันในกรณีแรกก็เป็นที่ชัดเจนกับผมว่าถ้าฟังก์ชั่นแรกผลตอบแทนที่เราอยู่ที่นี่และเรากำลังกลับมาNULL 0ในกรณีที่สองฉันต้องเลื่อนลงเพื่อดูว่าถ้ามีฟังก์ชั่นทั้งหมด ที่ได้รับคนแรกบ่งบอกสิ่งนี้กับฉันโวหาร (ชื่อ " out") และคนที่สองทำ syntactically คนแรกยังคงชัดเจนมากขึ้น

นอกจากนี้ฉันชอบที่จะมีfree()งบในตอนท้ายของฟังก์ชั่น ส่วนหนึ่งเป็นเพราะในประสบการณ์ของฉันfree()ข้อความที่อยู่ตรงกลางของฟังก์ชั่นมีกลิ่นเหม็นและบอกฉันว่าฉันควรสร้างรูทีนย่อย ในกรณีนี้ฉันสร้างขึ้นvar1ในฟังก์ชั่นของฉันและไม่สามารถทำได้free()ในรูทีนย่อย แต่นั่นเป็นเหตุผลว่าทำไมgoto out_freeสไตล์ goto out จึงใช้งานได้จริง

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

ฉันควรเพิ่มว่าฉันใช้สไตล์นี้อย่างสม่ำเสมอทุกฟังก์ชั่นมี int retval, out_freelabel และ out label เนื่องจากความสอดคล้องกับโวหารทำให้การอ่านง่ายขึ้น

โบนัส: ทำลายและดำเนินการต่อ

สมมติว่าคุณมีวงวนสักครู่

char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
    var1 = functionA(line,count);
    var2 = functionB(line,count);

    if( functionC(var1, var2){
         count++
         continue;
    }

    ...
    a bunch of statements
    ...

    count++;
    free(var1);
    free(var2);
}

มีสิ่งอื่น ๆ ที่ผิดกับรหัสนี้ แต่สิ่งหนึ่งคือคำสั่งที่ยังคง ฉันอยากจะเขียนใหม่ทั้งหมด แต่ฉันได้รับมอบหมายให้แก้ไขมันด้วยวิธีเล็ก ๆ น้อย ๆ ฉันต้องใช้เวลาหลายวันกว่าจะสร้างมันใหม่ในแบบที่ทำให้ฉันพึงพอใจ แต่การเปลี่ยนแปลงที่เกิดขึ้นจริงนั้นเกี่ยวกับการทำงานครึ่งวัน ปัญหาคือว่าแม้ว่าเรา ' continue' เรายังคงต้องเป็นอิสระและvar1 var2ฉันต้องเพิ่มvar3และมันทำให้ฉันต้องการอ้วกที่จะต้องสะท้อนงบ () ฟรี

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

char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
    var1 = functionA(line,count);
    var2 = functionB(line,count);
    var3 = newFunction(line,count);

    if( functionC(var1, var2){
         goto next;
    }

    ...
    a bunch of statements
    ...
next:
    count++;
    free(var1);
    free(var2);
}

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

และฉันควรจะเพิ่มด้วยว่าการใช้goto next;และnext:ฉลากนี้ไม่เป็นที่น่าพอใจสำหรับฉัน สิ่งเหล่านี้ดีกว่าการทำมิเรอร์free()และcount++ข้อความ

gotoมีความผิดพลาดอยู่เกือบตลอดเวลา แต่ก็ต้องรู้ว่าเมื่อไรจะใช้งานได้ดี

สิ่งหนึ่งที่ฉันไม่ได้พูดถึงคือการจัดการข้อผิดพลาดที่ได้รับคำตอบอื่น ๆ

ประสิทธิภาพ

หนึ่งสามารถดูการใช้งานของ strtok () http://opensource.apple.com //source/Libc/Libc-167/string.subproj/strtok.c

#include <stddef.h>
#include <string.h>

char *
strtok(s, delim)
    register char *s;
    register const char *delim;
{
    register char *spanp;
    register int c, sc;
    char *tok;
    static char *last;


    if (s == NULL && (s = last) == NULL)
        return (NULL);

    /*
     * Skip (span) leading delimiters (s += strspn(s, delim), sort of).
     */
cont:
    c = *s++;
    for (spanp = (char *)delim; (sc = *spanp++) != 0;) {
        if (c == sc)
            goto cont;
    }

    if (c == 0) {       /* no non-delimiter characters */
        last = NULL;
        return (NULL);
    }
    tok = s - 1;

    /*
     * Scan token (scan for delimiters: s += strcspn(s, delim), sort of).
     * Note that delim must have one NUL; we stop if we see that, too.
     */
    for (;;) {
        c = *s++;
        spanp = (char *)delim;
        do {
            if ((sc = *spanp++) == c) {
                if (c == 0)
                    s = NULL;
                else
                    s[-1] = 0;
                last = s;
                return (tok);
            }
        } while (sc != 0);
    }
    /* NOTREACHED */
}

โปรดแก้ไขฉันหากฉันผิด แต่ฉันเชื่อว่าcont:ฉลากและgoto cont;ข้อความสั่งนั้นมีไว้เพื่อการปฏิบัติงาน (แน่นอนว่าพวกเขาไม่ได้ทำให้อ่านรหัสได้มากขึ้น) พวกเขาสามารถถูกแทนที่ด้วยรหัสที่อ่านได้โดยการทำ

while( isDelim(*s++,delim));

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

ฉันอ่านบทความโดย Dijkstra และฉันพบว่ามันค่อนข้างลึกลับ

google "คำสั่ง dijkstra goto ถือว่าเป็นอันตราย" เพราะฉันไม่มีชื่อเสียงพอที่จะโพสต์มากกว่า 2 ลิงก์

ฉันเคยเห็นมันอ้างว่าเป็นเหตุผลที่จะไม่ใช้ goto และการอ่านมันไม่ได้เปลี่ยนแปลงอะไรเลยตราบใดที่การใช้ goto ของฉันแน่นแฟ้นขึ้น

ภาคผนวก :

ฉันคิดกฎระเบียบเรียบร้อยในขณะที่คิดถึงเรื่องทั้งหมดนี้เกี่ยวกับการดำเนินการต่อและการหยุดพัก

  • หากในขณะที่คุณมีการดำเนินการต่อจากนั้นร่างกายของห่วงขณะที่ควรจะเป็นฟังก์ชั่นและดำเนินการต่อเป็นคำสั่งกลับมา
  • หากใน a while loop คุณมีคำสั่ง break ดังนั้น while while นั้นควรจะเป็นฟังก์ชั่นและตัวแบ่งควรเป็นคำสั่ง return
  • หากคุณมีทั้งคู่แล้วอาจมีบางอย่างผิดปกติ

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


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

1
ฉันคิดว่าคุณพูดถูก ฉันคิดว่า GOTO นั้นไม่ได้ถูกใช้งานและโดยเหตุการณ์ที่เกิดขึ้นจริงฉันกำลังเรียกดูซอร์สโค้ด Linux ในเวลาที่ฉันทำงานกับโค้ดที่มีฟังก์ชั่นเหล่านี้ที่มีจุดทางออกหลายจุดพร้อมหน่วยความจำว่าง มิฉะนั้นฉันจะไม่เคยรู้เกี่ยวกับเทคนิคเหล่านี้
Philippe Carphin

1
@thb และเป็นเรื่องตลกฉันถามหัวหน้างานของฉันในเวลานั้นเพื่อรับอนุญาตให้ใช้ GOTO และฉันแน่ใจว่าฉันอธิบายให้เขาฟังว่าฉันจะใช้มันในลักษณะที่เหมือนกับวิธีที่ใช้ใน เคอร์เนลลินุกซ์และเขาพูดว่า "โอเคมันสมเหตุสมผลแล้วและฉันก็ไม่รู้ว่าคุณสามารถใช้ GOTO ใน C" ได้
Philippe Carphin

1
@thb ฉันไม่รู้ว่ามันเป็นการดีที่จะข้ามไปเป็นลูป (แทนที่จะทำลายลูป) แบบนี้ไหม? มันเป็นตัวอย่างที่ไม่ดี แต่ฉันพบว่า quicksort พร้อมคำสั่ง goto (ตัวอย่าง 7a) ในการเขียนโปรแกรมแบบโครงสร้างของ Knuth ด้วยการไปที่ Statementไม่เข้าใจมากนัก
Yai0Phah

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

5

โดยส่วนตัวแล้วฉันจะทำให้เป็นจริงอีกครั้งเช่นนี้:

int DoLotsOfStuffThatCouldFail (paramstruct *params)
{
    int errcode = EC_NOERROR;

    if ((errcode = FunctionThatCouldFail1 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail2 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail3 (params)) != EC_NOERROR) return errcode;
    if ((errcode = FunctionThatCouldFail4 (params)) != EC_NOERROR) return errcode;

    return EC_NOERROR;
}

void DoStuff (paramstruct *params)
{
    int errcode = EC_NOERROR;

    InitStuffThatMayNeedToBeCleaned (params);

    if ((errcode = DoLotsOfStuffThatCouldFail (params)) != EC_NOERROR)
    {
         CleanupAfterError (params, errcode);
    }
}

นั่นจะเป็นแรงบันดาลใจมากขึ้นโดยการหลีกเลี่ยงการซ้อนที่ลึกกว่าการหลีกเลี่ยงการข้ามไป (IMO เป็นปัญหาที่เลวร้ายกว่ากับโค้ดตัวอย่างแรก) และแน่นอนว่าจะต้องอาศัย CleanupAfterError เป็นไปได้นอกขอบเขต (ในกรณีนี้ "params" เป็นโครงสร้างที่มีหน่วยความจำที่จัดสรรซึ่งคุณต้องการให้ว่างไฟล์ * ที่คุณต้องปิดหรืออะไรก็ตาม)

ข้อได้เปรียบที่สำคัญอย่างหนึ่งที่ฉันเห็นด้วยวิธีนี้คือทั้งง่ายขึ้นและสะอาดขึ้นในการวางขั้นตอนพิเศษในอนาคตระหว่างสมมติว่า FTCF2 และ FTCF3 (หรือลบขั้นตอนปัจจุบันที่มีอยู่) ดังนั้นจึงเป็นการบำรุงรักษาที่ดีกว่า สืบทอดรหัสของฉันไม่ต้องการที่จะประชาทัณฑ์ฉัน!) - ข้ามไปแล้วรุ่นซ้อนกันขาดนั้น


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

3

ดูแนวทางการเขียนโค้ด C ของ MISRA (สมาคมอุตสาหกรรมซอฟต์แวร์ความน่าเชื่อถือ) ที่อนุญาตให้ข้ามไปภายใต้เกณฑ์ที่เข้มงวด (ซึ่งตัวอย่างของคุณตรงตาม)

ที่ฉันทำงานรหัสเดียวกันจะเขียน - ไม่จำเป็นต้องข้ามไป - หลีกเลี่ยงการอภิปรายทางศาสนาที่ไม่จำเป็นเกี่ยวกับพวกเขาเป็นข้อดีอย่างมากในบ้านซอฟต์แวร์ใด ๆ

error = function_that_could_fail_1();
if(!error) {
  error = function_that_could_fail_2();
}
if(!error) {
  error = function_that_could_fail_3();
} 
if(!error) {
...
if (error) {
  cleanup:
} 

หรือสำหรับ "goto in drag" - มีบางอย่างที่หลบไปกว่า goto แต่กลับไปที่ "No goto ever !!!" ค่าย) "แน่นอนว่ามันต้องโอเคไม่ใช้ Goto" ....

do {
  if (error = function_that_could_fail_1() ){
    break 
  }
  if (error = function_that_could_fail_2() ){
    break 
  }
  ....... 
} while (0) 
cleanup();
.... 

หากฟังก์ชั่นมีประเภทพารามิเตอร์เดียวกันวางลงในตารางและใช้วง -


2
แนวทางปัจจุบันของ MISRA-C: 2004 ไม่อนุญาตให้ข้ามไปในรูปแบบใด ๆ (ดูกฎ 14.4) หมายเหตุคณะกรรมการ MISRA มักจะสับสนเกี่ยวกับเรื่องนี้พวกเขาไม่รู้ว่าจะยืนตรงไหน ครั้งแรกพวกเขาถูกแบนโดยไม่มีเงื่อนไขห้ามใช้ goto ดำเนินการต่อ ฯลฯ แต่ในร่าง MISRA 2011 ที่กำลังจะมาถึงพวกเขาต้องการอนุญาตพวกเขาอีกครั้ง ในฐานะ sidenote โปรดทราบว่า MISRA ห้ามการใช้คำสั่ง if-if ด้วยเหตุผลที่ดีมากเพราะมันอันตรายกว่าการใช้ goto

1
จากมุมมองเชิงวิเคราะห์การเพิ่มแฟล็กลงในโปรแกรมเทียบเท่ากับการทำซ้ำโค้ดโค้ดทั้งหมดที่แฟล็กอยู่ในขอบเขตโดยที่ทุก ๆif(flag)สำเนาหนึ่งจะรับสาขา "if" และมีคำสั่งที่เกี่ยวข้องในสำเนาอื่น ๆ อื่น". การกระทำที่ตั้งค่าและล้างค่าสถานะเป็น "gotos" ที่ข้ามระหว่างโค้ดสองเวอร์ชันเหล่านี้ มีบางครั้งที่การใช้แฟล็กนั้นสะอาดกว่าทางเลือกอื่น ๆ แต่การเพิ่มการตั้งค่าสถานะเพื่อบันทึกหนึ่งgotoเป้าหมายนั้นไม่ได้เป็นการแลกเปลี่ยนที่ดี
supercat

1

ฉันยังใช้gotoถ้าdo/while/continue/breakแฮกเกอร์ทางเลือกจะอ่านได้น้อยลง

gotos goto something;มีความได้เปรียบที่เป้าหมายของพวกเขามีชื่อและที่พวกเขาอ่าน สิ่งนี้อาจอ่านได้ง่ายกว่าbreakหรือcontinueถ้าคุณไม่ได้หยุดหรือทำอะไรต่อไป


4
ที่ใดก็ได้ภายในdo ... while(0)หรืออื่นสร้างซึ่งไม่ได้เป็นวงที่เกิดขึ้นจริง แต่ความพยายาม harebrained gotoเพื่อป้องกันไม่ให้การใช้งานของที่
aib

1
อ่าขอบคุณฉันไม่รู้จักแบรนด์นี้โดยเฉพาะของ "ทำไมคนจะทำอย่างนั้น?!" ยังสร้าง
Benjamin Kloster

2
โดยปกติแล้วการแฮกเกอร์แบบทำในขณะที่ดำเนินการต่อ / หยุดพักจะไม่สามารถอ่านได้เมื่อโมดูลที่บรรจุอยู่นั้นมีความยาวเกินไปในตอนแรก
John R. Strohm

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

1
แน่นอนฉลากควรมองเห็นได้จากภายในลูป ฉันเห็นด้วยกับส่วนความยาว fricking ของความคิดเห็นของ @John R. Strohm และจุดของคุณแปลเป็นแฮกเกอร์วนกลายเป็น "แยกออกจากอะไรนี่ไม่ใช่วง!" ไม่ว่าในกรณีใดนี่เป็นสิ่งที่ OP กลัวว่าจะเป็นเช่นนั้นดังนั้นฉันจึงละทิ้งการอภิปราย
aib

-1
for (int y=0; y<height; ++y) {
    for (int x=0; x<width; ++x) {
        if (find(x, y)) goto found;
    }
}
found:

หากมีเพียงหนึ่งวงวนให้breakทำงานเหมือนgotoกันทุกประการแม้ว่าจะไม่ตีตรา
9000

6
-1: ก่อนอื่น x และ y อยู่นอกขอบเขตที่พบ: ดังนั้นสิ่งนี้จะไม่ช่วยคุณเลย ข้อสองด้วยรหัสตามที่เขียนไว้ความจริงที่คุณได้พบ: ไม่ได้หมายความว่าคุณพบสิ่งที่คุณกำลังมองหา
John R. Strohm

มันเป็นเพราะนี่เป็นตัวอย่างที่เล็กที่สุดที่ฉันนึกถึงในกรณีที่เกิดการแตกออกเป็นหลายวง โปรดแก้ไขเพื่อฉลากที่ดีขึ้นหรือตรวจสอบเสร็จแล้ว
aib

1
แต่โปรดจำไว้ว่าฟังก์ชั่น C ไม่จำเป็นต้องมีผลข้างเคียง
aib

1
@ JohnR.Strohm มันไม่สมเหตุสมผล ... ฉลาก 'Found' ถูกใช้เพื่อแยกลูปไม่ต้องตรวจสอบตัวแปร ถ้าฉันต้องการตรวจสอบตัวแปรฉันสามารถทำสิ่งนี้: สำหรับ (int y = 0; y <height; ++ y) {สำหรับ (int x = 0; x <กว้าง; ++ x) {ถ้า (ค้นหา ( x, y)) {doSomeThingWith (x, y); ข้ามไปพบ }}} พบ:
YoYoYonnY

-1

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

ใน C ฉันมักจะทำสิ่งต่อไปนี้:

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

สำหรับการประมวลผลโดยใช้ตัวอย่าง goto ของคุณฉันจะทำสิ่งนี้:

ข้อผิดพลาด = function_that_could_fail_1 (); if (! error) {error = function_that_could_fail_2 (); } if (! error) {error = function_that_could_fail_3 (); }

ไม่มีการซ้อนและในส่วนคำสั่ง if คุณสามารถทำการรายงานข้อผิดพลาดใด ๆ ได้หากขั้นตอนสร้างข้อผิดพลาด ดังนั้นจึงไม่จำเป็นต้อง "แย่" กว่าวิธีการใช้ gotos

ฉันยังไม่ได้วิ่งข้ามกรณีที่บางคนมี gotos ที่ไม่สามารถทำได้ด้วยวิธีอื่นและเป็นเพียงที่อ่าน / เข้าใจได้และนั่นคือกุญแจ IMHO

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