Membatalkan pernyataan IF

39

Jadi saya sudah pemrograman selama beberapa tahun sekarang dan baru-baru ini sudah mulai menggunakan ReSharper lebih banyak. Satu hal yang selalu disarankan ReSharper kepada saya adalah "membalikkan 'jika' pernyataan untuk mengurangi bersarang".

Katakanlah saya memiliki kode ini:

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

Dan ReSharper akan menyarankan agar saya melakukan ini:

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

Tampaknya ReSharper akan SELALU menyarankan agar saya membalikkan IF, tidak peduli berapa banyak sarang yang terjadi. Masalah ini adalah saya agak suka bersarang di setidaknya beberapa situasi. Bagi saya sepertinya lebih mudah untuk membaca dan mencari tahu apa yang sedang terjadi di tempat-tempat tertentu. Ini tidak selalu terjadi, tapi kadang-kadang saya merasa lebih nyaman bersarang.

Pertanyaan saya adalah: selain preferensi pribadi atau keterbacaan, apakah ada alasan untuk mengurangi bersarang? Apakah ada perbedaan kinerja atau hal lain yang mungkin tidak saya sadari?

Barry Franklin
sumber
1
Kemungkinan duplikat agar tingkat indentasi tetap rendah
agas
24
Kuncinya di sini adalah "Sarankan Resharper". Lihatlah sarannya, jika itu membuat kode lebih mudah dibaca dan konsisten, gunakanlah. Itu melakukan hal yang sama dengan untuk / untuk setiap loop.
Jon Raynor
Saya biasanya menurunkan petunjuk penyelamat itu, saya tidak selalu mengikuti, jadi mereka tidak muncul di bilah samping lagi.
CodesInChaos
1
ReSharper menghapus yang negatif, yang biasanya merupakan hal yang baik. Namun, itu tidak menyarankan persyaratan ternary yang bisa cocok di sini, jika blok benar-benar sesederhana contoh Anda.
bekerja
2
Bukankah ReSharper juga menyarankan untuk memindahkan persyaratan ke dalam kueri? foreach (someObject di someObjectList.Where (object => object! = null)) {...}
Roman Reiner

Jawaban:

63

Tergantung. Dalam contoh Anda memiliki pernyataan non-terbalik ifbukan masalah, karena tubuh ifsangat pendek. Namun Anda biasanya ingin membalikkan pernyataan ketika tubuh tumbuh.

Kita hanya manusia dan menyimpan banyak hal sekaligus di kepala kita adalah hal yang sulit.

Ketika tubuh pernyataan besar, membalikkan pernyataan tidak hanya mengurangi bersarang tetapi juga memberitahu pengembang mereka bisa melupakan segala sesuatu di atas pernyataan itu dan hanya fokus pada sisanya. Anda sangat mungkin menggunakan pernyataan terbalik untuk menyingkirkan nilai yang salah sesegera mungkin. Setelah Anda tahu bahwa mereka keluar dari permainan, satu-satunya hal yang tersisa adalah nilai-nilai yang sebenarnya dapat diproses.

Andy
sumber
64
Saya suka melihat gelombang opini pengembang berjalan seperti ini. Ada begitu banyak kode yang bersarang hanya karena ada khayalan populer yang luar biasa break, continuedan banyak returnyang tidak terstruktur.
JimmyJames
6
masalahnya adalah istirahat dll masih mengendalikan aliran Anda harus tetap di kepala Anda 'ini tidak bisa x atau itu akan keluar dari loop di atas' mungkin tidak perlu berpikir lebih lanjut jika itu adalah cek nol yang akan melempar pengecualian pula. Tetapi tidak begitu baik ketika lebih bernuansa 'hanya menjalankan setengah kode ini untuk status ini pada hari Kamis'
Ewan
2
@ Ewan: Apa perbedaan antara 'ini tidak bisa x atau itu akan keluar dari loop di atas' dan 'ini tidak bisa x atau tidak akan memasuki blok ini'?
Collin
tidak ada kompleksitas dan makna x yang penting
Ewan
4
@ Ewan: Saya pikir break/ continue/ returnmemiliki keunggulan nyata atas satu elseblok. Dengan keluar awal, ada 2 blok : blok keluar dan tinggal di sana; dengan elseada 3 blok : jika, yang lain, dan apa pun yang datang setelah (yang digunakan apa pun yang diambil cabang). Saya lebih suka memiliki blok yang lebih sedikit.
Matthieu M.
33

