Tại sao các biến cục bộ yêu cầu khởi tạo, nhưng các trường thì không?


140

Nếu tôi tạo một bool trong lớp của mình, chỉ cần một cái gì đó giống như bool check, nó mặc định là false.

Khi tôi tạo cùng một bool trong phương thức của mình, bool check(thay vì trong lớp), tôi gặp lỗi "sử dụng kiểm tra biến cục bộ chưa được gán". Tại sao?


Bình luận không dành cho thảo luận mở rộng; cuộc trò chuyện này đã được chuyển sang trò chuyện .
Martijn Pieters

14
Câu hỏi mơ hồ. Sẽ "bởi vì đặc điểm kỹ thuật nói như vậy" là một câu trả lời chấp nhận được?
Eric Lippert

4
Bởi vì đó là cách nó được thực hiện trong Java khi họ sao chép nó. : P
Alvin Thompson

Câu trả lời:


177

Câu trả lời của Yuval và David về cơ bản là chính xác; Tổng hợp:

  • Việc sử dụng một biến cục bộ chưa được gán là một lỗi có khả năng và trình biên dịch này có thể được phát hiện với chi phí thấp.
  • Việc sử dụng một trường hoặc phần tử mảng chưa được gán ít có khả năng là một lỗi và khó phát hiện điều kiện hơn trong trình biên dịch. Do đó, trình biên dịch không cố gắng phát hiện việc sử dụng biến chưa được khởi tạo cho các trường và thay vào đó dựa vào việc khởi tạo thành giá trị mặc định để làm cho hành vi của chương trình xác định.

Một người bình luận cho câu trả lời của David hỏi tại sao không thể phát hiện việc sử dụng một trường không được gán thông qua phân tích tĩnh; đây là điểm tôi muốn mở rộng trong câu trả lời này.

Trước hết, đối với bất kỳ biến, cục bộ hoặc mặt khác, trong thực tế không thể xác định chính xác liệu một biến được gán hay không được gán. Xem xét:

bool x;
if (M()) x = true;
Console.WriteLine(x);

Câu hỏi "được x giao?" tương đương với "M () có trả về đúng không?" Bây giờ, giả sử M () trả về giá trị đúng nếu Định lý cuối cùng của Fermat đúng với tất cả các số nguyên nhỏ hơn một trăm triệu và ngược lại là sai. Để xác định xem x có được gán chắc chắn hay không, về cơ bản, trình biên dịch phải tạo ra một bằng chứng về Định lý cuối cùng của Fermat. Trình biên dịch không thông minh.

Vì vậy, những gì trình biên dịch làm thay cho người địa phương là thực hiện một thuật toán nhanhđánh giá quá cao khi một địa phương không được gán chắc chắn. Đó là, nó có một số điểm tích cực sai, trong đó có ghi "Tôi không thể chứng minh rằng địa phương này được chỉ định" mặc dù bạn và tôi biết điều đó. Ví dụ:

bool x;
if (N() * 0 == 0) x = true;
Console.WriteLine(x);

