จะหลีกเลี่ยงการเชื่อมโยง“ ถ้า” ได้อย่างไร?


266

สมมติว่าฉันมีรหัสหลอกนี้:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}

executeThisFunctionInAnyCase();

ฟังก์ชั่นexecuteStepXควรจะดำเนินการถ้าหากว่าก่อนหน้านี้ประสบความสำเร็จ ไม่ว่าในกรณีใดexecuteThisFunctionInAnyCaseฟังก์ชันควรถูกเรียกใช้ในตอนท้าย ฉันเป็นมือใหม่ในการเขียนโปรแกรมดังนั้นขออภัยสำหรับคำถามพื้นฐานมาก: มีวิธี (ใน C / C ++ เป็นต้น) เพื่อหลีกเลี่ยงifสายโซ่ยาว ๆ ที่ผลิต "pyramid of code" ที่ค่าใช้จ่ายของการอ่านรหัสได้ง่าย ?

ฉันรู้ว่าถ้าเราสามารถข้ามการexecuteThisFunctionInAnyCaseเรียกใช้ฟังก์ชันรหัสอาจง่ายขึ้นเป็น:

bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

แต่ข้อ จำกัด คือการexecuteThisFunctionInAnyCaseเรียกใช้ฟังก์ชัน สามารถระบุbreakคำสั่งที่จะใช้ในวิธีการบางอย่าง?


254
@ FrédéricHamidiผิดผิดผิด! อย่าพูดว่าการขับขี่โฟลว์โปรแกรมของคุณด้วยข้อยกเว้นเป็นสิ่งที่ดี! ข้อยกเว้นไม่เหมาะสำหรับวัตถุประสงค์นี้อย่างแน่นอนด้วยเหตุผลหลายประการ
Piotr Zierhoffer

26
@Piotr ฉันเป็นคนใจแตกโดย Python (ซึ่งให้กำลังใจเรื่องนี้) ฉันรู้ว่าข้อยกเว้นไม่ควรถูกนำมาใช้สำหรับการควบคุมการไหลใน C ++ แต่มันคือการควบคุมการไหลจริงหรือ ฟังก์ชั่นที่ส่งคืนfalseไม่สามารถถูกพิจารณาว่าคล้ายกับสถานการณ์พิเศษหรือไม่?
Frédéric Hamidi

13
ขึ้นอยู่กับความหมายของโปรแกรม falseผลตอบแทนที่สามารถเป็นปกติสวย
dornhege

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

13
ฉันหวังว่า "โปรแกรมเมอร์มือใหม่" ทั้งหมดจะถามคำถามการออกแบบเช่นนี้
Jezen Thomas

คำตอบ:


486

คุณสามารถใช้&&(ตรรกะ AND):

if (executeStepA() && executeStepB() && executeStepC()){
    ...
}
executeThisFunctionInAnyCase();

สิ่งนี้จะตอบสนองความต้องการของคุณทั้งสอง:

  • executeStep<X>()ควรประเมินเฉพาะเมื่อก่อนหน้านี้ประสบความสำเร็จ (นี้เรียกว่าการประเมินผลการลัดวงจร )
  • executeThisFunctionInAnyCase() จะถูกดำเนินการในกรณีใด ๆ

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

58
@RobAu: ถ้าอย่างนั้นมันจะดีสำหรับโปรแกรมเมอร์รุ่นพี่ในที่สุดก็เห็นโค้ดที่ใช้การประเมินแบบลัดและอาจกระตุ้นให้พวกเขา reseach หัวข้อนี้ซึ่งจะช่วยพวกเขาในการเป็นโปรแกรมเมอร์อาวุโสในที่สุด เห็นได้ชัดว่า win-win: โค้ดที่เหมาะสมและมีคนเรียนรู้บางอย่างจากการอ่าน
x4u

24
นี่ควรเป็นคำตอบที่ดีที่สุด
ชื่อที่ปรากฏ

61
@RobAu: นี่ไม่ใช่การแฮ็กที่ใช้ประโยชน์จากเคล็ดลับไวยากรณ์ที่ไม่ชัดเจน แต่มันก็มีความหมายสูงในเกือบทุกภาษาการเขียนโปรแกรมจนถึงจุดที่เป็นแบบมาตรฐานที่ไม่มีการปฎิบัติ
BlueRaja - Danny Pflughoeft

38
วิธีการแก้ปัญหานี้ใช้เฉพาะในกรณีที่เงื่อนไขเป็นจริงว่าการเรียกฟังก์ชั่นที่เรียบง่าย ในรหัสจริงเงื่อนไขเหล่านั้นอาจมีความยาว 2-5 บรรทัด (และตัวเองรวมกันเป็นจำนวนมาก&&และอื่น ๆ||) ดังนั้นจึงไม่มีวิธีที่คุณต้องการรวมเข้ากับifคำแถลงเดียวโดยไม่ต้องอ่านค่าสกรู และมันไม่ง่ายเสมอไปที่จะย้ายเงื่อนไขเหล่านั้นไปยังฟังก์ชั่นด้านนอกเพราะมันอาจขึ้นอยู่กับตัวแปรท้องถิ่นที่คำนวณไว้ก่อนหน้านี้จำนวนมากซึ่งจะทำให้เกิดความยุ่งเหยิงอย่างมากหากคุณพยายามส่งผ่านแต่ละอาร์กิวเมนต์
hamstergene

358

เพียงใช้ฟังก์ชั่นเพิ่มเติมเพื่อให้รุ่นที่สองทำงานได้:

void foo()
{
  bool conditionA = executeStepA();
  if (!conditionA) return;

  bool conditionB = executeStepB();
  if (!conditionB) return;

  bool conditionC = executeStepC();
  if (!conditionC) return;
}

void bar()
{
  foo();
  executeThisFunctionInAnyCase();
}

การใช้ ifs ที่ซ้อนกันอย่างล้ำลึก (ตัวแปรแรกของคุณ) หรือความปรารถนาที่จะแยกออกจาก "ส่วนหนึ่งของฟังก์ชั่น" มักจะหมายความว่าคุณจำเป็นต้องมีฟังก์ชั่นพิเศษ


51
+1 ในที่สุดมีคนโพสต์คำตอบที่มีเหตุผล นี่เป็นวิธีที่ถูกต้องปลอดภัยและอ่านง่ายที่สุดในความคิดของฉัน
Lundin

31
+1 นี่เป็นภาพประกอบที่ดีของ "หลักการความรับผิดชอบเดี่ยว" ฟังก์ชั่นfooทำงานมาถึงผ่านลำดับของเงื่อนไขและการกระทำที่เกี่ยวข้อง ฟังก์ชั่นbarถูกแยกออกจากการตัดสินใจอย่างสมบูรณ์ ถ้าเราเห็นรายละเอียดของเงื่อนไขและการกระทำมันอาจกลายfooเป็นว่ามันยังทำอะไรมากเกินไป แต่ตอนนี้มันเป็นทางออกที่ดี
GraniteRobert

13
ข้อเสียคือ C ไม่ได้มีฟังก์ชั่นซ้อนกันดังนั้นหาก 3 ขั้นตอนเหล่านั้นจำเป็นต้องใช้ตัวแปรจากbarคุณก็จะต้องผ่านมันไปfooเป็นพารามิเตอร์ด้วยตนเอง หากเป็นกรณีนี้และหากfooถูกเรียกเพียงครั้งเดียวฉันจะใช้รุ่น goto เพื่อหลีกเลี่ยงการกำหนดฟังก์ชั่นการทำงานสองอย่างที่แน่นหนาซึ่งท้ายที่สุดจะไม่สามารถนำมาใช้ซ้ำได้
hugomg

7
ไม่แน่ใจเกี่ยวกับไวยากรณ์ลัดวงจรสำหรับ C แต่ใน C # foo () สามารถเขียนเป็นif (!executeStepA() || !executeStepB() || !executeStepC()) return
Travis

