Foreach-loop dengan break / return vs while-loop dengan invarian eksplisit dan post-kondisi

17

Ini adalah cara yang paling populer (menurut saya) untuk memeriksa apakah suatu nilai ada dalam array:

for (int x : array)
{
    if (x == value)
        return true;
}
return false;        

Namun, dalam sebuah buku yang saya baca bertahun-tahun yang lalu oleh, mungkin, Wirth atau Dijkstra, dikatakan bahwa gaya ini lebih baik (bila dibandingkan dengan loop sementara dengan jalan keluar di dalam):

int i = 0;
while (i < array.length && array[i] != value)
    i++;
return i < array.length;

Dengan cara ini, kondisi keluar tambahan menjadi bagian eksplisit dari invarian loop, tidak ada kondisi tersembunyi dan keluar di dalam loop, semuanya lebih jelas dan lebih dalam cara pemrograman terstruktur. Saya biasanya lebih suka pola yang terakhir ini bila memungkinkan dan menggunakan for-loop untuk hanya beralih dari ake b.

Namun saya tidak dapat mengatakan bahwa versi pertama kurang jelas. Mungkin bahkan lebih jelas dan lebih mudah dipahami, setidaknya untuk pemula. Jadi saya masih bertanya pada diri sendiri pertanyaan mana yang lebih baik?

Mungkin seseorang dapat memberikan alasan yang bagus untuk mendukung salah satu metode?

Pembaruan: Ini bukan pertanyaan tentang beberapa titik pengembalian fungsi, lambdas atau menemukan elemen dalam array per se. Ini tentang bagaimana menulis loop dengan invarian yang lebih kompleks daripada ketidaksetaraan tunggal.

Pembaruan: OK, saya melihat inti dari orang-orang yang menjawab dan berkomentar: Saya mencampur-adukkan dalam loop foreach di sini, yang itu sendiri sudah jauh lebih jelas dan mudah dibaca daripada loop-sementara. Seharusnya aku tidak melakukan itu. Tapi ini juga pertanyaan yang menarik, jadi mari kita biarkan apa adanya: foreach-loop dan kondisi tambahan di dalam, atau loop-sementara dengan invarian loop eksplisit dan setelah kondisi setelah. Tampaknya foreach-loop dengan kondisi dan jalan keluar / istirahat menang. Saya akan membuat pertanyaan tambahan tanpa foreach-loop (untuk daftar tertaut).

Danila Piatov
sumber
2
Contoh kode yang dikutip di sini mencampurkan beberapa masalah berbeda. Pengembalian awal & multipel (yang bagi saya masuk ke ukuran metode (tidak ditampilkan)), pencarian array (yang meminta diskusi yang melibatkan lambdas), foreach vs pengindeksan langsung ... Pertanyaan ini akan lebih jelas dan lebih mudah untuk jawablah jika fokusnya hanya pada satu dari masalah ini pada satu waktu.
Erik Eidt
5
Kemungkinan rangkap dari metode Haruskah selalu kembali dari satu tempat?
nyamuk
1
Saya tahu itu adalah contoh, tetapi ada bahasa yang memiliki API untuk menangani persis kasus penggunaan itu. Yaitucollection.contains(foo)
Berin Loritsch
2
Anda mungkin ingin menemukan buku itu dan membaca kembali sekarang untuk melihat apa yang sebenarnya dikatakannya.
Thorbjørn Ravn Andersen
1
"Lebih baik" adalah kata yang sangat subyektif. Yang mengatakan, orang dapat mengetahui secara sekilas apa yang dilakukan versi pertama. Bahwa versi kedua melakukan hal yang persis sama, perlu dicermati.
David Hammen

Jawaban:

19

Saya pikir untuk loop sederhana, seperti ini, sintaks standar pertama jauh lebih jelas. Beberapa orang menganggap banyak pengembalian membingungkan atau bau kode, tetapi untuk sepotong kode sekecil ini, saya tidak percaya ini adalah masalah nyata.

