Xác thực tham số của nhà xây dựng trong C # - Thực tiễn tốt nhất


34

Thực hành tốt nhất để xác nhận tham số constructor là gì?

Giả sử một chút đơn giản của C #:

public class MyClass
{
    public MyClass(string text)
    {
        if (String.IsNullOrEmpty(text))
            throw new ArgumentException("Text cannot be empty");

        // continue with normal construction
    }
}

Nó có thể được chấp nhận để ném một ngoại lệ?

Sự thay thế tôi gặp phải là xác nhận trước, trước khi bắt đầu:

public class CallingClass
{
    public MyClass MakeMyClass(string text)
    {
        if (String.IsNullOrEmpty(text))
        {
            MessageBox.Show("Text cannot be empty");
            return null;
        }
        else
        {
            return new MyClass(text);
        }
    }
}

10
"chấp nhận để ném một ngoại lệ?" Cái gì thay thế?
S.Lott

10
@ S.Lott: Không làm gì cả. Đặt giá trị mặc định. In một mesasge đến bàn điều khiển / logfile. Rung chuông. Nháy mắt
Thất vọngWithFormsDesigner

3
@FrustratedWithFormsDesigner: "Không làm gì cả?" Làm thế nào các ràng buộc "văn bản không trống" sẽ được thỏa mãn? "Đặt giá trị mặc định"? Giá trị đối số văn bản sẽ được sử dụng như thế nào? Các ý tưởng khác đều ổn, nhưng hai ý tưởng đó sẽ dẫn đến một đối tượng ở trạng thái vi phạm các ràng buộc đối với lớp này.
S.Lott

1
@ S.Lott Vì sợ lặp lại chính mình, tôi đã mở ra khả năng có một số cách tiếp cận khác, mà tôi không biết. Tôi tin rằng tôi không phải tất cả đều biết, chắc chắn tôi biết nhiều.
MPelletier

3
@MPelletier, xác nhận các tham số trước thời hạn sẽ kết thúc vi phạm DRY. (Đừng lặp lại chính mình) Vì bạn sẽ cần sao chép xác thực trước đó vào bất kỳ mã nào cố gắng khởi tạo lớp này.
CaffGeek

Câu trả lời:


26

Tôi có xu hướng thực hiện tất cả các xác nhận của tôi trong hàm tạo. Đây là điều bắt buộc bởi vì tôi hầu như luôn tạo ra những vật thể bất biến. Đối với trường hợp cụ thể của bạn tôi nghĩ rằng điều này là chấp nhận được.

if (string.IsNullOrEmpty(text))
    throw new ArgumentException("message", "text");

Nếu bạn đang sử dụng .NET 4, bạn có thể làm điều này. Tất nhiên điều này phụ thuộc vào việc bạn coi một chuỗi chỉ chứa khoảng trắng là không hợp lệ.

if (string.IsNullOrWhiteSpace(text))
    throw new ArgumentException("message", "text");

Tôi không chắc "trường hợp cụ thể" của anh ấy đủ cụ thể để đưa ra lời khuyên cụ thể như vậy. Chúng tôi không biết liệu nó có ý nghĩa trong bối cảnh này để ném một ngoại lệ hay đơn giản là cho phép đối tượng tồn tại bằng mọi cách.
Thất vọngWithFormsDesigner

@FrustratedWithFormsDesigner - Tôi cho rằng bạn đã bỏ phiếu cho tôi vì điều này? Tất nhiên bạn đúng rằng tôi đang ngoại suy nhưng rõ ràng đối tượng lý thuyết này phải không hợp lệ nếu văn bản đầu vào trống. Bạn có thực sự muốn gọi Initđể nhận một số loại mã lỗi khi một ngoại lệ cũng hoạt động tốt và buộc đối tượng của bạn luôn duy trì trạng thái hợp lệ?
ChaosPandion

