Cách nào để chấm dứt vòng lặp đọc là cách tiếp cận ưa thích?


13

Khi bạn phải lặp lại một trình đọc mà số lượng mục cần đọc là không xác định, và cách duy nhất để làm là tiếp tục đọc cho đến khi bạn kết thúc.

Đây thường là nơi bạn cần một vòng lặp vô tận.

  1. Luôn luôn truechỉ ra rằng phải có một breakhoặc một returncâu lệnh ở đâu đó bên trong khối.

    int offset = 0;
    while(true)
    {
        Record r = Read(offset);
        if(r == null)
        {
            break;
        }
        // do work
        offset++;
    }
  2. hai lần đọc cho phương thức vòng lặp.

    Record r = Read(0);
    for(int offset = 0; r != null; offset++)
    {
        r = Read(offset);
        if(r != null)
        {
            // do work
        }
    }
  3. Có vòng lặp đọc đơn. Không phải tất cả các ngôn ngữ đều hỗ trợ phương pháp này .

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

Tôi đang tự hỏi phương pháp nào ít có khả năng giới thiệu một lỗi nhất, dễ đọc nhất và thường được sử dụng.

Mỗi lần tôi phải viết một trong những điều này tôi nghĩ rằng "phải có một cách tốt hơn" .


2
Tại sao bạn duy trì một sự bù đắp? Đừng hầu hết các Trình đọc luồng cho phép bạn chỉ cần "đọc tiếp?"
Robert Harvey

@RobertHarvey trong nhu cầu hiện tại của tôi, người đọc có một truy vấn SQL cơ bản sử dụng phần bù để phân trang. Tôi không biết kết quả truy vấn là bao lâu cho đến khi trả về kết quả trống. Nhưng, câu hỏi không thực sự là một yêu cầu.
Phản ứng

3
Bạn có vẻ bối rối - tiêu đề câu hỏi là về các vòng lặp vô tận, nhưng văn bản câu hỏi là tất cả về chấm dứt các vòng lặp. Giải pháp cổ điển (từ thời lập trình có cấu trúc) là thực hiện đọc trước, lặp trong khi bạn có dữ liệu và đọc lại như là hành động cuối cùng trong vòng lặp. Thật đơn giản (đáp ứng yêu cầu lỗi), phổ biến nhất (vì nó đã được viết trong 50 năm). Dễ đọc nhất là một vấn đề về quan điểm.
andy256

@ andy256 nhầm lẫn là điều kiện tiền cà phê đối với tôi.
Phản ứng

1
À, vậy quy trình đúng là 1) Uống cà phê trong khi tránh bàn phím, 2) Bắt đầu vòng lặp mã hóa.
andy256

Câu trả lời:


49

Tôi sẽ lùi một bước ở đây. Bạn đang tập trung vào các chi tiết cầu kỳ của mã nhưng thiếu hình ảnh lớn hơn. Chúng ta hãy xem một trong các vòng lặp ví dụ của bạn:

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

Ý nghĩa của mã này là gì? Ý nghĩa là "thực hiện một số công việc cho mỗi bản ghi trong một tệp". Nhưng đó không phải là những gì mã vẻ thích . Mã này trông giống như "duy trì bù. Mở tệp. Nhập vòng lặp không có điều kiện kết thúc. Đọc bản ghi. Kiểm tra tính vô hiệu." Tất cả điều đó trước khi chúng tôi có được công việc! Câu hỏi bạn nên đặt ra là " làm thế nào tôi có thể làm cho diện mạo của mã này khớp với ngữ nghĩa của nó? " Mã này phải là:

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

Bây giờ mã đọc như ý định của nó. Tách các cơ chế của bạn khỏi ngữ nghĩa của bạn . Trong mã gốc của bạn, bạn trộn lẫn cơ chế - các chi tiết của vòng lặp - với ngữ nghĩa - công việc được thực hiện cho mỗi bản ghi.

Bây giờ chúng ta phải thực hiện RecordsFromFile(). Cách tốt nhất để thực hiện điều đó là gì? Ai quan tâm? Đó không phải là mã mà bất cứ ai sẽ nhìn vào. Đó là mã cơ chế cơ bản và dài mười dòng. Viết nó theo cách bạn muốn. Còn cái này thì sao?

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

Bây giờ chúng ta đang thao tác một chuỗi các bản ghi được tính toán một cách lười biếng, tất cả các loại kịch bản đều có thể xảy ra:

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

Và như thế.

