Bagaimana cara membuat kelas secara dinamis?


222

Saya memiliki kelas yang terlihat seperti ini:

public class Field
{
    public string FieldName;
    public string FieldType;
}

Dan objek List<Field>dengan nilai:

{"EmployeeID","int"},
{"EmployeeName","String"},
{"Designation","String"}

Saya ingin membuat kelas yang terlihat seperti ini:

Class DynamicClass
{
    int EmployeeID,
    String EmployeeName,
    String Designation
}

Apakah ada cara untuk melakukan ini?

Saya ingin ini dihasilkan saat runtime. Saya tidak ingin file CS fisik berada di sistem file saya.


4
Apakah Anda ingin menggunakan kelas itu di runtime atau hanya menghasilkan file?
Damian Leszczyński - Vash

Saya ingin ini dihasilkan dalam runtime. Saya tidak ingin file CS fisik berada di sistem file saya. Maaf karena tidak menyebutkan itu sebelumnya.
ashwnacharya

16
Bisakah Anda memberi kami gambaran kasar tentang apa yang ingin Anda lakukan dengan kelas ini?
Justin

3
@Justin mengimplementasikan antarmuka yang diselesaikan dengan runtime, misalnya.
AgentFire

Orang bisa memberinya makan untukSystem.ServiceModel.ChannelFactory<MyDynamicInterface>
Ilya Semenov

Jawaban:


298

Ya, Anda bisa menggunakan System.Reflection.Emitnamespace untuk ini. Ini tidak lurus ke depan jika Anda tidak memiliki pengalaman dengan itu, tetapi tentu saja mungkin.

Sunting: Kode ini mungkin cacat, tetapi itu akan memberi Anda ide umum dan semoga awal yang baik menuju tujuan.

using System;
using System.Reflection;
using System.Reflection.Emit;

namespace TypeBuilderNamespace
{
    public static class MyTypeBuilder
    {
        public static void CreateNewObject()
        {
            var myType = CompileResultType();
            var myObject = Activator.CreateInstance(myType);
        }
        public static Type CompileResultType()
        {
            TypeBuilder tb = GetTypeBuilder();
            ConstructorBuilder constructor = tb.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);

            // NOTE: assuming your list contains Field objects with fields FieldName(string) and FieldType(Type)
            foreach (var field in yourListOfFields)
                CreateProperty(tb, field.FieldName, field.FieldType);

            Type objectType = tb.CreateType();
            return objectType;
        }

        private static TypeBuilder GetTypeBuilder()
        {
            var typeSignature = "MyDynamicType";
            var an = new AssemblyName(typeSignature);
            AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run);
            ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
            TypeBuilder tb = moduleBuilder.DefineType(typeSignature,
                    TypeAttributes.Public |
                    TypeAttributes.Class |
                    TypeAttributes.AutoClass |
                    TypeAttributes.AnsiClass |
                    TypeAttributes.BeforeFieldInit |
                    TypeAttributes.AutoLayout,
                    null);
            return tb;
        }

        private static void CreateProperty(TypeBuilder tb, string propertyName, Type propertyType)
        {
            FieldBuilder fieldBuilder = tb.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);

            PropertyBuilder propertyBuilder = tb.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);
            MethodBuilder getPropMthdBldr = tb.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes);
            ILGenerator getIl = getPropMthdBldr.GetILGenerator();

            getIl.Emit(OpCodes.Ldarg_0);
            getIl.Emit(OpCodes.Ldfld, fieldBuilder);
            getIl.Emit(OpCodes.Ret);

            MethodBuilder setPropMthdBldr =
                tb.DefineMethod("set_" + propertyName,
                  MethodAttributes.Public |
                  MethodAttributes.SpecialName |
                  MethodAttributes.HideBySig,
                  null, new[] { propertyType });

            ILGenerator setIl = setPropMthdBldr.GetILGenerator();
            Label modifyProperty = setIl.DefineLabel();
            Label exitSet = setIl.DefineLabel();

            setIl.MarkLabel(modifyProperty);
            setIl.Emit(OpCodes.Ldarg_0);
            setIl.Emit(OpCodes.Ldarg_1);
            setIl.Emit(OpCodes.Stfld, fieldBuilder);

            setIl.Emit(OpCodes.Nop);
            setIl.MarkLabel(exitSet);
            setIl.Emit(OpCodes.Ret);

            propertyBuilder.SetGetMethod(getPropMthdBldr);
            propertyBuilder.SetSetMethod(setPropMthdBldr);
        }
    }
}

