Tại sao vòng lặp .NET foreach ném NullRefException khi bộ sưu tập là null?


231

Vì vậy, tôi thường xuyên gặp phải tình huống này ... nơi Do.Something(...)trả về một bộ sưu tập null, như vậy:

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

Sau đó, tôi cố gắng sử dụng bộ sưu tập này như vậy:

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

Tôi chỉ tò mò, tại sao một vòng lặp foreach không thể hoạt động trên một bộ sưu tập null? Nó có vẻ hợp lý với tôi rằng 0 lần lặp sẽ được thực hiện với một bộ sưu tập null ... thay vào đó nó ném a NullReferenceException. Bất cứ ai cũng biết tại sao điều này có thể được?

Điều này thật khó chịu vì tôi đang làm việc với các API không rõ ràng về chính xác những gì họ trả lại, vì vậy tôi kết thúc với if (someCollection != null)mọi nơi ...

Chỉnh sửa: Cảm ơn tất cả các bạn đã giải thích về việc foreachsử dụng GetEnumeratorvà nếu không có điều tra viên để nhận, foreach sẽ thất bại. Tôi đoán tôi đang hỏi tại sao ngôn ngữ / thời gian chạy không thể hoặc sẽ không thực hiện kiểm tra null trước khi lấy liệt kê. Dường như với tôi rằng hành vi vẫn sẽ được xác định rõ.


1
Một cái gì đó cảm thấy sai về việc gọi một mảng một bộ sưu tập. Nhưng có lẽ tôi chỉ là trường học cũ.
Robaticus

Có, tôi đồng ý ... Tôi thậm chí không chắc chắn tại sao rất nhiều phương thức trong mã cơ sở này trả về mảng x_x
Polaris878

4
Tôi cho rằng với cùng một lý do, nó sẽ được xác định rõ ràng cho tất cả các câu lệnh trong C # để trở thành không hoạt động khi được đưa ra một nullgiá trị. Bạn có đề xuất điều này cho chỉ foreachcác vòng lặp hoặc các tuyên bố khác không?
Ken

7
@ Ken ... Tôi đang nghĩ chỉ là các vòng lặp, bởi vì đối với tôi, dường như rõ ràng với lập trình viên rằng sẽ không có gì xảy ra nếu bộ sưu tập trống hoặc không tồn tại
Polaris878

Câu trả lời:


251

Vâng, câu trả lời ngắn gọn là "bởi vì đó là cách các nhà thiết kế trình biên dịch đã thiết kế nó." Trên thực tế, mặc dù, đối tượng bộ sưu tập của bạn là null, vì vậy không có cách nào để trình biên dịch có thể đưa trình liệt kê lặp qua bộ sưu tập.

Nếu bạn thực sự cần phải làm một cái gì đó như thế này, hãy thử toán tử hợp nhất null:

int[] array = null;

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

3
Xin thứ lỗi cho sự thiếu hiểu biết của tôi, nhưng điều này có hiệu quả không? Nó không dẫn đến một so sánh trên mỗi lần lặp?
dùng919426

20
Tôi không tin như vậy. Nhìn vào IL được tạo, vòng lặp nằm sau so sánh null.
Robaticus 30/03/2016

10
Holy necro ... Đôi khi bạn phải nhìn vào IL để xem trình biên dịch đang làm gì để tìm ra nếu có bất kỳ lượt truy cập hiệu quả nào. User919426 đã hỏi liệu nó có kiểm tra cho mỗi lần lặp không. Mặc dù câu trả lời có thể rõ ràng đối với một số người, nhưng mọi người không rõ ràng và cung cấp gợi ý rằng việc nhìn vào IL sẽ cho bạn biết trình biên dịch đang làm gì, giúp mọi người tự tìm kiếm cá trong tương lai.
Robaticus

2
@Robaticus (thậm chí tại sao sau này) IL xem đó là lý do tại sao thông số kỹ thuật nói như vậy. Việc mở rộng đường cú pháp (còn gọi là foreach) là để đánh giá biểu thức ở phía bên phải của "in" và gọi GetEnumeratorkết quả
Rune FS