Itu menjadi sedikit lebih bisa diperdebatkan untuk loop yang lebih kompleks. Jika konten loop tidak sesuai pada layar Anda dan memiliki beberapa pengembalian dalam loop, ada argumen yang harus dibuat bahwa beberapa titik keluar dapat membuat kode lebih sulit untuk dipelihara. Misalnya, jika Anda harus memastikan beberapa metode pemeliharaan negara berjalan sebelum keluar dari fungsi, akan mudah untuk tidak menambahkannya ke salah satu pernyataan pengembalian dan Anda akan menyebabkan bug. Jika semua kondisi akhir dapat diperiksa dalam loop sementara, Anda hanya memiliki satu titik keluar dan dapat menambahkan kode ini setelah itu.

Yang mengatakan, dengan loop terutama itu baik untuk mencoba dan menempatkan logika sebanyak mungkin ke dalam metode terpisah. Ini menghindari banyak kasus di mana metode kedua akan memiliki kelebihan. Lean loop dengan logika yang terpisah jelas akan lebih penting daripada gaya yang Anda gunakan. Juga, jika sebagian besar basis kode aplikasi Anda menggunakan satu gaya, Anda harus tetap dengan gaya itu.

Nathanael
sumber
56

Ini mudah.

Hampir tidak ada yang lebih penting daripada kejelasan bagi pembaca. Varian pertama yang saya temukan sangat sederhana dan jelas.

Versi 'membaik' kedua, saya harus membaca beberapa kali dan memastikan semua kondisi tepi benar.

Ada ZERO DOUBT yang merupakan gaya pengkodean yang lebih baik (yang pertama jauh lebih baik).

Sekarang - apa yang CLEAR bagi orang-orang dapat berbeda dari orang ke orang. Saya tidak yakin ada standar obyektif untuk itu (meskipun memposting ke forum seperti ini dan mendapatkan berbagai masukan orang dapat membantu).

Namun, dalam kasus khusus ini, saya dapat memberi tahu Anda mengapa algoritme pertama lebih jelas: Saya tahu seperti apa fungsi C ++ pada sintaksis wadah dan apa yang terlihat. Saya sudah menginternalisasi itu. Seseorang UNFAMILIAR (sintaks barunya) dengan sintaksis itu mungkin lebih suka variasi kedua.

Tapi begitu Anda tahu dan mengerti sintaks baru itu, itu konsep dasar yang bisa Anda gunakan. Dengan pendekatan loop iterasi (kedua), Anda harus hati-hati memeriksa bahwa pengguna dengan benar memeriksa semua kondisi tepi untuk mengulang seluruh array (misalnya kurang dari kurang dari atau sama dengan, indeks yang sama digunakan untuk pengujian dan untuk pengindeksan dll).

Lewis Pringle
sumber
4
Baru relatif, seperti yang sudah ada dalam standar 2011. Juga, demo kedua jelas bukan C ++.
Deduplicator
Solusi alternatif jika Anda ingin menggunakan satu titik keluar adalah dengan menetapkan bendera longerLength = true, lalu return longerLength.
Cullub
@Dupuplikator Mengapa demo C ++ kedua tidak? Saya tidak mengerti mengapa tidak atau saya kehilangan sesuatu yang jelas?
Rakete1111
2
@ Rakete1111 Array mentah tidak memiliki properti seperti length. Jika itu benar-benar dinyatakan sebagai array dan bukan pointer, mereka dapat menggunakan sizeof, atau jika itu adalah std::array, fungsi anggota yang benar adalah size(), tidak ada lengthproperti.
IllusiveBrian
@IllusiveBrian: sizeofakan dalam byte ... Yang paling umum sejak C ++ 17 adalah std::size().
Deduplicator
9
int i = 0;
while (i < array.length && array[i] != value)
    i++;
return i < array.length;

[...] semuanya lebih jelas dan lebih dengan cara pemrograman terstruktur.

Tidak terlalu. Variabel iada di luar loop sementara di sini dan dengan demikian bagian dari lingkup luar, sementara (pun intended) xdari for-loop hanya ada dalam lingkup loop. Lingkup adalah salah satu cara yang sangat penting untuk memperkenalkan struktur pada pemrograman.

