簡體   English   中英

我應該在私有/內部方法中拋出null參數嗎?

[英]Should I throw on null parameters in private/internal methods?

我正在編寫一個包含多個公共類和方法的庫,以及庫本身使用的幾個私有或內部類和方法。

在公共方法中,我有一個空檢查和拋出這樣的:

public int DoSomething(int number)
{
    if (number == null)
    {
        throw new ArgumentNullException(nameof(number));
    }
}

但是這讓我思考,我應該在什么級別添加參數null檢查方法? 我是否也開始將它們添加到私有方法? 我應該只為公共方法做嗎?

最終,對此沒有統一的共識。 因此,我將嘗試列出做出此決定的注意事項,而不是給出是或否答案:

  • 空檢查會使代碼膨脹。 如果您的程序簡明扼要,那么它們開頭的空值守衛可能構成程序整體規模的重要部分,而不表達該程序的目的或行為。

  • 空檢查表達了一個先決條件。 如果某個方法在其中一個值為null時失敗,那么在頂部進行空檢查是一種很好的方式,可以向一個隨意的讀者證明這一點,而不必去尋找它被解除引用的位置。 為了改善這一點,人們經常使用名為Guard.AgainstNull幫助方法,而不是每次都要寫支票。

  • 檢查私有方法是不可測試的。 通過在代碼中引入一個無法完全遍歷的分支,您無法完全測試該方法。 這與測試記錄類的行為以及該類的代碼存在以提供該行為的觀點相沖突。

  • 讓null通過的嚴重程度取決於具體情況。 通常情況下,如果空進入的方法,這將是一個幾行后提領,你會得到一個NullReferenceException 這實際上並不比拋出ArgumentNullException更清楚。 另一方面,如果該引用在被取消引用之前傳遞了很多,或者如果拋出NRE會使事情處於混亂狀態,那么提前投擲就更為重要了。

  • 某些庫(如.NET的代碼約定)允許一定程度的靜態分析,這可以為您的檢查增加額外的好處。

  • 如果您正在與他人合作開展項目,可能會有現有的團隊或項目標准。

如果您不是圖書館開發人員,請不要在代碼中采取防御措施

改為編寫單元測試

事實上,即使你正在開發一個圖書館,投擲也是大部分時間:不好

1.在int上測試null絕對不能在c#中完成

它引發了警告CS4072 ,因為它總是錯誤的。

2.拋出異常意味着它是例外:異常和罕見。

它永遠不應該提高生產代碼。 特別是因為異常堆棧跟蹤遍歷可以是cpu密集型任務。 而且你永遠不會確定異常將被捕獲的位置,是否被捕獲和記錄,或者只是默默地忽略(在殺死你的一個后台線程之后)因為你不控制用戶代碼。 c#中沒有“已檢查的異常” (就像在java中一樣),這意味着你永遠不知道 - 如果沒有詳細記錄 - 給定方法可以引發什么異常。 順便說一句,這種文檔必須與代碼保持同步,這並不總是容易做到(增加維護成本)。

3.例外會增加維護成本。

由於在運行時和某些條件下拋出異常,因此可以在開發過程的后期檢測到異常。 您可能已經知道,在開發過程中檢測到的錯誤越晚,修復的成本就越高。 我甚至看到異常提升代碼進入生產代碼而不是提前一周,只是為了以后每天提高(殺死生產。哎呀!)。

4.拋出無效輸入意味着您無法控制輸入

公共圖書館方法就是這種情況。 但是,如果您可以在編譯時使用其他類型(例如像int這樣的非可空類型)檢查它,那么它就是要走的路。 當然,由於它們是公開的,因此檢查輸入是他們的責任。

想象一下,使用他認為有效數據的用戶然后通過副作用,堆棧跟蹤深處的方法會產生ArgumentNullException

  • 他的反應是什么?
  • 他怎么能應付這個?
  • 您是否容易提供解釋信息?

5.私有和內部方法永遠不應該拋出與其輸入相關的異常。

您可能會在代碼中拋出異常,因為外部組件(可能是數據庫,文件或其他)行為不正常,您無法保證您的庫將繼續在其當前狀態下正常運行。