2
@RuneFS - chính xác. Hiểu đặc điểm kỹ thuật hoặc nhìn vào IL là một cách để tìm ra "lý do tại sao." Hoặc để đánh giá liệu hai cách tiếp cận C # khác nhau có sôi cùng một IL hay không. Về cơ bản, đó là quan điểm của tôi về Shimmy ở trên.
Robaticus

148

Một foreachvòng lặp gọi GetEnumeratorphương thức.
Nếu bộ sưu tập là null, phương thức này gọi kết quả là a NullReferenceException.

Đó là thực tế xấu để trả lại một nullbộ sưu tập; phương pháp của bạn sẽ trả về một bộ sưu tập trống thay thế.


7
Tôi đồng ý, các bộ sưu tập trống sẽ luôn được trả lại ... tuy nhiên tôi đã không viết các phương thức này :)
Polaris878

19
@Polaris, null liên kết toán tử để giải cứu! int[] returnArray = Do.Something() ?? new int[] {};
JSB

2
Hoặc : ... ?? new int[0].
Ken

3
+1 Giống như mẹo trả lại các bộ sưu tập trống thay vì null. Cảm ơn.
Galilyou

1
Tôi không đồng ý về một thực tiễn xấu: xem nếu một hàm bị lỗi thì nó có thể trả về một bộ sưu tập trống - đó là một lệnh gọi đến hàm tạo, cấp phát bộ nhớ và có lẽ là một loạt mã được thực thi. Hoặc bạn chỉ có thể trả về «null» → rõ ràng chỉ có một mã để trả về và một mã rất ngắn để kiểm tra là đối số là «null». Đó chỉ là một màn trình diễn.
Hi-Angel

47

Có một sự khác biệt lớn giữa một bộ sưu tập trống và một tài liệu tham khảo null cho một bộ sưu tập.

Khi bạn sử dụng foreach, trong nội bộ, đây là cách gọi phương thức GetEnumerator () của IEnumerable . Khi tham chiếu là null, điều này sẽ đưa ra ngoại lệ này.

Tuy nhiên, nó hoàn toàn hợp lệ để có một sản phẩm nào IEnumerablehoặc IEnumerable<T>. Trong trường hợp này, foreach sẽ không "lặp lại" bất cứ điều gì (vì bộ sưu tập trống), nhưng nó cũng sẽ không ném, vì đây là một kịch bản hoàn toàn hợp lệ.


Biên tập:

Cá nhân, nếu bạn cần giải quyết vấn đề này, tôi khuyên bạn nên sử dụng một phương pháp mở rộng:

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

Sau đó bạn có thể gọi:

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

3
Có, nhưng TẠI SAO không nên kiểm tra null trước khi lấy điều tra viên?
Polaris878

12
@ Polaris878: Bởi vì nó không bao giờ được sử dụng với bộ sưu tập null. Đây là, IMO, một điều tốt - vì một tham chiếu null và một bộ sưu tập trống nên được xử lý riêng. Nếu bạn muốn giải quyết vấn đề này, có nhiều cách .. Tôi sẽ chỉnh sửa để hiển thị một tùy chọn khác ...
Reed Copsey

1
@ Polaris878: Tôi sẽ đề nghị viết lại câu hỏi của bạn: "Tại sao NÊN thời gian chạy thực hiện kiểm tra null trước khi nhận được liệt kê?"
Sậy Copsey

Tôi đoán tôi đang hỏi "tại sao không?" lol có vẻ như hành vi vẫn sẽ được xác định rõ
Polaris878

2
@ Polaris878: Tôi đoán, cách tôi nghĩ về nó, trả về null cho bộ sưu tập là một lỗi. Hiện tại, thời gian chạy cung cấp cho bạn một ngoại lệ có ý nghĩa trong trường hợp này, nhưng thật dễ dàng để xử lý (ví dụ: ở trên) nếu bạn không thích hành vi này. Nếu trình biên dịch che giấu điều này khỏi bạn, bạn sẽ mất kiểm tra lỗi khi chạy, nhưng sẽ không có cách nào để "tắt nó" ...
Reed Copsey

12

