Debug.Assert so với Exception Throwing


92

Tôi đã đọc rất nhiều bài báo (và một vài câu hỏi tương tự khác đã được đăng trên StackOverflow) về cách thức và thời điểm sử dụng các xác nhận, và tôi hiểu rõ về chúng. Nhưng tôi vẫn không hiểu loại động lực nào nên thúc đẩy tôi sử dụng Debug.Assertthay vì ném ra một ngoại lệ rõ ràng. Ý tôi là, trong .NET, phản hồi mặc định cho một xác nhận không thành công là "dừng thế giới" và hiển thị một hộp thông báo cho người dùng. Mặc dù loại hành vi này có thể được sửa đổi, nhưng tôi thấy thật khó chịu và thừa thãi khi làm điều đó, trong khi thay vào đó, tôi có thể chỉ cần đưa ra một ngoại lệ phù hợp. Bằng cách này, tôi có thể dễ dàng ghi lỗi vào nhật ký của ứng dụng ngay trước khi tôi ném ngoại lệ và thêm vào đó, ứng dụng của tôi không nhất thiết phải đóng băng.

Vì vậy, tại sao tôi nên sử dụng Debug.Assertthay vì một ngoại lệ đơn giản? Đặt một khẳng định ở nơi không nên có thể chỉ gây ra tất cả các loại "hành vi không mong muốn", vì vậy theo quan điểm của tôi, tôi thực sự không thu được gì bằng cách sử dụng một khẳng định thay vì ném một ngoại lệ. Bạn có đồng ý với tôi, hay tôi đang thiếu một cái gì đó ở đây?

Lưu ý: Tôi hoàn toàn hiểu sự khác biệt "trên lý thuyết" (Gỡ lỗi so với Phát hành, cách sử dụng, v.v.), nhưng theo tôi thấy, tốt hơn là tôi nên ném một ngoại lệ thay vì thực hiện xác nhận. Vì nếu một lỗi được phát hiện trên bản phát hành sản xuất, tôi vẫn muốn "xác nhận" không thành công (xét cho cùng, "chi phí" nhỏ đến mức nực cười), vì vậy tốt hơn hết tôi nên ném một ngoại lệ.


Chỉnh sửa: Theo cách tôi thấy, nếu một xác nhận không thành công, điều đó có nghĩa là ứng dụng đã vào một loại trạng thái không mong muốn, bị hỏng. Vậy tại sao tôi lại muốn tiếp tục thực hiện? Không quan trọng nếu ứng dụng chạy trên phiên bản gỡ lỗi hoặc phát hành. Điều này cũng xảy ra với cả hai


1
Đối với những thứ mà bạn đang nói "nếu một lỗi được phát hiện trên một thông cáo sản xuất, tôi vẫn muốn rằng 'khẳng định' sẽ thất bại" về, trường hợp ngoại lệ là những gì bạn nên sử dụng
Tom Neyland

1
Hiệu suất là lý do duy nhất. Không kiểm tra mọi thứ mọi lúc có thể làm giảm tốc độ, mặc dù nó có thể hoàn toàn không đáng chú ý. Điều này chủ yếu dành cho các trường hợp không bao giờ nên xảy ra, ví dụ như bạn biết bạn đã kiểm tra null trong một chức năng trước đó, không có điểm lãng phí chu kỳ kiểm tra lại nó. Debug.assert hoạt động hiệu quả giống như một thử nghiệm đơn vị cơ hội cuối cùng để thông báo cho bạn.
cuộn

Câu trả lời:


175

Mặc dù tôi đồng ý rằng lý do của bạn là hợp lý - nghĩa là, nếu một khẳng định bị vi phạm bất ngờ, thì việc tạm dừng thực thi bằng cách ném là điều hợp lý - cá nhân tôi sẽ không sử dụng ngoại lệ thay cho các khẳng định. Đây là lý do tại sao:

