Khởi tạo các trường lớp trong constructor hoặc khai báo?


413

Gần đây tôi đã lập trình bằng C # và Java và tôi tò mò nơi nào tốt nhất là khởi tạo các trường lớp của tôi.

Tôi có nên làm điều đó khi khai báo không?:

public class Dice
{
    private int topFace = 1;
    private Random myRand = new Random();

    public void Roll()
    {
       // ......
    }
}

hoặc trong một nhà xây dựng?:

public class Dice
{
    private int topFace;
    private Random myRand;

    public Dice()
    {
        topFace = 1;
        myRand = new Random();
    }

    public void Roll()
    {
        // .....
    }
}

Tôi thực sự tò mò về những gì mà một số cựu chiến binh nghĩ là cách thực hành tốt nhất. Tôi muốn nhất quán và theo một cách tiếp cận.


3
Lưu ý rằng đối với các cấu trúc, bạn không thể có các trình khởi tạo trường thể hiện, vì vậy bạn không có lựa chọn nào khác ngoài sử dụng hàm tạo.
yoyo

Câu trả lời:


310

Luật của tôi:

  1. Đừng khởi tạo với các giá trị mặc định trong tuyên bố ( null, false, 0, 0.0...).
  2. Thích khởi tạo trong khai báo nếu bạn không có tham số hàm tạo thay đổi giá trị của trường.
  3. Nếu giá trị của trường thay đổi do tham số của hàm tạo, hãy đặt khởi tạo trong các hàm tạo.
  4. Hãy nhất quán trong thực hành của bạn (quy tắc quan trọng nhất).

4
Tôi hy vọng rằng kokos có nghĩa là bạn không nên khởi tạo thành viên theo các giá trị mặc định của họ (0, false, null, v.v.), vì trình biên dịch sẽ làm điều đó cho bạn (1.). Nhưng nếu bạn muốn khởi tạo một trường thành bất kỳ thứ gì ngoài giá trị mặc định của nó, bạn nên thực hiện nó trong khai báo (2.). Tôi nghĩ rằng nó có thể là cách sử dụng từ "mặc định" làm bạn bối rối.
Ricky Helgesson

95
Tôi không đồng ý với quy tắc 1 - bằng cách không chỉ định giá trị mặc định (bất kể nó có được trình biên dịch khởi tạo hay không), bạn sẽ để nhà phát triển đoán hoặc đi tìm tài liệu về giá trị mặc định cho ngôn ngữ cụ thể đó. Đối với mục đích dễ đọc, tôi sẽ luôn chỉ định giá trị mặc định.
James

32
Giá trị mặc định của một loại default(T)luôn là giá trị có biểu diễn nhị phân bên trong 0.
Olivier Jacot-Descombes

15
Cho dù bạn có thích quy tắc 1 hay không, nó không thể được sử dụng với các trường chỉ đọc, phải được khởi tạo rõ ràng trước thời điểm hàm tạo kết thúc.
yoyo

36
Tôi không đồng ý với những người không đồng ý với quy tắc 1. Bạn có thể mong đợi người khác học ngôn ngữ C #. Giống như chúng tôi không nhận xét từng foreachvòng lặp với "điều này lặp lại như sau cho tất cả các mục trong danh sách", chúng tôi không cần phải liên tục khôi phục các giá trị mặc định của C #. Chúng ta cũng không cần phải giả vờ rằng C # có ngữ nghĩa chưa được khởi tạo. Vì sự vắng mặt của một giá trị có ý nghĩa rõ ràng, nên bỏ nó đi. Nếu lý tưởng là rõ ràng, bạn nên luôn luôn sử dụng newkhi tạo đại biểu mới (như được yêu cầu trong C # 1). Nhưng ai làm điều đó? Ngôn ngữ được thiết kế cho các lập trình viên có lương tâm.
Edward Brey

149

Trong C # không thành vấn đề. Hai mẫu mã bạn đưa ra là hoàn toàn tương đương. Trong ví dụ đầu tiên, trình biên dịch C # (hoặc là CLR?) Sẽ xây dựng một hàm tạo rỗng và khởi tạo các biến như thể chúng ở trong hàm tạo (có một sắc thái nhỏ mà Jon Skeet giải thích trong các bình luận bên dưới). Nếu đã có một hàm tạo thì mọi khởi tạo "ở trên" sẽ được chuyển lên trên cùng của nó.

Về mặt thực hành tốt nhất, cái trước ít bị lỗi hơn cái sau vì ai đó có thể dễ dàng thêm một hàm tạo khác và quên xâu chuỗi nó.


