Có nên kiểm tra từng lỗi nhỏ trong C không?


60

Là một lập trình viên giỏi, người ta nên viết các mã mạnh mẽ sẽ xử lý mọi kết quả duy nhất của chương trình của mình. Tuy nhiên, gần như tất cả các chức năng từ thư viện C sẽ trả về 0 hoặc -1 hoặc NULL khi có lỗi.

Đôi khi rõ ràng là kiểm tra lỗi là cần thiết, ví dụ như khi bạn cố mở một tệp. Nhưng tôi thường bỏ qua việc kiểm tra lỗi trong các chức năng như printfhoặc thậm chí mallocvì tôi không cảm thấy cần thiết.

if(fprintf(stderr, "%s", errMsg) < 0){
    perror("An error occurred while displaying the previous error.");
    exit(1);
}

Có phải là một thực hành tốt để chỉ bỏ qua một số lỗi nhất định, hoặc có cách nào tốt hơn để xử lý tất cả các lỗi?


13
Phụ thuộc vào mức độ mạnh mẽ cần thiết cho dự án bạn đang làm việc. Các hệ thống có cơ hội nhận đầu vào từ các bên không tin cậy (ví dụ: máy chủ công khai) hoặc hoạt động trong môi trường không hoàn toàn tin cậy, cần được mã hóa rất thận trọng, để tránh mã trở thành quả bom hẹn giờ (hoặc liên kết yếu nhất bị hack ). Rõ ràng, các dự án sở thích và học tập không cần sự mạnh mẽ như vậy.
rwong

1
Một số ngôn ngữ cung cấp ngoại lệ. Nếu bạn không bắt ngoại lệ, chuỗi của bạn sẽ chấm dứt, điều này tốt hơn là để nó tiếp tục với trạng thái xấu. Nếu bạn chọn bắt ngoại lệ, bạn có thể bao gồm nhiều lỗi trong nhiều dòng mã bao gồm các hàm & phương thức được gọi bằng một trycâu lệnh, do đó bạn không phải kiểm tra từng cuộc gọi hoặc thao tác. (Cũng lưu ý rằng một số ngôn ngữ tốt hơn các ngôn ngữ khác trong việc phát hiện các lỗi đơn giản như vô hiệu hóa hoặc chỉ số mảng ngoài giới hạn.)
Erik Eidt

1
Vấn đề chủ yếu là vấn đề phương pháp: bạn không viết kiểm tra lỗi thông thường khi bạn vẫn đang tìm ra những gì bạn phải thực hiện và bạn không muốn thêm kiểm tra lỗi ngay bây giờ vì bạn muốn có được "con đường hạnh phúc" ngay trước Nhưng bạn vẫn phải kiểm tra malloc và co. Thay vì bỏ qua bước kiểm tra lỗi, bạn phải ưu tiên các hoạt động mã hóa của mình và để kiểm tra lỗi là bước tái cấu trúc vĩnh viễn trong danh sách TODO của bạn, được áp dụng bất cứ khi nào bạn hài lòng với mã của mình. Thêm ý kiến ​​"/ * KIỂM TRA * /" có thể greppable có thể giúp đỡ.
coredump

7
Tôi không thể tin rằng không ai nhắc đến errno! Trong trường hợp bạn không quen, trong khi sự thật là "gần như tất cả các chức năng từ thư viện C sẽ trả về 0 hoặc or1 hoặc NULLkhi có lỗi", họ cũng đặt errnobiến toàn cục mà bạn có thể truy cập bằng cách sử dụng #include <errno.h>và sau đó chỉ cần đọc giá trị của errno. Vì vậy, ví dụ, nếu open(2) trả về -1, bạn có thể muốn kiểm tra xem errno == EACCES, điều đó có cho biết lỗi quyền hay không ENOENT, điều đó cho thấy rằng tệp được yêu cầu không tồn tại.
wchargein

1
@ErikEidt C không hỗ trợ try/ catch, mặc dù bạn có thể mô phỏng nó bằng các bước nhảy.
Mast

Câu trả lời:


29

Nói chung, mã nên đối phó với các điều kiện đặc biệt bất cứ nơi nào phù hợp. Vâng, đây là một tuyên bố mơ hồ.

