การตรวจสอบพารามิเตอร์ Null ใน C #


88

ใน C # มีเหตุผลที่ดี (นอกเหนือจากข้อความแสดงข้อผิดพลาดที่ดีกว่า) สำหรับการเพิ่มการตรวจสอบพารามิเตอร์เป็นโมฆะให้กับทุกฟังก์ชันที่ null ไม่ใช่ค่าที่ถูกต้องหรือไม่? เห็นได้ชัดว่ารหัสที่ใช้ s จะทำให้เกิดข้อยกเว้นอยู่ดี และการตรวจสอบดังกล่าวทำให้โค้ดช้าลงและดูแลรักษายากขึ้น

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

  // Use s
}

13
ฉันสงสัยอย่างมากว่าการตรวจสอบค่าว่างแบบธรรมดาจะทำให้โค้ดของคุณช้าลงตามจำนวนที่มีนัยสำคัญ (หรือแม้แต่วัดได้)
Heinzi

มันขึ้นอยู่กับว่า "ใช้ s" ทำอะไร หากทำได้ทั้งหมดคือ "return s.SomeField" ดังนั้นการตรวจสอบเพิ่มเติมอาจทำให้วิธีการช้าลงตามลำดับขนาด
kaalus

11
@kaalus: น่าจะ? อาจไม่ได้ตัดมันในหนังสือของฉัน ข้อมูลการทดสอบของคุณอยู่ที่ไหน และความเป็นไปได้ที่การเปลี่ยนแปลงดังกล่าวจะเป็นปัญหาคอขวดของแอปพลิเคชันของคุณตั้งแต่แรก?
Jon Skeet

@ จอน: คุณพูดถูกฉันไม่มีข้อมูลที่ยากที่นี่ หากคุณพัฒนาสำหรับ Windows Phone หรือ Xbox โดยที่ JIT inlining จะได้รับผลกระทบจาก "if" พิเศษนี้และการต่อกิ่งมีราคาแพงมากคุณอาจหวาดระแวงได้มาก
kaalus

3
@kaalus: ดังนั้นให้ปรับรูปแบบโค้ดของคุณในกรณีที่เฉพาะเจาะจงเหล่านั้นหลังจากพิสูจน์แล้วว่าสร้างความแตกต่างอย่างมีนัยสำคัญหรือใช้ Debug ยืนยัน (อีกครั้งในสถานการณ์เหล่านั้นดูคำตอบของ Eric เท่านั้น) เพื่อให้การตรวจสอบเปิดใช้งานในบางรุ่นเท่านั้น
Jon Skeet

คำตอบ:


167

ใช่มีเหตุผลที่ดี:

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

ตอนนี้สำหรับการคัดค้านของคุณ:

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

และสำหรับการยืนยันของคุณ:

เห็นได้ชัดว่ารหัสที่ใช้ s จะทำให้เกิดข้อยกเว้นอยู่ดี

จริงๆ? พิจารณา:

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

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

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

หมายเหตุด้านข้าง: โอเวอร์โหลดตัวสร้างพารามิเตอร์เดียวArgumentNullExceptionควรเป็นชื่อพารามิเตอร์ดังนั้นการทดสอบของคุณควรเป็น:

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

อีกวิธีหนึ่งคือคุณสามารถสร้างวิธีการขยายโดยปล่อยให้ตัวปรับลดลง:

s.ThrowIfNull("s");

ในวิธีการขยาย (ทั่วไป) เวอร์ชันของฉันฉันทำให้มันคืนค่าเดิมถ้ามันไม่ใช่โมฆะทำให้คุณสามารถเขียนสิ่งต่างๆเช่น:

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

นอกจากนี้คุณยังสามารถโอเวอร์โหลดได้โดยไม่ใช้ชื่อพารามิเตอร์หากคุณไม่ใส่ใจเกินไป


8
+1 สำหรับวิธีการขยาย ฉันได้ทำสิ่งเดียวกันทั้งหมดรวมถึงการตรวจสอบความถูกต้องอื่น ๆ เช่นThrowIfEmptyเมื่อICollection
Davy8

5
ทำไมฉันถึงคิดว่ามันยากที่จะรักษา: เช่นเดียวกับกลไกแบบแมนนวลที่ต้องมีการแทรกแซงของโปรแกรมเมอร์ทุกครั้งสิ่งนี้มีแนวโน้มที่จะเกิดข้อผิดพลาดและการละเว้นและทำให้โค้ดกระจัดกระจาย ความปลอดภัยที่ไม่เป็นขุยแย่กว่าความปลอดภัยเลย
kaalus

