การตรวจจับ IEnumerable“ สถานะเครื่อง”


17

ฉันเพิ่งอ่านบทความที่น่าสนใจที่เรียกว่าน่ารักเกินไปกับผลตอบแทน c #

มันทำให้ฉันสงสัยว่าวิธีที่ดีที่สุดคือการตรวจสอบว่า IEnumerable เป็นคอลเล็กชันที่นับได้จริงหรือว่าเป็นเครื่องสถานะที่สร้างด้วยคีย์เวิร์ด yield

ตัวอย่างเช่นคุณสามารถแก้ไข DoubleXValue (จากบทความ) เป็นดังนี้:

private void DoubleXValue(IEnumerable<Point> points)
{
    if(points is List<Point>)
      foreach (var point in points)
        point.X *= 2;
    else throw YouCantDoThatException();
}

คำถามที่ 1) มีวิธีที่ดีกว่าในการทำเช่นนี้หรือไม่?

คำถามที่ 2) นี่คือสิ่งที่ฉันควรกังวลเมื่อสร้าง API หรือไม่


1
คุณควรใช้อินเตอร์เฟซมากกว่าคอนกรีตและยังหล่อลดลงจะเป็นทางเลือกที่ดีกว่าที่จะไม่ทั้งหมดที่มีคอลเลกชันICollection<T> List<T>ยกตัวอย่างเช่นอาร์เรย์Point[]ดำเนินการแต่ไม่IList<T> List<T>
Trevor Pilley

3
ยินดีต้อนรับสู่ปัญหาของประเภทที่ไม่แน่นอน Ienumerable มีความหมายว่าควรเป็นประเภทข้อมูลที่ไม่เปลี่ยนรูปดังนั้นการใช้งานการกลายพันธุ์จึงเป็นการละเมิด LSP บทความนี้เป็นคำอธิบายที่สมบูรณ์แบบเกี่ยวกับอันตรายของผลข้างเคียงดังนั้นฝึกเขียนประเภท C # ของคุณให้เป็นผลข้างเคียงโดยไม่เสียค่าใช้จ่ายมากที่สุดและคุณไม่ต้องกังวลเกี่ยวกับเรื่องนี้
จิมมี่ฮอฟฟา

1
@JimmyHoffa IEnumerableเปลี่ยนแปลงไม่ได้ในแง่ที่ว่าคุณไม่สามารถแก้ไขคอลเล็กชัน (เพิ่ม / ลบ) ในขณะที่แจกแจงได้อย่างไรก็ตามหากวัตถุในรายการไม่แน่นอนจึงมีความเหมาะสมอย่างยิ่งที่จะแก้ไขสิ่งเหล่านั้นในขณะที่แจกแจงรายการ
Trevor Pilley

@ TrevorPilley ฉันไม่เห็นด้วย หากคอลเลกชันที่คาดว่าจะไม่เปลี่ยนรูปแบบความคาดหวังจากผู้บริโภคคือสมาชิกจะไม่ถูกกลายพันธุ์โดยนักแสดงภายนอกที่จะเรียกว่าผลข้างเคียงและละเมิดความคาดหวังที่ไม่สามารถเปลี่ยนแปลงได้ ละเมิด POLA และ LSP โดยนัย
จิมมี่ฮอฟฟา

วิธีการเกี่ยวกับการใช้งานToList? msdn.microsoft.com/en-us/library/bb342261.aspxตรวจไม่พบว่ามันถูกสร้างขึ้นโดยผลตอบแทน แต่มันทำให้มันไม่เกี่ยวข้อง
luiscubal

คำตอบ:


22

คำถามของคุณที่ฉันเข้าใจดูเหมือนว่าจะขึ้นอยู่กับหลักฐานที่ไม่ถูกต้อง ให้ฉันดูว่าฉันสามารถสร้างเหตุผลใหม่ได้หรือไม่:

  • บทความที่เชื่อมโยงไปยังอธิบายถึงวิธีที่ลำดับที่สร้างขึ้นโดยอัตโนมัติแสดงพฤติกรรม "ขี้เกียจ" และแสดงให้เห็นว่าสิ่งนี้สามารถนำไปสู่ผลลัพธ์ที่ตอบโต้ได้ง่ายหรือไม่
  • ดังนั้นฉันสามารถตรวจสอบว่าอินสแตนซ์ที่กำหนดของ IEnumerable กำลังจะแสดงพฤติกรรมขี้เกียจนี้โดยการตรวจสอบเพื่อดูว่ามันถูกสร้างขึ้นโดยอัตโนมัติ
  • ฉันจะทำอย่างไร

