Ngoại lệ như khẳng định hay là lỗi?


10

Tôi là một lập trình viên C chuyên nghiệp và một lập trình viên Obj-C (OS X) có sở thích. Gần đây tôi đã cố gắng mở rộng sang C ++, vì cú pháp rất phong phú của nó.

Cho đến nay, tiền mã hóa tôi không xử lý nhiều ngoại lệ. Mục tiêu-C có chúng, nhưng chính sách của Apple khá nghiêm ngặt:

Quan trọng Bạn nên bảo lưu việc sử dụng các ngoại lệ để lập trình hoặc các lỗi thời gian chạy không mong muốn như truy cập bộ sưu tập ngoài giới hạn, cố gắng thay đổi các đối tượng bất biến, gửi tin nhắn không hợp lệ và mất kết nối với máy chủ cửa sổ.

C ++ dường như thích sử dụng ngoại lệ thường xuyên hơn. Ví dụ, ví dụ wikipedia trên RAII đưa ra một ngoại lệ nếu không thể mở tệp. Objective-C sẽ return nilcó lỗi được gửi bởi một param. Đáng chú ý, có vẻ như std :: ofstream có thể được đặt theo một trong hai cách.

Ở đây, các lập trình viên tôi đã tìm thấy một số câu trả lời tuyên bố sử dụng ngoại lệ thay vì mã lỗi hoặc hoàn toàn không sử dụng ngoại lệ . Các cựu dường như phổ biến hơn.

Tôi chưa tìm thấy ai thực hiện một nghiên cứu khách quan cho C ++. Dường như với tôi rằng vì con trỏ rất hiếm, tôi phải sử dụng cờ lỗi nội bộ nếu tôi chọn tránh ngoại lệ. Nó sẽ là quá nhiều bận tâm để xử lý, hoặc nó có thể sẽ làm việc thậm chí còn tốt hơn so với ngoại lệ? Một so sánh của cả hai trường hợp sẽ là câu trả lời tốt nhất.

Chỉnh sửa: Mặc dù không hoàn toàn phù hợp, tôi có lẽ nên làm rõ những gì nil. Về mặt kỹ thuật, nó giống như NULL, nhưng vấn đề là, bạn có thể gửi tin nhắn đến nil. Vì vậy, bạn có thể làm một cái gì đó như

NSError *err = nil;
id obj = [NSFileHandle fileHandleForReadingFromURL:myurl error:&err];

[obj retain];

ngay cả khi cuộc gọi đầu tiên trở lại nil. Và như bạn chưa bao giờ làm *objtrong Obj-C, sẽ không có rủi ro về việc hủy bỏ con trỏ NULL.


Imho, sẽ tốt hơn nếu bạn hiển thị một đoạn mã Mục tiêu C về cách bạn làm việc với các lỗi ở đó. Dường như, mọi người đang nói về một cái gì đó khác với bạn đang hỏi.
Gangnus

Theo một cách nào đó, tôi đoán tôi đang tìm kiếm sự biện minh cho việc sử dụng các ngoại lệ. Vì tôi có một số ý tưởng về cách chúng được triển khai, tôi biết chúng có thể đắt đến mức nào. Nhưng tôi đoán nếu tôi có thể có được một cuộc tranh luận đủ tốt cho việc sử dụng của họ thì tôi sẽ đi theo hướng đó.
Per Johansson

Dường như, đối với C ++, bạn cần biện minh cho việc không sử dụng chúng. Theo phản ứng ở đây.
Gangnus

2
Có lẽ, nhưng cho đến nay vẫn chưa có ai giải thích lý do tại sao chúng tốt hơn (ngoại trừ so với mã lỗi). Tôi không thích khái niệm sử dụng ngoại lệ cho những thứ không phải là ngoại lệ, nhưng nó mang tính bản năng hơn là dựa trên thực tế.
Per Johansson

"Tôi không thích ... sử dụng ngoại lệ cho những thứ không phải là ngoại lệ" - đồng ý.
Gangnus

Câu trả lời:


1

C ++ dường như thích sử dụng ngoại lệ thường xuyên hơn.

Tôi sẽ đề xuất thực sự ít hơn Objective-C ở một số khía cạnh vì thư viện chuẩn C ++ thường không gây ra các lỗi lập trình như truy cập ngoài giới hạn của chuỗi truy cập ngẫu nhiên trong mẫu thiết kế trường hợp phổ biến nhất của nó ( operator[]ví dụ) cố gắng để vô hiệu hóa một trình vòng lặp không hợp lệ. Ngôn ngữ không ném vào việc truy cập một mảng ngoài giới hạn, hoặc hủy bỏ một con trỏ null hoặc bất cứ thứ gì thuộc loại này.

Lấy lỗi lập trình viên phần lớn ra khỏi phương trình xử lý ngoại lệ thực sự lấy đi một loại lỗi rất lớn mà các ngôn ngữ khác thường mắc phải throwing. C ++ có xu hướng assert(không được biên dịch trong các bản dựng phát hành / sản xuất, chỉ các bản dựng gỡ lỗi) hoặc chỉ bị trục trặc (thường gặp sự cố) trong các trường hợp như vậy, có lẽ một phần vì ngôn ngữ không muốn áp đặt chi phí cho các kiểm tra thời gian chạy như vậy như sẽ được yêu cầu để phát hiện các lỗi lập trình viên như vậy trừ khi lập trình viên đặc biệt muốn trả chi phí bằng cách viết mã thực hiện kiểm tra như vậy.

Sutter thậm chí còn khuyến khích tránh các ngoại lệ trong các trường hợp như vậy trong Tiêu chuẩn mã hóa C ++:

Nhược điểm chính của việc sử dụng một ngoại lệ để báo cáo lỗi lập trình là bạn không thực sự muốn ngăn xếp ngăn chặn xảy ra khi bạn muốn trình gỡ lỗi khởi chạy trên dòng chính xác nơi phát hiện vi phạm, với trạng thái của dòng còn nguyên vẹn. Tóm lại: Có những lỗi mà bạn biết có thể xảy ra (xem Mục 69 đến 75). Đối với mọi thứ khác không nên, và đó là lỗi của lập trình viên nếu có, đó là assert.

Quy tắc đó không nhất thiết phải được đặt trong đá. Trong một số trường hợp quan trọng hơn về nhiệm vụ, có thể tốt hơn là sử dụng các hàm bao và một tiêu chuẩn mã hóa để ghi lại thống nhất các lỗi lập trình xảy ra và throwkhi có lỗi lập trình viên như cố gắng bảo vệ một cái gì đó không hợp lệ hoặc truy cập nó ra khỏi giới hạn, bởi vì có thể quá tốn kém để không phục hồi trong những trường hợp đó nếu phần mềm có cơ hội. Nhưng nhìn chung, việc sử dụng ngôn ngữ phổ biến hơn có xu hướng ủng hộ không ném vào những sai lầm của lập trình viên.

Ngoại lệ bên ngoài

Trường hợp tôi thấy các ngoại lệ được khuyến khích thường xuyên nhất trong C ++ (theo ủy ban tiêu chuẩn, ví dụ) là dành cho "ngoại lệ bên ngoài", như trong một kết quả không mong muốn ở một số nguồn bên ngoài chương trình. Một ví dụ là không phân bổ bộ nhớ. Một cái khác là không mở được một tệp quan trọng cần thiết để phần mềm chạy. Một cái khác là không kết nối được với một máy chủ cần thiết. Một người dùng khác đang gây nhiễu nút hủy bỏ để hủy bỏ một hoạt động có đường dẫn thực thi trường hợp chung dự kiến ​​sẽ thành công mà không có sự gián đoạn bên ngoài này. Tất cả những điều này nằm ngoài sự kiểm soát của phần mềm ngay lập tức và các lập trình viên đã viết nó. Chúng là kết quả bất ngờ từ các nguồn bên ngoài ngăn cản hoạt động (mà thực sự nên được coi là một giao dịch không thể tách rời trong cuốn sách của tôi *) để có thể thành công.

Giao dịch

Tôi thường khuyến khích xem một trykhối là một "giao dịch" vì các giao dịch sẽ thành công toàn bộ hoặc thất bại nói chung. Nếu chúng tôi đang cố gắng làm một cái gì đó và nó thất bại nửa chừng, thì mọi tác dụng phụ / đột biến được thực hiện đối với trạng thái chương trình thường cần được khôi phục để đưa hệ thống trở lại trạng thái hợp lệ như thể giao dịch chưa bao giờ được thực hiện, giống như một RDBMS không xử lý được một nửa truy vấn sẽ không ảnh hưởng đến tính toàn vẹn của cơ sở dữ liệu. Nếu bạn thay đổi trạng thái chương trình trực tiếp trong giao dịch đã nói, thì bạn phải "không sửa đổi" khi gặp lỗi (và ở đây phạm vi bảo vệ có thể hữu ích với RAII).

Cách thay thế đơn giản hơn nhiều là không làm thay đổi trạng thái chương trình gốc; bạn có thể thay đổi một bản sao của nó và sau đó, nếu nó thành công, trao đổi bản sao với bản gốc (đảm bảo trao đổi không thể ném). Nếu thất bại, loại bỏ các bản sao. Điều này cũng áp dụng ngay cả khi bạn không sử dụng ngoại lệ để xử lý lỗi nói chung. Một tư duy "giao dịch" là chìa khóa để phục hồi thích hợp nếu đột biến trạng thái chương trình xảy ra trước khi gặp lỗi. Nó hoặc thành công như một toàn thể hoặc thất bại như toàn bộ. Nó không thành công một nửa trong việc tạo đột biến của nó.

Đây là một trong những chủ đề ít được thảo luận nhất khi tôi thấy các lập trình viên hỏi về cách xử lý lỗi hoặc xử lý ngoại lệ đúng cách, nhưng khó nhất trong tất cả các phần mềm muốn chuyển đổi trực tiếp trạng thái chương trình trong nhiều phần mềm hoạt động của nó. Sự tinh khiết và bất biến có thể giúp ở đây đạt được sự an toàn ngoại lệ cũng giống như chúng giúp cho sự an toàn của luồng, vì một tác dụng phụ đột biến / bên ngoài không xảy ra không cần phải được khôi phục.

Hiệu suất

Một yếu tố định hướng khác trong việc có nên sử dụng ngoại lệ hay không là hiệu suất và tôi không có nghĩa là theo cách ám ảnh, chèn ép, phản tác dụng. Rất nhiều trình biên dịch C ++ thực hiện cái gọi là "Xử lý ngoại lệ chi phí bằng không".

Nó cung cấp chi phí thời gian chạy bằng không cho việc thực thi không có lỗi, vượt qua cả xử lý lỗi trả về giá trị C. Như một sự đánh đổi, việc truyền bá một ngoại lệ có một chi phí lớn.

Theo những gì tôi đã đọc về nó, nó làm cho các đường dẫn thực thi trường hợp thông thường của bạn không yêu cầu chi phí (thậm chí cả chi phí thường đi kèm với việc xử lý và truyền mã lỗi kiểu C), đổi lại là rất nhiều chi phí đối với các đường dẫn đặc biệt ( có nghĩa throwinglà bây giờ đắt hơn bao giờ hết).

"Đắt tiền" hơi khó định lượng nhưng, đối với người mới bắt đầu, có lẽ bạn không muốn bị ném hàng triệu lần trong một vòng lặp chặt chẽ. Kiểu thiết kế này giả định rằng các ngoại lệ không xảy ra bên trái và bên phải mọi lúc.

Không lỗi

Và điểm hiệu suất đó đưa tôi đến những lỗi không, điều này đáng ngạc nhiên mờ nhạt nếu chúng ta nhìn vào tất cả các loại ngôn ngữ khác. Nhưng tôi sẽ nói, với thiết kế EH chi phí bằng không được đề cập ở trên, bạn gần như chắc chắn không muốn throwphản hồi lại một khóa không được tìm thấy trong một bộ. Bởi vì không chỉ có thể là lỗi (người tìm kiếm khóa có thể đã tạo ra bộ và dự kiến ​​sẽ tìm kiếm các khóa không tồn tại), nhưng nó sẽ rất tốn kém trong bối cảnh đó.

Ví dụ, một hàm giao nhau được thiết lập có thể muốn lặp qua hai bộ và tìm kiếm các khóa mà chúng có chung. Nếu không tìm thấy khóa threw, bạn sẽ lặp lại và có thể gặp phải ngoại lệ trong một nửa hoặc nhiều lần lặp lại:

Set<int> set_intersection(const Set<int>& a, const Set<int>& b)
{
     Set<int> intersection;
     for (int key: a)
     {
          try
          {
              b.find(key);
              intersection.insert(other_key);
          }
          catch (const KeyNotFoundException&)
          {
              // Do nothing.
          }
     }
     return intersection;
}

Ví dụ trên hoàn toàn vô lý và cường điệu, nhưng tôi đã thấy, trong mã sản xuất, một số người đến từ các ngôn ngữ khác sử dụng ngoại lệ trong C ++ có phần giống như thế này và tôi nghĩ rằng đây là một tuyên bố thực tế hợp lý rằng đây không phải là cách sử dụng ngoại lệ phù hợp trong C ++. Một gợi ý khác ở trên là bạn sẽ nhận thấy catchkhối này hoàn toàn không có gì để làm và chỉ được viết để bỏ qua bất kỳ trường hợp ngoại lệ nào, và đó thường là một gợi ý (mặc dù không phải là người bảo lãnh) rằng các ngoại lệ có thể không được sử dụng rất phù hợp trong C ++.

Đối với các loại trường hợp đó, một số loại giá trị trả về biểu thị thất bại (bất cứ điều gì từ việc trở lại falsetrình lặp không hợp lệ nullptrhoặc bất cứ điều gì có ý nghĩa trong ngữ cảnh) thường phù hợp hơn nhiều, và cũng thường thực tế và hiệu quả hơn vì loại không có lỗi trường hợp thường không gọi cho một số quá trình giải nén ngăn xếp để đến catchtrang web tương tự .

Câu hỏi

Tôi phải đi với cờ lỗi nội bộ nếu tôi chọn để tránh ngoại lệ. Nó sẽ là quá nhiều bận tâm để xử lý, hoặc nó có thể sẽ làm việc thậm chí còn tốt hơn so với ngoại lệ? Một so sánh của cả hai trường hợp sẽ là câu trả lời tốt nhất.

Tránh các ngoại lệ hoàn toàn trong C ++ có vẻ cực kỳ phản tác dụng đối với tôi, trừ khi bạn làm việc trong một số hệ thống nhúng hoặc một loại trường hợp cụ thể cấm sử dụng chúng (trong trường hợp đó bạn cũng phải tránh ra để tránh tất cả thư viện và chức năng ngôn ngữ mà nếu không throw, như sử dụng nghiêm ngặt nothrow new).

Nếu bạn hoàn toàn phải tránh các ngoại lệ vì bất kỳ lý do gì (ví dụ: hoạt động trên các ranh giới API C của mô-đun có API C mà bạn xuất), nhiều người có thể không đồng ý với tôi nhưng tôi thực sự khuyên bạn nên sử dụng trình xử lý lỗi toàn cầu như OpenGL glGetError(). Bạn có thể làm cho nó sử dụng lưu trữ luồng cục bộ để có trạng thái lỗi duy nhất cho mỗi luồng.

Lý do của tôi cho điều đó là tôi không quen nhìn thấy các đội trong môi trường sản xuất kiểm tra kỹ tất cả các lỗi có thể xảy ra, thật không may, khi mã lỗi được trả về. Nếu chúng kỹ lưỡng, một số API C có thể gặp lỗi chỉ với mỗi cuộc gọi API C duy nhất và kiểm tra kỹ lưỡng sẽ yêu cầu một cái gì đó như:

if ((err = ApiCall(...)) != success)
{
     // Handle error
}

... với hầu hết mọi dòng mã gọi API yêu cầu kiểm tra như vậy. Tuy nhiên, tôi đã không có may mắn làm việc với các đội kỹ lưỡng. Họ thường bỏ qua một nửa lỗi như vậy, đôi khi thậm chí là hầu hết thời gian. Đó là sự hấp dẫn lớn nhất đối với tôi về các ngoại lệ. Nếu chúng tôi bọc API này và làm cho nó đồng nhất throwkhi gặp lỗi, ngoại lệ có thể bị bỏ qua , và theo quan điểm của tôi, và kinh nghiệm, đó là điểm ưu việt của ngoại lệ.

Nhưng nếu các trường hợp ngoại lệ không thể được sử dụng, thì trạng thái lỗi trên mỗi luồng toàn cầu ít nhất có lợi thế (rất lớn so với việc trả lại mã lỗi cho tôi) rằng nó có thể gặp lỗi cũ muộn hơn một chút so với khi nó xảy ra xảy ra trong một số cơ sở mã hóa cẩu thả thay vì hoàn toàn thiếu nó và khiến chúng ta hoàn toàn không biết gì về những gì đã xảy ra. Lỗi có thể đã xảy ra một vài dòng trước đó hoặc trong một cuộc gọi chức năng trước đó, nhưng với điều kiện phần mềm chưa bị lỗi, chúng tôi có thể bắt đầu làm việc ngược lại và tìm ra nơi và lý do xảy ra.

Dường như với tôi rằng vì con trỏ rất hiếm, tôi phải sử dụng cờ lỗi nội bộ nếu tôi chọn tránh ngoại lệ.

Tôi không nhất thiết phải nói con trỏ là hiếm. Thậm chí có các phương thức trong C ++ 11 trở đi để có được các con trỏ dữ liệu cơ bản của các container và một nullptrtừ khóa mới . Nói chung, nó được coi là không khôn ngoan khi sử dụng các con trỏ thô để sở hữu / quản lý bộ nhớ nếu bạn có thể sử dụng một cái gì đó giống như unique_ptrthay vào đó là mức độ quan trọng của việc tuân thủ RAII khi có ngoại lệ. Nhưng những con trỏ thô không sở hữu / quản lý bộ nhớ không nhất thiết bị coi là quá tệ (ngay cả từ những người như Sutter và Stroustrup) và đôi khi rất thực tế như một cách để chỉ vào mọi thứ (cùng với các chỉ số chỉ vào sự vật).

Chúng được cho là không kém an toàn so với các trình vòng lặp container tiêu chuẩn (ít nhất là trong bản phát hành, các trình vòng kiểm tra vắng mặt) sẽ không phát hiện ra nếu bạn cố gắng hủy đăng ký chúng sau khi chúng bị vô hiệu. C ++ vẫn còn là một ngôn ngữ nguy hiểm, tôi nói, trừ khi việc sử dụng cụ thể của nó muốn bao bọc mọi thứ và che giấu ngay cả những con trỏ thô không sở hữu. Điều này gần như rất quan trọng với các ngoại lệ rằng các tài nguyên tuân thủ RAII (thường không có chi phí thời gian chạy), nhưng khác với việc nó không nhất thiết phải là ngôn ngữ an toàn nhất để sử dụng để tránh chi phí mà nhà phát triển không muốn rõ ràng đổi lấy thứ khác Việc sử dụng được đề xuất không cố gắng bảo vệ bạn khỏi những thứ như con trỏ lơ lửng và các trình vòng lặp không hợp lệ, có thể nói (nếu không chúng tôi sẽ được khuyến khích sử dụngshared_ptrkhắp nơi, mà Stroustrup phản đối kịch liệt). Đó là cố gắng bảo vệ bạn khỏi thất bại trong việc giải phóng / giải phóng / phá hủy / mở khóa / dọn dẹp tài nguyên đúng cách khi có thứ gì đó throws.


14

Đây là vấn đề: Vì lịch sử và tính linh hoạt độc đáo của C ++, bạn có thể tìm thấy ai đó tuyên bố hầu như mọi ý kiến ​​về bất kỳ tính năng nào bạn muốn. Tuy nhiên, nói chung, những gì bạn đang làm trông giống C, ý tưởng đó càng tệ.

Một trong những lý do khiến C ++ trở nên lỏng lẻo hơn khi có ngoại lệ là bạn không thể chính xác return nilbất cứ khi nào bạn cảm thấy thích nó. Không có những thứ như niltrong phần lớn các trường hợp và loại.

Nhưng đây là thực tế đơn giản. Ngoại lệ làm công việc của họ tự động. Khi bạn ném một ngoại lệ, RAII tiếp quản và mọi thứ được xử lý bởi trình biên dịch. Khi bạn sử dụng mã lỗi, bạn phải nhớ kiểm tra chúng. Điều này vốn đã làm cho các ngoại lệ an toàn hơn đáng kể so với mã lỗi - chúng tự kiểm tra, như vậy. Ngoài ra, chúng còn biểu cảm hơn. Ném một ngoại lệ và bạn có thể nhận ra một chuỗi cho bạn biết lỗi là gì và thậm chí có thể chứa thông tin cụ thể, như "Tham số xấu X có giá trị Y thay vì Z". Nhận mã lỗi 0xDEADBEEF và chính xác, điều gì đã sai? Tôi chắc chắn hy vọng các tài liệu hướng dẫn đầy đủ và up-to-date, và thậm chí nếu bạn nhận được "thông số Bad", nó sẽ không bao giờ để cho bạn biết tham số, giá trị của nó và giá trị của nó phải có. Nếu bạn bắt bằng cách tham khảo, họ cũng có thể là đa hình. Cuối cùng, các ngoại lệ có thể được ném từ những nơi mà mã lỗi không bao giờ có thể, như các hàm tạo. Và làm thế nào về các thuật toán chung? Làm thế nào std::for_eachsẽ xử lý mã lỗi của bạn? Pro-tip: Không phải vậy.

Các ngoại lệ vượt trội hơn rất nhiều so với mã lỗi ở mọi khía cạnh. Câu hỏi thực sự là trong trường hợp ngoại lệ so với khẳng định.

Vấn đề là như thế này. Không ai có thể biết trước những điều kiện trước mà chương trình của bạn có để vận hành, điều kiện thất bại nào là bất thường, có thể được kiểm tra trước và đó là lỗi. Điều này thường có nghĩa là bạn không thể quyết định trước liệu một thất bại nhất định sẽ là một khẳng định hay một ngoại lệ mà không biết logic chương trình. Ngoài ra, một hoạt động có thể tiếp tục khi một trong các hoạt động phụ của nó không thành công là ngoại lệ , không phải là quy tắc.

Theo tôi, ngoại lệ là có để được bắt. Không nhất thiết phải ngay lập tức, nhưng cuối cùng. Một ngoại lệ là một vấn đề mà bạn mong đợi chương trình có thể phục hồi từ một lúc nào đó. Nhưng hoạt động trong câu hỏi không bao giờ có thể phục hồi từ một vấn đề đảm bảo một ngoại lệ.

Thất bại khẳng định luôn luôn gây tử vong, lỗi không thể phục hồi. Ký ức tham nhũng, đại loại thế.

Vì vậy, khi bạn không thể mở một tập tin, nó là một khẳng định hay ngoại lệ? Chà, trong một thư viện chung, sau đó có rất nhiều tình huống thể xử lý lỗi , ví dụ, khi tải tệp cấu hình, bạn có thể chỉ cần sử dụng mặc định dựng sẵn thay vào đó, vì vậy tôi nói ngoại lệ.

Như một chú thích, tôi muốn đề cập đến có một số điều "Mô hình đối tượng Null" đang diễn ra xung quanh. Mô hình này là khủng khiếp . Trong mười năm, nó sẽ là Singleton tiếp theo. Số lượng các trường hợp mà bạn có thể tạo ra một đối tượng null phù hợp là rất nhỏ .


Sự thay thế không nhất thiết là một int đơn giản. Tôi có nhiều khả năng sử dụng thứ gì đó giống với NSError mà tôi đã từng sử dụng từ Obj-C.
Per Johansson

Anh ta đang so sánh không phải với C, mà là Mục tiêu C. Đó là một sự khác biệt lớn . Và mã lỗi của anh ấy đang giải thích các dòng. Al mà bạn nói là chính xác, chỉ không trả lời về câu hỏi này bằng cách nào đó. Không có ý xúc phạm.
Gangnus

Bất cứ ai sử dụng Objective-C tất nhiên sẽ nói với bạn rằng bạn hoàn toàn sai lầm.
gnasher729

5

Các ngoại lệ được phát minh vì một lý do, đó là để tránh việc tất cả mã của bạn trông như thế này:

bool success = function1(&result1, &err);
if (!success) {
    error_handler(err);
    return;
}

success = function2(&result2, &err);
if (!success) {
    error_handler(err);
    return;
}

Thay vào đó, bạn nhận được một cái gì đó trông giống như sau, với một trình xử lý ngoại lệ đi lên mainhoặc nằm ở vị trí thuận tiện:

result1 = function1();
result2 = function2();

