Làm cách nào để tránh trả về vô dụng trong phương thức Java?


115

Tôi có một tình huống mà returntuyên bố lồng trong hai forvòng sẽ luôn luôn đạt được, theo lý thuyết.

Trình biên dịch không đồng ý và yêu cầu một returncâu lệnh bên ngoài forvòng lặp. Tôi muốn biết một cách thanh lịch để tối ưu hóa phương pháp này vượt quá sự hiểu biết hiện tại của tôi và không có bất kỳ nỗ lực thực hiện phá vỡ nào của tôi dường như hoạt động.

Đính kèm là một phương thức từ một phép gán tạo ra các số nguyên ngẫu nhiên và trả về các lần lặp được lặp đi lặp lại cho đến khi tìm thấy một số nguyên ngẫu nhiên thứ hai, được tạo trong một phạm vi được truyền vào phương thức dưới dạng tham số int.

private static int oneRun(int range) {
    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    rInt[0] = generator.nextInt(range); // Inital random number.

    for (int count = 1; count <= range; count++) { // Run until return.
        rInt[count] = generator.nextInt(range); // Add randint to current iteration.
        for (int i = 0; i < count; i++) { // Check for past occurence and return if found.
            if (rInt[i] == rInt[count]) {
                return count;
            }
        }
    }
    return 0; // Never reached
}

9
Nếu mục cuối cùng không bao giờ đạt được, thì bạn có thể sử dụng một while(true)vòng lặp thay vì chỉ mục. Điều này nói với trình biên dịch rằng vòng lặp sẽ không bao giờ trở lại.
nhện của

101
gọi hàm với phạm vi là 0 (hoặc bất kỳ số nào khác nhỏ hơn 1) ( oneRun(0)) và bạn thấy rằng bạn nhanh chóng đạt được không thể truy cập của mìnhreturn
mcfedr

55
Lợi nhuận đạt được khi phạm vi được cung cấp là âm. Bạn cũng có 0 xác nhận cho phạm vi đầu vào, vì vậy bạn hiện không nắm bắt được nó theo bất kỳ cách nào khác.
Hy vọng hữu ích

4
@Hop HopeHelpful Đó là câu trả lời thực sự, tất nhiên. Đó hoàn toàn không phải là một sự trở lại vô ích!
Ông Lister

4
@HopiouslyHelpful no, bởi vì nextIntném một ngoại lệ cho range < 0Trường hợp duy nhất đạt được lợi nhuận là khirange == 0
njzk2

Câu trả lời:


343

Các heuristic của trình biên dịch sẽ không bao giờ để bạn bỏ qua lần cuối return. Nếu bạn chắc chắn rằng nó sẽ không bao giờ đạt được, tôi sẽ thay thế nó bằng cách throwlàm cho tình huống rõ ràng hơn.

private static int oneRun(int range) {
    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    rInt[0] = generator.nextInt(range); // Inital random number.

    for (int count = 1; count <= range; count++) {
        ...
    }

    throw new AssertionError("unreachable code reached");
}

135
Không chỉ dễ đọc, mà còn đảm bảo bạn tìm ra nếu có gì đó không đúng với mã của bạn. Nó hoàn toàn hợp lý khi máy phát điện có thể có lỗi.
JollyJoker

6
Nếu bạn TUYỆT ĐỐI chắc chắn rằng mã của bạn không bao giờ có thể nhận được "mã không thể truy cập" đó thì hãy tạo một số thử nghiệm đơn vị cho tất cả các trường hợp cạnh có thể phát sinh (phạm vi là 0, phạm vi là -1, phạm vi là tối thiểu / tối đa). Nó có thể là một phương thức riêng tư bây giờ, nhưng nhà phát triển tiếp theo có thể không giữ nó như vậy. Cách bạn xử lý các giá trị không mong muốn (ném ngoại lệ, trả về giá trị lỗi, không làm gì) phụ thuộc vào cách bạn sẽ sử dụng phương thức. Theo kinh nghiệm của tôi, bạn thường chỉ muốn đăng nhập một lỗi và trả lại.
Rick Ryker

22
Có lẽ đó nên làthrow new AssertionError("\"unreachable\" code reached");
Bohemian

8
Tôi sẽ viết chính xác những gì tôi đưa vào câu trả lời của mình trong một chương trình sản xuất.
John Kugelman

