วิธีใดที่จะยุติการอ่านลูปเป็นวิธีที่ต้องการ


13

เมื่อคุณต้องย้ำผู้อ่านที่ไม่ทราบจำนวนรายการที่จะอ่านและวิธีเดียวที่จะทำคือการอ่านต่อไปจนกว่าจะถึงจุดสิ้นสุด

นี่เป็นสถานที่ที่คุณต้องการวนซ้ำไม่รู้จบ

  1. มีเสมอtrueที่ระบุว่าจะต้องมีbreakหรือreturnคำสั่งบางแห่งในบล็อก

    int offset = 0;
    while(true)
    {
        Record r = Read(offset);
        if(r == null)
        {
            break;
        }
        // do work
        offset++;
    }
    
  2. มีวิธีอ่านลูปสองครั้ง

    Record r = Read(0);
    for(int offset = 0; r != null; offset++)
    {
        r = Read(offset);
        if(r != null)
        {
            // do work
        }
    }
    
  3. มีการอ่านเพียงครั้งเดียวขณะที่วนซ้ำ ไม่ใช่ทุกภาษาที่รองรับวิธีนี้

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

ฉันสงสัยว่าวิธีการใดที่มีแนวโน้มที่จะแนะนำข้อบกพร่องน้อยที่สุดที่สามารถอ่านได้และใช้กันมากที่สุด

ทุกครั้งที่ผมต้องเขียนหนึ่งของเหล่านี้ผมคิดว่า"จะต้องมีวิธีที่ดีกว่า"


2
ทำไมคุณยังคงชดเชย ผู้อ่านสตรีมส่วนใหญ่ไม่อนุญาตให้คุณ "อ่านต่อไปหรือไม่"
Robert Harvey

@RobertHarvey ในความต้องการปัจจุบันของฉันผู้อ่านมีแบบสอบถาม SQL พื้นฐานที่ใช้ offset เพื่อแบ่งหน้าผลลัพธ์ ฉันไม่รู้ว่าผลลัพธ์ของแบบสอบถามจะนานแค่ไหนจนกว่าจะได้ผลลัพธ์ที่ว่างเปล่า แต่สำหรับคำถามมันไม่ได้เป็นข้อกำหนดจริงๆ
Reactgular

3
คุณดูเหมือนสับสน - ชื่อคำถามเกี่ยวกับการวนซ้ำไม่รู้จบ แต่ข้อความคำถามนั้นเกี่ยวกับการยกเลิกการวนซ้ำ วิธีแก้ปัญหาแบบคลาสสิก (นับจากวันที่มีการเขียนโปรแกรมแบบมีโครงสร้าง) คือการอ่านลูปล่วงหน้าในขณะที่คุณมีข้อมูลและอ่านอีกครั้งเป็นการกระทำสุดท้ายในลูป ง่าย (ตรงตามข้อกำหนดของข้อบกพร่อง) ซึ่งเป็นเรื่องธรรมดาที่สุด (เนื่องจากเขียนขึ้นมา 50 ปี) ส่วนใหญ่อ่านได้เป็นเรื่องของความคิดเห็น
andy256

@ andy256 สับสนเป็นเงื่อนไขก่อนกาแฟสำหรับฉัน
ซ้ำ

1
อ่าขั้นตอนที่ถูกต้องคือ 1) ดื่มกาแฟขณะที่หลีกเลี่ยงคีย์บอร์ด 2) เริ่มต้นการวนลูป
andy256

คำตอบ:


49

ฉันจะย้อนกลับไปที่นี่ คุณกำลังจดจ่อกับรายละเอียดที่พิถีพิถันของรหัส แต่ไม่มีภาพใหญ่ขึ้น ลองมาดูตัวอย่างหนึ่งในลูปของคุณ:

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

อะไรคือความหมายของรหัสนี้หรือไม่? ความหมายคือ "ทำงานบางอย่างกับแต่ละระเบียนในไฟล์" แต่นั่นไม่ใช่สิ่งที่รหัสดูเหมือนว่า รหัสดูเหมือนว่า "รักษาออฟเซ็ตเปิดไฟล์ป้อนลูปโดยไม่มีเงื่อนไขสิ้นสุดอ่านเร็กคอร์ดทดสอบความว่างเปล่า" ทั้งหมดก่อนที่เราจะไปทำงาน! คำถามที่คุณควรถามคือ " ฉันจะทำให้ลักษณะที่ปรากฏของรหัสนี้ตรงกับความหมายได้อย่างไร " รหัสนี้ควรเป็น:

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

ตอนนี้โค้ดอ่านตามความตั้งใจ เฉพาะกิจกลไกของคุณจากความหมายของคุณ ในรหัสต้นฉบับของคุณคุณรวมกลไก - รายละเอียดของวง - กับความหมาย - งานที่ทำกับแต่ละระเบียน

RecordsFromFile()ตอนนี้เรามีการดำเนินการ อะไรคือวิธีที่ดีที่สุดในการนำไปใช้ ใครสน? นั่นไม่ใช่รหัสที่ทุกคนกำลังดู มันเป็นรหัสกลไกพื้นฐานและมีความยาวสิบบรรทัด เขียนได้ตามที่คุณต้องการ แล้วเรื่องนี้ล่ะ

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

ตอนนี้เรากำลังจัดการลำดับของเร็กคอร์ดที่คำนวณอย่างเกียจคร้านทุกประเภทของสถานการณ์เป็นไปได้:

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

และอื่น ๆ

เมื่อใดก็ตามที่คุณเขียนลูปให้ถามตัวเองว่า "ลูปนี้อ่านเหมือนกลไกหรือชอบความหมายของโค้ดหรือไม่" หากคำตอบคือ "เหมือนกลไก" ให้ลองย้ายกลไกนั้นไปยังวิธีการของตัวเองแล้วเขียนรหัสเพื่อให้ความหมายชัดเจนยิ่งขึ้น