2
Tôi có xu hướng chia trường hợp đầu tiên đó thành hai trường hợp ngoại lệ riêng biệt: một trường hợp kiểm tra tính vô hiệu và ném một ArgumentNullExceptionvà trường hợp khác kiểm tra trống và ném một ArgumentException. Cho phép người gọi trở nên kén chọn hơn nếu họ muốn xử lý ngoại lệ.
Jesse C. Choper 23/211

1
@Jlie - Tôi đã sử dụng kỹ thuật đó nhưng tôi vẫn đang nói về tính hữu dụng chung của nó.
ChaosPandion

3
+1 cho các đối tượng bất biến! Tôi cũng sử dụng chúng bất cứ khi nào có thể (cá nhân tôi thấy hầu như luôn luôn).

22

Rất nhiều người nói rằng các nhà xây dựng không nên ném ngoại lệ. KyleG trên trang này , ví dụ, làm điều đó. Thành thật mà nói, tôi không thể nghĩ ra lý do tại sao không.

Trong C ++, ném ngoại lệ từ hàm tạo là một ý tưởng tồi, bởi vì nó để lại cho bạn bộ nhớ được phân bổ có chứa một đối tượng chưa được khởi tạo mà bạn không có tham chiếu đến (ví dụ: đó là rò rỉ bộ nhớ cổ điển). Đó có lẽ là nơi mà sự kỳ thị xuất phát - một nhóm các nhà phát triển C ++ trường học cũ đã thực hiện việc học C # của họ và chỉ áp dụng những gì họ biết từ C ++ vào nó. Ngược lại, trong Objective-C, Apple đã tách bước phân bổ khỏi bước khởi tạo, do đó, các nhà xây dựng trong ngôn ngữ đó có thể đưa ra các ngoại lệ.

C # không thể rò rỉ bộ nhớ từ một cuộc gọi không thành công đến một nhà xây dựng. Thậm chí một số lớp trong .NET framework sẽ đưa ra các ngoại lệ trong các hàm tạo của chúng.


Bạn sẽ xù lông với bạn nhận xét C ++. :)
ChaosPandion

5
Ehh, C ++ là một ngôn ngữ tuyệt vời, nhưng một trong những thú cưng của tôi là những người học tốt một ngôn ngữ và sau đó viết như vậy mọi lúc . C # không phải là C ++.
Kiến

10
Vẫn có thể nguy hiểm khi ném ngoại lệ từ một ctor trong C # vì ctor có thể đã phân bổ các tài nguyên không được quản lý mà sau đó sẽ không bao giờ được xử lý. Và bộ hoàn thiện cần phải được viết để phòng thủ trước kịch bản mà một đối tượng được xây dựng không hoàn chỉnh được hoàn thiện. Nhưng những kịch bản này là tương đối hiếm. Một bài viết sắp tới trên Trung tâm Dev của C # sẽ đề cập đến các kịch bản này và cách bảo vệ mã xung quanh chúng, vì vậy hãy theo dõi không gian đó để biết chi tiết.
Eric Lippert

6
hả ném một ngoại lệ từ ctor là điều duy nhất bạn có thể làm trong c ++ nếu bạn không thể xây dựng đối tượng và không có lý do gì điều này phải gây rò rỉ bộ nhớ (mặc dù rõ ràng là như vậy).
jk.

@jk: Hmm, bạn nói đúng! Mặc dù có vẻ như một giải pháp thay thế là gắn cờ các đối tượng xấu là "thây ma", mà STL dường như thực hiện thay cho việc ném ngoại lệ: yosefk.com/c++fqa/exceptions.html#fqa-17.2 Giải pháp của Objective-C gọn gàng hơn nhiều.
Kiến

12

Ném một ngoại lệ IFF, lớp không thể được đưa vào trạng thái nhất quán liên quan đến việc sử dụng ngữ nghĩa của nó. Nếu không thì không. KHÔNG BAO GIỜ cho phép một đối tượng tồn tại ở trạng thái không nhất quán. Điều này bao gồm việc không cung cấp các hàm tạo hoàn chỉnh (như có một hàm tạo trống + khởi tạo () trước khi đối tượng của bạn thực sự được xây dựng hoàn chỉnh) ... CHỈ NÓI KHÔNG!

