ทำไม. NET foreach วนโยน NullRefException เมื่อคอลเลกชันเป็นโมฆะ?


231

ดังนั้นฉันมักพบในสถานการณ์นี้ ... ซึ่งDo.Something(...)ส่งกลับคอลเลกชัน null เช่น:

int[] returnArray = Do.Something(...);

จากนั้นฉันพยายามใช้คอลเล็กชันนี้เช่น:

foreach (int i in returnArray)
{
    // do some more stuff
}

ฉันแค่อยากรู้อยากเห็นทำไมห่วง foreach ไม่สามารถทำงานในคอลเลกชันโมฆะ? มันดูเหมือนว่าตรรกะกับผมว่า 0 ซ้ำจะได้รับการดำเนินการด้วยคอลเลกชันโมฆะ ... NullReferenceExceptionแทนที่จะพ่น ใครรู้ว่าทำไมสิ่งนี้อาจเป็น

นี่เป็นเรื่องที่น่ารำคาญเพราะฉันทำงานกับ API ที่ไม่ชัดเจนว่าพวกเขากลับมาได้อย่างไรดังนั้นฉันก็เลยลงเอยด้วยif (someCollection != null)ทุกที่ ...

แก้ไข:ขอบคุณทุกท่านสำหรับคำอธิบายที่foreachใช้GetEnumeratorและหากไม่มีตัวแจงนับที่จะได้รับ foreach จะล้มเหลว ฉันเดาว่าฉันถามว่าทำไมภาษา / รันไทม์ไม่สามารถหรือไม่ตรวจสอบโมฆะก่อนที่จะจับตัวแจงนับ สำหรับฉันดูเหมือนว่าพฤติกรรมจะยังคงชัดเจน


1
มีบางอย่างผิดปกติเกี่ยวกับการเรียกชุดสะสม แต่บางทีฉันแค่โรงเรียนเก่า
Robaticus

ใช่ฉันเห็นด้วย ... ฉันไม่แน่ใจด้วยซ้ำว่าทำไมจึงมีวิธีการมากมายในโค้ดส่งคืนอาเรย์นี้ x_x
Polaris878

4
ฉันคิดว่าด้วยเหตุผลเดียวกันมันจะถูกกำหนดไว้อย่างดีสำหรับงบทั้งหมดใน C # ที่จะกลายเป็นไม่เลือกเมื่อได้รับnullค่า คุณกำลังแนะนำสิ่งนี้สำหรับแค่foreachลูปหรือข้อความอื่น ๆ เช่นกัน?
เคน

7
@ Ken ... ฉันคิดว่า foreach loops เพราะสำหรับฉันดูเหมือนว่าโปรแกรมเมอร์จะไม่มีอะไรเกิดขึ้นถ้าคอลเลกชันว่างเปล่าหรือไม่มีอยู่จริง
Polaris878

คำตอบ:


251

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

หากคุณต้องการทำสิ่งนี้ให้ลองตัวดำเนินการรวมศูนย์ว่าง:

int[] array = null;

foreach (int i in array ?? Enumerable.Empty<int>())
{
   System.Console.WriteLine(string.Format("{0}", i));
}

3
โปรดแก้ตัวความไม่รู้ของฉัน แต่สิ่งนี้มีประสิทธิภาพไหม มันไม่ได้ส่งผลให้เกิดการเปรียบเทียบในแต่ละรอบซ้ำหรือไม่?
user919426

20
ฉันไม่เชื่ออย่างนั้น ดูที่ IL ที่สร้างขึ้นลูปคือหลังจากการเปรียบเทียบเป็นโมฆะ
Robaticus

10
ศักดิ์สิทธิ์ necro ... บางครั้งคุณต้องดู IL เพื่อดูว่าคอมไพเลอร์กำลังทำอะไรเพื่อดูว่ามีประสิทธิภาพหรือไม่ ผู้ใช้ 919426 ได้ถามว่าได้ทำการตรวจสอบซ้ำแต่ละครั้งหรือไม่ แม้ว่าคำตอบอาจชัดเจนสำหรับทุกคน แต่ก็ไม่ชัดเจนสำหรับทุกคนและการให้คำใบ้ว่าการดู IL จะบอกคุณได้ว่าผู้รวบรวมกำลังทำอะไรอยู่
Robaticus

2
@Robaticus (แม้จะเป็นเหตุผลในภายหลัง) IL ดูว่าทำไมเพราะข้อมูลจำเพาะพูดอย่างนั้น การขยายตัวของน้ำตาล syntactic (aka foreach) คือการประเมินการแสดงออกทางด้านขวาของ "ใน" และเรียกGetEnumeratorผล
Rune FS