Như những người khác đã nói, các xác nhận phải ghi lại các tình huống không thể xảy ra , theo cách mà nếu tình huống được cho là không thể xảy ra, nhà phát triển sẽ được thông báo. Ngược lại, các trường hợp ngoại lệ cung cấp một cơ chế điều khiển dòng chảy cho các tình huống ngoại lệ, không chắc chắn hoặc sai sót, nhưng không phải là các tình huống bất khả thi. Đối với tôi, điểm khác biệt chính là:

  • LUÔN LUÔN có thể tạo ra một trường hợp thử nghiệm thực hiện một câu lệnh ném đã cho. Nếu không thể tạo ra một trường hợp thử nghiệm như vậy thì bạn có một đường dẫn mã trong chương trình của mình mà không bao giờ thực thi và nó sẽ bị xóa như là mã chết.

  • KHÔNG BAO GIỜ có thể tạo ra một trường hợp thử nghiệm gây ra sự khẳng định. Nếu một xác nhận kích hoạt, hoặc là mã sai hoặc khẳng định sai; một trong hai cách, một số thứ cần thay đổi trong mã.

Đó là lý do tại sao tôi sẽ không thay thế một khẳng định bằng một ngoại lệ. Nếu xác nhận thực sự không thể kích hoạt, thì việc thay thế nó bằng một ngoại lệ có nghĩa là bạn có một đường dẫn mã không thể kiểm tra được trong chương trình của mình . Tôi không thích các đường dẫn mã không thể kiểm chứng được.


16
Vấn đề với các xác nhận là chúng không có trong bản dựng sản xuất. Không đạt được một điều kiện giả định có nghĩa là chương trình của bạn đã đi vào vùng đất hành vi không xác định, trong trường hợp đó, một chương trình có trách nhiệm phải tạm dừng thực thi càng sớm càng tốt (việc tháo ngăn xếp cũng hơi nguy hiểm, tùy thuộc vào mức độ nghiêm ngặt mà bạn muốn đạt được). Đúng vậy, những lời khẳng định thường không thể được đưa ra, nhưng bạn không biết điều gì có thể xảy ra khi mọi thứ trở nên hoang dã. Điều bạn nghĩ là không thể có thể xảy ra trong quá trình sản xuất và một chương trình có trách nhiệm phải phát hiện các giả định bị vi phạm và hành động kịp thời.
kizzx2

2
@ kizzx2: OK, vậy bạn viết bao nhiêu ngoại lệ không thể cho mỗi dòng mã sản xuất?
Eric Lippert

4
@ kixxx2: Đây là C #, vì vậy bạn có thể giữ các xác nhận trong mã sản xuất bằng cách sử dụng Trace.Assert. Bạn thậm chí có thể sử dụng tệp app.config để chuyển hướng lại các xác nhận sản xuất sang tệp văn bản thay vì thô lỗ với người dùng cuối.
HTTP 410

3
@AnorZaken: Quan điểm của bạn minh họa một lỗ hổng thiết kế trong các trường hợp ngoại lệ. Như tôi đã lưu ý ở những nơi khác, các trường hợp ngoại lệ là (1) thảm họa chết người, (2) những sai lầm nguy hiểm không bao giờ xảy ra, (3) lỗi thiết kế trong đó một ngoại lệ được sử dụng để báo hiệu một điều kiện không đặc biệt, hoặc (4) các điều kiện ngoại sinh không mong muốn . Tại sao bốn điều hoàn toàn khác nhau này đều được biểu thị bằng các ngoại lệ? Nếu tôi có người say rượu của mình, các ngoại lệ "null đã được tham chiếu" đầu xương sẽ không thể bắt được . Nó không bao giờ đúng và nó sẽ chấm dứt chương trình của bạn trước khi nó gây hại nhiều hơn . Chúng nên giống những lời khẳng định hơn.
Eric Lippert

2
@Backwards_Dave: khẳng định sai là không tốt, không đúng. Các xác nhận cho phép bạn chạy các kiểm tra xác minh đắt tiền mà bạn không muốn chạy trong sản xuất. Và nếu một xác nhận bị vi phạm trong quá trình sản xuất, người dùng cuối phải làm gì với nó?
Eric Lippert

17

Các xác nhận được sử dụng để kiểm tra sự hiểu biết của lập trình viên về thế giới. Một xác nhận chỉ nên thất bại nếu lập trình viên đã làm sai điều gì đó. Ví dụ: không bao giờ sử dụng xác nhận để kiểm tra thông tin đầu vào của người dùng.

Kiểm tra cảnh báo cho các điều kiện "không thể xảy ra". Các trường hợp ngoại lệ dành cho các điều kiện "không nên xảy ra nhưng phải làm".

Các xác nhận hữu ích vì tại thời điểm xây dựng (hoặc thậm chí thời gian chạy), bạn có thể thay đổi hành vi của chúng. Ví dụ: thường trong các bản dựng phát hành, các xác nhận thậm chí không được kiểm tra, vì chúng giới thiệu chi phí không cần thiết. Đây cũng là điều cần cảnh giác: các bài kiểm tra của bạn thậm chí có thể không được thực hiện.