Bất cứ khi nào bạn viết một vòng lặp, hãy tự hỏi mình "vòng lặp này có đọc giống như một cơ chế hay giống như ý nghĩa của mã không?" Nếu câu trả lời là "giống như một cơ chế", thì hãy thử chuyển cơ chế đó sang phương thức riêng của nó và viết mã để làm cho ý nghĩa rõ ràng hơn.


3
+1 cuối cùng là một câu trả lời hợp lý. Đây chính xác là những gì tôi sẽ làm. Cảm ơn.
Phản ứng

1
"cố gắng di chuyển cơ chế đó sang phương thức riêng của nó" - nghe có vẻ giống như tái cấu trúc Phương thức trích xuất phải không? "Biến đoạn này thành một phương thức có tên giải thích mục đích của phương thức."
gnat

2
@gnat: Những gì tôi đề xuất có liên quan nhiều hơn một chút so với "phương pháp giải nén", mà tôi nghĩ chỉ đơn thuần là di chuyển một đoạn mã đến một nơi khác. Các phương thức trích xuất chắc chắn là một bước tốt trong việc làm cho mã đọc giống như ngữ nghĩa của nó. Tôi đề nghị rằng việc trích xuất phương pháp được thực hiện một cách chu đáo, với mục đích giữ cho các chính sách và cơ chế được tách biệt.
Eric Lippert

1
@gnat: Chính xác! Trong trường hợp này, chi tiết mà tôi muốn trích xuất là cơ chế mà tất cả các bản ghi được đọc từ tệp, trong khi duy trì chính sách. Chính sách là "chúng tôi phải thực hiện một số công việc trên mỗi hồ sơ".
Eric Lippert

1
Tôi hiểu rồi. Bằng cách đó, nó dễ đọc và bảo trì hơn. Nghiên cứu mã này, tôi có thể tập trung riêng vào chính sách và cơ chế, nó không buộc tôi phải phân chia sự chú ý
gnat

19

Bạn không cần một vòng lặp vô tận. Bạn không bao giờ cần một trong các kịch bản đọc C #. Đây là cách tiếp cận ưa thích của tôi, giả sử rằng bạn thực sự cần phải duy trì bù đắp:

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

Cách tiếp cận này thừa nhận thực tế là có một bước thiết lập cho người đọc, vì vậy có hai cuộc gọi phương thức đọc. Điều whilekiện là ở đầu vòng lặp, chỉ trong trường hợp không có dữ liệu nào trong đầu đọc.


6

Vâng, nó phụ thuộc vào tình hình của bạn. Nhưng một trong những giải pháp "C # -ish" hơn mà tôi có thể nghĩ đến là sử dụng giao diện IEnumerable tích hợp và vòng lặp foreach. Giao diện cho IEnumerator chỉ gọi MoveNext bằng đúng hoặc sai, vì vậy kích thước có thể không xác định. Sau đó, logic chấm dứt của bạn được viết một lần - trong bảng liệt kê - và bạn không phải lặp lại ở nhiều vị trí.

MSDN cung cấp một ví dụ về IEnumerator <T> . Bạn cũng sẽ cần tạo một IEnumerable <T> để trả về IEnumerator <T>.


Bạn có thể cho một ví dụ mã.
Phản ứng

cảm ơn, đây là những gì tôi đã quyết định làm Trong khi tôi không nghĩ việc tạo các lớp mới mỗi khi bạn có một vòng lặp vô tận sẽ giải quyết được câu hỏi.
Phản ứng

Vâng, tôi biết ý của bạn - rất nhiều chi phí. Thiên tài / vấn đề thực sự của các trình vòng lặp là chúng chắc chắn được thiết lập để có thể có hai thứ lặp đi lặp lại trên một bộ sưu tập cùng một lúc. Vì vậy, nhiều lần bạn không cần chức năng đó để bạn có thể có trình bao bọc xung quanh các đối tượng triển khai cả IEnumerable và IEnumerator. Một cách nhìn khác là bất kỳ bộ sưu tập nội dung cơ bản nào mà bạn đang lặp đi lặp lại đều không được thiết kế với mô hình C # được chấp nhận trong tâm trí. Và điều đó ổn. Một phần thưởng bổ sung của các trình vòng lặp là bạn có thể nhận được tất cả các công cụ LINQ song song miễn phí!
J Trana

2

Khi tôi có các hoạt động khởi tạo, điều kiện và gia tăng, tôi muốn sử dụng các vòng lặp cho các ngôn ngữ như C, C ++ và C #. Như thế này:

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

Hoặc điều này nếu bạn nghĩ rằng điều này là dễ đọc hơn. Cá nhân tôi thích cái đầu tiên.

for (int offset = 0, Record r = Read(offset); r != null; offset++, r = Read(offset)){
    // loop here!
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.