2
@RuneFS - อย่างแน่นอน การทำความเข้าใจข้อมูลจำเพาะหรือดู IL เป็นวิธีที่จะเข้าใจว่า "ทำไม" หรือเพื่อประเมินว่า C # สองวิธีที่แตกต่างกันต้มลงไปที่ IL เดียวกัน นั่นคือโดยพื้นฐานแล้วประเด็นของฉันคือชิมมี่ข้างต้น
Robaticus

148

การforeachวนซ้ำเรียกGetEnumeratorเมธอด
หากคอลเลกชันนี้เป็นวิธีการเรียกนี้จะส่งผลให้nullNullReferenceException

มันเป็นวิธีปฏิบัติที่ไม่ดีในการส่งคืนnullคอลเล็กชัน วิธีการของคุณควรส่งคืนคอลเลกชันที่ว่างเปล่าแทน


7
ผมเห็นด้วยคอลเลกชันที่ว่างเปล่าควรจะกลับมา ... แต่ฉันไม่ได้เขียนวิธีการเหล่านี้ :)
Polaris878

19
@ Polaris ผู้ประกอบการโมฆะรวมตัวกันเพื่อช่วยเหลือ! int[] returnArray = Do.Something() ?? new int[] {};
JSB ձոգչ

2
หรือ: ... ?? new int[0].
เคน

3
+1 ชอบเคล็ดลับในการส่งคืนคอลเล็กชันว่างเปล่าแทนที่จะเป็นโมฆะ ขอบคุณ
Galilyou

1
ฉันไม่เห็นด้วยเกี่ยวกับการปฏิบัติที่ไม่ดี: ดู⇒หากฟังก์ชั่นล้มเหลวมันอาจส่งคืนคอลเลกชันที่ว่างเปล่า - มันเป็นการเรียกไปยังคอนสตรัคเตอร์การจัดสรรหน่วยความจำและอาจเป็นรหัสที่เรียกใช้ คุณอาจจะกลับ« null »→เห็นได้ชัดว่ามีเพียงรหัสที่จะส่งคืนและรหัสสั้น ๆ เพื่อตรวจสอบว่าอาร์กิวเมนต์เป็น« null » มันเป็นเพียงการแสดง
Hi-Angel

47

มีความแตกต่างใหญ่ระหว่างคอลเลกชันที่ว่างเปล่าและการอ้างอิงเป็นโมฆะกับคอลเลกชัน

เมื่อคุณใช้foreachภายในจะเป็นการเรียกเมธอดGetEnumerator () ของ IEnumerable เมื่อการอ้างอิงเป็นโมฆะสิ่งนี้จะยกข้อยกเว้นนี้

แต่ก็เป็นสิ่งที่ถูกต้องสมบูรณ์จะมีที่ว่างเปล่าหรือIEnumerable IEnumerable<T>ในกรณีนี้ foreach จะไม่ "วนซ้ำ" เหนือสิ่งใด (เนื่องจากชุดสะสมว่างเปล่า) แต่จะไม่โยนเนื่องจากนี่เป็นสถานการณ์ที่สมบูรณ์แบบ


แก้ไข:

โดยส่วนตัวถ้าคุณต้องการหลีกเลี่ยงปัญหานี้ฉันขอแนะนำวิธีส่วนขยาย:

public static IEnumerable<T> AsNotNull<T>(this IEnumerable<T> original)
{
     return original ?? Enumerable.Empty<T>();
}

จากนั้นคุณสามารถโทร:

foreach (int i in returnArray.AsNotNull())
{
    // do some more stuff
}

3
ใช่ แต่ทำไมไม่มีการตรวจสอบค่า null ก่อนรับตัวแจงนับ
Polaris878

12
@ Polaris878: เพราะมันไม่เคยตั้งใจที่จะใช้กับคอลเลกชัน null นี่คือ IMO สิ่งที่ดี - เนื่องจากการอ้างอิงที่เป็นโมฆะและการรวบรวมที่ว่างเปล่าควรได้รับการปฏิบัติแยกกัน หากคุณต้องการหลีกเลี่ยงปัญหานี้มีวิธี .. ฉันจะแก้ไขเพื่อแสดงตัวเลือกอื่น ...
Reed Copsey

1
@ Polaris878: ฉันขอแนะนำ rewording คำถามของคุณ: "ทำไมรันไทม์ทำเช็คเป็นโมฆะก่อนที่จะรับ enumerator"
Reed Copsey

ฉันเดาว่าฉันถาม "ทำไมไม่" ฮ่า ๆ ดูเหมือนว่าพฤติกรรมจะยังคงชัดเจน
Polaris878

