Làm thế nào để tránh các chuỗi nếu các chuỗi?


266

Giả sử tôi có mã giả này:

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

executeThisFunctionInAnyCase();

Các hàm executeStepXnên được thực thi khi và chỉ khi thành công trước đó. Trong mọi trường hợp, executeThisFunctionInAnyCasehàm nên được gọi ở cuối. Tôi là người mới lập trình, rất xin lỗi vì câu hỏi rất cơ bản: có cách nào (ví dụ trong C / C ++) để tránh ifchuỗi dài đó tạo ra loại "kim tự tháp mã" đó, với chi phí dễ đọc của mã ?

Tôi biết rằng nếu chúng ta có thể bỏ qua lệnh executeThisFunctionInAnyCasegọi hàm, mã có thể được đơn giản hóa như sau:

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

Nhưng ràng buộc là executeThisFunctionInAnyCasechức năng gọi. Này có thể breaktuyên bố được sử dụng một cách nào đó?


254
@ Frédéric Hamidi sai sai! Đừng bao giờ nói rằng điều khiển chương trình của bạn với các ngoại lệ là tốt! Các ngoại lệ chắc chắn KHÔNG phù hợp cho mục đích này, vì quá nhiều lý do.
Piotr Zierhoffer

26
@Piotr, tôi đã bị Python làm hỏng (điều này thực sự khuyến khích điều này). Tôi biết các ngoại lệ không được sử dụng để kiểm soát luồng trong C ++, nhưng nó có thực sự kiểm soát luồng ở đây không? Không thể một chức năng trở lại falseđược coi là giống như một tình huống đặc biệt?
Frédéric Hamidi

13
Điều đó phụ thuộc vào ngữ nghĩa của một chương trình. Một sự falsetrở lại có thể là khá bình thường.
dornhege

28
Tôi đã quay lại câu hỏi của bạn để sửa đổi lần đầu tiên. Bạn không nên thay đổi hoàn toàn câu hỏi của mình sau khi bạn đã nhận được một số câu hỏi nhất định (> 0), vì điều đó sẽ làm mất hiệu lực tất cả các câu trả lời cho đến thời điểm đó và sẽ gây nhầm lẫn. Mở một câu hỏi mới thay thế.
Giày

13
Tôi ước tất cả các lập trình viên của Newbie, người đặt câu hỏi như thế này.
Jezen Thomas

Câu trả lời:


486

Bạn có thể sử dụng một &&(logic VÀ):

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

điều này sẽ đáp ứng cả hai yêu cầu của bạn:

  • executeStep<X>()chỉ nên đánh giá nếu lần trước thành công (điều này được gọi là đánh giá ngắn mạch )
  • executeThisFunctionInAnyCase() sẽ được thực hiện trong mọi trường hợp

29
Điều này vừa đúng về mặt ngữ nghĩa (thực sự, chúng tôi muốn TẤT CẢ các điều kiện là đúng) cũng như một kỹ thuật lập trình rất tốt (đánh giá ngắn mạch). Hơn nữa, điều này có thể được sử dụng trong bất kỳ tình huống phức tạp nào trong đó việc tạo các hàm sẽ làm rối mã.
Sanchise

58
@RobAu: Sau đó, sẽ tốt cho các lập trình viên cơ sở cuối cùng thấy mã dựa trên đánh giá cắt ngắn và có thể khiến họ nghiên cứu chủ đề này, điều này sẽ giúp họ trên con đường trở thành lập trình viên cao cấp. Vì vậy, rõ ràng một chiến thắng-thắng: mã tốt và ai đó đã học được điều gì đó từ việc đọc nó.
x4u

24
Đây phải là câu trả lời hàng đầu
Tên hiển thị

61
@RobAu: Đây không phải là một vụ hack lợi dụng một số thủ thuật cú pháp tối nghĩa, nó rất thành ngữ trong hầu hết mọi ngôn ngữ lập trình, đến mức không thể thực hiện được tiêu chuẩn.
BlueRaja - Daniel Pflughoeft

38
Giải pháp này chỉ áp dụng nếu các điều kiện thực sự là các lệnh gọi hàm đơn giản. Trong mã thực, các điều kiện đó có thể dài từ 2 - 5 dòng (và bản thân chúng là sự kết hợp của nhiều thứ khác &&||) vì vậy không có cách nào bạn muốn nối chúng thành một ifcâu lệnh mà không vặn vít dễ đọc. Và không phải lúc nào cũng dễ dàng chuyển các điều kiện đó sang các hàm bên ngoài, bởi vì chúng có thể phụ thuộc vào rất nhiều biến cục bộ được tính toán trước đó, điều này sẽ tạo ra một mớ hỗn độn khủng khiếp nếu bạn cố gắng chuyển từng đối số thành một đối số riêng lẻ.
hamstergene

358

Chỉ cần sử dụng một chức năng bổ sung để phiên bản thứ hai của bạn hoạt động:

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

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

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

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

Sử dụng ifs lồng nhau sâu (biến thể đầu tiên của bạn) hoặc mong muốn thoát ra khỏi "một phần của chức năng" thường có nghĩa là bạn cần một chức năng bổ sung.


51
+1 cuối cùng cũng có người đăng câu trả lời lành mạnh. Đây là cách chính xác nhất, an toàn và dễ đọc, theo ý kiến ​​của tôi.
Lundin

31
+1 Đây là một minh họa tốt về "Nguyên tắc trách nhiệm duy nhất". Chức năng foohoạt động theo cách của nó thông qua một chuỗi các điều kiện và hành động liên quan. Chức năng barđược phân tách rõ ràng từ các quyết định. Nếu chúng ta thấy chi tiết về các điều kiện và hành động, có thể hóa ra foovẫn còn làm quá nhiều, nhưng bây giờ đây là một giải pháp tốt.
GranitRobert

13
Nhược điểm là C không có các hàm lồng nhau, vì vậy nếu 3 bước cần thiết để sử dụng các biến từ barbạn sẽ phải chuyển chúng thành foocác tham số theo cách thủ công . Nếu đó là trường hợp và nếu foochỉ được gọi một lần, tôi sẽ sử dụng phiên bản goto để tránh xác định hai chức năng kết hợp chặt chẽ mà cuối cùng sẽ không thể tái sử dụng được.
hugomg

7
Không chắc chắn về cú pháp ngắn mạch cho C, nhưng trong C # foo () có thể được viết làif (!executeStepA() || !executeStepB() || !executeStepC()) return
Travis

6
@ user1598390 Goto được sử dụng mọi lúc, đặc biệt là trong lập trình hệ thống khi bạn cần thư giãn nhiều mã thiết lập.
Scotty Bauer

166

