Đảo ngược câu lệnh if if để giảm lồng


272

Khi tôi chạy ReSharper trên mã của mình, ví dụ:

    if (some condition)
    {
        Some code...            
    }

ReSharper đã đưa cho tôi cảnh báo ở trên (Đảo ngược câu lệnh "if" để giảm lồng nhau) và đề nghị sửa lỗi sau:

   if (!some condition) return;
   Some code...

Tôi muốn hiểu tại sao điều đó tốt hơn. Tôi luôn nghĩ rằng việc sử dụng "return" ở giữa một phương thức có vấn đề, hơi giống như "goto".


1
Tôi tin rằng việc kiểm tra ngoại lệ và quay lại lúc ban đầu là tốt, nhưng tôi sẽ thay đổi điều kiện để bạn kiểm tra ngoại lệ trực tiếp chứ không phải là một cái gì đó (ví dụ nếu (một số điều khoản) trở lại).
bruceatk

36
Không, nó sẽ không làm gì cho hiệu suất.
Seth Carnegie

3
Tôi muốn được ném một ArgumentException nếu phương thức của tôi bị truyền dữ liệu xấu thay thế.
asawyer

1
@asawyer Vâng, có một cuộc thảo luận toàn bộ ở đây về các chức năng quá tha thứ cho các đầu vào vô nghĩa - trái ngược với việc sử dụng một lỗi xác nhận. Viết Solid Code mở mắt cho tôi điều này. Trong trường hợp này, đây sẽ là một cái gì đó như ASSERT( exampleParam > 0 ).
Greg Hendershott

4
Các xác nhận là cho trạng thái nội bộ, không phải tham số. Bạn sẽ bắt đầu bằng cách xác thực các tham số, khẳng định rằng trạng thái bên trong của bạn là chính xác và sau đó thực hiện thao tác. Trong bản dựng phát hành, bạn có thể bỏ qua các xác nhận hoặc ánh xạ chúng tới tắt máy thành phần.
Simon Richter

Câu trả lời:


296

Một sự trở lại ở giữa phương thức không nhất thiết là xấu. Có thể tốt hơn để trở lại ngay lập tức nếu nó làm cho ý định của mã rõ ràng hơn. Ví dụ:

double getPayAmount() {
    double result;
    if (_isDead) result = deadAmount();
    else {
        if (_isSeparated) result = separatedAmount();
        else {
            if (_isRetired) result = retiredAmount();
            else result = normalPayAmount();
        };
    }
     return result;
};

Trong trường hợp này, nếu _isDeadlà đúng, chúng ta có thể thoát khỏi phương thức ngay lập tức. Thay vào đó, có thể tốt hơn để cấu trúc nó theo cách này:

double getPayAmount() {
    if (_isDead)      return deadAmount();
    if (_isSeparated) return separatedAmount();
    if (_isRetired)   return retiredAmount();

    return normalPayAmount();
};   

Tôi đã chọn mã này từ danh mục tái cấu trúc . Tái cấu trúc cụ thể này được gọi là: Thay thế các điều kiện lồng nhau bằng các khoản bảo vệ.


13
Đây là một ví dụ thực sự tốt đẹp! Mã tái cấu trúc đọc giống như một tuyên bố trường hợp.
Otherside

16
Có lẽ chỉ là vấn đề của hương vị: Tôi đề nghị thay đổi "nếu" thành "khác" thứ 2 và thứ 3 để tăng khả năng đọc hơn nữa. Nếu một người bỏ qua câu lệnh "trả lại" thì vẫn rõ ràng rằng trường hợp sau chỉ được kiểm tra nếu trường hợp trước không thành công, nghĩa là thứ tự kiểm tra là quan trọng.
trả trước

2
Trong một ví dụ đơn giản này, id đồng ý cách thứ 2 là tốt hơn, nhưng chỉ vì SO rõ ràng những gì đang xảy ra.
Andrew Bullock

Bây giờ, làm thế nào để chúng ta lấy cái mã đẹp đó đã viết và giữ cho studio hình ảnh không phá vỡ nó và đặt lợi nhuận trên các dòng riêng biệt? Nó thực sự làm tôi khó chịu khi nó định dạng lại mã theo cách đó. Làm cho mã rất dễ đọc này UGLY.
Đánh dấu T

@Mark T Có các cài đặt trong studio hình ảnh để ngăn chặn nó phá vỡ mã đó.
Aaron Smith

333

Nó không chỉ mang tính thẩm mỹ mà còn làm giảm mức độ lồng tối đa bên trong phương thức. Điều này thường được coi là một điểm cộng vì nó làm cho các phương thức dễ hiểu hơn (và thực tế, nhiều công cụ phân tích tĩnh cung cấp thước đo này là một trong những chỉ số về chất lượng mã).

Mặt khác, nó cũng làm cho phương thức của bạn có nhiều điểm thoát, điều mà một nhóm người khác tin là không.