Trong một nhúm, tất cả mọi người làm điều đó. Tôi đã làm điều đó vào một ngày khác cho một đối tượng được sử dụng rất hẹp trong phạm vi chặt chẽ. Một ngày nào đó trên đường, tôi hoặc người khác có thể sẽ trả giá cho sự trượt dốc đó trong thực tế.

Tôi nên lưu ý rằng bởi "constructor", ý tôi là thứ mà khách hàng gọi để xây dựng đối tượng. Điều đó có thể dễ dàng trở thành một cái gì đó khác hơn là một công trình thực tế có tên là "Nhà xây dựng". Ví dụ: một cái gì đó như thế này trong C ++ sẽ không vi phạm nguyên tắc IMNSHO:

struct funky_object
{
  ...
private:
  funky_object();
  bool initialize(std::string);

  friend boost::optional<funky_object> build_funky(std::string);
};
boost::optional<funky_object> build_funky(std::string str)
{
  funky_object fo;
  if (fo.initialize(str)) return fo;
  return boost::optional<funky_object>();
}

Vì cách duy nhất để tạo một funky_objectlà bằng cách gọi build_funkynguyên tắc không bao giờ cho phép một đối tượng không hợp lệ tồn tại vẫn còn nguyên vẹn mặc dù "Trình xây dựng" thực tế không hoàn thành công việc.

Đó là rất nhiều công việc làm thêm mặc dù cho lợi ích đáng ngờ (thậm chí có thể mất). Tôi vẫn thích con đường ngoại lệ.


9

Trong trường hợp này, tôi sẽ sử dụng phương pháp nhà máy. Về cơ bản, thiết lập lớp của bạn chỉ có các hàm tạo riêng và có một phương thức xuất xưởng trả về một thể hiện của đối tượng của bạn. Nếu các tham số ban đầu không hợp lệ, chỉ cần trả về null và để mã gọi quyết định phải làm gì.

public class MyClass
{
    private MyClass(string text)
    {
        //normal construction
    }

    public static MyClass MakeMyClass(string text)
    {
        if (String.IsNullOrEmpty(text))
            return null;
        else
            return new MyClass(text);
    }
}
public class CallingClass
{
    public MyClass MakeMyClass(string text)
    {
        var cls = MyClass.MakeMyClass(text);
        if(cls == null)
             //show messagebox or throw exception
        return cls;
    }
}

Không bao giờ ném ngoại lệ trừ khi các điều kiện là đặc biệt. Tôi nghĩ rằng trong trường hợp này, một giá trị trống có thể được thông qua dễ dàng. Nếu đó là trường hợp, sử dụng mẫu này sẽ tránh các trường hợp ngoại lệ và hình phạt hiệu suất mà họ phải chịu trong khi vẫn giữ trạng thái MyClass hợp lệ.


1
Tôi không thấy lợi thế. Tại sao việc kiểm tra kết quả của một nhà máy là không thích hợp hơn để có một lần thử ở đâu đó có thể bắt ngoại lệ từ nhà xây dựng? Cách tiếp cận của bạn dường như là "bỏ qua các ngoại lệ, quay lại kiểm tra rõ ràng giá trị trả về từ các phương thức cho các lỗi". Từ lâu chúng tôi đã chấp nhận rằng các trường hợp ngoại lệ thường vượt trội hơn so với việc phải kiểm tra rõ ràng mọi giá trị trả về của phương thức đối với các lỗi, vì vậy lời khuyên của bạn có vẻ đáng ngờ. Tôi muốn xem một số biện minh cho lý do tại sao bạn nghĩ rằng quy tắc chung không áp dụng ở đây.
DW