1
Đối với ví dụ đã cho, có một cách tốt hơn để giải quyết hơn là chỉ cần thêm một throwthứ sẽ không bao giờ đạt được. Cách quá thường xuyên, mọi người chỉ muốn nhanh chóng áp dụng "một giải pháp để cai trị tất cả" mà không suy nghĩ quá nhiều về vấn đề tiềm ẩn. Nhưng lập trình không chỉ là mã hóa. Đặc biệt là khi nói đến thuật toán. Nếu bạn (cẩn thận) kiểm tra vấn đề đã cho, bạn sẽ nhận ra rằng bạn có thể tính ra lần lặp cuối cùng của forvòng lặp bên ngoài và do đó thay thế trả về "vô dụng" bằng một câu trả lời hữu ích (xem câu trả lời này ).
a_guest

36

Như @BoristheSpider đã chỉ ra, bạn có thể chắc chắn rằng returncâu lệnh thứ hai không thể truy cập được về mặt ngữ nghĩa:

private static int oneRun(int range) {
    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    int count = 0;

    while (true) {
        rInt[count] = generator.nextInt(range); // Add randint to current iteration.
        for (int i = 0; i < count; i++) { // Check for past occurence and return if found.
            if (rInt[i] == rInt[count]) {
                return count;
            }
        }
        count++;
    }
}

Biên dịch & chạy tốt. Và nếu bạn từng nhận được thì ArrayIndexOutOfBoundsExceptionbạn sẽ biết việc triển khai là sai về mặt ngữ nghĩa, mà không cần phải ném bất cứ thứ gì rõ ràng.


1
Chính xác: Điều kiện không thực sự được sử dụng để kiểm soát vòng lặp, vì vậy nó không nên ở đó. Tuy nhiên, tôi sẽ viết điều này như là for(int count = 0; true; count++)thay thế.
cmaster - phục hồi monica

3
@cmaster: bạn có thể bỏ qua true:for(int count = 0; ; count++) …
Holger

@Holger À. Tôi không chắc về điều đó bởi vì đây là về java và tôi đã không tiếp xúc với ngôn ngữ đó trong nhiều năm kể từ khi tôi tham gia nhiều hơn vào C / C ++. Vì vậy, khi tôi thấy việc sử dụng while(true), tôi đã nghĩ rằng có lẽ java nghiêm ngặt hơn một chút về vấn đề này. Rất vui được biết rằng không cần phải lo lắng ... Thật ra, tôi thích truephiên bản bỏ qua tốt hơn nhiều: Điều này cho thấy rõ ràng rằng hoàn toàn không có điều kiện để xem :-)
cmaster - phục hồi monica

3
Tôi không muốn chuyển sang "cho". "While-true" là một lá cờ rõ ràng rằng khối được dự kiến ​​sẽ lặp vô điều kiện, trừ khi và cho đến khi một cái gì đó bên trong phá vỡ nó. Một "cho" với một điều kiện bị bỏ qua không giao tiếp rõ ràng; nó có thể được bỏ qua dễ dàng hơn.
Corrodias

1
@ l0b0 Toàn bộ câu hỏi có thể tốt hơn cho việc xem xét mã khi xem xét cảnh báo có liên quan đến chất lượng mã.
Sulthan

18

Vì bạn đã hỏi về việc thoát ra khỏi hai forvòng, bạn có thể sử dụng nhãn để làm điều đó (xem ví dụ bên dưới):

private static int oneRun(int range) {
    int returnValue=-1;

    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    rInt[0] = generator.nextInt(range); // Inital random number.

    OUTER: for (int count = 1; count <= range; count++) { // Run until return.
        rInt[count] = generator.nextInt(range); // Add randint to current iteration.   
        for (int i = 0; i < count; i++) { // Check for past occurence and return if found.
            if (rInt[i] == rInt[count]) {
                returnValue = count;
                break OUTER;
            }
        }
    }
    return returnValue;
}

Điều này sẽ không được biên dịch bởi vì returnValuecó thể được sử dụng chưa được khởi tạo?
pts

1
Nhiều khả năng. Mục tiêu của bài viết là chỉ ra cách thoát ra khỏi cả hai vòng, như người đăng ban đầu đã hỏi về điều này. OP đã chắc chắn rằng việc thực thi sẽ không bao giờ kết thúc phương thức, vì vậy những gì bạn đã đưa lên có thể được sửa bằng cách khởi tạo returnValue thành bất kỳ giá trị nào khi nó được khai báo.
David Choweller

4
Không phải Nhãn là loại những thứ bạn nên tránh bằng mọi cách?
Serverfrog

2
Tôi tin rằng bạn đang nghĩ về gotos.
David Choweller

4
Nhãn trong một ngôn ngữ lập trình cấp cao (-ish). Hoàn toàn man rợ ...
xDaizu

13

Trong khi một khẳng định là một giải pháp nhanh chóng tốt. Nói chung loại vấn đề này có nghĩa là mã của bạn quá phức tạp. Khi tôi đang xem mã của bạn, rõ ràng là bạn không thực sự muốn một mảng giữ các số trước đó. Bạn muốn một Set:

Set<Integer> previous = new HashSet<Integer>();

int randomInt = generator.nextInt(range);
previous.add(randomInt);

for (int count = 1; count <= range; count++) {
    randomInt = generator.nextInt(range);
    if (previous.contains(randomInt)) {
       break;
    }

    previous.add(randomInt);
}

return previous.size();

Bây giờ lưu ý rằng những gì chúng tôi đang trả lại thực sự là kích thước của bộ. Độ phức tạp của mã đã giảm từ bậc hai xuống tuyến tính và nó ngay lập tức dễ đọc hơn.

Bây giờ chúng tôi có thể nhận ra rằng chúng tôi thậm chí không cần countchỉ số đó :

Set<Integer> previous = new HashSet<Integer>();

int randomInt = generator.nextInt(range);

while (!previous.contains(randomInt)) {          
    previous.add(randomInt);      
    randomInt = generator.nextInt(range);
}

return previous.size();

8

Vì giá trị trả về của bạn dựa trên biến của vòng ngoài, bạn có thể chỉ cần thay đổi điều kiện của vòng ngoài thành count < rangevà sau đó trả về giá trị cuối cùng này (mà bạn vừa bỏ qua) ở cuối hàm:

private static int oneRun(int range) {
    ...

    for (int count = 1; count < range; count++) {
        ...
    }
    return range;
}

Bằng cách này, bạn không cần phải giới thiệu mã sẽ không bao giờ đạt được.


Nhưng điều này có đòi hỏi phải thoát ra khỏi hai lớp vòng khi trận đấu được tìm thấy không? Tôi không thấy một cách để giảm xuống một cho câu lệnh, một số ngẫu nhiên mới cần được tạo và so sánh với số trước khi nó được tạo trước.
Oliver Benning

Bạn vẫn còn hai vòng lặp lồng nhau (tôi chỉ bỏ qua vòng thứ hai ...) nhưng vòng lặp bên ngoài đã bị giảm bởi lần lặp cuối cùng của nó. Trường hợp tương ứng với lần lặp cuối cùng này được xử lý riêng bởi câu lệnh return cuối cùng. Mặc dù đây là một giải pháp tao nhã cho ví dụ của bạn, có những kịch bản trong đó cách tiếp cận này trở nên khó đọc hơn; ví dụ, nếu giá trị trả về phụ thuộc vào vòng lặp bên trong thì - thay vì câu lệnh trả về đơn ở cuối - bạn sẽ được để lại một vòng lặp bổ sung (đại diện cho vòng lặp bên trong cho lần lặp cuối cùng của vòng lặp bên ngoài).
a_guest

5

Sử dụng biến tạm thời, ví dụ "kết quả" và loại bỏ trả về bên trong. Thay đổi vòng lặp for cho một vòng lặp while với điều kiện thích hợp. Đối với tôi, nó luôn thanh lịch hơn khi chỉ có một lần trở lại là câu lệnh cuối cùng của hàm.


Vâng, trông giống như bên trong nếu có thể là bên ngoài trong khi điều kiện. Hãy nhớ rằng luôn có một khoảng thời gian tương đương với một (và thanh lịch hơn vì bạn không có kế hoạch luôn luôn trải qua tất cả các lần lặp lại). Hai cái lồng cho các vòng lặp có thể được đơn giản hóa cho chắc chắn. Tất cả các mã đó có mùi "quá phức tạp".
David

@ Solomonoff'sSecret Tuyên bố của bạn là vô lý. Nó ngụ ý chúng ta nên "nói chung" nhằm mục đích cho hai hoặc nhiều báo cáo hoàn trả. Hấp dẫn.
David

@ Solomonoff'sSecret Đó là vấn đề của các phong cách lập trình ưa thích và bất kể tuần này có gì hay ho vào tuần trước, vẫn có những ưu điểm nhất định khi chỉ có một điểm thoát và ở cuối phương thức (chỉ cần nghĩ đến một vòng lặp với nhiều người return resultrắc rắc xung quanh nó và sau đó nghĩ đến việc muốn thực hiện thêm một lần kiểm tra tìm thấy resulttrước khi trả lại, và đây chỉ là một ví dụ). Cũng có thể có những khuyết điểm nhưng một tuyên bố rộng như "Nói chung, không có lý do chính đáng để chỉ có một lần trở lại" không giữ được nước.
SantiBailors

@David Hãy để tôi nói lại: không có lý do chung để chỉ có một lần trở lại. Trong các trường hợp cụ thể, đó có thể là lý do nhưng điển hình là sự sùng bái hàng hóa ở mức tồi tệ nhất.
Phục hồi Monica

