Kiểm tra tham số rỗng trong C #


85

Trong C #, có bất kỳ lý do chính đáng nào (ngoài một thông báo lỗi tốt hơn) để thêm kiểm tra null tham số vào mọi hàm trong đó null không phải là giá trị hợp lệ không? Rõ ràng, mã sử dụng s sẽ ném ra một ngoại lệ. Và việc kiểm tra như vậy làm cho mã chậm hơn và khó bảo trì hơn.

void f(SomeType s)
{
  if (s == null)
  {
    throw new ArgumentNullException("s cannot be null.");
  }

  // Use s
}

12
Tôi thực sự nghi ngờ rằng việc kiểm tra rỗng đơn giản sẽ làm chậm mã của bạn một lượng đáng kể (hoặc thậm chí có thể đo lường được).
Heinzi

Vâng, nó phụ thuộc vào những gì "Sử dụng s" làm. Nếu tất cả những gì nó làm là "return s.SomeField" thì việc kiểm tra thêm có thể sẽ làm chậm phương thức theo một bậc lớn.
kaalus

10
@kaalus: Có lẽ vậy? Có lẽ không cắt nó trong cuốn sách của tôi. Dữ liệu thử nghiệm của bạn ở đâu? Và khả năng thay đổi như vậy sẽ là nút thắt cổ chai cho ứng dụng của bạn ngay từ đầu như thế nào?
Jon Skeet

@Jon: bạn nói đúng, tôi không có dữ liệu cứng ở đây. Nếu bạn phát triển cho Windows Phone hoặc Xbox trong đó nội tuyến JIT sẽ bị ảnh hưởng bởi "nếu" bổ sung này và nơi phân nhánh rất đắt, bạn có thể khá hoang tưởng.
kaalus

3
@kaalus: Vì vậy, hãy điều chỉnh kiểu mã của bạn trong những trường hợp rất cụ thể sau khi chứng minh rằng nó tạo ra sự khác biệt đáng kể hoặc sử dụng Debug.Assert (một lần nữa, chỉ trong những trường hợp đó, hãy xem câu trả lời của Eric) để việc kiểm tra chỉ được thực hiện trong một số bản dựng nhất định.
Jon Skeet

Câu trả lời:


163

Có, có những lý do chính đáng:

  • Nó xác định chính xác những gì là null, có thể không rõ ràng từ NullReferenceException
  • Nó làm cho mã không thành công trên đầu vào không hợp lệ ngay cả khi một số điều kiện khác có nghĩa là giá trị không được tham chiếu
  • Nó làm cho ngoại lệ xảy ra trước khi phương pháp có thể có bất kỳ tác dụng phụ nào khác mà bạn có thể gặp phải trước lần tham khảo đầu tiên
  • Điều đó có nghĩa là bạn có thể tự tin rằng nếu bạn chuyển tham số vào thứ khác, bạn không vi phạm hợp đồng của họ
  • Nó ghi lại các yêu cầu của phương pháp của bạn (tất nhiên là sử dụng Hợp đồng mã thậm chí còn tốt hơn)

Bây giờ đối với sự phản đối của bạn:

  • Nó chậm hơn : Bạn có thấy đây thực sự là nút thắt cổ chai trong mã của bạn hay bạn đang đoán? Kiểm tra vô hiệu rất nhanh chóng và trong phần lớn các trường hợp, chúng sẽ không phải là nút cổ chai
  • Nó làm cho mã khó bảo trì hơn : Tôi nghĩ ngược lại. Tôi nghĩ rằng sẽ dễ dàng hơn khi sử dụng mã khi nó được làm rõ ràng cho dù một tham số có thể rỗng hay không và bạn tin tưởng rằng điều kiện đó được thực thi.

Và khẳng định của bạn:

Rõ ràng, mã sử dụng s sẽ ném ra một ngoại lệ.

Có thật không? Xem xét:

void f(SomeType s)
{
  // Use s
  Console.WriteLine("I've got a message of {0}", s);
}

Điều đó sử dụng s, nhưng nó không ném ra một ngoại lệ. Nếu nó không hợp lệ slà null và điều đó cho thấy rằng có gì đó không ổn, thì một ngoại lệ là hành vi thích hợp nhất ở đây.