ปัญหาคือว่าหลักฐานที่สองเป็นเท็จ แม้ว่าคุณจะสามารถตรวจสอบได้ว่า IEnumerable ที่กำหนดหรือไม่นั้นเป็นผลมาจากการแปลงบล็อกตัววนซ้ำ (และใช่มีวิธีการทำเช่นนั้น) มันจะไม่ช่วยได้เนื่องจากการสันนิษฐานผิด เรามาอธิบายสาเหตุ

class M { public int P { get; set; } }
class C
{
  public static IEnumerable<M> S1()
  {
    for (int i = 0; i < 3; ++i) 
      yield return new M { P = i };
  }

  private static M[] ems = new M[] 
  { new M { P = 0 }, new M { P = 1 }, new M { P = 2 } };
  public static IEnumerable<M> S2()
  {
    for (int i = 0; i < 3; ++i)
      yield return ems[i];
  }

  public static IEnumerable<M> S3()
  {
    return new M[] 
    { new M { P = 0 }, new M { P = 1 }, new M { P = 2 } };
  }

  private class X : IEnumerable<M>
  {
    public IEnumerator<X> GetEnumerator()
    {
      return new XEnum();
    }
    // Omitted: non generic version
    private class XEnum : IEnumerator<X>
    {
      int i = 0;
      M current;
      public bool MoveNext()
      {
        current = new M() { P = i; }
        i += 1;
        return true;
      }
      public M Current { get { return current; } }
      // Omitted: other stuff.
    }
  }

  public static IEnumerable<M> S4()
  {
    return new X();
  }

  public static void Add100(IEnumerable<M> items)
  {
    foreach(M item in items) item.P += 100;
  }
}

เอาล่ะเรามีสี่วิธี S1 และ S2 เป็นลำดับที่สร้างขึ้นโดยอัตโนมัติ S3 และ S4 เป็นลำดับที่สร้างขึ้นด้วยตนเอง ตอนนี้สมมติว่าเรามี:

var items = C.Sn(); // S1, S2, S3, S4
S.Add100(items);
Console.WriteLine(items.First().P);

ผลลัพธ์สำหรับ S1 และ S4 จะเป็น 0 ทุกครั้งที่คุณระบุลำดับคุณจะได้รับการอ้างอิงใหม่กับ M ที่สร้างขึ้น ผลลัพธ์สำหรับ S2 และ S3 จะเท่ากับ 100 ทุกครั้งที่คุณระบุลำดับคุณจะได้รับการอ้างอิงเดียวกันกับ M ที่คุณได้รับครั้งล่าสุด ไม่ว่าจะเป็นรหัสลำดับที่ถูกสร้างขึ้นโดยอัตโนมัติหรือไม่เป็นคำถามที่ว่าวัตถุที่ระบุมีการอ้างอิงตัวตนหรือไม่ ทั้งสองคุณสมบัติ - การสร้างอัตโนมัติและตัวตนของการอ้างอิง - จริงๆแล้วไม่มีอะไรเกี่ยวข้องกัน บทความที่คุณเชื่อมโยงกับ conflates พวกเขาบ้าง

เว้นแต่ว่าผู้ให้บริการลำดับนั้นจะได้รับการบันทึกไว้เช่นเดียวกับการเสนอวัตถุที่มีข้อมูลประจำตัวอ้างอิงอยู่เสมอ


2
เราทุกคนรู้ว่าคุณจะมีคำตอบที่ถูกต้องที่นี่นั่นคือที่ได้รับ; แต่ถ้าคุณสามารถใส่คำจำกัดความเพิ่มเติมลงในคำว่า "อัตลักษณ์อ้างอิง" มันอาจจะดีฉันกำลังอ่านมันว่า "ไม่เปลี่ยนรูป" แต่นั่นเป็นเพราะฉันทำให้ไม่สามารถเปลี่ยนแปลงได้ใน C # กับวัตถุใหม่ที่ส่งคืนทุกประเภทหรือมูลค่า เป็นสิ่งเดียวกับที่คุณพูดถึง แม้ว่าการเปลี่ยนแปลงที่ไม่ได้รับสามารถทำได้ในหลากหลายวิธี แต่เป็นวิธีเดียวใน C # (ที่ฉันรู้คุณอาจรู้วิธีที่ฉันไม่รู้)
จิมมี่ฮอฟฟา

