2010-09-24 49 views
35

Tôi vừa mới tham gia vào một công ty mới và phần lớn cơ sở mã sử dụng các phương thức khởi tạo thay vì các nhà xây dựng.Tại sao sử dụng phương thức khởi tạo thay vì hàm tạo?

struct MyFancyClass : theUberClass 
{ 
    MyFancyClass(); 
    ~MyFancyClass(); 
    resultType initMyFancyClass(fancyArgument arg1, classyArgument arg2, 
           redundantArgument arg3=TODO); 
    // several fancy methods... 
}; 

Họ nói với tôi rằng điều này có liên quan đến thời gian. Một số điều phải được thực hiện sau khi xây dựng sẽ thất bại trong hàm tạo. Nhưng hầu hết các nhà xây dựng đều trống và tôi không thực sự thấy bất kỳ lý do gì để không sử dụng các nhà xây dựng.

Vì vậy, tôi chuyển sang bạn, các trình thuật sĩ của C++: tại sao bạn sẽ sử dụng phương thức init thay vì một hàm tạo?

+28

Nếu công ty của riêng bạn thậm chí không thể giải thích nó, tôi ngửi thấy mùi mã xấu. – Mike

+0

Có vẻ như thiếu 'void'. – sellibitze

+2

Tôi đồng ý với Mike. Bắt đầu tìm việc làm. – sbi

Trả lời

54

Vì chúng nói "thời gian", tôi đoán là vì chúng muốn hàm init của chúng có thể gọi các hàm ảo trên đối tượng. Điều này không phải lúc nào cũng làm việc trong một hàm tạo, bởi vì trong hàm tạo của lớp cơ sở, phần lớp dẫn xuất của đối tượng "chưa tồn tại", và đặc biệt bạn không thể truy cập các hàm ảo được định nghĩa trong lớp dẫn xuất. Thay vào đó, phiên bản lớp cơ sở của hàm được gọi, nếu được định nghĩa. Nếu nó không được định nghĩa, (ngụ ý rằng hàm là thuần ảo), bạn sẽ nhận được hành vi không xác định.

Lý do phổ biến khác cho chức năng init là mong muốn tránh ngoại lệ, nhưng đó là một phong cách lập trình khá cũ (và cho dù đó là một ý tưởng hay là toàn bộ đối số của chính nó). Nó không liên quan gì đến những thứ không thể làm việc trong một nhà xây dựng, thay vì làm với thực tế là các nhà xây dựng không thể trả về một giá trị lỗi nếu có điều gì đó không thành công. Vì vậy, trong phạm vi mà các đồng nghiệp của bạn đã cho bạn những lý do thực sự, tôi nghi ngờ điều này không phải là nó.

+2

Chỉ một tham chiếu Msdn cho bài đăng của bạn: http://msdn.microsoft.com/en-us/library /ms182331%28VS.80%29.aspx – Tarik

+2

Đầu tiên thực sự là một lý do hợp lệ. Nó còn được gọi là _two-phase-construction_. Nếu tôi cần thứ gì đó như thế này, tôi giấu nó vào bên trong một vật thể. Tôi sẽ không bao giờ phơi bày điều này với người dùng lớp học của mình. – sbi

+0

Vì lý do thứ hai, hàm tạo vẫn có thể được sử dụng với các hàm trợ giúp để quản lý các tài nguyên mà có thể có một ngoại lệ (nếu các ngoại lệ được kích hoạt). Nếu hàm tạo không thành công, thì hàm hủy sẽ không được gọi trên đối tượng được cấu tạo một phần. Đây là lý do tại sao đôi khi bạn thấy mã init/cleanup thủ công. –

-2

Bạn sử dụng phương thức khởi tạo thay vì hàm khởi tạo nếu trình khởi tạo cần được gọi sau khi lớp đã được tạo. Vì vậy, nếu lớp A được tạo ra như:

A *a = new A; 

và initisalizer của lớp A đòi hỏi một được thiết lập, sau đó rõ ràng là bạn cần một cái gì đó như:

A *a = new A; 
a->init(); 
+0

