Đảo ngược một câu lệnh IF


39

Vì vậy, tôi đã lập trình được vài năm nay và gần đây đã bắt đầu sử dụng ReSharper nhiều hơn. Một điều mà ReSharper luôn gợi ý cho tôi là "đảo ngược câu lệnh 'nếu' để giảm lồng nhau".

Hãy nói rằng tôi có mã này:

foreach (someObject in someObjectList) 
{
    if(someObject != null) 
    {
        someOtherObject = someObject.SomeProperty;
    }
}

Và ReSharper sẽ đề nghị tôi làm điều này:

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;
}

Có vẻ như ReSharper sẽ LUÔN đề nghị tôi đảo ngược IF, bất kể việc lồng nhau đang diễn ra như thế nào. Vấn đề này là tôi giống như làm tổ trong ít nhất MỘT SỐ tình huống. Đối với tôi có vẻ dễ đọc hơn và tìm hiểu những gì đang diễn ra ở một số nơi. Điều này không phải lúc nào cũng đúng, nhưng đôi khi tôi cảm thấy thoải mái hơn khi làm tổ.

Câu hỏi của tôi là: ngoài sở thích cá nhân hoặc khả năng đọc, có lý do để giảm lồng không? Có một sự khác biệt hiệu suất hoặc một cái gì đó khác mà tôi có thể không nhận thức được?


1
Bản sao có thể có của Giữ mức thụt thấp ở mức thấp
gnat

24
Điều quan trọng ở đây là "Đề xuất chia sẻ lại". Hãy xem gợi ý, nếu nó làm cho mã dễ đọc hơn và nhất quán, hãy sử dụng nó. Nó làm điều tương tự với for / cho mỗi vòng lặp.
Jon Raynor

Tôi thường giảm bớt những gợi ý chia sẻ lại, tôi không luôn theo dõi, vì vậy chúng không xuất hiện ở thanh bên nữa.
CodeInChaos

1
ReSharper đã loại bỏ tiêu cực, đó thường là một điều tốt. Tuy nhiên, nó không đề xuất một điều kiện ternary có thể phù hợp độc đáo ở đây, nếu khối thực sự đơn giản như ví dụ của bạn.
vào

2
Không ReSharper cũng đề nghị chuyển điều kiện vào truy vấn? foreach (someObject in someObjectList.Where (object => object! = null)) {...}
Roman Reiner

Câu trả lời:


63

Nó phụ thuộc. Trong ví dụ của bạn có ifcâu lệnh không đảo ngược không phải là vấn đề, bởi vì phần thân của ifnó rất ngắn. Tuy nhiên, bạn thường muốn đảo ngược các tuyên bố khi cơ thể phát triển.

Chúng ta chỉ là con người và giữ nhiều thứ cùng một lúc trong đầu thật khó khăn.

Khi phần thân của câu lệnh lớn, việc đảo ngược câu lệnh không chỉ làm giảm lồng mà còn cho nhà phát triển biết họ có thể quên mọi thứ bên trên câu lệnh đó và chỉ tập trung vào phần còn lại. Bạn rất có thể sử dụng các câu lệnh đảo ngược để loại bỏ các giá trị sai càng sớm càng tốt. Một khi bạn biết những thứ đó nằm ngoài trò chơi, điều duy nhất còn lại là các giá trị thực sự có thể được xử lý.


64
Tôi thích nhìn thấy làn sóng ý kiến ​​của nhà phát triển theo cách này. Có rất nhiều tổ trong mã đơn giản chỉ vì có một ảo tưởng phổ biến phi thường break, continuevà nhiều returnkhông có cấu trúc.
JimmyJames

6
rắc rối đã bị phá vỡ, v.v. vẫn là dòng kiểm soát mà bạn phải giữ trong đầu 'điều này không thể là x hoặc nó sẽ thoát khỏi vòng lặp ở trên cùng' có thể không cần suy nghĩ thêm nếu kiểm tra null sẽ ném ngoại lệ. Nhưng nó không tốt lắm khi nhiều sắc thái hơn 'chỉ chạy một nửa mã này cho trạng thái này vào thứ năm'
Ewan

2
@Ewan: Sự khác biệt giữa 'cái này không thể là x hoặc nó sẽ thoát khỏi vòng lặp ở trên cùng' và 'đây không thể là x hoặc nó sẽ không vào khối này'?
Collin