2
Hebat !! Bisakah Anda juga memberi tahu saya cara membuat objek jenis yang dikembalikan oleh Metode CompileResultType ()?
ashwnacharya

4
Anda dapat menggunakan System.Activator untuk itu. Saya akan memperbarui jawabannya dengan sebuah contoh.
danijels

4
Perhatikan juga bahwa Anda harus menggunakan refleksi untuk memeriksa, membaca, dan memperbarui bidang dalam tipe dinamis Anda. Jika Anda ingin intellisense dan tanpa refleksi, Anda harus memiliki kelas dasar statis atau antarmuka yang mewarisi dari kelas dinamis Anda dan dapat digunakan. Dalam hal ini Anda dapat memodifikasi metode GetTypeBuilder () dan mengubah panggilan moduleBuilder.DefineType untuk memasukkan tipe statis sebagai parameter terakhir (null sekarang)
danijels


3
@bugz gunakan kode di atas untuk membuat kelas, maka di kelas dasar Anda dapat menambahkan metode ini: public void SetValue <T> (nama string, nilai T) {GetType (). GetProperty (nama) .SetValue (ini, nilai ); }
stricq

71

Ini akan membutuhkan beberapa pekerjaan, tetapi tentu saja bukan tidak mungkin.

Apa yang telah saya lakukan adalah:

  • Buat sumber C # dalam sebuah string (tidak perlu menulis ke file),
  • Jalankan melalui Microsoft.CSharp.CSharpCodeProvider(CompileAssemblyFromSource)
  • Temukan Jenis yang dihasilkan
  • Dan buat instance dari Tipe itu ( Activator.CreateInstance)

Dengan cara ini Anda dapat menangani kode C # yang sudah Anda ketahui, daripada harus memancarkan MSIL.

Tapi ini bekerja paling baik jika kelas Anda mengimplementasikan beberapa antarmuka (atau berasal dari beberapa baseclass), kalau tidak bagaimana kode panggilan (baca: kompiler) untuk mengetahui tentang kelas yang akan dihasilkan saat runtime?


7
Mungkin ingin melihat diskusi ini: reflection-emit-vs-codedom
nawfal

1
@nawfal, ashwnacharya, ingin menghasilkan secara dinamis pada saat runtime kelasnya dengan anggotanya yang semula terkandung dalam daftar. Saya tidak berpikir memasukkannya ke dalam file yang dihasilkan saat runtime akan menjadi solusi kinerja yang baik.
sodjsn26fr

39

Anda juga dapat secara dinamis membuat kelas dengan menggunakan DynamicObject .

public class DynamicClass : DynamicObject
{
    private Dictionary<string, KeyValuePair<Type, object>> _fields;

    public DynamicClass(List<Field> fields)
    {
        _fields = new Dictionary<string, KeyValuePair<Type, object>>();
        fields.ForEach(x => _fields.Add(x.FieldName,
            new KeyValuePair<Type, object>(x.FieldType, null)));
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        if (_fields.ContainsKey(binder.Name))
        {
            var type = _fields[binder.Name].Key;
            if (value.GetType() == type)
            {
                _fields[binder.Name] = new KeyValuePair<Type, object>(type, value);
                return true;
            }
            else throw new Exception("Value " + value + " is not of type " + type.Name);
        }
        return false;
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        result = _fields[binder.Name].Value;
        return true;
    }
}

Saya menyimpan semua bidang kelas dalam kamus _fieldsbersama dengan jenis dan nilainya. Kedua metode adalah untuk bisa mendapatkan atau menetapkan nilai ke beberapa properti. Anda harus menggunakandynamic kata kunci untuk membuat turunan dari kelas ini.