Nó đang được trả lời từ lâu nhưng tôi đã cố gắng làm điều này theo cách sau để tránh ngoại lệ con trỏ null và có thể hữu ích cho ai đó sử dụng toán tử kiểm tra C # null ?.

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

@kjbartel đã đánh bại bạn với điều này hơn một năm (tại " stackoverflow.com/a/32134295/401246 "). ;) Đây là giải pháp tốt nhất, vì nó không: a) liên quan đến sự suy giảm hiệu năng của (ngay cả khi không null) khái quát toàn bộ vòng lặp cho LCD của Enumerable(như sử dụng ??), b) yêu cầu thêm Phương thức mở rộng cho mọi Dự án, và c) yêu cầu tránh null IEnumerables (Pffft! Puh-LEAZE! SMH.) để bắt đầu.
Tom

10

Một phương pháp mở rộng khác để khắc phục điều này:

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

Tiêu thụ theo nhiều cách:

(1) với phương thức chấp nhận T:

returnArray.ForEach(Console.WriteLine);

(2) với một biểu thức:

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

(3) với phương pháp ẩn danh nhiều dòng

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

Chỉ thiếu một dấu ngoặc đơn đóng trong ví dụ thứ 3. Mặt khác, mã đẹp có thể được mở rộng hơn nữa theo những cách thú vị (cho các vòng lặp, đảo ngược, nhảy vọt, v.v.). Cám ơn vì đã chia sẻ.
Lara

Cảm ơn về một mã tuyệt vời như vậy, nhưng tôi đã không hiểu các phương thức đầu tiên, tại sao bạn vượt qua console.writeline làm tham số, mặc dù nó in các phần tử mảng. Nhưng không hiểu
Ajay Singh

@AjaySingh Console.WriteLinechỉ là một ví dụ về phương thức lấy một đối số (an Action<T>). Các mục 1, 2 và 3 đang hiển thị các ví dụ về việc truyền các hàm cho .ForEachphương thức mở rộng.
Jay

Câu trả lời của @ kjbartel (tại " stackoverflow.com/a/32134295/401246 " là giải pháp tốt nhất, bởi vì nó không: a) liên quan đến sự suy giảm hiệu suất của (ngay cả khi không null) khái quát toàn bộ vòng lặp sang LCD của Enumerable(như sử dụng ??sẽ ), b) yêu cầu thêm Phương thức mở rộng cho mọi Dự án hoặc c) yêu cầu tránh null IEnumerables (Pffft! Puh-LEAZE! SMH.) để bắt đầu bằng (cuz, nullcó nghĩa là N / A, trong khi danh sách trống có nghĩa là, đó là táo. hiện tại, tốt, trống !, tức là một Empl. có thể có Hoa hồng không áp dụng cho Không bán hàng hoặc trống cho Bán hàng).
Tom

5

Chỉ cần viết một phương thức mở rộng để giúp bạn hiểu:

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

Bởi vì bộ sưu tập null không giống với bộ sưu tập trống. Một bộ sưu tập trống là một đối tượng bộ sưu tập không có yếu tố; một bộ sưu tập null là một đối tượng không tồn tại.

Đây là một cái gì đó để thử: Khai báo hai bộ sưu tập thuộc bất kỳ loại nào. Khởi tạo một cái bình thường để nó trống và gán giá trị kia null. Sau đó thử thêm một đối tượng vào cả hai bộ sưu tập và xem điều gì sẽ xảy ra.


3

Đó là lỗi của Do.Something(). Cách thực hành tốt nhất ở đây sẽ là trả về một mảng có kích thước 0 (điều đó là có thể) thay vì null.


2

Bởi vì đằng sau hậu trường, foreachmột người điều tra, tương đương với điều này:

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

2
vì thế? Tại sao nó không thể đơn giản kiểm tra nếu nó là null đầu tiên và bỏ qua vòng lặp? AKA, chính xác những gì được hiển thị trong các phương pháp mở rộng? Câu hỏi là, tốt hơn là mặc định bỏ qua vòng lặp nếu null hoặc ném ngoại lệ? Tôi nghĩ tốt hơn là bỏ qua! Có vẻ như các container rỗng có nghĩa là được bỏ qua thay vì lặp đi lặp lại vì các vòng lặp có nghĩa là để làm một cái gì đó NẾU container không phải là null.
Tóm tắt

