반복자가 비파괴 암시 적 계약을 가지고 있습니까?


11

스택이나 대기열과 같은 사용자 정의 데이터 구조를 설계한다고 가정 해 봅시다 (예 : 논리적으로 동등한 pushpop메소드 (예 : 파괴적 액세서 메소드) 를 갖는 다른 임의의 정렬 된 컬렉션 일 수 있음 ).

IEnumerable<T>각 반복에서 발생하는이 컬렉션에 대해 반복자를 .NET에서 (특히, ) 구현하는 경우 IEnumerable<T>암시적인 계약 이 위반 됩니까?

않습니다 IEnumerable<T>이 묵시적 계약이?

예 :

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

답변:


12

나는 파괴적 열거자가 최소 놀랍게도원칙을 위반한다고 생각합니다 . 고안된 예로, 일반적인 편의 기능을 제공하는 비즈니스 오브젝트 라이브러리를 상상해보십시오. 나는 무고하게 비즈니스 객체 컬렉션을 업데이트하는 함수를 작성합니다.

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

내 구현 또는 구현에 대해 전혀 모르는 다른 개발자는 두 가지를 모두 사용하기로 결심합니다. 결과에 놀라게 될 것입니다.

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

개발자가 파괴적인 열거를 알고 있더라도 컬렉션을 블랙 박스 함수로 전달할 때의 영향에 대해 생각하지 못할 수 있습니다. 의 저자로서 UpdateStatus, 아마도 내 디자인에서 파괴적인 열거를 고려하지 않을 것입니다.

그러나 이는 묵시적 계약일뿐입니다. 를 포함한 .NET 컬렉션 은 "열거자가 인스턴스화 된 후 컬렉션이 수정되었습니다" Stack<T>라는 명시 적 계약을 시행합니다 InvalidOperationException. 진정한 전문가는 자신의 코드가 아닌 코드에 대해 주의를 기울이는 경향 이 있다고 주장 할 수 있습니다. 파괴적인 열거 자의 놀람은 최소한의 테스트로 발견 될 것입니다.


1
+1- '최소한의 놀라움의 원리'에 대해서는 사람들에게 인용하겠습니다.

+1 이것은 기본적으로 내가 생각한 것이기도합니다. 또한 파괴적이라면 LINQ IEnumerable 확장의 예상되는 동작을 중단 할 수 있습니다. 정상적인 / 파괴적인 작업을 수행하지 않고 이러한 유형의 정렬 된 컬렉션을 반복하는 것이 이상하게 느껴집니다.
Steven Evers

1

인터뷰에서 내게 가장 흥미로운 코딩 과제 중 하나는 기능적 대기열을 만드는 것이 었습니다. 각 대기열에 대기 할 때마다 기존 대기열과 꼬리에 새 항목이 포함 된 새 대기열이 작성되어야합니다. 대기열에서 제외하면 새 대기열과 대기열에서 제외 된 항목이 출력 매개 변수로 반환됩니다.

이 구현에서 IEnumerator를 작성하는 것은 비파괴 적입니다. 또한 성능이 우수한 기능적 대기열을 구현하는 것이 성능이 뛰어난 기능적 스택을 구현하는 것보다 훨씬 어렵다고 가정합니다.

내 요점은 ... 자신 만의 포인터 메커니즘 (StackNode <T>)을 구현하고 열거 자에서 기능적 의미론을 사용하여 비파괴 적 스택 열거자를 만드는 것은 쉽지 않습니다.

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

몇 가지 참고할 사항. 명령문 current = _head 전에 푸시 또는 팝 호출. completes는 멀티 스레딩이없는 경우와는 다른 열거 형 스택을 제공합니다 (이를 방지하기 위해 ReaderWriterLock을 사용할 수 있습니다). StackNode에서 필드를 읽기 전용으로 만들었지 만 물론 T가 변경 가능한 객체이면 해당 값을 변경할 수 있습니다. StackNode를 매개 변수로 사용하는 스택 생성자를 만들고 노드에 전달 된 것으로 헤드를 설정하는 경우. 이런 식으로 구성된 두 스택은 서로 영향을 미치지 않습니다 (앞서 언급 한 변경 가능한 T 제외). 한 스택에서 원하는 것을 푸시하고 팝할 수 있으며 다른 스택은 변경되지 않습니다.