Penggunaan dengan contoh Anda:

var fields = new List<Field>() { 
    new Field("EmployeeID", typeof(int)),
    new Field("EmployeeName", typeof(string)),
    new Field("Designation", typeof(string)) 
};

dynamic obj = new DynamicClass(fields);

//set
obj.EmployeeID = 123456;
obj.EmployeeName = "John";
obj.Designation = "Tech Lead";

obj.Age = 25;             //Exception: DynamicClass does not contain a definition for 'Age'
obj.EmployeeName = 666;   //Exception: Value 666 is not of type String

//get
Console.WriteLine(obj.EmployeeID);     //123456
Console.WriteLine(obj.EmployeeName);   //John
Console.WriteLine(obj.Designation);    //Tech Lead

Sunting: Dan inilah tampilannya kelas saya Field:

public class Field
{
    public Field(string name, Type type)
    {
        this.FieldName = name;
        this.FieldType = type;
    }

    public string FieldName;

    public Type FieldType;
}

1
Saya menyukai pendekatan ini sampai saya perlu menginisialisasi bidang dengan konstruktor. yaitu dynamic obj = new DynamicClass(fields){EmployeeId=123456;EmployeeName = "John"; Designation = "Tech Lead";}akan sangat bagus melakukan ini.
rey_coder

14

Saya tahu saya membuka kembali tugas lama ini tetapi dengan c # 4.0 tugas ini benar-benar tidak menyakitkan.

dynamic expando = new ExpandoObject();
expando.EmployeeID=42;
expando.Designation="unknown";
expando.EmployeeName="curt"

//or more dynamic
AddProperty(expando, "Language", "English");

untuk lebih lanjut lihat https://www.oreilly.com/learning/building-c-objects-dynamically


Ya, tetapi Anda kehilangan keamanan jenis di sini. Bisakah kita melakukan hal serupa sambil menjaga keamanan tipe?
toughQuestions

13

Saya tidak tahu maksud penggunaan kelas dinamis seperti itu, dan pembuatan kode serta kompilasi run time dapat dilakukan, tetapi perlu upaya. Mungkin Jenis Anonim akan membantu Anda, seperti:

var v = new { EmployeeID = 108, EmployeeName = "John Doe" };

7
Anda tidak dapat membuat kode nama bidang dengan mudah. Dia memberi mereka miliknya Field.FieldName. Harus membuat kode nama-nama bidang mengalahkan tujuan. Jika Anda harus melakukan itu, Anda bisa membuat kelas.
toddmo

9

Anda ingin melihat CodeDOM . Itu memungkinkan mendefinisikan elemen kode dan mengompilasinya. Mengutip MSDN:

... Grafik objek ini dapat diterjemahkan sebagai kode sumber menggunakan generator kode CodeDOM untuk bahasa pemrograman yang didukung. CodeDOM juga dapat digunakan untuk mengkompilasi kode sumber ke dalam kumpulan biner.


Saya ingin ini dihasilkan dalam runtime. Saya tidak ingin file CS fisik berada di sistem file saya. Maaf karena tidak menyebutkan itu sebelumnya.
ashwnacharya

1
@ashwnacharya: Anda dapat menggunakan CodeDOM untuk menghasilkan file sumber sekaligus mengompilasinya saat runtime!
Hemant

1
Namun berhati-hatilah, bahwa kompiler CodeDOM mengambil string mentah, dan dengan demikian Anda mungkin ingin mempertimbangkan "serangan penyisipan kode" mirip dengan yang digunakan dalam injeksi XSS dan SQL.
cwap

6

Berdasarkan jawaban @ danijels, secara dinamis buat kelas di VB.NET:

Imports System.Reflection
Imports System.Reflection.Emit

Public Class ObjectBuilder

Public Property myType As Object
Public Property myObject As Object

Public Sub New(fields As List(Of Field))
    myType = CompileResultType(fields)
    myObject = Activator.CreateInstance(myType)