@ AbTHERDissonance Bạn có thể tranh luận tương tự với tất cả các nulltài liệu tham khảo, ví dụ như khi truy cập thành viên. Thông thường, đây là một lỗi và nếu không thì nó đủ đơn giản để xử lý ví dụ này với phương thức tiện ích mở rộng mà người dùng khác đã cung cấp làm câu trả lời.
Lucero

1
Tôi không nghĩ vậy. Các foreach có nghĩa là để hoạt động trên bộ sưu tập và khác với tham chiếu trực tiếp một đối tượng null. Mặc dù người ta có thể tranh luận tương tự, tôi cá là nếu bạn đã phân tích tất cả các mã trên thế giới, bạn sẽ có hầu hết các vòng lặp foreach có kiểm tra null nào đó trước mặt họ chỉ để bỏ qua vòng lặp khi bộ sưu tập là "null" (đó là do đó đối xử giống như trống rỗng). Tôi không nghĩ bất cứ ai coi việc lặp qua bộ sưu tập null là thứ họ muốn và chỉ đơn giản là bỏ qua vòng lặp nếu bộ sưu tập là null. Có lẽ, đúng hơn, một foreach? (Var x in C) có thể được sử dụng.
Tóm tắt

Điểm tôi chủ yếu cố gắng thực hiện là nó tạo ra một chút rác trong mã vì người ta phải kiểm tra mỗi lần mà không có lý do chính đáng. Các phần mở rộng, tất nhiên, hoạt động nhưng một tính năng ngôn ngữ có thể được thêm vào để tránh những điều này mà không có vấn đề gì nhiều. (chủ yếu tôi nghĩ rằng phương thức hiện tại tạo ra các lỗi ẩn do lập trình viên có thể quên đặt kiểm tra và do đó là một ngoại lệ ... bởi vì anh ta hy vọng kiểm tra xảy ra ở một nơi khác trước vòng lặp hoặc nghĩ rằng nó đã được khởi tạo trước (mà nó có thể hoặc có thể đã thay đổi). Nhưng trong cả hai nguyên nhân, hành vi sẽ giống như khi trống rỗng
Tóm tắt

@ AbTHERDissonance Vâng, với một số phân tích tĩnh thích hợp, bạn biết nơi bạn có thể có null và nơi nào không. Nếu bạn nhận được một giá trị mà bạn không mong đợi thì tốt hơn là thất bại thay vì âm thầm bỏ qua các vấn đề IMHO (trên tinh thần thất bại nhanh chóng ). Vì vậy tôi cảm thấy rằng đây là hành vi chính xác.
Lucero

1

Tôi nghĩ rằng lời giải thích tại sao ngoại lệ được ném là rất rõ ràng với các câu trả lời được cung cấp ở đây. Tôi chỉ muốn bổ sung với cách tôi thường làm việc với các bộ sưu tập của người Đức. Bởi vì, đôi khi, tôi sử dụng bộ sưu tập nhiều hơn một lần và phải kiểm tra nếu null mỗi lần. Để tránh điều đó, tôi làm như sau:

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

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

Bằng cách này, chúng tôi có thể sử dụng bộ sưu tập nhiều như chúng tôi muốn mà không sợ ngoại lệ và chúng tôi không xử lý mã với các tuyên bố có điều kiện quá mức.

Sử dụng toán tử kiểm tra null ?.cũng là một cách tiếp cận tuyệt vời. Nhưng, trong trường hợp mảng (như ví dụ trong câu hỏi), nó nên được chuyển thành Danh sách trước:

    int[] returnArray = DoSomething();

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

2
Chuyển đổi sang một danh sách chỉ để có quyền truy cập vào ForEachphương thức là một trong những điều tôi ghét trong một cơ sở mã.
huysentbeanw

Tôi đồng ý ... tôi tránh điều đó càng nhiều càng tốt. :(
Alielson Piffer

-2
SPListItem item;
DataRow dr = datatable.NewRow();

dr["ID"] = (!Object.Equals(item["ID"], null)) ? item["ID"].ToString() : string.Empty;
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.