ReadOnlyCollection หรือ IEnumerable สำหรับการเปิดเผยคอลเลกชันของสมาชิก?


124

มีเหตุผลใดหรือไม่ที่จะเปิดเผยคอลเลกชันภายในเป็น ReadOnlyCollection แทนที่จะเป็น IEnumerable หากรหัสการโทรวนซ้ำบนคอลเลกชันเท่านั้น

class Bar
{
    private ICollection<Foo> foos;

    // Which one is to be preferred?
    public IEnumerable<Foo> Foos { ... }
    public ReadOnlyCollection<Foo> Foos { ... }
}


// Calling code:

foreach (var f in bar.Foos)
    DoSomething(f);

อย่างที่ฉันเห็น IEnumerable เป็นส่วนย่อยของอินเทอร์เฟซของ ReadOnlyCollection และไม่อนุญาตให้ผู้ใช้แก้ไขคอลเล็กชัน ดังนั้นหากอินเทอร์เฟซ IEnumberable เพียงพอนั่นคือสิ่งที่ต้องใช้ นั่นเป็นวิธีที่เหมาะสมในการหาเหตุผลเกี่ยวกับเรื่องนี้หรือฉันพลาดอะไรไป?

ขอบคุณ / Erik


6
หากคุณใช้. NET 4.5 คุณอาจต้องการลองใช้อินเทอร์เฟซคอลเลคชันแบบอ่านอย่างเดียวใหม่ คุณยังคงต้องการรวมคอลเลคชันที่ส่งคืนในReadOnlyCollectionกรณีที่คุณหวาดระแวง แต่ตอนนี้คุณไม่ได้ผูกติดอยู่กับการนำไปใช้งานใด
StriplingWarrior

คำตอบ:


96

โซลูชันที่ทันสมัยกว่า

หากคุณไม่ต้องการให้คอลเลกชันภายในเปลี่ยนแปลงได้คุณสามารถใช้System.Collections.Immutableแพ็กเกจเปลี่ยนประเภทฟิลด์ของคุณเป็นคอลเล็กชันที่ไม่เปลี่ยนรูปได้จากนั้นเปิดเผยสิ่งนั้นโดยตรงโดยสมมติว่าFooตัวเองไม่เปลี่ยนรูปแน่นอน

อัปเดตคำตอบเพื่อตอบคำถามได้ตรงประเด็นมากขึ้น

มีเหตุผลใดหรือไม่ที่จะเปิดเผยคอลเลกชันภายในเป็น ReadOnlyCollection แทนที่จะเป็น IEnumerable หากรหัสการโทรวนซ้ำบนคอลเลกชันเท่านั้น

ขึ้นอยู่กับว่าคุณเชื่อถือรหัสการโทรมากแค่ไหน หากคุณสามารถควบคุมทุกสิ่งที่จะเรียกสมาชิกรายนี้ได้อย่างสมบูรณ์และคุณรับประกันได้ว่าจะไม่มีรหัสใดใช้:

ICollection<Foo> evil = (ICollection<Foo>) bar.Foos;
evil.Add(...);

แน่นอนว่าจะไม่เกิดอันตรายใด ๆ หากคุณส่งคืนคอลเลกชันโดยตรง โดยทั่วไปแล้วฉันพยายามที่จะหวาดระแวงมากกว่านั้นเล็กน้อย

ในทำนองเดียวกันอย่างที่คุณพูด: ถ้าคุณต้องการ เพียงอย่างเดียวIEnumerable<T>ทำไมต้องผูกมัดตัวเองกับสิ่งที่แข็งแกร่งกว่า?

คำตอบเดิม

หากคุณใช้. NET 3.5 คุณสามารถหลีกเลี่ยงการทำสำเนาและหลีกเลี่ยงการแคสต์แบบธรรมดาได้โดยใช้การเรียกง่ายๆเพื่อข้าม:

public IEnumerable<Foo> Foos {
    get { return foos.Skip(0); }
}

(มีตัวเลือกอื่น ๆ อีกมากมายสำหรับการตัดทอนเล็กน้อย - สิ่งที่ดีเกี่ยวกับSkipSelect / Where คือไม่มีผู้รับมอบสิทธิ์ที่จะดำเนินการอย่างไร้จุดหมายสำหรับการทำซ้ำแต่ละครั้ง)

หากคุณไม่ได้ใช้. NET 3.5 คุณสามารถเขียน Wrapper ง่ายๆเพื่อทำสิ่งเดียวกัน:

public static IEnumerable<T> Wrapper<T>(IEnumerable<T> source)
{
    foreach (T element in source)
    {
        yield return element;
    }
}

15
โปรดทราบว่ามีการลงโทษด้านประสิทธิภาพสำหรับสิ่งนี้: วิธีการนับ AFAIK the Enumerable.Count ได้รับการปรับให้เหมาะสมสำหรับคอลเล็กชันที่ส่งลงใน IEnumerables แต่ไม่ใช่สำหรับช่วงที่สร้างโดย Skip (0)
shojtsy

6
-1 เป็นแค่ฉันหรือนี่คือคำตอบสำหรับคำถามอื่น? การรู้จัก "โซลูชัน" นี้มีประโยชน์ แต่หน่วยงานได้ขอให้เปรียบเทียบระหว่างการส่งคืน ReadOnlyCollection และ IEnumerable คำตอบนี้ถือว่าคุณต้องการส่งคืน IEnumerable โดยไม่มีเหตุผลสนับสนุนการตัดสินใจนั้น
Zaid Masud

1
คำตอบแสดงการแคสต์ReadOnlyCollectionไปยังICollectionและรันAddสำเร็จแล้ว เท่าที่ฉันรู้นั่นไม่น่าจะเป็นไปได้ evil.Add(...)ควรโยนความผิดพลาด dotnetfiddle.net/GII2jW
Shaun Luttin

1
@ShaunLuttin: ใช่แน่นอน - ที่พ่น แต่ในคำตอบของฉันฉันไม่ได้โยน a ReadOnlyCollection- ฉันโยนสิ่งIEnumerable<T>ที่ไม่ได้อ่านอย่างเดียวจริง ๆ ... แทนที่จะเปิดเผยReadOnlyCollection
Jon Skeet

1
เอเอชเอ ความหวาดระแวงของคุณจะทำให้คุณต้องใช้ a ReadOnlyCollectionแทนIEnumerableเพื่อป้องกันการหล่อหลอมที่ชั่วร้าย
Shaun Luttin

43

หากคุณต้องการวนซ้ำผ่านคอลเลกชัน:

foreach (Foo f in bar.Foos)

จากนั้นคืนค่าIEnumerableก็เพียงพอแล้ว

หากคุณต้องการเข้าถึงรายการโดยสุ่ม:

Foo f = bar.Foos[17];

แล้วห่อไว้ในReadOnlyCollection


@Vojislav Stojkovic, +1. คำแนะนำนี้ยังใช้ได้ในปัจจุบันหรือไม่
w0051977

1
@ w0051977 ขึ้นอยู่กับเจตนาของคุณ การใช้System.Collections.Immutableตามที่ Jon Skeet แนะนำนั้นใช้ได้อย่างสมบูรณ์เมื่อคุณเปิดเผยบางสิ่งที่จะไม่มีวันเปลี่ยนแปลงหลังจากที่คุณได้รับการอ้างอิงถึงมัน ในทางกลับกันหากคุณกำลังเปิดเผยมุมมองแบบอ่านอย่างเดียวของคอลเลกชันที่ไม่แน่นอนคำแนะนำนี้ก็ยังใช้ได้แม้ว่าฉันจะเปิดเผยว่าเป็นIReadOnlyCollection(ซึ่งไม่มีอยู่เมื่อฉันเขียนสิ่งนี้)
Vojislav Stojkovic

@VojislavStojkovic คุณไม่สามารถใช้กับbar.Foos[17] IReadOnlyCollectionข้อแตกต่างเพียงอย่างเดียวคือCountทรัพย์สิน เพิ่มเติม public interface IReadOnlyCollection<out T> : IEnumerable<T>, IEnumerable
Konrad

@Konrad ขวาถ้าคุณต้องการbar.Foos[17]คุณต้องเปิดเผยเป็นReadOnlyCollection<Foo>. IReadOnlyCollection<Foo>หากคุณต้องการทราบขนาดและย้ำผ่านมันเผยให้เห็นว่ามันเป็น IEnumerable<Foo>ถ้าคุณไม่จำเป็นต้องรู้ขนาดการใช้งาน มีวิธีแก้ปัญหาอื่น ๆ และรายละเอียดอื่น ๆ แต่อยู่นอกเหนือขอบเขตของการอภิปรายเกี่ยวกับคำตอบนี้
Vojislav Stojkovic

31

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


14
นั่นเป็น "การออกแบบที่น่าหวาดระแวง" ซึ่งฉันไม่เห็นด้วยอย่างยิ่ง ฉันคิดว่าเป็นการปฏิบัติที่ไม่ดีในการเลือกใช้ความหมายที่ไม่เพียงพอเพื่อบังคับใช้นโยบาย
Vojislav Stojkovic

