การจัดการคำเตือนสำหรับการนับ IEnumerable ที่เป็นไปได้หลายรายการ


358

ในรหัสของฉันจำเป็นต้องใช้IEnumerable<>หลายครั้งจึงได้รับข้อผิดพลาด Resharper ของ "เป็นไปได้หลายการนับIEnumerable"

รหัสตัวอย่าง:

public List<object> Foo(IEnumerable<object> objects)
{
    if (objects == null || !objects.Any())
        throw new ArgumentException();

    var firstObject = objects.First();
    var list = DoSomeThing(firstObject);        
    var secondList = DoSomeThingElse(objects);
    list.AddRange(secondList);

    return list;
}
  • ฉันสามารถเปลี่ยนobjectsพารามิเตอร์ให้เป็นListแล้วหลีกเลี่ยงการนับหลายที่เป็นไปได้ แต่จากนั้นฉันไม่ได้รับวัตถุสูงสุดที่ฉันสามารถจัดการได้
  • สิ่งที่ฉันสามารถทำได้ก็คือการแปลงIEnumerableไปListที่จุดเริ่มต้นของวิธีการ:

 public List<object> Foo(IEnumerable<object> objects)
 {
    var objectList = objects.ToList();
    // ...
 }

แต่นี้เป็นเพียงที่น่าอึดอัดใจ

คุณจะทำอะไรในสถานการณ์นี้

คำตอบ:


470

ปัญหาในการรับIEnumerableพารามิเตอร์คือมันบอกผู้โทรว่า "ฉันต้องการแจกแจงสิ่งนี้" มันไม่ได้บอกพวกเขาว่าคุณต้องการแจกแจงกี่ครั้ง

ฉันสามารถเปลี่ยนพารามิเตอร์วัตถุเป็นรายการแล้วหลีกเลี่ยงการนับหลายที่เป็นไปได้ แต่จากนั้นฉันไม่ได้รับวัตถุสูงสุดที่ฉันสามารถจัดการได้

เป้าหมายของการรับสิ่งของที่สูงที่สุดนั้นสูงส่ง แต่มันก็มีพื้นที่เพียงพอสำหรับการตั้งสมมติฐานมากเกินไป คุณต้องการให้ใครสักคนผ่านการสอบถาม LINQ ไปยัง SQL ไปยังวิธีการนี้เพื่อให้คุณระบุเพียงสองครั้ง (รับผลลัพธ์ที่แตกต่างกันในแต่ละครั้ง?)

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

โดยการเปลี่ยนวิธีการเป็นIList/ ICollectionอย่างน้อยคุณจะทำให้ชัดเจนกับผู้โทรสิ่งที่คุณคาดหวังและพวกเขาสามารถหลีกเลี่ยงข้อผิดพลาดค่าใช้จ่าย

ไม่เช่นนั้นนักพัฒนาส่วนใหญ่ที่ดูวิธีการอาจคิดว่าคุณทำซ้ำเพียงครั้งเดียว หากการรับIEnumerableเป็นสิ่งที่สำคัญมากคุณควรพิจารณาดำเนินการ.ToList()เมื่อเริ่มต้นวิธีการ

มันน่าละอาย. NET ไม่มีอินเทอร์เฟซที่เป็น IEnumerable + Count + Indexer โดยไม่ต้องเพิ่ม / ลบ ฯลฯ วิธีซึ่งเป็นสิ่งที่ฉันสงสัยว่าจะแก้ปัญหานี้ได้


31
ReadOnlyCollection <T> ตรงตามข้อกำหนดของอินเตอร์เฟสที่คุณต้องการหรือไม่ msdn.microsoft.com/en-us/library/ms132474.aspx
Dan Is Fiddling โดย Firelight

73
@DanNeely ฉันขอแนะนำIReadOnlyCollection(T)(ใหม่กับ. net 4.5) เป็นอินเทอร์เฟซที่ดีที่สุดในการถ่ายทอดความคิดว่ามันเป็นสิ่งIEnumerable(T)ที่ตั้งใจจะแจกแจงหลายครั้ง เนื่องจากคำตอบนี้ระบุไว้ว่าIEnumerable(T)เป็นเรื่องทั่วไปที่อาจอ้างถึงเนื้อหาที่ไม่สามารถรีเซ็ตได้ซึ่งไม่สามารถระบุได้อีกโดยไม่มีผลข้างเคียง แต่IReadOnlyCollection(T)หมายถึงการทำซ้ำอีกครั้ง
binki