1
Đây là một vấn đề về những gì làm cho mã dễ đọc hơn. Nếu trong đầu bạn nói "nếu là cái này, hãy trả lại những gì chúng ta đã tìm thấy, nếu không thì hãy tiếp tục; nếu chúng ta không tìm thấy thứ gì đó trả lại giá trị khác này". Sau đó, mã của bạn nên có hai giá trị trả về. Luôn luôn mã những gì làm cho nó dễ hiểu hơn ngay lập tức cho nhà phát triển tiếp theo. Trình biên dịch chỉ có một điểm trả về từ một phương thức Java ngay cả khi trong mắt chúng ta, nó trông giống như hai.
Rick Ryker

3

Có lẽ đây là một dấu hiệu cho thấy bạn nên viết lại mã của mình. Ví dụ:

  1. Tạo một mảng các số nguyên 0 .. phạm vi-1. Đặt tất cả các giá trị thành 0.
  2. Thực hiện một vòng lặp. Trong vòng lặp, tạo một số ngẫu nhiên. Nhìn vào danh sách của bạn, tại chỉ mục đó, để xem giá trị là 1 Nếu có, hãy thoát ra khỏi vòng lặp. Mặt khác, đặt giá trị tại chỉ mục đó thành 1
  3. Đếm số lượng 1 trong danh sách và trả về giá trị đó.

3

Các phương thức có câu lệnh return và có vòng lặp / vòng lặp bên trong chúng luôn yêu cầu câu lệnh return bên ngoài (các) vòng lặp. Ngay cả khi tuyên bố này bên ngoài vòng lặp không bao giờ đạt được. Trong các trường hợp như vậy, để tránh các câu lệnh trả về không cần thiết, bạn có thể xác định một biến có kiểu tương ứng, một số nguyên trong trường hợp của bạn, ở đầu phương thức tức là trước và ngoài (các) vòng lặp tương ứng. Khi đạt được kết quả mong muốn bên trong vòng lặp, bạn có thể gán giá trị tương ứng cho biến được xác định trước này và sử dụng nó cho câu lệnh return bên ngoài vòng lặp.

Vì bạn muốn phương thức của mình trả về kết quả đầu tiên khi rInt [i] bằng rInt [Count], nên chỉ thực hiện biến đã đề cập ở trên là không đủ vì phương thức sẽ trả về kết quả cuối cùng khi rInt [i] bằng rInt [Count]. Một lựa chọn là thực hiện hai "tuyên bố phá vỡ" được gọi khi chúng ta có kết quả mong muốn. Vì vậy, phương thức sẽ trông giống như thế này:

private static int oneRun(int range) {

        int finalResult = 0; // the above-mentioned variable
        int[] rInt = new int[range + 1];
        rInt[0] = generator.nextInt(range);

        for (int count = 1; count <= range; count++) {
            rInt[count] = generator.nextInt(range);
            for (int i = 0; i < count; i++) {
                if (rInt[i] == rInt[count]) {
                    finalResult = count;
                    break; // this breaks the inside loop
                }
            }
            if (finalResult == count) {
                break; // this breaks the outside loop
            }
        }
        return finalResult;
    }

2

Tôi đồng ý rằng người ta nên ném một ngoại lệ nơi xảy ra tuyên bố không thể truy cập được. Chỉ muốn cho thấy làm thế nào cùng một phương thức có thể làm điều này theo cách dễ đọc hơn (yêu cầu java 8 luồng).

private static int oneRun(int range) {
    int[] rInt = new int[range + 1];
    return IntStream
        .rangeClosed(0, range)
        .peek(i -> rInt[i] = generator.nextInt(range))
        .filter(i -> IntStream.range(0, i).anyMatch(j -> rInt[i] == rInt[j]))
        .findFirst()
        .orElseThrow(() -> new RuntimeException("Shouldn't be reached!"));
}

-1
private static int oneRun(int range) {
    int result = -1; // use this to store your result
    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    rInt[0] = generator.nextInt(range); // Inital random number.

    for (int count = 1; count <= range && result == -1; count++) { // Run until result found.
        rInt[count] = generator.nextInt(range); // Add randint to current iteration.   
        for (int i = 0; i < count && result == -1; i++) { // Check for past occurence and leave after result found.
            if (rInt[i] == rInt[count]) {
                result = count;
            }
        }
    }
    return result; // return your result
}

Mã này cũng không hiệu quả vì một sẽ làm rất nhiều các result == -1tờ séc có thể bỏ qua với các returnbên trong vòng lặp ...
Willem Van Onsem
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.