Jangan menghindari negatif. Manusia menghisap seperti mengurai mereka bahkan ketika CPU mencintai mereka.

Namun, hargailah idiom. Kita manusia adalah makhluk kebiasaan, jadi kita lebih suka jalur yang dipakai dengan baik ke jalur langsung yang penuh dengan gulma.

foo != nullsayangnya, telah menjadi idiom. Saya hampir tidak melihat bagian yang terpisah lagi. Saya melihat itu dan hanya berpikir, "oh, aman untuk disingkirkan dari foosekarang".

Jika Anda ingin membatalkannya (oh, suatu hari, silakan), Anda memerlukan case yang sangat kuat. Anda membutuhkan sesuatu yang akan membuat mata saya dengan mudah lepas darinya dan memahaminya dalam sekejap.

Namun, apa yang Anda berikan kepada kami adalah ini:

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

Yang secara struktural bahkan tidak sama!

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

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

Itu tidak melakukan apa yang diharapkan oleh otak malas saya. Saya pikir saya bisa terus menambahkan kode independen tetapi saya tidak bisa. Ini membuat saya berpikir tentang hal-hal yang tidak ingin saya pikirkan sekarang. Anda mematahkan idiom saya tetapi tidak memberi saya yang lebih baik. Semua karena Anda ingin melindungi saya dari satu hal negatif yang telah membakar diri kecil yang jahat itu ke dalam jiwa saya.

Maaf, mungkin ReSharper benar untuk menyarankan ini 90% dari waktu tetapi dalam cek nol saya pikir Anda telah menemukan kasus sudut di mana itu sebaiknya diabaikan. Alat tidak pandai mengajarkan Anda apa kode yang dapat dibaca. Tanya manusia. Lakukan peer review. Tapi jangan membabi buta mengikuti alat.

candied_orange
sumber
1
Nah, jika Anda memiliki lebih banyak kode dalam loop, cara kerjanya tergantung pada bagaimana semuanya disatukan. Itu bukan kode independen. Kode refactored dalam pertanyaan itu benar untuk contoh, tetapi mungkin tidak benar ketika tidak dalam isolasi - apakah itu tujuan Anda? (+1 untuk yang tidak menghindari negatif)
Baldrickk
@Brickrickk if (foo != null){ foo.something() }saya dapat terus menambahkan kode tanpa peduli apakah fooitu nol. Ini tidak. Sekalipun saya tidak perlu melakukan itu sekarang, saya tidak merasa termotivasi untuk mengacaukan masa depan dengan mengatakan, "Yah, mereka bisa memperbaiki itu jika mereka membutuhkannya". Feh. Perangkat lunak hanya lunak jika mudah diubah.
candied_orange
4
"terus tambahkan kode tanpa peduli" Entah kode tersebut harus terkait (jika tidak, mungkin harus terpisah) atau harus ada perhatian untuk memahami fungsi fungsi / blok yang Anda modifikasi karena menambahkan kode apa pun berpotensi mengubah perilaku di luar yang dimaksud. Untuk kasus sederhana seperti ini, saya pikir itu tidak penting. Untuk kasus yang lebih kompleks, saya berharap memahami kode untuk didahulukan, jadi saya akan memilih mana yang menurut saya paling jelas, yang bisa berupa gaya.
Baldrickk
terkait dan terjalin bukan hal yang sama.
candied_orange
1
Jangan menghindari hal negatif: D
VitalyB
20

Argumen lama tentang apakah istirahat lebih buruk dari bersarang.

Orang-orang tampaknya menerima pengembalian awal untuk pemeriksaan parameter, tetapi itu disukai sebagian besar metode.

Secara pribadi saya menghindari melanjutkan, istirahat, kebagian dll bahkan jika itu berarti lebih bersarang. Tetapi dalam kebanyakan kasus Anda dapat menghindari keduanya.

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

Jelas bersarang memang membuat hal-hal sulit untuk diikuti ketika Anda memiliki banyak lapisan

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