Lập trình viên trường C cũ sử dụng gototrong trường hợp này. Đây là một cách sử dụng gotothực sự được khuyến khích bởi Linux styleguide, nó được gọi là lối thoát chức năng tập trung:

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;
}

Một số người làm việc xung quanh bằng gotocách quấn cơ thể vào một vòng lặp và phá vỡ nó, nhưng hiệu quả cả hai cách tiếp cận đều làm điều tương tự. Cách gototiếp cận sẽ tốt hơn nếu bạn cần một số dọn dẹp khác chỉ khi executeStepA()thành công:

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;
}

Với cách tiếp cận vòng lặp, bạn sẽ kết thúc với hai cấp độ vòng lặp trong trường hợp đó.


108
+1. Rất nhiều người nhìn thấy gotovà ngay lập tức nghĩ rằng "Đây là mã khủng khiếp" nhưng nó có những công dụng hợp lệ của nó.
Graeme Perrow

46
Ngoại trừ điều này thực sự là mã khá lộn xộn, từ quan điểm bảo trì. Đặc biệt khi có nhiều nhãn, nơi mã cũng khó đọc hơn. Có nhiều cách thanh lịch hơn để đạt được điều này: sử dụng các chức năng.
Lundin

29
-1 Tôi thấy gotovà tôi thậm chí không phải nghĩ rằng đây là mã khủng khiếp. Tôi đã từng phải duy trì như vậy, nó rất khó chịu. OP gợi ý một sự thay thế ngôn ngữ C hợp lý ở cuối câu hỏi và tôi đã đưa nó vào câu trả lời của mình.
Chúc mừng và hth. - Alf

56
Không có gì sai với việc sử dụng goto giới hạn, khép kín này. Nhưng hãy cẩn thận, goto một loại thuốc cửa ngõ và nếu bạn không cẩn thận, một ngày nào đó bạn sẽ nhận ra rằng không ai khác ăn mì spaghetti bằng chân của họ, và bạn đã làm nó trong 15 năm bắt đầu sau khi bạn nghĩ "Tôi chỉ có thể sửa nó nếu không thì lỗi ác mộng với một nhãn ... "
Tim Post

66
Tôi đã viết có lẽ một trăm ngàn dòng mã cực kỳ rõ ràng, dễ bảo trì theo phong cách này. Hai điều quan trọng để hiểu là (1) kỷ luật! thiết lập một hướng dẫn rõ ràng cho cách bố trí của mọi chức năng và chỉ vi phạm nó khi rất cần và (2) hiểu rằng những gì chúng ta đang làm ở đây là mô phỏng các ngoại lệ trong một ngôn ngữ không có chúng . throwvề nhiều mặt còn tệ hơn gotobởi vì thrownó thậm chí còn không rõ ràng từ bối cảnh địa phương nơi bạn sẽ kết thúc! Sử dụng các cân nhắc thiết kế tương tự cho luồng điều khiển kiểu goto như bạn muốn cho các ngoại lệ.
Eric Lippert

131

Đây là một tình huống phổ biến và có nhiều cách phổ biến để đối phó với nó. Đây là nỗ lực của tôi tại một câu trả lời kinh điển. Hãy bình luận nếu tôi bỏ lỡ bất cứ điều gì và tôi sẽ cập nhật bài viết này.

Đây là một mũi tên

Những gì bạn đang thảo luận được gọi là mô hình chống mũi tên . Nó được gọi là một mũi tên vì chuỗi ifs lồng nhau tạo thành các khối mã mở rộng ra xa hơn và sang phải rồi quay lại bên trái, tạo thành một mũi tên trực quan "chỉ" sang bên phải của khung soạn thảo mã.

Làm phẳng mũi tên với người bảo vệ

Một số cách phổ biến để tránh Mũi tên được thảo luận ở đây . Phương thức phổ biến nhất là sử dụng mẫu bảo vệ , trong đó mã xử lý các luồng ngoại lệ trước rồi xử lý luồng cơ bản, ví dụ thay vì

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

... bạn sẽ sử dụng ....

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

Khi có một loạt các vệ sĩ, điều này sẽ làm phẳng mã đáng kể vì tất cả các vệ sĩ đều xuất hiện ở bên trái và if của bạn không được lồng vào nhau. Ngoài ra, bạn đang ghép nối trực quan điều kiện logic với lỗi liên quan của nó, điều này giúp dễ dàng hơn nhiều để biết điều gì đang xảy ra:

Mũi tên:

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;
}

Bảo vệ:

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;
} 

Điều này là khách quan và dễ định lượng hơn để đọc bởi vì

  1. Các ký tự {và} cho một khối logic đã cho gần nhau hơn
  2. Số lượng bối cảnh tinh thần cần thiết để hiểu một dòng cụ thể là nhỏ hơn
  3. Toàn bộ logic liên quan đến một điều kiện if có nhiều khả năng nằm trên một trang
  4. Nhu cầu lập trình viên để cuộn trang / mắt theo dõi được giảm đi rất nhiều

Cách thêm mã chung vào cuối

Vấn đề với mô hình bảo vệ là nó dựa vào cái được gọi là "lợi nhuận cơ hội" hay "lối thoát cơ hội". Nói cách khác, nó phá vỡ mô hình rằng mỗi và mọi chức năng nên có chính xác một điểm thoát. Đây là một vấn đề vì hai lý do:

  1. Nó chà xát một số người sai cách, ví dụ những người đã học viết mã trên Pascal đã học được rằng một hàm = một điểm thoát.
  2. Nó không cung cấp một phần mã thực thi khi thoát bất kể là gì , đó là chủ đề trong tay.

Dưới đây tôi đã cung cấp một số tùy chọn để khắc phục giới hạn này bằng cách sử dụng các tính năng ngôn ngữ hoặc bằng cách tránh hoàn toàn vấn đề.

Tùy chọn 1. Bạn không thể làm điều này: sử dụng finally

Thật không may, là một nhà phát triển c ++, bạn không thể làm điều này. Nhưng đây là câu trả lời số một cho các ngôn ngữ có chứa từ khóa cuối cùng, vì đây chính xác là những gì nó dành cho.

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

Tùy chọn 2. Tránh vấn đề: Tái cấu trúc các chức năng của bạn

Bạn có thể tránh vấn đề bằng cách chia mã thành hai chức năng. Giải pháp này có lợi ích làm việc cho bất kỳ ngôn ngữ nào, và ngoài ra, nó có thể làm giảm độ phức tạp theo chu kỳ , đây là một cách đã được chứng minh để giảm tỷ lệ lỗi của bạn và cải thiện tính đặc hiệu của bất kỳ bài kiểm tra đơn vị tự động nào.

Đây là một ví dụ:

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

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

