Cara yang mana untuk mengakhiri loop pembacaan adalah pendekatan yang lebih disukai?

13

Ketika Anda harus mengulang pembaca di mana jumlah item untuk dibaca tidak diketahui, dan satu-satunya cara untuk melakukannya adalah terus membaca sampai Anda mencapai akhir.

Ini adalah tempat yang sering Anda butuhkan perulangan tanpa akhir.

  1. Selalu ada trueyang menunjukkan harus ada breakatau returnpernyataan di suatu tempat di dalam blok.

    int offset = 0;
    while(true)
    {
        Record r = Read(offset);
        if(r == null)
        {
            break;
        }
        // do work
        offset++;
    }
  2. Ada pembacaan ganda untuk metode loop.

    Record r = Read(0);
    for(int offset = 0; r != null; offset++)
    {
        r = Read(offset);
        if(r != null)
        {
            // do work
        }
    }
  3. Ada loop baca sementara tunggal. Tidak semua bahasa mendukung metode ini .

    int offset = 0;
    Record r = null;
    while((r = Read(++offset)) != null)
    {
        // do work
    }

Saya bertanya-tanya pendekatan mana yang paling tidak mungkin untuk memperkenalkan bug, yang paling mudah dibaca dan umum digunakan.

Setiap kali saya harus menulis salah satu dari ini saya pikir "harus ada cara yang lebih baik" .

Reactgular
sumber
2
Mengapa Anda mempertahankan offset? Bukankah sebagian besar Pembaca Stream mengizinkan Anda untuk "membaca selanjutnya?"
Robert Harvey
@ RobertTarvey dalam kebutuhan saya saat ini pembaca memiliki query SQL yang mendasari yang menggunakan hasil offset untuk paginate. Saya tidak tahu berapa lama hasil kueri sampai mengembalikan hasil kosong. Tapi, untuk pertanyaan itu sebenarnya bukan keharusan.
Reactgular
3
Anda sepertinya bingung - judul pertanyaannya adalah tentang loop tanpa akhir, tetapi teks pertanyaannya adalah tentang mengakhiri loop. Solusi klasik (dari hari pemrograman terstruktur) adalah dengan melakukan pra-baca, loop saat Anda memiliki data, dan membaca lagi sebagai tindakan terakhir dalam loop. Ini sederhana (memenuhi persyaratan bug), yang paling umum (karena sudah ditulis selama 50 tahun). Paling mudah dibaca adalah masalah pendapat.
andy256
@ andy256 bingung adalah kondisi pra-kopi untuk saya.
Reactgular
1
Ah, jadi prosedur yang benar adalah 1) Minum kopi sambil menghindari keyboard, 2) Mulailah coding loop.
andy256

Jawaban:

49

Saya akan mengambil langkah kembali ke sini. Anda berkonsentrasi pada detail pilih-pilih kode tetapi melewatkan gambar yang lebih besar. Mari kita lihat salah satu contoh loop Anda:

int offset = 0;
while(true)
{
    Record r = Read(offset);
    if(r == null)
    {
        break;
    }
    // do work
    offset++;
}

Apa arti dari kode ini? Artinya adalah "lakukan beberapa pekerjaan untuk setiap catatan dalam file". Tapi bukan itu yang terlihat seperti kode . Kode tersebut terlihat seperti "mempertahankan offset. Buka file. Masukkan loop tanpa kondisi akhir. Baca catatan. Uji untuk nullity." Semua itu sebelum kita mulai bekerja! Pertanyaan yang harus Anda tanyakan adalah " bagaimana saya bisa membuat penampilan kode ini cocok dengan semantiknya? " Kode ini harus:

foreach(Record record in RecordsFromFile())
    DoWork(record);

Sekarang kode itu dibaca seperti niatnya. Pisahkan mekanisme Anda dari semantik Anda . Dalam kode asli Anda, Anda mencampur mekanisme - rincian loop - dengan semantik - pekerjaan yang dilakukan untuk setiap catatan.

Sekarang kita harus mengimplementasikan RecordsFromFile(). Apa cara terbaik untuk mengimplementasikannya? Siapa peduli? Itu bukan kode yang akan dilihat siapa pun. Ini kode mekanisme dasar dan panjangnya sepuluh baris. Tulis sesuai keinginan Anda. Bagaimana dengan ini?

public IEnumerable<Record> RecordsFromFile()
{
    int offset = 0;
    while(true)
    {
        Record record = Read(offset);
        if (record == null) yield break;
        yield return record;
        offset += 1;
    }
}

Sekarang kita sedang memanipulasi urutan catatan dengan malas yang dihitung, segala macam skenario menjadi mungkin:

foreach(Record record in RecordsFromFile().Take(10))
    DoWork(record);

foreach(Record record in RecordsFromFile().OrderBy(r=>r.LastName))
    DoWork(record);