Tôi không mua cái này. Bạn chỉ có thể gọi init riêng trong dòng cuối cùng của hàm tạo, sau đó. (Tôi đã không bỏ phiếu) mặc dù – Mike

+7

Đó là một sự trả lời của câu hỏi, không phải là một câu trả lời. * Tại sao * bạn cần phải khởi tạo đối tượng sau khi xây dựng? –

+2

Nếu hàm tạo dựa trên biến đang được đặt, nó xảy ra với tôi. –

6

Có hai lý do tôi có thể nghĩ ra khỏi đầu trong đầu của tôi:

  • Nói việc tạo một đối tượng liên quan đến rất nhiều công việc tẻ nhạt có thể thất bại trong rất nhiều và rất nhiều cách khủng khiếp và tinh tế. Nếu bạn sử dụng một hàm khởi tạo ngắn để thiết lập những thứ không đúng, và sau đó yêu cầu người dùng gọi phương thức khởi tạo để thực hiện công việc lớn, bạn có thể chắc chắn rằng bạn có một số đối tượng được tạo ngay cả khi công việc lớn thất bại . Có lẽ đối tượng chứa thông tin về chính xác cách mà init không thành công, hoặc có thể điều quan trọng là giữ các đối tượng khởi tạo không thành công xung quanh vì các lý do khác.
  • Đôi khi bạn có thể muốn khởi tạo lại một đối tượng lâu sau khi nó đã được tạo. Bằng cách này, nó chỉ là vấn đề gọi phương thức khởi tạo lại mà không phá hủy và tái tạo đối tượng.
+0

+1 Tôi nghĩ về lập luận thứ hai của bạn; thực sự tôi nghĩ rằng điều này vẫn được coi là một thực tế xấu trong C + + và nó nên được thực hiện bằng cách sử dụng một số đối tượng trợ giúp. – mbq

+0

Dấu đầu dòng thứ hai của bạn có lẽ là lý do phổ biến nhất khiến đôi khi tôi sử dụng các hàm khởi tạo. Có, hàm tạo có thể gọi init, nhưng điều đó chỉ làm cho mã ít rõ ràng hơn. – Jay

+0

Đây là những lý do điển hình để xem init hoặc chức năng dọn dẹp, nhưng sau đó nó sẽ không là một phần của giao diện public class, là một hàm trợ giúp bên trong để tránh trùng lặp mã. Các bài viết khác bao gồm các lý do để làm điều này một cách hệ thống. –

25

Vâng tôi có thể nghĩ ra một số, nhưng nói chung thì đó không phải là một ý tưởng hay.

Hầu hết các lần mà lý do được gọi là bạn chỉ báo cáo lỗi thông qua các ngoại lệ trong một hàm tạo (điều này đúng) trong khi với phương pháp cổ điển, bạn có thể trả về mã lỗi.

Tuy nhiên trong mã OO được thiết kế phù hợp, hàm tạo chịu trách nhiệm thiết lập các biến thể lớp. Bằng cách cho phép một hàm tạo mặc định, bạn cho phép một lớp trống, do đó bạn phải sửa đổi các bất biến để được chấp nhận cả lớp "null" và lớp "có ý nghĩa" ...và mỗi lần sử dụng lớp học đầu tiên phải đảm bảo rằng đối tượng đã được xây dựng đúng cách ... nó thật thô tục.

Vì vậy, bây giờ, chúng ta hãy vạch trần những "lý do":

  • tôi cần phải sử dụng một phương pháp virtual: sử dụng thành ngữ Constructor ảo.
  • Có rất nhiều công việc phải làm: vì vậy những gì, công việc sẽ được thực hiện dù sao, chỉ làm điều đó trong các nhà xây dựng
  • Quá trình cài đặt có thể thất bại: ném một ngoại lệ
  • Tôi muốn giữ cho khởi tạo một phần object: sử dụng try/catch trong constructor và thiết lập nguyên nhân lỗi trong trường object, đừng quên assert ở đầu mỗi phương thức public để đảm bảo đối tượng có thể sử dụng được trước khi cố gắng sử dụng nó.
  • Tôi muốn khởi tạo lại đối tượng của tôi: gọi phương thức khởi tạo từ các nhà xây dựng, bạn sẽ tránh được mã trùng lặp trong khi vẫn có một đối tượng khởi tạo đầy đủ
  • Tôi muốn khởi tạo lại đối tượng của tôi (2): sử dụng operator= (và thực hiện nó bằng cách sử dụng thành phần sao chép và hoán đổi nếu trình biên dịch tạo ra phiên bản không phù hợp với nhu cầu của bạn).