4
Điều đó không đúng nếu bạn chọn khởi tạo lớp bằng GetUninitializedObject. Bất cứ điều gì trong ctor sẽ không được chạm vào nhưng khai báo trường sẽ được chạy.
Wolf5

28
Trên thực tế nó có vấn đề. Nếu hàm tạo của lớp cơ sở gọi một phương thức ảo (nói chung là một ý tưởng tồi, nhưng có thể xảy ra) bị ghi đè trong lớp dẫn xuất, thì sử dụng các khởi tạo biến thể hiện, biến sẽ được khởi tạo khi phương thức được gọi - trong khi sử dụng khởi tạo trong nhà xây dựng, họ sẽ không được. (Bộ khởi tạo biến sơ thẩm được thực thi trước khi hàm tạo của lớp cơ sở được gọi.)
Jon Skeet

2
Có thật không? Tôi thề tôi đã lấy thông tin này từ Richter's CLR thông qua C # (phiên bản thứ 2 tôi nghĩ) và ý chính của nó là đây là đường cú pháp (tôi có thể đã đọc sai?) Và CLR chỉ kẹt các biến vào hàm tạo. Nhưng bạn nói rằng đây không phải là trường hợp, tức là việc khởi tạo thành viên có thể kích hoạt trước khi khởi tạo ctor trong kịch bản điên rồ của việc gọi một ảo trong ctor cơ sở và có một ghi đè trong lớp trong câu hỏi. Tôi đã hiểu đúng chưa? Bạn vừa tìm ra điều này? Bối rối khi nhận xét cập nhật này về một bài viết 5 năm tuổi (OMG đã được 5 năm?).
Quibbledome

@Quibbledome: Một hàm tạo của lớp con sẽ chứa một lệnh gọi đến hàm tạo cha. Trình biên dịch ngôn ngữ có thể bao gồm nhiều hoặc ít mã trước khi nó thích, miễn là hàm tạo cha mẹ được gọi chính xác một lần trên tất cả các đường dẫn mã và việc sử dụng hạn chế được tạo ra từ đối tượng được xây dựng trước lệnh gọi đó. Một trong những phiền toái của tôi với C # là trong khi nó có thể tạo mã khởi tạo các trường và mã sử dụng các tham số của hàm tạo, nó không cung cấp cơ chế nào để khởi tạo các trường dựa trên các tham số của hàm tạo.
supercat

1
@Quibbledome, trước đây sẽ là một vấn đề, nếu giá trị được gán và chuyển cho phương thức WCF. Mà sẽ thiết lập lại dữ liệu về kiểu dữ liệu mặc định của mô hình. Cách thực hành tốt nhất là thực hiện khởi tạo trong hàm tạo (phần sau).
Pranesh Janarthanan 23/12/19

16

Các ngữ nghĩa của C # hơi khác với Java ở đây. Việc gán C # trong khai báo được thực hiện trước khi gọi hàm tạo của lớp bậc trên. Trong Java, nó được thực hiện ngay sau đó cho phép 'cái này' được sử dụng (đặc biệt hữu ích cho các lớp bên trong ẩn danh) và có nghĩa là ngữ nghĩa của hai dạng thực sự khớp với nhau.

Nếu bạn có thể, làm cho các lĩnh vực cuối cùng.


15

Tôi nghĩ rằng có một cảnh báo. Tôi đã từng phạm một lỗi như vậy: Bên trong một lớp dẫn xuất, tôi đã cố gắng "khởi tạo khai báo" các trường được kế thừa từ một lớp cơ sở trừu tượng. Kết quả là đã tồn tại hai bộ trường, một bộ là "cơ sở" và bộ khác là bộ mới được khai báo và tôi mất khá nhiều thời gian để gỡ lỗi.

Bài học: để khởi tạo các trường được kế thừa , bạn sẽ thực hiện bên trong hàm tạo.


Vì vậy, nếu bạn giới thiệu lĩnh vực này derivedObject.InheritedField, nó có liên quan đến cơ sở của bạn hay cơ sở xuất phát không?
RayLuo

6

Giả sử kiểu trong ví dụ của bạn, chắc chắn thích khởi tạo các trường trong hàm tạo. Các trường hợp đặc biệt là:

  • Các trường trong các lớp / phương thức tĩnh
  • Các trường được nhập dưới dạng tĩnh / Final / et al

Tôi luôn nghĩ danh sách trường ở đầu một lớp là mục lục (nội dung trong tài liệu này, không phải cách sử dụng nó) và hàm tạo như phần giới thiệu. Phương pháp của khóa học là chương.


Tại sao nó "chắc chắn" như vậy? Bạn chỉ cung cấp một sở thích phong cách, mà không giải thích lý do của nó. Đợi đã, đừng bận tâm, theo câu trả lời của @quibbledome , chúng "hoàn toàn tương đương", vì vậy nó thực sự chỉ là một sở thích phong cách cá nhân.
RayLuo