Tùy chọn 3. Thủ thuật ngôn ngữ: Sử dụng vòng lặp giả

Một mẹo phổ biến khác mà tôi thấy là sử dụng while (true) và break, như thể hiện trong các câu trả lời khác.

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

Mặc dù điều này ít "trung thực" hơn so với sử dụng goto, nhưng nó sẽ ít bị rối hơn khi tái cấu trúc, vì nó đánh dấu rõ ràng ranh giới của phạm vi logic. Một lập trình viên ngây thơ cắt và dán nhãn của bạn hoặc gototuyên bố của bạn có thể gây ra vấn đề lớn! (Và thẳng thắn, mô hình rất phổ biến bây giờ tôi nghĩ rằng nó truyền đạt rõ ràng ý định, và do đó không "không trung thực" chút nào).

Có các biến thể khác của tùy chọn này. Ví dụ, người ta có thể sử dụng switchthay vì while. Bất kỳ ngôn ngữ xây dựng với một breaktừ khóa có thể sẽ làm việc.

Tùy chọn 4. Tận dụng vòng đời của đối tượng

Một cách tiếp cận khác thúc đẩy vòng đời đối tượng. Sử dụng một đối tượng ngữ cảnh để mang theo các tham số của bạn (thứ mà ví dụ ngây thơ của chúng tôi thiếu một cách đáng ngờ) và loại bỏ nó khi bạn hoàn thành.

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
}

Lưu ý: Hãy chắc chắn rằng bạn hiểu vòng đời đối tượng của ngôn ngữ bạn chọn. Bạn cần một số loại bộ sưu tập rác xác định để làm việc này, tức là bạn phải biết khi nào hàm hủy sẽ được gọi. Trong một số ngôn ngữ, bạn sẽ cần sử dụng Disposethay vì hàm hủy.

Tùy chọn 4.1. Tận dụng vòng đời đối tượng (mẫu bao bọc)

Nếu bạn sẽ sử dụng một cách tiếp cận hướng đối tượng, cũng có thể làm điều đó đúng. Tùy chọn này sử dụng một lớp để "bọc" các tài nguyên cần dọn dẹp, cũng như các hoạt động khác của nó.

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

Một lần nữa, hãy chắc chắn bạn hiểu vòng đời đối tượng của bạn.

Tùy chọn 5. Thủ thuật ngôn ngữ: Sử dụng đánh giá ngắn mạch

Một kỹ thuật khác là tận dụng đánh giá ngắn mạch .

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

Giải pháp này tận dụng cách thức hoạt động của toán tử &&. Khi phía bên trái của && ước tính là sai, phía bên phải không bao giờ được đánh giá.

Thủ thuật này hữu ích nhất khi cần có mã compact và khi mã không có khả năng bảo trì nhiều, ví dụ: bạn đang thực hiện một thuật toán nổi tiếng. Đối với mã hóa tổng quát hơn, cấu trúc của mã này quá dễ vỡ; ngay cả một thay đổi nhỏ đối với logic cũng có thể kích hoạt việc viết lại hoàn toàn.


12
Cuối cùng? C ++ không có mệnh đề cuối cùng. Một đối tượng có hàm hủy được sử dụng trong các tình huống mà bạn nghĩ rằng bạn cần một mệnh đề cuối cùng.
Cửa Bill

1
Chỉnh sửa để phù hợp với hai ý kiến ​​trên.
John Wu

2
Với mã tầm thường (ví dụ trong ví dụ của tôi) các mẫu lồng nhau có lẽ dễ hiểu hơn. Với mã thế giới thực (có thể kéo dài vài trang), mẫu bảo vệ dễ đọc hơn vì nó sẽ yêu cầu ít cuộn hơn và ít theo dõi mắt hơn, ví dụ khoảng cách trung bình từ {đến} ngắn hơn.
John Wu