End Sub

Public Shared Function CompileResultType(fields As List(Of Field)) As Type
    Dim tb As TypeBuilder = GetTypeBuilder()
    Dim constructor As ConstructorBuilder = tb.DefineDefaultConstructor(MethodAttributes.[Public] Or MethodAttributes.SpecialName Or MethodAttributes.RTSpecialName)

    For Each field In fields
        CreateProperty(tb, field.Name, field.Type)
    Next

    Dim objectType As Type = tb.CreateType()
    Return objectType
End Function

Private Shared Function GetTypeBuilder() As TypeBuilder
    Dim typeSignature = "MyDynamicType"
    Dim an = New AssemblyName(typeSignature)
    Dim assemblyBuilder As AssemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run)
    Dim moduleBuilder As ModuleBuilder = assemblyBuilder.DefineDynamicModule("MainModule")
    Dim tb As TypeBuilder = moduleBuilder.DefineType(typeSignature, TypeAttributes.[Public] Or TypeAttributes.[Class] Or TypeAttributes.AutoClass Or TypeAttributes.AnsiClass Or TypeAttributes.BeforeFieldInit Or TypeAttributes.AutoLayout, Nothing)
    Return tb
End Function

Private Shared Sub CreateProperty(tb As TypeBuilder, propertyName As String, propertyType As Type)
    Dim fieldBuilder As FieldBuilder = tb.DefineField("_" & propertyName, propertyType, FieldAttributes.[Private])

    Dim propertyBuilder As PropertyBuilder = tb.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, Nothing)
    Dim getPropMthdBldr As MethodBuilder = tb.DefineMethod("get_" & propertyName, MethodAttributes.[Public] Or MethodAttributes.SpecialName Or MethodAttributes.HideBySig, propertyType, Type.EmptyTypes)
    Dim getIl As ILGenerator = getPropMthdBldr.GetILGenerator()

    getIl.Emit(OpCodes.Ldarg_0)
    getIl.Emit(OpCodes.Ldfld, fieldBuilder)
    getIl.Emit(OpCodes.Ret)

    Dim setPropMthdBldr As MethodBuilder = tb.DefineMethod("set_" & propertyName, MethodAttributes.[Public] Or MethodAttributes.SpecialName Or MethodAttributes.HideBySig, Nothing, {propertyType})

    Dim setIl As ILGenerator = setPropMthdBldr.GetILGenerator()
    Dim modifyProperty As Label = setIl.DefineLabel()
    Dim exitSet As Label = setIl.DefineLabel()

    setIl.MarkLabel(modifyProperty)
    setIl.Emit(OpCodes.Ldarg_0)
    setIl.Emit(OpCodes.Ldarg_1)
    setIl.Emit(OpCodes.Stfld, fieldBuilder)

    setIl.Emit(OpCodes.Nop)
    setIl.MarkLabel(exitSet)
    setIl.Emit(OpCodes.Ret)

    propertyBuilder.SetGetMethod(getPropMthdBldr)
    propertyBuilder.SetSetMethod(setPropMthdBldr)
End Sub

End Class

6

Anda juga dapat secara dinamis membuat kelas dengan menggunakan DynamicExpressions .

Karena 'Kamus memiliki inisialisasi yang ringkas dan menangani tabrakan kunci, Anda ingin melakukan sesuatu seperti ini.

  var list = new Dictionary<string, string> {
    {
      "EmployeeID",
      "int"
    }, {
      "EmployeeName",
      "String"
    }, {
      "Birthday",
      "DateTime"
    }
  };

Atau Anda mungkin ingin menggunakan konverter JSON untuk membuat objek string serial Anda menjadi sesuatu yang dapat dikelola.

Kemudian gunakan System.Linq.Dynamic;

  IEnumerable<DynamicProperty> props = list.Select(property => new DynamicProperty(property.Key, Type.GetType(property.Value))).ToList();

  Type t = DynamicExpression.CreateClass(props);