2
@JimmyHoffa: การอ้างอิงวัตถุสองรายการ x และ y มี "referential identity" ถ้า Object.ReferenceEquals (x, y) ส่งกลับค่าจริง
Eric Lippert

ยินดีเสมอที่ได้รับการตอบกลับจากแหล่งที่มาโดยตรง ขอบคุณเอริค
ConditionRacer

10

ฉันคิดว่าแนวคิดหลักคือคุณควรปฏิบัติต่อIEnumerable<T>คอลเลกชันใด ๆที่ไม่เปลี่ยนรูป : คุณไม่ควรแก้ไขวัตถุจากมันคุณควรสร้างคอลเลกชันใหม่ด้วยวัตถุใหม่:

private IEnumerable<Point> DoubleXValue(IEnumerable<Point> points)
{
    foreach (var point in points)
        yield return new Point(point.X * 2, point.Y);
}

(หรือเขียนแบบเดียวกันอย่างกระชับยิ่งขึ้นโดยใช้ LINQ Select())

หากคุณต้องการแก้ไขรายการในคอลเลกชันอย่าใช้IEnumerable<T>ใช้IList<T>(หรืออาจเป็นไปได้IReadOnlyList<T>ว่าถ้าคุณไม่ต้องการแก้ไขคอลเลกชันเองมีเพียงรายการในนั้นและคุณอยู่ใน. Net 4.5):

private void DoubleXValue(IList<Point> points)
{
    foreach (var point in points)
        point.X *= 2;
}

วิธีนี้หากมีคนพยายามใช้วิธีการของคุณด้วยIEnumerable<T>ก็จะล้มเหลวในเวลารวบรวม


1
คีย์จริงเป็นเหมือนที่คุณพูดส่งคืนค่าตัวเลขใหม่พร้อมการแก้ไขเมื่อคุณต้องการเปลี่ยนบางสิ่ง Ienumerable เป็นประเภทที่ทำงานได้อย่างสมบูรณ์และไม่มีเหตุผลที่จะเปลี่ยนมันเป็นเพียงเทคนิคในการใช้มันต้องเข้าใจตามที่คุณพูด +1 สำหรับการกล่าวถึงวิธีการทำงานกับประเภทที่ไม่เปลี่ยนรูป
จิมมี่ฮอฟฟา

1
ค่อนข้างเกี่ยวข้อง - คุณควรปฏิบัติต่อทุก ๆ IEnumerable ราวกับว่ามันทำผ่านการดำเนินการที่ถูกเลื่อนออกไป สิ่งที่อาจเป็นจริงในขณะที่คุณดึงข้อมูลอ้างอิง IEnumerable ของคุณอาจไม่เป็นจริงเมื่อคุณระบุผ่านจริง ตัวอย่างเช่นการส่งคืนคำสั่ง LINQ ภายในล็อคอาจทำให้ได้ผลลัพธ์ที่ไม่คาดคิด
Dan Lyons

@JimmyHoffa: ฉันเห็นด้วย ฉันก้าวไปอีกขั้นและพยายามหลีกเลี่ยงการวนซ้ำIEnumerableมากกว่าหนึ่งครั้ง จำเป็นต้องทำมากกว่าหนึ่งครั้งสามารถ obviated โดยการโทรToList()และวนซ้ำในรายการ แต่ในกรณีเฉพาะที่แสดงโดย OP ฉันชอบ svick ของโซลูชันที่มี DoubleXValue ส่งคืน IEnumerable แน่นอนในรหัสจริงฉันอาจจะใช้LINQเพื่อสร้างประเภทIEnumerableนี้ อย่างไรก็ตามทางเลือกของ svick นั้นyieldสมเหตุสมผลในบริบทของคำถามของ OP
Brian

@Brian Yeah ฉันไม่ต้องการเปลี่ยนรหัสมากเกินไปเพื่อให้ประเด็นของฉัน Select()ในรหัสจริงผมจะได้นำมาใช้ และเกี่ยวกับการทำซ้ำหลายรายการIEnumerable: ReSharper มีประโยชน์กับสิ่งนั้นเพราะมันสามารถเตือนคุณได้เมื่อคุณทำเช่นนั้น
svick