Cá nhân, tôi đồng ý với ReSharper và nhóm đầu tiên (trong ngôn ngữ có ngoại lệ tôi thấy thật ngớ ngẩn khi thảo luận về "nhiều điểm thoát"; hầu như mọi thứ đều có thể ném, vì vậy có rất nhiều điểm thoát tiềm năng trong tất cả các phương pháp).

Về hiệu suất : cả hai phiên bản phải tương đương (nếu không ở cấp độ IL, thì chắc chắn sau khi jitter thông qua mã) trong mọi ngôn ngữ. Về mặt lý thuyết điều này phụ thuộc vào trình biên dịch, nhưng thực tế, bất kỳ trình biên dịch được sử dụng rộng rãi ngày nay đều có khả năng xử lý các trường hợp tối ưu hóa mã tiên tiến hơn nhiều so với điều này.


41
điểm thoát đơn? Ai cần nó?
sq33G

3
@ sq33G: Câu hỏi đó trên SESE (và tất nhiên là câu trả lời) thật tuyệt vời. Cảm ơn các liên kết!
Jon

Tôi tiếp tục nghe câu trả lời rằng có một số người ủng hộ các điểm thoát đơn, nhưng tôi chưa bao giờ thấy ai đó thực sự ủng hộ điều đó, đặc biệt là trong các ngôn ngữ như C #.
Thomas Bonini

1
@AndreasBonini: Vắng mặt không phải là bằng chứng vắng mặt. :-)
Jon

Tất nhiên, tôi chỉ thấy lạ là mọi người đều cảm thấy cần phải nói rằng một số người thích cách tiếp cận khác nếu những người đó không cảm thấy cần phải tự nói điều đó =)
Thomas Bonini

102

Đây là một chút tranh luận về tôn giáo, nhưng tôi đồng ý với ReSharper rằng bạn nên thích làm tổ ít hơn. Tôi tin rằng điều này vượt xa những tiêu cực của việc có nhiều đường dẫn trả về từ một hàm.

Lý do chính để có ít lồng nhau là để cải thiện khả năng đọc và bảo trì mã . Hãy nhớ rằng nhiều nhà phát triển khác sẽ cần đọc mã của bạn trong tương lai và mã có ít thụt lề thường dễ đọc hơn nhiều.

Điều kiện tiên quyết là một ví dụ tuyệt vời về việc bạn có thể quay lại sớm khi bắt đầu chức năng. Tại sao khả năng đọc của phần còn lại của chức năng bị ảnh hưởng bởi sự hiện diện của kiểm tra điều kiện tiên quyết?

Đối với các tiêu cực về việc trả lại nhiều lần từ một phương thức - trình gỡ lỗi hiện khá mạnh mẽ và rất dễ dàng để tìm ra chính xác vị trí và thời điểm một chức năng cụ thể đang trở lại.

Có nhiều lợi nhuận trong một chức năng sẽ không ảnh hưởng đến công việc của lập trình viên bảo trì.

Khả năng đọc mã kém sẽ.


2
Nhiều trả về trong một hàm làm hiệu ứng lập trình bảo trì. Khi gỡ lỗi một chức năng, tìm kiếm giá trị trả lại giả mạo, người bảo trì phải đặt các điểm dừng trên tất cả các vị trí có thể trả về.
EvilTeach

10
Tôi sẽ đặt một điểm dừng trên cú đúp mở đầu. Nếu bạn bước qua chức năng, bạn không chỉ có thể thấy các kiểm tra được thực hiện theo thứ tự, mà còn rất rõ ràng kiểm tra chức năng đã hạ cánh cho lần chạy đó.
John Dunagan

3
Đồng ý với John ở đây ... Tôi không thấy vấn đề gì khi chỉ xem qua toàn bộ chức năng.
Thợ làm móng

5
@Nailer Rất muộn, tôi đặc biệt đồng ý, vì nếu chức năng quá lớn để bước qua, dù sao thì nó cũng nên được tách thành nhiều chức năng!
Aidiakapi

1
Đồng ý với John cũng được. Có thể đó là một trường học cũ được cho là đặt điểm dừng ở phía dưới nhưng nếu bạn tò mò chức năng nào sẽ quay trở lại thì bạn sẽ bước qua chức năng đã nói để xem tại sao nó lại trả về cái mà nó sẽ trả về. Nếu bạn chỉ muốn xem những gì nó trả về thì đặt nó vào dấu ngoặc cuối cùng.
user441521

70

Như những người khác đã đề cập, không nên có một hiệu suất thành công, nhưng có những cân nhắc khác. Bên cạnh những mối quan tâm hợp lệ đó, điều này cũng có thể mở ra cho bạn những vấn đề khó khăn trong một số trường hợp. Giả sử bạn đang giao dịch với một doublethay thế:

public void myfunction(double exampleParam){
    if(exampleParam > 0){
        //Body will *not* be executed if Double.IsNan(exampleParam)
    }
}

Ngược lại với sự đảo ngược dường như tương đương:

public void myfunction(double exampleParam){
    if(exampleParam <= 0)
        return;
    //Body *will* be executed if Double.IsNan(exampleParam)
}

Vì vậy, trong một số trường hợp, những gì dường như được đảo ngược chính xác ifcó thể không được.


4
Cũng cần lưu ý rằng việc chia sẻ lại sửa chữa nhanh chóng chuyển đổi exampleParam> 0 thành exampleParam <0 không exampleParam <= 0 đã khiến tôi bị loại.
nickd

1
Và đây là lý do tại sao kiểm tra đó phải được đưa lên phía trước và trả lại cũng như các nhà phát triển nghĩ rằng một nan sẽ dẫn đến việc bảo lãnh.
dùng441521

51

Ý tưởng chỉ trở lại vào cuối của một chức năng đã trở lại từ những ngày trước khi các ngôn ngữ có hỗ trợ cho các trường hợp ngoại lệ. Nó cho phép các chương trình dựa vào việc có thể đặt mã dọn dẹp ở cuối phương thức, và sau đó chắc chắn rằng nó sẽ được gọi và một số lập trình viên khác sẽ không ẩn trả về phương thức khiến mã dọn dẹp bị bỏ qua . Bỏ qua mã dọn dẹp có thể dẫn đến rò rỉ bộ nhớ hoặc tài nguyên.

Tuy nhiên, trong một ngôn ngữ hỗ trợ các ngoại lệ, nó không cung cấp sự đảm bảo như vậy. Trong một ngôn ngữ hỗ trợ các ngoại lệ, việc thực hiện bất kỳ câu lệnh hoặc biểu thức nào có thể gây ra luồng điều khiển khiến phương thức kết thúc. Điều này có nghĩa là việc dọn dẹp phải được thực hiện thông qua việc sử dụng cuối cùng hoặc sử dụng từ khóa.

Dù sao, tôi đang nói rằng tôi nghĩ rằng nhiều người trích dẫn "chỉ quay lại khi kết thúc phương pháp" mà không hiểu tại sao nó lại là một việc nên làm và việc giảm lồng để cải thiện khả năng đọc có lẽ là mục đích tốt hơn.


6
Bạn chỉ cần tìm ra lý do tại sao các trường hợp ngoại lệ là UglyAndEvil [tm] ... ;-) Các ngoại lệ là hình ảnh trong trang phục đắt tiền, ngụy trang.
EricSchaefer

16
@Eric bạn phải đã tiếp xúc với mã đặc biệt xấu. Điều này khá rõ ràng khi chúng được sử dụng không chính xác và chúng thường cho phép bạn viết mã chất lượng cao hơn.
Robert Paulson

Đây là câu trả lời tôi đã thực sự tìm kiếm! Có những câu trả lời tuyệt vời ở đây, nhưng hầu hết trong số chúng tập trung vào các ví dụ, không phải lý do thực sự tại sao khuyến nghị này được sinh ra.
Gustavo Mori

Đây là câu trả lời tốt nhất, vì nó giải thích tại sao toàn bộ cuộc tranh luận này nảy sinh ở nơi đầu tiên.
Butussy Butkus

If you've got deep nesting, maybe your function is trying to do too many things.=> đây không phải là một hậu quả chính xác từ cụm từ trước của bạn. Bởi vì ngay trước khi bạn nói rằng bạn có thể cấu trúc lại hành vi A với mã C thành hành vi A với mã D. mã D sạch hơn, được cấp, nhưng "quá nhiều thứ" đề cập đến hành vi KHÔNG thay đổi. Vì vậy, bạn không có ý nghĩa gì với kết luận này.
v.oddou

30

Tôi muốn thêm rằng có tên cho những người bị đảo ngược nếu - Điều khoản bảo vệ. Tôi sử dụng nó bất cứ khi nào tôi có thể.

Tôi ghét đọc mã ở đâu nếu ở đầu, hai màn hình mã và không có gì khác. Chỉ cần đảo ngược nếu và trở lại. Bằng cách đó, không ai sẽ lãng phí thời gian để cuộn.

http://c2.com/cgi/wiki?GuardClause


2
Chính xác. Nó là nhiều hơn một tái cấu trúc. Nó giúp đọc mã dễ dàng hơn như bạn đã đề cập.
rpattabi