Như đã nói, nói chung, ý tưởng tồi. Nếu bạn thực sự muốn có hàm tạo "void", hãy đặt chúng là private và sử dụng các phương thức Trình tạo. Nó hiệu quả với NRVO ... và bạn có thể trả lại boost::optional<FancyObject> trong trường hợp việc xây dựng thất bại.

+2

Tôi muốn tìm giải pháp thay thế, "Tôi muốn khả năng khởi tạo lại đối tượng của mình: thực hiện một toán tử' sao chép và hoán đổi '= 01" –

+0

Tôi nghĩ về nó, sau đó nhận ra rằng có thể có cơ hội tối ưu hóa. Ví dụ, hãy nghĩ về phương thức 'assign' trên đối tượng' vector': bằng cách sử dụng 'assign' bạn sử dụng lại lưu trữ đã được cấp phát và do đó không yêu cầu cấp phát bộ nhớ giả. Thật khó để làm điều đó một cách chính xác mặc dù, vì vậy tôi đồng ý rằng sao chép và trao đổi nên được sử dụng trong trường hợp chung. –

+0

đồng ý, không phải là sai khi có chức năng khởi tạo lại, nó không chỉ là thứ tôi bắt đầu. Nếu không có gì khác, cho "cơ hội tối ưu hóa" đọc "bảo đảm ngoại lệ cơ bản". –

15

Những người khác đã liệt kê rất nhiều lý do có thể (và giải thích đúng về lý do tại sao hầu hết những điều này thường không phải là một ý tưởng hay). Hãy để tôi đăng một ví dụ về một (nhiều hoặc ít hơn) sử dụng hợp lệ các phương thức init, mà thực sự phải làm với thời gian.

Trong một dự án trước, chúng tôi có nhiều lớp và đối tượng dịch vụ, mỗi lớp là một phần của cấu trúc phân cấp và tham chiếu chéo lẫn nhau theo nhiều cách khác nhau. Vì vậy, thông thường, để tạo ServiceA, bạn cần một đối tượng dịch vụ cha mẹ, do đó cần một thùng chứa dịch vụ, đã phụ thuộc vào sự hiện diện của một số dịch vụ cụ thể (có thể bao gồm ServiceA) tại thời điểm khởi tạo. Lý do là trong quá trình khởi tạo, hầu hết các dịch vụ đã đăng ký chính nó với các dịch vụ khác như người nghe đến các sự kiện cụ thể và/hoặc thông báo cho các dịch vụ khác về sự kiện khởi tạo thành công. Nếu dịch vụ khác không tồn tại tại thời điểm thông báo, đăng ký đã không xảy ra, do đó dịch vụ này sẽ không nhận được thông báo quan trọng sau này, trong quá trình sử dụng ứng dụng. Để phá vỡ chuỗi phụ thuộc vòng tròn, chúng tôi đã phải sử dụng các phương thức khởi tạo rõ ràng tách biệt với các nhà xây dựng, do đó hiệu quả làm cho dịch vụ toàn cầu khởi tạo một quy trình hai pha.