Sisanya hanya menggunakan System.Reflection.

  object obj = Activator.CreateInstance(t);
  t.GetProperty("EmployeeID").SetValue(obj, 34, null);
  t.GetProperty("EmployeeName").SetValue(obj, "Albert", null);
  t.GetProperty("Birthday").SetValue(obj, new DateTime(1976, 3, 14), null);
}  

4

Bagi mereka yang ingin membuat properti kelas dinamis saja (yaitu POCO), dan buat daftar kelas ini. Menggunakan kode yang disediakan kemudian, ini akan membuat kelas dinamis dan membuat daftar ini.

var properties = new List<DynamicTypeProperty>()
{
    new DynamicTypeProperty("doubleProperty", typeof(double)),
    new DynamicTypeProperty("stringProperty", typeof(string))
};

// create the new type
var dynamicType = DynamicType.CreateDynamicType(properties);
// create a list of the new type
var dynamicList = DynamicType.CreateDynamicList(dynamicType);

// get an action that will add to the list
var addAction = DynamicType.GetAddAction(dynamicList);

// call the action, with an object[] containing parameters in exact order added
addAction.Invoke(new object[] {1.1, "item1"});
addAction.Invoke(new object[] {2.1, "item2"});
addAction.Invoke(new object[] {3.1, "item3"});

Berikut adalah kelas-kelas yang menggunakan kode sebelumnya.

Catatan: Anda juga harus merujuk pustaka Microsoft.CodeAnalysis.CSharp.

       /// <summary>
    /// A property name, and type used to generate a property in the dynamic class.
    /// </summary>
    public class DynamicTypeProperty
    {
        public DynamicTypeProperty(string name, Type type)
        {
            Name = name;
            Type = type;
        }
        public string Name { get; set; }
        public Type Type { get; set; }
    }

   public static class DynamicType
    {
        /// <summary>
        /// Creates a list of the specified type
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static IEnumerable<object> CreateDynamicList(Type type)
        {
            var listType = typeof(List<>);
            var dynamicListType = listType.MakeGenericType(type);
            return (IEnumerable<object>) Activator.CreateInstance(dynamicListType);
        }

        /// <summary>
        /// creates an action which can be used to add items to the list
        /// </summary>
        /// <param name="listType"></param>
        /// <returns></returns>
        public static Action<object[]> GetAddAction(IEnumerable<object> list)
        {
            var listType = list.GetType();
            var addMethod = listType.GetMethod("Add");
            var itemType = listType.GenericTypeArguments[0];
            var itemProperties = itemType.GetProperties();

            var action = new Action<object[]>((values) =>
            {
                var item = Activator.CreateInstance(itemType);

                for(var i = 0; i < values.Length; i++)
                {
                    itemProperties[i].SetValue(item, values[i]);
                }

                addMethod.Invoke(list, new []{item});
            });

            return action;
        }

        /// <summary>
        /// Creates a type based on the property/type values specified in the properties
        /// </summary>
        /// <param name="properties"></param>
        /// <returns></returns>
        /// <exception cref="Exception"></exception>
        public static Type CreateDynamicType(IEnumerable<DynamicTypeProperty> properties)
        {
            StringBuilder classCode = new StringBuilder();

            // Generate the class code
            classCode.AppendLine("using System;");
            classCode.AppendLine("namespace Dexih {");
            classCode.AppendLine("public class DynamicClass {");

            foreach (var property in properties)
            {
                classCode.AppendLine($"public {property.Type.Name} {property.Name} {{get; set; }}");
            }
            classCode.AppendLine("}");
            classCode.AppendLine("}");

            var syntaxTree = CSharpSyntaxTree.ParseText(classCode.ToString());

            var references = new MetadataReference[]
            {
                MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location),
                MetadataReference.CreateFromFile(typeof(DictionaryBase).GetTypeInfo().Assembly.Location)
            };

            var compilation = CSharpCompilation.Create("DynamicClass" + Guid.NewGuid() + ".dll",
                syntaxTrees: new[] {syntaxTree},
                references: references,
                options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));

            using (var ms = new MemoryStream())
            {
                var result = compilation.Emit(ms);

                if (!result.Success)
                {
                    var failures = result.Diagnostics.Where(diagnostic =>
                        diagnostic.IsWarningAsError ||
                        diagnostic.Severity == DiagnosticSeverity.Error);

                    var message = new StringBuilder();

                    foreach (var diagnostic in failures)
                    {
                        message.AppendFormat("{0}: {1}", diagnostic.Id, diagnostic.GetMessage());
                    }

                    throw new Exception($"Invalid property definition: {message}.");
                }
                else
                {

                    ms.Seek(0, SeekOrigin.Begin);
                    var assembly = System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromStream(ms);
                    var dynamicType = assembly.GetType("Dexih.DynamicClass");
                    return dynamicType;
                }
            }
        }
    }