24
คุณไม่เห็นด้วยไม่ได้ทำให้มันผิด หากฉันกำลังออกแบบไลบรารีสำหรับการแจกจ่ายภายนอกฉันควรมีอินเทอร์เฟซที่หวาดระแวงมากกว่าที่จะจัดการกับข้อบกพร่องที่เกิดจากผู้ใช้ที่พยายามใช้ API ในทางที่ผิด ดูหลักเกณฑ์การออกแบบ C # ที่msdn.microsoft.com/en-us/library/k2604h5s(VS.71).aspx
Stu Mackellar

5
ใช่ในกรณีเฉพาะของการออกแบบไลบรารีสำหรับการแจกจ่ายภายนอกการมีอินเทอร์เฟซที่น่าหวาดระแวงไม่ว่าคุณจะเปิดเผยอะไรก็ตาม ถึงกระนั้นถ้าความหมายของคุณสมบัติ Foos เป็นการเข้าถึงตามลำดับให้ใช้ ReadOnlyCollection แล้วส่งคืน IEnumerable ไม่จำเป็นต้องใช้ความหมายผิด;)
Vojislav Stojkovic

9
คุณไม่จำเป็นต้องทำอย่างใดอย่างหนึ่ง คุณสามารถส่งคืนตัววนซ้ำแบบอ่านอย่างเดียวโดยไม่ต้องคัดลอก ...
Jon Skeet

1
@shojtsy สายรหัสของคุณดูเหมือนจะใช้ไม่ได้ เป็นสร้างnull นักแสดงมีข้อยกเว้น
Nick Alexeev

3

ฉันหลีกเลี่ยงการใช้ ReadOnlyCollection ให้มากที่สุดจริงๆแล้วมันช้ากว่าการใช้รายการปกติมาก ดูตัวอย่างนี้:

List<int> intList = new List<int>();
        //Use a ReadOnlyCollection around the List
        System.Collections.ObjectModel.ReadOnlyCollection<int> mValue = new System.Collections.ObjectModel.ReadOnlyCollection<int>(intList);

        for (int i = 0; i < 100000000; i++)
        {
            intList.Add(i);
        }
        long result = 0;

        //Use normal foreach on the ReadOnlyCollection
        TimeSpan lStart = new TimeSpan(System.DateTime.Now.Ticks);
        foreach (int i in mValue)
            result += i;
        TimeSpan lEnd = new TimeSpan(System.DateTime.Now.Ticks);
        MessageBox.Show("Speed(ms): " + (lEnd.TotalMilliseconds - lStart.TotalMilliseconds).ToString());
        MessageBox.Show("Result: " + result.ToString());

        //use <list>.ForEach
        lStart = new TimeSpan(System.DateTime.Now.Ticks);
        result = 0;
        intList.ForEach(delegate(int i) { result += i; });
        lEnd = new TimeSpan(System.DateTime.Now.Ticks);
        MessageBox.Show("Speed(ms): " + (lEnd.TotalMilliseconds - lStart.TotalMilliseconds).ToString());
        MessageBox.Show("Result: " + result.ToString());

15
การเพิ่มประสิทธิภาพก่อนกำหนด จากการวัดของฉันการทำซ้ำบน ReadOnlyCollection จะใช้เวลานานกว่ารายการปกติประมาณ 36% โดยสมมติว่าคุณไม่ได้ทำอะไรเลยใน for loop มีโอกาสที่คุณจะไม่สังเกตเห็นความแตกต่างนี้ แต่ถ้านี่เป็นจุดที่น่าสนใจในโค้ดของคุณและต้องการประสิทธิภาพการทำงานสุดท้ายคุณสามารถบีบออกได้ทำไมไม่ใช้ Array แทนล่ะ? นั่นจะเร็วกว่า
StriplingWarrior

เกณฑ์มาตรฐานของคุณมีข้อบกพร่องคุณไม่สนใจการทำนายสาขาโดยสิ้นเชิง
Trojaner

0

บางครั้งคุณอาจต้องการใช้อินเทอร์เฟซบางทีอาจเป็นเพราะคุณต้องการล้อเลียนคอลเลคชันระหว่างการทดสอบหน่วย โปรดดูรายการบล็อกของฉันสำหรับการเพิ่มอินเทอร์เฟซของคุณเองใน ReadonlyCollection โดยใช้อะแดปเตอร์

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.