Liệu một iterator có một hợp đồng ngụ ý không phá hủy?


11

Giả sử tôi đang thiết kế một cấu trúc dữ liệu tùy chỉnh như ngăn xếp hoặc hàng đợi (ví dụ - có thể là một số bộ sưu tập có thứ tự tùy ý khác có phương thức pushpopphương thức logic tương đương - tức là các phương thức truy cập phá hoại).

Nếu bạn đang triển khai một trình vòng lặp (cụ thể là .NET IEnumerable<T>) trên bộ sưu tập này xuất hiện trên mỗi lần lặp, thì điều đó có phá vỡ IEnumerable<T>hợp đồng ngụ ý không?

IEnumerable<T>hợp đồng ngụ ý này?

ví dụ:

public IEnumerator<T> GetEnumerator()
{
    if (this.list.Count > 0)
        yield return this.Pop();
    else
        yield break;
}

Câu trả lời:


12

Tôi tin rằng một ĐTV hủy diệt vi phạm Nguyên tắc tối thiểu ngạc nhiên . Như một ví dụ giả định, hãy tưởng tượng một thư viện đối tượng kinh doanh cung cấp các hàm tiện lợi chung. Tôi hồn nhiên viết một hàm cập nhật một bộ sưu tập các đối tượng kinh doanh:

static void UpdateStatus<T>(IEnumerable<T> collection) where T : IBusinessObject
{
    foreach (var item in collection)
    {
        item.Status = BusinessObjectStatus.Foo;
    }
}

Một nhà phát triển khác không biết gì về triển khai của tôi hoặc việc triển khai của bạn một cách vô tư quyết định sử dụng cả hai. Có khả năng họ sẽ ngạc nhiên với kết quả:

//developer uses your type without thinking about the details
var collection = GetYourEnumerableType<SomeBusinessObject>();

//developer uses my function without thinking about the details
UpdateStatus<SomeBusinessObject>(collection);

Ngay cả khi nhà phát triển nhận thức được phép liệt kê phá hoại, họ có thể không nghĩ về hậu quả khi bàn giao bộ sưu tập cho chức năng hộp đen. Là tác giả của UpdateStatus, có lẽ tôi sẽ không xem xét liệt kê phá hoại trong thiết kế của mình.

Tuy nhiên, nó chỉ là một hợp đồng ngụ ý. Các bộ sưu tập .NET, bao gồm Stack<T>, thực thi một hợp đồng rõ ràng với InvalidOperationException- "Bộ sưu tập đã được sửa đổi sau khi điều tra viên được khởi tạo". Bạn có thể lập luận rằng một chuyên gia thực sự có thái độ làm trống cảnh báo đối với bất kỳ mã nào không phải là mã của họ. Sự ngạc nhiên của một điều tra viên phá hoại sẽ được phát hiện với thử nghiệm tối thiểu.


1
+1 - cho 'Nguyên tắc ngạc nhiên tối thiểu' Tôi sẽ trích dẫn điều đó với mọi người.

+1 Về cơ bản, đây cũng là điều tôi đã nghĩ, việc phá hoại có thể phá vỡ hành vi dự kiến ​​của các tiện ích mở rộng LINQ IEn. Nó chỉ cảm thấy kỳ lạ khi lặp lại loại bộ sưu tập được đặt hàng này mà không thực hiện các hoạt động bình thường / phá hoại.
Steven Evers

1

Một trong những thử thách mã hóa thú vị nhất được đưa ra cho tôi cho một cuộc phỏng vấn là tạo ra một hàng đợi chức năng. Yêu cầu là mỗi lệnh gọi enqueue sẽ tạo ra một hàng đợi mới bao gồm hàng đợi cũ và mục mới ở đuôi. Dequeue cũng sẽ trả lại một hàng đợi mới và vật phẩm bị mất như là một thông số.

Tạo một IEnumerator từ việc thực hiện này sẽ là không phổ biến. Và để tôi nói với bạn rằng việc thực hiện Hàng đợi chức năng hoạt động tốt khó khăn hơn rất nhiều so với việc thực hiện Ngăn chức năng biểu diễn (stack Push / Pop đều hoạt động trên Đuôi, vì Queue Enqueue hoạt động ở phần đuôi, dequeue hoạt động trên đầu).

Quan điểm của tôi là ... thật đơn giản để tạo ra một Trình liệt kê ngăn xếp không phá hủy bằng cách thực hiện cơ chế Con trỏ của riêng bạn (StackNode <T>) và sử dụng ngữ nghĩa chức năng trong Trình liệt kê.

public class Stack<T> implements IEnumerator<T>
{
  private class StackNode<T>
  {
    private readonly T _data;
    private readonly StackNode<T> _next;
    public StackNode(T data, StackNode<T> next)
    {
       _data=data;
       _next=next;
    }
    public <T> Data{get {return _data;}}
    public StackNode<T> Next{get {return _Next;}}
  }

  private StackNode<T> _head;

  public void Push(T item)
  {
    _head =new StackNode<T>(item,_head);
  }

  public T Pop()
  {
    //Add in handling for a null head (i.e. fresh stack)
    var temp=_head.Data;
    _head=_head.Next;
    return temp;
  }

