Các phương pháp hay nhất: loại bỏ các ngoại lệ khỏi thuộc tính


110

Khi nào thì thích hợp để ném một ngoại lệ từ bên trong một thuộc tính getter hoặc setter? Khi nào thì không thích hợp? Tại sao? Các liên kết đến các tài liệu bên ngoài về chủ đề này sẽ hữu ích ... Google xuất hiện rất ít một cách đáng ngạc nhiên.




1
Tôi đã đọc cả hai câu hỏi đó, nhưng không trả lời câu hỏi này hoàn toàn IMO.
Jon Seigel

Bất cứ khi nào cần thiết. Cả câu hỏi và câu trả lời trước đây đều cho thấy rằng việc nêu ra các ngoại lệ từ getter hoặc setter là được phép và đánh giá cao, vì vậy bạn có thể đơn giản "trở nên thông minh".
Lex Li

Câu trả lời:


134

Microsoft có các đề xuất về cách thiết kế thuộc tính tại http://msdn.microsoft.com/en-us/library/ms229006.aspx

Về cơ bản, họ khuyến nghị rằng những người nhận tài sản là những người truy cập nhẹ và luôn an toàn khi gọi. Họ khuyên bạn nên thiết kế lại getters thành các phương pháp nếu ngoại lệ là thứ bạn cần phải ném. Đối với những người thiết lập, họ chỉ ra rằng ngoại lệ là một chiến lược xử lý lỗi phù hợp và có thể chấp nhận được.

Đối với trình lập chỉ mục, Microsoft chỉ ra rằng có thể chấp nhận được cả getters và setters để ném ngoại lệ. Và trên thực tế, nhiều trình chỉ mục trong thư viện .NET thực hiện điều này. Ngoại lệ phổ biến nhất là ArgumentOutOfRangeException.

Có một số lý do khá tốt tại sao bạn không muốn đưa ra các ngoại lệ trong bộ nhận tài sản:

  • Bởi vì các thuộc tính "có vẻ" là các trường, không phải lúc nào cũng rõ ràng rằng chúng có thể ném một ngoại lệ (theo thiết kế); trong khi với các phương thức, các lập trình viên được đào tạo để mong đợi và điều tra xem các ngoại lệ có phải là hệ quả mong đợi của việc gọi phương thức hay không.
  • Getters được sử dụng bởi rất nhiều cơ sở hạ tầng .NET, như trình tuần tự hóa và databinding (trong WinForms và WPF chẳng hạn) - việc xử lý các ngoại lệ trong các ngữ cảnh như vậy có thể nhanh chóng trở thành vấn đề.
  • Trình gỡ lỗi được đánh giá tự động bởi trình gỡ rối khi bạn xem hoặc kiểm tra một đối tượng. Một ngoại lệ ở đây có thể gây nhầm lẫn và làm chậm nỗ lực gỡ lỗi của bạn. Cũng không nên thực hiện các thao tác tốn kém khác trong các thuộc tính (như truy cập cơ sở dữ liệu) vì những lý do tương tự.
  • Các thuộc tính thường được sử dụng trong một quy ước chuỗi: obj.PropA.AnotherProp.YetAnother- với kiểu cú pháp này, việc quyết định vị trí đưa các câu lệnh bắt ngoại lệ trở nên khó khăn.

Một lưu ý nhỏ, cần lưu ý rằng chỉ vì một thuộc tính không được thiết kế để tạo ra một ngoại lệ, điều đó không có nghĩa là nó sẽ không; nó có thể dễ dàng là mã gọi điện. Ngay cả hành động đơn giản là cấp phát một đối tượng mới (như một chuỗi) có thể dẫn đến ngoại lệ. Bạn nên luôn viết mã của mình một cách phòng thủ và mong đợi các ngoại lệ từ bất kỳ thứ gì bạn gọi ra.


41
Nếu bạn đang gặp phải một ngoại lệ nghiêm trọng như "hết bộ nhớ", thì việc bạn nhận được ngoại lệ trong một thuộc tính hay ở một nơi nào khác hầu như không quan trọng. Nếu bạn không có nó trong thuộc tính, bạn sẽ chỉ nhận được nó một vài nano giây sau đó từ thứ tiếp theo phân bổ bộ nhớ. Câu hỏi không phải là "một thuộc tính có thể ném một ngoại lệ không?" Hầu như tất cả các mã có thể ném một ngoại lệ do một điều kiện nghiêm trọng. Câu hỏi đặt ra là liệu một tài sản theo thiết kế có nên đưa ra một ngoại lệ như một phần của hợp đồng cụ thể của nó hay không.
Eric Lippert 28/09/09