Nếu bạn sử dụng ngoại lệ thay vì xác nhận, bạn sẽ mất một số giá trị:

  1. Mã dài hơn, vì kiểm tra và ném một ngoại lệ là ít nhất hai dòng, trong khi một xác nhận chỉ là một.

  2. Mã thử nghiệm và ném của bạn sẽ luôn chạy, trong khi các xác nhận có thể được biên dịch.

  3. Bạn mất một số thông tin liên lạc với các nhà phát triển khác, bởi vì xác nhận có ý nghĩa khác với mã sản phẩm kiểm tra và ném. Nếu bạn thực sự đang kiểm tra một xác nhận lập trình, hãy sử dụng một xác nhận.

Thêm tại đây: http://nedbatchelder.com/text/assert.html


Nếu nó "không thể xảy ra", thì tại sao viết một khẳng định. Điều đó không thừa phải không? Nếu nó thực sự có thể xảy ra nhưng không nên xảy ra, thì điều này không giống như "không nên xảy ra nhưng hãy làm" dành cho các trường hợp ngoại lệ?
David Klempfner

2
"Không thể xảy ra" nằm trong dấu ngoặc kép vì một lý do: nó chỉ có thể xảy ra nếu lập trình viên đã làm sai điều gì đó trong phần khác của chương trình. Sự khẳng định là một sự kiểm tra chống lại những sai lầm của lập trình viên.
Ned Batchelder

@NedBatchelder Tuy nhiên, thuật ngữ lập trình viên hơi mơ hồ khi bạn phát triển một thư viện. Phải chăng những "tình huống bất khả thi" đó không thể xảy ra với người sử dụng thư viện, nhưng lại có thể xảy ra khi tác giả thư viện mắc lỗi?
Bruno Zell ngày

12

CHỈNH SỬA: Đáp lại chỉnh sửa / ghi chú bạn đã thực hiện trong bài đăng của mình: Có vẻ như sử dụng ngoại lệ là điều đúng đắn để sử dụng thay vì sử dụng xác nhận cho loại công việc bạn đang cố gắng hoàn thành. Tôi nghĩ rằng trở ngại tinh thần mà bạn đang gặp phải là bạn đang xem xét các ngoại lệ và xác nhận để thực hiện cùng một mục đích, và vì vậy bạn đang cố gắng tìm ra cái nào là 'đúng' để sử dụng. Mặc dù có thể có một số trùng lặp về cách sử dụng các xác nhận và ngoại lệ, nhưng đừng nhầm lẫn rằng chúng là các giải pháp khác nhau cho cùng một vấn đề - chúng không phải vậy. Mỗi khẳng định và ngoại lệ đều có mục đích, điểm mạnh và điểm yếu riêng.

Tôi đã định gõ một câu trả lời bằng lời của mình nhưng điều này không có khái niệm công lý tốt hơn tôi sẽ có:

Trạm C #: Khẳng định

Việc sử dụng các câu lệnh khẳng định có thể là một cách hiệu quả để bắt lỗi logic chương trình trong thời gian chạy, nhưng chúng dễ dàng bị lọc ra khỏi mã sản xuất. Khi quá trình phát triển hoàn tất, chi phí thời gian chạy của các bài kiểm tra dư thừa này đối với lỗi mã hóa có thể được loại bỏ đơn giản bằng cách xác định ký hiệu tiền xử lý NDEBUG [vô hiệu hóa tất cả các xác nhận] trong quá trình biên dịch. Tuy nhiên, hãy chắc chắn rằng mã được đặt trong chính xác nhận sẽ bị bỏ qua trong phiên bản sản xuất.

Một khẳng định chỉ được sử dụng để kiểm tra một điều kiện khi tất cả các điều kiện sau đây được giữ nguyên:

* the condition should never be false if the code is correct,
* the condition is not so trivial so as to obviously be always true, and
* the condition is in some sense internal to a body of software.

Các xác định hầu như không bao giờ được sử dụng để phát hiện các tình huống phát sinh trong quá trình hoạt động bình thường của phần mềm. Ví dụ, thông thường không nên sử dụng các xác nhận để kiểm tra lỗi trong đầu vào của người dùng. Tuy nhiên, có thể hợp lý khi sử dụng các xác nhận để xác minh rằng người gọi đã kiểm tra thông tin đầu vào của người dùng.