Vì vậy, mặc dù thành ngữ này không nên được theo sau nói chung, IMHO nó có một số sử dụng hợp lệ. Tuy nhiên, tốt nhất là giới hạn mức sử dụng của nó ở mức tối thiểu, sử dụng các hàm tạo bất cứ khi nào có thể. Trong trường hợp của chúng tôi, đây là một dự án kế thừa, và chúng tôi chưa hoàn toàn hiểu được kiến ​​trúc của nó. Ít nhất việc sử dụng các phương thức init được giới hạn trong các lớp dịch vụ - các lớp thông thường được khởi tạo thông qua các nhà xây dựng. Tôi tin rằng có thể có một cách để cấu trúc lại kiến ​​trúc đó để loại bỏ sự cần thiết cho các phương pháp init dịch vụ, nhưng ít nhất tôi không thấy làm thế nào để thực hiện nó (và thành thật mà nói, chúng tôi có nhiều vấn đề khẩn cấp hơn để giải quyết tại thời điểm đó một phần của dự án).

+0

Có vẻ như bạn đang sử dụng các đĩa đơn. (Đừng làm vậy.) –

+0

@Roger, không, đây không phải là Singletons. Bạn có thể thực tế có nhiều cái gọi là "mô hình" mở cùng một lúc, mỗi mô hình chứa một cá thể chuyên dụng duy nhất của từng loại Dịch vụ. Tôi cũng nhận thức được những vấn đề với Singletons, nhưng vẫn nhờ :-) –

+2

nó chỉ có 2 giai đoạn khởi tạo nếu bạn xem xét việc đăng ký của người nghe là một phần của việc khởi tạo. Trong một cái nhìn năng động của thế giới, nơi các dịch vụ bật lên và đi, việc đăng ký người nghe là một phần của cuộc sống của hệ thống. –

1

Và tôi cũng muốn đính kèm một mẫu mã để trả lời # 1 -

Kể từ cũng MSDN nói:

Khi một phương pháp ảo được gọi, các loại thực mà thực hiện phương pháp này không được chọn cho đến khi thời gian chạy. Khi một hàm tạo gọi phương thức ảo, nó có thể là hàm tạo cho trường hợp gọi phương thức chưa được thực hiện.

Ví dụ: Ví dụ sau đây cho thấy hiệu quả của việc vi phạm quy tắc này. Ứng dụng thử nghiệm tạo ra một cá thể của DerivedType, làm cho hàm tạo của lớp cơ sở (BadlyConstructedType) của nó thực thi. Trình xây dựng của BadlyConstructedType gọi sai phương thức ảo DoSomething. Như đầu ra cho thấy, DerivedType.DoSomething() thực hiện, và làm như vậy trước khi hàm tạo của DerivedType thực hiện.

using System; 

namespace UsageLibrary 
{ 
    public class BadlyConstructedType 
    { 
     protected string initialized = "No"; 

     public BadlyConstructedType() 
     { 
      Console.WriteLine("Calling base ctor."); 
      // Violates rule: DoNotCallOverridableMethodsInConstructors. 
      DoSomething(); 
     } 
     // This will be overridden in the derived type. 
     public virtual void DoSomething() 
     { 
      Console.WriteLine ("Base DoSomething"); 
     } 
    } 

    public class DerivedType : BadlyConstructedType 
    { 
     public DerivedType() 
     { 
      Console.WriteLine("Calling derived ctor."); 
      initialized = "Yes"; 
     } 
     public override void DoSomething() 
     { 
      Console.WriteLine("Derived DoSomething is called - initialized ? {0}", initialized); 
     } 
    } 

    public class TestBadlyConstructedType 
    { 
     public static void Main() 
     { 
      DerivedType derivedInstance = new DerivedType(); 
     } 
    } 
} 

Output:

Calling ctor cơ sở.

DoSomething có nguồn gốc được gọi là - được khởi tạo? Không

Gọi ctor có nguồn gốc.

1

