ตัววนซ้ำมีสัญญาโดยนัยแบบไม่ทำลายหรือไม่?


11

สมมติว่าฉันกำลังออกแบบโครงสร้างข้อมูลที่กำหนดเองเช่นสแต็กหรือคิว (ตัวอย่างเช่น - อาจเป็นคอลเลกชันที่สั่งโดยพลการอื่น ๆ ที่มีตรรกะเทียบเท่าpushและpopวิธีการ - นั่นคือวิธีการเข้าถึงแบบทำลายล้าง)

หากคุณกำลังใช้ตัววนซ้ำ (ใน. NET โดยเฉพาะIEnumerable<T>) กับคอลเล็กชันนี้ที่ปรากฏในแต่ละการวนซ้ำนั่นจะเป็นการผิด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 collection ซึ่งรวมถึงการStack<T>บังคับใช้สัญญาอย่างชัดเจนกับInvalidOperationException- "Collection ได้รับการแก้ไขหลังจากที่ enumerator ถูกสร้างอินสแตนซ์" คุณสามารถยืนยันว่าเป็นมืออาชีพที่แท้จริงมีข้อแม้ emptorทัศนคติต่อรหัสใด ๆ ที่ไม่ได้เป็นของตัวเอง ความประหลาดใจของตัวแจงนับทำลายจะถูกค้นพบด้วยการทดสอบขั้นต่ำ


1
+1 - สำหรับ 'หลักการแห่งความพิศวงน้อยที่สุด' ฉันจะพูดแบบนั้นกับผู้คน

+1 นี่คือสิ่งที่ฉันคิดด้วยเช่นกันการทำลายอาจทำลายพฤติกรรมที่คาดหวังของส่วนขยายที่นับจำนวน IEQ ของ LINQ มันรู้สึกแปลกที่จะย้ำกับคอลเลกชันที่สั่งซื้อประเภทนี้โดยไม่ต้องดำเนินการตามปกติ / การทำลายล้าง
Steven Evers

1

หนึ่งในความท้าทายการเขียนโค้ดที่น่าสนใจที่สุดที่ฉันได้รับจากการสัมภาษณ์คือการสร้างคิวการทำงาน ความต้องการคือการเรียกเข้าสู่คิวแต่ละครั้งจะสร้างคิวใหม่ที่มีการกำหนดคิวเก่าและรายการใหม่ที่ส่วนท้าย Dequeue จะส่งคืนคิวใหม่และรายการที่ dequeued เป็นพารามิเตอร์ออก

การสร้าง IEnumerator จากการนำไปใช้นี้จะไม่เป็นการทำลาย และให้ฉันบอกคุณว่าการใช้ Functional Queue นั้นทำงานได้ดีนั้นยากกว่าการใช้ Function Stack ที่มีประสิทธิภาพ (Stack Push / Pop ทั้งสองทำงานที่ Tail สำหรับ Queue Enqueue ทำงานที่ส่วนท้าย

ประเด็นของฉันคือ ... มันเป็นเรื่องไม่สำคัญที่จะสร้างตัวแบ่ง Stack แบบไม่ทำลายโดยใช้กลไก Pointer ของคุณเอง (StackNode <T>) และใช้ semantics เชิงหน้าที่ใน Enumerator

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

บางสิ่งที่ควรทราบ การเรียกเพื่อพุชหรือป๊อปก่อนคำสั่งปัจจุบัน = _head; การเติมเต็มจะให้สแต็กที่แตกต่างกันสำหรับการแจงนับกว่าที่ไม่มีมัลติเธรด (คุณอาจต้องการใช้ ReaderWriterLock เพื่อป้องกันสิ่งนี้) ฉันสร้างเขตข้อมูลใน StackNode แบบอ่านอย่างเดียว แต่แน่นอนถ้า T เป็นวัตถุที่ไม่แน่นอนคุณสามารถเปลี่ยนค่าได้ หากคุณต้องสร้างตัวสร้างสแต็กที่ใช้ StackNode เป็นพารามิเตอร์ (และตั้งค่าส่วนหัวให้เป็นผ่านในโหนด) สองกองที่สร้างด้วยวิธีนี้จะไม่ส่งผลกระทบซึ่งกันและกัน (ยกเว้น T ที่ไม่แน่นอนดังที่ฉันกล่าวไว้) คุณสามารถผลักดันและป๊อปทั้งหมดที่คุณต้องการในหนึ่งกองอื่น ๆ จะไม่เปลี่ยนแปลง

และเพื่อนของฉันเป็นวิธีที่คุณทำการแจงนับแบบไม่ทำลาย


+1: ตัวแจงนับที่ไม่ทำลายและคิวของฉันถูกนำไปใช้ในทำนองเดียวกัน ฉันคิดมากขึ้นตามแนวของ PQs / Heaps กับตัวเปรียบเทียบแบบกำหนดเอง
Steven Evers

1
ฉันจะบอกว่าใช้วิธีการเดียวกัน ... ไม่สามารถพูดได้ว่าฉันใช้งาน PQ ก่อนหน้านี้แล้ว ... ดูเหมือนว่าบทความนี้จะแนะนำให้ใช้ลำดับที่ได้รับคำสั่งเป็นการประมาณแบบไม่เชิงเส้น
Michael Brown

0

ในความพยายามที่จะทำให้สิ่งต่าง ๆ "ง่าย". NET มีคู่ทั่วไป / ไม่ใช่คู่เดียวของอินเทอร์เฟซสำหรับสิ่งต่าง ๆ ที่ระบุโดยการโทร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.