本文譯自:Generating C# .NET Classes at Runtime
做者:WedPortgit
在個人C#職業生涯中,有幾回我不得不在運行時生成新的類型。但願把它寫下來能幫助有相同應用需求的人。這也意味着我之後沒必要在查找相同問題的StackOverflow文章了。我最初是在.NET 4.6.2中這樣作的,但我已經更新到爲.NET Core 3.0提供了示例。全部代碼均可以在個人GitHub上面找到。
GitHub:https://github.com/cheungt6/public/tree/master/ReflectionEmitClassGenerationgithub
在運行時生產新類型的需求一般是因爲運行時才知道類屬性,知足性能要求以及須要在新類型中添加功能。當你嘗試這樣作的時候,你應該考慮的第一件事是:這是否真的是一個明智的解決方案。在深刻思考以前,還有不少其餘事情能夠嘗試,問你本身這樣的問題:數組
若是你認爲這仍然是必要的,請繼續閱讀下面的內容。框架
做爲一名開發人員,我將大量數據綁定到各類WPF Grids中。大多數時候屬性是固定的,我可使用預約義的類。有時候,我不得不動態的構建網格,而且可以在應用程序運行時更改數據。採起如下顯示ID和一些財務數據的類(FTSE和CAC是指數,其屬性表明指數價格):ide
public class PriceHolderViewModel : ViewModelBase { public long Id { get; set; } public decimal FTSE100 { get; set; } public decimal CAC40 { get; set; } }
若是咱們僅對其中的屬性感興趣,該類定義的很是棒。可是,若是要使用更多屬性擴展此類,則須要在代碼中添加它,從新編譯並在新版本中進行部署。性能
相反的,咱們能夠作的是跟蹤對象所需的屬性,並在運行時構建類。這將容許咱們在須要是不斷的添加和刪除屬性,並使用反射來更新它們的值。ui
// Keep track of my properties var _properties = new Dictionary<string, Type>(new[]{ new KeyValuePair<string, Type>( "FTSE100", typeof(Decimal) ), new KeyValuePair<string, Type>( "CAC40", typeof(Decimal) ) });
下面的示例向您展現瞭如何在運行時構建新類型。你須要使用**System.Reflection.Emit**
庫來構造一個新的動態程序集,您的類將在其中建立,而後是模塊和類型。與舊的** .NET Framework**
框架不一樣,在舊的版本中,你須要在當前程序的AppDomain
中建立程序集 ,而在** .NET Core**
中,AppDomain
再也不可用。你將看到我使用GUID建立了一個新類型名稱,以便於跟蹤類型的版本。在之前,你不能建立具備相同名稱的兩個類型,可是如今彷佛不是這樣了。調試
public Type GeneratedType { private set; get; } private void Initialise() { var newTypeName = Guid.NewGuid().ToString(); var assemblyName = new AssemblyName(newTypeName); var dynamicAssembly = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); var dynamicModule = dynamicAssembly.DefineDynamicModule("Main"); var dynamicType = dynamicModule.DefineType(newTypeName, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout, typeof(T)); // This is the type of class to derive from. Use null if there isn't one dynamicType.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName); foreach (var property in Properties) AddProperty(dynamicType, property.Key, property.Value); GeneratedType = dynamicType.CreateType(); }
在定義類型時,你能夠提供一種類型,從中派生新的類型。若是你的基類具備要包含在新類型中的某些功能或屬性,這將很是有用。以前,我曾使用它在運行時擴展ViewModel
和Serializable
類型。code
在你建立了TypeBuilder
後,你可使用下面提供的代碼開始添加屬性。它建立了支持字段和所需的中間語言,以便經過Getter
和Setter
訪問它們。爲每一個屬性完成此操做後,可使用CreateType()
建立類型的實例。對象
private static void AddProperty(TypeBuilder typeBuilder, string propertyName, Type propertyType) { var fieldBuilder = typeBuilder.DefineField("_" + propertyName, propertyType, FieldAttributes.Private); var propertyBuilder = typeBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null); var getMethod = typeBuilder.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes); var getMethodIL = getMethod.GetILGenerator(); getMethodIL.Emit(OpCodes.Ldarg_0); getMethodIL.Emit(OpCodes.Ldfld, fieldBuilder); getMethodIL.Emit(OpCodes.Ret); var setMethod = typeBuilder.DefineMethod("set_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, null, new[] { propertyType }); var setMethodIL = setMethod.GetILGenerator(); Label modifyProperty = setMethodIL.DefineLabel(); Label exitSet = setMethodIL.DefineLabel(); setMethodIL.MarkLabel(modifyProperty); setMethodIL.Emit(OpCodes.Ldarg_0); setMethodIL.Emit(OpCodes.Ldarg_1); setMethodIL.Emit(OpCodes.Stfld, fieldBuilder); setMethodIL.Emit(OpCodes.Nop); setMethodIL.MarkLabel(exitSet); setMethodIL.Emit(OpCodes.Ret); propertyBuilder.SetGetMethod(getMethod); propertyBuilder.SetSetMethod(setMethod); }
有了類型後,就很容易經過使用Activator.CreateInstance()
來建立它的實例。可是,你但願可以更改已建立的屬性的值,爲了作到這一點,你能夠再次使用反射來獲取propertyInfos
並提取Set方法。一旦有了這些屬性,電影它們類設置屬性值就相對簡單了。
foreach (var property in Properties) { var propertyInfo = GeneratedType.GetProperty(property.Key); var setMethod = propertyInfo.GetSetMethod(); setMethod.Invoke(objectInstance, new[] { propertyValue }); }
如今,您能夠在運行時使用自定義屬性來建立本身的類型,並具備更新其值的功能,一切就緒。 我發現的惟一障礙是建立一個能夠存儲新類型實例的列表。 WPF中的DataGrid傾向於只讀取List的常規參數類型的屬性。 這意味着即便您使用新屬性擴展了基類,使用AutoGenerateProperties也只能看到基類中的屬性。 解決方案是使用生成的類型顯式建立一個新的List。 我在下面提供瞭如何執行此操做的示例:
var listGenericType = typeof(List<>); var list = listGenericType.MakeGenericType(GeneratedType); var constructor = list.GetConstructor(new Type[] { }); var newList = (IList)constructor.Invoke(new object[] { }); foreach (var value in values) newList.Add(value);
我已經在GitHub中建立了一個示例應用程序。它包含一個UI來幫助您調試和理解運行時新類型的建立,以及如何更新值。若是您有任何問題或意見,請隨時與咱們聯繫。