1
Tôi đã thấy mẫu lồng nhau trong đó mã không hiển thị trên màn hình 1920 x 1080 nữa ... Hãy thử tìm hiểu mã xử lý lỗi nào sẽ được thực hiện nếu hành động thứ ba không thành công ... Tôi đã sử dụng làm {...} trong khi (0) thay vào đó, do đó bạn không cần nghỉ cuối cùng (mặt khác, trong khi (đúng) {...} cho phép "tiếp tục" bắt đầu lại từ đầu.
gnasher729

2
Tùy chọn 4 của bạn thực sự là rò rỉ bộ nhớ trong C ++ (bỏ qua lỗi cú pháp nhỏ). Người ta không sử dụng "mới" trong trường hợp này, chỉ cần nói "MyContext myContext;".
Sumudu Fernando

60

Cứ làm đi

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

Nó đơn giản mà.


Do ba lần chỉnh sửa mà mỗi lần sửa đổi về cơ bản câu hỏi (bốn nếu một lần đếm lại bản sửa đổi thành phiên bản # 1), tôi bao gồm ví dụ mã tôi đang trả lời:

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

executeThisFunctionInAnyCase();

14
Tôi đã trả lời câu hỏi này (chính xác hơn) để trả lời phiên bản đầu tiên của câu hỏi và nó đã nhận được 20 lượt upvote trước khi nó bị Bill the Lizard xóa, sau một số bình luận và chỉnh sửa của Darkness Races trong Orbit.
Chúc mừng và hth. - Alf

1
@ Cheersandhth-Alf: Tôi không thể tin rằng nó đã bị xóa mod. Điều đó thật tệ. (+1)
Croissant Paramag từ

2
+1 của tôi để can đảm của bạn! (3 lần không thành vấn đề: D).
haccks

Điều hoàn toàn quan trọng là các lập trình viên mới học về những thứ như thực thi đơn hàng với nhiều "ands" boolean, cách khác nhau trong các ngôn ngữ khác nhau, v.v. Và câu trả lời này là một con trỏ tuyệt vời cho điều đó. Nhưng nó chỉ là "không khởi đầu" trong lập trình thương mại thông thường. Nó không đơn giản, nó không thể bảo trì, nó không thể sửa đổi. Chắc chắn, nếu bạn chỉ đang viết một số "mã cao bồi" nhanh chóng cho chính mình, hãy làm điều này. Nhưng nó chỉ có loại "không có kết nối với công nghệ phần mềm hàng ngày như được thực hiện ngày nay." BTW xin lỗi vì "sự nhầm lẫn chỉnh sửa" lố bịch mà bạn phải chịu đựng ở đây, @cheers :)
Fattie

1
@JoeBlow: Tôi với Alf về điều này - Tôi thấy &&danh sách rõ ràng hơn nhiều. Tôi thường phá vỡ các điều kiện để phân tách các dòng và thêm một dấu vết // explanationcho mỗi dòng .... Cuối cùng, nó sẽ ít mã hơn để xem xét, và một khi bạn hiểu cách thức &&hoạt động thì không có nỗ lực tinh thần nào đang diễn ra. Ấn tượng của tôi là hầu hết các lập trình viên C ++ chuyên nghiệp sẽ quen với điều này, nhưng như bạn nói trong các ngành / dự án khác nhau, trọng tâm và kinh nghiệm khác nhau.
Tony Delroy

35

Thực sự có một cách để trì hoãn các hành động trong C ++: sử dụng hàm hủy của đối tượng.

Giả sử rằng bạn có quyền truy cập vào 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

Và sau đó sử dụng tiện ích đó:

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
Đây chỉ là hoàn toàn obfuscation với tôi. Tôi thực sự không hiểu tại sao rất nhiều lập trình viên C ++ thích giải quyết vấn đề bằng cách ném càng nhiều tính năng ngôn ngữ vào nó càng tốt, cho đến khi mọi người quên mất vấn đề bạn thực sự đang giải quyết: họ không còn quan tâm nữa, vì họ không còn quan tâm nữa bây giờ rất quan tâm đến việc sử dụng tất cả các tính năng ngôn ngữ kỳ lạ. Và từ đó, bạn có thể giữ cho mình bận rộn trong nhiều ngày và tuần để viết mã meta, sau đó duy trì mã meta, sau đó viết mã meta để xử lý mã meta.
Lundin

24
@Lundin: Chà, tôi không hiểu làm thế nào người ta có thể hài lòng với mã dễ vỡ sẽ bị hỏng ngay khi tiếp tục / phá vỡ / trả lại sớm được đưa ra hoặc một ngoại lệ được đưa ra. Mặt khác, giải pháp này có khả năng phục hồi khi đối mặt với việc bảo trì trong tương lai và chỉ dựa vào thực tế là các bộ hủy được thực thi trong quá trình tháo gỡ vốn là một trong những tính năng quan trọng nhất của C ++. Đủ điều kiện này là kỳ lạ trong khi đó là nguyên tắc cơ bản mà sức mạnh của tất cả các container Tiêu chuẩn là gây cười, để nói rằng ít nhất.
Matthieu M.

7
@Lundin: Lợi ích của mã của Matthieu là executeThisFunctionInAnyCase();sẽ thực thi ngay cả khi foo();ném ngoại lệ. Khi viết mã an toàn ngoại lệ, cách tốt nhất là đặt tất cả các hàm dọn dẹp như vậy vào một hàm hủy.
Brian

6
@Brian Sau đó, đừng ném bất kỳ ngoại lệ nào foo(). Và nếu bạn làm, bắt nó. Vấn đề được giải quyết. Sửa lỗi bằng cách sửa chúng, không phải bằng cách viết các công việc.
Lundin

18
@Lundin: Deferlớp là một đoạn mã nhỏ có thể tái sử dụng , cho phép bạn thực hiện bất kỳ việc dọn dẹp cuối khối nào, theo cách an toàn ngoại lệ. nó thường được gọi là bảo vệ phạm vi . có, bất kỳ việc sử dụng bộ bảo vệ phạm vi nào cũng có thể được thể hiện theo các cách thủ công khác, giống như bất kỳ forvòng lặp nào có thể được biểu thị dưới dạng khối và whilevòng lặp, có thể được biểu thị bằng ifgoto, có thể được thể hiện bằng ngôn ngữ lắp ráp nếu bạn muốn, hoặc đối với những người là bậc thầy thực sự, bằng cách thay đổi các bit trong bộ nhớ thông qua các tia vũ trụ được định hướng bởi hiệu ứng cánh bướm của những tiếng càu nhàu ngắn đặc biệt. Nhưng, tại sao làm điều đó.
Chúc mừng và hth. - Alf

34

Có một kỹ thuật hay mà không cần một hàm bao bọc bổ sung với các câu lệnh return (phương thức được quy định bởi Itjax). Nó sử dụng một while(0)vòng lặp giả. Việc while (0)đảm bảo rằng nó thực sự không phải là một vòng lặp mà chỉ được thực hiện một lần. Tuy nhiên, cú pháp vòng lặp cho phép sử dụng câu lệnh break.

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

11
Theo tôi, việc sử dụng một hàm có nhiều trả về là tương tự, nhưng dễ đọc hơn.
Lundin

4
Có, chắc chắn nó dễ đọc hơn ... tuy nhiên từ quan điểm hiệu quả, có thể tránh được việc gọi hàm (truyền tham số và trả về) bổ sung với cấu trúc do {} while (0).
Khởi nghiệp

5
Bạn vẫn có thể tự do thực hiện chức năng inline. Dù sao, đây là một kỹ thuật tốt để biết, bởi vì nó giúp nhiều hơn vấn đề này.
Ruslan

2
@Lundin Bạn phải tính đến địa phương mã, việc phát tán mã ra quá nhiều nơi cũng có vấn đề.
API-Beast

3
Theo kinh nghiệm của tôi, đây là một thành ngữ rất bất thường. Tôi sẽ mất một lúc để tìm hiểu xem cái quái gì đang diễn ra và đó là một dấu hiệu xấu khi tôi đang xem lại mã. Cho rằng nó dường như không có lợi thế so với các cách tiếp cận khác, phổ biến hơn và do đó dễ đọc hơn, tôi không thể đăng xuất.
Cody Grey

19

Bạn cũng có thể làm điều này:

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();

Bằng cách này, bạn có kích thước tăng trưởng tuyến tính tối thiểu, 1 dòng trên mỗi cuộc gọi và có thể dễ dàng duy trì.


EDIT : (Cảm ơn @Unda) Không phải là một fan hâm mộ lớn vì bạn mất khả năng hiển thị 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
Đó là về chức năng tình cờ gọi bên trong push_back (), nhưng dù sao bạn cũng đã sửa nó :)
Quentin

4
Tôi muốn biết lý do tại sao điều này có một downvote. Giả sử các bước thực hiện thực sự đối xứng, vì chúng có vẻ như vậy, sau đó nó sạch sẽ và có thể bảo trì.
ClickRick

1
Mặc dù điều này có thể trông có vẻ sạch sẽ hơn. Nó có thể khó hiểu hơn đối với mọi người, và chắc chắn là khó hiểu hơn đối với trình biên dịch!
Roy T.

1
Đối xử với các chức năng của bạn nhiều hơn vì dữ liệu thường là một ý tưởng hay - một khi bạn đã thực hiện nó, bạn cũng sẽ nhận thấy các phép tái cấu trúc tiếp theo. Thậm chí tốt hơn là nơi mỗi phần bạn đang xử lý là một tham chiếu đối tượng, không chỉ là một tham chiếu hàm - điều này sẽ cung cấp cho bạn nhiều khả năng hơn để cải thiện mã của bạn xuống dòng.
Bill K