Yang terburuk adalah ketika Anda memiliki keduanya

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;
    }
}
Ewan
sumber
11
Suka baris ini: foreach (subObject in someObjectList.where(i=>i != null))Tidak tahu mengapa saya tidak pernah memikirkan hal seperti itu.
Barry Franklin
@BarryFranklin ReSharper harus memiliki garis bawah bertitik hijau pada bagian muka dengan "Konversi ke ekspresi LINQ" sebagai saran yang akan melakukannya untuk Anda. Tergantung pada operasinya, ia mungkin melakukan metode LINQ lain seperti Sum atau Max juga.
Kevin Fee
@BarryFranklin Mungkin ini saran yang ingin Anda atur di level rendah dan hanya gunakan secara bebas. Sementara itu berfungsi dengan baik untuk kasus-kasus sepele seperti contoh ini saya menemukan dalam kode yang lebih kompleks cara mengambil bagian kondisional yang lebih kompleks menjadi bit yang Linqifiable dan badan yang tersisa cenderung membuat hal-hal yang jauh lebih sulit untuk dipahami daripada yang asli. Saya biasanya paling tidak akan mencoba saran itu karena ketika itu dapat mengkonversi seluruh loop ke Linq hasilnya sering masuk akal; tetapi hasil setengah dan setengah cenderung mendapatkan IMO cepat jelek.
Dan Neely
Ironisnya, hasil edit oleh resharper memiliki tingkat yang sama persis bersarang, karena itu juga memiliki jika diikuti oleh satu-liner.
Peter
heh, tidak ada kawat gigi! yang tampaknya diperbolehkan: /
Ewan
6

Masalah dengan continueadalah bahwa jika Anda menambahkan lebih banyak baris ke kode logika menjadi rumit dan cenderung mengandung bug. misalnya

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

Apakah Anda ingin memanggil doSomeOtherFunction()untuk semua someObject, atau hanya jika non-null? Dengan kode asli Anda memiliki opsi dan lebih jelas. Dengan yang continuesatu mungkin lupa "oh, ya, di sana kita melewatkan nol" dan Anda bahkan tidak memiliki pilihan untuk memanggilnya untuk semua beberapa proyek, kecuali Anda menempatkan panggilan itu pada awal metode yang mungkin tidak mungkin atau tidak benar karena alasan lain.

Ya, banyak bersarang jelek dan mungkin membingungkan, tetapi dalam hal ini saya akan menggunakan gaya tradisional.

Pertanyaan bonus: Mengapa ada nol dalam daftar itu untuk memulai? Mungkin lebih baik untuk tidak pernah menambahkan nol di tempat pertama, dalam hal ini logika pemrosesan jauh lebih sederhana.

pengguna949300
sumber
12
Seharusnya tidak ada cukup kode di dalam loop sehingga Anda tidak dapat melihat tanda nol di bagian atas tanpa menggulir.
Bryan Boettcher
@Bryan Boettcher setuju, tetapi tidak semua kode ditulis dengan baik. Dan bahkan beberapa baris kode kompleks mungkin membuat orang lupa tentang (atau kesalahpahaman tentang) melanjutkan. Dalam prakteknya saya baik-baik saja dengan returnsebab mereka membentuk "istirahat bersih", tetapi IMO breakdan continuemenyebabkan kebingungan dan, dalam kebanyakan kasus, sebaiknya dihindari.
user949300
4
@ user949300 Setelah sepuluh tahun pengembangan, saya tidak pernah menemukan istirahat atau terus menyebabkan kebingungan. Saya sudah melibatkan mereka dalam kebingungan tetapi mereka tidak pernah menjadi penyebabnya. Biasanya, keputusan yang buruk pada pengembang adalah penyebabnya, dan mereka akan menyebabkan kebingungan tidak peduli senjata apa yang mereka gunakan.
corsiKa
1
@corsiKa Bug Apple SSL sebenarnya adalah bug goto, tetapi bisa dengan mudahnya berupa break atau melanjutkan bug. Namun kodenya mengerikan ...
user949300
3
@BryanBoettcher masalahnya bagi saya bahwa devs yang sama yang berpikir terus atau beberapa pengembalian ok juga berpikir mengubur mereka dalam tubuh metode besar juga ok. Jadi, setiap pengalaman yang saya miliki dengan melanjutkan / beberapa pengembalian telah dalam kode besar berantakan.
Andy
3

Idenya adalah kembalinya awal. Awal returndalam satu lingkaran menjadi awal continue.

Banyak jawaban bagus untuk pertanyaan ini: Haruskah saya kembali dari suatu fungsi lebih awal atau menggunakan pernyataan if?

Perhatikan bahwa sementara pertanyaan ditutup berdasarkan opini, 430+ suara mendukung pengembalian awal, ~ 50 suara "tergantung", dan 8 menentang pengembalian awal. Jadi ada konsensus yang sangat kuat (baik itu, atau saya buruk dalam menghitung, yang masuk akal).