Bây giờ bạn đặt các kiểm tra xác thực đối số đó ở đâu là một vấn đề khác. Bạn có thể quyết định tin tưởng tất cả mã trong lớp của riêng mình, vì vậy đừng bận tâm đến các phương thức riêng tư. Bạn có thể quyết định tin tưởng phần còn lại của assembly của mình, vì vậy đừng bận tâm đến các phương pháp nội bộ. Bạn gần như chắc chắn nên xác nhận các đối số cho các phương thức công khai.

Một lưu ý phụ: quá tải hàm tạo một tham số của ArgumentNullExceptionchỉ nên là tên tham số, vì vậy thử nghiệm của bạn phải là:

if (s == null)
{
  throw new ArgumentNullException("s");
}

Ngoài ra, bạn có thể tạo một phương thức mở rộng, cho phép phần nào ngắn gọn hơn:

s.ThrowIfNull("s");

Trong phiên bản của tôi về phương thức mở rộng (chung), tôi đặt nó trả về giá trị ban đầu nếu nó không phải null, cho phép bạn viết những thứ như:

this.name = name.ThrowIfNull("name");

Bạn cũng có thể có quá tải không lấy tên tham số, nếu bạn không quá bận tâm về điều đó.


8
+1 cho phương thức mở rộng. Tôi đã làm điều tương tự chính xác, bao gồm cả xác thực khác chẳng hạn như ThrowIfEmptytrênICollection
Davy8

5
Tại sao tôi nghĩ rằng nó khó bảo trì hơn: như với mọi cơ chế thủ công luôn cần sự can thiệp của lập trình viên, điều này dễ xảy ra sai sót và thiếu sót và làm lộn xộn mã. An toàn bong tróc tồi tệ hơn là không an toàn chút nào.
kaalus

8
@kaalus: Bạn có áp dụng cùng một thái độ để kiểm tra không? "Các bài kiểm tra của tôi sẽ không bắt được tất cả các lỗi có thể xảy ra, do đó tôi sẽ không viết bất kỳ lỗi nào"? Khi các cơ chế an toàn làm việc, họ sẽ làm cho nó dễ dàng hơn để tìm ra vấn đề và giảm tác động ở những nơi khác (bằng cách bắt các vấn đề trước đó). Nếu điều đó xảy ra 9 lần trong số 10, đó là vẫn còn tốt hơn so với nó xảy ra 0 lần trong số 10 ...
Jon Skeet

2
@DoctorOreo: Tôi không sử dụng Debug.Assert. Việc bắt lỗi trong quá trình sản xuất (trước khi chúng làm hỏng dữ liệu thực) thậm chí còn quan trọng hơn trong quá trình phát triển.
Jon Skeet

2
Bây giờ với C # 6.0, chúng tôi thậm chí có thể sử dụng throw new ArgumentNullException(nameof(s))
hrzafer

52

Tôi đồng ý với Jon, nhưng tôi sẽ thêm một điều vào điều đó.

Thái độ của tôi về thời điểm thêm kiểm tra rỗng rõ ràng dựa trên các cơ sở sau:

  • Cần có một cách để các bài kiểm tra đơn vị của bạn thực hiện mọi câu lệnh trong một chương trình.
  • throwcâu lệnh là câu lệnh .
  • Hệ quả của an iflà một câu lệnh .
  • Do đó, cần có một cách để thực hiện throwtrongif (x == null) throw whatever;

Nếu không có cách nào khả thi để thực thi câu lệnh đó thì nó không thể được kiểm tra và nên được thay thế bằng Debug.Assert(x != null);.

Nếu có một cách khả thi để câu lệnh đó được thực thi thì hãy viết câu lệnh đó, rồi viết một bài kiểm tra đơn vị thực hiện nó.

Điều đặc biệt quan trọng là các phương thức public của các loại public kiểm tra các đối số của chúng theo cách này; bạn không biết người dùng của bạn sẽ làm điều điên rồ gì. Hãy cho họ câu "Này bạn, bạn đang làm sai!" ngoại lệ càng sớm càng tốt.

Ngược lại, các phương thức private của các loại private có nhiều khả năng xảy ra trong trường hợp bạn kiểm soát các đối số và có thể đảm bảo chắc chắn rằng đối số không bao giờ là rỗng; sử dụng một khẳng định để ghi lại sự bất biến đó.