Trong các ngôn ngữ cấp cao hơn có xử lý ngoại lệ phần mềm, điều này thường được nêu là "bắt ngoại lệ trong phương thức mà bạn thực sự có thể làm gì đó về nó." Nếu xảy ra lỗi tệp, có thể bạn để nó nổi lên ngăn xếp thành mã UI thực sự có thể cho người dùng biết "tệp của bạn không lưu vào đĩa." Cơ chế ngoại lệ có hiệu quả nuốt chửng "từng lỗi nhỏ" và ngầm xử lý nó ở nơi thích hợp.

Trong C, bạn không có sự xa xỉ đó. Có một số cách để xử lý lỗi, một số trong đó là các tính năng ngôn ngữ / thư viện, một số trong đó là thực hành mã hóa.

Có phải là một thực hành tốt để chỉ bỏ qua một số lỗi nhất định, hoặc có cách nào tốt hơn để xử lý tất cả các lỗi?

Bỏ qua những lỗi nhất định? Có lẽ. Ví dụ, thật hợp lý khi cho rằng việc ghi vào đầu ra tiêu chuẩn sẽ không thất bại. Nếu nó không thành công, bạn sẽ nói với người dùng như thế nào? Có, đó là một ý tưởng tốt để bỏ qua một số lỗi nhất định, hoặc mã phòng thủ để ngăn chặn chúng. Ví dụ, kiểm tra số không trước khi chia.

Có nhiều cách để xử lý tất cả hoặc ít nhất là các lỗi:

  1. Bạn có thể sử dụng các bước nhảy, tương tự như gotos, để xử lý lỗi . Mặc dù là một vấn đề gây tranh cãi giữa các chuyên gia phần mềm, nhưng có những cách sử dụng hợp lệ cho chúng, đặc biệt là trong mã nhúng và mã quan trọng về hiệu năng (ví dụ: nhân Linux).
  1. Xếp tầng ifs:

    if (!<something>) {
      printf("oh no 1!");
      return;
    }
    if (!<something else>) {
      printf("oh no 2!");
      return;
    }
    
  2. Kiểm tra điều kiện đầu tiên, ví dụ mở hoặc tạo tệp, sau đó giả sử các thao tác tiếp theo thành công.

Mã mạnh mẽ là tốt, và người ta nên kiểm tra và xử lý lỗi. Phương pháp nào là tốt nhất cho mã của bạn phụ thuộc vào mã làm gì, mức độ nghiêm trọng của lỗi, v.v. và chỉ bạn mới có thể thực sự trả lời được. Tuy nhiên, các phương thức này được thử nghiệm chiến đấu và được sử dụng trong các dự án nguồn mở khác nhau, nơi bạn có thể xem để xem cách mã thực kiểm tra lỗi.


21
Xin lỗi, nit nhỏ để chọn ở đây - "ghi vào đầu ra tiêu chuẩn sẽ không thất bại. Nếu thất bại, bạn sẽ nói với người dùng như thế nào?" - bằng cách viết vào lỗi tiêu chuẩn? Không có gì đảm bảo rằng việc không viết cho người này ngụ ý rằng không thể viết cho người kia.
Damien_The_Unbeliever

4
@Damien_The_Unbeliever - đặc biệt là vì stdoutcó thể được chuyển hướng đến một tệp hoặc thậm chí không tồn tại tùy thuộc vào hệ thống. stdoutkhông phải là một luồng "ghi vào bàn điều khiển", nó thường là vậy, nhưng nó không phải như vậy.
Davor dralo

3
@JAB Bạn gửi tin nhắn đến stdout... hiển nhiên :-)
TripeHound

7
@JAB: Bạn có thể thoát bằng EX_IOERR hoặc hơn, nếu điều đó là phù hợp.
John Marshall

6
Dù bạn làm gì, đừng cứ chạy như thể không có gì xảy ra! Nếu lệnh dump_database > backups/db_dump.txtkhông ghi vào đầu ra tiêu chuẩn tại bất kỳ điểm nào, tôi sẽ không muốn nó tiếp tục và thoát thành công. (Không phải cơ sở dữ liệu được sao lưu theo cách đó, nhưng điểm vẫn còn)
Ben S

15

Tuy nhiên, gần như tất cả các chức năng từ thư viện C sẽ trả về 0 hoặc -1 hoặc NULL khi có lỗi.

Vâng, nhưng bạn biết chức năng nào bạn gọi, phải không?