không có gì là phức tạp và ý nghĩa của x là quan trọng
Ewan

4
@Ewan: Tôi nghĩ break/ continue/ returncó lợi thế thực sự so với một elsekhối. Với việc ra ngoài sớm, có 2 khối : khối thoát ra và khối ở đó; với else3 khối : nếu, khác, và bất cứ điều gì xảy ra sau khi (được sử dụng bất cứ chi nhánh thực hiện). Tôi thích có ít khối hơn.
Matthieu M.

33

Đừng tránh tiêu cực. Con người mút như phân tích chúng ngay cả khi CPU yêu thích chúng.

Tuy nhiên, hãy tôn trọng thành ngữ. Con người chúng ta là sinh vật của thói quen vì vậy chúng tôi thích những con đường mòn hơn là những con đường trực tiếp đầy cỏ dại.

foo != nullthật đáng buồn, đã trở thành một thành ngữ. Tôi hầu như không nhận thấy các phần riêng biệt nữa. Tôi nhìn vào đó và chỉ nghĩ, "ồ, an toàn để chấm dứt foongay bây giờ".

Nếu bạn muốn lật ngược điều đó (ồ, một ngày nào đó, làm ơn) bạn cần một trường hợp thực sự mạnh mẽ. Bạn cần một cái gì đó sẽ khiến mắt tôi dễ dàng trượt khỏi nó và hiểu nó ngay lập tức.

Tuy nhiên, những gì bạn đã cho chúng tôi là:

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;
}

Mà thậm chí không giống nhau về mặt cấu trúc!

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;

    if(someGlobalObject == null) continue;
    someSideEffectObject = someGlobalObject.SomeProperty;
}

Đó không phải là điều mà bộ não lười biếng của tôi mong đợi nó sẽ làm. Tôi nghĩ rằng tôi có thể tiếp tục thêm mã độc lập nhưng tôi không thể. Điều này khiến tôi nghĩ về những điều tôi không muốn nghĩ về bây giờ. Bạn đã phá vỡ thành ngữ của tôi nhưng không cho tôi một cái tốt hơn. Tất cả chỉ vì bạn muốn bảo vệ tôi khỏi một điều tiêu cực đã thiêu rụi cái tôi nhỏ bé khó chịu vào tâm hồn tôi.

Rất tiếc, có thể ReSharper đã đúng khi đề xuất 90% thời gian này nhưng trong kiểm tra null tôi nghĩ bạn đã tìm thấy một trường hợp góc mà nó bị bỏ qua tốt nhất. Các công cụ đơn giản là không tốt trong việc dạy cho bạn biết mã nào có thể đọc được. Hỏi một con người. Làm một đánh giá ngang hàng. Nhưng đừng mù quáng làm theo công cụ.


1
Chà, nếu bạn có nhiều mã hơn trong vòng lặp, cách thức hoạt động của nó phụ thuộc vào cách tất cả chúng kết hợp với nhau. Nó không phải là mã độc lập. Mã được cấu trúc lại trong câu hỏi là chính xác cho ví dụ, nhưng có thể không chính xác khi không bị cô lập - đó có phải là điểm bạn sẽ làm không? (+1 cho dù không tránh tiêu cực)
Baldrickk

@Baldrickk if (foo != null){ foo.something() }cho phép tôi tiếp tục thêm mã mà không cần quan tâm nếu fookhông có giá trị. Điều này không. Ngay cả khi tôi không cần phải làm điều đó bây giờ tôi cũng không cảm thấy có động lực để vặn vẹo tương lai bằng cách nói "tốt, họ chỉ có thể tái cấu trúc nó nếu họ cần điều đó". Feh. Phần mềm chỉ mềm nếu dễ thay đổi.
candied_orange

4
"tiếp tục thêm mã mà không quan tâm" Hoặc mã phải liên quan (nếu không, có lẽ nên tách riêng) hoặc cần chú ý để hiểu chức năng của chức năng / khối bạn đang sửa đổi vì thêm bất kỳnào có khả năng thay đổi hành vi ngoài dự định đó Đối với những trường hợp đơn giản như thế này, tôi không nghĩ nó cũng quan trọng. Đối với các trường hợp phức tạp hơn, tôi mong muốn hiểu mã sẽ được ưu tiên, vì vậy tôi sẽ chọn bất cứ điều gì tôi nghĩ là rõ ràng nhất, có thể là phong cách.
Baldrickk