將方法公之於眾並不意味着它應該(只有它可以)從你的庫外部調用( 看看公共與發布的Martin Fowler )。 使用IOC,接口,工廠並僅發布用戶需要的內容,同時使整個庫類可用於單元測試。 (或者您可以使用InternalsVisibleTo機制)。

6.在沒有任何解釋信息的情況下拋出異常就是取笑用戶

無需提醒工具被破壞時可以有什么樣的感受,而不需要知道如何修復它。 是的我知道。 你來到SO並問一個問題......

7.輸入無效意味着它會破壞您的代碼

如果您的代碼可以生成帶有該值的有效輸出,則它不是無效的,您的代碼應該對其進行管理。 添加單元測試以測試此值。

8.用用戶術語思考:

你喜歡當你使用的圖書館拋出異常粉碎你的臉時,你喜歡嗎? 喜歡:“嘿,這是無效的,你應該知道的!”

即使從您的角度來看 - 根據您對庫內部的了解 ,輸入無效,您如何向用戶解釋( 善良和禮貌 ):

  • 清晰的文檔(在Xml doc和體系結構摘要中可能有所幫助)。
  • 使用庫發布xml doc。
  • 如果有的話,在異常中清除錯誤說明。
  • 給出選擇:

看看Dictionary類,你更喜歡什么? 你覺得哪個電話最快? 什么電話會引發異常?

        Dictionary<string, string> dictionary = new Dictionary<string, string>();
        string res;
        dictionary.TryGetValue("key", out res);

要么

        var other = dictionary["key"];

9.為什么不使用代碼合同

這是一種避免丑陋的優雅方法, if then throw並將契約與實現隔離開來,允許同時重用不同實現的契約。 您甚至可以將合同發布給您的庫用戶,以進一步向他解釋如何使用該庫。

總而言之,即使您可以輕松使用throw ,即使您在使用.Net Framework時可能會遇到異常提升,但這並不意味着可以毫不謹慎地使用它。

以下是我的意見:


一般情況

一般來說, 最好在出於穩健性原因的方法中處理它們之前檢查任何無效輸入 - 無論是private, protected, internal, protected internal, or public方法。 雖然這種方法需要支付一些性能成本 ,但在大多數情況下,這是值得做的,而不是花費更多時間來調試和稍后修補代碼。


嚴格說來,但......

然而,嚴格地說, 並不總是需要這樣做 某些方法(通常是private方法)可以在沒有任何輸入檢查的情況下保留只要您完全保證沒有單獨調用具有無效輸入的方法。 這可能會為您帶來一些性能優勢 ,特別是如果經常調用該方法來執行某些基本計算/操作 對於這種情況,檢查輸入有效性可能會顯着影響性能。


公共方法

現在public方法比較棘手。 這是因為,更嚴格地說,雖然訪問修飾符單獨可以告訴誰可以使用該方法,它不能告訴誰將會使用的方法。 更重要的是,它也無法告訴方法將如何使用(即,是否將在給定范圍內使用無效輸入調用方法)。


終極決定因素

雖然代碼中方法的訪問修飾符可以提示如何使用這些方法,但最終, 人類將使用這些方法,並且由人們決定如何使用它們以及使用什么輸入。 因此,在極少數情況下,可以使用僅在某個private范圍內調用的public方法,並且在該private范圍內, public方法的輸入在調用public方法之前保證有效。

在這種情況下,即使訪問修飾符是public ,除了穩健的設計原因外,沒有任何實際需要檢查無效輸入。 為什么會這樣呢? 因為有些完全知道何時以及如何調用這些方法!

在這里我們可以看到,不能保證public方法總是需要檢查無效輸入。 如果對於public方法也是如此,那么對於protected, internal, protected internal, and private方法也必須如此。


結論