1
หากคุณทำ.ToList()ที่จุดเริ่มต้นของวิธีการคุณต้องตรวจสอบก่อนว่าพารามิเตอร์นั้นเป็นโมฆะหรือไม่ นั่นหมายความว่าคุณจะได้สองตำแหน่งที่คุณใช้ ArgumentException: หากพารามิเตอร์นั้นเป็นโมฆะและหากไม่มีรายการใด ๆ
comecme

ฉันอ่านบทความนี้เกี่ยวกับความหมายของIReadOnlyCollection<T>vs IEnumerable<T>แล้วโพสต์คำถามเกี่ยวกับการใช้IReadOnlyCollection<T>เป็นพารามิเตอร์และในกรณีใด ฉันคิดว่าข้อสรุปที่ฉันได้มาในที่สุดก็คือตรรกะที่จะทำตาม ควรใช้เมื่อมีการแจงนับไม่จำเป็นว่าจะมีการแจงนับหลายครั้ง
Neo

32

หากข้อมูลของคุณจะสามารถทำซ้ำได้เสมอไม่ต้องกังวล อย่างไรก็ตามคุณสามารถเปิดใช้งานได้เช่นกัน - ซึ่งเป็นประโยชน์อย่างยิ่งหากข้อมูลขาเข้าอาจมีขนาดใหญ่ (ตัวอย่างเช่นการอ่านจากดิสก์ / เครือข่าย):

if(objects == null) throw new ArgumentException();
using(var iter = objects.GetEnumerator()) {
    if(!iter.MoveNext()) throw new ArgumentException();

    var firstObject = iter.Current;
    var list = DoSomeThing(firstObject);  

    while(iter.MoveNext()) {
        list.Add(DoSomeThingElse(iter.Current));
    }
    return list;
}

หมายเหตุฉันเปลี่ยน semantic ของ DoSomethingElse นิดหน่อย แต่นี่ส่วนใหญ่จะแสดงการใช้งานที่ไม่ได้ควบคุม ตัวอย่างเช่นคุณสามารถใส่ตัววนซ้ำได้ คุณสามารถทำให้เป็นบล็อกตัววนซ้ำได้เช่นกันซึ่งอาจดี จึงไม่มีlist- และคุณต้องการyield returnรายการต่าง ๆ ที่คุณได้รับมากกว่าที่จะเพิ่มในรายการที่จะถูกส่งคืน


4
+1 นั่นเป็นรหัสที่ดีมาก แต่ - ฉันปล่อยความน่ารักและความสามารถในการอ่านของรหัสออกมา
gdoron ให้การสนับสนุนโมนิกา

5
@ gdoron ไม่ใช่ทุกอย่างที่น่ารัก แต่ฉันก็ไม่สามารถอ่านได้ทั้งหมดข้างต้น โดยเฉพาะอย่างยิ่งถ้ามันถูกสร้างเป็นบล็อคตัววนซ้ำ - น่ารักทีเดียว IMO
Marc Gravell

ฉันสามารถเขียน: "var objectList = objects.ToList ();" และบันทึกการจัดการแจงนับทั้งหมด
gdoron ให้การสนับสนุนโมนิกา

10
@ gdoron ในคำถามที่คุณบอกอย่างชัดเจนว่าคุณไม่ต้องการที่จะทำเช่นนั้น p วิธีการ ToList () อาจไม่เหมาะกับข้อมูลหากมีขนาดใหญ่มาก (อาจเกิดขึ้นไม่สิ้นสุด - ลำดับไม่จำเป็นต้องมี จำกัด ยังคงถูกทำซ้ำ) ท้ายที่สุดฉันแค่นำเสนอทางเลือก มีเพียงคุณเท่านั้นที่รู้บริบททั้งหมดเพื่อดูว่ามีการรับประกันหรือไม่ หากข้อมูลของคุณสามารถทำซ้ำตัวเลือกที่ถูกต้องคือ: อย่าเปลี่ยนแปลงอะไร! ไม่สนใจ R # แทน ... ทุกอย่างขึ้นอยู่กับบริบท
Marc Gravell

