C #: Iterator so với hàm trả về IEnumerable

Oct 15 2020

Giả sử a có hai phương pháp:

IEnumerable<int> DoSomething1();
IEnumerable<int> DoSomething2();

sửa đổi trạng thái của đối tượng của tôi. Tôi không biết nếu hàm sử dụng yieldbên trong hay chỉ trả về a List<int>.

Và sau đó tôi muốn lấy đầu ra và chuyển sang hai hàm khác:

void SendToUser(IEnumerable<int> values);
void PrintOut(IEnumerable<int> values);

Sau đó, chỉ dựa trên giao diện hàm ( DoSomethingX), tôi không thể nói đây có phải là một hoạt động hợp lệ hay không:

var values = DoSomethingX();
SendToUser(values);
PrintOut(values)

Vì trong trường hợp của trình lặp, nó sẽ dẫn đến việc gọi DoSomethingXhai lần.

Đây có phải là một số loại không nhất quán hoặc tôi đang sử dụng trình lặp / IEnumerabletheo cách sai? Vấn đề ở đâu?

Trả lời

4 JohnathanBarclay Oct 15 2020 at 19:13

Nếu một phương thức trả về IEnumerablethì bạn chỉ nên dựa vào thực tế là nó có thể lặp lại, không cần gì hơn, vì việc triển khai phương thức đó có thể thay đổi.

Nếu bạn có quyền kiểm soát phương pháp và biết rằng người tiêu dùng yêu cầu một loại cụ thể hơn, thì hãy trả lại một loại cụ thể hơn.

Nếu bạn không có quyền kiểm soát và cần thực hiện hành vi danh sách, bạn luôn có thể chuyển đổi IEnumerablethành danh sách:

var values = DoSomethingX().ToList();
SendToUser(values);
PrintOut(values);
1 apocalypse Oct 15 2020 at 19:50

Đọc về CQS (tách truy vấn lệnh): Wikipedia

Tóm lại, có 2 loại phương pháp:

  1. các lệnh, ví dụ void DoSomething(string data)- chúng thay đổi trạng thái của phiên bản của bạn
  2. các truy vấn, ví dụ IEnumerable<Person> GetPeople(Filter filter)- không thay đổi trạng thái, chúng chỉ trả về một số dữ liệu

Đôi khi các lệnh có thể trả về giá trị, tức là PersonID CreatePerson(...);

Vì vậy, nếu bạn cấu trúc lại mã của mình thành:

void DoSomething1();
void DoSomething2();
IEnumerable<int> GetData();

nó sẽ giải quyết vấn đề của bạn.

Các IEnumerableloại chỉ là một giao diện cho phép bạn duyệt qua một số yếu tố. Bạn không biết bên trong triển khai cụ thể là gì.
Thông thường việc thực hiện IEnumerablekhông nên có bất kỳ tác dụng phụ nào, tức là thay đổi trạng thái của một số đối tượng.

Nếu bạn đang xử lý IEnumerabletrạng thái thay đổi đó, bạn luôn có thể hiện thực hóa nó bằng cách sử dụng .ToList()tiện ích mở rộng LINQ. Sau đó, bạn có thể chuyển nó cho bất kỳ phương pháp nào nếu không có bất kỳ tác dụng phụ nào.

OlivierJacot-Descombes Oct 15 2020 at 20:11

Việc gán var values = DoSomethingX();không liệt kê bộ sưu tập hoặc trình lặp mà nó trả về.

  1. Nó có thể trả về a List<int>. Trong trường hợp này, danh sách được gán cho values. Chính xác hơn: một tham chiếu đến danh sách này.

  2. Nếu DoSomethingXlà một phương thức trình lặp, thì C # tạo một đối tượng của một lớp ẩn danh triển khai IEnumerable<int>như một máy trạng thái. Đối tượng này được gán cho values.

Vì vậy, không có sự khác biệt trong cách sử dụng của hai IEnumerable<int>. Không quan trọng là nó dựa trên một tập hợp hay một phương thức trình lặp. Sự lặp lại thường bắt đầu trong một vòng lặp for-each hoặc khi gọi một phương thức LINQ như ToListhoặc ToArray.

Bạn có thể lặp lại theo cách thủ công. Đầu tiên bạn phải có được điều tra viên:

IEnumerator<int> enumerator = values.GetEnumerator();

Sau đó, gọi các phương thức của điều tra viên này:

while (enumerator.MoveNext()) { // Enumeration starts here.
    int value = enumerator.Current;
    ...
}

Khi bạn lặp lại IEnumerablehoặc lặp lại IEnumerable<T>hai lần, tất nhiên mã của nó sẽ chạy hai lần. Nếu bạn cho rằng việc này có thể tốn thời gian, ví dụ: nếu trình vòng lặp truy vấn cơ sở dữ liệu hoặc hệ thống tệp, thì bạn nên gọi .ToList()trước khi gọi nó hai lần:

var records = QueryDatabaseReturnsIEnumerable().ToList();
SendToUser(records);
PrintOut(records);

Mặt khác, gọi .ToList()khi liệt kê dựa trên một tập hợp, khi đó bạn thực hiện thao tác sao chép bộ nhớ không cần thiết và tốn thời gian.

Thông thường bạn có thể nói từ ngữ cảnh, liệu điều này có cần thiết hay không. Nếu không, tốt nhất bạn có thể làm là kiểm tra các giao diện khác:

IEnumerable<int> values = DoSomethingX();
if (!(values is IList<int>) && !(values is ICollection<int>)) {
    values = values.ToList();
}
SendToUser(values);
PrintOut(values);

Nhưng tôi nghi ngờ rằng đây là một thực hành tốt. Các giao diện nên được sử dụng mà không cần lo lắng về việc thực hiện chúng. Làm như vậy là bất chấp mục đích của họ.