1
Tôi không chắc mình hiểu lập luận trong câu trả lời này. Đối với xample, liên quan đến databinding - cả WinForms và WPF đều được viết cụ thể để xử lý đúng cách các ngoại lệ do thuộc tính ném ra và coi chúng là lỗi xác thực - đó là cách hoàn toàn tốt (một số người thậm chí tin rằng đó là cách tốt nhất) để cung cấp xác thực mô hình miền .
Pavel Minaev 28/09/09

6
@Pavel - mặc dù cả WinForms và WPF đều có thể khôi phục một cách duyên dáng khỏi các ngoại lệ trong trình truy cập thuộc tính, nhưng không phải lúc nào cũng dễ dàng xác định và khôi phục từ các lỗi như vậy. Trong một số trường hợp nhất định, (chẳng hạn như khi trong WPF, bộ thiết lập Mẫu điều khiển ném một ngoại lệ), ngoại lệ sẽ bị nuốt âm thầm. Điều này có thể dẫn đến các phiên gỡ lỗi khó khăn nếu bạn chưa từng gặp những trường hợp như vậy trước đây.
LBushkin

1
@Steven: Vậy thì lớp đó được sử dụng nhiều như thế nào đối với bạn trong trường hợp ngoại lệ? Nếu sau đó bạn phải viết mã phòng thủ để xử lý tất cả các trường hợp ngoại lệ đó chỉ vì một lần thất bại và có lẽ cung cấp các giá trị mặc định phù hợp, tại sao không cung cấp các giá trị mặc định đó trong lần bắt của bạn? Ngoài ra, nếu các ngoại lệ thuộc tính được ném cho người dùng, tại sao không chỉ ném "InvalidArgumentException" ban đầu hoặc tương tự để họ có thể cung cấp tệp cài đặt bị thiếu?
Zhaph - Ben Duguid 28/09/09

6
Có một lý do tại sao đây là hướng dẫn chứ không phải quy tắc; không có hướng dẫn nào bao gồm tất cả các trường hợp cạnh điên rồ. Tôi có thể đã tự tạo các phương thức này chứ không phải thuộc tính, nhưng đó là một cuộc gọi phán xét.
Eric Lippert 28/09/09

34

Không có gì sai khi ném các ngoại lệ từ những người thiết lập. Rốt cuộc, cách nào tốt hơn để chỉ ra rằng giá trị không hợp lệ cho một thuộc tính nhất định?

Đối với getters, nó thường bị coi là khó hiểu, và điều đó có thể được giải thích khá dễ dàng: nói chung, một property getter báo cáo trạng thái hiện tại của một đối tượng; do đó, trường hợp duy nhất hợp lý để một getter ném là khi trạng thái không hợp lệ. Nhưng nó cũng thường được coi là một ý tưởng hay để thiết kế các lớp của bạn sao cho ban đầu không thể lấy một đối tượng không hợp lệ hoặc đưa nó vào trạng thái không hợp lệ thông qua các phương tiện thông thường (tức là luôn đảm bảo khởi tạo đầy đủ trong các hàm tạo, và thử làm cho các phương thức ngoại lệ an toàn đối với tính hợp lệ của trạng thái và các bất biến của lớp). Miễn là bạn tuân theo quy tắc đó, người nhận tài sản của bạn sẽ không bao giờ rơi vào tình huống họ phải báo cáo trạng thái không hợp lệ, và do đó không bao giờ ném.

Có một ngoại lệ mà tôi biết, và nó thực sự là một ngoại lệ khá quan trọng: bất kỳ đối tượng nào đang triển khai IDisposable. Disposeđược dự định cụ thể như một cách để đưa đối tượng vào trạng thái không hợp lệ và thậm chí còn có một lớp ngoại lệ đặc biệt ObjectDisposedException, được sử dụng trong trường hợp đó. Hoàn toàn bình thường khi ném ObjectDisposedExceptiontừ bất kỳ thành viên nào trong lớp, bao gồm cả những người nhận thuộc tính (và loại trừ Disposechính nó), sau khi đối tượng đã được xử lý.


