42

Có ai biết nếu nó có thể xác định tương đương với "trình nạp lớp tùy chỉnh java" trong .NET không?Tương đương với Trình nạp Lớp trong .NET

Để cung cấp cho một chút nền:

Tôi đang trong quá trình phát triển một ngôn ngữ lập trình mới mà mục tiêu CLR, được gọi là "Liberty". Một trong những tính năng của ngôn ngữ là khả năng xác định "các hàm tạo kiểu", là các phương thức được trình biên dịch thực thi tại thời gian biên dịch và tạo ra các kiểu làm đầu ra. Họ là loại một sự tổng quát của generics (ngôn ngữ không có Generics bình thường trong nó), và cho phép mã như thế này phải được viết (trong "Liberty" cú pháp):

var t as tuple<i as int, j as int, k as int>; 
t.i = 2; 
t.j = 4; 
t.k = 5; 

đâu "tuple" được định nghĩa như vậy :

public type tuple(params variables as VariableDeclaration[]) as TypeDeclaration 
{ 
    //... 
} 

Trong ví dụ cụ thể này, trình tạo kiểu tuple cung cấp một cái gì đó tương tự như loại ẩn danh trong VB và C#.

Tuy nhiên, không giống như các loại ẩn danh, "bộ dữ liệu" có tên và có thể được sử dụng bên trong chữ ký phương thức công khai.

Điều này có nghĩa là tôi cần một cách cho loại cuối cùng kết thúc được phát ra bởi trình biên dịch để có thể chia sẻ trên nhiều cụm. Ví dụ, tôi muốn

tuple<x as int> quy định tại hội A đến kết thúc là loại giống như tuple<x as int> quy định tại hội B.

Vấn đề với điều này, tất nhiên, là hội A và B hội sẽ được biên dịch vào các thời điểm khác nhau, điều đó có nghĩa là cả hai đều sẽ phát ra các phiên bản không tương thích của riêng kiểu tuple.

Tôi nhìn vào sử dụng một số loại "loại tẩy xoá" để làm điều này, vì vậy mà tôi sẽ có một thư viện chia sẻ với một loạt các loại như thế này (là "Liberty" cú pháp này):

class tuple<T> 
{ 
    public Field1 as T; 
} 

class tuple<T, R> 
{ 
    public Field2 as T; 
    public Field2 as R; 
} 

và sau đó chỉ chuyển hướng truy cập từ các trường i, j và k tới Field1, Field2Field3.

Tuy nhiên đó không thực sự là một lựa chọn khả thi. Điều này có nghĩa là tại thời điểm biên dịch tuple<x as int>tuple<y as int> sẽ kết thúc là các loại khác nhau, trong khi thời gian chạy chúng sẽ được coi là cùng loại. Điều đó sẽ gây ra nhiều vấn đề cho những thứ như bình đẳng và nhận dạng loại. Đó là quá rò rỉ của một trừu tượng cho thị hiếu của tôi.

Các tùy chọn có thể khác sẽ là sử dụng "đối tượng túi trạng thái". Tuy nhiên, bằng cách sử dụng một túi nhà nước sẽ đánh bại toàn bộ mục đích của việc có hỗ trợ cho "loại nhà thầu" trong ngôn ngữ. Ý tưởng có để cho phép "mở rộng ngôn ngữ tùy chỉnh" để tạo ra các loại mới tại thời gian biên dịch mà trình biên dịch có thể làm kiểm tra kiểu tĩnh với.

Trong Java, điều này có thể được thực hiện bằng cách sử dụng trình tải lớp tùy chỉnh. Về cơ bản, mã sử dụng các loại tuple có thể được phát ra mà không thực sự xác định loại trên đĩa. Sau đó, một trình nạp lớp "tùy chỉnh" có thể được định nghĩa sẽ tự động tạo kiểu tuple khi chạy. Điều đó sẽ cho phép kiểm tra kiểu tĩnh bên trong trình biên dịch, và sẽ thống nhất các kiểu tuple trên các biên dịch biên dịch.