chúng tôi không bao giờ chấp nhận rằng các trường hợp ngoại lệ là vượt trội so với trả về mã, chúng có thể là một cú đánh hiệu suất cao nếu bị ném quá thường xuyên. Tuy nhiên, việc ném một ngoại lệ từ hàm tạo sẽ làm rò rỉ bộ nhớ, ngay cả trong .NET, nếu bạn đã cấp phát thứ gì đó không được quản lý. Bạn phải làm rất nhiều công việc trong bộ hoàn thiện của bạn để bù đắp cho trường hợp này. Dù sao, nhà máy là một ý tưởng tốt ở đây, và TBH nó nên ném một ngoại lệ thay vì trả về null. Giả sử bạn chỉ tạo ra một vài lớp 'xấu', thì thực hiện ném nhà máy là thích hợp hơn.
gbjbaanb

2
  • Một nhà xây dựng không nên có bất kỳ tác dụng phụ.
    • Bất cứ điều gì nhiều hơn khởi tạo trường riêng nên được xem là một tác dụng phụ.
    • Một nhà xây dựng với các tác dụng phụ phá vỡ Nguyên tắc Đơn trách nhiệm (SRP) và chạy ngược lại với tinh thần của lập trình hướng đối tượng (OOP).
  • Một nhà xây dựng nên nhẹ và không bao giờ thất bại.
    • Chẳng hạn, tôi luôn rùng mình khi thấy một khối thử bắt bên trong một hàm tạo. Một constructor không nên ném ngoại lệ hoặc lỗi log.

Người ta có thể đặt câu hỏi một cách hợp lý các hướng dẫn này và nói, "Nhưng tôi không tuân theo các quy tắc này và mã của tôi hoạt động tốt!" Về điều đó, tôi sẽ trả lời, "Điều đó có thể đúng, cho đến khi nó không."

  • Các ngoại lệ và lỗi bên trong của một constructor là rất bất ngờ. Trừ khi họ được yêu cầu làm như vậy, các lập trình viên trong tương lai sẽ không có xu hướng bao quanh các cuộc gọi của nhà xây dựng này với mã phòng thủ.
  • Nếu bất cứ điều gì thất bại trong sản xuất, dấu vết ngăn xếp được tạo ra có thể khó phân tích. Phần đầu của dấu vết ngăn xếp có thể trỏ đến lệnh gọi của hàm tạo, nhưng nhiều điều xảy ra trong hàm tạo và nó có thể không trỏ đến LỘC thực tế đã thất bại.
    • Tôi đã phân tích nhiều dấu vết ngăn xếp .NET trong trường hợp này.

0

Nó phụ thuộc vào những gì MyClass đang làm. Nếu MyClass thực sự là một lớp kho lưu trữ dữ liệu và văn bản tham số là một chuỗi kết nối, thì cách tốt nhất là ném một ArgumentException. Tuy nhiên, nếu MyClass là lớp StringBuilder (chẳng hạn), thì bạn có thể để trống.

Vì vậy, nó phụ thuộc vào cách văn bản tham số thiết yếu đối với phương thức - đối tượng có ý nghĩa với giá trị null hoặc trống không?


2
Tôi nghĩ rằng câu hỏi ngụ ý rằng lớp thực sự yêu cầu chuỗi không được trống. Đó không phải là trường hợp trong ví dụ StringBuilder của bạn.
Sergio Acosta

Sau đó, câu trả lời phải là có - có thể chấp nhận ném ngoại lệ. Để tránh phải thử / bắt lỗi này, sau đó bạn có thể sử dụng mẫu nhà máy để xử lý việc tạo đối tượng cho bạn, trong đó bao gồm trường hợp chuỗi rỗng.
Steven Striga

0

Sở thích của tôi là đặt mặc định, nhưng tôi biết Java có thư viện "Apache Commons" hoạt động như thế này và tôi nghĩ đó cũng là một ý tưởng khá hay. Tôi không thấy vấn đề với việc ném ngoại lệ nếu giá trị không hợp lệ sẽ đặt đối tượng ở trạng thái không sử dụng được; một chuỗi không phải là một ví dụ tốt nhưng nếu nó là dành cho người nghèo DI thì sao? Bạn không thể vận hành nếu một giá trị nullđược truyền thay cho, giả sử, một ICustomerRepositorygiao diện. Trong một tình huống như thế này, ném một ngoại lệ là cách xử lý chính xác, tôi sẽ nói.


-1