3
Hơi quá kỹ thuật cho một trường hợp tầm thường, nhưng kỹ thuật này chắc chắn có một tính năng hay mà người khác không có: Bạn có thể thay đổi thứ tự thực hiện và [số lượng] các chức năng được gọi trong thời gian chạy, và điều đó thật tuyệt :)
Xocoatzin

18

Điều này sẽ làm việc? Tôi nghĩ rằng điều này là tương đương với mã của bạn.

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

3
Tôi thường gọi biến okkhi sử dụng cùng một biến như thế này.
Macke

1
Tôi sẽ rất quan tâm đến việc tại sao các downvote. Có chuyện gì ở đây vậy?
ABCplus

1
so sánh câu trả lời của bạn với phương pháp ngắn mạch cho độ phức tạp chu kỳ.
AlphaGoku

14

Giả sử mã mong muốn là như tôi hiện thấy:

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

Tôi sẽ nói rằng cách tiếp cận chính xác, trong đó đơn giản nhất để đọc và dễ duy trì nhất, sẽ có ít mức độ thụt hơn, hiện tại (mục đích) là mục đích đã nêu của câu hỏi.

// 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();

Điều này tránh mọi nhu cầu về gotos, ngoại lệ, whilevòng lặp giả hoặc các cấu trúc khó khăn khác và chỉ đơn giản là bắt tay vào công việc đơn giản trong tay.


Khi sử dụng các vòng lặp, nó thường được chấp nhận để sử dụng returnbreaknhảy ra khỏi vòng lặp mà không cần phải đưa ra các biến "cờ" bổ sung. Trong trường hợp này, sử dụng một goto sẽ tương tự innocuou - hãy nhớ rằng bạn đang giao dịch độ phức tạp goto thêm cho độ phức tạp biến đổi thêm.
hugomg

2
@hugomg Các biến đã có trong câu hỏi ban đầu. Không có sự phức tạp thêm ở đây. Có những giả định được đưa ra về câu hỏi (ví dụ: các biến là cần thiết trong mã được tái cấu trúc) để chúng được giữ lại. Nếu chúng không cần thiết thì mã có thể được đơn giản hóa, nhưng với bản chất không đầy đủ của câu hỏi, không có giả định nào khác có thể được đưa ra một cách hợp lệ.
ClickRick

Cách tiếp cận rất hữu ích, đặc biệt. để sử dụng bởi một người tự xưng newbie, nó cung cấp một giải pháp sạch hơn không có nhược điểm. Tôi lưu ý rằng nó cũng không phụ thuộc vào việc stepscó cùng chữ ký hoặc thậm chí là các chức năng hơn là các khối. Tôi có thể thấy điều này đang được sử dụng như một công cụ tái cấu trúc vượt qua đầu tiên ngay cả khi cách tiếp cận tinh vi hơn là hợp lệ.
Keith

12

Tuyên bố phá vỡ có thể được sử dụng theo một cách nào đó?

Có thể không phải là giải pháp tốt nhất nhưng bạn có thể đặt các câu lệnh của mình trong một do .. while (0)vòng lặp và sử dụng các breakcâu lệnh thay vì return.


2
Không phải tôi đã hạ thấp nó, nhưng đây sẽ là sự lạm dụng cấu trúc vòng lặp cho một cái gì đó mà hiệu quả là những gì hiện đang muốn nhưng chắc chắn sẽ dẫn đến đau đớn. Có lẽ đối với nhà phát triển tiếp theo phải duy trì nó trong 2 năm sau khi bạn chuyển sang dự án khác.
ClickRick

3
@ClickRick sử dụng do .. while (0)cho các định nghĩa macro cũng lạm dụng các vòng lặp nhưng nó được coi là OK.
ouah

1
Có lẽ, nhưng có những cách sạch hơn để đạt được nó.
ClickRick

4
@ClickRick mục đích duy nhất của câu trả lời của tôi là trả lời Có thể sử dụng câu lệnh break theo cách nào đó và câu trả lời là có, những từ đầu tiên trong câu trả lời của tôi cho thấy đây có thể không phải là giải pháp để sử dụng.
ouah

2
Câu trả lời này chỉ nên là một nhận xét
msmucker0527

12

Bạn có thể đặt tất cả các ifđiều kiện, được định dạng như bạn muốn nó trong một hàm của riêng chúng, khi trả về thực thi executeThisFunctionInAnyCase()hàm.

Từ ví dụ cơ bản trong OP, việc kiểm tra và thực hiện điều kiện có thể được tách ra như vậy;

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

Và sau đó được gọi như vậy;

InitialSteps();
executeThisFunctionInAnyCase();

Nếu C ++ 11 lambdas có sẵn (không có thẻ C ++ 11 trong OP, nhưng chúng vẫn có thể là một tùy chọn), thì chúng ta có thể từ bỏ chức năng tách biệt và bọc nó thành lambda.

// 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

Nếu bạn không thích gotovà không thích do { } while (0);các vòng lặp và thích sử dụng C ++, bạn cũng có thể sử dụng lambda tạm thời để có tác dụng tương tự.

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

executeThisFunctionInAnyCase();

1
if bạn không thích goto && bạn không thích làm {} trong khi (0) && bạn thích C ++ ... Xin lỗi, không thể cưỡng lại, nhưng điều kiện cuối cùng không thành công vì câu hỏi được gắn thẻ c cũng như c ++
ClickRick

@ClickRick luôn khó khăn để làm hài lòng tất cả mọi người. Theo ý kiến của tôi , không có thứ gì như C / C ++, bạn thường viết mã trong một trong những cái đó và việc sử dụng cái kia bị cau mày.
Alex

9