Bạn thực sự có rất nhiều thông tin mà bạn có thể đưa vào một thông báo lỗi. Bạn biết hàm nào được gọi, tên của hàm đã gọi nó, tham số nào được truyền và giá trị trả về của hàm. Đó là nhiều thông tin cho một thông báo lỗi rất nhiều thông tin.

Bạn không phải làm điều này cho mọi cuộc gọi chức năng. Nhưng lần đầu tiên bạn thấy thông báo lỗi "Xảy ra lỗi trong khi hiển thị lỗi trước đó", khi thông tin bạn thực sự cần là thông tin hữu ích, sẽ là lần cuối cùng bạn thấy thông báo lỗi đó, bởi vì bạn sẽ ngay lập tức thay đổi thông báo lỗi cho một cái gì đó thông tin sẽ giúp bạn khắc phục vấn đề.


5
Câu trả lời này khiến tôi mỉm cười, bởi vì nó đúng, nhưng không trả lời câu hỏi.
RubberDuck

Làm thế nào nó không trả lời câu hỏi?
Robert Harvey

2
một lý do tôi nghĩ rằng C (và các ngôn ngữ khác) đã trộn lẫn boolean 1 và 0 là cùng một lý do rằng bất kỳ hàm tốt nào trả về mã lỗi đều trả về "0" không có lỗi. nhưng sau đó bạn phải nói if (!myfunc()) {/*proceed*/}. 0 không có lỗi và bất cứ điều gì khác không là một số lỗi và mỗi lỗi loại đều có mã khác không. theo cách mà một người bạn của tôi nói là "chỉ có một Sự thật, nhưng có nhiều sự giả dối". "0" phải là "true" trong if()thử nghiệm và mọi thứ khác không phải là "false".
robert bristow-johnson

1
không, làm theo cách của bạn, chỉ có một mã lỗi tiêu cực có thể xảy ra. và nhiều mã no_error có thể.
robert bristow-johnson