4

Điều gì nếu tôi nói với bạn, nó phụ thuộc?

Tôi nói chung khởi tạo mọi thứ và làm nó một cách nhất quán. Vâng, nó quá rõ ràng nhưng nó cũng dễ bảo trì hơn một chút.

Nếu chúng ta lo lắng về hiệu suất, thì tôi chỉ khởi tạo những gì phải làm và đặt nó vào những khu vực mà nó mang lại nhiều tiếng vang nhất.

Trong một hệ thống thời gian thực, tôi đặt câu hỏi liệu tôi thậm chí có cần biến hay hằng không.

Và trong C ++, tôi thường làm bên cạnh không khởi tạo ở một trong hai vị trí và chuyển nó thành hàm init (). Tại sao? Chà, trong C ++ nếu bạn đang khởi tạo thứ gì đó có thể tạo ra ngoại lệ trong quá trình xây dựng đối tượng, bạn sẽ tự mở rò rỉ bộ nhớ.


4

Có rất nhiều tình huống khác nhau.

Tôi chỉ cần một danh sách trống

Tình hình đã rõ ràng. Tôi chỉ cần chuẩn bị danh sách của mình và ngăn không cho ngoại lệ bị ném khi ai đó thêm một mục vào danh sách.

public class CsvFile
{
    private List<CsvRow> lines = new List<CsvRow>();

    public CsvFile()
    {
    }
}

Tôi biết các giá trị

Tôi chính xác biết những giá trị nào tôi muốn có theo mặc định hoặc tôi cần sử dụng một số logic khác.

public class AdminTeam
{
    private List<string> usernames;

    public AdminTeam()
    {
         usernames = new List<string>() {"usernameA", "usernameB"};
    }
}

hoặc là

public class AdminTeam
{
    private List<string> usernames;

    public AdminTeam()
    {
         usernames = GetDefaultUsers(2);
    }
}

Danh sách trống với các giá trị có thể

Đôi khi tôi mong đợi một danh sách trống theo mặc định với khả năng thêm giá trị thông qua một hàm tạo khác.

public class AdminTeam
{
    private List<string> usernames = new List<string>();

    public AdminTeam()
    {
    }

    public AdminTeam(List<string> admins)
    {
         admins.ForEach(x => usernames.Add(x));
    }
}

3

Trong Java, một trình khởi tạo với khai báo có nghĩa là trường luôn được khởi tạo theo cùng một cách, bất kể hàm tạo nào được sử dụng (nếu bạn có nhiều hơn một) hoặc các tham số của hàm tạo của bạn (nếu chúng có đối số), mặc dù sau đó hàm tạo có thể thay đổi giá trị (nếu nó không phải là cuối cùng). Vì vậy, việc sử dụng trình khởi tạo với khai báo gợi ý cho người đọc rằng giá trị khởi tạo là giá trị mà trường có trong mọi trường hợp , bất kể hàm tạo nào được sử dụng và bất kể tham số nào được truyền cho bất kỳ hàm tạo nào. Do đó, chỉ sử dụng một trình khởi tạo với khai báo nếu và luôn luôn nếu, giá trị cho tất cả các đối tượng được xây dựng là như nhau.


3

Thiết kế của C # cho thấy rằng khởi tạo nội tuyến được ưu tiên hoặc nó sẽ không có trong ngôn ngữ. Bất cứ lúc nào bạn có thể tránh tham chiếu chéo giữa các vị trí khác nhau trong mã, bạn thường tốt hơn.

Ngoài ra còn có vấn đề về tính nhất quán với khởi tạo trường tĩnh, cần phải được đặt nội tuyến để có hiệu suất tốt nhất. Nguyên tắc thiết kế khung cho thiết kế nhà xây dựng cho biết điều này:

✓ NGƯỜI DÙNG khởi tạo các trường tĩnh nội tuyến thay vì sử dụng rõ ràng các hàm tạo tĩnh, vì thời gian chạy có thể tối ưu hóa hiệu suất của các loại không có hàm tạo tĩnh được xác định rõ ràng.

"Xem xét" trong bối cảnh này có nghĩa là làm như vậy trừ khi có lý do chính đáng để không. Trong trường hợp các trường khởi tạo tĩnh, một lý do chính đáng sẽ là nếu việc khởi tạo quá phức tạp để được mã hóa nội tuyến.


2

Tính nhất quán rất quan trọng, nhưng đây là câu hỏi để bạn tự hỏi: "Tôi có một nhà xây dựng cho bất cứ điều gì khác không?"