Thật không may, tuy nhiên, CLR không cung cấp hỗ trợ cho việc tải lớp tùy chỉnh.Tất cả tải trong CLR được thực hiện ở cấp độ lắp ráp. Có thể định nghĩa một assembly riêng biệt cho mỗi "kiểu xây dựng", nhưng điều đó sẽ dẫn đến các vấn đề hiệu suất rất nhanh (có nhiều assembly chỉ có một loại trong chúng sẽ sử dụng quá nhiều tài nguyên).

Vì vậy, những gì tôi muốn biết là:

Có thể mô phỏng một cái gì đó như Java Class xúc lật trong .NET, nơi tôi có thể phát ra một tham chiếu đến một loại không tồn tại trong và sau đó tự động tạo một tham chiếu đến kiểu đó khi chạy trước mã mà nhu cầu sử dụng nó chạy?

LƯU Ý:

* Tôi thực sự đã biết câu trả lời cho câu hỏi, mà tôi cung cấp như một câu trả lời dưới đây. Tuy nhiên, nó đã cho tôi khoảng 3 ngày nghiên cứu, và khá một chút của IL hacking để đến với một giải pháp. Tôi nghĩ rằng nó sẽ là một ý tưởng tốt để tài liệu nó ở đây trong trường hợp bất cứ ai khác chạy vào cùng một vấn đề. *

+0

Oh wow, bài đăng đầu tiên tôi từng nghĩ nên có tiêu đề Chương. Thông tin tuyệt vời! Cảm ơn vì đăng! –

Trả lời

51

Câu trả lời là có, nhưng giải pháp hơi phức tạp một chút.

Không gian tên System.Reflection.Emit xác định các loại cho phép tạo hội đồng được tạo động. Chúng cũng cho phép các assembly được tạo ra được xác định theo từng bước. Nói cách khác, có thể thêm các kiểu vào assembly động, thực thi mã được sinh ra, và sau đó thêm các kiểu khác vào assembly.

Lớp System.AppDomain cũng xác định sự kiện AssemblyResolve kích hoạt bất cứ khi nào khung không tải được lắp ráp. Bằng cách thêm một trình xử lý cho sự kiện đó, có thể định nghĩa một assembly "thời gian chạy" đơn mà tất cả các kiểu "được xây dựng" được đặt. Mã được tạo bởi trình biên dịch sử dụng một kiểu được xây dựng sẽ tham chiếu đến một kiểu trong assembly chạy. Bởi vì assembly thời gian thực không tồn tại trên đĩa, sự kiện AssemblyResolve sẽ được kích hoạt lần đầu tiên mã được biên dịch thử truy cập một kiểu được xây dựng. Trình xử lý cho sự kiện sau đó sẽ tạo ra cụm động và trả về CLR.

Thật không may, có một vài điểm phức tạp để việc này hoạt động. Vấn đề đầu tiên là đảm bảo rằng trình xử lý sự kiện sẽ luôn được cài đặt trước khi mã được biên dịch được chạy. Với một ứng dụng giao diện điều khiển, điều này rất dễ dàng. Mã để hookup trình xử lý sự kiện chỉ có thể được thêm vào phương thức Main trước khi mã khác chạy. Tuy nhiên, đối với các thư viện lớp, không có phương pháp chính. Một dll có thể được tải như là một phần của một ứng dụng được viết bằng ngôn ngữ khác, do đó, nó không thực sự có thể giả định luôn có một phương thức chính có sẵn để kết nối mã xử lý sự kiện.

Vấn đề thứ hai là đảm bảo rằng tất cả các loại tham chiếu đều được chèn vào cụm động trước khi sử dụng bất kỳ mã nào tham chiếu đến chúng. Lớp System.AppDomain cũng định nghĩa một sự kiện TypeResolve được thực thi bất cứ khi nào CLR không thể giải quyết một loại trong một hội động. Nó cung cấp cho trình xử lý sự kiện cơ hội để xác định loại bên trong assembly động trước khi mã sử dụng nó chạy. Tuy nhiên, sự kiện đó sẽ không hoạt động trong trường hợp này. CLR sẽ không kích hoạt sự kiện cho các assembly được "tham chiếu tĩnh" bởi các assembly khác, ngay cả khi assembly được tham chiếu được định nghĩa động. Điều này có nghĩa rằng chúng ta cần một cách để chạy mã trước khi bất kỳ mã nào khác trong assembly được biên dịch chạy và nó tự động tiêm các kiểu nó cần vào assembly chạy nếu chúng chưa được định nghĩa.Nếu không, khi CLR cố gắng tải các kiểu đó, nó sẽ thông báo rằng assembly động không chứa các kiểu mà chúng cần và sẽ ném một ngoại lệ tải kiểu.

