Bagaimana cara menghindari pengembalian yang tidak berguna dalam metode Java?


115

Saya memiliki situasi di mana returnpernyataan yang bersarang dalam dua forloop akan selalu tercapai, secara teoritis.

Kompilator tidak setuju dan membutuhkan returnpernyataan di luar forloop. Saya ingin mengetahui cara elegan untuk mengoptimalkan metode ini yang berada di luar pemahaman saya saat ini, dan tidak ada upaya penerapan break yang berhasil saya lakukan.

Terlampir adalah metode dari penugasan yang menghasilkan bilangan bulat acak dan mengembalikan iterasi yang melalui siklus hingga bilangan bulat acak kedua ditemukan, yang dihasilkan dalam rentang yang diteruskan ke metode sebagai parameter 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
Jika item terakhir tidak pernah tercapai, maka Anda dapat menggunakan while(true)daripada loop yang diindeks. Ini memberi tahu kompiler bahwa loop tidak akan pernah kembali.
Boris the Spider

101
panggil fungsi dengan rentang sebagai 0 (atau angka lain yang kurang dari 1) ( oneRun(0)) dan Anda melihat bahwa Anda dengan cepat mencapai tak terjangkau Andareturn
mcfedr

55
Pengembalian tercapai ketika kisaran yang diberikan negatif. Anda juga memiliki 0 validasi untuk rentang input, jadi saat ini Anda tidak menangkapnya dengan cara lain.
Semoga

4
@HopefullyHelpful Itu adalah jawaban yang sebenarnya, tentu saja. Ini sama sekali bukan pengembalian yang tidak berguna!
Tuan Lister

4
@HopefullyHelpful tidak, karena nextIntmelempar pengecualian untuk range < 0Satu-satunya kasus di mana pengembalian tercapai adalah ketikarange == 0
njzk2

Jawaban:


343

Heuristik kompiler tidak akan pernah membiarkan Anda menghilangkan yang terakhir return. Jika Anda yakin itu tidak akan pernah tercapai, saya akan menggantinya dengan a throwuntuk memperjelas situasinya.

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
Tidak hanya keterbacaan, tetapi juga memastikan Anda mengetahui apakah ada yang salah dengan kode Anda. Sangat masuk akal jika generator memiliki bug.
JollyJoker

6
Jika Anda BENAR-BENAR yakin bahwa kode Anda tidak akan pernah bisa sampai ke "kode yang tidak dapat dijangkau" maka buat beberapa pengujian unit untuk semua kasus tepi yang dapat muncul (kisaran adalah 0, kisaran -1, kisaran min / max int). Ini mungkin metode pribadi sekarang, tetapi pengembang berikutnya mungkin tidak menyimpannya seperti itu. Bagaimana Anda menangani nilai yang tidak diharapkan (melempar pengecualian, mengembalikan nilai kesalahan, tidak melakukan apa-apa) bergantung pada bagaimana Anda akan menggunakan metode ini. Dalam pengalaman saya, Anda biasanya hanya ingin mencatat kesalahan dan kembali.
Rick Ryker

22
Mungkin itu seharusnyathrow new AssertionError("\"unreachable\" code reached");
Bohemian

8
Saya akan menulis dengan tepat apa yang saya masukkan dalam jawaban saya dalam sebuah program produksi.
John Kugelman

1
Untuk contoh yang diberikan ada cara yang lebih baik untuk menangani daripada hanya menambahkan throwyang tidak akan pernah tercapai. Terlalu sering orang hanya ingin cepat menerapkan "satu solusi untuk mengatur semuanya" tanpa terlalu memikirkan masalah yang mendasarinya. Tetapi pemrograman lebih dari sekadar pengkodean. Terutama jika menyangkut algoritme. Jika Anda (hati-hati) memeriksa masalah yang diberikan, Anda akan menyadari bahwa Anda dapat memfaktorkan keluar iterasi terakhir dari forloop luar dan karenanya mengganti pengembalian "tidak berguna" dengan yang berguna (lihat jawaban ini ).
tamu

36

Seperti yang ditunjukkan @BoristheSpider, Anda dapat memastikan bahwa returnpernyataan kedua secara semantik tidak dapat dijangkau:

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

Mengompilasi & berjalan dengan baik. Dan jika Anda pernah mendapatkannya, ArrayIndexOutOfBoundsExceptionAnda akan tahu bahwa penerapannya salah secara semantik, tanpa harus secara eksplisit membuang apa pun.


1
Persis: Kondisi sebenarnya tidak digunakan untuk mengontrol loop, jadi seharusnya tidak ada. Namun, saya akan menulis ini sebagai for(int count = 0; true; count++)gantinya.
cmaster

3
@cmaster: Anda dapat menghilangkan true:for(int count = 0; ; count++) …
Holger

@Holger Ah. Saya tidak yakin tentang itu karena ini tentang java, dan saya belum menyentuh bahasa itu selama bertahun-tahun karena saya lebih menyukai C / C ++. Jadi, ketika saya melihat penggunaan while(true), saya pikir mungkin java sedikit lebih ketat dalam hal ini. Senang mengetahui bahwa tidak perlu khawatir ... Sebenarnya, saya lebih menyukai trueversi yang dihilangkan : Ini membuatnya jelas secara visual bahwa sama sekali tidak ada syarat untuk dilihat :-)
cmaster - kembalikan monica

3
Saya lebih suka tidak beralih ke "untuk". A "while-true" adalah tanda yang jelas bahwa blok diharapkan untuk mengulang tanpa syarat, kecuali dan sampai sesuatu di dalamnya memecahnya. A "untuk" dengan kondisi yang dihilangkan tidak berkomunikasi dengan jelas; itu bisa diabaikan dengan lebih mudah.
Corrodias

1
@ l0b0 Seluruh pertanyaan mungkin akan lebih baik untuk tinjauan kode mengingat peringatan terkait dengan kualitas kode.
Sulthan

18

Karena Anda bertanya tentang keluar dari dua forloop, Anda dapat menggunakan label untuk melakukannya (lihat contoh di bawah):

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

Bukankah ini gagal untuk dikompilasi karena returnValuedapat digunakan tanpa inisialisasi?
poin

1
Yang paling disukai. Objek posting adalah untuk menunjukkan bagaimana untuk keluar dari kedua loop, seperti yang ditanyakan oleh poster asli tentang ini. OP yakin bahwa eksekusi tidak akan pernah sampai ke akhir metode, jadi apa yang Anda kemukakan dapat diperbaiki dengan menginisialisasi returnValue ke nilai apa pun saat dideklarasikan.
David Choweller

4
Bukankah Label adalah hal yang sebaiknya Anda hindari dengan cara apa pun?
Serverfrog

2
Saya yakin Anda sedang memikirkan gotos.
David Choweller

4
Label dalam bahasa pemrograman tingkat tinggi (-ish). Benar-benar biadab ...
xDaizu

13

Sedangkan assert adalah solusi cepat yang bagus. Secara umum, masalah seperti ini berarti kode Anda terlalu rumit. Ketika saya melihat kode Anda, jelas bahwa Anda tidak benar-benar ingin array menyimpan nomor sebelumnya. Anda menginginkan 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();

Sekarang perhatikan bahwa apa yang kita kembalikan sebenarnya adalah ukuran set. Kompleksitas kode telah menurun dari kuadratik menjadi linier dan segera lebih mudah dibaca.

Sekarang kita dapat menyadari bahwa kita bahkan tidak membutuhkan countindeks itu:

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

Karena nilai kembalian Anda didasarkan pada variabel loop luar, Anda cukup mengubah kondisi loop luar menjadi count < rangedan kemudian mengembalikan nilai terakhir ini (yang baru saja Anda hilangkan) di akhir fungsi:

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

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

Dengan cara ini Anda tidak perlu memasukkan kode yang tidak akan pernah bisa dijangkau.


Tapi bukankah ini membutuhkan istirahat dari dua lapisan loop saat pertandingan ditemukan? Saya tidak melihat cara untuk mengurangi menjadi satu untuk pernyataan, nomor acak baru perlu dibuat dan dibandingkan dengan yang sebelumnya sebelum yang lain dibuat.
Oliver Benning

Anda masih tersisa dengan dua loop bersarang (saya baru saja menghilangkan yang kedua ...) tetapi loop luar telah dikurangi dengan iterasi terakhirnya. Kasus yang sesuai dengan iterasi terakhir ini ditangani secara terpisah oleh pernyataan pengembalian terakhir. Meskipun ini adalah solusi yang bagus untuk contoh Anda, ada skenario di mana pendekatan ini menjadi lebih sulit untuk dibaca; jika misalnya nilai kembalian bergantung pada pengulangan dalam maka - alih-alih pernyataan pengembalian tunggal di akhir - Anda akan mendapatkan pengulangan tambahan (mewakili pengulangan dalam untuk pengulangan terakhir pengulangan luar).
Tamu

5

Gunakan variabel temp, misalnya "hasil", dan hapus hasil dalam. Ubah loop for untuk loop sementara dengan kondisi yang tepat. Bagi saya itu selalu lebih elegan untuk memiliki hanya satu pengembalian sebagai pernyataan terakhir dari fungsi tersebut.


