Ngoại lệ, mã lỗi và công đoàn bị phân biệt đối xử


80

Gần đây tôi đã bắt đầu một công việc lập trình C #, nhưng tôi đã có một chút nền tảng về Haskell.

Nhưng tôi hiểu C # là một ngôn ngữ hướng đối tượng, tôi không muốn buộc một cái chốt tròn vào một lỗ vuông.

Tôi đọc bài viết Ngoại lệ Ném từ Microsoft trong đó nêu:

KHÔNG trả lại mã lỗi.

Nhưng khi đã quen với Haskell, tôi đã sử dụng kiểu dữ liệu C # OneOf, trả về kết quả là giá trị "bên phải" hoặc lỗi (thường là Giá trị liệt kê) làm giá trị "bên trái".

Điều này giống như quy ước Eithertrong Haskell.

Đối với tôi, điều này có vẻ an toàn hơn ngoại lệ. Trong C #, bỏ qua các ngoại lệ không tạo ra lỗi biên dịch và nếu chúng không bị bắt, chúng chỉ nổi bong bóng và làm hỏng chương trình của bạn. Điều này có lẽ tốt hơn là bỏ qua mã lỗi và tạo ra hành vi không xác định, nhưng việc đánh sập phần mềm của khách hàng của bạn vẫn không phải là một điều tốt, đặc biệt khi nó thực hiện nhiều nhiệm vụ kinh doanh quan trọng khác trong nền.

Với OneOf, người ta phải khá rõ ràng về việc giải nén nó và xử lý giá trị trả về và mã lỗi. Và nếu người ta không biết cách xử lý nó ở giai đoạn đó trong ngăn xếp cuộc gọi, thì nó cần được đưa vào giá trị trả về của hàm hiện tại, vì vậy người gọi biết có thể xảy ra lỗi.

Nhưng đây dường như không phải là cách tiếp cận mà Microsoft gợi ý.

Việc sử dụng OneOfthay vì các ngoại lệ để xử lý các ngoại lệ "thông thường" (như Không tìm thấy tệp, v.v.) là một cách tiếp cận hợp lý hay đó là một thực tiễn khủng khiếp?


Điều đáng chú ý là tôi đã nghe nói rằng các ngoại lệ như luồng điều khiển được coi là một phản hạt nghiêm trọng , vì vậy nếu "ngoại lệ" là thứ bạn thường xử lý mà không kết thúc chương trình, thì đó có phải là "luồng điều khiển" không? Tôi hiểu có một chút màu xám ở đây.

Lưu ý Tôi không sử dụng OneOfcho những thứ như "Hết bộ nhớ", các điều kiện tôi không mong đợi để phục hồi vẫn sẽ đưa ra ngoại lệ. Nhưng tôi cảm thấy các vấn đề khá hợp lý, như đầu vào của người dùng không phân tích cú pháp về cơ bản là "luồng kiểm soát" và có lẽ không nên đưa ra ngoại lệ.


Suy nghĩ tiếp theo:

Từ cuộc thảo luận này, những gì tôi đang lấy đi hiện tại như sau:

  1. Nếu bạn mong đợi người gọi ngay lập tức catchvà xử lý ngoại lệ hầu hết thời gian và tiếp tục công việc của mình, có lẽ thông qua một đường dẫn khác, nó có thể là một phần của kiểu trả về. Optionalhoặc OneOfcó thể hữu ích ở đây.
  2. Nếu bạn mong muốn người gọi ngay lập tức không bắt được ngoại lệ hầu hết thời gian, hãy ném một ngoại lệ, để tiết kiệm sự tỉnh táo của việc chuyển nó thủ công lên ngăn xếp.
  3. Nếu bạn không chắc chắn người gọi ngay lập tức sẽ làm gì, có thể cung cấp cả hai, như ParseTryParse.

13
Sử dụng F # Result;-)
Astrinus

8
Hơi lạc đề, nhưng lưu ý rằng cách tiếp cận bạn đang xem có lỗ hổng chung mà không có cách nào để đảm bảo nhà phát triển xử lý lỗi đúng cách. Chắc chắn, hệ thống gõ có thể hoạt động như một lời nhắc nhở, nhưng bạn mất mặc định làm nổ chương trình vì không xử lý được lỗi, điều này khiến bạn có nhiều khả năng gặp phải tình trạng không mong muốn. Dù lời nhắc có đáng để mất mặc định đó hay không là một cuộc tranh luận mở. Tôi có xu hướng nghĩ rằng tốt hơn hết là viết tất cả mã của bạn với giả định rằng một ngoại lệ sẽ chấm dứt nó vào một lúc nào đó; sau đó lời nhắc có giá trị ít hơn.
jpmc26

9
Bài viết liên quan: Eric Lippert về bốn "thùng" ngoại lệ . Ví dụ mà ông đưa ra về các cuộc sinh thái ngoại sinh cho thấy có những kịch bản mà bạn chỉ phải đối phó với các ngoại lệ, tức là FileNotFoundException.
Søren D. Ptæus

7
Tôi có thể một mình về điều này, nhưng tôi nghĩ rằng sụp đổ là tốt . Không có bằng chứng nào tốt hơn cho thấy có vấn đề trên phần mềm của bạn hơn là nhật ký sự cố với dấu vết thích hợp. Nếu bạn có một nhóm có thể sửa và triển khai một bản vá trong 30 phút hoặc ít hơn, để những người bạn xấu xí đó xuất hiện có thể không phải là một điều xấu.
T. Sar

9
Tại sao không ai trong số các câu trả lời làm rõ rằng các Eitherđơn nguyên không phải là một mã lỗi , và cũng không phải là OneOf? Chúng khác nhau về cơ bản, và câu hỏi do đó dường như được dựa trên một sự hiểu lầm. (Mặc dù ở dạng đã được sửa đổi, đây vẫn là một câu hỏi hợp lệ.)
Konrad Rudolph