Các chuỗi IF / ELSE trong mã của bạn không phải là vấn đề ngôn ngữ, mà là thiết kế chương trình của bạn. Nếu bạn có thể tính lại hoặc viết lại chương trình của mình, tôi muốn đề xuất rằng bạn nên tìm trong Mẫu thiết kế ( http://sourcemaking.com/design_potypes ) để tìm giải pháp tốt hơn.

Thông thường, khi bạn thấy rất nhiều IF & khác trong mã của mình, đó là cơ hội để triển khai Mẫu thiết kế chiến lược ( http://sourcemaking.com/design_potypes/strargety/c-sharp-dot-net ) hoặc có thể là sự kết hợp của các mẫu khác.

Tôi chắc chắn có những lựa chọn thay thế để viết một danh sách dài if / other, nhưng tôi nghi ngờ họ sẽ thay đổi bất cứ điều gì ngoại trừ chuỗi sẽ trông đẹp mắt đối với bạn (Tuy nhiên, vẻ đẹp trong mắt của kẻ si tình vẫn áp dụng cho mã quá:-) ) . Bạn nên quan tâm đến những điều như (trong 6 tháng khi tôi có một điều kiện mới và tôi không nhớ bất cứ điều gì về mã này, tôi có thể thêm nó dễ dàng không? Hoặc nếu chuỗi thay đổi, nhanh và không có lỗi tôi sẽ thực hiện nó)


9

Bạn chỉ cần làm điều này ..

coverConditions();
executeThisFunctionInAnyCase();

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

99 lần 100, đây là cách duy nhất để làm điều đó.

Không bao giờ, bao giờ, cố gắng làm một cái gì đó "khó khăn" trong mã máy tính.


Nhân tiện, tôi khá chắc chắn sau đây là giải pháp thực tế bạn có trong đầu ...

Các tiếp tục tuyên bố là rất quan trọng trong lập trình thuật toán. (Phần lớn, câu lệnh goto rất quan trọng trong lập trình thuật toán.)

Trong nhiều ngôn ngữ lập trình, bạn có thể làm điều này:

-(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");
    }

(Lưu ý trước hết: các khối trần như ví dụ đó là một phần quan trọng và quan trọng của việc viết mã đẹp, đặc biệt nếu bạn đang làm việc với lập trình "thuật toán".)

Một lần nữa, đó chính xác là những gì bạn có trong đầu, phải không? Và đó là cách hay để viết nó, vì vậy bạn có bản năng tốt.

Tuy nhiên, thật bi thảm, trong phiên bản hiện tại của object-c (Ngoài ra - tôi không biết về Swift, xin lỗi) có một tính năng có thể xảy ra khi nó kiểm tra xem khối kèm theo có phải là một vòng lặp hay không.

nhập mô tả hình ảnh ở đây

Đây là cách bạn vượt qua ...

-(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");
    }

Vì vậy, đừng quên điều đó ..

làm {} trong khi (sai);

chỉ có nghĩa là "làm khối này một lần".

tức là, hoàn toàn không có sự khác biệt giữa viết do{}while(false);và chỉ đơn giản là viết {}.

Điều này bây giờ hoạt động hoàn hảo như bạn muốn ... đây là đầu ra ...

nhập mô tả hình ảnh ở đây

Vì vậy, có thể đó là cách bạn nhìn thấy thuật toán trong đầu. Bạn nên luôn luôn cố gắng viết những gì trong đầu của bạn. (Đặc biệt nếu bạn không tỉnh táo, vì đó là khi cái đẹp xuất hiện! :))

Trong các dự án "thuật toán" nơi điều này xảy ra rất nhiều, trong mục tiêu-c, chúng ta luôn có một macro như ...

#define RUNONCE while(false)

... vậy thì bạn có thể làm điều này ...

-(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");
    }

Có hai điểm:

a, mặc dù thật ngu ngốc khi mục tiêu-c kiểm tra loại khối mà một tuyên bố tiếp tục được đưa vào, nó gây rắc rối để "chống lại điều đó". Vì vậy, đó là một quyết định khó khăn.

b, có câu hỏi nào bạn nên thụt lề, trong ví dụ, khối đó không? Tôi mất ngủ vì những câu hỏi như vậy, vì vậy tôi không thể khuyên.

Hy vọng nó giúp.



Bạn đã đặt tiền thưởng để có thêm đại diện? :)
TMS

Thay vì đặt tất cả các nhận xét đó vào if, bạn cũng có thể sử dụng các tên hàm mô tả nhiều hơn và đặt các nhận xét trong các hàm.
Thomas Ahle

Kinh quá. Tôi sẽ sử dụng giải pháp 1 dòng đọc chính xác với đánh giá ngắn mạch (đã có ngôn ngữ trong hơn 20 năm và được biết đến) qua mớ hỗn độn này bất cứ ngày nào. Tôi nghĩ rằng cả hai chúng tôi có thể đồng ý rằng chúng tôi rất vui khi không làm việc với nhau.
M2tM

8

Có các hàm thực thi của bạn ném một ngoại lệ nếu chúng thất bại thay vì trả về false. Sau đó, mã cuộc gọi của bạn có thể trông như thế này:

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

Tất nhiên tôi giả sử rằng trong ví dụ ban đầu của bạn, bước thực thi sẽ chỉ trả về false trong trường hợp có lỗi xảy ra bên trong bước?


3
sử dụng ngoại lệ để kiểm soát luồng thường được coi là thông lệ xấu và mã có mùi
user902383

8

Rất nhiều câu trả lời hay, nhưng hầu hết trong số chúng dường như đánh đổi một số (thừa nhận rất ít) về tính linh hoạt. Một cách tiếp cận phổ biến không yêu cầu sự đánh đổi này là thêm một biến trạng thái / tiếp tục . Tất nhiên, giá là một giá trị bổ sung để theo dõi:

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;

Tại sao ok &= conditionX;và không đơn giản ok = conditionX;?
ABCplus

@ user3253359 Trong nhiều trường hợp, vâng, bạn có thể làm điều đó. Đây là một bản demo khái niệm; trong mã làm việc, chúng tôi sẽ cố gắng đơn giản hóa nó nhiều nhất có thể
blgt

+1 Một trong số ít câu trả lời rõ ràng và có thể duy trì hoạt động trong c , như được quy định trong câu hỏi.
ClickRick

6

Trong C ++ (câu hỏi được gắn thẻ cả C và C ++), nếu bạn không thể thay đổi các hàm để sử dụng ngoại lệ, bạn vẫn có thể sử dụng cơ chế ngoại lệ nếu bạn viết một hàm trợ giúp nhỏ như

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

Sau đó, mã của bạn có thể đọc như sau:

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

executeThisFunctionInAnyCase();

Nếu bạn thích cú pháp ưa thích, thay vào đó, bạn có thể làm cho nó hoạt động thông qua phân vai rõ ràng:

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

Sau đó, bạn có thể viết mã của bạn như

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

executeThisFunctionInAnyCase();

Tái cấu trúc kiểm tra giá trị thành các ngoại lệ không nhất thiết là một cách tốt để đi, có những ngoại lệ mở rộng đáng kể.
John Wu

4
-1 Sử dụng ngoại lệ cho luồng bình thường trong C ++ như thế này là thực hành lập trình kém. Trong trường hợp ngoại lệ C ++ nên được dành riêng cho các trường hợp đặc biệt.
Jack Aidley

1
Từ văn bản câu hỏi (nhấn mạnh bởi tôi): "Hàm execStepX nên được thực thi khi và chỉ khi thành công trước đó . " Nói cách khác, giá trị trả về được sử dụng để biểu thị thất bại. Đó là, đây là xử lý lỗi (và người ta sẽ hy vọng rằng các thất bại đặc biệt). Xử lý lỗi là chính xác những gì ngoại lệ được phát minh ra.
celtschk