그리고 내 친구는 스택을 비파괴 적으로 열거하는 방법입니다.


+1 : 비파괴 스택 및 큐 열거자가 비슷하게 구현됩니다. 사용자 지정 비교기를 사용하여 PQ / 힙 라인에 대해 더 많이 생각하고있었습니다.
Steven Evers 2019

1
나는 같은 접근법을 사용한다고 말하지만 ... 전에 기능적 PQ를 구현했다고 말할 수는 없습니다 ... 이 기사 는 순서가 지정된 시퀀스를 비선형 근사치로 사용하는 것이 좋습니다
Michael Brown

0

사물을 "간단하게"유지하기 위해 .NET에는 최소한 4 가지 종류의 개체가 있더라도이를 호출 GetEnumerator()한 다음 사용 MoveNext하고 Current받는 개체에 대해 열거 된 사물에 대한 하나의 일반 / 비 제네릭 인터페이스 쌍만 있습니다. 그 방법 만 지원해야합니다.

  1. 적어도 한 번은 열거 할 수 있지만 반드시 그 이상이어야하는 것은 아닙니다.
  2. 자유 스레드 컨텍스트에서 임의의 횟수로 열거 될 수 있지만 매번 다른 컨텐츠를 임의로 생성 할 수있는 것
  3. 자유 스레드 컨텍스트에서 임의의 횟수로 열거 될 수 있지만 반복적으로 열거하는 코드가 변경 메소드를 호출하지 않는 경우 모든 열거가 동일한 컨텐츠를 리턴 함을 보장 할 수 있습니다.
  4. 임의의 횟수로 열거 될 수 있고 존재하는 한 매번 동일한 컨텐츠를 리턴하도록 보장되는 것.

높은 번호의 정의 중 하나를 만족하는 모든 인스턴스는 모든 하위 정의를 만족하지만 높은 정의 중 하나를 만족하는 객체를 필요로하는 코드는 하위 정의 중 하나가 주어지면 중단 될 수 있습니다.

Microsoft는 구현하는 클래스가 IEnumerable<T>두 번째 정의를 만족해야한다고 결정 했지만 더 높은 것을 만족시킬 의무는 없습니다. 논란의 여지가 있지만, 첫 번째 정의 만 충족시킬 수있는 IEnumerable<T>것이 오히려 구현해야하는 이유는 많지 않습니다 IEnumerator<T>. foreach루프가 받아 들일 수 있다면 IEnumerator<T>, 후자의 인터페이스를 간단히 구현하기 위해 한 번만 열거 할 수있는 것들에 적합합니다. 불행히도 IEnumerator<T>C # 및 VB.NET에서 구현하는 유형 만 GetEnumerator메서드 가있는 유형보다 사용하기 편리하지 않습니다 .

어쨌든, 다른 보장을 할 수있는 것들에 대해 열거 가능한 유형이 다르거 나 / 또는 IEnumerable<T>보장 할 수있는 것을 구현하는 인스턴스를 요청하는 표준 방법이있는 경우 유용 하지만 아직 존재하지 않는 유형은 없습니다.


0

이것이 Liskov 대체 원칙 을 위반하는 것이라고 생각합니다 .

프로그램의 오브젝트는 해당 프로그램의 정확성을 변경하지 않고 서브 타입의 인스턴스로 대체 할 수 있어야합니다.

내 프로그램에서 다음과 같은 방법을 두 번 이상 호출하면

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

프로그램의 정확성을 변경하지 않고 IEnumerable을 바꿀 수 있어야하지만 파괴 큐를 사용하면 프로그램 동작이 광범위하게 변경됩니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.