Câu trả lời:


131

nhưng sự cố phần mềm của khách hàng của bạn vẫn không phải là một điều tốt

Nó chắc chắn là một điều tốt.

Bạn muốn bất cứ điều gì khiến hệ thống ở trạng thái không xác định sẽ dừng hệ thống vì một hệ thống không xác định có thể làm những việc khó chịu như dữ liệu bị hỏng, định dạng ổ cứng và gửi email đe dọa tổng thống. Nếu bạn không thể khôi phục và đưa hệ thống trở lại trạng thái xác định thì sự cố là việc phải làm. Đó chính xác là lý do tại sao chúng tôi xây dựng các hệ thống gặp sự cố thay vì lặng lẽ tự xé toạc. Bây giờ chắc chắn, tất cả chúng ta đều muốn một hệ thống ổn định không bao giờ gặp sự cố nhưng chúng tôi chỉ thực sự muốn điều đó khi hệ thống ở trong trạng thái an toàn có thể dự đoán được xác định.

Tôi đã nghe nói rằng các trường hợp ngoại lệ như luồng điều khiển được coi là một phản hạt nghiêm trọng

Điều đó hoàn toàn đúng nhưng nó thường bị hiểu lầm. Khi họ phát minh ra hệ thống ngoại lệ, họ sợ rằng họ đã phá vỡ lập trình có cấu trúc. Lập trình có cấu trúc là lý do tại sao chúng tôi có for, while, until, break, và continuekhi tất cả chúng ta cần, để làm tất cả điều đó, là goto.

Dijkstra đã dạy chúng tôi rằng sử dụng goto một cách không chính thức (nghĩa là nhảy xung quanh bất cứ nơi nào bạn muốn) khiến việc đọc mã trở thành một cơn ác mộng. Khi họ đưa cho chúng tôi hệ thống ngoại lệ, họ sợ rằng họ đang phát minh lại goto. Vì vậy, họ bảo chúng tôi không "sử dụng nó để kiểm soát dòng chảy" với hy vọng chúng tôi hiểu. Thật không may, nhiều người trong chúng ta đã không.

Kỳ lạ thay, chúng ta thường không lạm dụng các ngoại lệ để tạo mã spaghetti như trước đây với goto. Những lời khuyên dường như đã gây ra nhiều rắc rối.

Các ngoại lệ cơ bản là về việc từ chối một giả định. Khi bạn yêu cầu một tệp được lưu, bạn cho rằng tệp đó có thể và sẽ được lưu. Ngoại lệ bạn nhận được khi không thể là tên bất hợp pháp, HD đầy hoặc do một con chuột đã gặm nhấm cáp dữ liệu của bạn. Bạn có thể xử lý tất cả các lỗi đó một cách khác nhau, bạn có thể xử lý tất cả các lỗi theo cùng một cách hoặc bạn có thể để chúng dừng hệ thống. Có một đường dẫn hạnh phúc trong mã của bạn, nơi các giả định của bạn phải giữ đúng. Một cách hay một ngoại lệ khác đưa bạn ra khỏi con đường hạnh phúc đó. Nói đúng ra, ừ đó là một loại "kiểm soát dòng chảy" nhưng đó không phải là điều họ đang cảnh báo bạn. Họ đã nói về những điều vô nghĩa như thế này :

nhập mô tả hình ảnh ở đây

"Trường hợp ngoại lệ nên được đặc biệt". Tautology nhỏ này đã được sinh ra bởi vì các nhà thiết kế hệ thống ngoại lệ cần thời gian để xây dựng dấu vết ngăn xếp. So với nhảy xung quanh, điều này là chậm. Nó ăn thời gian CPU. Nhưng nếu bạn chuẩn bị đăng nhập và tạm dừng hệ thống hoặc ít nhất là tạm dừng quá trình xử lý chuyên sâu thời gian hiện tại trước khi bắt đầu kế tiếp thì bạn có một chút thời gian để giết. Nếu mọi người bắt đầu sử dụng ngoại lệ "để kiểm soát dòng chảy" thì những giả định đó về thời gian sẽ xuất hiện ngoài cửa sổ. Vì vậy, "Ngoại lệ nên là ngoại lệ" thực sự được đưa ra cho chúng tôi như là một xem xét hiệu suất.

Quan trọng hơn nhiều so với điều đó không làm chúng ta bối rối. Mất bao lâu để bạn phát hiện ra vòng lặp vô hạn trong đoạn mã trên?

KHÔNG trả lại mã lỗi.

... là lời khuyên tốt khi bạn ở trong một cơ sở mã thường không sử dụng mã lỗi. Tại sao? Bởi vì không ai sẽ nhớ lưu giá trị trả về và kiểm tra mã lỗi của bạn. Đó vẫn là một quy ước tốt khi bạn ở C.

OneOf

Bạn đang sử dụng một quy ước khác. Điều đó tốt miễn là bạn thiết lập quy ước và không chỉ đơn giản là chiến đấu với người khác. Thật khó hiểu khi có hai quy ước lỗi trong cùng một cơ sở mã. Nếu bằng cách nào đó bạn đã thoát khỏi tất cả các mã sử dụng quy ước khác thì hãy tiếp tục.

Tôi thích bản quy ước. Một trong những lời giải thích tốt nhất về nó tôi tìm thấy ở đây * :

nhập mô tả hình ảnh ở đây

Nhưng nhiều như tôi thích, tôi vẫn sẽ không trộn lẫn nó với các quy ước khác. Chọn một và gắn bó với nó. 1

1: Ý tôi là đừng khiến tôi phải suy nghĩ về nhiều hơn một quy ước cùng một lúc.