6
@ user1598390 Goto ถูกใช้ตลอดเวลาโดยเฉพาะอย่างยิ่งในการเขียนโปรแกรมระบบเมื่อคุณต้องการผ่อนคลายรหัสการตั้งค่าจำนวนมาก
Scotty Bauer

166

โปรแกรมเมอร์ซีโรงเรียนเก่าใช้gotoในกรณีนี้ มันเป็นการใช้งานครั้งเดียวgotoที่สนับสนุนโดย styleguide จริง ๆ ของ Linux ซึ่งเรียกว่า exit function function ส่วนกลาง:

int foo() {
    int result = /*some error code*/;
    if(!executeStepA()) goto cleanup;
    if(!executeStepB()) goto cleanup;
    if(!executeStepC()) goto cleanup;

    result = 0;
cleanup:
    executeThisFunctionInAnyCase();
    return result;
}

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

int foo() {
    int result = /*some error code*/;
    if(!executeStepA()) goto cleanupPart;
    if(!executeStepB()) goto cleanup;
    if(!executeStepC()) goto cleanup;

    result = 0;
cleanup:
    innerCleanup();
cleanupPart:
    executeThisFunctionInAnyCase();
    return result;
}

ด้วยวิธีการวนซ้ำคุณจะจบลงด้วยการวนซ้ำสองระดับในกรณีนั้น


108
+1 ผู้คนจำนวนมากเห็นgotoและคิดในทันทีว่า "นี่เป็นรหัสที่แย่มาก" แต่มันมีประโยชน์ในการใช้งาน
แกรมเพอร์โรว์

46
ยกเว้นว่านี่เป็นรหัสที่ค่อนข้างยุ่งจริงๆจากมุมมองการบำรุงรักษา โดยเฉพาะอย่างยิ่งเมื่อมีป้ายกำกับหลายป้ายซึ่งรหัสยังอ่านยาก มีวิธีหรูหรากว่านี้เพื่อให้บรรลุ: ใช้ฟังก์ชั่น
Lundin

29
-1 ฉันเห็นgotoและไม่ต้องคิดว่าจะเป็นรหัสที่น่ากลัว ฉันเคยต้องรักษามันไว้มันน่ารังเกียจมาก OP แนะนำทางเลือกภาษา C ที่สมเหตุสมผลในตอนท้ายของคำถามและฉันรวมไว้ในคำตอบของฉัน
ไชโยและ hth - Alf

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

66
ฉันได้เขียนโค้ดที่ชัดเจนและง่ายต่อการดูแลรักษาในรูปแบบนี้เป็นแสนบรรทัด สิ่งสำคัญสองประการที่ต้องเข้าใจคือวินัย (1)! สร้างแนวทางที่ชัดเจนสำหรับรูปแบบของทุกฟังก์ชั่นและเพียงละเมิดมันที่จำเป็นมากและ (2) เข้าใจว่าสิ่งที่เรากำลังทำอะไรที่นี่มีการจำลองข้อยกเว้นในภาษาที่ไม่ได้มีพวกเขา throwมีหลายวิธีที่เลวร้ายยิ่งกว่าgotoเพราะthrowมันยังไม่ชัดเจนจากบริบทท้องถิ่นที่คุณกำลังจะจบลง! ใช้ข้อควรพิจารณาการออกแบบเดียวกันสำหรับโฟลว์การควบคุมสไตล์ goto ตามที่คุณต้องการยกเว้น
Eric Lippert

131

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

นี่คือลูกศร

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

แผ่ธนูกับ Guard

บางวิธีการทั่วไปของการหลีกเลี่ยงลูกศรที่จะกล่าวถึงที่นี่ วิธีที่พบมากที่สุดคือการใช้รูปแบบการป้องกันซึ่งในรหัสจะจัดการข้อยกเว้นการไหลก่อนแล้วจัดการการไหลขั้นพื้นฐานเช่นแทน

if (ok)
{
    DoSomething();
}
else
{
    _log.Error("oops");
    return;
}

... คุณจะใช้ ....

if (!ok)
{
    _log.Error("oops");
    return;
} 
DoSomething(); //notice how this is already farther to the left than the example above

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

ลูกศร:

ok = DoSomething1();
if (ok)
{
    ok = DoSomething2();
    if (ok)
    {
        ok = DoSomething3();
        if (!ok)
        {
            _log.Error("oops");  //Tip of the Arrow
            return;
        }
    }
    else
    {
       _log.Error("oops");
       return;
    }
}
else
{
    _log.Error("oops");
    return;
}

ป้องกัน:

ok = DoSomething1();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething2();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething3();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething4();
if (!ok)
{
    _log.Error("oops");
    return;
} 

นี่เป็นวิธีที่อ่านง่ายและเป็นเชิงปริมาณ

  1. อักขระ {และ} สำหรับบล็อกตรรกะที่กำหนดนั้นอยู่ใกล้กันมากขึ้น
  2. จำนวนบริบททางจิตที่จำเป็นในการทำความเข้าใจกับบรรทัดใดบรรทัดหนึ่งมีขนาดเล็กกว่า
  3. ตรรกะทั้งหมดที่เกี่ยวข้องกับเงื่อนไข if น่าจะอยู่ในหน้าเดียวมากกว่า
  4. ความต้องการใช้ coder เพื่อเลื่อนหน้า / ติดตามตาลดลงอย่างมาก

วิธีเพิ่มรหัสทั่วไปในตอนท้าย

ปัญหาเกี่ยวกับรูปแบบของการ์ดรักษาความปลอดภัยนั้นขึ้นอยู่กับสิ่งที่เรียกว่า "การส่งคืนโอกาส" หรือ "ทางออกการฉวยโอกาส" มันแบ่งรูปแบบที่แต่ละฟังก์ชั่นและทุกคนควรมีจุดทางออกเดียว ปัญหานี้เกิดจากสองสาเหตุ:

  1. มันทำให้คนอื่นถูวิธีที่ผิดเช่นคนที่เรียนรู้ที่จะใช้รหัสบน Pascal ได้เรียนรู้ว่าการทำงานหนึ่ง = หนึ่งจุดทางออก
  2. มันไม่ได้ให้ส่วนของรหัสที่ดำเนินการเมื่อออกไม่ว่าสิ่งที่อยู่ในมือ

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

ตัวเลือกที่ 1 คุณไม่สามารถทำได้: ใช้ finally

น่าเสียดายที่ในฐานะนักพัฒนา c ++ คุณไม่สามารถทำได้ แต่นี่เป็นคำตอบหมายเลขหนึ่งสำหรับภาษาที่มีคำหลักในที่สุดเนื่องจากนี่คือสิ่งที่มันเป็น

try
{
    if (!ok)
    {
        _log.Error("oops");
        return;
    } 
    DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
    DoSomethingNoMatterWhat();
}

ตัวเลือกที่ 2 หลีกเลี่ยงปัญหา: ปรับโครงสร้างการทำงานของคุณ

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

นี่คือตัวอย่าง:

void OuterFunction()
{
    DoSomethingIfPossible();
    DoSomethingNoMatterWhat();
}

void DoSomethingIfPossible()
{
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    DoSomething();
}

ตัวเลือก 3. เคล็ดลับภาษา: ใช้วนซ้ำปลอม

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

while(true)
{
     if (!ok) break;
     DoSomething();
     break;  //important
}
DoSomethingNoMatterWhat();

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

มีตัวแปรอื่น ๆ ของตัวเลือกนี้ ตัวอย่างเช่นหนึ่งสามารถใช้แทนswitch whileโครงสร้างภาษาใด ๆ ที่มีbreakคำหลักอาจจะทำงานได้

ตัวเลือก 4 ยกระดับวัฏจักรชีวิตของวัตถุ

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

class MyContext
{
   ~MyContext()
   {
        DoSomethingNoMatterWhat();
   }
}