Giả sử N () trả về một số nguyên. Bạn và tôi biết rằng N () * 0 sẽ là 0, nhưng trình biên dịch không biết điều đó. (Lưu ý: trình biên dịch C # 2.0 đã biết điều đó, nhưng tôi đã loại bỏ tối ưu hóa đó, vì thông số kỹ thuật không nói rằng trình biên dịch biết điều đó.)

Được rồi, vậy chúng ta biết gì cho đến nay? Thật không thực tế khi người dân địa phương có được câu trả lời chính xác, nhưng chúng ta có thể đánh giá quá cao việc không được chỉ định với giá rẻ và nhận được một kết quả khá tốt mà lỗi ở phía "làm cho bạn sửa chương trình không rõ ràng của bạn". Điều đó thật tốt. Tại sao không làm điều tương tự cho các lĩnh vực? Đó là, làm một trình kiểm tra chuyển nhượng xác định mà đánh giá quá cao với giá rẻ?

Chà, có bao nhiêu cách để một địa phương được khởi tạo? Nó có thể được chỉ định trong văn bản của phương thức. Nó có thể được gán trong lambda trong văn bản của phương thức; rằng lambda có thể không bao giờ được gọi, vì vậy những bài tập đó không liên quan. Hoặc nó có thể được chuyển thành "out" cho phương thức anothe, tại thời điểm đó chúng ta có thể giả sử nó được gán khi phương thức trở lại bình thường. Đó là những điểm rất rõ ràng tại đó địa phương được chỉ định và chúng ở ngay trong cùng một phương thức mà địa phương được khai báo . Xác định phân công xác định cho người dân địa phương chỉ yêu cầu phân tích địa phương . Các phương thức có xu hướng ngắn - ít hơn một triệu dòng mã trong một phương thức - và vì vậy việc phân tích toàn bộ phương thức khá nhanh chóng.

Bây giờ những gì về các lĩnh vực? Các trường có thể được khởi tạo trong một hàm tạo. Hoặc một trình khởi tạo trường. Hoặc hàm tạo có thể gọi một phương thức cá thể khởi tạo các trường. Hoặc hàm tạo có thể gọi một phương thức ảo khởi tạo các trường. Hoặc hàm tạo có thể gọi một phương thức trong một lớp khác , có thể nằm trong thư viện , khởi tạo các trường. Các trường tĩnh có thể được khởi tạo trong các hàm tạo tĩnh. Các trường tĩnh có thể được khởi tạo bởi các hàm tạo tĩnh khác .

Về cơ bản, trình khởi tạo cho một trường có thể ở bất kỳ đâu trong toàn bộ chương trình , bao gồm cả các phương thức ảo sẽ được khai báo trong các thư viện chưa được viết :

// Library written by BarCorp
public abstract class Bar
{
    // Derived class is responsible for initializing x.
    protected int x;
    protected abstract void InitializeX(); 
    public void M() 
    { 
       InitializeX();
       Console.WriteLine(x); 
    }
}

Có phải là một lỗi để biên dịch thư viện này? Nếu có, BarCorp phải sửa lỗi như thế nào? Bằng cách gán một giá trị mặc định cho x? Nhưng đó là những gì trình biên dịch đã làm.

Giả sử thư viện này là hợp pháp. Nếu FooCorp viết

public class Foo : Bar
{
    protected override void InitializeX() { } 
}

rằng một lỗi? Làm thế nào là trình biên dịch phải tìm ra điều đó? Cách duy nhất là thực hiện phân tích toàn bộ chương trình theo dõi tĩnh khởi tạo của mọi trường trên mọi đường dẫn có thể đi qua chương trình , bao gồm các đường dẫn liên quan đến việc lựa chọn phương thức ảo khi chạy . Vấn đề này có thể khó tùy ý ; nó có thể liên quan đến việc thực hiện mô phỏng hàng triệu đường dẫn điều khiển. Phân tích các luồng điều khiển cục bộ mất vài giây và phụ thuộc vào kích thước của phương pháp. Phân tích các luồng điều khiển toàn cầu có thể mất nhiều giờ vì nó phụ thuộc vào độ phức tạp của mọi phương thức trong chương trình và tất cả các thư viện .

Vậy tại sao không làm một phân tích rẻ hơn mà không phải phân tích toàn bộ chương trình, và chỉ đánh giá quá cao thậm chí còn nghiêm trọng hơn? Chà, đề xuất một thuật toán hoạt động mà không quá khó để viết một chương trình chính xác thực sự biên dịch và nhóm thiết kế có thể xem xét nó. Tôi không biết bất kỳ thuật toán như vậy.

Bây giờ, người bình luận đề xuất "yêu cầu một nhà xây dựng khởi tạo tất cả các trường". Đó không phải là một ý tưởng tồi. Trên thực tế, một ý tưởng không tồi là C # đã có tính năng đó cho các cấu trúc . Một constructor struct được yêu cầu để gán chắc chắn tất cả các trường vào thời điểm ctor trở lại bình thường; hàm tạo mặc định khởi tạo tất cả các trường thành các giá trị mặc định của chúng.

Còn lớp học thì sao? Chà, làm thế nào để bạn biết rằng một constructor đã khởi tạo một trường ? Các ctor có thể gọi một phương thức ảo để khởi tạo các trường và bây giờ chúng ta quay lại vị trí tương tự như trước đây. Structs không có các lớp dẫn xuất; các lớp học có thể. Là một thư viện chứa một lớp trừu tượng cần thiết để chứa một hàm tạo khởi tạo tất cả các trường của nó? Làm thế nào để lớp trừu tượng biết những giá trị nào các trường nên được khởi tạo?

John đề nghị đơn giản là cấm các phương thức gọi trong một ctor trước khi các trường được khởi tạo. Vì vậy, tóm tắt, các tùy chọn của chúng tôi là:

  • Làm cho các thành ngữ lập trình phổ biến, an toàn, thường xuyên được sử dụng bất hợp pháp.
  • Thực hiện một phân tích toàn bộ chương trình đắt tiền khiến cho quá trình biên dịch mất nhiều giờ để tìm kiếm các lỗi có thể không có ở đó.
  • Dựa vào khởi tạo tự động đến các giá trị mặc định.

Nhóm thiết kế đã chọn phương án thứ ba.


1
Câu trả lời tuyệt vời, như thường lệ. Tôi có một câu hỏi mặc dù: Tại sao không tự động gán giá trị mặc định cho các biến cục bộ? Nói cách khác, tại sao không làm cho bool x;tương đương bool x = false; ngay cả trong một phương thức ?
durron597

8
@ durron597: Bởi vì kinh nghiệm đã chỉ ra rằng việc quên gán giá trị cho một địa phương có lẽ là một lỗi. Nếu nó có thể là một lỗi nó rẻ và dễ phát hiện, thì có động cơ tốt để thực hiện hành vi đó là bất hợp pháp hoặc cảnh báo.
Eric Lippert

27

Khi tôi tạo cùng một bool trong phương thức của mình, kiểm tra bool (thay vì trong lớp), tôi gặp lỗi "sử dụng kiểm tra biến cục bộ chưa được gán". Tại sao?

Bởi vì trình biên dịch đang cố gắng ngăn bạn mắc lỗi.

Có phải việc khởi tạo biến của bạn để falsethay đổi bất cứ điều gì trong đường dẫn thực hiện cụ thể này không? Có lẽ là không, xem xét default(bool)là sai dù sao, nhưng nó buộc bạn phải nhận thức rằng điều này đang xảy ra. Môi trường .NET ngăn bạn truy cập "bộ nhớ rác", vì nó sẽ khởi tạo bất kỳ giá trị nào về mặc định của chúng. Tuy nhiên, hãy tưởng tượng đây là một loại tham chiếu và bạn sẽ chuyển một giá trị chưa được khởi tạo (null) cho một phương thức mong đợi một giá trị không null và nhận được NRE khi chạy. Trình biên dịch chỉ đơn giản là cố gắng ngăn chặn điều đó, chấp nhận thực tế rằng điều này đôi khi có thể dẫn đến các bool b = falsebáo cáo.

Eric Lippert nói về điều này trong một bài đăng trên blog :

Lý do tại sao chúng tôi muốn biến điều này thành bất hợp pháp là không, như nhiều người tin, bởi vì biến cục bộ sẽ được khởi tạo thành rác và chúng tôi muốn bảo vệ bạn khỏi rác. Trên thực tế, chúng tôi tự động khởi tạo người dân địa phương theo các giá trị mặc định của họ. (Mặc dù ngôn ngữ lập trình C và C ++ không có, và sẽ vui vẻ cho phép bạn đọc rác từ một địa phương chưa được khởi tạo.) Thay vào đó, đó là vì sự tồn tại của một đường dẫn mã như vậy có thể là một lỗi và chúng tôi muốn ném bạn vào hố chất lượng; bạn nên làm việc chăm chỉ để viết lỗi đó.

Tại sao điều này không áp dụng cho một lĩnh vực lớp học? Chà, tôi cho rằng dòng phải được vẽ ở đâu đó và việc khởi tạo biến cục bộ dễ dàng hơn rất nhiều để chẩn đoán và nhận đúng, trái ngược với các trường lớp. Trình biên dịch có thể làm điều này, nhưng nghĩ về tất cả các kiểm tra có thể cần thực hiện (trong đó một số trong số chúng độc lập với chính mã lớp) để đánh giá xem mỗi trường trong một lớp có được khởi tạo không. Tôi không phải là người thiết kế trình biên dịch, nhưng tôi chắc chắn sẽ khó hơn vì có rất nhiều trường hợp được tính đến, và cũng phải được thực hiện một cách kịp thời . Đối với mọi tính năng bạn phải thiết kế, viết, kiểm tra và triển khai và giá trị của việc thực hiện điều này trái ngược với nỗ lực đưa vào sẽ không xứng đáng và phức tạp.


"hãy tưởng tượng đây là một kiểu tham chiếu và bạn sẽ chuyển đối tượng chưa được khởi tạo này sang một phương thức mong đợi một kiểu khởi tạo" Ý của bạn là: "hãy tưởng tượng đây là một kiểu tham chiếu và bạn đã chuyển mặc định (null) thay vì tham chiếu của một vật"?
Ded repeatator

@Ded repeatator Có. Một phương thức mong đợi một giá trị khác null. Chỉnh sửa phần đó. Hy vọng nó rõ ràng hơn bây giờ.
Yuval Itzchakov

Tôi không nghĩ rằng đó là vì đường vẽ. Mỗi lớp giả sử có một hàm tạo, ít nhất là hàm tạo mặc định. Vì vậy, khi bạn gắn bó với hàm tạo mặc định, bạn sẽ nhận được các giá trị mặc định (trong suốt yên tĩnh). Khi xác định hàm tạo, bạn được mong đợi hoặc phải biết bạn đang làm gì trong nó và trường nào bạn muốn được khởi tạo theo cách bao gồm kiến ​​thức về các giá trị mặc định.
Peter

Ngược lại: Một trường trong một phương thức có thể bằng các giá trị được khai báo và gán cho các đường dẫn thực thi khác nhau. Có thể có các ngoại lệ dễ dàng giám sát cho đến khi bạn xem tài liệu về khung bạn có thể sử dụng hoặc thậm chí trong các phần khác của mã bạn không thể duy trì. Điều này có thể giới thiệu một con đường thực hiện rất phức tạp. Do đó, trình biên dịch gợi ý.
Peter

@Peter Tôi không thực sự hiểu bình luận thứ hai của bạn. Về phần đầu tiên, không có yêu cầu khởi tạo bất kỳ trường nào bên trong hàm tạo. Đó là một thực tế phổ biến . Công việc biên dịch không phải để thực thi một thực hành như vậy. Bạn không thể dựa vào bất kỳ triển khai nào của hàm tạo đang chạy và nói "được rồi, tất cả các trường đều tốt". Eric đã xây dựng rất nhiều câu trả lời của mình về những cách người ta có thể khởi tạo một trường của một lớp và cho thấy sẽ mất bao lâu để tính toán tất cả các cách khởi tạo logic.
Yuval Itzchakov

25

Tại sao các biến cục bộ yêu cầu khởi tạo, nhưng các trường thì không?

Câu trả lời ngắn gọn là mã truy cập các biến cục bộ chưa được khởi tạo có thể được trình biên dịch phát hiện một cách đáng tin cậy, sử dụng phân tích tĩnh. Trong khi đó đây không phải là trường hợp của các lĩnh vực. Vì vậy, trình biên dịch thực thi trường hợp đầu tiên, nhưng không phải trường hợp thứ hai.

Tại sao các biến cục bộ yêu cầu khởi tạo?

Đây không phải là một quyết định thiết kế của ngôn ngữ C #, như được giải thích bởi Eric Lippert . Môi trường CLR và .NET không yêu cầu nó. VB.NET, chẳng hạn, sẽ biên dịch tốt với các biến cục bộ chưa được khởi tạo và trong thực tế, CLR khởi chạy tất cả các biến chưa được khởi tạo thành các giá trị mặc định.

Điều tương tự có thể xảy ra với C #, nhưng các nhà thiết kế ngôn ngữ đã chọn không. Lý do là các biến khởi tạo là một nguồn lỗi rất lớn và do đó, bằng cách bắt buộc khởi tạo, trình biên dịch giúp cắt giảm các lỗi vô ý.

Tại sao các trường không yêu cầu khởi tạo?

Vậy tại sao việc khởi tạo rõ ràng bắt buộc này không xảy ra với các trường trong một lớp? Đơn giản là vì việc khởi tạo rõ ràng đó có thể xảy ra trong quá trình xây dựng, thông qua một thuộc tính được gọi bởi trình khởi tạo đối tượng hoặc thậm chí bởi một phương thức được gọi lâu sau sự kiện. Trình biên dịch không thể sử dụng phân tích tĩnh để xác định xem mọi đường dẫn có thể thông qua mã có dẫn đến biến được khởi tạo rõ ràng trước chúng ta hay không. Làm cho nó sai sẽ gây phiền nhiễu, vì nhà phát triển có thể bị bỏ lại với mã hợp lệ không được biên dịch. Vì vậy, C # hoàn toàn không thực thi nó và CLR được để tự động khởi tạo các trường thành một giá trị mặc định nếu không được đặt rõ ràng.

Còn các loại bộ sưu tập thì sao?

Việc thực thi khởi tạo biến cục bộ của C # bị hạn chế, điều này thường thu hút các nhà phát triển. Hãy xem xét bốn dòng mã sau đây:

string str;
var len1 = str.Length;
var array = new string[10];
var len2 = array[0].Length;

Dòng mã thứ hai sẽ không được biên dịch, vì nó đang cố đọc một biến chuỗi chưa được khởi tạo. Dòng mã thứ tư chỉ biên dịch tốt, như arrayđã được khởi tạo, nhưng chỉ với các giá trị mặc định. Vì giá trị mặc định của chuỗi là null, chúng tôi nhận được một ngoại lệ vào thời gian chạy. Bất cứ ai đã dành thời gian ở đây trên Stack Overflow sẽ biết rằng sự không nhất quán khởi tạo rõ ràng / ngầm định này dẫn đến rất nhiều "Tại sao tôi nhận được một tham chiếu Object Object không được đặt thành một trường hợp của một lỗi đối tượng." câu hỏi


"Trình biên dịch không thể sử dụng phân tích tĩnh để xác định xem mọi đường dẫn có thể thông qua mã có dẫn đến biến được khởi tạo rõ ràng trước chúng ta hay không." Tôi không tin đây là sự thật. Bạn có thể đăng một ví dụ về một chương trình chống phân tích tĩnh không?
John Kugelman

@JohnKugelman, hãy xem xét trường hợp đơn giản public interface I1 { string str {get;set;} }và một phương pháp int f(I1 value) { return value.str.Length; }. Nếu điều này tồn tại trong một thư viện, trình biên dịch không thể biết thư viện đó sẽ được liên kết với cái gì, do đó liệu setcó được gọi trước không get, Trường sao lưu có thể không được khởi tạo một cách rõ ràng, nhưng nó phải biên dịch mã đó.
David Arno

Điều đó đúng, nhưng tôi không mong đợi lỗi sẽ được tạo trong khi biên dịch f. Nó sẽ được tạo khi biên dịch các hàm tạo. Nếu bạn để một hàm tạo với một trường có thể chưa được khởi tạo, đó sẽ là một lỗi. Cũng có thể phải có các hạn chế trong việc gọi các phương thức lớp và getters trước khi tất cả các trường được khởi tạo.
John Kugelman

@JohnKugelman: Tôi sẽ đăng câu trả lời thảo luận về vấn đề bạn nêu ra.
Eric Lippert

4
Điều đó không công bằng. Chúng tôi đang cố gắng để có một bất đồng ở đây!
John Kugelman

10

Những câu trả lời hay ở trên, nhưng tôi nghĩ tôi sẽ đăng một câu trả lời đơn giản / ngắn gọn hơn nhiều để mọi người lười đọc một câu dài (như bản thân tôi).

Lớp học

class Foo {
    private string Boo;
    public Foo() { /** bla bla bla **/ }
    public string DoSomething() { return Boo; }
}

Tài sản Boocó thể hoặc không thể được khởi tạo trong hàm tạo. Vì vậy, khi tìm thấy return Boo;nó không cho rằng nó đã được khởi tạo. Nó chỉ đơn giản là ngăn chặn lỗi.

Chức năng

public string Foo() {
   string Boo;
   return Boo; // triggers error
}

Các { }ký tự xác định phạm vi của một khối mã. Trình biên dịch đi theo các nhánh của các { }khối này theo dõi các công cụ. Nó có thể dễ dàng nói rằng Bookhông được khởi tạo. Lỗi sau đó được kích hoạt.

Tại sao lỗi tồn tại?

Lỗi được đưa ra để giảm số lượng dòng mã cần thiết để làm cho mã nguồn an toàn. Nếu không có lỗi ở trên sẽ như thế này.

public string Foo() {
   string Boo;
   /* bla bla bla */
   if(Boo == null) {
      return "";
   }
   return Boo;
}

Từ hướng dẫn:

Trình biên dịch C # không cho phép sử dụng các biến chưa được khởi tạo. Nếu trình biên dịch phát hiện việc sử dụng một biến có thể chưa được khởi tạo, nó sẽ tạo ra lỗi trình biên dịch CS0165. Để biết thêm thông tin, hãy xem Trường (Hướng dẫn lập trình C #). Lưu ý rằng lỗi này được tạo khi trình biên dịch gặp một cấu trúc có thể dẫn đến việc sử dụng biến không được gán, ngay cả khi mã cụ thể của bạn không. Điều này tránh sự cần thiết của các quy tắc quá phức tạp cho việc gán xác định.

Tham khảo: https://msdn.microsoft.com/en-us/l Library / 4y7h161d.aspx

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.