因此,總之,我們可以說幾件事來幫助我們做出決定:

  • 通常 ,如果性能不受影響 ,最好對穩健的設計原因檢查任何無效輸入。 對於任何類型的訪問修飾符都是如此。
  • 如果通過這樣做可以顯着提高性能增益,則可以跳過無效輸入檢查,前提是還可以保證調用方法的范圍始終為方法提供有效輸入。
  • private方法通常是我們跳過這種檢查的地方,但不能保證我們也不能為public方法這樣做
  • 人類是最終使用這些方法的人。 無論訪問修飾符如何暗示方法的使用,實際使用和調用方法的方式取決於編碼器。 因此,我們只能說一般/良好實踐,而不是限制它是唯一的方法
  1. 您的圖書館的公共界面值得仔細檢查前提條件,因為您應該期望圖書館的用戶犯錯誤並違反前提條件。 幫助他們了解您圖書館的動態。

  2. 庫中的私有方法不需要這樣的運行時檢查,因為您自己調用它們。 你完全可以控制你的傳球。 如果你想添加檢查,因為你害怕搞砸,那么使用斷言。 它們會捕獲您自己的錯誤,但不會妨礙運行時的性能。

雖然你標記了language-agnostic ,但在我看來它可能不存在一般的反應。

值得注意的是,在你的例子中,你暗示了這個論點:所以當一個語言接受暗示時,它會在你進行任何動作之前立即觸發錯誤。
在這種情況下,唯一的解決方案是在調用函數之前檢查參數...但是因為你正在編寫一個庫,所以沒有意義!

另一方面,沒有提示,檢查功能內部仍然是現實的。
所以在反思的這一步,我已經建議放棄暗示。

現在讓我們回到您的確切問題:應該檢查什么級別 對於給定的數據片段,它只發生在它可以“進入”的最高級別(可能是相同數據的幾次出現),所以邏輯上它只涉及公共方法。

那就是理論。 但也許您計划一個龐大而復雜的庫,因此要確保注冊所有“入口點”可能並不容易。
在這種情況下,我建議相反:考慮只是在任何地方應用你的控件,然后只在你清楚地看到它重復的地方省略它。

希望這可以幫助。

在我看來,你應該總是檢查“無效”數據 - 無論是私人還是公共方法。

從另一個方面來看......為什么你應該能夠處理一些無效的東西,因為這個方法是私有的? 沒有意義,對吧? 總是嘗試使用防御性編程,你會在生活中更快樂;-)

這是一個偏好問題。 但請考慮為什么要檢查null或者檢查有效輸入。 這可能是因為你想讓你的圖書館的消費者知道他/她錯誤地使用它。

讓我們假設我們在庫中實現了一個PersonList類。 此列表只能包含Person類型的對象。 我們還在PersonList實現了一些操作,因此我們不希望它包含任何空值。

考慮以下兩個Add列表的實現:

實施1

public void Add(Person item)
{
    if(_size == _items.Length)
    {
        EnsureCapacity(_size + 1);
    }

    _items[_size++] = item;
}

實施2

public void Add(Person item)
{
    if(item == null)
    {
        throw new ArgumentNullException("Cannot add null to PersonList");
    }

    if(_size == _items.Length)
    {
        EnsureCapacity(_size + 1);
    }

    _items[_size++] = item;
}

假設我們采用實現1

  • 現在可以在列表中添加空值
  • 列表上實現的所有操作都必須處理theese null值
  • 如果我們應該檢查並在我們的操作中拋出異常,當他/她正在調用其中一個操作時,消費者將被告知該異常,並且在此狀態下將非常不清楚他/她做錯了什么(它只是不會采取這種方法是沒有任何意義的。

如果我們選擇使用實現2,我們確保對我們的庫的輸入具有我們的類對其進行操作所需的質量。 這意味着我們只需要處理這個問題,然后在實施其他操作時我們可以忘記它。

當消費者在.Add而不是.Sort或similair上獲得ArgumentNullException時,他/她以錯誤的方式使用庫也將變得更加清楚。

總結一下,我的偏好是在消費者提供它時檢查有效參數,而不是由庫的私有/內部方法處理。 這基本上意味着我們必須檢查public的構造函數/方法中的參數並獲取參數。 我們的private / internal方法只能從我們的公共方法調用,他們已經檢查了輸入,這意味着我們很高興!

驗證輸入時還應考慮使用代碼合同

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM