2012-04-18 52 views
12

phép nói rằng chúng tôi đã theo mẫu mã trong C#:Tại sao biên dịch C# gọi phương thức sản để gọi phương thức BaseClass trong IL

class BaseClass 
    { 
    public virtual void HelloWorld() 
    { 
     Console.WriteLine("Hello Tarik"); 
    } 
    } 

    class DerivedClass : BaseClass 
    { 
    public override void HelloWorld() 
    { 
     base.HelloWorld(); 
    } 
    } 

    class Program 
    { 
    static void Main(string[] args) 
    { 
     DerivedClass derived = new DerivedClass(); 
     derived.HelloWorld(); 
    } 
    } 

Khi tôi ildasmed đoạn mã sau:

.method private hidebysig static void Main(string[] args) cil managed 
{ 
    .entrypoint 
    // Code size  15 (0xf) 
    .maxstack 1 
    .locals init ([0] class EnumReflection.DerivedClass derived) 
    IL_0000: nop 
    IL_0001: newobj  instance void EnumReflection.DerivedClass::.ctor() 
    IL_0006: stloc.0 
    IL_0007: ldloc.0 
    IL_0008: callvirt instance void EnumReflection.BaseClass::HelloWorld() 
    IL_000d: nop 
    IL_000e: ret 
} // end of method Program::Main 

Tuy nhiên, csc .exe đã chuyển đổi derived.HelloWorld(); ->callvirt instance void EnumReflection.BaseClass::HelloWorld(). Tại sao vậy? Tôi đã không đề cập đến BaseClass ở bất cứ đâu trong phương thức Main.

Và cũng có thể nếu nó đang gọi BaseClass::HelloWorld() thì tôi mong đợi call thay vì callvirt vì có vẻ như gọi trực tiếp đến phương thức BaseClass::HelloWorld().

+2

Chỉ cần ném ý tưởng này ra khỏi đó, nhưng tôi không biết - đây có thể là tối ưu hóa trình biên dịch vì phương pháp của bạn chỉ đơn giản là gọi triển khai cơ sở. – payo

+0

@payo no; ngay cả khi ghi đè có một số logic duy nhất, callvirt sẽ giống nhau. – phoog

+0

@phoog điều này không giống nhau đối với tất cả các ngôn ngữ - tôi có thể thấy trường hợp này như thế nào đối với C# tho – payo

Trả lời

20

Cuộc gọi đến BaseClass :: HelloWorld vì BaseClass là lớp xác định phương thức. Cách điều phối ảo hoạt động trong C# là phương thức được gọi trên lớp cơ sở, và hệ thống gửi đi ảo chịu trách nhiệm đảm bảo rằng việc ghi đè xuất phát nhiều nhất của phương thức được gọi.

Câu trả lời này của Eric Lippert là rất nhiều thông tin: https://stackoverflow.com/a/5308369/385844

Như là loạt blog của mình về chủ đề này: http://blogs.msdn.com/b/ericlippert/archive/tags/virtual+dispatch/

Bạn có bất cứ ý tưởng tại sao điều này được thực hiện theo cách này? Điều gì sẽ xảy ra nếu nó được gọi là phương thức ToString lớp dẫn xuất trực tiếp? Theo cách này, tôi không cảm nhận được điều này với tôi ngay từ cái nhìn đầu tiên ...

Nó thực hiện theo cách này vì trình biên dịch không theo dõi kiểu thời gian chạy của đối tượng, chỉ là kiểu tham chiếu thời gian. Với mã bạn đã đăng, thật dễ dàng để thấy rằng cuộc gọi sẽ đi đến triển khai phương pháp của DerivedClass. Nhưng giả sử biến derived được khởi tạo như thế này:

Derived derived = GetDerived(); 

Có thể là GetDerived() trả về một thể hiện của StillMoreDerived. Nếu StillMoreDerived (hoặc bất kỳ lớp nào giữa DerivedStillMoreDerived trong chuỗi thừa kế) ghi đè phương pháp, thì sẽ không chính xác khi gọi phương thức thực hiện phương pháp này là Derived.

Để tìm tất cả các giá trị có thể, một biến có thể chứa thông qua phân tích tĩnh là giải quyết vấn đề dừng. Với một hội đồng .NET, vấn đề thậm chí còn tồi tệ hơn, bởi vì một hội đồng có thể không phải là một chương trình hoàn chỉnh. Vì vậy, số lượng các trường hợp trình biên dịch có thể chứng minh hợp lý rằng derived không giữ tham chiếu đến đối tượng có nguồn gốc nhiều hơn (hoặc tham chiếu null) sẽ nhỏ.

Chi phí để thêm logic này là bao nhiêu để có thể phát hành call thay vì hướng dẫn callvirt? Không nghi ngờ gì, chi phí sẽ cao hơn nhiều so với lợi ích nhỏ có được.

+0

Làm cho tinh thần với tôi :) Cảm ơn câu trả lời. – Tarik

+0

Điều đó có nghĩa là, nói cách khác, những gì anh ta thực sự thấy là tính đa hình làm việc? IL được xử lý theo cách này sao cho khi thực thi phương thức ghi đè thích hợp được gọi? –

+0

@BradRem đó chính xác là ý nghĩa của nó. – phoog

9

Cách suy nghĩ về điều này là các phương pháp ảo xác định một "khe" mà bạn có thể đặt một phương thức vào lúc chạy. Khi chúng ta phát ra một hướng dẫn callvirt, chúng ta đang nói "trong thời gian chạy, hãy nhìn xem cái gì nằm trong khe này và gọi nó".

Vị trí được xác định theo thông tin phương pháp về loại đã khai báo phương thức ảo, không phải loại ghi đè.

Sẽ hoàn toàn hợp pháp khi phát ra lời gọi đến phương thức có nguồn gốc; thời gian chạy sẽ nhận ra rằng phương thức dẫn xuất là cùng một vị trí như phương thức cơ sở và kết quả sẽ giống hệt nhau. Nhưng không bao giờ có bất kỳ lý do nào lý do để thực hiện điều đó. Rõ ràng hơn nếu chúng tôi xác định vị trí bằng cách xác định loại tuyên bố vị trí đó.

1

Lưu ý rằng điều này xảy ra ngay cả khi bạn khai báo DerivedClasssealed.

C# sử dụng toán tử callvirt để gọi bất kỳ phương thức thể hiện nào (virtual hoặc không) để tự động kiểm tra đối tượng tham chiếu - để tăng NullReferenceException tại điểm mà phương thức được gọi. Nếu không, NullReferenceException sẽ chỉ được nâng lên khi sử dụng thực tế đầu tiên của bất kỳ cá thể thành viên nào của lớp bên trong phương thức, điều này có thể gây ngạc nhiên. Nếu không có thành viên cá thể nào được sử dụng, phương thức này có thể thực sự hoàn tất thành công mà không bao giờ tăng ngoại lệ.

Bạn cũng nên nhớ rằng IL không được thực thi trực tiếp. Đầu tiên nó được biên dịch thành các hướng dẫn nguyên gốc của trình biên dịch JIT - và nó thực hiện một số tối ưu hóa tùy thuộc vào việc bạn đang gỡ lỗi quá trình. Tôi thấy rằng JIT x86 cho CLR 2.0 đã phác thảo một phương thức không ảo nhưng được gọi là phương thức ảo - nó cũng được in Console.WriteLine!

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