void MainMethod()
{
    MyContext myContext;
    ok = DoSomething(myContext);
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    ok = DoSomethingElse(myContext);
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    ok = DoSomethingMore(myContext);
    if (!ok)
    {
        _log.Error("Oops");
    }

    //DoSomethingNoMatterWhat will be called when myContext goes out of scope
}

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

ตัวเลือก 4.1 ยกระดับวัฏจักรของวัตถุ (รูปแบบตัวห่อ)

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

class MyWrapper 
{
   bool DoSomething() {...};
   bool DoSomethingElse() {...}


   void ~MyWapper()
   {
        DoSomethingNoMatterWhat();
   }
}

void MainMethod()
{
    bool ok = myWrapper.DoSomething();
    if (!ok)
        _log.Error("Oops");
        return;
    }
    ok = myWrapper.DoSomethingElse();
    if (!ok)
       _log.Error("Oops");
        return;
    }
}
//DoSomethingNoMatterWhat will be called when myWrapper is destroyed

อีกครั้งให้แน่ใจว่าคุณเข้าใจวงจรชีวิตของวัตถุของคุณ

ตัวเลือกที่ 5 เคล็ดลับภาษา: ใช้การประเมินผลการลัดวงจร

อีกเทคนิคหนึ่งคือการใช้ประโยชน์จากการประเมินผลการลัดวงจร

if (DoSomething1() && DoSomething2() && DoSomething3())
{
    DoSomething4();
}
DoSomethingNoMatterWhat();

โซลูชันนี้ใช้ประโยชน์จากวิธีการทำงานของผู้ให้บริการ เมื่อด้านซ้ายของ && ประเมินเป็นเท็จทางด้านขวาจะไม่ถูกประเมิน

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


12
สุดท้าย? C ++ ไม่มีประโยคสุดท้าย วัตถุที่มี destructor จะถูกใช้ในสถานการณ์ที่คุณคิดว่าคุณต้องการประโยคสุดท้าย
Bill Door

1
แก้ไขเพื่อรองรับความคิดเห็นทั้งสองด้านบน
John Wu

2
ด้วยรหัสเล็กน้อย (เช่นในตัวอย่างของฉัน) รูปแบบที่ซ้อนกันอาจจะเข้าใจได้มากกว่า ด้วยรหัสโลกแห่งความจริง (ซึ่งสามารถขยายได้หลายหน้า) รูปแบบการป้องกันนั้นง่ายต่อการอ่านเนื่องจากจะต้องใช้การเลื่อนน้อยลงและการติดตามสายตาที่น้อยลงเช่นระยะห่างเฉลี่ยจาก {ถึง} สั้นลง
John Wu

1
ฉันเห็นรูปแบบที่ซ้อนกันซึ่งไม่สามารถมองเห็นรหัสบนหน้าจอ 1920 x 1080 อีกต่อไป ... ลองค้นหารหัสการจัดการข้อผิดพลาดที่จะดำเนินการหากการกระทำที่สามล้มเหลว ... ฉันเคยทำ {... } ขณะที่ใช้ (0) แทนดังนั้นคุณไม่จำเป็นต้องหยุดพักสุดท้าย (ในขณะที่ (จริง) {... } อนุญาตให้ "ดำเนินการต่อ" เพื่อเริ่มต้นใหม่อีกครั้ง
gnasher729

2
ตัวเลือกที่ 4 ของคุณเป็นหน่วยความจำรั่วใน C ++ จริง ๆ (ละเว้นข้อผิดพลาดทางไวยากรณ์เล็กน้อย) ไม่มีใครใช้ "ใหม่" ในกรณีเช่นนี้เพียงแค่พูดว่า "MyContext myContext;"
Sumudu Fernando

60

แค่ทำ

if( executeStepA() && executeStepB() && executeStepC() )
{
    // ...
}
executeThisFunctionInAnyCase();

มันง่ายมาก


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

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}

executeThisFunctionInAnyCase();

14
ฉันตอบคำถามนี้ (สั้นกระชับขึ้น) เพื่อตอบคำถามรุ่นแรกและมันมี upvotes 20 คะแนนก่อนที่ Bill the Lizard จะถูกลบหลังจากความเห็นและแก้ไขโดย Darkness Races ใน Orbit
ไชโยและ hth - Alf

1
@ Cheersandhth-Alf: ฉันไม่อยากจะเชื่อเลยว่ามันถูกดัดแปลงแล้ว มันแย่มาก (+1)
ครัวซองต์ Paramagnetic

2
+1 ความกล้าหาญของคุณ! (3 ครั้งสำคัญ: D)
haccks

มันเป็นสิ่งสำคัญอย่างยิ่งที่โปรแกรมเมอร์มือใหม่ต้องเรียนรู้เกี่ยวกับสิ่งต่าง ๆ เช่นการสั่งดำเนินการกับบูลีน "ands" หลาย ๆ แบบวิธีที่แตกต่างกันในภาษาอื่น ๆ และคำตอบนี้เป็นตัวชี้ที่ยอดเยี่ยม แต่มันเป็นเพียง "ไม่ใช่ผู้เริ่มต้น" ในการเขียนโปรแกรมเชิงพาณิชย์ปกติ มันไม่ง่ายไม่บำรุงรักษาไม่สามารถแก้ไขได้ แน่นอนว่าถ้าคุณเพียงแค่เขียน "โคบาลรหัส" ด้วยตัวคุณเองให้ทำสิ่งนี้ แต่มันมีแค่ "ไม่มีการเชื่อมต่อกับวิศวกรรมซอฟต์แวร์ทุกวันอย่างที่ปฏิบัติกันในวันนี้" BTW ขออภัยในความไร้สาระ "แก้ไขสับสน" คุณต้องทนนี่ @cheers :)
Fattie

1
@ JoeBlow: ฉันกับ Alf เกี่ยวกับเรื่องนี้ - ฉันพบ&&รายการที่ชัดเจนมากขึ้น ฉันมักจะทำผิดเงื่อนไขเพื่อแยกบรรทัดและเพิ่มส่วนท้าย// explanationให้กับแต่ละ .... ท้ายที่สุดมันเป็นโค้ดที่ดูน้อยกว่ามากและเมื่อคุณเข้าใจว่าการ&&ทำงานไม่มีความพยายามทางจิตอย่างต่อเนื่อง ความประทับใจของฉันคือโปรแกรมเมอร์ C ++ มืออาชีพส่วนใหญ่จะคุ้นเคยกับสิ่งนี้ แต่เมื่อคุณพูดในอุตสาหกรรม / โครงการที่แตกต่างกันโฟกัสและประสบการณ์ที่แตกต่างกัน
Tony Delroy

35

มีวิธีการเลื่อนการทำงานใน C ++: การใช้ประโยชน์จาก destructor ของวัตถุ

สมมติว่าคุณมีสิทธิ์เข้าถึง C ++ 11:

class Defer {
public:
    Defer(std::function<void()> f): f_(std::move(f)) {}
    ~Defer() { if (f_) { f_(); } }

    void cancel() { f_ = std::function<void()>(); }

private:
    Defer(Defer const&) = delete;
    Defer& operator=(Defer const&) = delete;

    std::function<void()> f_;
}; // class Defer

จากนั้นใช้ยูทิลิตีนั้น:

int foo() {
    Defer const defer{&executeThisFunctionInAnyCase}; // or a lambda

    // ...

    if (!executeA()) { return 1; }

    // ...

    if (!executeB()) { return 2; }

    // ...

    if (!executeC()) { return 3; }

    // ...

    return 4;
} // foo

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