1
ฉันคิดว่าทางออกของ Marc ค่อนข้างดี 1. มันชัดเจนมากว่า 'รายการ' ถูกเริ่มต้นอย่างไรมันชัดเจนมากว่ารายการนั้นถูกทำซ้ำอย่างไรและชัดเจนมากว่าสมาชิกของรายการนั้นดำเนินการอย่างไร
Keith Hoffman

8

การใช้IReadOnlyCollection<T>หรือIReadOnlyList<T>ในลายเซ็นเมธอดแทนIEnumerable<T>มีข้อดีของการทำให้ชัดเจนว่าคุณอาจต้องตรวจสอบจำนวนก่อนที่จะวนซ้ำหรือซ้ำหลายครั้งด้วยเหตุผลอื่น

อย่างไรก็ตามพวกเขามีข้อเสียอย่างมากที่จะทำให้เกิดปัญหาหากคุณพยายามปรับโครงสร้างรหัสของคุณให้ใช้อินเทอร์เฟซอีกครั้งเพื่อให้สามารถทดสอบได้และเป็นมิตรกับพร็อกซี่แบบไดนามิกมากขึ้น จุดสำคัญคือสิ่งที่IList<T>ไม่ได้รับสืบทอดIReadOnlyList<T>และในทำนองเดียวกันสำหรับคอลเลกชันอื่น ๆ และอินเทอร์เฟซแบบอ่านอย่างเดียวที่เกี่ยวข้อง (โดยย่อนี่เป็นเพราะ. NET 4.5 ต้องการรักษาความเข้ากันได้ของ ABI กับรุ่นก่อนหน้าแต่พวกเขาไม่ได้ใช้โอกาสในการเปลี่ยนแปลงใน. NET core )

ซึ่งหมายความว่าหากคุณได้รับIList<T>จากบางส่วนของโปรแกรมและต้องการส่งต่อไปยังส่วนอื่นที่คาดหวังIReadOnlyList<T>คุณจะไม่สามารถ! แต่คุณสามารถผ่านการIList<T>IEnumerable<T>เป็น

ในท้ายที่สุดIEnumerable<T>เป็นอินเทอร์เฟซแบบอ่านอย่างเดียวเท่านั้นที่ได้รับการสนับสนุนโดยคอลเลกชัน. NET ทั้งหมดรวมถึงอินเทอร์เฟซการรวบรวมทั้งหมด ทางเลือกอื่น ๆ จะกลับมากัดคุณเมื่อคุณตระหนักว่าคุณล็อคตัวเองจากตัวเลือกสถาปัตยกรรมบางอย่าง ดังนั้นฉันคิดว่ามันเป็นประเภทที่เหมาะสมที่จะใช้ในฟังก์ชันลายเซ็นเพื่อแสดงว่าคุณเพียงแค่ต้องการคอลเลกชันแบบอ่านอย่างเดียว

(โปรดทราบว่าคุณสามารถเขียนIReadOnlyList<T> ToReadOnly<T>(this IList<T> list)วิธีการขยายที่ให้ค่าคงที่ได้ง่ายหากประเภทที่รองรับรองรับทั้งสองอินเตอร์เฟส แต่คุณต้องเพิ่มมันด้วยตนเองทุกครั้งเมื่อทำการเปลี่ยนโครงสร้างซึ่งIEnumerable<T>เข้ากันได้เสมอ)

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


2
+1 สำหรับการโพสต์เก่าและการเพิ่มเอกสารเกี่ยวกับวิธีการใหม่ในการแก้ปัญหานี้ด้วยคุณสมบัติใหม่ที่มีให้โดยแพลตฟอร์ม. NET (เช่น IReadOnlyCollection <T>)
Kevin Avignon

4

หากเป้าหมายคือการป้องกันไม่ให้มีการแจกแจงจำนวนมากเกินกว่าคำตอบของ Marc Gravell คือการอ่าน แต่การคงไว้ซึ่งความหมายเดียวกันคุณสามารถลบซ้ำซ้อนAnyและการFirstโทรออกด้วย:

public List<object> Foo(IEnumerable<object> objects)
{
    if (objects == null)
        throw new ArgumentNullException("objects");

    var first = objects.FirstOrDefault();

    if (first == null)
        throw new ArgumentException(
            "Empty enumerable not supported.", 
            "objects");

    var list = DoSomeThing(first);  

    var secondList = DoSomeThingElse(objects);

    list.AddRange(secondList);

    return list;
}