Một số người yêu cầu lợi ích hiệu suất theo cách tiếp cận không có ngoại lệ, nhưng theo tôi, mối lo ngại về khả năng đọc có thể tăng hơn hiệu suất nhỏ, đặc biệt là khi bạn bao gồm thời gian thực hiện cho tất cả các if (!success)nồi hơi bạn phải rắc khắp nơi, hoặc có nguy cơ khó gỡ lỗi hơn nếu bạn không ' bao gồm nó, và xem xét các cơ hội ngoại lệ xảy ra là tương đối hiếm.

Tôi không biết tại sao Apple không khuyến khích sử dụng ngoại lệ. Nếu họ đang cố gắng tránh truyền bá các ngoại lệ chưa được xử lý, tất cả những gì bạn thực sự đạt được là những người sử dụng con trỏ null để chỉ ra các ngoại lệ thay vào đó, do đó, một lập trình viên sẽ dẫn đến một ngoại lệ con trỏ null thay vì một tệp hữu ích hơn không tìm thấy ngoại lệ hoặc bất cứ điều gì.


1
Điều này sẽ có ý nghĩa hơn nếu mã không có ngoại lệ thực sự trông như thế, nhưng nó thường không. Mẫu này xuất hiện khi error_handler không bao giờ quay lại, nhưng hiếm khi khác.
Per Johansson

1

Trong bài đăng mà bạn tham khảo, (ngoại lệ so với mã lỗi), tôi nghĩ có một cuộc thảo luận khác biệt đang diễn ra. Câu hỏi dường như là liệu bạn có một số danh sách mã lỗi #define toàn cầu hay không, có thể hoàn thành với các tên như ERR001_FILE_NOT_WRITEABLE (hoặc viết tắt, nếu bạn không may mắn). Và, tôi nghĩ rằng điểm chính trong luồng đó là nếu bạn đang lập trình bằng ngôn ngữ đa hình, sử dụng các thể hiện đối tượng, thì việc xây dựng thủ tục như vậy là không cần thiết. Các trường hợp ngoại lệ có thể diễn tả lỗi nào xảy ra đơn giản nhờ vào loại của chúng và chúng có thể gói gọn thông tin như thông điệp cần in ra (và cả những thứ khác nữa). Vì vậy, tôi sẽ coi cuộc trò chuyện đó như một cuộc trò chuyện về việc bạn có nên lập trình theo thủ tục bằng ngôn ngữ hướng đối tượng hay không.

Nhưng, cuộc trò chuyện về thời điểm và mức độ dựa vào các ngoại lệ để xử lý các tình huống phát sinh trong mã là một cách khác nhau. Khi các ngoại lệ được ném và bắt, bạn đang đưa ra một mô hình luồng điều khiển hoàn toàn khác với ngăn xếp cuộc gọi truyền thống. Ném ngoại lệ về cơ bản là một tuyên bố goto giúp bạn thoát khỏi ngăn xếp cuộc gọi và gửi bạn đến một vị trí không xác định (bất cứ nơi nào mã khách hàng của bạn quyết định xử lý ngoại lệ của bạn). Điều này làm cho logic ngoại lệ rất khó để lý do về .

Và vì vậy, có một tình cảm như thế được thể hiện bởi jheriko rằng những điều này nên được giảm thiểu. Đó là, giả sử rằng bạn đang đọc một tệp có thể tồn tại hoặc không tồn tại trên đĩa. Jheriko và Apple và những người nghĩ như họ (bao gồm cả tôi) sẽ lập luận rằng việc ném một ngoại lệ là không phù hợp - sự vắng mặt của tệp đó không phải là ngoại lệ , điều đó được mong đợi. Các ngoại lệ không nên thay thế cho luồng điều khiển bình thường. Thật dễ dàng để trả về phương thức ReadFile () của bạn, giả sử là boolean và để mã máy khách nhìn thấy từ giá trị trả về sai mà không tìm thấy tệp. Mã khách hàng sau đó có thể cho biết tệp người dùng không được tìm thấy hoặc lặng lẽ xử lý trường hợp đó hoặc bất cứ điều gì nó muốn làm.