'trở về' là không đủ và khủng khiếp cho một điều khoản bảo vệ. Tôi sẽ làm: if (ex <= 0) throw WrongParamValueEx ("[Tên phương thức] Giá trị param 1 đầu vào xấu {0} ... và bạn sẽ cần phải bắt ngoại lệ và ghi vào nhật ký ứng dụng của mình
JohnJohnGa

3
@John. Bạn đúng nếu đây là lỗi thực tế. Nhưng thường thì không. Và thay vì kiểm tra một cái gì đó ở mọi nơi mà phương thức được gọi là phương thức, chỉ cần kiểm tra nó và trả về không làm gì cả.
Piotr Perak

3
Tôi ghét đọc một phương thức gồm hai màn hình mã, dấu chấm, ifs hoặc không. lol
jpmc26

1
Cá nhân tôi thích trả lại sớm vì chính xác lý do này (cũng: tránh làm tổ). Tuy nhiên, điều này không liên quan đến câu hỏi ban đầu về hiệu suất và có lẽ nên là một nhận xét.
Patrick M

22

Nó không chỉ ảnh hưởng đến tính thẩm mỹ, mà còn ngăn chặn việc lồng mã.

Nó thực sự có thể hoạt động như một điều kiện tiên quyết để đảm bảo rằng dữ liệu của bạn cũng hợp lệ.


18

Điều này là tất nhiên chủ quan, nhưng tôi nghĩ rằng nó cải thiện mạnh mẽ trên hai điểm:

  • Bây giờ rõ ràng là chức năng của bạn không còn gì để làm nếu bị conditiongiữ.
  • Nó giữ mức độ làm tổ xuống. Nesting làm tổn thương khả năng đọc nhiều hơn bạn nghĩ.

15

Nhiều điểm trả về là một vấn đề trong C (và ở mức độ thấp hơn C ++) vì chúng buộc bạn phải sao chép mã dọn dẹp trước mỗi điểm trả về. Với bộ sưu tập rác, try| finallyxây dựng và usingkhối, thực sự không có lý do tại sao bạn nên sợ chúng.

Cuối cùng, nó thuộc về những gì bạn và đồng nghiệp của bạn thấy dễ đọc hơn.


Đó không phải là lý do duy nhất. Có một lý do học thuật đề cập đến mã giả không liên quan gì đến những cân nhắc thực tế như công cụ làm sạch. Lý do acamedical này có liên quan đến việc tôn trọng các hình thức cơ bản của các cấu trúc mệnh lệnh. Giống như cách bạn không đặt trình thoát vòng lặp ở giữa. Bằng cách này, bất biến có thể được phát hiện nghiêm ngặt và hành vi có thể được chứng minh. Hoặc chấm dứt có thể được chứng minh.
v.oddou

1
tin tức: thực sự tôi đã tìm thấy một lý do rất thực tế tại sao nghỉ sớm và trả lại là xấu, và đó là hậu quả trực tiếp của những gì tôi đã nói, phân tích tĩnh. Ở đây bạn đi, hướng dẫn sử dụng trình biên dịch intel C ++: d3f8ykwhia686p.cloudfront.net/1live/intel/ . Cụm từ chính:a loop that is not vectorizable due to a second data-dependent exit
v.oddou

12

Hiệu suất-khôn ngoan, sẽ không có sự khác biệt đáng chú ý giữa hai phương pháp.

Nhưng mã hóa là về nhiều hơn hiệu suất. Rõ ràng và bảo trì cũng rất quan trọng. Và, trong những trường hợp như thế này khi nó không ảnh hưởng đến hiệu suất, đó là điều duy nhất quan trọng.

Có những trường phái tư tưởng cạnh tranh về cách tiếp cận nào là thích hợp hơn.

Một quan điểm là một quan điểm khác đã đề cập: cách tiếp cận thứ hai làm giảm mức lồng nhau, giúp cải thiện độ rõ của mã. Điều này là tự nhiên trong một phong cách bắt buộc: khi bạn không còn gì để làm, bạn cũng có thể trở về sớm.

Một quan điểm khác, từ quan điểm của một phong cách chức năng hơn, là một phương thức chỉ nên có một điểm thoát. Tất cả mọi thứ trong một ngôn ngữ chức năng là một biểu thức. Vì vậy, nếu các câu lệnh phải luôn luôn có một mệnh đề khác. Mặt khác, biểu thức if sẽ không luôn có giá trị. Vì vậy, trong phong cách chức năng, cách tiếp cận đầu tiên là tự nhiên hơn.


11

Điều khoản bảo vệ hoặc điều kiện trước (như bạn có thể thấy) kiểm tra xem liệu một điều kiện nhất định có được đáp ứng và sau đó phá vỡ dòng chảy của chương trình. Chúng tuyệt vời cho những nơi mà bạn thực sự chỉ quan tâm đến một kết quả của một iftuyên bố. Vì vậy, thay vì nói:

if (something) {
    // a lot of indented code
}

Bạn đảo ngược điều kiện và ngắt nếu điều kiện đảo ngược đó được thỏa mãn

if (!something) return false; // or another value to show your other code the function did not execute

// all the code from before, save a lot of tabs

returnkhông nơi nào gần bẩn như goto. Nó cho phép bạn chuyển một giá trị để hiển thị phần còn lại của mã mà hàm không thể chạy.

Bạn sẽ thấy các ví dụ tốt nhất về nơi có thể áp dụng điều này trong các điều kiện lồng nhau:

if (something) {
    do-something();
    if (something-else) {
        do-another-thing();
    } else {
        do-something-else();
    }
}

vs

if (!something) return;
do-something();

if (!something-else) return do-something-else();
do-another-thing();

Bạn sẽ thấy ít người tranh luận đầu tiên là sạch hơn nhưng tất nhiên, nó hoàn toàn chủ quan. Một số lập trình viên muốn biết những điều kiện mà một cái gì đó đang hoạt động theo cách thụt lề, trong khi tôi muốn giữ dòng phương thức tuyến tính hơn.

Tôi sẽ không đề xuất trong một khoảnh khắc rằng các tiền tố sẽ thay đổi cuộc sống của bạn hoặc khiến bạn cảm thấy thoải mái nhưng bạn có thể thấy mã của mình dễ đọc hơn một chút.


6
Tôi đoán tôi là một trong số ít Tôi thấy dễ dàng hơn để đọc phiên bản đầu tiên. Các if lồng nhau làm cho cây quyết định rõ ràng hơn. Mặt khác, nếu có một vài điều kiện trước, tôi đồng ý tốt hơn là đặt tất cả chúng ở đầu hàm.
Otherside

@Otherside: hoàn toàn đồng ý. lợi nhuận được viết theo kiểu nối tiếp làm cho bộ não của bạn cần phải tuần tự hóa các đường dẫn có thể. Khi cây if có thể được ánh xạ trực tiếp vào một số logic xuyên suốt, ví dụ, nó có thể được biên dịch thành lisp. nhưng thời trang trả về nối tiếp sẽ đòi hỏi một trình biên dịch khó hơn nhiều để làm. Vấn đề ở đây rõ ràng không liên quan gì đến kịch bản giả định này, nó liên quan đến phân tích mã, cơ hội tối ưu hóa, bằng chứng sửa lỗi, bằng chứng chấm dứt và phát hiện bất biến.
v.oddou

1
Không có cách nào bất cứ ai tìm thấy nó dễ dàng hơn để đọc mã với nhiều tổ. Càng nhiều khối như một cái gì đó càng dễ đọc trong nháy mắt. Không có cách nào ví dụ đầu tiên ở đây tôi dễ đọc hơn và nó chỉ đi được 2 tổ khi hầu hết các mã như thế này trong thế giới thực đi sâu hơn và với mỗi tổ, nó đòi hỏi nhiều năng lực não hơn để theo dõi.
dùng441521

1
Nếu việc làm tổ sâu hơn gấp đôi, bạn sẽ mất bao lâu để trả lời câu hỏi: "Khi nào bạn 'làm-việc-khác'?" Tôi nghĩ rằng bạn khó có thể trả lời nó mà không có bút và giấy. Nhưng bạn có thể trả lời nó khá dễ dàng nếu tất cả đều được bảo vệ.
David Storfer

9

Có một số điểm tốt được thực hiện ở đây, nhưng nhiều điểm trả về cũng có thể không đọc được , nếu phương thức này rất dài. Điều đó đang được nói, nếu bạn sẽ sử dụng nhiều điểm trả về, chỉ cần đảm bảo rằng phương thức của bạn ngắn, nếu không phần thưởng dễ đọc của nhiều điểm trả về có thể bị mất.


8

Hiệu suất là hai phần. Bạn có hiệu suất khi phần mềm đang được sản xuất, nhưng bạn cũng muốn có hiệu suất trong khi phát triển và gỡ lỗi. Điều cuối cùng mà một nhà phát triển muốn là "chờ đợi" một cái gì đó tầm thường. Cuối cùng, việc biên dịch cái này với kích hoạt tối ưu hóa sẽ dẫn đến mã tương tự. Vì vậy, thật tốt khi biết những thủ thuật nhỏ này đã được đền đáp trong cả hai tình huống.

Trường hợp trong câu hỏi là rõ ràng, ReSharper là chính xác. Thay vì lồng các ifcâu lệnh và tạo phạm vi mới trong mã, bạn đang đặt một quy tắc rõ ràng khi bắt đầu phương thức của mình. Nó làm tăng khả năng đọc, nó sẽ dễ dàng hơn để duy trì và nó làm giảm số lượng quy tắc người ta phải sàng lọc để tìm nơi họ muốn đi.


7

Cá nhân tôi chỉ thích 1 điểm thoát. Thật dễ dàng để thực hiện nếu bạn giữ các phương thức của mình ngắn gọn và chính xác, và nó cung cấp một mẫu có thể dự đoán được cho người tiếp theo làm việc với mã của bạn.

ví dụ.

 bool PerformDefaultOperation()
 {
      bool succeeded = false;

      DataStructure defaultParameters;
      if ((defaultParameters = this.GetApplicationDefaults()) != null)
      {
           succeeded = this.DoSomething(defaultParameters);
      }

      return succeeded;
 }

Điều này cũng rất hữu ích nếu bạn chỉ muốn kiểm tra các giá trị của các biến cục bộ nhất định trong một hàm trước khi nó thoát. Tất cả những gì bạn cần làm là đặt một điểm dừng trên lợi nhuận cuối cùng và bạn được đảm bảo đánh nó (trừ khi có ngoại lệ được ném).


4
bool PerformanceDefaultOperation () {DataStr struct defaultParameter = this.GetApplicationDefaults (); return (defaultParameter! = NULL && this.DoS Something (defaultParameter);} Ở đó, đã sửa nó cho bạn. :)
tchen

1
Ví dụ này rất cơ bản và bỏ lỡ điểm của mẫu này. Có khoảng 4 kiểm tra khác bên trong chức năng này và sau đó cho chúng tôi biết nó dễ đọc hơn, bởi vì không phải vậy.
dùng441521

5

Nhiều lý do tốt về cách mã trông như thế nào . Nhưng kết quả thì sao?

Chúng ta hãy xem một số mã C # và mẫu được biên dịch IL của nó:


using System;

public class Test {
    public static void Main(string[] args) {
        if (args.Length == 0) return;
        if ((args.Length+2)/3 == 5) return;
        Console.WriteLine("hey!!!");
    }
}

Đoạn mã đơn giản này có thể được biên dịch. Bạn có thể mở tệp .exe được tạo bằng ildasm và kiểm tra xem kết quả là gì. Tôi sẽ không đăng tất cả các công cụ biên dịch nhưng tôi sẽ mô tả kết quả.

Mã IL được tạo thực hiện như sau:

  1. Nếu điều kiện thứ nhất là sai, nhảy tới mã có thứ hai.
  2. Nếu đó là sự thật nhảy đến hướng dẫn cuối cùng. (Lưu ý: hướng dẫn cuối cùng là trả lại).
  3. Trong điều kiện thứ hai, điều tương tự xảy ra sau khi kết quả được tính toán. So sánh và: đã đến Console.WriteLine nếu sai hoặc kết thúc nếu điều này là đúng.
  4. In tin nhắn và trở về.

Vì vậy, có vẻ như mã sẽ nhảy đến cuối. Điều gì xảy ra nếu chúng ta làm bình thường nếu với mã lồng nhau?

using System;

public class Test {
    public static void Main(string[] args) {
        if (args.Length != 0 && (args.Length+2)/3 != 5) 
        {
            Console.WriteLine("hey!!!");
        }
    }
}

Các kết quả khá giống nhau trong hướng dẫn IL. Sự khác biệt là trước khi có các bước nhảy theo điều kiện: nếu sai đi đến đoạn mã tiếp theo, nếu đúng thì đi đến kết thúc. Và bây giờ mã IL chảy tốt hơn và có 3 lần nhảy (trình biên dịch đã tối ưu hóa điều này một chút): 1. Bước nhảy đầu tiên: khi Độ dài bằng 0 đến một phần mà mã nhảy lại (Bước nhảy thứ ba) đến cuối. 2. Thứ hai: ở giữa điều kiện thứ hai để tránh một lệnh. 3. Thứ ba: nếu điều kiện thứ hai là sai, nhảy đến cuối.

Dù sao, các chương trình truy cập sẽ luôn luôn nhảy.


5
Đây là thông tin tốt - nhưng cá nhân tôi không thể quan tâm ít hơn nếu IL "chảy tốt hơn" bởi vì những người sẽ quản lý mã sẽ không thấy bất kỳ IL nào
JohnIdol

1
Bạn thực sự không thể lường trước việc vượt qua tối ưu hóa trong bản cập nhật tiếp theo của trình biên dịch C # sẽ hoạt động như thế nào so với những gì bạn thấy ngày hôm nay. Cá nhân, tôi sẽ không dành bất kỳ thời gian nào để cố gắng điều chỉnh mã C # để tạo ra một kết quả khác trong IL, trừ khi việc này cho thấy bạn có vấn đề về hiệu suất SEVERE trong một số vòng lặp chặt chẽ ở đâu đó và bạn đã hết ý tưởng khác . Thậm chí sau đó, tôi sẽ suy nghĩ nhiều hơn về các lựa chọn khác. Đồng quan điểm, tôi nghi ngờ rằng bạn có bất kỳ ý tưởng nào về trình tối ưu hóa trình biên dịch JIT đang làm với IL được cung cấp cho nó cho mỗi nền tảng ...
Craig

5

Về lý thuyết, đảo ngược ifcó thể dẫn đến hiệu suất tốt hơn nếu nó tăng tỷ lệ trúng dự đoán chi nhánh. Trong thực tế, tôi nghĩ rất khó để biết chính xác dự đoán chi nhánh sẽ hoạt động như thế nào, đặc biệt là sau khi biên dịch, vì vậy tôi sẽ không làm điều đó trong quá trình phát triển hàng ngày của mình, trừ khi tôi đang viết mã lắp ráp.

Thêm về dự đoán chi nhánh ở đây .


4

Điều đó chỉ đơn giản là gây tranh cãi. Không có "thỏa thuận giữa các lập trình viên" về câu hỏi hoàn trả sớm. Nó luôn luôn chủ quan, theo như tôi biết.

Có thể đưa ra một đối số hiệu suất, vì tốt hơn là có các điều kiện được viết để chúng thường đúng nhất; nó cũng có thể được lập luận rằng nó rõ ràng hơn. Mặt khác, nó tạo ra các bài kiểm tra lồng nhau.

Tôi không nghĩ bạn sẽ nhận được câu trả lời kết luận cho câu hỏi này.


Tôi không hiểu lý lẽ hiệu suất của bạn. Các điều kiện là boolean vì vậy dù kết quả có ra sao, luôn có hai kết quả ... Tôi không hiểu tại sao đảo ngược một tuyên bố (hoặc không) sẽ thay đổi kết quả. (Trừ khi bạn nói thêm "KHÔNG" vào điều kiện sẽ thêm số lượng xử lý có thể đo được ...
Oli

2
Việc tối ưu hóa hoạt động như thế này: Nếu bạn đảm bảo rằng trường hợp phổ biến nhất nằm trong khối if thay vì khối khác, thì CPU thường sẽ có các câu lệnh từ khối if được tải trong đường ống của nó. Nếu trở về tình trạng sai, CPU cần phải làm rỗng đường ống dẫn của nó và ...
Otherside

Tôi cũng thích làm những gì người khác đang nói chỉ để dễ đọc, tôi ghét có những trường hợp tiêu cực trong khối "sau đó" hơn là "khác". Thật hợp lý khi làm điều này ngay cả khi không biết hoặc xem xét CPU đang làm gì
Andrew Bullock

3

Có rất nhiều câu trả lời sâu sắc đã có, nhưng tôi vẫn sẽ hướng đến một tình huống hơi khác: Thay vì điều kiện tiên quyết, điều đó thực sự nên được đặt lên trên một chức năng, hãy nghĩ đến việc khởi tạo từng bước, nơi bạn phải kiểm tra từng bước để thành công và sau đó tiếp tục với bước tiếp theo. Trong trường hợp này, bạn không thể kiểm tra mọi thứ ở trên cùng.

Tôi thấy mã của mình thực sự không thể đọc được khi viết ứng dụng máy chủ ASIO với ASIOSDK của Steinberg, khi tôi theo mô hình lồng nhau. Nó đã đi sâu như tám cấp độ, và tôi không thể thấy một lỗ hổng thiết kế nào ở đó, như Andrew Bullock đã đề cập ở trên. Tất nhiên, tôi có thể đã đóng gói một số mã bên trong cho một chức năng khác, và sau đó lồng các cấp độ còn lại ở đó để làm cho nó dễ đọc hơn, nhưng điều này có vẻ khá ngẫu nhiên đối với tôi.

Bằng cách thay thế lồng nhau bằng các mệnh đề bảo vệ, tôi thậm chí đã phát hiện ra một quan niệm sai lầm của tôi về một phần mã dọn dẹp đã xảy ra sớm hơn nhiều trong hàm thay vì ở cuối. Với các nhánh lồng nhau, tôi sẽ không bao giờ thấy điều đó, thậm chí bạn có thể nói rằng chúng dẫn đến quan niệm sai lầm của tôi.

Vì vậy, đây có thể là một tình huống khác trong đó các if đảo ngược có thể đóng góp cho một mã rõ ràng hơn.


3

Tránh nhiều điểm thoát có thể dẫn đến tăng hiệu suất. Tôi không chắc chắn về C # nhưng trong C ++, Tối ưu hóa giá trị trả về được đặt tên (Sao chép bản sao, ISO C ++ '03 12.8 / 15) phụ thuộc vào việc có một điểm thoát duy nhất. Tối ưu hóa này tránh sao chép xây dựng giá trị trả về của bạn (trong ví dụ cụ thể của bạn không thành vấn đề). Điều này có thể dẫn đến tăng đáng kể hiệu năng trong các vòng lặp chặt chẽ, vì bạn đang lưu một hàm tạo và hàm hủy mỗi khi hàm được gọi.

Nhưng đối với 99% các trường hợp lưu các lệnh gọi hàm tạo và hàm hủy bổ sung thì không đáng để mất các ifkhối lồng nhau có thể đọc được (như những người khác đã chỉ ra).


2
ở đó tôi đã mất 3 lần đọc dài hàng chục câu trả lời trong 3 câu hỏi tương tự (2 trong số đó bị đóng băng), để cuối cùng tìm thấy ai đó đề cập đến NRVO. gee ... cảm ơn bạn
v.oddou

2

Đó là một vấn đề về quan điểm.

Cách tiếp cận thông thường của tôi sẽ là tránh các ifs dòng đơn và trả về ở giữa một phương thức.

Bạn sẽ không muốn các dòng như nó gợi ý ở mọi nơi trong phương thức của bạn nhưng có một điều cần nói để kiểm tra một loạt các giả định ở đầu phương thức của bạn và chỉ thực hiện công việc thực tế của bạn nếu tất cả đều vượt qua.


2

Theo tôi, trả lại sớm là tốt nếu bạn chỉ trả về khoảng trống (hoặc một số mã trả lại vô dụng mà bạn sẽ không bao giờ kiểm tra) và nó có thể cải thiện khả năng đọc vì bạn tránh lồng nhau và đồng thời bạn nói rõ rằng chức năng của bạn đã được thực hiện.

Nếu bạn thực sự trả về returnValue - lồng thường là cách tốt hơn để bạn trả lại returnValue chỉ ở một nơi (ở cuối - duh), và nó có thể giúp mã của bạn dễ bảo trì hơn trong nhiều trường hợp.


1

Tôi không chắc, nhưng tôi nghĩ, R # cố gắng tránh những cú nhảy xa. Khi bạn có IF-ELSE, trình biên dịch sẽ thực hiện một số thứ như thế này:

Điều kiện sai -> nhảy xa đến false_condition_label

true_condition_label: hướng dẫn1 ... hướng dẫn_n

false_condition_label: hướng dẫn1 ... hướng dẫn_n

khối kết thúc

Nếu điều kiện là đúng thì không có nhảy và không có bộ đệm L1, nhưng nhảy tới false_condition_label có thể rất xa và bộ xử lý phải triển khai bộ đệm của chính mình. Đồng bộ hóa bộ nhớ cache là tốn kém. R # cố gắng thay thế các bước nhảy xa thành các bước nhảy ngắn và trong trường hợp này có xác suất lớn hơn, rằng tất cả các hướng dẫn đã có trong bộ đệm.


0

Tôi nghĩ rằng nó phụ thuộc vào những gì bạn thích, như đã đề cập, không có thỏa thuận chung afaik. Để giảm bớt sự khó chịu, bạn có thể giảm loại cảnh báo này thành "Gợi ý"


0

Ý tưởng của tôi là sự trở lại "ở giữa một hàm" không nên quá "chủ quan". Lý do khá đơn giản, lấy mã này:

    hàm do_s Something (data) {

      if (! is_valid_data (dữ liệu)) 
            trả lại sai;


       do_s Something_that_take_an_hour (dữ liệu);

       istance = new object_with_very_painful_constructor (dữ liệu);

          if (istance không hợp lệ) {
               thông báo lỗi( );
                trở về ;

          }
       kết nối_to_database ();
       get_some_other_data ();
       trở về;
    }

Có thể "trở về" đầu tiên không phải là trực quan, nhưng đó thực sự là tiết kiệm. Có quá nhiều "ý tưởng" về mã sạch, chỉ cần thực hành nhiều hơn để đánh mất những ý tưởng tồi "chủ quan" của họ.


Với một ngôn ngữ ngoại lệ, bạn không có những vấn đề này.
dùng441521

0

Có một số lợi thế cho loại mã hóa này nhưng đối với tôi, chiến thắng lớn là, nếu bạn có thể quay lại nhanh, bạn có thể cải thiện tốc độ của ứng dụng. IE Tôi biết rằng vì Điều kiện tiên quyết X mà tôi có thể quay lại nhanh chóng với một lỗi. Điều này được loại bỏ các trường hợp lỗi đầu tiên và làm giảm độ phức tạp của mã của bạn. Trong rất nhiều trường hợp vì đường ống cpu bây giờ có thể sạch hơn, nó có thể dừng sự cố hoặc chuyển mạch đường ống. Thứ hai, nếu bạn đang ở trong một vòng lặp, phá vỡ hoặc quay trở lại nhanh chóng có thể giúp bạn tiết kiệm rất nhiều cpu. Một số lập trình viên sử dụng các bất biến vòng lặp để thực hiện loại thoát nhanh này nhưng trong trường hợp này, bạn có thể phá vỡ đường dẫn cpu của mình và thậm chí tạo ra vấn đề tìm kiếm bộ nhớ và có nghĩa là cpu cần phải tải từ bộ nhớ cache bên ngoài. Nhưng về cơ bản tôi nghĩ bạn nên làm những gì bạn dự định, đó là kết thúc vòng lặp hoặc hàm không tạo ra một đường dẫn mã phức tạp chỉ để thực hiện một số khái niệm trừu tượng về mã chính xác. Nếu công cụ duy nhất bạn có là một cái búa thì mọi thứ trông giống như một cái đinh.


Đọc câu trả lời của graffic, có vẻ như mã lồng nhau được trình biên dịch tối ưu hóa theo cách mã được thực thi nhanh hơn so với sử dụng nhiều trả về. Tuy nhiên, ngay cả khi nhiều lần trả về nhanh hơn, việc tối ưu hóa này có thể sẽ không tăng tốc ứng dụng của bạn đến mức ... :)
hangy

1
Tôi không đồng ý - Luật đầu tiên là về việc giải thuật chính xác. Nhưng tất cả chúng ta đều là Kỹ sư, đó là về việc giải quyết vấn đề chính xác với các tài nguyên chúng ta có và làm trong ngân sách có sẵn. Một ứng dụng quá chậm không phù hợp với mục đích.
David Allan Finch
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.