2010-09-02 42 views
6

Một câu hỏi về SO cảm hứng cho tôi để thử mã này trong C#:Tạo một thể hiện của một lớp trong hàm tạo tĩnh của nó - tại sao nó được cho phép?

class Program 
{ 
    static Program() 
    { 
     new Program().Run(); 
    } 

    static void Main(string[] args) { } 

    void Run() 
    { 
     System.Console.WriteLine("Running"); 
    } 
} 

này in ra "Chạy" khi chạy.

Tôi thực sự mong đợi trình biên dịch khiếu nại về điều này. Sau khi tất cả, nếu lớp chưa được khởi tạo bởi hàm tạo tĩnh; làm thế nào chúng ta có thể chắc chắn rằng nó là hợp lệ để gọi các phương pháp trên đó?

Vậy tại sao trình biên dịch không hạn chế chúng tôi thực hiện việc này? Có tình huống sử dụng quan trọng nào cho điều này không?

Chỉnh sửa

Tôi biết về mẫu Singleton; điểm trong câu hỏi là tại sao tôi có thể gọi một phương thức trên cá thể trước khi constructor tĩnh của tôi kết thúc. Cho đến nay câu trả lời của JaredPar có một số lý do tốt về điều này.

+1

Với mẫu đơn, thực tế là * thực hành tốt nhất * để khởi tạo lớp kèm theo trong hàm tạo tĩnh (hoặc trong trình khởi tạo trường tĩnh, số tiền tương tự) –

Trả lời

6

Điều này được cho phép bởi vì không cho phép nó là số nhiều hơn tồi tệ hơn. Mã số như thế này sẽ bế tắc nặng nề:

class A { 
    public static readonly A a; 
    public static readonly B b; 
    static A() { 
     b = new B(); 
     a = B.a; 
    } 
} 

class B { 
    public static readonly A a; 
    public static readonly B b; 
    static B() { 
     a = new A(); 
     b = A.b; 
    } 
} 

Tất nhiên bạn chỉ một khẩu súng được nạp vào chân bạn.

Hành vi này được diễn tả trong CLI Spec (ECMA 335) phân vùng II, Chương 10.5.3.2 "đảm bảo thư giãn":

Một loại có thể được đánh dấu bằng beforefieldinit thuộc tính (§10.1.6) để chỉ ra rằng các đảm bảo được quy định trong §10.5.3.1 không nhất thiết phải được yêu cầu. Cụ thể, yêu cầu cuối cùng ở trên không cần phải được cung cấp: trình khởi tạo kiểu không cần phải được thực hiện trước khi một phương thức tĩnh được gọi hoặc tham chiếu.

[Lý do: Khi mã có thể được thực hiện trong nhiều miền ứng dụng, nó trở nên đặc biệt tốn kém để đảm bảo đảm bảo cuối cùng này. Đồng thời, việc kiểm tra các cơ quan lớn của mã được quản lý đã chỉ ra rằng sự đảm bảo cuối cùng này hiếm khi được yêu cầu, vì các trình khởi tạo kiểu hầu như luôn là các phương thức đơn giản để khởi tạo các trường tĩnh .Để nó lên đến máy phát điện CIL (và do đó, có thể, với lập trình viên) để quyết định liệu đảm bảo này là cần thiết do đó cung cấp hiệu quả khi nó được mong muốn với chi phí đảm bảo nhất quán.
cuối Lý]

Các biên dịch C# thực sự phát ra các beforefieldinit thuộc tính trên một lớp:

.class private auto ansi beforefieldinit ConsoleApplication2.Program 
     extends [mscorlib]System.Object 
{ 
    // etc... 
} 
0

Trình xây dựng tĩnh chỉ có thể khởi tạo thành viên lớp tĩnh, điều này không liên quan đến các phiên bản lớp và các thành viên lớp không tĩnh thông thường.

6

Câu hỏi hơi khác.

Trình biên dịch ngăn bạn làm việc này như thế nào?

Chắc chắn nó rất dễ phát hiện trong mẫu của bạn, nhưng về mẫu này thì sao?

class Program { 
    static void Fun() { 
    new Program(); 
    } 
    static Program() { 
    Fun(); 
    } 
} 

Cách thức bạn có thể lừa trình biên dịch cho phép điều này hầu như vô tận. Ngay cả khi trình biên dịch có tất cả các câu trả lời bạn vẫn có thể đánh bại nó với sự phản ánh.

Cuối cùng, mặc dù điều này thực sự hợp pháp, nếu một chút nguy hiểm, mã trong cả C# và IL. Nó là an toàn để làm điều này miễn là bạn cẩn thận về việc truy cập tĩnh từ bên trong mã này. Nó cũng hữu ích/có thể cần thiết cho một số mẫu như Singleton's

+1

+1, ví dụ điển hình. – driis

0

Những gì bạn có thể không nhận ra là cho mỗi lớp mà không có một constructor không tĩnh , trình biên dịch sẽ tạo ra một trình biên dịch. Điều này khác với constructor tĩnh của bạn, khi bạn đun sôi nó xuống MSIL thì ít hơn một lá cờ nói với CLR "Này, chạy mã này trước khi bạn chạy cái gì trong main()". Vì vậy, mã của constructor tĩnh của bạn được thực thi đầu tiên. Nó khởi tạo một đối tượng Program-scoped cục bộ bằng cách sử dụng hàm tạo NON-static được tạo ra phía sau hậu trường, và một khi đã khởi tạo, Run() được gọi trên đối tượng. Sau đó, bởi vì bạn chưa lưu trữ đối tượng mới này ở bất kỳ đâu, nó được xử lý khi hàm tạo kết thúc thực hiện. Hàm main() sau đó chạy (và không làm gì cả).

Hãy thử mở rộng này:

class Program 
{ 
    static Program() 
    { 
     new Program().Run(); 
    } 

    public Program() 
    { 
     Console.WriteLine("Instantiating a Program"); 
    } 

    public override void Finalize() 
    { 
     Console.WriteLine("Finalizing a Program"); 
    } 

    static void Main(string[] args) { Console.WriteLine("main() called"); } 

    void Run() 
    { 
     System.Console.WriteLine("Running"); 
    } 
} 

Xem những gì đầu ra là. Tôi đoán là nó sẽ giống như thế này:

Instantiating a Program 
Running 
Finalizing a Program 
main() called 

Hai dòng cuối cùng có thể được hoán đổi vì thu gom rác thải có thể không có cơ hội để tiêu diệt các ví dụ trước khi chính bắt đầu chạy (GC chạy trong một thread được quản lý riêng biệt, và vì vậy nó hoạt động theo thời gian riêng của nó trong suốt vòng đời của quá trình), nhưng cá thể IS là cục bộ đối với hàm tạo tĩnh trong phạm vi và do đó được đánh dấu để thu thập trước khi hàm main() bắt đầu chạy. Vì vậy, nếu bạn gọi Thread.Sleep (1000) trong main() trước khi in tin nhắn, GC sẽ thu thập đối tượng trong thời gian đó.

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