1
Không. Thứ nhất, các ngoại lệ được tạo ra để cho phép lan truyền lỗi , không xử lý lỗi ; thứ hai, "Hàm execStepX nên được thực thi khi và chỉ khi thành công trước đó." không có nghĩa là boolean false được trả về bởi hàm trước biểu thị một trường hợp rõ ràng là ngoại lệ / sai sót. Tuyên bố của bạn là do đó không sequitur . Xử lý lỗi và khử trùng dòng chảy có thể được thực hiện bằng nhiều phương tiện khác nhau, các trường hợp ngoại lệ là một công cụ cho phép lan truyền lỗi và xử lý lỗi không đúng chỗ , và vượt trội ở đó.

6

Nếu mã của bạn đơn giản như ví dụ của bạn và ngôn ngữ của bạn hỗ trợ các đánh giá ngắn mạch, bạn có thể thử điều này:

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

Nếu bạn chuyển các đối số cho các hàm của mình và nhận lại các kết quả khác để mã của bạn không thể được viết theo kiểu trước đó, nhiều câu trả lời khác sẽ phù hợp hơn với vấn đề.


Trong thực tế, tôi đã chỉnh sửa câu hỏi của mình để giải thích rõ hơn về chủ đề này, nhưng nó đã bị từ chối để không làm mất hiệu lực hầu hết các câu trả lời. : \
ABCplus

Tôi là người dùng mới trong SO và là lập trình viên mới. Sau đó, có 2 câu hỏi: có nguy cơ một câu hỏi khác giống như câu hỏi bạn nói sẽ bị đánh dấu là trùng lặp vì câu hỏi NÀY không? Một điểm khác là: làm thế nào một người dùng / lập trình viên SO mới có thể chọn câu trả lời tốt nhất giữa tất cả ở đó (tôi cho rằng gần như tốt ..)?
ABCplus

6

Đối với C ++ 11 và hơn thế nữa, một cách tiếp cận hay có thể là triển khai hệ thống thoát phạm vi tương tự như cơ chế phạm vi (thoát) của D.

Một cách có thể để thực hiện nó là sử dụng C ++ 11 lambdas và một số macro trợ giúp:

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 })

Điều này sẽ cho phép bạn quay lại sớm từ chức năng và đảm bảo mọi mã dọn dẹp mà bạn xác định luôn được thực thi khi thoát khỏi phạm vi:

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

if (!executeStepA())
    return;

if (!executeStepB())
    return;

if (!executeStepC())
    return;

Các macro thực sự chỉ là trang trí. MakeScopeExit()có thể được sử dụng trực tiếp.


Không cần macro để thực hiện công việc này. Và [=]thường là sai đối với một lambda có phạm vi.
Yakk - Adam Nevraumont

Vâng, các macro chỉ để trang trí và có thể được ném đi. Nhưng bạn có nói rằng nắm bắt theo giá trị là cách tiếp cận "chung chung" an toàn nhất không?
glampert

1
không: nếu lambda của bạn sẽ không tồn tại ngoài phạm vi hiện tại nơi lambda được tạo, hãy sử dụng [&]: nó an toàn và tối thiểu đáng ngạc nhiên. Chỉ bị bắt theo giá trị khi lambda (hoặc bản sao) có thể tồn tại lâu hơn phạm vi tại điểm khai báo ...
Yakk - Adam Nevraumont

Vâng, điều đó có ý nghĩa. Tôi sẽ thay đổi nó. Cảm ơn!
glampert

6

Tại sao không ai đưa ra giải pháp đơn giản nhất? : D

Nếu tất cả các chức năng của bạn có cùng chữ ký thì bạn có thể thực hiện theo cách này (đối với ngôn ngữ C):

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

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

    if (!condition) {
        break;
    }
}

executeThisFunctionInAnyCase();

Đối với một giải pháp C ++ sạch, bạn nên tạo một lớp giao diện có chứa một phương thức thực thi và bao bọc các bước của bạn trong các đối tượng.
Sau đó, giải pháp trên sẽ như thế này:

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

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

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

executeThisFunctionInAnyCase();

5

Giả sử bạn không cần các biến điều kiện riêng lẻ, đảo ngược các bài kiểm tra và sử dụng phương thức khác như đường dẫn "ok" sẽ cho phép bạn có được một tập hợp các câu lệnh if / other khác:

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();

Việc bỏ qua biến không thành công làm cho mã quá tối nghĩa IMO.

Khai báo các biến bên trong là ổn, không phải lo lắng về = 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();

Điều này tối nghĩa, nhưng nhỏ gọn:

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

runThisFunctionInAnyCase();

5

Cái này trông giống như một máy trạng thái, tiện dụng vì bạn có thể dễ dàng thực hiện nó với một mẫu trạng thái .

Trong Java, nó sẽ trông giống như thế này:

interface StepState{
public StepState performStep();
}

Một triển khai sẽ hoạt động như sau:

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

Và như thế. Sau đó, bạn có thể thay thế lớn nếu điều kiện bằng:

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

5

Như Rommik đã đề cập, bạn có thể áp dụng một mẫu thiết kế cho việc này, nhưng tôi sẽ sử dụng mẫu Trang trí thay vì Chiến lược vì bạn muốn xâu chuỗi các cuộc gọi. Nếu mã đơn giản, thì tôi sẽ đi với một trong những câu trả lời có cấu trúc độc đáo để ngăn chặn việc lồng nhau. Tuy nhiên, nếu nó phức tạp hoặc đòi hỏi chuỗi động, thì mẫu Decorator là một lựa chọn tốt. Đây là sơ đồ lớp yUML :

sơ đồ lớp yUML

Đây là một chương trình LinqPad C # mẫu :

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;
    }
}

Cuốn sách tốt nhất để đọc về các mẫu thiết kế, IMHO, là Head First Design Forms .


Lợi ích của việc này so với câu trả lời của Jefffrey là gì?
Dason

Thay đổi nhanh hơn nhiều, khi các yêu cầu thay đổi cách tiếp cận này đơn giản hơn để quản lý mà không cần nhiều kiến ​​thức về miền. Đặc biệt là khi bạn xem xét làm thế nào sâu và dài một số phần của ifs lồng nhau có thể nhận được. Tất cả có thể trở nên rất mong manh và do đó rủi ro cao để làm việc với. Đừng hiểu sai về tôi một số tình huống tối ưu hóa có thể dẫn đến việc bạn trích xuất nó và quay lại ifs nhưng 99% thời gian là ổn. Nhưng vấn đề là khi bạn đạt đến mức đó, bạn không quan tâm đến khả năng bảo trì mà bạn cần hiệu suất.
John Nicholas