24
@Lundin: เอาละฉันไม่เข้าใจว่าใครจะพึงพอใจกับรหัสที่เปราะบางที่จะแตกเร็วที่สุดเมื่อมีการแนะนำตัวต่อ / ทำลาย / คืนเร็วขึ้นหรือมีข้อยกเว้นเกิดขึ้น ในทางกลับกันโซลูชันนี้มีความยืดหยุ่นเมื่อเผชิญกับการบำรุงรักษาในอนาคตและขึ้นอยู่กับความจริงที่ว่า destructors ถูกดำเนินการในระหว่างการคลายซึ่งเป็นหนึ่งในคุณสมบัติที่สำคัญที่สุดของ C ++ มีคุณสมบัตินี้แปลกใหม่ในขณะที่มันเป็นหลักการพื้นฐานที่ให้อำนาจภาชนะมาตรฐานทั้งหมดเป็นที่น่าขบขันที่จะพูดน้อย
Matthieu M.

7
@Lundin: ประโยชน์ของรหัสของ Matthieu คือมันexecuteThisFunctionInAnyCase();จะทำงานแม้ว่าจะfoo();มีข้อยกเว้น เมื่อเขียนโค้ดที่ปลอดภัยยกเว้นเป็นแนวปฏิบัติที่ดีที่จะวางฟังก์ชันการล้างข้อมูลทั้งหมดใน destructor
Brian

6
@ Brian แล้วไม่ได้โยนข้อยกเว้นใด ๆ foo()ใน และถ้าคุณจับมันได้ แก้ไขปัญหา. แก้ไขข้อบกพร่องด้วยการแก้ไขไม่ใช่โดยการเขียนวิธีแก้ไข
Lundin

18
@Lundin: Deferคลาสนี้เป็นโค้ดขนาดเล็กที่สามารถใช้ซ้ำได้ซึ่งช่วยให้คุณทำการล้างข้อมูลส่วนท้ายของบล็อกด้วยวิธีที่ปลอดภัย เป็นที่รู้จักกันมากกว่าปกติเป็นยามขอบเขต ใช่การใช้ขอบเขตยามใด ๆ สามารถแสดงออกด้วยวิธีอื่น ๆ ได้เช่นเดียวกับการforวนซ้ำใด ๆ ที่สามารถแสดงเป็นบล็อกและwhileลูปซึ่งจะสามารถแสดงด้วยifและgotoซึ่งสามารถแสดงเป็นภาษาแอสเซมบลีได้ถ้าคุณต้องการ หรือสำหรับผู้ที่เป็นอาจารย์ตัวจริงโดยการปรับเปลี่ยนบิตในหน่วยความจำผ่านรังสีคอสมิกที่ควบคุมโดยเอฟเฟกต์ผีเสื้อของคำรามสั้น ๆ และบทสวด แต่ทำไมทำอย่างนั้น
ไชโยและ hth - Alf

34

มีเทคนิคที่ดีที่ไม่ต้องการฟังก์ชั่นการห่อหุ้มเพิ่มเติมพร้อมคำสั่ง return (วิธีที่กำหนดโดย Itjax) มันใช้ประโยชน์จากwhile(0)วงหลอกทำ while (0)เพื่อให้แน่ใจว่ามันเป็นจริงไม่ห่วง แต่ดำเนินการได้เพียงครั้งเดียว อย่างไรก็ตามไวยากรณ์วนซ้ำอนุญาตให้ใช้คำสั่ง break

void foo()
{
  // ...
  do {
      if (!executeStepA())
          break;
      if (!executeStepB())
          break;
      if (!executeStepC())
          break;
  }
  while (0);
  // ...
}

11
ในการใช้ฟังก์ชั่นที่มีผลตอบแทนหลายรายการจะคล้ายกัน แต่สามารถอ่านได้มากขึ้นในความคิดของฉัน
Lundin

4
ใช่แน่นอนมันอ่านได้ง่ายกว่า ... อย่างไรก็ตามจากมุมมองประสิทธิภาพการเรียกใช้ฟังก์ชันเพิ่มเติม (การผ่านพารามิเตอร์และการส่งคืน) สามารถหลีกเลี่ยงได้โดยใช้ do {} ขณะที่ (0) การสร้าง
Debasis

5
inlineคุณยังคงมีอิสระในการทำงาน อย่างไรก็ตามนี่เป็นเทคนิคที่น่ารู้เพราะช่วยได้มากกว่าปัญหานี้
Ruslan

2
@Lundin คุณต้องคำนึงถึงตำแหน่งของโค้ดด้วยการกระจายโค้ดในหลาย ๆ ที่ก็มีปัญหาเช่นกัน
API-Beast

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

19

คุณสามารถทำได้เช่นกัน:

bool isOk = true;
std::vector<bool (*)(void)> funcs; //vector of function ptr

funcs.push_back(&executeStepA);
funcs.push_back(&executeStepB);
funcs.push_back(&executeStepC);
//...

//this will stop at the first false return
for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it) 
    isOk = (*it)();
if (isOk)
 //doSomeStuff
executeThisFunctionInAnyCase();

วิธีนี้คุณมีขนาดการเติบโตเชิงเส้นที่น้อยที่สุด +1 สายต่อการโทรหนึ่งครั้งและสามารถบำรุงรักษาได้ง่าย


แก้ไข : (ขอบคุณ @ Unda) ไม่ใช่แฟนตัวยงเพราะคุณขาดการมองเห็น IMO:

bool isOk = true;
auto funcs { //using c++11 initializer_list
    &executeStepA,
    &executeStepB,
    &executeStepC
};

for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it) 
    isOk = (*it)();
if (isOk)
 //doSomeStuff
executeThisFunctionInAnyCase();

1
มันเป็นเรื่องของอุบัติเหตุสายฟังก์ชั่นภายใน push_back () แต่คุณคงที่เลยล่ะค่ะ :)
เควนติน

4
ฉันชอบที่จะรู้ว่าทำไมมันถึงได้ลงคะแนน สมมติว่าขั้นตอนการดำเนินการมีความสมมาตรตามที่ดูเหมือนว่าจะสะอาดและสามารถบำรุงรักษาได้
ClickRick

1
แม้ว่านี่อาจดูสะอาดกว่า อาจเป็นการยากที่จะเข้าใจผู้คนและยากที่จะเข้าใจผู้แปล!
Roy T.

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

3
over-engineered เล็กน้อยสำหรับกรณีเล็กน้อย แต่เทคนิคนี้แน่นอนมีคุณสมบัติที่ดีอื่น ๆ ที่ไม่: คุณสามารถเปลี่ยนลำดับของการดำเนินการและ [จำนวน] ฟังก์ชั่นที่เรียกว่ารันไทม์และนั่นเป็นเรื่องที่ดี :)
Xocoatzin

18

จะใช้งานได้ไหม ฉันคิดว่านี่เทียบเท่ากับรหัสของคุณ

bool condition = true; // using only one boolean variable
if (condition) condition = executeStepA();
if (condition) condition = executeStepB();
if (condition) condition = executeStepC();
...
executeThisFunctionInAnyCase();

3
ฉันมักจะเรียกตัวแปรokเมื่อใช้ตัวแปรเดียวกันเช่นนี้
Macke

1
ฉันจะสนใจมากในการรู้ว่าทำไม downvotes เกิดอะไรขึ้นที่นี่?
ABCplus

1
เปรียบเทียบคำตอบของคุณกับวิธีลัดวงจรสำหรับความซับซ้อนของวงจร
AlphaGoku

14

สมมติว่ารหัสที่ต้องการเป็นอย่างที่ฉันเห็นในขณะนี้:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}    
executeThisFunctionInAnyCase();

ฉันจะบอกว่าวิธีที่ถูกต้องในการที่ง่ายที่สุดในการอ่านและง่ายที่สุดในการรักษาจะมีระดับของการเยื้องน้อยลงซึ่งเป็น (ปัจจุบัน) วัตถุประสงค์ที่ระบุไว้ของคำถาม

// Pre-declare the variables for the conditions
bool conditionA = false;
bool conditionB = false;
bool conditionC = false;

// Execute each step only if the pre-conditions are met
conditionA = executeStepA();
if (conditionA)
    conditionB = executeStepB();