3
+1 ในที่สุดคำตอบที่สมเหตุสมผล นี่คือสิ่งที่ฉันจะทำ ขอบคุณ
ซ้ำ

1
"ลองย้ายกลไกนั้นไปยังวิธีการของตัวเอง" - ซึ่งฟังดูคล้ายกับการปรับโครงสร้างวิธีการแยกใหม่ใช่ไหม "เปลี่ยนส่วนให้เป็นวิธีที่ชื่ออธิบายถึงวัตถุประสงค์ของวิธีการ"
ริ้น

2
@gnat: สิ่งที่ฉันแนะนำมีส่วนเกี่ยวข้องมากกว่า "แยกวิธี" ซึ่งฉันคิดว่าเป็นเพียงการย้ายรหัสก้อนใหญ่หนึ่งไปยังสถานที่อื่น วิธีการแตกไฟล์เป็นขั้นตอนที่ดีในการสร้างโค้ดให้อ่านมากขึ้นเช่นความหมายของมัน ฉันแนะนำให้ทำการแยกวิธีอย่างรอบคอบโดยมีจุดประสงค์เพื่อแยกนโยบายและกลไกออกจากกัน
Eric Lippert

1
@gnat: แน่นอน! ในกรณีนี้รายละเอียดที่ฉันต้องการแยกเป็นกลไกที่บันทึกทั้งหมดจะถูกอ่านจากไฟล์ในขณะที่ยังคงนโยบาย นโยบายคือ "เราต้องทำงานบางอย่างในทุก ๆ บันทึก"
Eric Lippert

1
ฉันเห็น. วิธีนี้ง่ายต่อการอ่านและบำรุงรักษา การศึกษารหัสนี้ฉันสามารถมุ่งเน้นไปที่นโยบายและกลไกแยกกันมันไม่ได้บังคับให้ฉันแยกความสนใจ
gnat

19

คุณไม่จำเป็นต้องวนซ้ำไม่รู้จบ คุณไม่จำเป็นต้องใช้สิ่งใดในสถานการณ์จำลองการอ่าน C # นี่เป็นวิธีที่ฉันชอบโดยสมมติว่าคุณจำเป็นต้องรักษาออฟเซ็ต:

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

วิธีการนี้ยอมรับความจริงที่ว่ามีขั้นตอนการตั้งค่าสำหรับตัวอ่านดังนั้นจึงมีวิธีการอ่านสองวิธี whileสภาพที่ด้านบนของห่วงเพียงในกรณีที่ไม่มีข้อมูลที่ทุกคนในผู้อ่าน


6

มันขึ้นอยู่กับสถานการณ์ของคุณ แต่หนึ่งในวิธีแก้ปัญหา "C # -ish" ที่ฉันคิดได้ก็คือใช้อินเทอร์เฟซ IEnumerable ในตัวและวนรอบ foreach อินเทอร์เฟซสำหรับ IEnumerator เรียกใช้ MoveNext ด้วยจริงหรือเท็จเท่านั้นดังนั้นขนาดจึงไม่เป็นที่รู้จัก จากนั้นตรรกะการเลิกจ้างของคุณจะถูกเขียนหนึ่งครั้ง - ในตัวแจงนับ - และคุณไม่ต้องทำซ้ำในจุดที่มากกว่าหนึ่งจุด

MSDN ให้ตัวอย่างของ IEnumerator <T> คุณจะต้องสร้าง IEnumerable <T> เพื่อส่งคืน IEnumerator <T>


คุณช่วยยกตัวอย่างรหัสได้ไหม
ซ้ำ

ขอบคุณนี่คือสิ่งที่ฉันตัดสินใจที่จะทำ ในขณะที่ฉันไม่คิดว่าจะสร้างชั้นเรียนใหม่ทุกครั้งที่คุณมีการวนซ้ำไม่รู้จบแก้คำถาม
ปฏิกิริยา

ใช่ฉันรู้ว่าคุณหมายถึงอะไร - ค่าใช้จ่ายจำนวนมาก อัจฉริยะ / ปัญหาที่แท้จริงของตัววนซ้ำนั้นคือพวกเขาถูกสร้างขึ้นอย่างแน่นอนเพื่อให้สามารถมีสองสิ่งที่ย้ำผ่านคอลเลกชันในเวลาเดียวกัน หลายครั้งที่คุณไม่จำเป็นต้องใช้ฟังก์ชั่นนั้นดังนั้นคุณสามารถใช้ตัวล้อมรอบวัตถุที่ใช้ทั้ง IEnumerable และ IEnumerator อีกวิธีในการดูคืออะไรก็ตามที่เป็นพื้นฐานของสิ่งที่คุณกำลังทำซ้ำไม่ได้ออกแบบมาโดยคำนึงถึงกระบวนทัศน์ของ C # และก็ไม่เป็นไร โบนัสเพิ่มเติมของการวนซ้ำคือคุณสามารถรับ LINQ ขนานทั้งหมดได้ฟรี!
J Trana

2

เมื่อฉันมีการเริ่มต้นเงื่อนไขและการดำเนินการเพิ่มฉันชอบที่จะใช้สำหรับลูปของภาษาเช่น C, C ++ และ C # แบบนี้:

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

หรือสิ่งนี้ถ้าคุณคิดว่าสิ่งนี้สามารถอ่านได้มากขึ้น ฉันชอบคนแรก

for (int offset = 0, Record r = Read(offset); r != null; offset++, r = Read(offset)){
    // loop here!
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.