4

Một số câu trả lời gợi ý về một mẫu mà tôi đã thấy và sử dụng nhiều lần, đặc biệt là trong lập trình mạng. Trong các ngăn xếp mạng thường có một chuỗi các yêu cầu dài, bất kỳ yêu cầu nào cũng có thể thất bại và sẽ dừng quá trình.

Mô hình phổ biến là sử dụng do { } while (false);

Tôi đã sử dụng một macro cho while(false)để làm cho nó do { } once;Mô hình phổ biến là:

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

Mẫu này tương đối dễ đọc và cho phép các đối tượng được sử dụng sẽ phá hủy đúng cách và cũng tránh được nhiều trả về khiến việc bước và gỡ lỗi dễ dàng hơn một chút.


4

Để cải thiện câu trả lời C ++ 11 của Mathieu và tránh chi phí thời gian chạy phát sinh thông qua việc sử dụng std::function, tôi khuyên bạn nên sử dụng cách sau

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));
}

Lớp mẫu đơn giản này sẽ chấp nhận bất kỳ functor nào có thể được gọi mà không có bất kỳ tham số nào, và làm như vậy mà không cần phân bổ bộ nhớ động và do đó phù hợp hơn với mục tiêu trừu tượng của C ++ mà không cần chi phí không cần thiết. Mẫu hàm bổ sung có sẵn để đơn giản hóa việc sử dụng bằng cách khấu trừ tham số mẫu (không có sẵn cho các tham số mẫu lớp)

Ví dụ sử dụng:

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

Giống như câu trả lời của Mathieu, giải pháp này hoàn toàn ngoại lệ an toàn và executeThisFunctionInAnyCasesẽ được gọi trong mọi trường hợp. Nếu executeThisFunctionInAnyCasechính nó ném, các hàm hủy được đánh dấu ngầm noexceptvà do đó, một lệnh gọi std::terminatesẽ được phát ra thay vì gây ra một ngoại lệ được ném trong quá trình giải nén stack.


+1 Tôi đang tìm câu trả lời này vì vậy tôi sẽ không phải đăng nó. Bạn nên hoàn thiện chuyển tiếp functortrong deferred'd constructor, không cần ép buộc a move.
Yakk - Adam Nevraumont

@Yakk đã thay đổi hàm tạo thành hàm tạo chuyển tiếp
Joe

3

Có vẻ như bạn muốn thực hiện tất cả cuộc gọi của mình từ một khối. Như những người khác đã đề xuất, bạn nên sử dụng một whilevòng lặp và bỏ sử dụng breakhoặc một chức năng mới mà bạn có thể để lại return(có thể sạch hơn).

Cá nhân tôi trục xuất goto, thậm chí cho chức năng thoát. Chúng khó phát hiện hơn khi gỡ lỗi.

Một thay thế thanh lịch nên hoạt động cho quy trình công việc của bạn là xây dựng một mảng chức năng và lặp lại trên mảng này.

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();

May mắn thay, trình gỡ lỗi phát hiện ra chúng cho bạn. Nếu bạn đang gỡ lỗi và không thực hiện từng bước thông qua mã, bạn đã làm sai.
Cody Grey

Tôi không hiểu ý của bạn, tại sao tôi không thể sử dụng bước đơn?
AxFab

3

Vì bạn cũng có [... khối mã ...] giữa các lần thực thi, tôi đoán bạn có cấp phát bộ nhớ hoặc khởi tạo đối tượng. Theo cách này, bạn phải quan tâm đến việc làm sạch tất cả những gì bạn đã khởi tạo khi thoát, và cũng làm sạch nó nếu bạn gặp vấn đề và bất kỳ chức năng nào sẽ trả về sai.

Trong trường hợp này, tốt nhất những gì tôi có trong trải nghiệm của mình (khi tôi làm việc với CryptoAPI) là tạo các lớp nhỏ, trong hàm tạo, bạn khởi tạo dữ liệu của mình, trong hàm hủy bạn hủy kích hoạt dữ liệu. Mỗi lớp chức năng tiếp theo phải là con của lớp chức năng trước đó. Nếu một cái gì đó đã đi sai - ném ngoại lệ.

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;
};

Và sau đó trong mã của bạn, bạn chỉ cần gọi:

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();

Tôi đoán đó là giải pháp tốt nhất nếu mỗi cuộc gọi của conditionX khởi tạo một cái gì đó, bộ nhớ allocs và vv Tốt nhất để đảm bảo mọi thứ sẽ được làm sạch.


3

một cách thú vị là làm việc với các ngoại lệ.

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

Nếu bạn viết mã như vậy, bạn đang đi sai hướng. Tôi sẽ không coi đó là "vấn đề" để có mã như vậy, nhưng để có một "kiến trúc" lộn xộn như vậy.

Mẹo: thảo luận về các trường hợp đó với nhà phát triển dày dạn mà bạn tin tưởng ;-)


Tôi nghĩ ý tưởng này không thể thay thế mọi if-chain. Dù sao, trong nhiều trường hợp, đây là một cách tiếp cận rất tốt!
WoIIe

3

Một cách tiếp cận khác - do - whilevòng lặp, mặc dù nó đã được đề cập trước đó, không có ví dụ nào về nó sẽ cho thấy nó trông như thế nào:

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

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

executeThisFunctionInAnyCase();

(Vâng, đã có câu trả lời với whilevòng lặp nhưng do - whilevòng lặp không kiểm tra dự phòng đúng (lúc bắt đầu) mà thay vào đó là ở cuối xD (tuy nhiên điều này có thể được bỏ qua).


Này Zaffy - câu trả lời này có một lời giải thích lớn về cách tiếp cận do {} while (false). stackoverflow.com/a/24588605/294884 Hai câu trả lời khác cũng đề cập đến nó.
Fattie

Đó là một câu hỏi hấp dẫn liệu sử dụng TIẾP TỤC hoặc BREAK trong tình huống này sẽ thanh lịch hơn!
Fattie

Này @JoeBlow Tôi đã thấy tất cả các câu trả lời ... chỉ muốn hiển thị thay vì nói về nó :)
Zaffy

Câu trả lời đầu tiên của tôi ở đây tôi đã nói "Không ai đã đề cập đến NÀY ..." và ngay lập tức ai đó vui lòng chỉ ra đó là câu trả lời hàng đầu thứ 2 :)
Fattie

@JoeBlow Eh, bạn nói đúng. Tôi sẽ cố gắng khắc phục điều đó. Tôi cảm thấy như ... xD Dù sao cũng cảm ơn bạn, lần sau tôi sẽ chú ý hơn một chút :)
Zaffy
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.