batal
sumber
1
@ruakh Saya tidak yakin apa yang harus diambil dari komentar Anda. Itu muncul sebagai agak pasif-agresif, seolah-olah jawaban saya menentang apa yang tertulis di halaman wiki. Tolong jelaskan.
null
"Pemrograman terstruktur" adalah istilah seni dengan makna tertentu, dan OP secara objektif benar bahwa versi # 2 sesuai dengan aturan pemrograman terstruktur sementara versi # 1 tidak. Dari jawaban Anda, tampaknya Anda tidak terbiasa dengan istilah seni, dan menafsirkan istilah itu secara harfiah. Saya tidak yakin mengapa komentar saya dianggap pasif-agresif; Maksud saya itu hanya sebagai informatif.
ruakh
@ruakh Saya tidak setuju bahwa versi # 2 lebih sesuai dengan aturan di setiap aspek dan menjelaskannya dalam jawaban saya.
null
Anda mengatakan "Saya tidak setuju" seolah-olah itu adalah hal subjektif, tetapi tidak. Kembali dari dalam satu lingkaran adalah pelanggaran kategoris terhadap aturan pemrograman terstruktur. Saya yakin bahwa banyak penggemar pemrograman terstruktur adalah penggemar variabel cakupan minimal, tetapi jika Anda mengurangi cakupan variabel dengan menyimpang dari pemrograman terstruktur, maka Anda telah meninggalkan pemrograman terstruktur, titik, dan mengurangi cakupan variabel tidak membatalkan bahwa.
ruakh
2

Kedua loop memiliki semantik yang berbeda:

  • Loop pertama hanya menjawab pertanyaan ya / tidak sederhana: "Apakah array berisi objek yang saya cari?" Itu dilakukan dengan cara yang sesingkat mungkin.

  • Loop kedua menjawab pertanyaan: "Jika array berisi objek yang saya cari, apa indeks dari kecocokan pertama?" Sekali lagi, ia melakukannya dengan cara sesingkat mungkin.

Karena jawaban untuk pertanyaan kedua benar-benar memberikan lebih banyak informasi daripada jawaban untuk pertanyaan pertama, Anda dapat memilih untuk menjawab pertanyaan kedua dan kemudian mendapatkan jawaban dari pertanyaan pertama. Itulah yang dilakukan garis return i < array.length;itu.

Saya percaya bahwa biasanya yang terbaik adalah hanya menggunakan alat yang sesuai dengan tujuan kecuali Anda dapat menggunakan kembali alat yang sudah ada dan lebih fleksibel. Yaitu:

  • Menggunakan varian pertama dari loop itu baik-baik saja.
  • Mengubah varian pertama untuk hanya menetapkan boolvariabel dan istirahat juga baik-baik saja. (Menghindari returnpernyataan kedua , jawaban tersedia dalam variabel alih-alih fungsi).
  • Penggunaannya std::findbaik-baik saja (penggunaan kembali kode!).
  • Namun, secara eksplisit mengkodekan sebuah temuan dan kemudian mengurangi jawaban menjadi booltidak.
cmaster - mengembalikan monica
sumber
Akan lebih baik jika downvoters akan meninggalkan komentar ...
cmaster - mengembalikan monica
2

Saya akan menyarankan opsi ketiga sama sekali:

return array.find(value);

Ada banyak alasan berbeda untuk beralih pada array: Periksa apakah ada nilai tertentu, ubah array menjadi array lain, hitung nilai agregat, filter beberapa nilai dari array ... Jika Anda menggunakan dataran untuk loop, tidak jelas sekilas secara khusus bagaimana for loop digunakan. Namun, sebagian besar bahasa modern memiliki API kaya pada struktur data lariknya yang membuat maksud yang berbeda ini sangat eksplisit.

Bandingkan mengubah satu array menjadi yang lain dengan for loop:

int[] doubledArray = new int[array.length];
for (int i = 0; i < array.length; i++) {
  doubledArray[i] = array[i] * 2;
}

dan menggunakan mapfungsi gaya JavaScript :

array.map((value) => value * 2);

Atau menjumlahkan array:

int sum = 0;
for (int i = 0; i < array.length; i++) {
  sum += array[i];
}

melawan:

array.reduce(
  (sum, nextValue) => sum + nextValue,
  0
);

Berapa lama Anda mengerti apa ini?

int[] newArray = new int[array.length];
int numValuesAdded = 0;