liên quan và đan xen không giống nhau.
candied_orange

1
Đừng tránh những tiêu cực: D
VitalyB 28/07/17

20

Đó là lập luận cũ về việc phá vỡ có tệ hơn khi làm tổ không.

Mọi người dường như chấp nhận trả về sớm để kiểm tra tham số, nhưng nó cau mày với phần lớn phương thức.

Cá nhân tôi tránh tiếp tục, phá vỡ, goto vv ngay cả khi nó có nghĩa là làm tổ nhiều hơn. Nhưng trong hầu hết các trường hợp, bạn có thể tránh cả hai.

foreach (someObject in someObjectList.where(i=>i != null))
{
    someOtherObject = someObject.SomeProperty;
}

Rõ ràng việc làm tổ không làm cho mọi thứ khó theo dõi khi bạn có nhiều lớp

foreach (someObject in someObjectList) 
{
    if(someObject != null) 
    {
        someOtherObject = someObject.SomeProperty;
        if(someObject.x > 5) 
        {
            x ++;
            if(someObject.y > 5) 
            {
                x ++;
            }
        }
    }
}

Điều tồi tệ nhất là khi bạn có cả hai

foreach (someObject in someObjectList) 
{
    if(someObject != null) 
    {
        someOtherObject = someObject.SomeProperty;
        if(someObject.x > 5) 
        {
            x ++;
            if(someObject.y > 5) 
            {
                x ++;
                return;
            }
            continue;
        }
        someObject.z --;
        if(myFunctionWithSideEffects(someObject) > 6) break;
    }
}

11
Yêu dòng này: foreach (subObject in someObjectList.where(i=>i != null))Không biết tại sao tôi chưa bao giờ nghĩ về điều gì đó như thế.
Barry Franklin

@BarryFranklin ReSharper nên có một gạch dưới chấm màu xanh lá cây trên foreach với "Chuyển đổi sang biểu thức LINQ" là gợi ý sẽ làm điều đó cho bạn. Tùy thuộc vào hoạt động, nó có thể thực hiện các phương thức linq khác như Sum hoặc Max.
Kevin Phí

@BarryFranklin Đây có thể là một đề xuất bạn muốn đặt ở mức thấp và chỉ sử dụng tùy ý. Mặc dù nó hoạt động tốt đối với các trường hợp tầm thường như ví dụ này, tôi thấy trong mã phức tạp hơn theo cách nó chọn một phần có điều kiện phức tạp hơn thành các bit có thể xác định được và phần còn lại có xu hướng tạo ra những thứ khó hiểu hơn nhiều so với bản gốc. Nói chung, tôi ít nhất sẽ thử đề xuất vì khi nó có thể chuyển đổi toàn bộ vòng lặp thành Linq, kết quả thường hợp lý; nhưng kết quả một nửa và một nửa có xu hướng nhận được IMO nhanh xấu xí.
Dan Neely

Trớ trêu thay, bản chỉnh sửa bằng cách chia sẻ lại có cùng mức độ lồng nhau, bởi vì nó cũng có một phần tiếp theo là một lớp lót.
Peter

heh, không có niềng răng mặc dù! rõ ràng là được phép: /
Ewan

6

Vấn đề với continuelà nếu bạn thêm nhiều dòng vào mã, logic sẽ trở nên phức tạp và có khả năng chứa lỗi. ví dụ

foreach (someObject in someObjectList)
{
    if(someObject == null) continue;
    someOtherObject = someObject.SomeProperty;

    ...  imagine that there is some code here ...

    // added later by a Junior Developer
    doSomeOtherFunction(someObject);
}

Bạn có muốn gọi doSomeOtherFunction()cho tất cả someObject, hoặc chỉ khi không null? Với mã gốc, bạn có tùy chọn và nó rõ ràng hơn. Với continuemột người có thể quên "oh, vâng, ở đó chúng tôi đã bỏ qua null" và bạn thậm chí không có tùy chọn để gọi nó cho tất cả một số Dự án, trừ khi bạn thực hiện cuộc gọi đó khi bắt đầu phương thức có thể không thực hiện được hoặc không chính xác vì những lý do khác