22

Tôi đã sử dụng cái này được một năm rồi:

_ = s ?? throw new ArgumentNullException(nameof(s));

Nó là oneliner, và discard ( _) có nghĩa là không có phân bổ không cần thiết.


8

Nếu không có ifkiểm tra rõ ràng , có thể rất khó để tìm ra đónull nếu bạn không sở hữu mã.

Nếu bạn nhận được NullReferenceExceptiontừ sâu bên trong một thư viện mà không có mã nguồn, bạn có thể gặp rất nhiều khó khăn để tìm ra những gì bạn đã làm sai.

Những ifkiểm tra này sẽ không làm cho mã của bạn chậm hơn đáng kể.


Lưu ý rằng tham số của hàm ArgumentNullExceptiontạo là tên tham số, không phải là thông báo.
Mã của bạn phải là

if (s == null) throw new ArgumentNullException("s");

Tôi đã viết một đoạn mã để làm cho việc này dễ dàng hơn:

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>Check for null arguments</Title>
            <Shortcut>tna</Shortcut>
            <Description>Code snippet for throw new ArgumentNullException</Description>
            <Author>SLaks</Author>
            <SnippetTypes>
                <SnippetType>Expansion</SnippetType>
                <SnippetType>SurroundsWith</SnippetType>
            </SnippetTypes>
        </Header>
        <Snippet>
            <Declarations>
                <Literal>
                    <ID>Parameter</ID>
                    <ToolTip>Paremeter to check for null</ToolTip>
                    <Default>value</Default>
                </Literal>
            </Declarations>
            <Code Language="csharp"><![CDATA[if ($Parameter$ == null) throw new ArgumentNullException("$Parameter$");
        $end$]]>
            </Code>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>

5

Bạn có thể muốn xem qua Hợp đồng mã nếu bạn cần một cách tốt hơn để đảm bảo rằng bạn không nhận bất kỳ đối tượng rỗng nào làm tham số.


2

Lợi ích chính là bạn đang rõ ràng với các yêu cầu của phương pháp của bạn ngay từ đầu. Điều này làm cho các nhà phát triển khác làm việc trên mã hiểu rõ rằng việc người gọi gửi giá trị null đến phương thức của bạn thực sự là một lỗi.

Việc kiểm tra cũng sẽ tạm dừng việc thực thi phương thức trước khi bất kỳ mã nào khác thực thi. Điều đó có nghĩa là bạn sẽ không phải lo lắng về việc sửa đổi được thực hiện bằng phương pháp bị bỏ dở.


2

Nó tiết kiệm một số gỡ lỗi, khi bạn nhấn vào ngoại lệ đó.

ArgumentNullException tuyên bố rõ ràng rằng đó là "s" là null.

Nếu bạn không có kiểm tra đó và để cho mã nổ tung, bạn sẽ nhận được NullReferenceException từ một số dòng không xác định trong phương thức đó. Trong một phiên bản phát hành, bạn không nhận được số dòng!


0

Mã gốc:

void f(SomeType s)
{
  if (s == null)
  {
    throw new ArgumentNullException("s cannot be null.");
  }

  // Use s
}

Viết lại nó thành:

void f(SomeType s)
{
  if (s == null) throw new ArgumentNullException(nameof(s));
}

[Chỉnh sửa] Lý do để viết lại bằng cách sử dụng nameoflà vì nó cho phép tái cấu trúc dễ dàng hơn. Nếu tên biến của bạn sthay đổi thì các thông báo gỡ lỗi cũng sẽ được cập nhật, trong khi nếu bạn chỉ mã hóa tên của biến thì cuối cùng nó sẽ lỗi thời khi cập nhật theo thời gian. Đó là một thực tiễn tốt được sử dụng trong ngành công nghiệp.


Nếu bạn đang đề xuất thay đổi, hãy giải thích lý do. Đồng thời đảm bảo rằng bạn đang trả lời câu hỏi đang được hỏi.
CodeCaster

-1
int i = Age ?? 0;

Vì vậy, ví dụ của bạn:

if (age == null || age == 0)

Hoặc là:

if (age.GetValueOrDefault(0) == 0)

Hoặc là:

if ((age ?? 0) == 0)

Hoặc bậc ba:

int i = age.HasValue ? age.Value : 0;
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.