5

โดยส่วนตัวฉันคิดว่าปัญหาที่เน้นในบทความนั้นเกิดจากการใช้IEnumerableอินเทอร์เฟซมากเกินไปโดยผู้พัฒนา C # จำนวนมากในปัจจุบัน ดูเหมือนว่าจะเป็นประเภทเริ่มต้นเมื่อคุณต้องการชุดของวัตถุ - แต่มีข้อมูลจำนวนมากที่IEnumerableไม่ได้บอกคุณ ... เป็นเรื่องสำคัญ: ไม่ว่าจะได้รับการแก้ไขหรือไม่ *

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

แก้ไขสำหรับ downvoters โปรดย้อนกลับและอ่านคำถามอย่างระมัดระวัง OP กำลังขอวิธีในการตรวจสอบให้แน่ใจว่าIEnumerableอินสแตนซ์ถูกทำให้เป็นรูปธรรมก่อนที่จะถูกส่งผ่านไปยังวิธีการ API ของเขา คำตอบของฉันตอบคำถามนั้น มีปัญหาที่กว้างขึ้นเกี่ยวกับวิธีการใช้ที่ถูกต้องIEnumerableและความคาดหวังของรหัสที่วาง OP นั้นขึ้นอยู่กับความเข้าใจผิดของปัญหาที่กว้างกว่านั้น แต่ OP ไม่ได้ถามวิธีการใช้อย่างถูกต้องIEnumerableหรือวิธีการทำงานกับประเภทที่ไม่เปลี่ยนรูป . มันไม่ยุติธรรมที่จะลงคะแนนฉันโดยไม่ตอบคำถามที่ไม่ได้โพสต์

* ผมใช้คำว่าreifyคนอื่น ๆ ใช้หรือstabilize materializeฉันไม่คิดว่าจะมีการประชุมร่วมกันในชุมชน


2
ปัญหาอย่างหนึ่งของICollectionและIListก็คือพวกมันไม่แน่นอน (หรืออย่างน้อยก็มองอย่างนั้น) IReadOnlyCollectionและIReadOnlyListจาก. Net 4.5 แก้ไขปัญหาเหล่านั้นบางอย่าง
svick

กรุณาอธิบาย downvote?
MattDavey

1
ฉันไม่เห็นด้วยที่คุณควรเปลี่ยนประเภทที่คุณใช้ Ienumerable เป็นประเภทที่ดีและมักจะได้ประโยชน์จากการเปลี่ยนไม่ได้ ฉันคิดว่าปัญหาที่แท้จริงคือคนไม่เข้าใจวิธีการทำงานกับประเภทที่ไม่เปลี่ยนรูปแบบใน C # ไม่แปลกใจเลยว่ามันเป็นภาษาที่พูดไม่ออกมากดังนั้นจึงเป็นแนวคิดใหม่สำหรับนักพัฒนาซอฟต์แวร์ C # หลายคน
จิมมี่ฮอฟฟา

เฉพาะ ienumerable เท่านั้นที่อนุญาตให้มีการประมวลผลที่เลื่อนออกไปดังนั้นการไม่อนุญาตให้ใช้เป็นพารามิเตอร์จะลบหนึ่งคุณลักษณะทั้งหมดของ C #
Jimmy Hoffa

2
ฉันลงคะแนนเพราะฉันคิดว่ามันผิด ฉันคิดว่ามันอยู่ในจิตวิญญาณของ SE เพื่อให้มั่นใจว่าผู้คนจะไม่ใส่ข้อมูลที่ไม่ถูกต้องมันขึ้นอยู่กับพวกเราทุกคนที่จะลงคะแนนในคำตอบเหล่านั้น บางทีฉันผิดอาจเป็นไปได้ทั้งหมดว่าฉันผิดและคุณพูดถูก ขึ้นอยู่กับชุมชนที่กว้างขึ้นที่จะตัดสินใจด้วยการลงคะแนน ขออภัยคุณทำแบบส่วนตัวถ้าเป็นคำปลอบใจที่ฉันได้รับคำตอบเชิงลบมากมายที่ฉันเขียนและลบออกไปอย่างรวดเร็วเพราะฉันยอมรับว่าชุมชนคิดว่ามันผิดดังนั้นฉันจึงไม่ถูกต้อง
จิมมี่ฮอฟฟา
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.