Khi tôi từng làm việc trong c ++, tôi đã không sử dụng để ném ngoại lệ vào hàm tạo vì vấn đề rò rỉ bộ nhớ có thể dẫn đến, tôi đã học được nó một cách khó khăn. Bất cứ ai làm việc với c ++ đều biết việc rò rỉ bộ nhớ khó khăn và có vấn đề như thế nào.

Nhưng nếu bạn đang ở c # / Java, thì bạn không gặp vấn đề này, vì trình thu gom rác sẽ thu thập bộ nhớ. Nếu bạn đang sử dụng C #, tôi nghĩ nên sử dụng thuộc tính cho cách tốt và nhất quán để đảm bảo rằng các ràng buộc dữ liệu được đảm bảo.

public class MyClass
{
    private string _text;
    public string Text 
    {
        get
        {
            return _text;
        } 
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("Text cannot be empty");
            _text = value;
        } 
    }

    public MyClass(string text)
    {
        Text = text;
        // continue with normal construction
    }
}

-3

Ném một ngoại lệ sẽ là một nỗi đau thực sự cho những người dùng trong lớp của bạn. Nếu xác nhận là một vấn đề thì tôi thường thực hiện việc tạo đối tượng theo quy trình 2 bước.

1 - Tạo ví dụ

2 - Gọi một phương thức Khởi tạo với kết quả trả về.

HOẶC LÀ

Gọi một phương thức / hàm tạo lớp cho bạn để có thể thực hiện xác nhận.

HOẶC LÀ

Có một cờ isValid, điều mà tôi không đặc biệt thích nhưng một số người làm.


3
@Dunk, điều đó thật sai lầm.
CaffGeek

4
@Chad, đó là ý kiến ​​của bạn, nhưng ý kiến ​​của tôi không sai. Nó chỉ khác với bạn. Tôi thấy mã try-Catch khó đọc hơn rất nhiều và tôi đã thấy nhiều lỗi được tạo ra hơn khi mọi người sử dụng try-Catch để xử lý lỗi nhỏ hơn so với các phương thức truyền thống. Đó là kinh nghiệm của tôi và nó không sai. Đó là những gì nó được.
Dunk

5
ĐIỂM là sau khi tôi gọi một nhà xây dựng, nếu nó không tạo vì đầu vào không hợp lệ, đó là NGOẠI TRỪ. Dữ liệu của ứng dụng hiện ở trạng thái không nhất quán / không hợp lệ nếu tôi chỉ cần tạo đối tượng và đặt cờ nói isValid = false. Phải kiểm tra sau khi một lớp được tạo nếu nó hợp lệ là thiết kế dễ bị lỗi. Và, bạn đã nói, có một hàm tạo, sau đó gọi khởi tạo ... Nếu tôi không gọi khởi tạo thì sao? Sau đó thì sao? Và nếu Khởi tạo có dữ liệu xấu, tôi có thể ném ngoại lệ ngay bây giờ không? Lớp học của bạn không nên khó sử dụng.
CaffGeek

5
@Dunk, nếu bạn thấy ngoại lệ khó sử dụng, bạn đang sử dụng chúng sai. Nói một cách đơn giản, đầu vào xấu được cung cấp cho một nhà xây dựng. Đó là một ngoại lệ. Ứng dụng không nên tiếp tục chạy trừ khi nó được sửa. Giai đoạn. Nếu nó xảy ra, bạn kết thúc với một vấn đề tồi tệ hơn, dữ liệu xấu. Nếu bạn không biết cách xử lý ngoại lệ, thì bạn không, bạn hãy để nó nổi lên ngăn xếp cuộc gọi cho đến khi có thể xử lý được, ngay cả khi điều đó có nghĩa là chỉ cần đăng nhập và dừng thực thi. Nói một cách đơn giản, bạn không cần xử lý ngoại lệ hoặc kiểm tra chúng. Họ chỉ đơn giản là làm việc.
CaffGeek

3
Xin lỗi, đây chỉ là quá nhiều mô hình chống xem xét.
MPelletier
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.