Về cơ bản, sử dụng các ngoại lệ cho những thứ cần phải nắm bắt / xử lý trong ứng dụng sản xuất, sử dụng các xác nhận để thực hiện kiểm tra logic sẽ hữu ích cho phát triển nhưng bị tắt trong sản xuất.


Tôi nhận ra tất cả những điều đó. Nhưng vấn đề là, cùng một tuyên bố mà bạn đã đánh dấu là in đậm cũng có ngoại lệ. Vì vậy, theo cách tôi nhìn thấy nó, thay vì khẳng định, tôi có thể chỉ cần đưa ra một ngoại lệ (vì nếu "tình huống không bao giờ nên xảy ra" xảy ra trên phiên bản đã triển khai, tôi vẫn muốn được thông báo về nó [cộng, ứng dụng có thể đi vào trạng thái bị hỏng, vì vậy tôi có một ngoại lệ phù hợp, tôi có thể không muốn tiếp tục quy trình thực thi bình thường)

1
Các xác nhận nên được sử dụng trên các bất biến; ngoại lệ nên được sử dụng khi, chẳng hạn, một cái gì đó không được rỗng, nhưng nó sẽ là (giống như một tham số của một phương thức).
Ed S.

Tôi đoán tất cả đều phụ thuộc vào mức độ phòng thủ mà bạn muốn viết mã.
Ned Batchelder 23/09/09

Tôi đồng ý, đối với những gì bạn cần, ngoại lệ là con đường để đi. Bạn cho biết bạn muốn: Các lỗi được phát hiện trong quá trình sản xuất, khả năng ghi lại thông tin về các lỗi và kiểm soát luồng thực thi, v.v. Ba điều đó khiến tôi nghĩ rằng điều bạn cần làm là bỏ qua một số ngoại lệ.
Tom Neyland 23/09/09

7

Tôi nghĩ một ví dụ thực tế (có sẵn) có thể giúp làm sáng tỏ sự khác biệt:

(phỏng theo phần mở rộng Batch của MoreLinq )

// 'public facing' method
public int DoSomething(List<string> stuff, object doohickey, int limit) {

    // validate user input and report problems externally with exceptions

    if(stuff == null) throw new ArgumentNullException("stuff");
    if(doohickey == null) throw new ArgumentNullException("doohickey");
    if(limit <= 0) throw new ArgumentOutOfRangeException("limit", limit, "Should be > 0");

    return DoSomethingImpl(stuff, doohickey, limit);
}

// 'developer only' method
private static int DoSomethingImpl(List<string> stuff, object doohickey, int limit) {

    // validate input that should only come from other programming methods
    // which we have control over (e.g. we already validated user input in
    // the calling method above), so anything using this method shouldn't
    // need to report problems externally, and compilation mode can remove
    // this "unnecessary" check from production

    Debug.Assert(stuff != null);
    Debug.Assert(doohickey != null);
    Debug.Assert(limit > 0);

    /* now do the actual work... */
}

Vì vậy, như Eric Lippert và cộng sự đã nói, bạn chỉ xác nhận nội dung mà bạn mong đợi là đúng, đề phòng trường hợp bạn (nhà phát triển) vô tình sử dụng sai ở nơi khác, để bạn có thể sửa mã của mình. Về cơ bản, bạn ném ra các ngoại lệ khi bạn không kiểm soát được hoặc không thể lường trước được điều gì xảy ra, ví dụ như thông tin người dùng nhập , để bất cứ thứ gì cung cấp cho dữ liệu xấu đều có thể phản hồi thích hợp (ví dụ: người dùng).


Không phải 3 Asserts của bạn hoàn toàn thừa sao? Không thể cho các thông số của họ đánh giá là sai.
David Klempfner

1
Đó là vấn đề - các xác nhận ở đó để ghi lại những thứ không thể. Tại sao bạn lại làm vậy? Bởi vì bạn có thể có một cái gì đó giống như ReSharper cảnh báo bạn bên trong phương thức DoSomethingImpl rằng "bạn có thể đang tham khảo null ở đây" và bạn muốn nói với nó "Tôi biết những gì tôi đang làm, điều này không bao giờ có thể là null". Đó cũng là một dấu hiệu cho một số lập trình viên sau này, những người có thể không nhận ra ngay mối liên hệ giữa DoSomething và DoSomethingImpl, đặc biệt nếu chúng cách nhau hàng trăm dòng.
Marcel Popescu

4

Một nugget khác từ Code Complete :

"Xác nhận là một hàm hoặc macro sẽ phàn nàn rất nhiều nếu một giả định không đúng. Sử dụng xác nhận để ghi lại các giả định được thực hiện trong mã và để loại bỏ các điều kiện không mong muốn. ...

"Trong quá trình phát triển, các khẳng định làm nảy sinh các giả định mâu thuẫn, các điều kiện không mong muốn, các giá trị xấu được chuyển sang các quy trình, v.v."

Ông tiếp tục bổ sung một số hướng dẫn về những gì nên và không nên khẳng định.

Mặt khác, các ngoại lệ:

"Sử dụng xử lý ngoại lệ để thu hút sự chú ý đến các trường hợp không mong muốn. Các trường hợp ngoại lệ phải được xử lý theo cách khiến chúng rõ ràng trong quá trình phát triển và có thể phục hồi khi mã sản xuất đang chạy."

Nếu bạn không có cuốn sách này, bạn nên mua nó.


2
Tôi đã đọc cuốn sách, nó rất xuất sắc. Tuy nhiên .. bạn đã không trả lời câu hỏi của tôi :)

Bạn nói đúng tôi đã không trả lời nó. Câu trả lời của tôi là không Tôi không đồng ý với bạn. Assertions và Exceptions là các động vật khác nhau như đã đề cập ở trên và một số câu trả lời khác được đăng ở đây.
Andrew Cowenhoven 23/09/09

0

Debug.Assert theo mặc định sẽ chỉ hoạt động trong các bản dựng gỡ lỗi, vì vậy nếu bạn muốn phát hiện bất kỳ loại hành vi không mong muốn nào xấu trong các bản dựng phát hành của mình, bạn sẽ cần sử dụng các ngoại lệ hoặc bật hằng số gỡ lỗi trong các thuộc tính dự án của mình (được xem xét trong nói chung không phải là một ý kiến ​​hay).


một phần câu đầu tiên là đúng, phần còn lại nói chung là một ý tưởng tồi: các xác nhận là giả định và không có xác nhận (như đã nêu ở trên), cho phép gỡ lỗi trong bản phát hành thực sự không có lựa chọn nào.
Marc Wittke 23/09/09

0

Sử dụng khẳng định cho những điều CÓ THỂ nhưng không nên xảy ra (nếu không thể xảy ra, tại sao bạn lại đặt một khẳng định?).

Đó không phải là một trường hợp để sử dụng một Exception? Tại sao bạn lại sử dụng một khẳng định thay vì một Exception?

Bởi vì sẽ có mã được gọi trước xác nhận của bạn, điều này sẽ ngăn tham số của xác nhận là sai.

Thông thường không có mã nào trước mã của bạn Exceptionđảm bảo rằng nó sẽ không bị ném.

Tại sao nó tốt lại Debug.Assert()được tổng hợp lại trong sản phẩm? Nếu bạn muốn biết về nó trong gỡ lỗi, bạn sẽ không muốn biết về nó trong sản phẩm?

Bạn chỉ muốn nó trong quá trình phát triển, bởi vì một khi bạn tìm thấy các Debug.Assert(false)tình huống, bạn sẽ viết mã để đảm bảo điều Debug.Assert(false)đó không xảy ra nữa. Khi quá trình phát triển được thực hiện, giả sử bạn đã tìm thấy các Debug.Assert(false)tình huống và khắc phục chúng, Debug.Assert()có thể được biên dịch một cách an toàn vì chúng hiện đang thừa.


0

Giả sử bạn là thành viên của một nhóm khá lớn và có một số người làm việc trên cùng một cơ sở mã chung, bao gồm cả chồng chéo trên các lớp. Bạn có thể tạo một phương thức được gọi bằng một số phương thức khác và để tránh tranh chấp về khóa, bạn không thêm một khóa riêng vào nó, mà "giả sử" trước đó nó đã bị khóa bởi phương thức gọi bằng một khóa cụ thể. Chẳng hạn như, Debug.Assert (RepositoryLock.IsReadLockHeld || RepositoryLock.IsWriteLockHeld); Các nhà phát triển khác có thể bỏ qua một nhận xét nói rằng phương thức gọi phải sử dụng khóa, nhưng họ không thể bỏ qua điều này.

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.