4
Cảm ơn Pavel. Câu trả lời này đi vào 'lý do tại sao' thay vì chỉ đơn giản nói lại rằng không phải là một ý kiến ​​hay nếu ném một ngoại lệ khỏi các thuộc tính.
SolutionYogi

1
Tôi không thích quan điểm cho rằng tuyệt đối tất cả các thành viên của một IDisposablephải trở nên vô dụng sau khi a Dispose. Nếu việc gọi một thành viên sẽ yêu cầu sử dụng một tài nguyên Disposekhông có sẵn (ví dụ: thành viên sẽ đọc dữ liệu từ một luồng đã bị đóng), thành viên nên ném ObjectDisposedExceptionthay vì rò rỉ ArgumentException, ví dụ , nhưng nếu một thành viên có một biểu mẫu với các thuộc tính đại diện cho giá trị trong các lĩnh vực nhất định, nó sẽ có vẻ xa hữu ích hơn để cho phép tài sản đó phải được đọc sau khi xử lý (năng suất các giá trị đánh máy cuối cùng) so với yêu cầu ...
supercat

1
... Disposeđược hoãn lại cho đến khi tất cả các thuộc tính như vậy đã được đọc. Trong một số trường hợp, một luồng có thể sử dụng chặn các lần đọc trên một đối tượng trong khi một luồng khác đóng nó và nơi dữ liệu có thể đến bất kỳ lúc nào trước đó Dispose, có thể hữu ích nếu bạn Disposecắt dữ liệu đến nhưng cho phép đọc dữ liệu đã nhận trước đó. Người ta không nên tạo ra sự phân biệt giả tạo giữa CloseDisposetrong những tình huống mà không cần phải tồn tại.
supercat

Hiểu lý do của quy tắc cho phép bạn biết khi nào nên phá vỡ quy tắc (Raymond Chen). Trong trường hợp này, chúng ta có thể thấy rằng nếu có lỗi không thể khôi phục thuộc bất kỳ loại nào, bạn không nên ẩn nó trong getter, vì trong những trường hợp như vậy, ứng dụng cần thoát ra càng sớm càng tốt.
Ben

Điểm tôi đang cố gắng đưa ra là các trình nhận thuộc tính của bạn nói chung không nên chứa logic cho phép các lỗi không thể khôi phục được. Nếu đúng như vậy, có thể tốt hơn là Get...thay vào đó, nó là một phương pháp. Một ngoại lệ ở đây là khi bạn phải triển khai một giao diện hiện có yêu cầu bạn cung cấp một thuộc tính.
Pavel Minaev

24

Nó hầu như không bao giờ thích hợp trên một getter, và đôi khi thích hợp trên một setter.

Tài nguyên tốt nhất cho những loại câu hỏi này là "Hướng dẫn thiết kế khung" của Cwalina và Abrams; nó có sẵn dưới dạng sách đóng gáy và các phần lớn của nó cũng có sẵn trực tuyến.

Từ phần 5.2: Thiết kế tài sản

TRÁNH ném các ngoại lệ từ người nhận tài sản. Người nhận tài sản phải là những hoạt động đơn giản và không nên có điều kiện tiên quyết. Nếu một getter có thể ném một ngoại lệ, nó có lẽ nên được thiết kế lại để trở thành một phương thức. Lưu ý rằng quy tắc này không áp dụng cho các trình chỉ mục, nơi chúng tôi mong đợi các trường hợp ngoại lệ là kết quả của việc xác thực các đối số.

Lưu ý rằng hướng dẫn này chỉ áp dụng cho người nhận tài sản. Có thể đưa ra một ngoại lệ trong một bộ thiết lập thuộc tính.


2
Mặc dù (nói chung) tôi đồng ý với các hướng dẫn như vậy, nhưng tôi nghĩ sẽ hữu ích nếu cung cấp thêm một số thông tin chi tiết về lý do tại sao chúng nên được tuân thủ - và loại hậu quả nào có thể phát sinh khi chúng bị bỏ qua.
LBushkin

