Ngôn ngữ Go của Google không có ngoại lệ là một lựa chọn thiết kế, và Linus của Linux nổi tiếng đã gọi ngoại lệ là tào lao. Tại sao?
Ngôn ngữ Go của Google không có ngoại lệ là một lựa chọn thiết kế, và Linus của Linux nổi tiếng đã gọi ngoại lệ là tào lao. Tại sao?
Câu trả lời:
Các ngoại lệ làm cho nó thực sự dễ dàng để viết mã trong đó một ngoại lệ được ném ra sẽ phá vỡ các bất biến và để các đối tượng ở trạng thái không nhất quán. Về cơ bản, chúng buộc bạn phải nhớ rằng hầu hết mọi tuyên bố bạn đưa ra đều có khả năng bị ném và xử lý điều đó một cách chính xác. Làm như vậy có thể phức tạp và phản trực giác.
Hãy coi một cái gì đó như thế này như một ví dụ đơn giản:
class Frobber
{
int m_NumberOfFrobs;
FrobManager m_FrobManager;
public:
void Frob()
{
m_NumberOfFrobs++;
m_FrobManager.HandleFrob(new FrobObject());
}
};
Giả sử ý FrobManager
chí delete
là FrobObject
, điều này có vẻ ổn, phải không? Hoặc có thể không ... Hãy tưởng tượng sau đó nếu một trong hai FrobManager::HandleFrob()
hoặc operator new
ném một ngoại lệ. Trong ví dụ này, số gia tăng của m_NumberOfFrobs
không được quay trở lại. Do đó, bất kỳ ai sử dụng phiên bản này của Frobber
sẽ có một đối tượng có thể bị hỏng.
Ví dụ này có vẻ ngu ngốc (được rồi, tôi đã phải căng mình một chút để tạo ra một cái :-)), nhưng, điểm rút ra là nếu một lập trình viên không liên tục nghĩ đến các ngoại lệ và đảm bảo rằng mọi hoán vị của trạng thái đều được cuộn quay lại bất cứ khi nào có ném, bạn sẽ gặp rắc rối theo cách này.
Ví dụ, bạn có thể nghĩ về nó giống như bạn nghĩ về mutexes. Bên trong phần quan trọng, bạn dựa vào một số câu lệnh để đảm bảo rằng cấu trúc dữ liệu không bị hỏng và các luồng khác không thể nhìn thấy các giá trị trung gian của bạn. Nếu bất kỳ câu nào trong số đó ngẫu nhiên không chạy, bạn sẽ kết thúc với một thế giới đau đớn. Bây giờ loại bỏ các khóa và đồng thời, và suy nghĩ về mỗi phương pháp như vậy. Hãy coi mỗi phương thức như một giao dịch của các hoán vị trên trạng thái đối tượng, nếu bạn muốn. Khi bắt đầu cuộc gọi phương thức của bạn, đối tượng phải ở trạng thái sạch và ở cuối cũng phải có trạng thái sạch. Ở giữa, biếnfoo
có thể không nhất quán vớibar
, nhưng mã của bạn cuối cùng sẽ khắc phục điều đó. Ngoại lệ có nghĩa là bất kỳ câu lệnh nào của bạn có thể làm gián đoạn bạn bất cứ lúc nào. Tùy thuộc vào bạn trong từng phương pháp riêng lẻ để làm cho nó đúng và quay trở lại khi điều đó xảy ra, hoặc sắp xếp các hoạt động của bạn để việc ném không ảnh hưởng đến trạng thái đối tượng. Nếu bạn hiểu sai (và rất dễ mắc phải lỗi này), thì người gọi sẽ nhìn thấy các giá trị trung gian của bạn.
Các phương thức như RAII, mà các lập trình viên C ++ yêu thích đề cập đến như là giải pháp cuối cùng cho vấn đề này, sẽ đi một chặng đường dài để bảo vệ khỏi điều này. Nhưng chúng không phải là một viên đạn bạc. Nó sẽ đảm bảo rằng bạn giải phóng tài nguyên ngay lập tức, nhưng không giải phóng bạn khỏi việc phải suy nghĩ về sự hỏng hóc của trạng thái đối tượng và người gọi nhìn thấy các giá trị trung gian. Vì vậy, đối với nhiều người, nói dễ dàng hơn, theo định nghĩa của phong cách mã hóa, không có ngoại lệ . Nếu bạn hạn chế loại mã bạn viết, thì việc giới thiệu những lỗi này sẽ khó hơn. Nếu không, bạn rất dễ mắc sai lầm.
Toàn bộ sách đã được viết về mã hóa an toàn ngoại lệ trong C ++. Rất nhiều chuyên gia đã hiểu sai. Nếu nó thực sự phức tạp và có quá nhiều sắc thái, có lẽ đó là một dấu hiệu tốt cho thấy bạn cần bỏ qua tính năng đó. :-)
Lý do cho việc Go không có ngoại lệ được giải thích trong Câu hỏi thường gặp về thiết kế ngôn ngữ Go:
Ngoại lệ là một câu chuyện tương tự. Một số thiết kế cho các trường hợp ngoại lệ đã được đề xuất nhưng mỗi thiết kế đều tăng thêm độ phức tạp đáng kể cho ngôn ngữ và thời gian chạy. Theo bản chất của chúng, các ngoại lệ kéo dài các chức năng và thậm chí có thể là các goroutines; chúng có ý nghĩa trên phạm vi rộng. Cũng có lo ngại về ảnh hưởng của chúng đối với các thư viện. Theo định nghĩa, chúng đặc biệt nhưng có kinh nghiệm với các ngôn ngữ khác hỗ trợ chúng cho thấy chúng có ảnh hưởng sâu sắc đến đặc điểm kỹ thuật thư viện và giao diện. Sẽ thật tuyệt nếu bạn tìm được một thiết kế cho phép chúng thực sự đặc biệt mà không khuyến khích các lỗi thông thường biến thành luồng điều khiển đặc biệt đòi hỏi mọi lập trình viên phải bù đắp.
Giống như thuốc chung, ngoại lệ vẫn là một vấn đề mở.
Nói cách khác, họ vẫn chưa tìm ra cách hỗ trợ các ngoại lệ trong cờ vây theo cách mà họ cho là thỏa đáng. Họ không nói rằng ngoại lệ là xấu cho mỗi gia nhập ;
CẬP NHẬT - tháng 5 năm 2012
Các nhà thiết kế cờ vây hiện đã leo xuống khỏi hàng rào. Câu hỏi thường gặp của họ bây giờ cho biết điều này:
Chúng tôi tin rằng việc kết hợp các ngoại lệ với cấu trúc điều khiển, như trong thành ngữ try-catch-last, dẫn đến mã phức tạp. Nó cũng có xu hướng khuyến khích các lập trình viên gắn nhãn quá nhiều lỗi thông thường, chẳng hạn như không mở được tệp, là ngoại lệ.
Go có một cách tiếp cận khác. Để xử lý lỗi đơn giản, trả về nhiều giá trị của Go giúp bạn dễ dàng thông báo lỗi mà không làm quá tải giá trị trả về. Một loại lỗi chính tắc, cùng với các tính năng khác của Go, làm cho việc xử lý lỗi trở nên dễ chịu nhưng hoàn toàn khác với các ngôn ngữ khác.
Go cũng có một số chức năng tích hợp để báo hiệu và phục hồi từ các điều kiện thực sự đặc biệt. Cơ chế khôi phục chỉ được thực thi khi một phần trạng thái của chức năng bị chia nhỏ sau lỗi, đủ để xử lý thảm họa nhưng không yêu cầu cấu trúc điều khiển bổ sung và khi được sử dụng tốt, có thể dẫn đến mã xử lý lỗi sạch.
Xem bài viết Trì hoãn, Hoảng sợ và Khôi phục để biết chi tiết.
Vì vậy, câu trả lời ngắn gọn là họ có thể làm theo cách khác bằng cách sử dụng lợi tức đa giá trị. (Và dù sao thì họ cũng có một hình thức xử lý ngoại lệ.)
... và Linus của Linux nổi tiếng đã gọi các ngoại lệ là tào lao.
Nếu bạn muốn biết tại sao Linus cho rằng những trường hợp ngoại lệ là tào lao, thì điều tốt nhất là hãy tìm các bài viết của anh ấy về chủ đề này. Điều duy nhất tôi đã theo dõi cho đến nay là câu trích dẫn này được nhúng trong một vài email trên C ++ :
"Toàn bộ việc xử lý ngoại lệ C ++ về cơ bản đã bị hỏng. Nó đặc biệt bị hỏng đối với hạt nhân."
Bạn sẽ lưu ý rằng anh ấy đang nói về các ngoại lệ C ++ nói riêng và không phải ngoại lệ nói chung. (Và C ++ ngoại lệ làm rõ ràng có một số vấn đề mà làm cho họ khó khăn để sử dụng một cách chính xác.)
Kết luận của tôi là Linus đã không gọi các ngoại lệ (nói chung) là "tào lao" cả!
Các ngoại lệ không phải là xấu, nhưng nếu bạn biết chúng sẽ xảy ra rất nhiều, chúng có thể tốn kém về mặt hiệu suất.
Quy tắc chung là các ngoại lệ phải gắn cờ các điều kiện ngoại lệ và bạn không nên sử dụng chúng để kiểm soát luồng chương trình.
Tôi không đồng ý với việc "chỉ ném các ngoại lệ trong một tình huống ngoại lệ." Trong khi nói chung là đúng, nó gây hiểu lầm. Các ngoại lệ dành cho các điều kiện lỗi (lỗi thực thi).
Bất kể ngôn ngữ bạn sử dụng là gì, hãy chọn một bản sao của Nguyên tắc thiết kế khung : Quy ước, Thành ngữ và Mẫu cho Thư viện .NET có thể tái sử dụng (Phiên bản thứ 2). Chương về ném ngoại lệ không có ngang hàng. Một số trích dẫn từ ấn bản đầu tiên (ấn bản thứ 2 tại công việc của tôi):
Có các trang ghi chú về lợi ích của các ngoại lệ (tính nhất quán của API, lựa chọn vị trí mã xử lý lỗi, độ mạnh được cải thiện, v.v.) Có một phần về hiệu suất bao gồm một số mẫu (Tester-Doer, Try-Parse).
Các ngoại lệ và xử lý ngoại lệ không tồi. Giống như bất kỳ tính năng nào khác, chúng có thể bị sử dụng sai.
Từ quan điểm của golang, tôi đoán không có xử lý ngoại lệ giữ cho quá trình biên dịch đơn giản và an toàn.
Từ quan điểm của Linus, tôi hiểu rằng mã nhân là TẤT CẢ về các trường hợp góc. Vì vậy, nó là hợp lý để từ chối các trường hợp ngoại lệ.
Các ngoại lệ có ý nghĩa trong mã là không sao để bỏ nhiệm vụ hiện tại xuống sàn và trong đó mã trường hợp phổ biến có tầm quan trọng hơn việc xử lý lỗi. Nhưng chúng yêu cầu tạo mã từ trình biên dịch.
Ví dụ: chúng tốt trong hầu hết các mã cấp cao, hướng tới người dùng, chẳng hạn như mã ứng dụng web và máy tính để bàn.
Bản thân các ngoại lệ không phải là "xấu", đó là cách mà các ngoại lệ được xử lý đôi khi có xu hướng xấu. Có một số nguyên tắc có thể được áp dụng khi xử lý các trường hợp ngoại lệ để giúp giảm bớt một số vấn đề này. Một số trong số này bao gồm (nhưng chắc chắn không giới hạn):
Option<T>
thay vì trả về null
hiện nay. Chẳng hạn, vừa được giới thiệu trong Java 8, lấy một gợi ý từ Guava (và những người khác).
Các lập luận điển hình là không có cách nào để biết những ngoại lệ nào sẽ xuất hiện từ một đoạn mã cụ thể (tùy thuộc vào ngôn ngữ) và chúng quá giống goto
s, gây khó khăn cho việc theo dõi thực thi.
http://www.joelonsoftware.com/items/2003/10/13.html
Chắc chắn không có sự đồng thuận về vấn đề này. Tôi có thể nói rằng từ quan điểm của một lập trình viên C cứng như Linus, các ngoại lệ chắc chắn là một ý tưởng tồi. Tuy nhiên, một lập trình viên Java điển hình lại ở trong một tình huống rất khác.
setjmp
/ longjmp
thứ, khá tệ.
Các trường hợp ngoại lệ không tệ. Chúng rất phù hợp với mô hình RAII của C ++, đây là điều tao nhã nhất về C ++. Nếu bạn đã có một loạt mã không phải là ngoại lệ an toàn, thì chúng không tốt trong bối cảnh đó. Nếu bạn đang viết phần mềm thực sự cấp thấp, chẳng hạn như hệ điều hành Linux, thì chúng rất tệ. Nếu bạn thích rải mã của mình với một loạt các kiểm tra trả về lỗi, thì chúng không hữu ích. Nếu bạn không có kế hoạch kiểm soát tài nguyên khi một ngoại lệ được ném ra (mà các trình hủy C ++ cung cấp) thì chúng rất tệ.
Một trường hợp sử dụng tuyệt vời cho các trường hợp ngoại lệ là do đó ....
Giả sử bạn đang tham gia một dự án và mọi bộ điều khiển (khoảng 20 bộ điều khiển chính khác nhau) mở rộng một bộ điều khiển siêu lớp duy nhất với một phương thức hành động. Sau đó, mỗi bộ điều khiển thực hiện một loạt các công cụ khác nhau gọi các đối tượng B, C, D trong một trường hợp và F, G, D trong một trường hợp khác. Các trường hợp ngoại lệ đến giải cứu ở đây trong nhiều trường hợp có hàng tấn mã trả lại và MỌI bộ điều khiển đang xử lý nó theo cách khác nhau. Tôi đã đánh tất cả mã đó, ném ngoại lệ thích hợp từ "D", bắt nó trong phương thức hành động của bộ điều khiển siêu lớp và bây giờ tất cả các bộ điều khiển của chúng tôi đều nhất quán. Trước đây D trả về null cho MULTIPLE trường hợp lỗi khác nhau mà chúng tôi muốn thông báo cho người dùng cuối nhưng không thể và tôi đã không làm như vậy '
vâng, chúng tôi phải lo lắng về từng cấp độ và bất kỳ việc dọn dẹp / rò rỉ tài nguyên nào nhưng nói chung không bộ điều khiển nào của chúng tôi có bất kỳ tài nguyên nào để dọn dẹp sau đó.
cảm ơn chúa, chúng tôi đã có những ngoại lệ nếu không tôi đã tham gia vào một công ty tái cấu trúc khổng lồ và lãng phí quá nhiều thời gian cho một thứ mà lẽ ra là một vấn đề lập trình đơn giản.
Về mặt lý thuyết thì chúng thực sự rất tệ. Trong thế giới toán học hoàn hảo, bạn không thể có những tình huống ngoại lệ. Hãy nhìn vào các ngôn ngữ chức năng, chúng không có tác dụng phụ, vì vậy chúng hầu như không có nguồn cho các tình huống bất thường.
Nhưng, thực tế lại là một câu chuyện khác. Chúng ta luôn có những tình huống “bất ngờ”. Đây là lý do tại sao chúng ta cần ngoại lệ.
Tôi nghĩ rằng chúng ta có thể nghĩ về các ngoại lệ đối với đường cú pháp cho ExceptionSituationObserver. Bạn chỉ nhận được thông báo về các trường hợp ngoại lệ. Chỉ có bấy nhiêu thôi.
Với cờ vây, tôi nghĩ họ sẽ giới thiệu một thứ gì đó sẽ giải quyết những tình huống “bất ngờ”. Tôi có thể đoán rằng họ sẽ cố gắng làm cho nó nghe ít phá hoại hơn như các ngoại lệ và nhiều hơn như logic ứng dụng. Nhưng đây chỉ là suy đoán của tôi.
Mô hình xử lý ngoại lệ của C ++, tạo thành cơ sở một phần cho Java và đến lượt nó .net, giới thiệu một số khái niệm tốt, nhưng cũng có một số hạn chế nghiêm trọng. Một trong những mục đích thiết kế chính của việc xử lý ngoại lệ là cho phép các phương thức đảm bảo rằng chúng sẽ thỏa mãn các điều kiện hậu kỳ của chúng hoặc tạo ra một ngoại lệ, đồng thời cũng đảm bảo rằng bất kỳ quá trình dọn dẹp nào cần xảy ra trước khi một phương thức có thể thoát ra, sẽ xảy ra. Thật không may, các mô hình xử lý ngoại lệ của C ++, Java và .net đều không cung cấp bất kỳ phương tiện tốt nào để xử lý tình huống trong đó các yếu tố bất ngờ ngăn cản quá trình dọn dẹp dự kiến được thực hiện. Đến lượt nó, điều này có nghĩa là người ta phải có nguy cơ khiến mọi thứ dừng lại nếu có điều gì đó không mong muốn xảy ra (phương pháp C ++ để xử lý một ngoại lệ xảy ra trong quá trình giải nén ngăn xếp),
Ngay cả khi việc xử lý ngoại lệ nói chung là tốt, không có gì vô lý khi coi là không thể chấp nhận được một mô hình xử lý ngoại lệ không cung cấp phương tiện tốt để xử lý các vấn đề xảy ra khi dọn dẹp sau các vấn đề khác. Điều đó không có nghĩa là một khuôn khổ không thể được thiết kế với một mô hình xử lý ngoại lệ có thể đảm bảo hành vi hợp lý ngay cả trong các tình huống nhiều lỗi, nhưng không có ngôn ngữ hoặc khung công tác hàng đầu nào có thể làm như vậy.
Tôi đã không đọc tất cả các câu trả lời khác, vì vậy câu hỏi này đã được đề cập đến, nhưng một lời chỉ trích là chúng khiến các chương trình bị hỏng trong chuỗi dài, gây khó khăn cho việc tìm kiếm lỗi khi gỡ lỗi mã. Ví dụ, nếu Foo () gọi Bar () gọi Wah () gọi ToString () thì việc vô tình đẩy dữ liệu sai vào ToString () sẽ giống như một lỗi trong Foo (), một hàm gần như hoàn toàn không liên quan.
Được rồi, câu trả lời nhàm chán ở đây. Tôi đoán nó phụ thuộc vào ngôn ngữ thực sự. Trường hợp một ngoại lệ có thể để lại các tài nguyên được phân bổ, chúng nên được tránh. Trong các ngôn ngữ kịch bản, chúng chỉ loại bỏ hoặc chèn ép các phần của luồng ứng dụng. Bản thân điều đó là không đáng tin cậy, nhưng việc thoát khỏi các lỗi gần như nghiêm trọng với các ngoại lệ là một ý tưởng có thể chấp nhận được.
Đối với báo hiệu lỗi, tôi thường thích tín hiệu lỗi hơn. Tất cả phụ thuộc vào API, trường hợp sử dụng và mức độ nghiêm trọng hoặc nếu việc ghi nhật ký là đủ. Ngoài ra, tôi đang cố gắng xác định lại hành vi và throw Phonebooks()
thay vào đó. Ý tưởng cho rằng "Ngoại lệ" thường là kết thúc, nhưng "Danh bạ" chứa thông tin hữu ích về khôi phục lỗi hoặc các lộ trình thực thi thay thế. (Chưa tìm thấy một trường hợp sử dụng tốt nào, nhưng hãy tiếp tục thử.)
Đối với tôi vấn đề rất đơn giản. Nhiều lập trình viên sử dụng trình xử lý ngoại lệ một cách không thích hợp. Nhiều nguồn ngôn ngữ hơn là tốt hơn. Có khả năng xử lý các trường hợp ngoại lệ là tốt. Một ví dụ về việc sử dụng sai là một giá trị phải là số nguyên không được xác minh, hoặc một đầu vào khác có thể chia và không được kiểm tra để chia cho 0 ... xử lý ngoại lệ có thể là một cách dễ dàng để tránh làm việc nhiều hơn và suy nghĩ nhiều hơn, lập trình viên có thể muốn thực hiện một phím tắt bẩn và áp dụng một xử lý ngoại lệ ... Tuyên bố: "một mã chuyên nghiệp KHÔNG BAO GIỜ thất bại" có thể là viển vông, nếu một số vấn đề được xử lý bởi thuật toán là không chắc chắn về bản chất của chính nó. Có lẽ trong những tình huống không xác định theo bản chất thì tốt nhất là xử lý ngoại lệ. Thực hành lập trình tốt là một vấn đề tranh luận.