Thêm trường hợp đặc biệt: Nếu bạn tạo một người nghe, bạn có thể muốn làm cho nó tự đăng ký ở đâu đó (chẳng hạn như với một singleton hoặc GUI). Nếu bạn làm điều đó trong hàm tạo của nó, nó sẽ rò rỉ một con trỏ/tham chiếu đến chính nó mà vẫn chưa an toàn, vì hàm tạo chưa hoàn thành (và thậm chí có thể thất bại hoàn toàn). Giả sử singleton thu thập tất cả người nghe và gửi cho họ các sự kiện khi mọi thứ xảy ra và sự kiện, rồi vòng qua danh sách người nghe (một trong số đó là ví dụ mà chúng ta đang nói đến), gửi cho họ mỗi thông điệp. Nhưng trường hợp này vẫn còn giữa đường trong nhà xây dựng của nó, do đó, cuộc gọi có thể thất bại trong tất cả các loại cách xấu. Trong trường hợp này, bạn cần đăng ký trong một hàm riêng biệt, điều mà bạn rõ ràng thực hiện là không phải là gọi từ nhà xây dựng (sẽ đánh bại hoàn toàn mục đích), nhưng từ đối tượng cha mẹ, sau khi xây dựng xong.

Nhưng đó là trường hợp cụ thể, không phải là trường hợp chung.

4

Một lần nữa sử dụng việc khởi tạo như vậy có thể nằm trong nhóm đối tượng. Về cơ bản bạn chỉ cần yêu cầu đối tượng từ hồ bơi. Các hồ bơi sẽ có một số đối tượng N tạo ra được để trống. Đó là người gọi ngay bây giờ có thể gọi bất kỳ phương pháp anh/cô ấy thích để thiết lập các thành viên. Khi người gọi đã thực hiện xong với đối tượng, nó sẽ báo cho hồ bơi biết đến nó. Ưu điểm là cho đến khi đối tượng đang được sử dụng, bộ nhớ sẽ được lưu lại và người gọi có thể sử dụng phương thức thành viên phù hợp của riêng nó để khởi tạo đối tượng. Một đối tượng có thể phục vụ rất nhiều mục đích nhưng người gọi có thể không cần tất cả, và cũng có thể không cần phải khởi tạo tất cả các thành viên của các đối tượng.

Thường nghĩ về kết nối cơ sở dữ liệu. Một nhóm có thể có nhiều đối tượng kết nối và người gọi có thể điền tên người dùng, mật khẩu, v.v.

1

Nó rất hữu ích cho việc quản lý tài nguyên. Giả sử bạn có các lớp với destructors để tự động phân bổ tài nguyên khi thời gian tồn tại của đối tượng kết thúc.Giả sử bạn cũng có một lớp chứa các lớp tài nguyên này và bạn khởi tạo chúng trong hàm tạo của lớp trên này. Điều gì sẽ xảy ra khi bạn sử dụng toán tử gán để khởi tạo lớp cao hơn này? Một khi các nội dung được sao chép, lớp cao hơn cũ trở nên ngoài ngữ cảnh và các destructor được gọi cho tất cả các lớp tài nguyên. Nếu các lớp tài nguyên này có các con trỏ được sao chép trong khi gán, thì tất cả các con trỏ này bây giờ là các con trỏ xấu. Thay vào đó, nếu bạn bắt đầu các lớp tài nguyên trong một hàm init riêng biệt trong lớp cao hơn, bạn hoàn toàn bỏ qua lớp hủy tài nguyên từ khi được gọi, vì toán tử gán không bao giờ phải tạo và xóa các lớp này. Tôi tin rằng đây là ý nghĩa của yêu cầu "thời gian".

5

Hàm init() rất tốt khi trình biên dịch của bạn không hỗ trợ ngoại lệ, hoặc ứng dụng đích của bạn không thể sử dụng heap (ngoại lệ thường được thực hiện bằng cách sử dụng vùng heap để tạo và hủy chúng).

init() thường trình cũng hữu ích khi thứ tự xây dựng cần được xác định. Tức là, nếu bạn phân bổ toàn bộ các đối tượng, thứ tự mà trong đó hàm tạo được gọi không được định nghĩa. Ví dụ:

[file1.cpp] 
some_class instance1; //global instance 

[file2.cpp] 
other_class must_construct_before_instance1; //global instance 

Tiêu chuẩn cung cấp không đảm bảo rằng must_construct_before_instance1 's constructor sẽ được gọi trước khi instance1' constructor s. Khi nó được gắn với phần cứng, thứ tự khởi tạo trong đó mọi thứ có thể rất quan trọng.

Các vấn đề liên quan