3
Điều này liên quan như thế nào đến các đồ vật dùng một lần và hướng dẫn mà bạn nên xem xét ném ObjectDisposedExceptionkhi đồ vật đã được Dispose()gọi và một thứ gì đó sau đó yêu cầu giá trị thuộc tính? Có vẻ như hướng dẫn nên là "tránh ném các ngoại lệ từ bộ lấy thuộc tính, trừ khi đối tượng đã được xử lý trong trường hợp đó bạn nên xem xét ném một ObjectDisposedExcpetion".
Scott Dorman

4
Thiết kế là nghệ thuật và khoa học nhằm tìm ra những thỏa hiệp hợp lý khi đối mặt với những yêu cầu trái ngược nhau. Dù bằng cách nào cũng có vẻ là một thỏa hiệp hợp lý; Tôi sẽ không ngạc nhiên khi có một đồ vật được xử lý ném vào tài sản; Tôi cũng không ngạc nhiên nếu nó không xảy ra. Vì sử dụng một đối tượng được xử lý là một thực hành lập trình tồi tệ, sẽ không khôn ngoan nếu có bất kỳ kỳ vọng nào.
Eric Lippert 28/09/09

1
Một tình huống khác hoàn toàn hợp lệ để ném các ngoại lệ từ bên trong getters là khi một đối tượng đang sử dụng các bất biến của lớp để xác thực trạng thái bên trong của nó, điều này cần được kiểm tra bất cứ khi nào thực hiện truy cập công khai, bất kể đó là phương thức hay thuộc tính
Trap

2

Một cách tiếp cận hay đối với Ngoại lệ là sử dụng chúng để ghi lại mã cho chính bạn và các nhà phát triển khác như sau:

Các trường hợp ngoại lệ nên dành cho các trạng thái chương trình đặc biệt. Điều này có nghĩa là bạn có thể viết chúng ở bất cứ đâu bạn muốn!

Một lý do mà bạn có thể muốn đưa chúng vào getters là ghi lại API của một lớp - nếu phần mềm ném ra một ngoại lệ ngay khi lập trình viên cố gắng sử dụng sai thì họ sẽ không sử dụng sai! Ví dụ: nếu bạn có xác thực trong quá trình đọc dữ liệu, bạn có thể không tiếp tục và truy cập kết quả của quá trình nếu có lỗi nghiêm trọng trong dữ liệu. Trong trường hợp này, bạn có thể muốn thực hiện ném đầu ra nếu có lỗi để đảm bảo rằng một lập trình viên khác kiểm tra điều kiện này.

Chúng là một cách ghi lại các giả định và ranh giới của một hệ thống con / phương pháp / bất cứ thứ gì. Trong trường hợp chung, họ không nên bị bắt! Điều này cũng là do chúng không bao giờ được ném nếu hệ thống đang hoạt động cùng nhau theo cách mong đợi: Nếu một ngoại lệ xảy ra, nó cho thấy rằng các giả định của một đoạn mã không được đáp ứng - ví dụ: nó không tương tác với thế giới xung quanh theo cách nó được dự định ban đầu. Nếu bạn bắt gặp một ngoại lệ được viết cho mục đích này, điều đó có thể có nghĩa là hệ thống đã đi vào trạng thái không thể đoán trước / không nhất quán - điều này cuối cùng có thể dẫn đến sự cố hoặc hỏng dữ liệu hoặc tương tự có khả năng khó phát hiện / gỡ lỗi hơn nhiều.

Thông báo ngoại lệ là một cách báo cáo lỗi rất thô sơ - chúng không thể được thu thập một cách tổng thể và chỉ thực sự chứa một chuỗi. Điều này khiến chúng không phù hợp để báo cáo các vấn đề trong dữ liệu đầu vào. Trong quá trình chạy bình thường, bản thân hệ thống sẽ không rơi vào trạng thái lỗi. Do đó, các thông điệp trong chúng nên được thiết kế cho các lập trình viên chứ không phải cho người dùng - những thứ sai trong dữ liệu đầu vào có thể được phát hiện và chuyển tiếp đến người dùng ở các định dạng phù hợp hơn (tùy chỉnh).

Ngoại lệ (haha!) Đối với quy tắc này là những thứ như IO, nơi các ngoại lệ không nằm trong tầm kiểm soát của bạn và không thể kiểm tra trước.