Suy nghĩ tiếp theo:

Từ cuộc thảo luận này, những gì tôi đang lấy đi hiện tại như sau:

  1. Nếu bạn mong đợi người gọi ngay lập tức bắt và xử lý ngoại lệ hầu hết thời gian và tiếp tục công việc của mình, có lẽ thông qua một đường dẫn khác, nó có thể là một phần của kiểu trả về. Tùy chọn hoặc OneOf có thể hữu ích ở đây.
  2. Nếu bạn mong muốn người gọi ngay lập tức không bắt được ngoại lệ hầu hết thời gian, hãy ném một ngoại lệ, để tiết kiệm sự tỉnh táo của việc chuyển nó thủ công lên ngăn xếp.
  3. Nếu bạn không chắc chắn người gọi ngay lập tức sẽ làm gì, có thể cung cấp cả hai, như Parse và TryPude.

Nó thực sự không đơn giản. Một trong những điều cơ bản bạn cần hiểu là số 0 là gì.

Tháng 5 còn lại bao nhiêu ngày? 0 (vì không phải là tháng 5. Đã là tháng 6 rồi).

Ngoại lệ là một cách để từ chối một giả định nhưng chúng không phải là cách duy nhất. Nếu bạn sử dụng ngoại lệ để từ chối giả định, bạn sẽ rời khỏi con đường hạnh phúc. Nhưng nếu bạn chọn các giá trị để gửi xuống đường dẫn hạnh phúc báo hiệu rằng mọi thứ không đơn giản như đã giả định thì bạn có thể ở trên đường đó miễn là nó có thể xử lý các giá trị đó. Đôi khi 0 đã được sử dụng để có nghĩa là một cái gì đó vì vậy bạn phải tìm một giá trị khác để ánh xạ ý tưởng từ chối giả định của bạn. Bạn có thể nhận ra ý tưởng này từ việc sử dụng nó trong đại số cũ tốt . Các đơn nguyên có thể giúp với điều đó nhưng không phải lúc nào cũng phải là một đơn nguyên.

Ví dụ 2 :

IList<int> ParseAllTheInts(String s) { ... }

Bạn có thể nghĩ ra bất kỳ lý do chính đáng nào mà nó phải được thiết kế để nó cố tình ném bất cứ thứ gì bao giờ không? Đoán xem bạn nhận được gì khi không có int có thể được phân tích cú pháp? Tôi thậm chí không cần nói với bạn.

Đó là một dấu hiệu của một cái tên hay. Xin lỗi nhưng TryPude không phải là ý tưởng của tôi về một cái tên hay.

Chúng ta thường tránh ném ngoại lệ vào việc không nhận được gì khi câu trả lời có thể nhiều hơn một điều cùng một lúc nhưng vì một số lý do nếu câu trả lời là một điều hoặc không có gì chúng ta bị ám ảnh khi khăng khăng rằng nó cho chúng ta một điều hoặc ném:

IList<Point> Intersection(Line a, Line b) { ... }

Các đường song song có thực sự cần phải gây ra một ngoại lệ ở đây không? Có thực sự xấu nếu danh sách này sẽ không bao giờ chứa nhiều hơn một điểm?

Có thể về mặt ngữ nghĩa, bạn không thể thực hiện điều đó. Nếu vậy, thật đáng tiếc. Nhưng có lẽ Monads, không có kích thước tùy ý như Listvậy, sẽ khiến bạn cảm thấy tốt hơn về nó.

Maybe<Point> Intersection(Line a, Line b) { ... }

Các Monads là những bộ sưu tập mục đích đặc biệt nhỏ được sử dụng theo những cách cụ thể mà không cần phải kiểm tra chúng. Chúng ta phải tìm cách đối phó với chúng bất kể chúng chứa gì. Bằng cách đó, con đường hạnh phúc vẫn đơn giản. Nếu bạn mở và kiểm tra mọi Monad bạn chạm vào thì bạn đang sử dụng sai.

Tôi biết, nó thật kỳ lạ. Nhưng đó là một công cụ mới (tốt, với chúng tôi). Vì vậy, cho nó một chút thời gian. Búa có ý nghĩa hơn khi bạn ngừng sử dụng chúng trên ốc vít.


Nếu bạn thưởng thức tôi, tôi muốn giải quyết nhận xét này:

Tại sao không có câu trả lời nào làm rõ rằng đơn vị Either không phải là mã lỗi và OneOf cũng không? Chúng khác nhau về cơ bản, và câu hỏi do đó dường như được dựa trên một sự hiểu lầm. (Mặc dù ở dạng đã được sửa đổi, đây vẫn là một câu hỏi hợp lệ.) - Konrad Rudolph ngày 4 tháng 6 `18 lúc 14:08

Điều này là hoàn toàn đúng. Monads gần với các bộ sưu tập hơn các trường hợp ngoại lệ, cờ hoặc mã lỗi. Họ làm những thùng chứa tốt cho những thứ như vậy khi được sử dụng một cách khôn ngoan.


9
Sẽ không phù hợp hơn nếu bản nhạc màu đỏ nằm dưới các hộp, do đó không chạy qua chúng?
Flater

4
Theo logic, bạn đúng. Tuy nhiên, mỗi hộp trong sơ đồ cụ thể này đại diện cho một hoạt động liên kết đơn âm. liên kết bao gồm (có thể tạo ra!) theo dõi màu đỏ. Tôi khuyên bạn nên xem trình bày đầy đủ. Thật thú vị và Scott là một diễn giả tuyệt vời.
Gusdor

7
Tôi nghĩ về các trường hợp ngoại lệ giống như một hướng dẫn COMEFROM hơn là GOTO, vì throwthực sự không biết / nói chúng ta sẽ nhảy đến đâu;)
Warbo

3
Không có gì sai với các quy ước trộn nếu bạn có thể thiết lập các ranh giới, đó là tất cả những gì đóng gói.
Robert Harvey

4
Toàn bộ phần đầu tiên của câu trả lời này đọc mạnh mẽ như thể bạn đã ngừng đọc câu hỏi sau câu trích dẫn. Câu hỏi thừa nhận rằng việc làm hỏng chương trình là tốt hơn so với việc chuyển sang hành vi không xác định: chúng tương phản với các sự cố thời gian chạy không phải với việc thực thi liên tục, không xác định, mà là với các lỗi thời gian biên dịch khiến bạn không thể xây dựng chương trình mà không xem xét lỗi tiềm ẩn và xử lý nó (có thể sẽ là một sự cố nếu không có gì khác phải làm, nhưng nó sẽ là một sự cố bởi vì bạn muốn nó xảy ra, không phải vì bạn quên nó).
KRyan

35

C # không phải là Haskell và bạn nên tuân theo sự đồng thuận chuyên môn của cộng đồng C #. Thay vào đó, nếu bạn cố gắng làm theo các thực tiễn của Haskell trong dự án C #, bạn sẽ xa lánh mọi người khác trong nhóm của mình và cuối cùng bạn có thể sẽ khám phá ra lý do tại sao cộng đồng C # làm những điều khác biệt. Một lý do lớn là C # không hỗ trợ thuận tiện cho các công đoàn bị phân biệt đối xử.

Tôi đã nghe nói rằng các ngoại lệ như luồng điều khiển được coi là một phản hạt nghiêm trọng,

Đây không phải là một sự thật được chấp nhận phổ biến. Sự lựa chọn trong mọi ngôn ngữ hỗ trợ các ngoại lệ là đưa ra một ngoại lệ (mà người gọi được tự do không xử lý) hoặc trả về một số giá trị ghép (mà người gọi PHẢI xử lý).

Việc truyền các điều kiện lỗi lên trên qua ngăn xếp cuộc gọi đòi hỏi phải có điều kiện ở mọi cấp độ, nhân đôi độ phức tạp theo chu kỳ của các phương thức đó và do đó nhân đôi số lượng các trường hợp thử nghiệm đơn vị. Trong các ứng dụng kinh doanh thông thường, nhiều trường hợp ngoại lệ nằm ngoài khả năng phục hồi và có thể được để lại ở mức cao nhất (ví dụ: điểm nhập dịch vụ của ứng dụng web).


9
Tuyên truyền đơn nguyên không đòi hỏi gì.
Basilevs

23
@Basilevs Nó yêu cầu hỗ trợ để truyền bá các đơn nguyên trong khi vẫn giữ mã có thể đọc được, điều mà C # không có. Bạn có thể nhận được các hướng dẫn if-return hoặc chuỗi lambdas.
Sebastian Redl

4
@SebastianRedl lambdas trông ổn trong C #.
Basilevs

@Basilevs Từ chế độ xem cú pháp có, nhưng tôi nghi ngờ từ quan điểm hiệu suất, chúng có thể ít hấp dẫn hơn.
Pharap

5
Trong C # hiện đại, bạn có thể sử dụng một phần các chức năng cục bộ để lấy lại tốc độ. Trong mọi trường hợp, hầu hết các phần mềm kinh doanh đều bị làm chậm đáng kể bởi IO so với các phần mềm khác.
Turksarama

26

Bạn đã đến C # tại một thời điểm thú vị. Cho đến gần đây, ngôn ngữ ngồi vững trong không gian lập trình bắt buộc. Sử dụng ngoại lệ để giao tiếp lỗi chắc chắn là tiêu chuẩn. Sử dụng mã trả về bị thiếu sự hỗ trợ ngôn ngữ (ví dụ: xung quanh các hiệp hội bị phân biệt đối xử và khớp mẫu). Khá hợp lý, hướng dẫn chính thức từ Microsoft là để tránh các mã trả về và sử dụng các ngoại lệ.

Luôn luôn có ngoại lệ cho điều này, dưới dạng TryXXXphương thức, sẽ trả về booleankết quả thành công và cung cấp kết quả giá trị thứ hai thông qua một outtham số. Chúng rất giống với các mẫu "thử" trong lập trình chức năng, lưu lại rằng kết quả đi qua một tham số out, thay vì thông qua Maybe<T>giá trị trả về.

Nhưng mọi thứ đang thay đổi. Lập trình chức năng đang trở nên phổ biến hơn và các ngôn ngữ như C # đang đáp ứng. C # 7.0 đã giới thiệu một số tính năng khớp mẫu cơ bản với ngôn ngữ; C # 8 sẽ giới thiệu nhiều hơn nữa, bao gồm biểu thức chuyển đổi, mẫu đệ quy, v.v ... Cùng với điều này, đã có sự phát triển trong "thư viện chức năng" cho C #, chẳng hạn như thư viện Succinc <T> của riêng tôi , cũng cung cấp hỗ trợ cho các hiệp hội bị phân biệt đối xử .

Hai điều này kết hợp có nghĩa là mã như sau đang dần phổ biến.

static Maybe<int> TryParseInt(string source) =>
    int.TryParse(source, out var result) ? new Some<int>(result) : none; 

public int GetNumberFromUser()
{
    Console.WriteLine("Please enter a number");
    while (true)
    {
        var userInput = Console.ReadLine();
        if (TryParseInt(userInput) is int value)
        {
            return value;
        }
        Console.WriteLine("That's not a valid number. Please try again");
    }
}

Hiện tại nó vẫn còn khá phù hợp, mặc dù trong hai năm qua tôi đã nhận thấy sự thay đổi rõ rệt từ sự thù địch chung giữa các nhà phát triển C # sang mã như vậy sang việc sử dụng các kỹ thuật như vậy ngày càng tăng.

Chúng tôi chưa có câu nói " KHÔNG trả lại mã lỗi." là cổ xưa và cần nghỉ hưu. Nhưng cuộc hành quân xuống đường hướng tới mục tiêu đó cũng đang được tiến hành. Vì vậy, hãy lựa chọn: gắn bó với những cách cũ để loại bỏ ngoại lệ với việc từ bỏ đồng tính nếu đó là cách bạn muốn làm; hoặc bắt đầu khám phá một thế giới mới, nơi các quy ước từ các ngôn ngữ chức năng đang trở nên phổ biến hơn trong C #.


22
Đây là lý do tại sao tôi ghét C # :) Họ tiếp tục thêm cách để nuôi mèo, và tất nhiên những cách cũ không bao giờ biến mất. Cuối cùng, không có quy ước cho bất cứ điều gì, một lập trình viên cầu toàn có thể ngồi hàng giờ để quyết định phương pháp nào là "đẹp hơn", và một lập trình viên mới phải học mọi thứ trước khi có thể đọc mã của người khác.
Alexanderr Dubinsky

24
@AleksandrDubinsky, Bạn gặp phải một vấn đề cốt lõi với ngôn ngữ lập trình nói chung, không chỉ C #. Ngôn ngữ mới được tạo ra, tinh gọn, tươi mới và đầy những ý tưởng hiện đại. Và rất ít người dân sử dụng chúng. Theo thời gian, họ có được người dùng và các tính năng mới, được bổ sung vào các tính năng cũ không thể xóa được vì nó sẽ phá vỡ mã hiện có. Sự phình to. Vì vậy, dân gian tạo ra những ngôn ngữ mới tinh gọn, mới mẻ và đầy những ý tưởng hiện đại ...
David Arno

7
@AleksandrDubinsky bạn không ghét nó khi họ thêm các tính năng bây giờ từ những năm 1960.
ctrl-alt-delor

6
@AleksandrDubinsky Vâng. Bởi vì Java đã cố gắng thêm một thứ một lần (generic), họ đã thất bại, giờ chúng tôi phải sống với mớ hỗn độn khủng khiếp dẫn đến việc họ đã học được bài học và bỏ thêm bất cứ điều gì :)
Agent_L

3
@Agent_L: Bây giờ bạn đã biết Java đã thêm java.util.Stream(một IEnumerable-alike nửa giống nhau) và lambdas, phải không? :)
cHao

5

Tôi nghĩ rằng hoàn toàn hợp lý khi sử dụng DU để trả về lỗi, nhưng sau đó tôi sẽ nói rằng khi tôi viết OneOf :) Nhưng tôi sẽ nói rằng dù sao đi nữa, vì đó là cách làm phổ biến trong F # vv (ví dụ như loại Kết quả, như được đề cập bởi những người khác ).

Tôi không nghĩ về các lỗi mà tôi thường trở lại là các tình huống đặc biệt, nhưng thay vào đó là bình thường, nhưng hy vọng hiếm khi gặp các tình huống có thể hoặc không cần phải xử lý, nhưng nên được mô hình hóa.

Đây là ví dụ từ trang dự án.

public OneOf<User, InvalidName, NameTaken> CreateUser(string username)
{
    if (!IsValid(username)) return new InvalidName();
    var user = _repo.FindByUsername(username);
    if(user != null) return new NameTaken();
    var user = new User(username);
    _repo.Save(user);
    return user;
}

Việc đưa ra các ngoại lệ cho các lỗi này có vẻ như quá mức cần thiết - chúng có chi phí hiệu năng cao, ngoài những nhược điểm của việc không rõ ràng trong chữ ký phương thức hoặc cung cấp kết hợp toàn diện.

Ở mức độ nào OneOf là thành ngữ trong c #, đó là một câu hỏi khác. Cú pháp hợp lệ và hợp lý trực quan. DU và lập trình định hướng đường sắt là những khái niệm nổi tiếng.

Tôi sẽ lưu Ngoại lệ cho những thứ chỉ ra thứ gì đó bị hỏng nếu có thể.


Những gì bạn đang nói là OneOfvề việc làm cho giá trị trả về trở nên phong phú hơn, giống như trả về một Nullable. Nó thay thế ngoại lệ cho các tình huống không phải là ngoại lệ để bắt đầu.
Alexanderr Dubinsky

Có - và cung cấp một cách dễ dàng để thực hiện khớp hoàn toàn trong trình gọi, không thể sử dụng phân cấp Kết quả dựa trên kế thừa.
mcintyre321

4

OneOftương đương với các ngoại lệ được kiểm tra trong Java. Có một số khác biệt:

  • OneOfkhông hoạt động với các phương thức tạo ra các phương thức được gọi là tác dụng phụ của chúng (vì chúng có thể được gọi một cách có ý nghĩa và kết quả đơn giản bị bỏ qua). Rõ ràng, tất cả chúng ta nên cố gắng sử dụng các hàm thuần túy, nhưng điều này không phải lúc nào cũng có thể.

  • OneOfkhông cung cấp thông tin về nơi xảy ra sự cố. Điều này là tốt vì nó tiết kiệm hiệu năng (ném ngoại lệ trong Java rất tốn kém vì điền vào dấu vết ngăn xếp và trong C # sẽ giống nhau) và buộc bạn phải cung cấp đủ thông tin trong chính thành viên lỗi OneOf. Điều này cũng tệ vì thông tin này có thể không đủ và bạn có thể khó tìm ra nguồn gốc vấn đề.

OneOf và ngoại lệ được kiểm tra có một "tính năng" quan trọng chung:

  • cả hai đều buộc bạn phải xử lý chúng ở mọi nơi trong ngăn xếp cuộc gọi

Điều này ngăn chặn nỗi sợ hãi của bạn

Trong C #, bỏ qua các ngoại lệ không tạo ra lỗi biên dịch và nếu chúng không bị bắt, chúng chỉ nổi bong bóng và làm hỏng chương trình của bạn.

nhưng như đã nói, nỗi sợ này là phi lý. Về cơ bản, thường có một vị trí duy nhất trong chương trình của bạn, nơi bạn cần nắm bắt mọi thứ (rất có thể bạn sẽ sử dụng một khung làm việc đó). Vì bạn và nhóm của bạn thường dành hàng tuần hoặc hàng năm để làm việc cho chương trình, bạn sẽ không quên điều đó chứ?

Ưu điểm lớn của thể bỏ qua trường hợp ngoại lệ là họ có thể bị xử lý bất cứ nơi nào bạn muốn xử lý chúng, chứ không phải sau đó ở khắp mọi nơi trong stack trace. Điều này giúp loại bỏ hàng tấn nồi hơi (chỉ cần nhìn vào một số mã Java khai báo "ném ...." hoặc gói ngoại lệ) và nó cũng làm cho mã của bạn ít bị lỗi hơn: Vì bất kỳ phương pháp nào cũng có thể ném, bạn cần phải biết về nó. May mắn thay, hành động đúng thường không làm gì cả , tức là để nó bong bóng đến một nơi mà nó có thể được xử lý hợp lý.


+1 nhưng tại sao OneOf không hoạt động với các tác dụng phụ? (Tôi đoán đó là vì nó sẽ vượt qua kiểm tra nếu bạn không gán giá trị trả về, nhưng như tôi không biết oneof, cũng không nhiều C # Tôi không chắc chắn -. Làm rõ sẽ cải thiện câu trả lời của bạn)
dcorking

1
Bạn có thể trả về thông tin lỗi với OneOf bằng cách làm cho đối tượng lỗi được trả về chứa thông tin hữu ích, ví dụ như OneOf<SomeSuccess, SomeErrorInfo>nơi SomeErrorInfocung cấp chi tiết về lỗi.
mcintyre321

@dcorking Đã chỉnh sửa. Chắc chắn, nếu bạn bỏ qua giá trị trả về, thì bạn không biết gì về vấn đề đã xảy ra. Nếu bạn làm điều này với một chức năng thuần túy, nó sẽ ổn (bạn chỉ lãng phí thời gian).
maaartinus

2
@ mcintyre321 Chắc chắn, bạn có thể thêm thông tin, nhưng bạn phải thực hiện thủ công và nó có thể bị lười biếng và quên. Tôi đoán, trong những trường hợp phức tạp hơn, dấu vết ngăn xếp sẽ hữu ích hơn.
maaartinus

4

Việc sử dụng OneOf thay vì các ngoại lệ để xử lý các ngoại lệ "thông thường" (như Tệp không tìm thấy, v.v.) là một cách tiếp cận hợp lý hay đó là một thực tiễn khủng khiếp?

Điều đáng chú ý là tôi đã nghe nói rằng các ngoại lệ như luồng điều khiển được coi là một phản hạt nghiêm trọng, vì vậy nếu "ngoại lệ" là thứ bạn thường xử lý mà không kết thúc chương trình, thì đó có phải là "luồng điều khiển" không?

Lỗi có một số kích thước cho chúng. Tôi xác định ba chiều: chúng có thể được ngăn chặn, tần suất chúng xảy ra và liệu chúng có thể được phục hồi hay không. Trong khi đó, báo hiệu lỗi chủ yếu chỉ có một chiều: quyết định mức độ nào để đánh bại người gọi qua đầu để buộc họ xử lý lỗi hoặc để lỗi "lặng lẽ" nổi lên như một ngoại lệ.

Các lỗi không thể phòng ngừa, thường xuyên và có thể phục hồi là những lỗi thực sự cần phải xử lý tại trang web cuộc gọi. Họ biện minh tốt nhất cho việc buộc người gọi phải đối đầu với họ. Trong Java, tôi làm cho chúng được kiểm tra các ngoại lệ và đạt được hiệu quả tương tự bằng cách tạo cho chúng Try*các phương thức hoặc trả về một liên minh bị phân biệt đối xử. Các hiệp hội phân biệt đối xử đặc biệt tốt khi một hàm có giá trị trả về. Họ là một phiên bản tinh tế hơn nhiều của sự trở lại null. Cú pháp của try/catchcác khối không tuyệt vời (quá nhiều dấu ngoặc), làm cho các lựa chọn thay thế trông tốt hơn. Ngoài ra, các ngoại lệ hơi chậm vì chúng ghi lại dấu vết ngăn xếp.

Các lỗi có thể phòng ngừa (không được ngăn chặn do lỗi / sơ suất của lập trình viên) và các lỗi không thể phục hồi hoạt động thực sự tốt như các ngoại lệ thông thường. Lỗi không thường xuyên cũng hoạt động tốt hơn như các trường hợp ngoại lệ thông thường vì một lập trình viên thường có thể cân nhắc nó không đáng để nỗ lực dự đoán (nó phụ thuộc vào mục đích của chương trình).

Một điểm quan trọng là trang web thường sử dụng sẽ xác định cách các chế độ lỗi của phương thức phù hợp với các kích thước này, cần lưu ý khi xem xét các điều sau.

Theo kinh nghiệm của tôi, việc buộc người gọi phải đối mặt với các điều kiện lỗi là ổn khi các lỗi không thể ngăn chặn được, thường xuyên và có thể phục hồi được, nhưng sẽ rất khó chịu khi người gọi buộc phải nhảy qua các vòng khi họ không cần hoặc không muốn . Điều này có thể là do người gọi biết lỗi sẽ không xảy ra hoặc vì họ chưa muốn làm cho mã trở nên linh hoạt. Trong Java, điều này giải thích sự giận dữ chống lại việc sử dụng thường xuyên các ngoại lệ được kiểm tra và trả về Tùy chọn (tương tự như Có thể và được giới thiệu gần đây trong bối cảnh nhiều tranh cãi). Khi nghi ngờ, hãy ném ngoại lệ và để người gọi quyết định cách họ muốn xử lý chúng.

Cuối cùng, hãy nhớ rằng điều quan trọng nhất về các điều kiện lỗi, vượt xa mọi sự xem xét khác như cách chúng được báo hiệu, là ghi lại chúng kỹ lưỡng .


2

Các câu trả lời khác đã thảo luận về các ngoại lệ so với mã lỗi một cách chi tiết, vì vậy tôi muốn thêm một quan điểm cụ thể khác cho câu hỏi:

OneOf không phải là mã lỗi, nó giống như một đơn nguyên

Cách nó được sử dụng, OneOf<Value, Error1, Error2>nó là một thùng chứa đại diện cho kết quả thực tế hoặc một số trạng thái lỗi. Nó giống như một Optional, ngoại trừ khi không có giá trị, nó có thể cung cấp thêm chi tiết tại sao lại như vậy.

Vấn đề chính với mã lỗi là bạn quên kiểm tra chúng. Nhưng ở đây, bạn thực sự không thể truy cập kết quả mà không kiểm tra lỗi.

Vấn đề duy nhất là khi bạn không quan tâm đến kết quả. Ví dụ từ tài liệu của OneOf đưa ra:

public OneOf<User, InvalidName, NameTaken> CreateUser(string username) { ... }

... Và sau đó họ tạo các phản hồi HTTP khác nhau cho từng kết quả, tốt hơn nhiều so với sử dụng ngoại lệ. Nhưng nếu bạn gọi phương thức như thế này.

public void CreateUserButtonClick(String username) {
    UserManager.CreateUser(string username)
}

sau đó bạn làm có một vấn đề và ngoại lệ sẽ là sự lựa chọn tốt hơn. Hãy so sánh đường sắt savevs save!.


1

Một điều bạn cần xem xét là mã trong C # thường không thuần túy. Trong một cơ sở mã chứa đầy các hiệu ứng phụ và với trình biên dịch không quan tâm đến những gì bạn làm với giá trị trả về của một số hàm, cách tiếp cận của bạn có thể khiến bạn đau buồn đáng kể. Ví dụ: nếu bạn có một phương thức xóa một tệp, bạn muốn đảm bảo ứng dụng thông báo khi phương thức không thành công, mặc dù không có lý do nào để kiểm tra bất kỳ mã lỗi nào "trên đường dẫn hạnh phúc". Đây là một sự khác biệt đáng kể so với các ngôn ngữ chức năng yêu cầu bạn bỏ qua một cách rõ ràng các giá trị trả về mà bạn không sử dụng. Trong C #, các giá trị trả về luôn bị bỏ qua ngầm (ngoại lệ duy nhất là thuộc tính getters IIRC).

Lý do tại sao MSDN nói với bạn "không trả lại mã lỗi" chính xác là thế này - không ai bắt bạn phải đọc mã lỗi. Trình biên dịch không giúp bạn chút nào. Tuy nhiên, nếu chức năng của bạn không có tác dụng phụ, bạn có thể sử dụng một cách an toàn như thế Either- vấn đề là ngay cả khi bạn bỏ qua kết quả lỗi (nếu có), bạn chỉ có thể làm điều đó nếu bạn không sử dụng "kết quả phù hợp" hoặc. Có một số cách bạn có thể thực hiện điều này - ví dụ: bạn chỉ có thể cho phép "đọc" giá trị bằng cách thông qua một đại biểu để xử lý thành công các trường hợp lỗi và bạn có thể dễ dàng kết hợp điều đó với các phương pháp như Lập trình định hướng đường sắt (nó chỉ hoạt động tốt trong C #).

int.TryParselà một nơi nào đó ở giữa. Đó là tinh khiết. Nó xác định giá trị của hai kết quả là gì - vì vậy bạn biết rằng nếu giá trị trả về là false, tham số đầu ra sẽ được đặt thành 0. Nó vẫn không ngăn bạn sử dụng tham số đầu ra mà không kiểm tra giá trị trả về, nhưng ít nhất bạn được đảm bảo kết quả là gì ngay cả khi chức năng bị lỗi.

Nhưng một điều hoàn toàn quan trọng ở đây là tính nhất quán . Trình biên dịch sẽ không cứu bạn nếu ai đó thay đổi chức năng đó để có tác dụng phụ. Hoặc để ném ngoại lệ. Vì vậy, trong khi phương pháp này hoàn toàn tốt trong C # (và tôi có sử dụng nó), bạn cần đảm bảo rằng tất cả mọi người trong nhóm của bạn hiểu và sử dụng nó . Nhiều bất biến cần thiết để lập trình chức năng hoạt động tốt không được thực thi bởi trình biên dịch C # và bạn cần đảm bảo rằng chúng đang được theo dõi chính mình. Bạn phải giữ cho mình ý thức về những điều bị phá vỡ nếu bạn không tuân theo các quy tắc mà Haskell thực thi theo mặc định - và đây là điều bạn cần lưu ý đối với bất kỳ mô hình chức năng nào bạn giới thiệu cho mã của mình.


0

Tuyên bố chính xác sẽ là Đừng sử dụng mã lỗi cho các trường hợp ngoại lệ! Và đừng sử dụng ngoại lệ cho những trường hợp không ngoại lệ.

Nếu có điều gì đó xảy ra mà mã của bạn không thể phục hồi (nghĩa là làm cho nó hoạt động), đó là một ngoại lệ. Mã ngay lập tức của bạn sẽ không làm gì với mã lỗi ngoại trừ việc đưa mã lên trên để đăng nhập hoặc có thể cung cấp mã cho người dùng theo một cách nào đó. Tuy nhiên, đây chính xác là những trường hợp ngoại lệ dành cho - chúng xử lý tất cả các bàn giao xung quanh và giúp phân tích những gì thực sự đã xảy ra.

Tuy nhiên, nếu có điều gì đó xảy ra, như đầu vào không hợp lệ mà bạn có thể xử lý, bằng cách định dạng lại tự động hoặc bằng cách yêu cầu người dùng sửa dữ liệu (và bạn nghĩ về trường hợp đó), đó không thực sự là ngoại lệ ở nơi đầu tiên - như bạn mong đợi nó Bạn có thể thoải mái xử lý những trường hợp đó bằng mã lỗi hoặc bất kỳ kiến ​​trúc nào khác. Chỉ không ngoại lệ.

(Lưu ý rằng tôi không có nhiều kinh nghiệm về C #, nhưng câu trả lời của tôi nên được giữ trong trường hợp chung dựa trên mức độ khái niệm của ngoại lệ là gì.)


7
Về cơ bản, tôi không đồng ý với tiền đề của câu trả lời này. Nếu các nhà thiết kế ngôn ngữ không có ý định ngoại lệ được sử dụng cho các lỗi có thể phục hồi, catchtừ khóa sẽ không tồn tại. Và vì chúng ta đang nói chuyện trong một bối cảnh C #, điều đáng chú ý là triết lý được tán thành ở đây không được tuân theo khung .NET; có lẽ ví dụ đơn giản nhất có thể tưởng tượng về "đầu vào không hợp lệ mà bạn có thể xử lý" là người dùng nhập một số không trong một trường nơi một số được mong đợi ... và int.Parseđưa ra một ngoại lệ.
Đánh dấu Amery

@MarkAmery Đối với int.Pude, không có gì nó có thể phục hồi cũng như bất cứ điều gì nó mong đợi. Mệnh đề bắt thường được sử dụng để tránh thất bại hoàn toàn từ chế độ xem bên ngoài, nó sẽ không yêu cầu người dùng cung cấp thông tin cơ sở dữ liệu mới hoặc tương tự, nó sẽ tránh được sự cố chương trình. Đó là một lỗi kỹ thuật không phải là một luồng điều khiển ứng dụng.
Frank Hopkins

3
Vấn đề liệu một hàm có tốt hơn để đưa ra một ngoại lệ hay không khi một điều kiện xảy ra tùy thuộc vào việc người gọi ngay lập tức của hàm có thể xử lý hữu ích tình trạng đó hay không. Trong trường hợp có thể, ném một ngoại lệ mà người gọi ngay lập tức phải nắm bắt đại diện cho công việc bổ sung cho tác giả của mã gọi và công việc bổ sung cho máy xử lý nó. Tuy nhiên, nếu người gọi sẽ không được chuẩn bị để xử lý một điều kiện, tuy nhiên, các trường hợp ngoại lệ sẽ giảm bớt sự cần thiết phải có người gọi mã rõ ràng cho nó.
supercat

@Darkwing "Bạn có thể thoải mái xử lý những trường hợp đó bằng mã lỗi hoặc bất kỳ kiến ​​trúc nào khác". Vâng, bạn có thể tự do làm điều đó. Tuy nhiên, bạn không nhận được bất kỳ sự hỗ trợ nào từ .NET để làm như vậy, vì bản thân .NET CL sử dụng các ngoại lệ thay vì mã lỗi. Vì vậy, bạn không chỉ trong nhiều trường hợp bị buộc phải bắt ngoại lệ .NET và trả lại mã lỗi tương ứng thay vì để bong bóng ngoại lệ xuất hiện, bạn còn buộc những người khác trong nhóm của bạn phải sử dụng khái niệm xử lý lỗi khác mà họ phải sử dụng song song với các ngoại lệ, khái niệm xử lý lỗi tiêu chuẩn.
Aleksander

-2

Đó là một 'Ý tưởng khủng khiếp'.

Điều đó thật tồi tệ bởi vì, ít nhất là trong .net, bạn không có một danh sách đầy đủ các ngoại lệ có thể xảy ra. Bất kỳ mã nào có thể có thể ném bất kỳ ngoại lệ. Đặc biệt là nếu bạn đang thực hiện OOP và có thể đang gọi các phương thức được ghi đè trên một loại phụ thay vì loại khai báo

Vì vậy, bạn sẽ phải thay đổi tất cả các phương thức và hàm thành OneOf<Exception, ReturnType>, sau đó nếu bạn muốn xử lý các loại ngoại lệ khác nhau, bạn sẽ phải kiểm tra loại If(exception is FileNotFoundException)vv

try catch tương đương với OneOf<Exception, ReturnType>

Biên tập ----

Tôi biết rằng bạn đề nghị quay lại OneOf<ErrorEnum, ReturnType>nhưng tôi cảm thấy đây là một sự khác biệt không có sự khác biệt.

Bạn đang kết hợp mã trả lại, ngoại lệ dự kiến ​​và phân nhánh bằng ngoại lệ. Nhưng hiệu quả tổng thể là như nhau.


2
Ngoại lệ 'Bình thường' cũng là một ý tưởng tồi tệ. manh mối có tên
Ewan

4
Tôi không đề xuất trả lại Exceptions.
Clinton

bạn có đang đề xuất rằng giải pháp thay thế cho một trong số đó là kiểm soát luồng thông qua các ngoại lệ
Ewan
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.