Thông thường, tôi đang tạo các mô hình để truyền dữ liệu mà bản thân lớp không làm gì ngoại trừ hoạt động như nhà ở cho các biến.

Trong các kịch bản này, tôi thường không có bất kỳ phương thức hoặc hàm tạo nào. Tôi cảm thấy thật ngớ ngẩn khi tạo ra một nhà xây dựng cho mục đích độc quyền là khởi tạo danh sách của mình, đặc biệt là vì tôi có thể khởi tạo chúng phù hợp với khai báo.

Vì vậy, như nhiều người khác đã nói, nó phụ thuộc vào cách sử dụng của bạn. Giữ cho nó đơn giản và không làm bất cứ điều gì thêm mà bạn không phải làm.


2

Xem xét tình huống mà bạn có nhiều hơn một nhà xây dựng. Việc khởi tạo sẽ khác nhau đối với các nhà xây dựng khác nhau? Nếu chúng sẽ giống nhau, thì tại sao lặp lại cho mỗi constructor? Điều này phù hợp với tuyên bố kokos, nhưng có thể không liên quan đến các tham số. Ví dụ, giả sử bạn muốn giữ một lá cờ cho biết cách tạo đối tượng. Sau đó, cờ đó sẽ được khởi tạo khác nhau cho các hàm tạo khác nhau bất kể các tham số của hàm tạo. Mặt khác, nếu bạn lặp lại cùng một khởi tạo cho mỗi hàm tạo, bạn sẽ có khả năng bạn (vô tình) thay đổi tham số khởi tạo trong một số hàm tạo nhưng không phải ở các hàm tạo khác. Vì vậy, khái niệm cơ bản ở đây là mã chung nên có một vị trí chung và không có khả năng lặp lại ở các vị trí khác nhau.


1

Có một lợi ích hiệu suất nhỏ để thiết lập giá trị trong khai báo. Nếu bạn đặt nó trong hàm tạo, nó thực sự được đặt hai lần (lần đầu tiên đến giá trị mặc định, sau đó đặt lại trong ctor).


2
Trong C #, các trường luôn được đặt giá trị mặc định đầu tiên. Sự hiện diện của một trình khởi tạo làm cho không có sự khác biệt.
Jeffrey L Whitledge

0

Tôi thường cố gắng xây dựng để không làm gì ngoài việc có được các phụ thuộc và khởi tạo các thành viên thể hiện liên quan với chúng. Điều này sẽ làm cho cuộc sống của bạn dễ dàng hơn nếu bạn muốn đơn vị kiểm tra các lớp học của bạn.

Nếu giá trị bạn định gán cho một biến thể hiện không bị ảnh hưởng bởi bất kỳ tham số nào bạn sẽ truyền cho nhà xây dựng thì hãy gán nó vào thời điểm khai báo.


0

Không phải là câu trả lời trực tiếp cho câu hỏi của bạn về cách thực hành tốt nhất nhưng một điểm làm mới quan trọng và có liên quan là trong trường hợp định nghĩa lớp chung, hãy để nó trên trình biên dịch để khởi tạo với các giá trị mặc định hoặc chúng ta phải sử dụng một phương thức đặc biệt để khởi tạo các trường đến các giá trị mặc định của chúng (nếu điều đó là tuyệt đối cần thiết cho khả năng đọc mã).

class MyGeneric<T>
{
    T data;
    //T data = ""; // <-- ERROR
    //T data = 0; // <-- ERROR
    //T data = null; // <-- ERROR        

    public MyGeneric()
    {
        // All of the above errors would be errors here in constructor as well
    }
}

Và phương pháp đặc biệt để khởi tạo trường chung thành giá trị mặc định của nó là như sau:

class MyGeneric<T>
{
    T data = default(T);

    public MyGeneric()
    {           
        // The same method can be used here in constructor
    }
}

0

Khi bạn không cần xử lý logic hoặc lỗi:

  • Khởi tạo các trường lớp khi khai báo

Khi bạn cần một số xử lý logic hoặc lỗi:

  • Khởi tạo các trường lớp trong hàm tạo

Điều này hoạt động tốt khi giá trị khởi tạo có sẵn và việc khởi tạo có thể được đặt trên một dòng. Tuy nhiên, hình thức khởi tạo này có những hạn chế vì tính đơn giản của nó. Nếu khởi tạo yêu cầu một số logic (ví dụ: xử lý lỗi hoặc vòng lặp for để điền vào một mảng phức tạp), việc gán đơn giản là không đủ. Các biến sơ thẩm có thể được khởi tạo trong các hàm tạo, trong đó xử lý lỗi hoặc logic khác có thể được sử dụng.

Từ https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html .

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.