โปรดทราบว่านี่ถือว่าคุณIEnumerableไม่ใช่คนทั่วไปหรืออย่างน้อยก็ถูก จำกัด ให้เป็นประเภทอ้างอิง


2
objectsยังคงถูกส่งผ่านไปยัง DoSomethingElse ซึ่งส่งคืนรายการดังนั้นความเป็นไปได้ที่คุณยังคงแจกแจงสองครั้งยังคงอยู่ที่นั่น ไม่ว่าจะด้วยวิธีใดหากคอลเลกชันเป็น LINQ ไปยังคิวรี SQL คุณจะต้องกด DB สองครั้ง
Paul Stovell

1
@PaulStovell, อ่านสิ่งนี้: "ถ้าเป้าหมายจริงๆเพื่อป้องกันการแจกแจงจำนวนมากเกินกว่าคำตอบของ Marc Gravell เป็นคำตอบที่อ่านได้" =)
gdoron ให้การสนับสนุน Monica

มีการแนะนำในโพสต์สแต็คโอเวอร์โฟลว์อื่น ๆ เกี่ยวกับการโยนข้อยกเว้นสำหรับรายการว่างและฉันจะแนะนำที่นี่: ทำไมต้องโยนข้อยกเว้นในรายการว่าง รับรายการที่ว่างเปล่าให้รายการที่ว่างเปล่า เป็นกระบวนทัศน์ที่ค่อนข้างตรงไปตรงมา หากผู้โทรไม่ทราบว่าพวกเขากำลังผ่านรายการที่ว่างเปล่าแสดงว่ามีปัญหา
Keith Hoffman

@Keith Hoffman โค้ดตัวอย่างจะคงการทำงานเช่นเดียวกับรหัสที่โพสต์ไว้ซึ่งการใช้งานFirstจะทำให้เกิดข้อยกเว้นสำหรับรายการว่าง ฉันไม่คิดว่าเป็นไปได้ที่จะบอกว่าไม่เคยทิ้งข้อยกเว้นในรายการว่างเปล่าหรือโยนข้อยกเว้นในรายการว่างเสมอ ขึ้นอยู่กับ ...
João Angelo

ยุติธรรมพอ Joao มีอาหารให้คิดมากกว่าคำวิจารณ์ในโพสต์ของคุณ
Keith Hoffman

3

ฉันมักจะเกินวิธีการของฉันกับ IEnumerable และ IList ในสถานการณ์นี้

public static IEnumerable<T> Method<T>( this IList<T> source ){... }

public static IEnumerable<T> Method<T>( this IEnumerable<T> source )
{
    /*input checks on source parameter here*/
    return Method( source.ToList() );
}

ฉันระมัดระวังที่จะอธิบายในความคิดเห็นสรุปของวิธีการที่เรียก IEnumerable จะดำเนินการ. ToList ()

โปรแกรมเมอร์สามารถเลือกที่จะ. ToList () ในระดับที่สูงขึ้นหากมีการดำเนินการหลายอย่างพร้อมกันแล้วเรียก IList overload หรือให้ IEnumerable overload ของฉันจัดการได้


20
นี่มันสุดยอดมาก
Mike Chamberlain

1
@ MikeChamberlain ใช่มันน่ากลัวและสะอาดที่สุดในเวลาเดียวกัน โชคไม่ดีที่สถานการณ์บางอย่างต้องการแนวทางนี้
Mauro Sampietro

1
แต่จากนั้นคุณจะสร้างอาร์เรย์ขึ้นใหม่ทุกครั้งที่คุณส่งผ่านไปยังวิธีการแบบกระจายจำนวน IEnumerable ฉันเห็นด้วยกับ @MikeChamberlain ในเรื่องนี้นี่เป็นคำแนะนำที่แย่มาก
Krythic

2
@Krythic หากคุณไม่ต้องการให้มีการสร้างรายการขึ้นใหม่ให้ทำ ToList ที่อื่นและใช้ IList โอเวอร์โหลด นั่นคือประเด็นของการแก้ปัญหานี้
Mauro Sampietro

0

หากคุณต้องการตรวจสอบองค์ประกอบแรกคุณสามารถดูได้โดยไม่ต้องวนซ้ำคอลเลกชันทั้งหมด:

public List<object> Foo(IEnumerable<object> objects)
{
    object firstObject;
    if (objects == null || !TryPeek(ref objects, out firstObject))
        throw new ArgumentException();

    var list = DoSomeThing(firstObject);
    var secondList = DoSomeThingElse(objects);
    list.AddRange(secondList);

    return list;
}

public static bool TryPeek<T>(ref IEnumerable<T> source, out T first)
{
    if (source == null)
        throw new ArgumentNullException(nameof(source));

    IEnumerator<T> enumerator = source.GetEnumerator();
    if (!enumerator.MoveNext())
    {
        first = default(T);
        source = Enumerable.Empty<T>();
        return false;
    }

    first = enumerator.Current;
    T firstElement = first;
    source = Iterate();
    return true;

    IEnumerable<T> Iterate()
    {
        yield return firstElement;
        using (enumerator)
        {
            while (enumerator.MoveNext())
            {
                yield return enumerator.Current;
            }
        }
    }
}

-3

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

ในสถานการณ์นี้คุณสามารถใช้ประโยชน์จากวิธีการขยาย linq ที่มีประสิทธิภาพมากยิ่งขึ้น

var firstObject = objects.First();
return DoSomeThing(firstObject).Concat(DoSomeThingElse(objects).ToList();

มีความเป็นไปได้ที่จะประเมินเพียงIEnumerableครั้งเดียวในกรณีนี้ด้วยความยุ่งยาก แต่โปรไฟล์ก่อนและดูว่ามันเป็นปัญหาหรือไม่


15
ฉันทำงานมากด้วย IQueryable และการปิดคำเตือนแบบนั้นเป็นสิ่งที่อันตรายมาก
gdoron ให้การสนับสนุนโมนิกา

5
การประเมินสองครั้งเป็นสิ่งที่ไม่ดีในหนังสือของฉัน หากวิธีการใช้ IEnumerable ฉันอาจถูกล่อลวงให้ส่ง LINQ ไปยังแบบสอบถาม SQL ซึ่งส่งผลให้มีการเรียก DB หลายครั้ง เนื่องจากลายเซ็นวิธีการไม่ได้ระบุว่าอาจนับสองครั้งจึงควรเปลี่ยนลายเซ็น (ยอมรับ IList / ICollection) หรือซ้ำเพียงครั้งเดียว
Paul Stovell

4
ปรับให้เหมาะสมก่อนและทำลายการอ่านและดังนั้นความเป็นไปได้ที่จะทำการเพิ่มประสิทธิภาพที่ทำให้การกระจายในภายหลังเป็นวิธีที่เลวร้ายยิ่งในหนังสือของฉัน
vidstige

เมื่อคุณได้มีการสืบค้นฐานข้อมูลทำให้แน่ใจว่ามันประเมินเพียงครั้งเดียวแล้วทำให้ความแตกต่าง มันไม่ใช่แค่ประโยชน์ในเชิงทฤษฎีในอนาคตเท่านั้น แต่ยังเป็นประโยชน์ที่แท้จริงที่วัดได้ในขณะนี้ ไม่เพียงแค่นั้นการประเมินเพียงครั้งเดียวนั้นไม่เพียง แต่เป็นประโยชน์ต่อการปฏิบัติงานเท่านั้น แต่ยังจำเป็นสำหรับความถูกต้องอีกด้วย การดำเนินการสืบค้นฐานข้อมูลสองครั้งอาจให้ผลลัพธ์ที่แตกต่างกันเนื่องจากบางคนอาจออกคำสั่ง update ระหว่างการประเมินแบบสอบถามทั้งสองครั้ง

1
@hvd ฉันไม่ได้แนะนำอย่างนั้น แน่นอนว่าคุณไม่ควรระบุIEnumerablesว่าให้ผลลัพธ์ที่แตกต่างกันในแต่ละครั้งที่คุณทำซ้ำหรือใช้เวลานานมากในการทำซ้ำ ดูคำตอบของ Marc Gravells - นอกจากนี้เขายังระบุเป็นลำดับแรกและสำคัญที่สุด "อาจไม่ต้องกังวล" มีหลายครั้งที่คุณเห็นสิ่งนี้คุณไม่มีอะไรต้องกังวล
vidstige
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.