Tôi có thể viện dẫn cơn thịnh nộ của Pythonistas (không biết vì tôi không sử dụng Python nhiều) hoặc lập trình viên từ các ngôn ngữ khác có câu trả lời này, nhưng theo tôi, hầu hết các chức năng không nên có một catch
khối, nói một cách lý tưởng. Để cho thấy lý do tại sao, hãy để tôi đối chiếu điều này với việc truyền mã lỗi thủ công thuộc loại tôi phải làm khi làm việc với Turbo C vào cuối những năm 80 và đầu thập niên 90.
Vì vậy, giả sử chúng ta có một chức năng để tải một hình ảnh hoặc một cái gì đó tương tự để đáp ứng với người dùng chọn một tệp hình ảnh để tải và điều này được viết bằng C và lắp ráp:
Tôi đã bỏ qua một số chức năng cấp thấp nhưng chúng ta có thể thấy rằng tôi đã xác định các loại chức năng khác nhau, được mã hóa màu, dựa trên trách nhiệm của chúng đối với việc xử lý lỗi.
Điểm thất bại và phục hồi
Bây giờ không bao giờ khó để viết các loại chức năng mà tôi gọi là "điểm có thể xảy ra lỗi" (các chức năng throw
, nghĩa là) và các chức năng "phục hồi lỗi và báo cáo" ( catch
nghĩa là).
Những chức năng luôn tầm thường để viết một cách chính xác trước khi xử lý ngoại lệ là có sẵn từ một chức năng mà có thể chạy vào một thất bại bên ngoài, giống như thất bại trong việc cấp phát bộ nhớ, chỉ có thể trả về một NULL
hoặc 0
hoặc -1
hoặc thiết lập một mã lỗi toàn cầu hoặc một cái gì đó để tác động này. Và phục hồi / báo cáo lỗi luôn dễ dàng vì một khi bạn đã tìm cách ngăn xếp cuộc gọi đến điểm có ý nghĩa để khôi phục và báo cáo lỗi, bạn chỉ cần lấy mã lỗi và / hoặc thông báo và báo cáo cho người dùng. Và tự nhiên, một chức năng trong lá của hệ thống phân cấp này không bao giờ có thể thất bại cho dù nó có thay đổi như thế nào trong tương lai ( Convert Pixel
) rất đơn giản để viết chính xác (ít nhất là đối với việc xử lý lỗi).
Sự truyền lỗi
Tuy nhiên, các chức năng tẻ nhạt dễ bị lỗi của con người là các bộ truyền lỗi , những chức năng không trực tiếp gặp sự cố nhưng được gọi là các chức năng có thể thất bại ở đâu đó sâu hơn trong hệ thống phân cấp. Vào thời điểm đó, Allocate Scanline
có thể phải xử lý một sự thất bại từ malloc
và sau đó trả về một lỗi xuống Convert Scanlines
, sau đó Convert Scanlines
sẽ phải kiểm tra xem có lỗi đó và vượt qua nó xuống Decompress Image
, sau đó Decompress Image->Parse Image
, và Parse Image->Load Image
, và Load Image
để lệnh do người dùng cuối mà lỗi cuối cùng được báo cáo .
Đây là nơi rất nhiều người mắc lỗi vì chỉ cần một người truyền lỗi không kiểm tra và chuyển lỗi cho toàn bộ hệ thống phân cấp các chức năng để lật đổ khi xử lý lỗi đúng.
Hơn nữa, nếu các mã lỗi được trả về bởi các hàm, chúng ta sẽ mất rất nhiều khả năng, giả sử, 90% cơ sở mã của chúng ta, để trả về các giá trị quan tâm khi thành công vì rất nhiều hàm sẽ phải dự trữ giá trị trả về của chúng để trả về mã lỗi thất bại .
Giảm lỗi của con người: Mã lỗi toàn cầu
Vậy làm thế nào chúng ta có thể giảm khả năng lỗi của con người? Ở đây tôi thậm chí có thể viện dẫn cơn thịnh nộ của một số lập trình viên C, nhưng theo tôi, một cải tiến ngay lập tức là sử dụng mã lỗi toàn cầu , như OpenGL với glGetError
. Điều này ít nhất giải phóng các chức năng để trả về các giá trị quan tâm có ý nghĩa về thành công. Có nhiều cách để làm cho luồng này an toàn và hiệu quả trong đó mã lỗi được bản địa hóa thành một luồng.
Cũng có một số trường hợp hàm có thể gặp lỗi nhưng tương đối vô hại để nó tiếp tục tồn tại lâu hơn một chút trước khi nó trả về sớm do phát hiện ra lỗi trước đó. Điều này cho phép điều đó xảy ra mà không phải kiểm tra lỗi đối với 90% các lệnh gọi chức năng được thực hiện trong mỗi chức năng, do đó nó vẫn có thể cho phép xử lý lỗi thích hợp mà không quá tỉ mỉ.
Giảm lỗi của con người: Xử lý ngoại lệ
Tuy nhiên, giải pháp trên vẫn đòi hỏi rất nhiều chức năng để xử lý khía cạnh luồng điều khiển của việc truyền lỗi thủ công, ngay cả khi nó có thể làm giảm số lượng dòng if error happened, return error
mã loại thủ công. Nó sẽ không loại bỏ nó hoàn toàn vì thường vẫn cần phải có ít nhất một nơi kiểm tra lỗi và trả lại cho hầu hết mọi chức năng lan truyền lỗi. Vì vậy, đây là khi xử lý ngoại lệ đi vào hình ảnh để lưu ngày (sorta).
Nhưng giá trị của xử lý ngoại lệ ở đây là giải phóng nhu cầu xử lý khía cạnh luồng điều khiển của việc truyền lỗi thủ công. Điều đó có nghĩa là giá trị của nó được gắn với khả năng tránh phải viết một catch
khối thuyền trong toàn bộ cơ sở mã của bạn. Trong sơ đồ trên, nơi duy nhất cần phải có một catch
khối là Load Image User Command
nơi báo cáo lỗi. Không có gì khác lý tưởng phải có catch
bất cứ điều gì vì nếu không, nó bắt đầu trở nên tẻ nhạt và dễ bị lỗi như xử lý mã lỗi.
Vì vậy, nếu bạn hỏi tôi, nếu bạn có một codebase rằng thực sự hưởng lợi từ ngoại lệ xử lý một cách tao nhã, nó cần phải có tối thiểu số lượng catch
các khối (bằng cách tối thiểu tôi không có nghĩa là không, nhưng nhiều hơn như một cho mỗi cao độc đáo hoạt động của người dùng cuối có thể thất bại và thậm chí có thể ít hơn nếu tất cả các hoạt động của người dùng cao cấp được gọi thông qua hệ thống lệnh trung tâm).
Dọn dẹp tài nguyên
Tuy nhiên, xử lý ngoại lệ chỉ giải quyết được yêu cầu tránh xử lý thủ công các khía cạnh luồng điều khiển của lan truyền lỗi trong các đường dẫn đặc biệt tách biệt với các luồng thực thi thông thường. Thường thì một chức năng đóng vai trò là công cụ truyền lỗi, ngay cả khi nó tự động thực hiện điều này với EH, vẫn có thể có được một số tài nguyên cần thiết để phá hủy. Ví dụ, một chức năng như vậy có thể mở một tệp tạm thời mà nó cần phải đóng trước khi quay trở lại từ chức năng đó, hoặc khóa một mutex mà nó cần để mở khóa bất kể điều gì.
Đối với điều này, tôi có thể viện dẫn cơn thịnh nộ của rất nhiều lập trình viên từ tất cả các loại ngôn ngữ, nhưng tôi nghĩ cách tiếp cận C ++ cho việc này là lý tưởng. Ngôn ngữ giới thiệu các hàm hủy được gọi theo kiểu xác định ngay lập tức một đối tượng đi ra khỏi phạm vi. Do đó, mã C ++, giả sử, khóa một mutex thông qua một đối tượng mutex có phạm vi với một hàm hủy không cần phải mở khóa bằng tay, vì nó sẽ được mở khóa tự động khi đối tượng ra khỏi phạm vi bất kể điều gì xảy ra (ngay cả khi có ngoại lệ đã gặp). Vì vậy, thực sự không cần mã C ++ được viết tốt bao giờ phải xử lý việc dọn dẹp tài nguyên cục bộ.
Trong các ngôn ngữ thiếu công cụ hủy, họ có thể cần sử dụng một finally
khối để dọn dẹp thủ công các tài nguyên cục bộ. Điều đó nói rằng, nó vẫn đánh bại việc phải xả mã của bạn bằng cách truyền lỗi thủ công với điều kiện bạn không phải catch
ngoại lệ ở khắp nơi.
Đảo ngược tác dụng phụ bên ngoài
Đây là những vấn đề khái niệm khó khăn nhất để giải quyết. Nếu bất kỳ chức năng nào, cho dù đó là bộ truyền lỗi hay điểm hỏng gây ra tác dụng phụ bên ngoài, thì nó cần phải khôi phục hoặc "hoàn tác" các tác dụng phụ đó để đưa hệ thống trở lại trạng thái như thể hoạt động không bao giờ xảy ra, thay vì " trạng thái nửa hợp lệ "trong đó hoạt động nửa chừng đã thành công. Tôi biết không có ngôn ngữ nào làm cho vấn đề khái niệm này dễ dàng hơn nhiều ngoại trừ các ngôn ngữ đơn giản là giảm nhu cầu về hầu hết các chức năng để gây ra tác dụng phụ bên ngoài, như các ngôn ngữ chức năng xoay quanh cấu trúc dữ liệu không thay đổi và liên tục.
Có finally
thể nói đây là một trong những giải pháp tao nhã nhất cho vấn đề ngôn ngữ xoay quanh tính biến đổi và tác dụng phụ, bởi vì loại logic này rất đặc trưng cho một chức năng cụ thể và không phù hợp với khái niệm "dọn dẹp tài nguyên ". Và tôi khuyên bạn nên sử dụng finally
tự do trong những trường hợp này để đảm bảo chức năng của bạn đảo ngược tác dụng phụ trong các ngôn ngữ hỗ trợ nó, bất kể bạn có cần một catch
khối hay không (và một lần nữa, nếu bạn hỏi tôi, mã được viết tốt nên có số lượng tối thiểu catch
các khối và tất cả catch
các khối phải ở những nơi có ý nghĩa nhất như với sơ đồ ở trên Load Image User Command
).
Ngôn ngữ mơ ước
Tuy nhiên, IMO finally
gần với lý tưởng cho việc đảo ngược hiệu ứng phụ nhưng không hoàn toàn. Chúng ta cần giới thiệu một boolean
biến để đẩy lùi hiệu quả các tác dụng phụ trong trường hợp thoát sớm (từ một ngoại lệ bị ném hoặc nếu không), như vậy:
bool finished = false;
try
{
// Cause external side effects.
...
// Indicate that all the external side effects were
// made successfully.
finished = true;
}
finally
{
// If the function prematurely exited before finishing
// causing all of its side effects, whether as a result of
// an early 'return' statement or an exception, undo the
// side effects.
if (!finished)
{
// Undo side effects.
...
}
}
Nếu tôi có thể thiết kế một ngôn ngữ, cách giải quyết vấn đề mơ ước của tôi sẽ như thế này để tự động hóa đoạn mã trên:
transaction
{
// Cause external side effects.
...
}
rollback
{
// This block is only executed if the above 'transaction'
// block didn't reach its end, either as a result of a premature
// 'return' or an exception.
// Undo side effects.
...
}
... Với các công cụ hủy diệt để tự động dọn dẹp các tài nguyên cục bộ, khiến chúng ta chỉ cần transaction
, rollback
và catch
(mặc dù tôi vẫn có thể muốn thêm finally
vào, giả sử, làm việc với các tài nguyên C không tự dọn sạch). Tuy nhiên, finally
với một boolean
biến là điều gần nhất để thực hiện điều này một cách đơn giản mà tôi thấy cho đến nay vẫn thiếu ngôn ngữ mơ ước của mình. Giải pháp đơn giản thứ hai mà tôi tìm thấy cho vấn đề này là bộ bảo vệ phạm vi trong các ngôn ngữ như C ++ và D, nhưng tôi luôn thấy bộ bảo vệ phạm vi hơi lúng túng về mặt khái niệm vì nó làm mờ ý tưởng "dọn sạch tài nguyên" và "đảo ngược hiệu ứng phụ". Theo tôi đó là những ý tưởng rất khác biệt cần được giải quyết theo một cách khác.
Ước mơ về ngôn ngữ nhỏ bé của tôi cũng sẽ xoay quanh các cấu trúc dữ liệu bất biến và bền bỉ để giúp dễ dàng hơn, mặc dù không cần thiết, để viết các hàm hiệu quả mà không phải sao chép toàn bộ cấu trúc dữ liệu lớn mặc dù chức năng này gây ra không có tác dụng phụ.
Phần kết luận
Vì vậy, dù sao đi nữa, với những lùm xùm của tôi sang một bên, tôi nghĩ rằng try/finally
mã của bạn để đóng ổ cắm là tốt và tuyệt vời khi xem xét rằng Python không có công cụ hủy diệt tương đương C ++ và cá nhân tôi nghĩ rằng bạn nên sử dụng nó một cách tự do cho những nơi cần đảo ngược tác dụng phụ và giảm thiểu số lượng nơi bạn phải catch
đến những nơi có ý nghĩa nhất.