2
Làm thế nào mà câu trả lời hợp lệ và có liên quan này lại bị phản đối? Không nên có chính trị trong StackOverflow và nếu câu trả lời này dường như không đạt được xu hướng tăng trưởng, hãy thêm nhận xét vào hiệu ứng đó. Từ chối là cho các câu trả lời không liên quan hoặc sai.
debater

1

Tất cả điều này được ghi lại trong MSDN (như được liên kết trong các câu trả lời khác) nhưng đây là quy tắc chung ...

Trong setter, nếu thuộc tính của bạn nên được xác thực ở trên và ngoài loại. Ví dụ: một thuộc tính có tên PhoneNumber có lẽ phải có xác thực regex và sẽ gây ra lỗi nếu định dạng không hợp lệ.

Đối với getters, có thể khi giá trị là null, nhưng rất có thể đó là thứ bạn sẽ muốn xử lý trên mã gọi (theo hướng dẫn thiết kế).



0

Đây là một câu hỏi rất phức tạp và câu trả lời phụ thuộc vào cách đối tượng của bạn được sử dụng. Theo quy tắc thông thường, các bộ nhận và thiết lập thuộc tính là "liên kết muộn" không nên đưa ra các ngoại lệ, trong khi các thuộc tính chỉ có "liên kết sớm" sẽ ném các ngoại lệ khi có nhu cầu. BTW, công cụ phân tích mã của Microsoft đang định nghĩa việc sử dụng các thuộc tính quá hẹp theo quan điểm của tôi.

"late binding" có nghĩa là các thuộc tính được tìm thấy thông qua phản xạ. Ví dụ: thuộc tính Serializable "được sử dụng để tuần tự hóa / deserialize một đối tượng thông qua các thuộc tính của nó. Việc ném một ngoại lệ trong loại tình huống này sẽ phá vỡ mọi thứ theo một cách thảm khốc và không phải là một cách tốt để sử dụng ngoại lệ để tạo mã mạnh hơn.

"early binding" có nghĩa là trình biên dịch sử dụng thuộc tính được ràng buộc trong mã. Ví dụ: khi một số mã mà bạn viết tham chiếu đến một bộ lấy thuộc tính. Trong trường hợp này, bạn có thể đưa ra các ngoại lệ khi chúng có ý nghĩa.

Một đối tượng có các thuộc tính bên trong có trạng thái được xác định bởi các giá trị của các thuộc tính đó. Các thuộc tính thể hiện thuộc tính nhận biết và nhạy cảm với trạng thái bên trong của đối tượng không nên được sử dụng để ràng buộc muộn. Ví dụ, giả sử bạn có một đối tượng phải được mở, truy cập, sau đó đóng. Trong trường hợp này, việc truy cập các thuộc tính mà không gọi open trước sẽ dẫn đến một ngoại lệ. Giả sử, trong trường hợp này, chúng tôi không ném một ngoại lệ và chúng tôi cho phép mã truy cập vào một giá trị mà không ném một ngoại lệ? Mã sẽ có vẻ hạnh phúc mặc dù nó có một giá trị từ một getter không có ý nghĩa. Bây giờ chúng ta đã đặt đoạn mã được gọi là getter vào một tình huống xấu vì nó phải biết cách kiểm tra giá trị để xem nó có phải là không. Điều này có nghĩa là mã phải đưa ra các giả định về giá trị mà nó nhận được từ thuộc tính getter để xác thực nó. Đây là cách mã xấu được viết.


0

Tôi đã có mã này mà tôi không chắc chắn về ngoại lệ nào để ném.

public Person
{
    public string Name { get; set; }
    public boolean HasPets { get; set; }
}

public void Foo(Person person)
{
    if (person.Name == null) {
        throw new Exception("Name of person is null.");
        // I was unsure of which exception to throw here.
    }

    Console.WriteLine("Name is: " + person.Name);
}

Tôi đã ngăn mô hình có thuộc tính là null ngay từ đầu bằng cách buộc nó như một đối số trong hàm tạo.

public Person
{
    public Person(string name)
    {
        if (name == null) {
            throw new ArgumentNullException(nameof(name));
        }
        Name = name;
    }

    public string Name { get; private set; }
    public boolean HasPets { get; set; }
}

public void Foo(Person person)
{
    Console.WriteLine("Name is: " + person.Name);
}
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.