Nah, sepertinya bagian paling dalam kalau bisa jadi kondisi luar sementara. Perlu diingat bahwa selalu ada waktu yang setara dengan untuk (dan lebih elegan karena Anda tidak berencana untuk selalu melakukan semua iterasi). Kedua loop bersarang untuk dapat disederhanakan dengan pasti. Semua kode itu berbau "terlalu rumit".
David

@Solomonoff'sSecret Pernyataan Anda tidak masuk akal. Ini menyiratkan kita harus "secara umum" bertujuan untuk dua atau lebih pernyataan kembali. Menarik.
David

@Solomonoff'sSecret Ini adalah masalah gaya pemrograman yang disukai, dan terlepas dari apa yang keren minggu ini yang timpang minggu lalu, ada kelebihan yang pasti untuk hanya memiliki satu titik keluar, dan di akhir metode (pikirkan saja loop dengan banyak yang return resultbertaburan di dalamnya dan kemudian berpikir sekarang ingin melakukan satu pemeriksaan lagi pada barang yang ditemukan resultsebelum mengembalikannya, dan ini hanyalah sebuah contoh). Mungkin juga ada kontra, tetapi pernyataan yang luas seperti "Secara umum, tidak ada alasan yang baik untuk hanya memiliki satu kesempatan" tidak berarti apa-apa.
SantiBailors

@David Biar saya ungkapkan ulang: tidak ada alasan umum untuk hanya memiliki satu pengembalian. Dalam kasus tertentu, mungkin menjadi alasan tetapi biasanya itu adalah kultus kargo yang paling buruk.
Kembalikan Monica

1
Ini adalah masalah apa yang membuat kode lebih mudah dibaca. Jika di kepala Anda Anda mengatakan "jika ini, kembalikan apa yang kami temukan; jika tidak teruskan; jika kami tidak menemukan sesuatu, kembalikan nilai lain ini". Maka kode Anda harus memiliki dua nilai kembali. Selalu buat kode yang membuatnya lebih mudah dipahami oleh pengembang berikutnya. Kompiler hanya memiliki satu titik balik dari metode Java meskipun menurut pandangan kita itu terlihat seperti dua.
Rick Ryker

3

Mungkin ini merupakan indikasi bahwa Anda harus menulis ulang kode Anda. Sebagai contoh:

  1. Buat array bilangan bulat 0 .. range-1. Setel semua nilai menjadi 0.
  2. Lakukan satu putaran. Di loop, buat nomor acak. Lihat dalam daftar Anda, pada indeks itu, untuk melihat apakah nilainya 1 Jika ya, keluarlah dari pengulangan. Jika tidak, setel nilai pada indeks itu ke 1
  3. Hitung jumlah 1s dalam daftar, dan kembalikan nilai itu.

3

Metode yang memiliki pernyataan return dan memiliki loop / loop di dalamnya selalu membutuhkan pernyataan return di luar loop. Bahkan jika pernyataan di luar perulangan ini tidak pernah tercapai. Dalam kasus seperti itu, untuk menghindari pernyataan kembali yang tidak perlu, Anda dapat mendefinisikan variabel dari tipe masing-masing, sebuah bilangan bulat dalam kasus Anda, pada awal metode yaitu sebelum dan di luar perulangan masing-masing. Ketika hasil yang diinginkan di dalam loop tercapai, Anda dapat mengaitkan nilai masing-masing ke variabel yang ditentukan sebelumnya ini dan menggunakannya untuk pernyataan return di luar loop.

Karena Anda ingin metode Anda mengembalikan hasil pertama ketika rInt [i] sama dengan rInt [count], menerapkan hanya variabel yang disebutkan di atas tidak cukup karena metode akan mengembalikan hasil terakhir ketika rInt [i] sama dengan rInt [count]. Salah satu opsinya adalah dengan mengimplementasikan dua "pernyataan break" yang dipanggil ketika kita mendapatkan hasil yang diinginkan. Jadi, metodenya akan terlihat seperti ini:

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

Saya setuju bahwa seseorang harus memberikan pengecualian jika pernyataan yang tidak dapat dijangkau terjadi. Hanya ingin menunjukkan bagaimana metode yang sama dapat melakukan ini dengan cara yang lebih mudah dibaca (diperlukan aliran java 8).

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
}

Kode ini juga tidak efisien karena salah satu akan melakukan banyak dari result == -1pemeriksaan yang dapat dihilangkan dengan returndi dalam loop ...
Willem Van Onsem
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.