Sepotong besar kode. Apakah mungkin untuk menggunakan juga AddRange di daftar dinamis, untuk akhirnya menambahkan beberapa catatan sekaligus?
RickyTad

2

Anda dapat melihat menggunakan modul dan kelas dinamis yang dapat melakukan pekerjaan. Satu-satunya kelemahan adalah tetap dimuat di domain aplikasi. Tetapi dengan versi .NET framework yang digunakan, itu bisa berubah. .NET 4.0 mendukung rakitan dinamis yang dapat ditagih dan karenanya Anda dapat membuat ulang kelas / tipe secara dinamis.


2

Wow! Terima kasih atas jawaban itu! Saya menambahkan beberapa fitur untuk itu untuk membuat konverter "datatable to json" yang saya bagikan kepada Anda.

    Public Shared Sub dt2json(ByVal _dt As DataTable, ByVal _sb As StringBuilder)
    Dim t As System.Type

    Dim oList(_dt.Rows.Count - 1) As Object
    Dim jss As New JavaScriptSerializer()
    Dim i As Integer = 0

    t = CompileResultType(_dt)

    For Each dr As DataRow In _dt.Rows
        Dim o As Object = Activator.CreateInstance(t)

        For Each col As DataColumn In _dt.Columns
            setvalue(o, col.ColumnName, dr.Item(col.ColumnName))
        Next

        oList(i) = o
        i += 1
    Next

    jss = New JavaScriptSerializer()
    jss.Serialize(oList, _sb)


End Sub

Dan di sub "compileresulttype", saya mengubah itu:

    For Each column As DataColumn In _dt.Columns
        CreateProperty(tb, column.ColumnName, column.DataType)
    Next


Private Shared Sub setvalue(ByVal _obj As Object, ByVal _propName As String, ByVal _propValue As Object)
    Dim pi As PropertyInfo
    pi = _obj.GetType.GetProperty(_propName)
    If pi IsNot Nothing AndAlso pi.CanWrite Then
        If _propValue IsNot DBNull.Value Then
            pi.SetValue(_obj, _propValue, Nothing)

        Else
            Select Case pi.PropertyType.ToString
                Case "System.String"
                    pi.SetValue(_obj, String.Empty, Nothing)
                Case Else
                    'let the serialiser use javascript "null" value.
            End Select

        End If
    End If

End Sub


-1

Runtime Code Generation with JVM and CLR - Peter Sestoft

Bekerja untuk orang yang benar-benar tertarik dengan jenis pemrograman ini.

Kiat saya untuk Anda adalah bahwa jika Anda mendeklarasikan sesuatu, cobalah untuk menghindari string, jadi jika Anda memiliki Field kelas, lebih baik menggunakan class System.Type untuk menyimpan tipe field daripada string. Dan demi solusi terbaik, alih-alih membuat kelas baru, coba gunakan yang telah dibuat, FiledInfo, bukan penciptaan yang baru.


2
Tautan sudah mati: -1
Glenn Slayden

Glenn: googling cepat mengungkapkan tautan yang berfungsi: pdfs.semanticscholar.org/326a/…
Andreas Pardeike

@AndreasPardeike Terima kasih, saya memperbaikinya di artikel.
Glenn Slayden
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.