1
@ robertbristow-johnson: Sau đó đọc nó là "Không có lỗi." if(function()) { // do something }đọc là "nếu hàm thực thi không có lỗi, thì hãy làm gì đó." hoặc, thậm chí tốt hơn, "nếu chức năng thực thi thành công, hãy làm một cái gì đó." Nghiêm túc mà nói, những người hiểu quy ước không bị nhầm lẫn bởi điều này. Trả về false(hoặc 0) khi xảy ra lỗi sẽ không cho phép sử dụng mã lỗi. Không có nghĩa là sai trong C vì đó là cách toán học hoạt động. Xem lập trình
Robert Harvey

11

TLDR; bạn gần như không bao giờ bỏ qua lỗi.

Ngôn ngữ C thiếu tính năng xử lý lỗi tốt để mỗi nhà phát triển thư viện thực hiện các giải pháp riêng của mình. Các ngôn ngữ hiện đại hơn có các ngoại lệ được xây dựng trong đó làm cho vấn đề đặc biệt này dễ xử lý hơn rất nhiều.

Nhưng khi bạn bị mắc kẹt với C, bạn không có đặc quyền như vậy. Thật không may, bạn chỉ cần trả giá mỗi khi bạn gọi một chức năng có khả năng thất bại từ xa. Hoặc nếu không, bạn sẽ phải chịu hậu quả tồi tệ hơn nhiều như ghi đè dữ liệu vào bộ nhớ ngoài ý muốn. Vì vậy, như một quy tắc chung, bạn phải kiểm tra lỗi luôn.

Nếu bạn không kiểm tra sự trở lại của fprintfmình thì rất có thể bạn sẽ để lại một lỗi phía sau, trong trường hợp tốt nhất sẽ không làm những gì người dùng mong đợi và trường hợp xấu hơn sẽ làm nổ tung toàn bộ trong suốt chuyến bay. Không có lý do gì để làm suy yếu chính mình theo cách đó.

Tuy nhiên, là một nhà phát triển C, công việc của bạn là làm cho mã dễ bảo trì. Vì vậy, đôi khi bạn có thể bỏ qua các lỗi một cách an toàn vì mục đích rõ ràng nếu (và chỉ khi) chúng không gây ra bất kỳ mối đe dọa nào đối với hành vi chung của ứng dụng. .

Đó là vấn đề tương tự như làm điều này:

try
{
    run();
} catch (Exception) {
    // comments expected here!!!
}

Nếu bạn thấy rằng không có bình luận tốt trong khối bắt trống thì đây chắc chắn là một vấn đề. Không có lý do gì để nghĩ rằng một cuộc gọi malloc()sẽ thực hiện thành công 100% thời gian.


Tôi đồng ý khả năng duy trì là một khía cạnh thực sự quan trọng và đoạn đó thực sự đã trả lời câu hỏi của tôi. Cảm ơn các câu trả lời chi tiết.
Derek 朕 會

Tôi nghĩ rằng các chương trình không bao giờ bận tâm để kiểm tra các cuộc gọi malloc của họ và chưa bao giờ gặp sự cố là lý do chính đáng để lười biếng khi một chương trình máy tính để bàn chỉ sử dụng một vài MB bộ nhớ.
whatsisname

5
@whatsisname: Chỉ vì họ không gặp sự cố không có nghĩa là họ không âm thầm làm hỏng dữ liệu. malloc()là một chức năng bạn chắc chắn muốn kiểm tra các giá trị trả về!
TMN

1
@TMN: Nếu malloc thất bại, chương trình sẽ ngay lập tức phân tách và chấm dứt, nó sẽ không tiếp tục làm việc. Đó là tất cả về rủi ro và những gì phù hợp, không phải "gần như không bao giờ".
whatsisname

2
@Alex C không thiếu xử lý lỗi tốt. Nếu bạn muốn ngoại lệ, bạn có thể sử dụng chúng. Thư viện chuẩn không sử dụng ngoại lệ là một lựa chọn có chủ ý (ngoại lệ buộc bạn phải quản lý bộ nhớ tự động và thường bạn không muốn điều đó đối với mã cấp thấp).
martinkunev

9

Câu hỏi không thực sự là ngôn ngữ cụ thể, mà là người dùng cụ thể. Hãy suy nghĩ về câu hỏi từ góc độ người dùng. Người dùng làm một cái gì đó, như gõ tên chương trình trên một dòng lệnh và nhấn enter. Người dùng mong đợi điều gì? Làm thế nào họ có thể nói nếu một cái gì đó đã đi sai? Họ có đủ khả năng để can thiệp nếu xảy ra lỗi không?

Trong nhiều loại mã, kiểm tra như vậy là quá mức cần thiết. Tuy nhiên, trong mã quan trọng an toàn có độ tin cậy cao, chẳng hạn như mã cho các lò phản ứng hạt nhân, kiểm tra lỗi bệnh lý và các lộ trình phục hồi theo kế hoạch là một phần của tính chất công việc hàng ngày. Nó được coi là đáng giá để dành thời gian để hỏi "Điều gì xảy ra nếu X thất bại? Làm thế nào để tôi trở lại trạng thái an toàn?" Trong mã ít tin cậy hơn, chẳng hạn như các mã cho trò chơi video, bạn có thể thoát khỏi việc kiểm tra lỗi ít hơn nhiều.

Một cách tiếp cận tương tự khác là bạn thực sự có thể cải thiện bao nhiêu về trạng thái bằng cách bắt lỗi? Tôi không thể đếm số lượng chương trình C ++ tự hào có ngoại lệ, chỉ để suy nghĩ lại vì họ thực sự không biết phải làm gì với chúng ... nhưng họ biết rằng họ phải xử lý ngoại lệ. Các chương trình như vậy không thu được gì từ những nỗ lực thêm. Chỉ thêm mã kiểm tra lỗi mà bạn nghĩ có thể thực sự xử lý tình huống tốt hơn là không kiểm tra mã lỗi. Điều đó đang được nói, C ++ có các quy tắc cụ thể để xử lý các trường hợp ngoại lệ xảy ra trong quá trình xử lý ngoại lệ để bắt chúng theo cách có ý nghĩa hơn (và bằng cách đó, tôi có nghĩa là gọi chấm dứt () để đảm bảo rằng đám tang bạn đã xây dựng cho mình sáng lên trong vinh quang thích hợp của nó)

Làm thế nào bạn có thể nhận được bệnh lý? Tôi đã làm việc trên một chương trình xác định "SafetyBool" có các giá trị đúng và sai trong đó được chọn cẩn thận để có phân phối chẵn 1 và 0, và chúng được chọn sao cho bất kỳ một trong số các lỗi phần cứng (lật một bit, bus dữ liệu dấu vết bị phá vỡ, v.v.) không làm cho boolean bị hiểu sai. Không cần phải nói, tôi sẽ không tuyên bố đây là một thực hành lập trình mục đích chung sẽ được sử dụng trong bất kỳ chương trình cũ nào.


6
  • Yêu cầu an toàn khác nhau đòi hỏi mức độ chính xác khác nhau. Trong phần mềm điều khiển hàng không hoặc ô tô, tất cả các giá trị trả về sẽ được kiểm tra, xem MISRA.FUNC.UNUSEDRET . Trong một bằng chứng nhanh chóng về khái niệm không bao giờ rời khỏi máy của bạn, có lẽ là không.
  • Mã hóa chi phí thời gian. Quá nhiều kiểm tra không liên quan trong phần mềm không quan trọng là nỗ lực chi tiêu tốt hơn ở nơi khác. Nhưng đâu là điểm ngọt ngào giữa chất lượng và giá thành? Điều đó phụ thuộc vào các công cụ gỡ lỗi và độ phức tạp của phần mềm.
  • Xử lý lỗi có thể làm mờ luồng điều khiển và đưa ra các lỗi mới. Tôi khá thích chức năng trình bao bọc của Richard "mạng" của Stevens mà ít nhất là báo cáo lỗi.
  • Xử lý lỗi có thể, hiếm khi, là một vấn đề hiệu suất. Nhưng hầu hết các cuộc gọi thư viện C sẽ mất nhiều thời gian đến mức chi phí kiểm tra giá trị trả lại là vô cùng nhỏ.

3

Một chút trừu tượng về câu hỏi. Và nó không nhất thiết phải là ngôn ngữ C.

Đối với các chương trình lớn hơn, bạn sẽ có một lớp trừu tượng; có lẽ là một phần của động cơ, thư viện hoặc trong khuôn khổ. Lớp đó sẽ không quan tâm đến thời tiết bạn có được một dữ liệu hợp lệ hoặc đầu ra sẽ có một số giá trị mặc định: 0, -1, null, vv

Sau đó, có một lớp sẽ là giao diện của bạn với lớp trừu tượng, sẽ xử lý nhiều lỗi và có lẽ những thứ khác như tiêm phụ thuộc, lắng nghe sự kiện, v.v.

Và sau này bạn sẽ có lớp triển khai cụ thể nơi bạn thực sự đặt quy tắc và xử lý đầu ra.

Vì vậy, tôi cho rằng điều này đôi khi tốt hơn là loại trừ hoàn toàn việc xử lý lỗi từ một phần của mã bởi vì phần đó đơn giản là không thực hiện công việc đó. Và sau đó có một số bộ xử lý sẽ đánh giá đầu ra và chỉ ra lỗi.

Điều này chủ yếu được thực hiện để phân tách các trách nhiệm dẫn đến khả năng đọc mã và khả năng mở rộng tốt hơn.


0

Nói chung, trừ khi bạn có lý do chính đáng để không kiểm tra tình trạng lỗi đó, bạn nên. Lý do chính đáng duy nhất tôi có thể nghĩ đến khi không kiểm tra tình trạng lỗi là khi bạn không thể làm điều gì đó có ý nghĩa nếu thất bại. Đây là một thanh rất khó để đáp ứng, bởi vì luôn có một lựa chọn khả thi: thoát một cách duyên dáng.


Tôi thường không đồng ý với bạn vì lỗi là những tình huống mà bạn không tính đến. Thật khó để biết lỗi có thể biểu hiện như thế nào nếu bạn không biết trong điều kiện nào thì lỗi xuất hiện. Và do đó xử lý lỗi trước khi xử lý dữ liệu thực tế.
Ảo thuật sáng tạo

Giống như, tôi đã nói, nếu một cái gì đó trả lại hoặc ném lỗi, ngay cả khi bạn không biết cách sửa lỗi và tiếp tục, bạn luôn có thể thất bại một cách duyên dáng, ghi lại lỗi, nói với người dùng rằng nó không hoạt động như mong đợi, v.v. Vấn đề là lỗi không nên được chú ý, không được lưu ý, "âm thầm thất bại" hoặc làm hỏng ứng dụng. Đó là những gì "thất bại một cách duyên dáng" hoặc "thoát ra một cách duyên dáng" nghĩa là gì.
Thần kinh học thông minh
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.