2
@ Polaris878: ฉันคิดว่าวิธีที่ฉันคิดว่ามันคืน null สำหรับคอลเลกชันเป็นข้อผิดพลาด ตอนนี้ทางรันไทม์ให้ข้อยกเว้นที่มีความหมายในกรณีนี้ แต่มันง่ายที่จะแก้ไข (เช่น: ด้านบน) ถ้าคุณไม่ชอบพฤติกรรมนี้ หากคอมไพเลอร์ซ่อนสิ่งนี้จากคุณคุณจะสูญเสียการตรวจสอบข้อผิดพลาดที่รันไทม์ แต่ไม่มีทางที่จะ "ปิดมัน" ...
Reed Copsey

12

มันกำลังตอบกลับนาน แต่ฉันได้ลองทำในวิธีต่อไปนี้เพื่อหลีกเลี่ยงข้อยกเว้นตัวชี้โมฆะและอาจเป็นประโยชน์สำหรับคนที่ใช้ตัวดำเนินการตรวจสอบ C # null?

     //fragments is a list which can be null
     fragments?.ForEach((obj) =>
        {
            //do something with obj
        });

@kjbartel เอาชนะคุณได้มากกว่าหนึ่งปี (ที่ " stackoverflow.com/a/32134295/401246 ") ;) นี่เป็นทางออกที่ดีที่สุดเนื่องจากไม่ได้: a) เกี่ยวข้องกับการเสื่อมประสิทธิภาพของ (แม้ว่าจะไม่ใช่null) การวางวงทั้งวงลงบนจอ LCD ของEnumerable(ตามที่ใช้??), b) ต้องการการเพิ่มวิธีการขยายในทุกโครงการ และ c) ต้องหลีกเลี่ยงnull IEnumerables (Pffft! Puh-LEAZE! SMH.) เพื่อเริ่มต้น
ทอม

10

อีกวิธีการขยายการแก้ไข:

public static void ForEach<T>(this IEnumerable<T> items, Action<T> action)
{
    if(items == null) return;
    foreach (var item in items) action(item);
}

กินได้หลายวิธี:

(1) ด้วยวิธีการที่ยอมรับT:

returnArray.ForEach(Console.WriteLine);

(2) ด้วยการแสดงออก:

returnArray.ForEach(i => UpdateStatus(string.Format("{0}% complete", i)));

(3) ด้วยวิธีการไม่ระบุชื่อหลายบรรทัด

int toCompare = 10;
returnArray.ForEach(i =>
{
    var thisInt = i;
    var next = i++;
    if(next > 10) Console.WriteLine("Match: {0}", i);
});

เพิ่งหายไปวงเล็บปิดในตัวอย่างที่ 3 มิฉะนั้นโค้ดที่สวยงามที่สามารถขยายเพิ่มเติมในรูปแบบที่น่าสนใจ (สำหรับลูป, ย้อนกลับ, กระโดด, ฯลฯ ) ขอบคุณสำหรับการแบ่งปัน.
Lara

ขอบคุณสำหรับรหัสที่ยอดเยี่ยม แต่ฉันไม่เข้าใจวิธีการแรกทำไมคุณผ่าน console.writeline เป็นพารามิเตอร์แม้ว่าการพิมพ์องค์ประกอบอาร์เรย์ แต่ไม่เข้าใจ
Ajay Singh

@AjaySingh Console.WriteLineเป็นเพียงตัวอย่างของเมธอดที่รับอาร์กิวเมนต์หนึ่งตัว (an Action<T>) รายการที่ 1, 2 และ 3 แสดงตัวอย่างของฟังก์ชันส่งผ่านไปยัง.ForEachวิธีการขยาย
Jay

คำตอบของ @ kjbartel (ที่ " stackoverflow.com/a/32134295/401246 " เป็นทางออกที่ดีที่สุดเพราะมันไม่ได้: a) เกี่ยวข้องกับการเสื่อมประสิทธิภาพของ (แม้เมื่อไม่ได้null) ทำให้วงรอบทั้งหมดไปยัง LCD ของEnumerable(ตามที่ใช้??จะ ), b) ต้องการเพิ่มวิธีการขยายไปยังทุกโครงการหรือ c) ต้องหลีกเลี่ยงnull IEnumerables (Pffft! Puh-LEAZE! SMH.) เพื่อเริ่มต้นด้วย (cuz nullหมายถึง N / A ในขณะที่รายการที่ว่างเปล่าหมายถึงมันเป็น appl แต่ ขณะนี้ว่างเปล่า ! เช่น Empl. อาจมีค่าคอมมิชชั่นที่ไม่ใช่สำหรับการขายหรือเปล่าสำหรับการขาย)
ทอม

5

เพียงเขียนวิธีการขยายเพื่อช่วยคุณ:

public static class Extensions
{
   public static void ForEachWithNull<T>(this IEnumerable<T> source, Action<T> action)
   {
      if(source == null)
      {
         return;
      }

      foreach(var item in source)
      {
         action(item);
      }
   }
}