Vâng, rất nhiều lồng là xấu xí và có thể gây nhầm lẫn, nhưng trong trường hợp này tôi sử dụng phong cách truyền thống.

Câu hỏi thưởng: Tại sao có null trong danh sách đó để bắt đầu? Có thể tốt hơn là không bao giờ thêm null ở vị trí đầu tiên, trong trường hợp đó, logic xử lý đơn giản hơn nhiều.


12
Không nên có đủ mã bên trong vòng lặp mà bạn không thể thấy kiểm tra null ở đầu vòng mà không cuộn.
Bryan Boettcher

@Bryan Boettcher đồng ý, nhưng không phải tất cả các mã đều được viết tốt như vậy. Và thậm chí một vài dòng mã phức tạp có thể khiến người ta quên đi (hoặc hiểu sai về) tiếp tục. Trong thực tế, tôi ổn với returnlý do chúng tạo thành một "phá vỡ sạch", nhưng IMO breakcontinuedẫn đến sự nhầm lẫn và, trong hầu hết các trường hợp, tốt nhất nên tránh.
dùng949300

4
@ user949300 Sau mười năm phát triển, tôi chưa bao giờ thấy bị phá vỡ hoặc tiếp tục gây nhầm lẫn. Tôi đã có họ liên quan đến sự nhầm lẫn nhưng họ chưa bao giờ là nguyên nhân. Thông thường, các quyết định kém về nhà phát triển là nguyên nhân và chúng sẽ gây ra sự nhầm lẫn cho dù họ sử dụng loại vũ khí nào.
corsiKa

1
@corsiKa Lỗi Apple SSL thực sự là một lỗi goto, nhưng có thể dễ dàng bị lỗi hoặc tiếp tục lỗi. Tuy nhiên, mã này thật kinh khủng ...
user949300

3
@BryanBoettcher vấn đề đối với tôi có vẻ như các nhà phát triển cùng nghĩ rằng tiếp tục hoặc trả lại nhiều lần cũng ok, nghĩ rằng chôn họ trong các cơ quan phương pháp lớn cũng ok. Vì vậy, mọi kinh nghiệm tôi có với tiếp tục / nhiều lần trả lại đều nằm trong mớ hỗn độn mã lớn.
Andy

3

Ý tưởng là sự trở lại sớm. Sớm returntrong một vòng lặp trở thành sớm continue.

Rất nhiều câu trả lời hay cho câu hỏi này: Tôi nên trở về từ một chức năng sớm hay sử dụng câu lệnh if?

Lưu ý rằng trong khi câu hỏi được đóng lại dựa trên ý kiến, 430+ phiếu ủng hộ trả lại sớm, ~ 50 phiếu là "nó phụ thuộc" và 8 là chống lại trả lại sớm. Vì vậy, có một sự đồng thuận đặc biệt mạnh mẽ (hoặc điều đó, hoặc tôi rất tệ trong việc đếm, điều đó là hợp lý).

Cá nhân, nếu thân vòng lặp có thể tiếp tục sớm và đủ lâu để nó có vấn đề (3-4 dòng), tôi có xu hướng trích xuất thân vòng lặp thành một chức năng nơi tôi sử dụng trả về sớm thay vì tiếp tục sớm.


2

Kỹ thuật này rất hữu ích để xác nhận các điều kiện trước khi phần lớn phương thức của bạn xảy ra khi các điều kiện đó được đáp ứng.

Chúng tôi thường xuyên đảo ngược ifscông việc của tôi và sử dụng một bóng ma khác. Nó thường được sử dụng khá thường xuyên khi xem xét PR tôi có thể chỉ ra làm thế nào đồng nghiệp của tôi có thể loại bỏ ba cấp độ lồng nhau! Nó xảy ra ít thường xuyên hơn kể từ khi chúng tôi tung ra ifs của mình.

Dưới đây là một ví dụ đồ chơi của một cái gì đó có thể được tung ra

function foobar(x, y, z) {
    if (x > 0) {
        if (z != null && isGamma(y)) {
            // ~10 lines of code
            // return something
        }
        else {
           return y
        }
    }
    else {
      return y + z
    }
}

Mã như thế này, trong đó có MỘT trường hợp thú vị (trong đó phần còn lại chỉ đơn giản là trả về hoặc khối lệnh nhỏ) là phổ biến. Nó là tầm thường để viết lại như trên