if (conditionB)
    conditionC = executeStepC();
if (conditionC) {
    ...
}

// Unconditionally execute the 'cleanup' part.
executeThisFunctionInAnyCase();

สิ่งนี้จะช่วยหลีกเลี่ยงความต้องการgotos, ข้อยกเว้น, dummy whileloops, หรือสิ่งก่อสร้างที่ยากอื่น ๆ และเพียงแค่ทำงานต่อไป


เมื่อใช้ลูปก็มักจะยอมรับได้ในการใช้returnและbreakกระโดดออกจากลูปโดยไม่จำเป็นต้องแนะนำตัวแปร "แฟล็ก" เพิ่มเติม ในกรณีนี้การใช้ goto จะเหมือนกับ innocuou เหมือนกัน - โปรดจำไว้ว่าคุณกำลังแลกเปลี่ยนความซับซ้อน goto พิเศษเพื่อความซับซ้อนของตัวแปรที่ไม่แน่นอน
hugomg

2
@hugomg ตัวแปรอยู่ในคำถามเดิม ไม่มีความซับซ้อนเพิ่มเติมที่นี่ มีการตั้งสมมติฐานเกี่ยวกับคำถาม (เช่นว่าตัวแปรจำเป็นในโค้ดที่เติมใหม่) ดังนั้นจึงถูกเก็บรักษาไว้ หากพวกเขาไม่ต้องการรหัสก็สามารถทำให้ง่ายขึ้นได้ แต่เนื่องจากลักษณะที่ไม่สมบูรณ์ของคำถามนั้นไม่มีข้อสันนิษฐานอื่นใดที่สามารถทำได้อย่างถูกต้อง
ClickRick

แนวทางที่มีประโยชน์มากโดยเฉพาะ สำหรับการใช้งานโดยตนเองที่ยอมรับnewbieก็มีวิธีการแก้ปัญหาที่สะอาดและไม่มีข้อบกพร่อง ฉันทราบว่ามันไม่ได้ขึ้นอยู่กับการstepsมีลายเซ็นเดียวกันหรือแม้กระทั่งฟังก์ชั่นมากกว่าบล็อก ฉันสามารถเห็นการใช้สิ่งนี้เป็น refactor ผ่านแรกแม้ในขณะที่วิธีการที่ซับซ้อนมากขึ้นถูกต้อง
Keith

12

สามารถใช้ break statement อย่างใดได้ไหม?

อาจจะไม่ได้เป็นทางออกที่ดีที่สุด แต่คุณสามารถใส่งบของคุณในdo .. while (0)วงและการใช้งบแทนbreak return


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

3
@ClickRick ที่ใช้do .. while (0)สำหรับคำจำกัดความของแมโครยังทำผิดกฎของลูป แต่ก็ถือว่าใช้ได้
ouah

1
บางที แต่มีวิธีที่สะอาดกว่าเพื่อให้บรรลุ
ClickRick

4
@ClickRick จุดประสงค์เพียงอย่างเดียวของคำตอบของฉันคือการตอบสามารถแบ่งคำสั่งได้ด้วยวิธีใดวิธีหนึ่งและคำตอบคือใช่คำแรกในคำตอบของฉันแนะนำว่านี่อาจไม่ใช่วิธีการแก้ปัญหาที่ใช้
ouah

2
คำตอบนี้ควรเป็นความคิดเห็น
msmucker0527

12

คุณสามารถใส่ifเงื่อนไขทั้งหมดจัดรูปแบบตามที่คุณต้องการในฟังก์ชั่นของตัวเองเมื่อกลับมาดำเนินการexecuteThisFunctionInAnyCase()ฟังก์ชัน

จากตัวอย่างพื้นฐานใน OP การทดสอบสภาพและการปฏิบัติสามารถแยกออกได้เช่น

void InitialSteps()
{
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
}

แล้วเรียกว่าเป็นเช่นนั้น

InitialSteps();
executeThisFunctionInAnyCase();

ถ้า C ++ 11 lambdas พร้อมใช้งาน (ไม่มีแท็ก C ++ 11 ใน OP แต่อาจยังคงเป็นตัวเลือก) จากนั้นเราสามารถละฟังก์ชันแยกและห่อสิ่งนี้ลงในแลมบ์ดา

// Capture by reference (variable access may be required)
auto initialSteps = [&]() {
  // any additional code
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  // any additional code
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  // any additional code
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
};

initialSteps();
executeThisFunctionInAnyCase();

10

หากคุณไม่ชอบgotoและไม่ชอบdo { } while (0);ลูปและต้องการใช้ C ++ คุณสามารถใช้แลมบ์ดาชั่วคราวเพื่อให้ได้ผลเหมือนกัน

[&]() { // create a capture all lambda
  if (!executeStepA()) { return; }
  if (!executeStepB()) { return; }
  if (!executeStepC()) { return; }
}(); // and immediately call it

executeThisFunctionInAnyCase();

1
if คุณไม่ชอบ goto && คุณไม่ชอบทำ {} ในขณะที่ (0) && คุณชอบ C ++ ... ขออภัยไม่สามารถต้านทานได้ แต่เงื่อนไขสุดท้ายล้มเหลวเนื่องจากคำถามถูกติดแท็กcรวมทั้งc ++
ClickRick

@ClickRick เป็นเรื่องยากที่จะทำให้ทุกคนพอใจ ในความคิดของฉันไม่มีสิ่งเช่น C / C ++ คุณมักจะรหัสในอย่างใดอย่างหนึ่งเหล่านั้นและการใช้งานของคนอื่นเป็นขมวดคิ้ว
อเล็กซ์

9

กลุ่มของ IF / ELSE ในรหัสของคุณไม่ใช่ปัญหาทางภาษา แต่เป็นการออกแบบโปรแกรมของคุณ หากคุณสามารถพิจารณาปัจจัยใหม่หรือเขียนโปรแกรมของคุณใหม่ฉันอยากจะแนะนำให้คุณดูใน Design Patterns ( http://sourcemaking.com/design_patterns ) เพื่อหาทางออกที่ดีกว่า

โดยปกติเมื่อคุณเห็น IF จำนวนมาก & อยู่ในรหัสของคุณมันเป็นโอกาสที่จะใช้รูปแบบการออกแบบกลยุทธ์ ( http://sourcemaking.com/design_patterns/strategy/c-sharp-dot-net ) หรืออาจเป็นการรวมกัน ของรูปแบบอื่น ๆ

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


9

คุณเพิ่งทำสิ่งนี้ ..

coverConditions();
executeThisFunctionInAnyCase();

function coverConditions()
 {
 bool conditionA = executeStepA();
 if (!conditionA) return;
 bool conditionB = executeStepB();
 if (!conditionB) return;
 bool conditionC = executeStepC();
 if (!conditionC) return;
 }

99 เท่าของ 100 นี่เป็นวิธีเดียวที่จะทำได้

ไม่เคยเคยลองทำอะไรที่ "ยุ่งยาก" ในรหัสคอมพิวเตอร์


โดยวิธีการที่ฉันค่อนข้างแน่ใจว่าต่อไปนี้เป็นทางออกที่แท้จริงที่คุณมีในใจ ...

คำสั่งดำเนินการต่อเป็นสิ่งสำคัญในการเขียนโปรแกรมอัลกอริทึม (เช่นเดียวกับคำสั่ง goto มีความสำคัญในการเขียนโปรแกรมอัลกอริธึม)

ในหลาย ๆ ภาษาการเขียนโปรแกรมคุณสามารถทำได้:

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");
    
    int x = 69;
    
    {
    
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    
    }
    
    NSLog(@"code g");
    }

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

นั่นคือสิ่งที่คุณมีในหัวใช่มั้ย และนั่นคือวิธีเขียนที่สวยงามดังนั้นคุณจึงมีสัญชาตญาณที่ดี

อย่างไรก็ตามอนาถาในรุ่นปัจจุบันของวัตถุประสงค์ -c (นอกเหนือจาก - ฉันไม่ทราบเกี่ยวกับ Swift, เสียใจ) มีคุณสมบัติที่สามารถตรวจสอบได้ว่ามันจะตรวจสอบว่าบล็อกล้อมรอบเป็นวง

ป้อนคำอธิบายรูปภาพที่นี่

นี่คือวิธีที่คุณได้รับ ...

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");
    
    int x = 69;
    
    do{
    
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    
    }while(false);
    
    NSLog(@"code g");
    }