for (int i = 0; i < array.length; i++) {
  if (array[i] >= 0) {
    newArray[numValuesAdded] = array[i];
    numValuesAdded++;
  }
}

melawan

array.filter((value) => (value >= 0));

Dalam ketiga kasus tersebut, sementara for loop jelas dapat dibaca, Anda harus meluangkan waktu untuk mencari tahu bagaimana for loop digunakan dan memeriksa apakah semua penghitung dan kondisi keluar sudah benar. Fungsi gaya lambda modern membuat tujuan loop sangat eksplisit, dan Anda tahu pasti bahwa fungsi API yang dipanggil diimplementasikan dengan benar.

Sebagian besar bahasa modern, termasuk JavaScript , Ruby , C # , dan Java , menggunakan gaya interaksi fungsional ini dengan array dan koleksi serupa.

Secara umum, sementara saya tidak berpikir menggunakan untuk loop selalu salah, dan ini adalah masalah selera pribadi, saya telah menemukan diri saya sangat menyukai menggunakan gaya ini bekerja dengan array. Ini secara khusus karena meningkatnya kejelasan dalam menentukan apa yang dilakukan setiap loop. Jika bahasa Anda memiliki fitur atau alat serupa di perpustakaan standarnya, saya sarankan Anda mempertimbangkan untuk mengadopsi gaya ini juga!

Kevin
sumber
2
Merekomendasikan array.findmemunculkan pertanyaan, karena kita kemudian harus membahas cara terbaik untuk menerapkan array.find. Kecuali jika Anda menggunakan perangkat keras dengan findoperasi bawaan, kami harus menulis loop di sana.
Barmar
2
@Barmar saya tidak setuju. Seperti yang saya tunjukkan dalam jawaban saya, banyak bahasa yang banyak digunakan menyediakan fungsi seperti finddi perpustakaan standar mereka. Tidak diragukan lagi, perpustakaan ini mengimplementasikan finddan kerabatnya menggunakan untuk loop, tapi itulah fungsi yang baik: itu mengabstraksi rincian teknis dari konsumen fungsi, yang memungkinkan programmer untuk tidak perlu memikirkan rincian itu. Jadi meskipun findkemungkinan diimplementasikan dengan for loop, tetap membantu membuat kode lebih mudah dibaca, dan karena sering di perpustakaan standar, menggunakannya tidak menambah overhead atau risiko yang berarti.
Kevin
4
Tetapi seorang insinyur perangkat lunak harus mengimplementasikan perpustakaan ini. Bukankah prinsip rekayasa perangkat lunak yang sama berlaku untuk penulis perpustakaan sebagai pemrogram aplikasi? Pertanyaannya adalah tentang menulis loop secara umum, bukan cara terbaik untuk mencari elemen array dalam bahasa tertentu
Barmar
4
Dengan kata lain, mencari elemen array hanyalah sebuah contoh sederhana yang ia gunakan untuk menunjukkan teknik looping yang berbeda.
Barmar
-2

Semuanya bermuara pada apa yang dimaksud dengan 'lebih baik'. Untuk programmer praktis, umumnya berarti efisien - yaitu dalam hal ini, keluar langsung dari loop menghindari satu perbandingan ekstra, dan mengembalikan konstanta Boolean menghindari perbandingan duplikat; ini menghemat siklus. Dijkstra lebih mementingkan pembuatan kode yang lebih mudah dibuktikan dengan benar . [Tampaknya bagi saya bahwa pendidikan CS di Eropa menganggap 'membuktikan kebenaran kode' jauh lebih serius daripada pendidikan CS di AS, di mana kekuatan ekonomi cenderung mendominasi praktik pengkodean]

PMar
sumber
3
PMar, kinerja-bijaksana kedua loop cukup banyak setara - mereka berdua memiliki dua perbandingan.
Danila Piatov
1
Jika orang benar-benar peduli dengan kinerja, orang akan menggunakan algoritma yang lebih cepat. mis. mengurutkan array dan melakukan pencarian biner, atau menggunakan Hashtable.
user949300
Danila - Anda tidak tahu struktur data apa di balik ini. Sebuah iterator selalu cepat. Akses yang diindeks dapat berupa waktu linier, dan panjang tidak perlu ada.
gnasher729