function foobar(x, y, z) {
    if (x <=0) {
        return y + z
    }
    if (z == null || !isGamma(y) {
       return y
    }
    // ~ 10 lines of code
    // return something
}

Ở đây, mã quan trọng của chúng tôi, 10 dòng không bao gồm, không được thụt ba trong hàm. Nếu bạn có kiểu đầu tiên, hoặc bạn muốn cấu trúc lại khối dài thành một phương thức hoặc chịu đựng sự thụt lề. Đây là nơi tiết kiệm. Ở một nơi, đây là một khoản tiết kiệm không đáng kể nhưng qua nhiều phương thức trong nhiều lớp, bạn tiết kiệm được nhu cầu tạo một hàm bổ sung để làm cho mã sạch hơn và bạn không có nhiều mức độ ghê gớm thụt đầu dòng .


Đáp lại mẫu mã của OP, tôi đồng ý rằng đó là sự sụp đổ quá mức. Đặc biệt là trong 1TB, kiểu tôi sử dụng, đảo ngược làm cho mã tăng kích thước.


0

Đề xuất chia sẻ lại thường hữu ích nhưng phải luôn được đánh giá nghiêm túc. Nó tìm kiếm một số mẫu mã được xác định trước và đề xuất các phép biến đổi, nhưng nó không thể tính đến tất cả các yếu tố làm cho mã ít nhiều có thể đọc được cho con người.

Tất cả những thứ khác đều bằng nhau, ít làm tổ hơn là tốt hơn, đó là lý do tại sao ReSharper sẽ đề xuất một phép biến đổi làm giảm việc lồng. Nhưng tất cả những thứ khác không bằng nhau: Trong trường hợp này, phép biến đổi yêu cầu giới thiệu a continue, có nghĩa là mã kết quả thực sự khó theo dõi hơn.

Trong một số trường hợp, việc trao đổi một mức lồng nhau để thực sự continue có thể là một sự cải tiến, nhưng điều này phụ thuộc vào ngữ nghĩa của mã được đề cập, vượt quá sự hiểu biết về các quy tắc cơ học của ReSharpers.

Trong trường hợp của bạn, đề xuất ReSharper sẽ làm cho mã tồi tệ hơn. Vì vậy, chỉ cần bỏ qua nó. Hoặc tốt hơn: Đừng sử dụng chuyển đổi được đề xuất, nhưng hãy suy nghĩ một chút về cách cải thiện mã. Lưu ý rằng bạn có thể gán cùng một biến nhiều lần, nhưng chỉ có phép gán cuối cùng có hiệu lực, vì trước đó sẽ bị ghi đè. Điều này trông giống như một lỗi. Đề xuất ReSharpers không sửa lỗi, nhưng nó làm cho rõ ràng hơn một chút rằng logic đáng ngờ đang diễn ra.


0

Tôi nghĩ rằng điểm lớn hơn ở đây là mục đích của vòng lặp quyết định cách tốt nhất để tổ chức dòng chảy trong vòng lặp. Nếu bạn đang lọc ra một số yếu tố trước khi bạn lặp lại các yếu tố còn lại, thì việc continuera sớm có ý nghĩa. Tuy nhiên, nếu bạn đang thực hiện các loại xử lý khác nhau trong một vòng lặp, có thể có ý nghĩa hơn để làm cho điều đó ifthực sự rõ ràng, chẳng hạn như:

for(obj in objectList) {
    if(obj == null) continue;
    if(obj.getColour().equals(Color.BLUE)) {
        // Handle blue objects
    } else {
        // Handle non-blue objects
    }
}

Lưu ý rằng trong Luồng Java hoặc các thành ngữ chức năng khác, bạn có thể tách bộ lọc khỏi vòng lặp, bằng cách sử dụng:

items.stream()
    .filter(i -> (i != null))
    .filter(i -> i.getColour().equals(Color.BLUE))
    .forEach(i -> {
        // Process blue objects
    });

Ngẫu nhiên, đây là một trong những điều tôi thích về Perl bị đảo ngược nếu ( unless) được áp dụng cho các câu lệnh đơn, cho phép loại mã sau đây, mà tôi thấy rất dễ đọc:

foreach my $item (@items) {
    next unless defined $item;
    carp "Only blue items are supported! Failed on item $item" 
       unless ($item->get_colour() eq 'blue');
    ...
}
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.