May mắn thay, CLR cung cấp giải pháp cho cả hai vấn đề: Trình khởi tạo mô-đun. Trình khởi tạo mô-đun tương đương với một "hàm tạo lớp tĩnh", ngoại trừ việc nó khởi tạo toàn bộ một mô-đun, không chỉ là một lớp đơn lẻ. Ba bên, CLR sẽ:

  1. Chạy hàm tạo mô-đun trước bất kỳ loại nào bên trong mô-đun được truy cập.
  2. Đảm bảo rằng chỉ những loại được truy cập trực tiếp bởi trình xây dựng mô-đun mới được tải trong khi đang thực hiện
  3. Không cho phép mã bên ngoài mô-đun truy cập bất kỳ thành viên nào của nó cho đến sau khi người xây dựng hoàn tất.

Nó thực hiện điều này cho tất cả các hội đồng, bao gồm cả thư viện lớp và thực thi, và cho EXE sẽ chạy hàm tạo mô-đun trước khi thực hiện phương pháp chính.

Xem điều này blog post để biết thêm thông tin về các nhà thầu.

Trong mọi trường hợp, một giải pháp hoàn chỉnh cho vấn đề của tôi đòi hỏi phải có một vài miếng:

  1. Định nghĩa lớp sau đây, được định nghĩa bên trong một "runtime dll ngôn ngữ", mà được tham chiếu bởi tất cả các cụm sản xuất bởi trình biên dịch (đây là mã C#).

    using System; 
    using System.Collections.Generic; 
    using System.Reflection; 
    using System.Reflection.Emit; 
    
    namespace SharedLib 
    { 
        public class Loader 
        { 
         private Loader(ModuleBuilder dynamicModule) 
         { 
          m_dynamicModule = dynamicModule; 
          m_definedTypes = new HashSet<string>(); 
         } 
    
         private static readonly Loader m_instance; 
         private readonly ModuleBuilder m_dynamicModule; 
         private readonly HashSet<string> m_definedTypes; 
    
         static Loader() 
         { 
          var name = new AssemblyName("$Runtime"); 
          var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run); 
          var module = assemblyBuilder.DefineDynamicModule("$Runtime"); 
          m_instance = new Loader(module); 
          AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve); 
         } 
    
         static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) 
         { 
          if (args.Name == Instance.m_dynamicModule.Assembly.FullName) 
          { 
           return Instance.m_dynamicModule.Assembly; 
          } 
          else 
          { 
           return null; 
          } 
         } 
    
         public static Loader Instance 
         { 
          get 
          { 
           return m_instance; 
          } 
         } 
    
         public bool IsDefined(string name) 
         { 
          return m_definedTypes.Contains(name); 
         } 
    
         public TypeBuilder DefineType(string name) 
         { 
          //in a real system we would not expose the type builder. 
          //instead a AST for the type would be passed in, and we would just create it. 
          var type = m_dynamicModule.DefineType(name, TypeAttributes.Public); 
          m_definedTypes.Add(name); 
          return type; 
         } 
        } 
    } 
    

    Lớp định nghĩa một singleton chứa một tham chiếu đến assembly năng động mà các loại xây dựng sẽ được tạo trong. Nó cũng giữ một "băm set" mà các cửa hàng tập các loại đã được tạo ra tự động, và cuối cùng xác định một thành viên có thể được sử dụng để xác định loại. Ví dụ này chỉ trả về một cá thể System.Reflection.Emit.TypeBuilder mà sau đó có thể được sử dụng để định nghĩa lớp đang được tạo ra. Trong một hệ thống thực sự, phương pháp này có lẽ sẽ lấy một biểu diễn AST của lớp, và chỉ làm thế hệ đó là bản thân.

  2. hội Biên soạn phát ra hai tài liệu tham khảo sau đây (hiển thị trong cú pháp ILASM):

    .assembly extern $Runtime 
    { 
        .ver 0:0:0:0 
    } 
    .assembly extern SharedLib 
    { 
        .ver 1:0:0:0 
    } 
    

    Here "SharedLib" là thư viện runtime xác định trước của ngôn ngữ bao gồm các lớp "Loader" định nghĩa ở trên và "$ Runtime "là hội đồng thời gian chạy động mà các loại cấu trúc sẽ được chèn vào.

  3. Một "hàm tạo mô-đun" bên trong mỗi assembly được biên dịch bằng ngôn ngữ.

    Theo như tôi biết, không có ngôn ngữ .NET cho phép Mô-đun Constructors được xác định trong nguồn. Trình biên dịch C++/CLI là trình biên dịch duy nhất tôi biết để tạo ra chúng. Trong IL, chúng trông như thế này, được định nghĩa trực tiếp trong mô-đun và không nằm trong bất kỳ định nghĩa kiểu nào:

    .method privatescope specialname rtspecialname static 
         void .cctor() cil managed 
    { 
        //generate any constructed types dynamically here... 
    } 
    

    Đối với tôi, tôi không phải viết IL tùy chỉnh để làm việc này. Tôi đang viết một trình biên dịch, vì thế việc tạo mã không phải là một vấn đề.

    Trong trường hợp của một assembly mà sử dụng các loại tuple<i as int, j as int>tuple<x as double, y as double, z as double> các nhà xây dựng mô-đun sẽ cần phải tạo ra các loại như sau (ở đây trong C# cú pháp):

    class Tuple_i_j<T, R> 
    { 
        public T i; 
        public R j; 
    } 
    
    class Tuple_x_y_z<T, R, S> 
    { 
        public T x; 
        public R y; 
        public S z; 
    } 
    

    Các lớp tuple được tạo ra như kiểu generic để giải quyết các vấn đề về trợ năng. Điều đó sẽ cho phép mã trong assembly được biên dịch sử dụng tuple<x as Foo>, trong đó Foo là một dạng không công khai.

    Phần nội dung của các nhà xây dựng mô-đun đã làm điều này (ở đây chỉ hiển thị một loại, và viết bằng C# cú pháp) sẽ trông như thế này:

    var loader = SharedLib.Loader.Instance; 
    lock (loader) 
    { 
        if (! loader.IsDefined("$Tuple_i_j")) 
        { 
         //create the type. 
         var Tuple_i_j = loader.DefineType("$Tuple_i_j"); 
         //define the generic parameters <T,R> 
         var genericParams = Tuple_i_j.DefineGenericParameters("T", "R"); 
         var T = genericParams[0]; 
         var R = genericParams[1]; 
         //define the field i 
         var fieldX = Tuple_i_j.DefineField("i", T, FieldAttributes.Public); 
         //define the field j 
         var fieldY = Tuple_i_j.DefineField("j", R, FieldAttributes.Public); 
         //create the default constructor. 
         var constructor= Tuple_i_j.DefineDefaultConstructor(MethodAttributes.Public); 
    
         //"close" the type so that it can be used by executing code. 
         Tuple_i_j.CreateType(); 
        } 
    } 
    

Vì vậy, trong mọi trường hợp, đây là cơ chế Tôi đã có thể đưa ra để cho phép tương đương thô của bộ nạp lớp tùy chỉnh trong CLR.

Có ai biết cách dễ dàng hơn để thực hiện việc này không?

+0

Ugh, định nghĩa hàm dựng mô-đun của bạn khác với hàm tạo lớp thông thường như thế nào? Sự khác biệt trong việc sử dụng 'privatescope' trái ngược với' hidebysig' riêng? –

+0

Ahh, chỉ cần tìm ra. Không có sự khác biệt ngoại trừ mô-đun cctor không được đặt trong bất kỳ loại cụ thể nào. Không biết bạn thậm chí có thể làm điều đó :) –

-5

Tôi nghĩ đây là loại thứ mà DLR được cho là cung cấp trong C# 4.0. Rất khó để có được thông tin, nhưng có lẽ chúng ta sẽ tìm hiểu thêm tại PDC08. Mong đợi chờ đợi để xem giải pháp C# 3 của bạn mặc dù ... Tôi đoán nó sử dụng các loại vô danh.

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