Có ổn không khi sử dụng các trường hợp ngoại lệ làm công cụ để bắt lỗi lỗi sớm?


29

Tôi sử dụng ngoại lệ để bắt vấn đề sớm. Ví dụ:

public int getAverageAge(Person p1, Person p2){
    if(p1 == null || p2 == null)
        throw new IllegalArgumentException("One or more of input persons is null").
    return (p1.getAge() + p2.getAge()) / 2;
}

Chương trình của tôi không bao giờ nên vượt qua nulltrong chức năng này. Tôi không bao giờ có ý định đó. Tuy nhiên như chúng ta đều biết, những thứ vô tình xảy ra trong lập trình.

Ném một ngoại lệ nếu sự cố này xảy ra, cho phép tôi phát hiện và khắc phục nó, trước khi nó gây ra nhiều vấn đề hơn ở những nơi khác trong chương trình. Ngoại lệ dừng chương trình và nói với tôi "những thứ xấu đã xảy ra ở đây, hãy sửa nó". Thay vì điều này nulldi chuyển xung quanh chương trình gây ra vấn đề ở nơi khác.

Bây giờ, bạn đã đúng, trong trường hợp nullnày đơn giản sẽ gây ra một NullPointerExceptionquyền ngay lập tức, vì vậy nó có thể không phải là ví dụ tốt nhất.

Nhưng hãy xem xét một phương pháp như phương pháp này chẳng hạn:

public void registerPerson(Person person){
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

Trong trường hợp này, một nulltham số sẽ được truyền xung quanh chương trình và có thể gây ra lỗi nhiều sau đó, điều này sẽ khó theo dõi lại nguồn gốc của chúng.

Thay đổi chức năng như vậy:

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

Cho phép tôi phát hiện ra vấn đề nhiều trước khi nó gây ra lỗi lạ ở những nơi khác.

Ngoài ra, một nulltham chiếu như một tham số chỉ là một ví dụ. Nó có thể là nhiều loại vấn đề, từ các đối số không hợp lệ đến bất cứ điều gì khác. Luôn luôn tốt hơn để phát hiện ra chúng sớm.


Vì vậy, câu hỏi của tôi chỉ đơn giản là: đây có phải là thực hành tốt? Việc sử dụng ngoại lệ của tôi là công cụ ngăn chặn sự cố có tốt không? Đây có phải là một ứng dụng hợp pháp của các trường hợp ngoại lệ hay nó có vấn đề?


2
Downvoter có thể giải thích những gì sai với câu hỏi này?
Giorgio

2
"Sụp đổ sớm, sụp đổ thường xuyên"
Bryan Chen

16
Đó là nghĩa đen của những gì ngoại lệ là có.
raptortech97

Lưu ý rằng hợp đồng mã hóa cho phép bạn phát hiện nhiều lỗi như vậy một cách tĩnh. Điều này thường vượt trội so với việc ném một ngoại lệ trong thời gian chạy. Sự hỗ trợ và hiệu quả của các hợp đồng mã hóa thay đổi theo ngôn ngữ.
Brian

3
Vì bạn đang ném IllegalArgumentException, nên nói "Người đầu vào " là không cần thiết .
kevin cline

Câu trả lời:


29

Đúng, "thất bại sớm" là một nguyên tắc rất tốt và đây đơn giản là một cách có thể để thực hiện nó. Và trong các phương pháp phải trả về một giá trị cụ thể, thực sự bạn không thể làm gì khác để thất bại một cách có chủ ý - đó là ném ngoại lệ hoặc kích hoạt các xác nhận. Các ngoại lệ được cho là báo hiệu các điều kiện 'ngoại lệ' và việc phát hiện lỗi lập trình chắc chắn là ngoại lệ.


Thất bại sớm là cách để đi nếu không có cách nào để phục hồi từ một lỗi hoặc điều kiện. Ví dụ: nếu bạn cần sao chép một tệp và đường dẫn nguồn hoặc đích trống thì bạn nên ném ngoại lệ ngay lập tức.
Michael Storesin

Nó còn được gọi là "thất bại nhanh"
keuleJ

8

Vâng, ném ngoại lệ là một ý tưởng tốt. Ném chúng sớm, ném chúng thường xuyên, ném chúng háo hức.

Tôi biết có một cuộc tranh luận "ngoại lệ so với các xác nhận", với một số loại hành vi đặc biệt (cụ thể là các hành vi được cho là phản ánh lỗi lập trình) được xử lý bằng các xác nhận có thể được "biên dịch" cho thời gian chạy thay vì gỡ lỗi / xây dựng thử nghiệm. Nhưng số lượng hiệu suất tiêu thụ trong một vài kiểm tra độ chính xác bổ sung là tối thiểu trên phần cứng hiện đại và bất kỳ chi phí bổ sung nào cũng vượt xa giá trị của việc có kết quả chính xác, chưa được xử lý. Tôi chưa bao giờ thực sự gặp một cơ sở mã ứng dụng mà tôi muốn (hầu hết) kiểm tra loại bỏ khi chạy.

Tôi muốn nói rằng tôi sẽ không muốn có nhiều kiểm tra và điều kiện bổ sung trong các vòng lặp chặt chẽ của mã chuyên sâu về số lượng ... nhưng đó thực sự là nơi tạo ra rất nhiều lỗi số, và nếu không được phát hiện ra, sẽ lan truyền ra bên ngoài ảnh hưởng tất cả kết quả. Vì vậy, ngay cả ở đó, kiểm tra là đáng làm. Trong thực tế, một số thuật toán số tốt nhất, hiệu quả nhất được dựa trên đánh giá lỗi.

Một vị trí cuối cùng rất ý thức về mã bổ sung là mã rất nhạy cảm với độ trễ, trong đó các điều kiện bổ sung có thể gây ra các quầy hàng đường ống. Vì vậy, ở giữa hệ điều hành, DBMS và các hạt nhân phần mềm trung gian khác và xử lý giao thức / giao thức cấp thấp. Nhưng một lần nữa, đó là một số lỗi có thể được quan sát và các hiệu ứng (bảo mật, chính xác và toàn vẹn dữ liệu) của chúng có thể gây hại nhiều nhất.

Một cải tiến tôi đã tìm thấy là không chỉ ném ngoại lệ cấp cơ sở. IllegalArgumentExceptionlà tốt, nhưng nó có thể đến từ bất cứ nơi nào. Nó không mất nhiều trong hầu hết các ngôn ngữ để thêm ngoại lệ tùy chỉnh. Đối với mô-đun xử lý cá nhân của bạn, nói:

public class PersonArgumentException extends IllegalArgumentException {
    public MyException(String message) {
        super(message);
    }
}

Sau đó khi ai đó nhìn thấy một PersonArgumentException, rõ ràng nó đến từ đâu. Có một hành động cân bằng về số lượng ngoại lệ tùy chỉnh mà bạn muốn thêm, bởi vì bạn không muốn nhân các thực thể không cần thiết (Occam's Razor). Thường thì chỉ cần một vài ngoại lệ tùy chỉnh là đủ để báo hiệu "mô-đun này không nhận được dữ liệu phù hợp!" hoặc "mô-đun này không thể làm những gì nó phải làm!" theo cách cụ thể và phù hợp, nhưng không quá chính xác đến mức bạn phải thực hiện lại toàn bộ hệ thống phân cấp ngoại lệ. Tôi thường đi đến tập hợp ngoại lệ tùy chỉnh nhỏ đó bằng cách bắt đầu bằng ngoại lệ chứng khoán, sau đó quét mã và nhận ra rằng "những nơi N này đang tăng ngoại lệ chứng khoán, nhưng họ hiểu được ý tưởng cấp cao hơn là họ không nhận được dữ liệu họ cần; hãy '


I've never actually met an application codebase for which I'd want (most) checks removed at runtime. Sau đó, bạn đã không thực hiện mã đó là hiệu suất quan trọng. Tôi đang làm việc trên một cái gì đó ngay bây giờ với tốc độ 37M mỗi giây với các xác nhận được biên dịch và 42 triệu mà không có chúng. Các xác nhận không có xác nhận bên ngoài đầu vào, chúng ở đó để đảm bảo mã là chính xác. Khách hàng của tôi rất vui khi được tăng 13% sau khi tôi hài lòng rằng công cụ của tôi không bị hỏng.
Blrfl

Nên tránh sử dụng một cơ sở mã với các ngoại lệ tùy chỉnh vì trong khi nó có thể giúp bạn, nó không giúp những người khác phải làm quen với chúng. Thông thường nếu bạn thấy mình mở rộng một ngoại lệ phổ biến và không thêm bất kỳ thành viên hoặc chức năng nào, bạn thực sự không cần nó. Ngay cả trong ví dụ của bạn, PersonArgumentExceptionkhông rõ ràng như IllegalArgumentException. Cái sau được biết đến là bị ném khi một cuộc tranh cãi bất hợp pháp được thông qua. Tôi thực sự mong đợi cái trước sẽ bị ném nếu a Personở trạng thái không hợp lệ cho một cuộc gọi (Tương tự như InvalidOperationExceptiontrong C #).
Selali Adobor

Đúng là nếu bạn không cẩn thận, bạn có thể kích hoạt ngoại lệ tương tự từ một nơi khác trong hàm, nhưng đó là một lỗ hổng của mã, không phải là bản chất của các ngoại lệ phổ biến. Và đó là nơi mà các dấu vết gỡ lỗi và ngăn xếp xuất hiện. Nếu bạn đang cố gắng "gắn nhãn" các ngoại lệ của mình, thì hãy sử dụng các phương tiện hiện có mà các ngoại lệ phổ biến nhất cung cấp, như sử dụng hàm tạo thư (chính xác là những gì nó có). Một số trường hợp ngoại lệ thậm chí cung cấp cho các nhà xây dựng các tham số bổ sung để có được tên của đối số có vấn đề, nhưng bạn có thể cung cấp cùng một cơ sở với một thông điệp được hình thành tốt.
Selali Adobor

Tôi thừa nhận rằng chỉ đơn thuần đặt lại thương hiệu cho một ngoại lệ phổ biến đối với nhãn riêng về cơ bản ngoại lệ đó là một ví dụ yếu và hiếm khi đáng để nỗ lực. Trong thực tế, tôi cố gắng phát ra các ngoại lệ tùy chỉnh ở cấp độ ngữ nghĩa cao hơn, gắn chặt hơn với mục đích của mô-đun.
Jonathan Eunice

Tôi đã thực hiện công việc nhạy cảm với hiệu suất trong các lĩnh vực số / HPC và hệ điều hành / phần mềm trung gian. Một vết sưng hiệu suất 13% là điều không nhỏ. Tôi có thể tắt một số kiểm tra để có được nó, giống như cách một chỉ huy quân sự có thể yêu cầu 105% sản lượng lò phản ứng danh nghĩa. Nhưng tôi đã thấy "nó sẽ chạy nhanh hơn theo cách này" thường được đưa ra như một lý do để tắt kiểm tra, sao lưu và bảo vệ khác. Về cơ bản, nó đánh đổi khả năng phục hồi và an toàn (trong nhiều trường hợp, bao gồm cả tính toàn vẹn dữ liệu) để có hiệu suất cao hơn. Cho dù đó là giá trị là một cuộc gọi phán xét.
Jonathan Eunice

3

Thất bại càng sớm càng tốt khi gỡ lỗi một ứng dụng. Tôi nhớ một lỗi phân đoạn cụ thể trong chương trình C ++ cũ: nơi phát hiện ra lỗi không liên quan đến nơi được giới thiệu (con trỏ null đã vui vẻ di chuyển từ nơi này sang nơi khác trong bộ nhớ trước khi cuối cùng nó gây ra sự cố ). Dấu vết ngăn xếp có thể giúp bạn trong những trường hợp.

Vì vậy, có, lập trình phòng thủ là một cách tiếp cận thực sự hiệu quả để phát hiện và sửa lỗi nhanh chóng. Mặt khác, nó có thể là quá mức, đặc biệt là với các tài liệu tham khảo null.

Trong trường hợp cụ thể của bạn, ví dụ: nếu bất kỳ tham chiếu nào là null, thì NullReferenceExceptionsẽ được ném vào câu lệnh tiếp theo, khi cố gắng lấy tuổi của một người. Bạn không thực sự cần phải tự kiểm tra mọi thứ ở đây: hãy để hệ thống cơ bản nắm bắt những lỗi đó và đưa ra ngoại lệ, đó là lý do tại sao chúng tồn tại .

Để có ví dụ thực tế hơn, bạn có thể sử dụng các assertcâu lệnh:

  1. Ngắn hơn để viết và đọc:

        assert p1 : "p1 is null";
        assert p2 : "p2 is null";
    
  2. Được thiết kế đặc biệt cho phương pháp của bạn. Trong một thế giới nơi bạn có cả khẳng định và ngoại lệ, bạn có thể phân biệt chúng như sau:

    • Các xác nhận dành cho lỗi lập trình ("/ * điều này sẽ không bao giờ xảy ra * /"),
    • Trường hợp ngoại lệ cho các trường hợp góc (tình huống đặc biệt nhưng có thể xảy ra)

Do đó, việc đưa ra các giả định của bạn về các đầu vào và / hoặc trạng thái của ứng dụng của bạn với các xác nhận để cho nhà phát triển tiếp theo hiểu thêm một chút mục đích của mã của bạn.

Một bộ phân tích tĩnh (ví dụ trình biên dịch) cũng có thể hạnh phúc hơn.

Cuối cùng, các xác nhận có thể được xóa khỏi ứng dụng được triển khai bằng một công tắc duy nhất. Nhưng nói chung, đừng mong đợi cải thiện hiệu quả với điều đó: kiểm tra xác nhận trong thời gian chạy là không đáng kể.


1

Theo tôi biết các lập trình viên khác nhau thích giải pháp này hay giải pháp khác.

Giải pháp đầu tiên thường được ưa thích vì nó ngắn gọn hơn, đặc biệt, bạn không phải kiểm tra cùng một điều kiện trong các chức năng khác nhau.

Tôi tìm giải pháp thứ hai, vd

public void registerPerson(Person person){
    if(person == null) throw new IllegalArgumentException("Input person is null.");
    persons.add(person);
    notifyRegisterObservers(person); // sends the person object to all kinds of objects.
}

vững chắc hơn, bởi vì

  1. Nó bắt lỗi càng sớm càng tốt, tức là khi registerPerson()được gọi, không phải khi một ngoại lệ con trỏ null được ném ở đâu đó xuống ngăn xếp cuộc gọi. Việc gỡ lỗi trở nên dễ dàng hơn nhiều: tất cả chúng ta đều biết giá trị không hợp lệ có thể đi qua mã bao xa trước khi nó xuất hiện như một lỗi.
  2. Nó làm giảm sự ghép nối giữa các hàm: registerPerson()không đưa ra bất kỳ giả định nào về việc các hàm khác sẽ kết thúc bằng cách sử dụng personđối số và cách chúng sẽ sử dụng nó: quyết định đó nulllà một lỗi được đưa ra và được thực thi cục bộ.

Vì vậy, đặc biệt nếu mã khá phức tạp, tôi có xu hướng thích cách tiếp cận thứ hai này.


1
Đưa ra lý do ("Người đầu vào là null.") Cũng giúp hiểu vấn đề, không giống như khi một số phương pháp tối nghĩa trong thư viện thất bại.
Florian F

1

Nói chung, vâng, đó là một ý tưởng tốt để "thất bại sớm". Tuy nhiên, trong ví dụ cụ thể của bạn, tường minh IllegalArgumentExceptionkhông cung cấp một cải tiến đáng kể so với NullReferenceException- bởi vì cả hai đối tượng được vận hành đều được phân phối dưới dạng đối số cho hàm.

Nhưng hãy nhìn vào một ví dụ hơi khác.

class PersonCalculator {
    PersonCalculator(Person p) {
        if (p == null) throw new ArgumentNullException("p");
        _p = p;
    }

    void Calculate() {
        // Do something to p
    }
}

Nếu không có đối số kiểm tra trong hàm tạo, bạn sẽ nhận được NullReferenceExceptionkhi gọi Calculate.

Nhưng đoạn mã bị hỏng không phải là Calculatechức năng, cũng không phải là người tiêu dùng của Calculatechức năng. Đoạn mã bị hỏng là mã cố gắng xây dựng PersonCalculatorbằng null Person- vì vậy đó là nơi chúng ta muốn ngoại lệ xảy ra.

Nếu chúng tôi xóa kiểm tra đối số rõ ràng đó, bạn sẽ phải tìm ra lý do tại sao NullReferenceExceptionxảy ra khi Calculateđược gọi. Và theo dõi lý do tại sao đối tượng được xây dựng với một nullngười có thể gặp khó khăn, đặc biệt là nếu mã xây dựng máy tính không gần với mã thực sự gọi Calculatehàm.


0

Không phải trong các ví dụ bạn đưa ra.

Như bạn nói, ném một cách rõ ràng sẽ không mang lại cho bạn nhiều khi bạn sẽ có một ngoại lệ ngay sau đó. Nhiều người sẽ lập luận rằng có ngoại lệ rõ ràng với một thông điệp tốt là tốt hơn, mặc dù tôi không đồng ý. Trong các kịch bản được phát hành trước, theo dõi ngăn xếp là đủ tốt. Trong các tình huống hậu phát hành, trang web cuộc gọi thường có thể cung cấp các tin nhắn tốt hơn bên trong chức năng.

Hình thức thứ hai là cung cấp quá nhiều thông tin cho chức năng. Hàm đó không nhất thiết phải biết rằng các hàm khác sẽ đưa vào đầu vào null. Ngay cả khi bây giờ họ ném vào đầu vào null , việc tái cấu trúc sẽ trở nên rất rắc rối nếu điều đó dừng lại vì trường hợp kiểm tra null được lan truyền khắp mã.

Nhưng nói chung, bạn nên ném sớm một khi bạn xác định rằng có điều gì đó không ổn (trong khi tôn trọng DRY). Đây mặc dù có thể không phải là ví dụ tuyệt vời về điều đó.


-3

Trong chức năng ví dụ của bạn, tôi muốn bạn không kiểm tra và chỉ cho phép điều đó NullReferenceExceptionxảy ra.

Đối với một người, dù sao đi nữa cũng không có ý nghĩa gì, vì vậy tôi sẽ tìm ra vấn đề ngay lập tức dựa trên việc ném a NullReferenceException.

Thứ hai, là nếu mọi chức năng đưa ra các ngoại lệ hơi khác nhau dựa trên loại đầu vào rõ ràng đã được cung cấp, thì chẳng mấy chốc bạn đã có các chức năng có thể đưa ra 18 loại ngoại lệ khác nhau, và chẳng mấy chốc bạn thấy mình cũng nói vậy nhiều việc phải giải quyết, và dù sao cũng chỉ cần triệt tiêu mọi ngoại lệ.

Bạn không thể làm gì để giải quyết tình huống lỗi thời gian thiết kế trong chức năng của mình, vì vậy hãy để lỗi xảy ra mà không thay đổi.


Tôi đã làm việc trong một vài năm trên cơ sở mã dựa trên nguyên tắc này: các con trỏ chỉ đơn giản là được tham chiếu khi cần và hầu như không bao giờ được kiểm tra đối với NULL và được xử lý rõ ràng. Việc gỡ lỗi mã này luôn rất tốn thời gian và rắc rối vì các trường hợp ngoại lệ (hoặc kết xuất cốt lõi) xảy ra sau đó rất nhiều, tại thời điểm xảy ra lỗi logic. Tôi đã lãng phí hàng tuần để tìm kiếm một số lỗi nhất định. Vì những lý do này, tôi thích kiểu thất bại càng sớm càng tốt, ít nhất là đối với mã phức tạp, nơi nó không rõ ràng ngay lập tức xuất phát từ một ngoại lệ con trỏ null.
Giorgio

"Đối với một người, dù sao đi nữa cũng không có ý nghĩa gì, vì vậy tôi sẽ tìm ra vấn đề ngay lập tức dựa trên việc ném NullReferenceException.": Không nhất thiết, như ví dụ thứ hai của Prog minh họa.
Giorgio

"Hai, là nếu mọi chức năng đưa ra các ngoại lệ hơi khác nhau dựa trên loại đầu vào rõ ràng sai đã được cung cấp, thì chẳng mấy chốc bạn đã có các chức năng có thể đưa ra 18 loại ngoại lệ khác nhau ...": Trong trường hợp này bạn có thể sử dụng cùng một ngoại lệ (thậm chí NullPulumException) trong tất cả các chức năng: điều quan trọng là ném sớm, không ném ngoại lệ khác nhau từ mỗi chức năng.
Giorgio

Đó là những gì RuntimeExceptions dành cho. Bất kỳ điều kiện nào "không nên xảy ra" nhưng ngăn mã của bạn hoạt động nên ném một cái. Bạn không cần phải khai báo chúng trong chữ ký phương thức. Nhưng bạn cần phải bắt chúng tại một số điểm và báo cáo rằng chức năng yêu cầu hành động không thành công.
Florian F

1
Câu hỏi không chỉ định ngôn ngữ, vì vậy việc dựa vào ví dụ RuntimeExceptionkhông nhất thiết là một giả định hợp lệ.
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.