Khi bạn ném một ngoại lệ, bạn đang tạo gánh nặng cho khách hàng của mình. Đôi khi, bạn đang cung cấp cho họ mã sẽ đưa họ ra khỏi ngăn xếp cuộc gọi thông thường và buộc họ viết mã bổ sung để ngăn ứng dụng của họ bị sập. Một gánh nặng mạnh mẽ và chói tai như vậy chỉ nên được áp đặt lên chúng nếu thực sự cần thiết trong thời gian chạy, hoặc vì lợi ích của triết lý "thất bại sớm". Vì vậy, trong trường hợp trước, bạn có thể đưa ra các ngoại lệ nếu mục đích của ứng dụng của bạn là giám sát một số hoạt động của mạng và ai đó rút cáp mạng (bạn đang báo hiệu sự cố thảm khốc). Trong trường hợp sau, bạn có thể đưa ra một ngoại lệ nếu phương thức của bạn mong đợi một tham số không null và bạn được trao null (bạn đang báo hiệu rằng bạn đang sử dụng không phù hợp).

Đối với những gì cần làm thay vì các ngoại lệ trong thế giới hướng đối tượng, bạn có các tùy chọn ngoài cấu trúc mã lỗi thủ tục. Một điều khiến người ta chú ý ngay lập tức là bạn có thể tạo các đối tượng được gọi là OperationResult hoặc một số thứ tương tự. Một phương thức có thể trả về một ActivityResult và đối tượng đó có thể có thông tin về việc hoạt động đó có thành công hay không và bất kỳ thông tin nào bạn muốn nó có (ví dụ, thông báo trạng thái, nhưng cũng có thể gói gọn các chiến lược để phục hồi lỗi ). Trả lại điều này thay vì ném một ngoại lệ cho phép đa hình, bảo toàn luồng điều khiển và làm cho việc gỡ lỗi đơn giản hơn nhiều.


Tôi đồng ý với bạn, nhưng đó là lý do dường như không khiến tôi phải đặt câu hỏi.
Per Johansson

0

Có một vài chương đáng yêu trong Clean Code nói về chính chủ đề này - trừ quan điểm của Apple.

Ý tưởng cơ bản là bạn không bao giờ trả về số 0 từ các hàm của mình, bởi vì nó:

  • Thêm rất nhiều mã trùng lặp
  • Làm cho mã lộn xộn của bạn - Tức là: Nó trở nên khó đọc hơn
  • Thường có thể dẫn đến lỗi nil-con trỏ - hoặc vi phạm truy cập tùy thuộc vào "tôn giáo" lập trình cụ thể của bạn

Ý tưởng đi kèm khác là bạn sử dụng các ngoại lệ vì nó giúp giảm thêm sự lộn xộn trong mã của bạn và thay vì cần tạo một loạt mã lỗi mà cuối cùng trở nên khó theo dõi quản lý và bảo trì (đặc biệt là trên nhiều mô-đun và nền tảng) và Thay vào đó, khách hàng của bạn khó giải mã, thay vào đó, bạn tạo các thông điệp có ý nghĩa xác định chính xác bản chất của vấn đề, giúp gỡ lỗi đơn giản hơn và có thể giảm mã xử lý lỗi của bạn thành một câu đơn giản như một try..catchcâu lệnh cơ bản .

Quay trở lại những ngày phát triển COM của tôi, tôi đã có một dự án trong đó mọi thất bại đều đưa ra một ngoại lệ và mỗi ngoại lệ cần sử dụng id mã lỗi duy nhất dựa trên việc mở rộng các ngoại lệ COM tiêu chuẩn của windows. Đó là một dự án từ một dự án trước đó, nơi các mã lỗi đã được sử dụng trước đó, nhưng công ty muốn đi theo hướng đối tượng và nhảy lên băng thông COM mà không thay đổi cách họ làm quá nhiều. Chỉ mất 4 tháng để họ hết số mã lỗi và nhờ tôi tái cấu trúc các vùng mã lớn để sử dụng ngoại lệ thay thế. Tốt hơn để tiết kiệm cho mình những nỗ lực lên phía trước và chỉ cần sử dụng ngoại lệ một cách thận trọng.


Cảm ơn, nhưng tôi không xem xét việc trả lại NULL. Dường như không thể đối mặt với các nhà xây dựng. Như tôi đã đề cập, có lẽ tôi phải sử dụng cờ lỗi trong đối tượng.
Per Johansson
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.