8
@kaalus: คุณใช้ทัศนคติเดียวกันกับการทดสอบหรือไม่? "การทดสอบของฉันไม่สามารถตรวจจับข้อบกพร่องที่เป็นไปได้ทั้งหมดดังนั้นฉันจะไม่เขียน" เมื่อกลไกความปลอดภัยทำการทำงานของพวกเขาจะทำให้มันง่ายต่อการค้นหาปัญหาและลดผลกระทบอื่น ๆ (โดยการจับปัญหาก่อนหน้านี้) ถ้าเกิดขึ้น 9 ครั้งจาก 10 ครั้งก็ยังดีกว่าเกิดขึ้น 0 ครั้งจาก 10 ครั้ง ...
Jon Skeet

2
@DoctorOreo: ฉันไม่ใช้Debug.Assert. การจับข้อผิดพลาดในการผลิต (ก่อนที่จะทำให้ข้อมูลจริงเสียหาย) สำคัญกว่าในการพัฒนา
Jon Skeet

3
ตอนนี้ด้วย C # 6.0 เรายังสามารถใช้ได้ throw new ArgumentNullException(nameof(s))
hrzafer

52

ฉันเห็นด้วยกับจอน แต่ฉันจะเพิ่มสิ่งหนึ่งเข้าไป

ทัศนคติของฉันเกี่ยวกับเวลาที่จะเพิ่มการตรวจสอบ null ที่ชัดเจนขึ้นอยู่กับสถานที่เหล่านี้:

  • ควรมีวิธีสำหรับการทดสอบหน่วยของคุณเพื่อใช้ทุกคำสั่งในโปรแกรม
  • throwงบงบ
  • ผลที่ตามมาจากการifเป็นคำสั่ง
  • ดังนั้นควรมีวิธีการออกกำลังกายthrowในif (x == null) throw whatever;

หากมีไม่มีทางเป็นไปได้Debug.Assert(x != null);สำหรับคำสั่งที่จะต้องถูกประหารชีวิตแล้วมันไม่สามารถทดสอบและควรถูกแทนที่ด้วย

หากมีวิธีที่เป็นไปได้สำหรับการดำเนินการคำสั่งนั้นให้เขียนคำสั่งนั้นจากนั้นเขียนแบบทดสอบหน่วยที่ใช้คำสั่งนั้น

เป็นสิ่งสำคัญอย่างยิ่งที่วิธีการสาธารณะประเภทสาธารณะจะตรวจสอบข้อโต้แย้งของตนด้วยวิธีนี้ คุณไม่รู้ว่าผู้ใช้ของคุณกำลังจะทำอะไร ให้พวกเขาว่า "เฮ้คุณกระดูกคุณทำผิด!" ข้อยกเว้นโดยเร็วที่สุด

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


23

ฉันใช้สิ่งนี้มาเป็นปีแล้ว:

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

มันคือ oneliner และ discard ( _) หมายความว่าไม่มีการจัดสรรที่ไม่จำเป็น


8

หากไม่มีการifตรวจสอบอย่างชัดเจนอาจเป็นเรื่องยากมากที่จะทราบว่าเกิดอะไรขึ้นnullถ้าคุณไม่ได้เป็นเจ้าของรหัส

หากคุณได้รับNullReferenceExceptionจากส่วนลึกในไลบรารีโดยไม่มีซอร์สโค้ดคุณอาจมีปัญหาในการหาสิ่งที่คุณทำผิดพลาด

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


โปรดสังเกตว่าพารามิเตอร์ของตัวArgumentNullExceptionสร้างเป็นชื่อพารามิเตอร์ไม่ใช่ข้อความ
รหัสของคุณควรเป็น

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

ฉันเขียนข้อมูลโค้ดเพื่อให้ง่ายขึ้น:

<?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

คุณอาจต้องการดูCode Contractsหากคุณต้องการวิธีที่ดีกว่าเพื่อให้แน่ใจว่าคุณไม่ได้รับวัตถุว่างใด ๆ เป็นพารามิเตอร์


2

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

นอกจากนี้การตรวจสอบจะหยุดการทำงานของวิธีการก่อนที่รหัสอื่น ๆ จะดำเนินการ นั่นหมายความว่าคุณจะไม่ต้องกังวลว่าจะมีการแก้ไขด้วยวิธีการที่ยังไม่เสร็จสิ้น


2

ช่วยประหยัดการดีบักเมื่อคุณกดข้อยกเว้นนั้น

ArgumentNullException ระบุอย่างชัดเจนว่าเป็น "s" ซึ่งเป็นโมฆะ

หากคุณไม่มีการตรวจสอบนั้นและปล่อยให้รหัสระเบิดคุณจะได้รับ NullReferenceException จากบรรทัดที่ไม่ระบุชื่อในวิธีการนั้น ในรุ่นที่เผยแพร่คุณจะไม่ได้รับหมายเลขบรรทัด!


0

รหัสเดิม:

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

  // Use s
}

เขียนใหม่เป็น:

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

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


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