ดังนั้นอย่าลืมว่า ..

ทำ {} ในขณะที่ (เท็จ);

เพียงหมายถึง "ทำบล็อกนี้เพียงครั้งเดียว"

กล่าวคือไม่มีความแตกต่างอย่างมากระหว่างการเขียนdo{}while(false);และการเขียนเพียงอย่าง{}เดียว

ตอนนี้ทำงานได้อย่างสมบูรณ์แบบตามที่คุณต้องการ ... นี่คือผลลัพธ์ ...

ป้อนคำอธิบายรูปภาพที่นี่

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

ในโครงการ "อัลกอริทึม" ที่สิ่งนี้เกิดขึ้นมากมายในวัตถุประสงค์ -c เรามักมีมาโครอย่าง ...

#define RUNONCE while(false)

... ดังนั้นคุณสามารถทำสิ่งนี้ได้ ...

-(void)_testKode
    {
    NSLog(@"code a");
    int x = 69;
    
    do{
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    }RUNONCE
    
    NSLog(@"code g");
    }

มีสองจุดคือ

a ถึงแม้ว่ามันจะโง่ที่วัตถุ -c ตรวจสอบชนิดของคำสั่งบล็อกที่ยังคงอยู่ในนั้น ดังนั้นการตัดสินใจที่ยากลำบาก

b, มีคำถามที่คุณควรเยื้องในตัวอย่างบล็อกที่? ฉันนอนไม่หลับเพราะคำถามเช่นนั้นดังนั้นฉันจึงไม่สามารถให้คำแนะนำได้

หวังว่ามันจะช่วย


คุณพลาดคำตอบที่ดีที่สุดที่สองได้รับการโหวต
Palec

คุณเพิ่มค่าหัวให้ตัวแทนมากขึ้นหรือไม่? :)
TMS

แทนที่จะใส่ความคิดเห็นเหล่านั้นไว้ในif, คุณสามารถใช้ชื่อฟังก์ชั่นอธิบายเพิ่มเติมและใส่ความคิดเห็นในฟังก์ชั่น
โทมัส Ahle

yuck ฉันจะใช้วิธีแก้ปัญหา 1 บรรทัดที่อ่านอย่างกระชับกับการประเมินการลัดวงจร (ซึ่งเป็นภาษามานานกว่า 20 ปีและเป็นที่รู้จักกันดี) เรื่องระเบียบนี้ทุกวัน ฉันคิดว่าเราทั้งคู่เห็นด้วยว่าเรามีความสุขที่ไม่ได้ทำงานร่วมกัน
M2tM

8

ให้ฟังก์ชันที่เรียกใช้งานของคุณมีข้อยกเว้นหากพวกเขาล้มเหลวแทนที่จะกลับมาเป็นเท็จ จากนั้นรหัสโทรของคุณอาจมีลักษณะเช่นนี้:

try {
    executeStepA();
    executeStepB();
    executeStepC();
}
catch (...)

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


3
การใช้ข้อยกเว้นในการควบคุมโฟลว์นั้นมักจะถือว่าเป็นการปฏิบัติที่ไม่ดีและมีรหัสเหม็น
user902383

8

มีคำตอบที่ดีอยู่แล้ว แต่ส่วนใหญ่ดูเหมือนจะแลกกับความยืดหยุ่น (บางคนยอมรับน้อยมาก) วิธีการทั่วไปที่ไม่ต้องการการแลกเปลี่ยนนี้คือการเพิ่มสถานะ /ตัวแปรต่อเนื่อง แน่นอนว่าราคามีค่าพิเศษหนึ่งอย่างที่ต้องติดตาม:

bool ok = true;
bool conditionA = executeStepA();
// ... possibly edit conditionA, or just ok &= executeStepA();
ok &= conditionA;

if (ok) {
    bool conditionB = executeStepB();
    // ... possibly do more stuff
    ok &= conditionB;
}
if (ok) {
    bool conditionC = executeStepC();
    ok &= conditionC;
}
if (ok && additionalCondition) {
    // ...
}

executeThisFunctionInAnyCase();
// can now also:
return ok;

ทำไมok &= conditionX;และไม่ใช่แค่ok = conditionX;?
ABCplus

@ user3253359 ในหลายกรณีใช่คุณสามารถทำได้ นี่คือการสาธิตแนวคิด ในรหัสการทำงานเราจะพยายามทำให้มันง่ายที่สุดเท่าที่จะเป็นไปได้
blgt

+1 หนึ่งในไม่กี่คำตอบที่สะอาดและบำรุงรักษาได้ซึ่งทำงานในcตามที่ระบุไว้ในคำถาม
ClickRick

6

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

struct function_failed {};
void attempt(bool retval)
{
  if (!retval)
    throw function_failed(); // or a more specific exception class
}

จากนั้นโค้ดของคุณสามารถอ่านได้ดังนี้:

try
{
  attempt(executeStepA());
  attempt(executeStepB());
  attempt(executeStepC());
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();

หากคุณอยู่ในรูปแบบแฟนซีคุณสามารถทำให้มันทำงานผ่านการส่งแบบชัดเจน:

struct function_failed {};
struct attempt
{
  attempt(bool retval)
  {
    if (!retval)
      throw function_failed();
  }
};

จากนั้นคุณสามารถเขียนรหัสของคุณเป็น

try
{
  (attempt) executeStepA();
  (attempt) executeStepB();
  (attempt) executeStepC();
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();

การปรับค่าใหม่การตรวจสอบเป็นข้อยกเว้นไม่ได้เป็นวิธีที่ดีในการไปมีข้อยกเว้นคลี่คลายค่าใช้จ่ายมาก
John Wu

4
-1 การใช้ข้อยกเว้นสำหรับการไหลปกติใน C ++ เช่นนี้เป็นการฝึกการเขียนโปรแกรมที่ไม่ดี ในข้อยกเว้น C ++ ควรสำรองไว้สำหรับกรณีพิเศษ
Jack Aidley

1
จากข้อความคำถาม (เน้นที่ฉัน): "ฟังก์ชั่น executeStepX ควรจะถูกดำเนินการถ้าหากสำเร็จก่อนหน้านี้" ในคำอื่น ๆ ค่าส่งคืนจะถูกใช้เพื่อระบุความล้มเหลว นั่นคือการจัดการข้อผิดพลาด (และเราหวังว่าความล้มเหลวจะได้รับการยกเว้น) จัดการข้อผิดพลาดเป็นว่าสิ่งที่ถูกคิดค้นข้อยกเว้นสำหรับ
celtschk

1
Nope ประการแรกข้อยกเว้นที่ถูกสร้างขึ้นเพื่อให้การขยายพันธุ์ข้อผิดพลาดที่ไม่ได้จัดการข้อผิดพลาด ; ประการที่สอง "ฟังก์ชั่น executeStepX ควรได้รับการดำเนินการถ้าหากสำเร็จก่อนหน้านี้" ไม่ได้หมายความว่าบูลีนเท็จที่ส่งคืนโดยฟังก์ชันก่อนหน้านี้บ่งชี้ว่าเป็นกรณีพิเศษ / ผิดพลาดอย่างเห็นได้ชัด คำสั่งของคุณจึงไม่ใช่ sequitur การจัดการข้อผิดพลาดและการฆ่าเชื้อด้วยการไหลสามารถนำไปใช้ได้หลายวิธียกเว้นเป็นเครื่องมือที่ช่วยให้เกิดข้อผิดพลาดและการจัดการข้อผิดพลาดนอกสถานที่

6

หากรหัสของคุณนั้นง่ายเหมือนตัวอย่างของคุณและภาษาของคุณรองรับการประเมินการลัดวงจรคุณสามารถลองทำสิ่งนี้:

StepA() && StepB() && StepC() && StepD();
DoAlways();

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


ในความเป็นจริงฉันแก้ไขคำถามของฉันเพื่ออธิบายหัวข้อให้ดีขึ้น แต่ถูกปฏิเสธที่จะไม่ทำให้คำตอบส่วนใหญ่ไม่ถูกต้อง : \
ABCplus

ฉันเป็นผู้ใช้ใหม่ใน SO และโปรแกรมเมอร์มือใหม่ 2 คำถามแล้ว: มีความเสี่ยงหรือไม่ที่คำถามอื่นเช่นคำถามที่คุณพูดจะถูกทำเครื่องหมายว่าซ้ำเนื่องจากคำถามนี้หรือไม่ อีกประเด็นคือ: ผู้ใช้งาน / โปรแกรมเมอร์มือใหม่สามารถเลือกคำตอบที่ดีที่สุดระหว่างที่อยู่ที่นั่นได้อย่างไร (เกือบจะดีฉันคิดว่า .. )
ABCplus

6

สำหรับ C ++ 11 หรือใหม่กว่าแนวทางที่ดีอาจใช้ระบบออกจากขอบเขตคล้ายกับกลไกขอบเขต (ออก) ของ D

วิธีหนึ่งที่เป็นไปได้ในการนำไปใช้คือใช้ lambdas C ++ 11 และมาโครตัวช่วยบางตัว:

template<typename F> struct ScopeExit 
{
    ScopeExit(F f) : fn(f) { }
    ~ScopeExit() 
    { 
         fn();
    }

    F fn;
};

template<typename F> ScopeExit<F> MakeScopeExit(F f) { return ScopeExit<F>(f); };

#define STR_APPEND2_HELPER(x, y) x##y
#define STR_APPEND2(x, y) STR_APPEND2_HELPER(x, y)

#define SCOPE_EXIT(code)\
    auto STR_APPEND2(scope_exit_, __LINE__) = MakeScopeExit([&](){ code })

สิ่งนี้จะช่วยให้คุณกลับมาก่อนจากฟังก์ชันและให้แน่ใจว่าโค้ดการล้างข้อมูลใด ๆ

SCOPE_EXIT(
    delete pointerA;
    delete pointerB;
    close(fileC); );

if (!executeStepA())
    return;

if (!executeStepB())
    return;

if (!executeStepC())
    return;

มาโครเป็นของตกแต่งจริงๆ MakeScopeExit()สามารถนำมาใช้โดยตรง


ไม่จำเป็นต้องมีมาโครในการทำงานนี้ และ[=]มักเป็นความผิดของแลมบ์ดาที่ถูกกำหนดขอบเขต
Yakk - Adam Nevraumont

ใช่มาโครเป็นเพียงการตกแต่งและสามารถโยนทิ้งไปได้ แต่คุณจะไม่พูดว่าการยึดตามมูลค่าเป็นวิธีที่ปลอดภัยที่สุดหรือไม่?
glampert

1
ไม่: ถ้าแลมบ์ดาของคุณจะไม่คงอยู่นอกเหนือขอบเขตปัจจุบันที่สร้างแลมบ์ดาใช้[&]: มันปลอดภัยและน่าประหลาดใจเล็กน้อย จับตามมูลค่าเมื่อแลมบ์ดา (หรือสำเนา) สามารถอยู่รอดได้นานกว่าขอบเขตของจุดที่ประกาศ ...
Yakk - Adam Nevraumont

ใช่มันสมเหตุสมผลแล้ว ฉันจะเปลี่ยนมัน ขอบคุณ!
glampert

6

ทำไมไม่มีใครให้ทางออกที่ง่ายที่สุด? : D

หากฟังก์ชั่นทั้งหมดของคุณมีลายเซ็นเหมือนกันคุณสามารถทำได้ด้วยวิธีนี้ (สำหรับภาษา C):

bool (*step[])() = {
    &executeStepA,
    &executeStepB,
    &executeStepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    bool condition = step[i]();

    if (!condition) {
        break;
    }
}

executeThisFunctionInAnyCase();

สำหรับวิธีทำความสะอาด C ++ คุณควรสร้างคลาสอินเตอร์เฟสที่มี วิธีการดำเนินการและล้อมขั้นตอนของคุณในวัตถุ
จากนั้นวิธีแก้ปัญหาด้านบนจะมีลักษณะดังนี้:

Step *steps[] = {
    stepA,
    stepB,
    stepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    Step *step = steps[i];

    if (!step->execute()) {
        break;
    }
}

executeThisFunctionInAnyCase();

5

สมมติว่าคุณไม่ต้องการตัวแปรเงื่อนไขแต่ละตัวการย้อนกลับการทดสอบและการใช้ else-falthrough เนื่องจากพา ธ "ok" จะช่วยให้คุณได้ชุดคำสั่ง if / else แนวตั้งเพิ่มเติม:

bool failed = false;

// keep going if we don't fail
if (failed = !executeStepA())      {}
else if (failed = !executeStepB()) {}
else if (failed = !executeStepC()) {}
else if (failed = !executeStepD()) {}

runThisFunctionInAnyCase();

การละเว้นตัวแปรที่ล้มเหลวทำให้โค้ดนั้นค่อนข้างคลุมเครือเกินไป IMO

การประกาศตัวแปรภายในไม่เป็นไรไม่ต้องกังวลกับ = vs ==

// keep going if we don't fail
if (bool failA = !executeStepA())      {}
else if (bool failB = !executeStepB()) {}
else if (bool failC = !executeStepC()) {}
else if (bool failD = !executeStepD()) {}
else {
     // success !
}

runThisFunctionInAnyCase();

นี่เป็นสิ่งที่คลุมเครือ แต่มีขนาดกะทัดรัด:

// keep going if we don't fail
if (!executeStepA())      {}
else if (!executeStepB()) {}
else if (!executeStepC()) {}
else if (!executeStepD()) {}
else { /* success */ }

runThisFunctionInAnyCase();

5

ดูเหมือนว่าเครื่องรัฐซึ่งมีประโยชน์เพราะคุณสามารถนำไปใช้กับรูปแบบสถานะได้อย่างง่ายดายรัฐรูปแบบ

ใน Java มันจะมีลักษณะเช่นนี้:

interface StepState{
public StepState performStep();
}

การใช้งานจะทำงานดังนี้:

class StepA implements StepState{ 
    public StepState performStep()
     {
         performAction();
         if(condition) return new StepB()
         else return null;
     }
}

และอื่น ๆ จากนั้นคุณสามารถเปลี่ยนขนาดใหญ่หากมีเงื่อนไขด้วย:

Step toDo = new StepA();
while(toDo != null)
      toDo = toDo.performStep();
executeThisFunctionInAnyCase();

5

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

แผนภาพคลาส yUML

นี่คือตัวอย่างโปรแกรมLinqPad C #:

void Main()
{
    IOperation step = new StepC();
    step = new StepB(step);
    step = new StepA(step);
    step.Next();
}

public interface IOperation 
{
    bool Next();
}

public class StepA : IOperation
{
    private IOperation _chain;
    public StepA(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step A success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepB : IOperation
{
    private IOperation _chain;
    public StepB(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {   
        bool localResult = false;

        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to false, 
            // to show breaking out of the chain
        localResult = false;
        Console.WriteLine("Step B success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepC : IOperation
{
    private IOperation _chain;
    public StepC(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step C success={0}", localResult);
        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

หนังสือที่ดีที่สุดที่จะอ่านบนรูปแบบการออกแบบ IMHO เป็นรูปแบบการออกแบบหัวแรก


อะไรคือประโยชน์ของสิ่งนี้เช่นคำตอบของ Jefffrey
Dason

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

4

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

รูปแบบทั่วไปคือการใช้ do { } while (false);

ฉันใช้มาโครสำหรับการwhile(false)สร้างdo { } once;รูปแบบทั่วไปคือ:

do
{
    bool conditionA = executeStepA();
    if (! conditionA) break;
    bool conditionB = executeStepB();
    if (! conditionB) break;
    // etc.
} while (false);

รูปแบบนี้ค่อนข้างง่ายต่อการอ่านและอนุญาตให้ใช้วัตถุที่จะทำลายอย่างถูกต้องและหลีกเลี่ยงการส่งคืนหลายครั้งทำให้การก้าวและการดีบั๊กง่ายขึ้น


4

เพื่อปรับปรุงคำตอบ C ++ 11 ของ Mathieu และหลีกเลี่ยงค่าใช้จ่ายรันไทม์ที่เกิดขึ้นจากการใช้งานstd::functionฉันขอแนะนำให้ใช้สิ่งต่อไปนี้

template<typename functor>
class deferred final
{
public:
    template<typename functor2>
    explicit deferred(functor2&& f) : f(std::forward<functor2>(f)) {}
    ~deferred() { this->f(); }

private:
    functor f;
};

template<typename functor>
auto defer(functor&& f) -> deferred<typename std::decay<functor>::type>
{
    return deferred<typename std::decay<functor>::type>(std::forward<functor>(f));
}

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

ตัวอย่างการใช้งาน:

auto guard = defer(executeThisFunctionInAnyCase);
bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

เช่นเดียวกับคำตอบของ Mathieu การแก้ปัญหานี้ก็ปลอดภัยอย่างสมบูรณ์และexecuteThisFunctionInAnyCaseจะถูกเรียกในทุกกรณี ควรexecuteThisFunctionInAnyCaseโยนทิ้งตัวทำลายล้างจะถูกทำเครื่องหมายโดยปริยายnoexceptและดังนั้นการโทรออกstd::terminateจะถูกส่งออกไปแทนที่จะทำให้เกิดข้อยกเว้นในระหว่างการคลี่คลายสแตก


+1 ฉันกำลังมองหาคำตอบนี้ดังนั้นฉันไม่ต้องโพสต์ คุณควรไปข้างหน้าอย่างสมบูรณ์แบบfunctorในdeferred'นวกรรมิก' ไม่จำเป็นต้องบังคับกmove.
Yakk - Adam Nevraumont

@Yakk เปลี่ยน Constructor เป็น Constructor ที่ส่งต่อ
Joe

3

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

ฉันขับไล่โดยส่วนตัวgotoแม้กระทั่งสำหรับฟังก์ชั่นออก พวกเขาจะสังเกตเห็นได้ยากขึ้นเมื่อทำการดีบั๊ก

ทางเลือกที่สวยงามที่ควรใช้กับเวิร์กโฟลว์ของคุณคือการสร้างอาร์เรย์ฟังก์ชันและวนซ้ำในอันนี้

const int STEP_ARRAY_COUNT = 3;
bool (*stepsArray[])() = {
   executeStepA, executeStepB, executeStepC
};

for (int i=0; i<STEP_ARRAY_COUNT; ++i) {
    if (!stepsArray[i]()) {
        break;
    }
}

executeThisFunctionInAnyCase();

โชคดีที่ผู้ดีบั๊กพบพวกเขา หากคุณกำลังดีบั๊กและไม่ใช่การก้าวผ่านรหัสเดียวแสดงว่าคุณทำผิด
Cody Gray

ฉันไม่เข้าใจสิ่งที่คุณหมายถึงไม่ทำไมฉันไม่สามารถใช้ก้าวเดียว?
AxFab

3

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

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

class CondA
{
public:
    CondA() { 
        if (!executeStepA()) 
            throw int(1);
        [Initialize data]
    }
    ~CondA() {        
        [Clean data]
    }
    A* _a;
};

class CondB : public CondA
{
public:
    CondB() { 
        if (!executeStepB()) 
            throw int(2);
        [Initialize data]
    }
    ~CondB() {        
        [Clean data]
    }
    B* _b;
};

class CondC : public CondB
{
public:
    CondC() { 
        if (!executeStepC()) 
            throw int(3);
        [Initialize data]
    }
    ~CondC() {        
        [Clean data]
    }
    C* _c;
};

และจากนั้นในรหัสของคุณคุณเพียงแค่ต้องโทร:

shared_ptr<CondC> C(nullptr);
try{
    C = make_shared<CondC>();
}
catch(int& e)
{
    //do something
}
if (C != nullptr)
{
   C->a;//work with
   C->b;//work with
   C->c;//work with
}
executeThisFunctionInAnyCase();

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


3

วิธีที่น่าสนใจคือการทำงานกับข้อยกเว้น

try
{
    executeStepA();//function throws an exception on error
    ......
}
catch(...)
{
    //some error handling
}
finally
{
    executeThisFunctionInAnyCase();
}

หากคุณเขียนรหัสดังกล่าวคุณจะไปในทิศทางที่ผิด ฉันไม่เห็นว่ามันเป็น "ปัญหา" ที่จะมีรหัสดังกล่าว แต่มีสถาปัตยกรรม "ยุ่ง"

เคล็ดลับ: หารือเกี่ยวกับกรณีเหล่านี้กับนักพัฒนาที่มีประสบการณ์ซึ่งคุณเชื่อถือ ;-)


ฉันคิดว่าความคิดนี้ไม่สามารถแทนที่ if-chain ทั้งหมดได้ อย่างไรก็ตามในหลาย ๆ กรณีนี่เป็นวิธีการที่ดีมาก!
WoIIe

3

วิธีการอื่น - do - whileวนซ้ำแม้ว่ามันจะถูกกล่าวถึงก่อนหน้านี้ไม่มีตัวอย่างของมันซึ่งจะแสดงให้เห็นว่ามันดูเหมือน:

do
{
    if (!executeStepA()) break;
    if (!executeStepB()) break;
    if (!executeStepC()) break;
    ...

    break; // skip the do-while condition :)
}
while (0);

executeThisFunctionInAnyCase();

(มีคำตอบที่whileวนซ้ำอยู่แล้วแต่do - whileวนซ้ำไม่ได้ตรวจสอบซ้ำซ้อน (ตอนเริ่มต้น) แต่แทนที่ท้าย xD (ซึ่งสามารถข้ามได้)


สวัสดี Zaffy - คำตอบนี้มีคำอธิบายอย่างมากเกี่ยวกับวิธีทำ {} ในขณะที่ (เป็นเท็จ) วิธีการ stackoverflow.com/a/24588605/294884 อีกสองคำตอบยังกล่าวถึงมัน
Fattie

มันเป็นคำถามที่น่าสนใจไม่ว่าจะใช้ CONTINUE หรือ BREAK ในสถานการณ์นี้ดีกว่านี้หรือไม่!
Fattie

เฮ้ @JoeBlow ผมเห็นคำตอบทั้งหมด ... เพียงแค่อยากจะแสดงแทนของการพูดคุยเกี่ยวกับมัน :)
Zaffy

คำตอบแรกของฉันที่นี่ผมบอกว่า "ไม่มีใครได้กล่าวถึงนี้ ..." และทันทีที่มีคนชี้กรุณาออกมามันเป็นคำตอบด้านบน 2 :)
Fattie

@ JoeBlow เอ๊ะคุณพูดถูก ฉันจะพยายามแก้ไข ฉันรู้สึกเหมือน ... xD ขอบคุณแล้วครั้งต่อไปฉันจะจ่าย attetion น้อยมาก :)
Zaffy
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.