foreach(Record record in RecordsFromFile().Where(r=>r.City == "London")
    DoWork(record);

Dan seterusnya.

Setiap kali Anda menulis sebuah loop, tanyakan pada diri Anda "apakah loop ini membaca seperti mekanisme atau seperti arti dari kode?" Jika jawabannya "seperti mekanisme", maka cobalah untuk memindahkan mekanisme itu ke metode sendiri, dan tulis kode untuk membuat maknanya lebih terlihat.

Eric Lippert
sumber
3
+1 akhirnya jawaban yang masuk akal. Inilah yang akan saya lakukan. Terima kasih.
Reactgular
1
"coba pindahkan mekanisme itu ke metodenya sendiri" - itu kedengarannya mirip dengan Extract Method refactoring bukan? "Ubah fragmen menjadi metode yang namanya menjelaskan tujuan metode."
nyamuk
2
@gnat: Apa yang saya sarankan sedikit lebih terlibat daripada "metode ekstrak", yang saya pikir hanya memindahkan satu sebongkah kode ke tempat lain. Mengekstrak metode jelas merupakan langkah yang baik dalam membuat kode membaca lebih seperti semantiknya. Saya menyarankan agar ekstraksi metode dilakukan dengan penuh pertimbangan, dengan tujuan menjaga kebijakan dan mekanisme tetap terpisah.
Eric Lippert
1
@gnat: Tepat! Dalam hal ini detail yang ingin saya ekstrak adalah mekanisme di mana semua catatan dibaca dari file, sambil mempertahankan kebijakan. Kebijakannya adalah "kita harus melakukan beberapa pekerjaan pada setiap catatan".
Eric Lippert
1
Saya melihat. Dengan begitu, lebih mudah dibaca dan dipelihara. Mempelajari kode ini, saya dapat fokus secara terpisah pada kebijakan dan mekanisme, itu tidak memaksa saya untuk memecah perhatian
Agas
19

Anda tidak perlu loop tanpa akhir. Anda seharusnya tidak memerlukan satu skenario C # baca. Ini adalah pendekatan yang saya sukai, dengan asumsi bahwa Anda benar-benar perlu mempertahankan offset:

Record r = Read(0);
offset=1;
while(r != null)
{
    // Do work
    r = Read(offset);
    offset++
}

Pendekatan ini mengakui fakta bahwa ada langkah pengaturan untuk pembaca, jadi ada dua panggilan metode baca. The whilesyarat adalah di bagian atas loop, hanya dalam kasus tidak ada data sama sekali dalam pembaca.

Robert Harvey
sumber
6

Baik itu tergantung pada situasi Anda. Tapi salah satu solusi "C # -ish" yang dapat saya pikirkan adalah menggunakan antarmuka IEnumerable bawaan dan loop foreach. Antarmuka untuk IEnumerator hanya memanggil MoveNext dengan benar atau salah, sehingga ukurannya tidak diketahui. Kemudian logika terminasi Anda ditulis sekali - di enumerator - dan Anda tidak perlu mengulangi lebih dari satu tempat.

MSDN memberikan contoh IEnumerator <T> . Anda juga perlu membuat IEnumerable <T> untuk mengembalikan IEnumerator <T>.

J Trana
sumber
Bisakah Anda memberikan contoh kode.
Reactgular
terima kasih, inilah yang saya putuskan untuk lakukan. Meskipun saya tidak berpikir membuat kelas baru setiap kali Anda memiliki loop tanpa akhir menyelesaikan pertanyaan.
Reactgular
Ya, saya tahu apa yang Anda maksud - banyak overhead. Jenius / masalah iterator yang sebenarnya adalah bahwa mereka diatur untuk dapat memiliki dua hal yang berulang pada koleksi pada saat yang sama. Berkali-kali Anda tidak memerlukan fungsi itu sehingga Anda hanya dapat memiliki pembungkus di sekitar objek mengimplementasikan IEnumerable dan IEnumerator. Cara lain untuk melihat adalah bahwa apa pun koleksi yang mendasari hal yang Anda iterasi tidak dirancang dengan paradigma C # yang diterima dalam pikiran. Dan itu tidak masalah. Bonus tambahan dari iterator adalah Anda bisa mendapatkan semua barang paralel LINQ secara gratis!
J Trana
2

Ketika saya memiliki operasi inisialisasi, kondisi dan kenaikan, saya suka menggunakan for for loop bahasa seperti C, C ++ dan C #. Seperti ini:

for (int offset = 0, Record r = Read(offset); r != null; r = Read(++offset)){
    // loop here!
}

Atau ini jika Anda pikir ini lebih mudah dibaca. Saya pribadi lebih suka yang pertama.

for (int offset = 0, Record r = Read(offset); r != null; offset++, r = Read(offset)){
    // loop here!
}
Trylks
sumber