Secara pribadi, jika tubuh loop akan menjadi subjek awal terus dan cukup lama untuk itu menjadi masalah (3-4 baris), saya cenderung untuk mengekstrak tubuh loop ke dalam fungsi di mana saya menggunakan pengembalian awal, bukan awal melanjutkan.

Peter
sumber
2

Teknik ini berguna untuk mensertifikasi pra-kondisi ketika sebagian besar metode Anda terjadi ketika kondisi tersebut terpenuhi.

Kami sering membalikkan ifspekerjaan saya dan menggunakan hantu lain. Dulu cukup sering bahwa ketika meninjau PR saya bisa menunjukkan bagaimana rekan saya dapat menghilangkan tiga tingkat bersarang! Itu terjadi lebih jarang karena kita melakukan ifs.

Berikut adalah contoh mainan dari sesuatu yang dapat diluncurkan

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

Kode seperti ini, di mana ada SATU kasus menarik (di mana sisanya hanya mengembalikan atau blok pernyataan kecil) adalah umum. Sepele untuk menulis ulang di atas sebagai

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

Di sini, kode signifikan kami, 10 baris yang tidak termasuk, bukan indentasi tiga fungsi. Jika Anda memiliki gaya pertama, Anda tergoda untuk mengubah blok panjang menjadi metode atau mentolerir indentasi. Di sinilah penghematan masuk. Di satu tempat ini adalah penghematan sepele tetapi di banyak metode di banyak kelas, Anda menghemat kebutuhan untuk menghasilkan fungsi tambahan untuk membuat kode lebih bersih dan Anda tidak memiliki banyak tingkat multi yang mengerikan lekukan .


Menanggapi contoh kode OP, saya setuju bahwa itu adalah keruntuhan berlebihan. Terutama karena di 1TB, gaya yang saya gunakan, inversi menyebabkan kode bertambah besar.

Lan
sumber
0

Saran Resharpers seringkali bermanfaat tetapi harus selalu dievaluasi secara kritis. Ia mencari beberapa pola kode yang telah ditentukan sebelumnya dan menyarankan transformasi, tetapi tidak dapat memperhitungkan semua faktor yang membuat kode lebih atau kurang dapat dibaca oleh manusia.

Semua hal lain dianggap sama, lebih sedikit bersarang lebih disukai, itulah sebabnya ReSharper akan menyarankan transformasi yang mengurangi bersarang. Tetapi semua hal lain tidak sama: Dalam hal ini transformasi membutuhkan pengenalan a continue, yang berarti kode yang dihasilkan sebenarnya lebih sulit untuk diikuti.

Dalam beberapa kasus, bertukar tingkat bersarang dengan yang memang continue bisa menjadi perbaikan, tetapi ini tergantung pada semantik kode yang dimaksud, yang berada di luar pemahaman aturan mekanik ReSharpers.

Dalam kasus Anda, saran ReSharper akan membuat kode lebih buruk. Jadi abaikan saja. Atau lebih baik: Jangan gunakan transformasi yang disarankan, tetapi berikan sedikit pemikiran tentang bagaimana kode dapat ditingkatkan. Perhatikan bahwa Anda mungkin menugaskan variabel yang sama beberapa kali, tetapi hanya penugasan terakhir yang berpengaruh, karena yang sebelumnya hanya akan ditimpa. Ini memang terlihat seperti bug. Saran ReSharpers tidak memperbaiki bug, tetapi itu membuatnya sedikit lebih jelas bahwa beberapa logika yang meragukan sedang terjadi.

JacquesB
sumber
0

Saya pikir poin yang lebih besar di sini adalah bahwa tujuan dari loop menentukan cara terbaik untuk mengatur aliran dalam loop. Jika Anda memfilter beberapa elemen sebelum Anda mengulang sisanya, maka continuekeluar lebih awal masuk akal. Namun, jika Anda melakukan berbagai jenis pemrosesan dalam satu lingkaran, mungkin lebih masuk akal untuk membuatnya ifsangat jelas, seperti:

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

Perhatikan bahwa di Java Streaming atau idiom fungsional lainnya, Anda dapat memisahkan pemfilteran dari pengulangan, dengan menggunakan:

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

Kebetulan, ini adalah salah satu hal yang saya sukai dari Perl terbalik jika ( unless) diterapkan pada pernyataan tunggal, yang memungkinkan jenis kode berikut, yang menurut saya sangat mudah dibaca:

foreach my $item (@items) {
    next unless defined $item;
    carp "Only blue items are supported! Failed on item $item" 
       unless ($item->get_colour() eq 'blue');
    ...
}
Gaurav
sumber