  ///Here's the fun part
  public IEnumerator<T> GetEnumerator()
  {
    //make a copy.
    var current=_head;
    while(current!=null)
    {
       yield return current.Data;
       current=_head.Next;
    }
  }    
}

Một số điều cần lưu ý. Một cuộc gọi để đẩy hoặc bật trước câu lệnh current = _head; hoàn thành sẽ cung cấp cho bạn một ngăn xếp khác để liệt kê so với khi không có đa luồng (bạn có thể muốn sử dụng ReaderWriterLock để bảo vệ chống lại điều này). Tôi đã thực hiện các trường trong StackNode chỉ đọc nhưng tất nhiên nếu T là một đối tượng có thể thay đổi, bạn có thể thay đổi giá trị của nó. Nếu bạn định tạo một hàm tạo Stack lấy một StackNode làm tham số (và đặt đầu vào đó được truyền trong nút). Hai ngăn xếp được xây dựng theo cách này sẽ không tác động lẫn nhau (ngoại trừ chữ T có thể thay đổi như tôi đã đề cập). Bạn có thể Đẩy và bật tất cả những gì bạn muốn trong một ngăn xếp, cái kia sẽ không thay đổi.

Và bạn của tôi là cách bạn thực hiện phép liệt kê không phá hủy của Stack.


+1: Các liệt kê Stack và Queue không phá hủy của tôi được triển khai tương tự. Tôi đã suy nghĩ nhiều hơn về các dòng PQ / Heaps với các so sánh tùy chỉnh.
Steven Evers

1
Tôi muốn nói sử dụng cùng một cách tiếp cận ... không thể nói rằng tôi đã triển khai PQ chức năng trước đây ... có vẻ như bài viết này đề xuất sử dụng các chuỗi theo thứ tự như một xấp xỉ phi tuyến tính
Michael Brown

0

Trong nỗ lực giữ mọi thứ "đơn giản", .NET chỉ có một cặp giao diện chung / không chung cho những thứ được liệt kê bằng cách gọi GetEnumerator()và sau đó sử dụng MoveNextCurrenttrên đối tượng nhận được từ đó, mặc dù có ít nhất bốn loại đối tượng chỉ nên hỗ trợ các phương thức đó:

  1. Những thứ có thể được liệt kê ít nhất một lần, nhưng không nhất thiết phải nhiều hơn thế.
  2. Những thứ có thể được liệt kê một số lần tùy ý trong bối cảnh luồng tự do, nhưng có thể tùy ý mang lại nội dung khác nhau mỗi lần
  3. Những thứ có thể được liệt kê một số lần tùy ý trong các bối cảnh luồng tự do, nhưng có thể đảm bảo rằng nếu mã liệt kê chúng lặp đi lặp lại sẽ không gọi bất kỳ phương thức đột biến nào, tất cả các phép liệt kê sẽ trả về cùng một nội dung.
  4. Những thứ có thể được liệt kê một số lần tùy ý và được đảm bảo trả lại cùng một nội dung mỗi khi chúng tồn tại.

Bất kỳ trường hợp nào thỏa mãn một trong các định nghĩa được đánh số cao hơn cũng sẽ thỏa mãn tất cả các định nghĩa thấp hơn, nhưng mã yêu cầu một đối tượng thỏa mãn một trong các định nghĩa cao hơn có thể bị phá vỡ nếu được đưa ra một trong các định nghĩa thấp hơn.

Có vẻ như Microsoft đã quyết định rằng các lớp thực hiện IEnumerable<T>phải đáp ứng định nghĩa thứ hai, nhưng không có nghĩa vụ phải thỏa mãn bất cứ điều gì cao hơn. Có thể cho rằng, không có nhiều lý do mà một cái gì đó chỉ có thể đáp ứng định nghĩa đầu tiên nên thực hiện IEnumerable<T>hơn là IEnumerator<T>; nếu foreachcác vòng lặp có thể chấp nhận IEnumerator<T>, sẽ có ý nghĩa đối với những thứ chỉ có thể được liệt kê một lần để thực hiện giao diện sau. Thật không may, các loại chỉ thực hiện IEnumerator<T>ít thuận tiện hơn để sử dụng trong C # và VB.NET so với các loại có GetEnumeratorphương thức.

Trong mọi trường hợp, mặc dù nó sẽ hữu ích nếu có nhiều loại khác nhau cho những thứ có thể đảm bảo khác nhau, và / hoặc một cách tiêu chuẩn để hỏi một trường hợp thực hiện IEnumerable<T>những gì đảm bảo nó có thể tạo ra, không có loại nào tồn tại.


0

Tôi nghĩ rằng điều này sẽ vi phạm nguyên tắc thay thế Liskov , trong đó nêu rõ,

các đối tượng trong một chương trình nên có thể thay thế bằng các thể hiện của các kiểu con của chúng mà không làm thay đổi tính chính xác của chương trình đó.

Nếu tôi có một phương thức như sau được gọi nhiều lần trong chương trình của tôi,

PrintCollection<T>(IEnumerable<T> collection)
{
    foreach (T item in collection)
    {
        Console.WriteLine(item);
    }
}

Tôi có thể trao đổi bất kỳ IEnumerable nào mà không làm thay đổi tính chính xác của chương trình, nhưng sử dụng hàng đợi phá hủy sẽ thay đổi rộng rãi hành vi của chương trình.

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.