5

เพราะคอลเลกชันที่เป็นโมฆะไม่ใช่สิ่งเดียวกันกับคอลเลกชันที่ว่างเปล่า คอลเลกชันที่ว่างเปล่าเป็นวัตถุคอลเลกชันที่ไม่มีองค์ประกอบ คอลเลกชัน null เป็นวัตถุที่ไม่มีอยู่

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


3

Do.Something()มันเป็นความผิดของ วิธีปฏิบัติที่ดีที่สุดในที่นี้คือส่งคืนอาร์เรย์ขนาด 0 (ที่เป็นไปได้) แทนที่จะเป็นค่าว่าง


2

เพราะเบื้องหลังผู้foreachได้รับการแจงนับเท่ากับ:

using (IEnumerator<int> enumerator = returnArray.getEnumerator()) {
    while (enumerator.MoveNext()) {
        int i = enumerator.Current;
        // do some more stuff
    }
}

2
ดังนั้น? ทำไมมันไม่สามารถตรวจสอบว่ามันเป็นโมฆะก่อนและข้ามลูปหรือไม่ AKA สิ่งที่ปรากฏในวิธีการขยายได้อย่างไร คำถามคือจะดีกว่าไหมถ้าเริ่มต้นที่จะข้ามลูปถ้าเป็นโมฆะหรือโยนข้อยกเว้น? ฉันคิดว่ามันจะดีกว่าที่จะข้าม! ดูเหมือนว่าเป็นไปได้ว่าการข้ามภาชนะบรรจุเป็นโมฆะแทนที่จะวนซ้ำมากกว่าเนื่องจากมีการวนซ้ำเพื่อทำบางสิ่งบางอย่างถ้าคอนเทนเนอร์ไม่เป็นโมฆะ
AbstractDissonance

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

1
ฉันไม่คิดอย่างนั้น foreach นั้นหมายถึงการใช้งานบนคอลเลกชันและแตกต่างจากการอ้างอิงวัตถุ null โดยตรง ในขณะที่ใคร ๆ ก็เถียงกันฉันเดิมพันถ้าคุณวิเคราะห์รหัสทั้งหมดในโลกคุณจะมีลูป foreach ส่วนใหญ่มีการตรวจสอบโมฆะบางชนิดต่อหน้าพวกเขาเท่านั้นที่จะข้ามวงเมื่อคอลเลกชันเป็น "โมฆะ" (ซึ่งเป็น จึงถือว่าเหมือนกับว่างเปล่า) ฉันไม่คิดว่าจะมีใครพิจารณาวนรอบคอลเลกชันโมฆะเป็นสิ่งที่พวกเขาต้องการและค่อนข้างจะไม่สนใจลูปถ้าคอลเลกชันเป็นโมฆะ อาจจะเป็น foreach? (var x ใน C) ก็ได้
AbstractDissonance

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

@AbstractDissonance ดีด้วยการวิเคราะห์แบบคงที่ที่เหมาะสมคุณรู้ว่าคุณสามารถมีโมฆะและที่ไหน หากคุณได้รับค่า null ซึ่งคุณไม่คาดหวังว่าจะดีกว่าที่จะล้มเหลวแทนที่จะเพิกเฉยต่อปัญหาเงียบ ๆ IMHO (ในความมุ่งมั่นที่ล้มเหลวอย่างรวดเร็ว ) ดังนั้นฉันรู้สึกว่านี่เป็นพฤติกรรมที่ถูกต้อง
Lucero

1

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

    var returnArray = DoSomething() ?? Enumerable.Empty<int>();

    foreach (int i in returnArray)
    {
        // do some more stuff
    }

วิธีนี้เราสามารถใช้คอลเลกชันได้มากเท่าที่เราต้องการโดยไม่ต้องกลัวข้อยกเว้นและเราจะไม่ polute รหัสด้วยข้อความที่มีเงื่อนไขมากเกินไป

การใช้ตัวดำเนินการตรวจสอบค่า Null ?.ก็เป็นวิธีที่ยอดเยี่ยมเช่นกัน แต่ในกรณีของอาร์เรย์ (เช่นตัวอย่างในคำถาม) ควรแปลงเป็นรายการก่อนหน้า:

    int[] returnArray = DoSomething();

    returnArray?.ToList().ForEach((i) =>
    {
        // do some more stuff
    });

2
การแปลงเป็นรายการเพื่อให้สามารถเข้าถึงForEachวิธีการเป็นหนึ่งในสิ่งที่ฉันเกลียดใน codebase
huysentruitw

ฉันเห็นด้วย ... ฉันหลีกเลี่ยงสิ่งนั้นให้มากที่สุด :(
Alielson Piffer

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