本文譯自:Generating C# .NET Classes at Runtime
作者:WedPort
在我的C#職業生涯中,有幾次我不得不在執行時生成新的型別。希望把它寫下來能幫助有相同應用需求的人。這也意味著我以後不必在查詢相同問題的StackOverflow文章了。我最初是在.NET 4.6.2中這樣做的,但我已經更新到為.NET Core 3.0提供了示例。所有程式碼都可以在我的GitHub上面找到。
GitHub:https://github.com/cheungt6/public/tree/master/ReflectionEmitClassGeneration
為什麼我需要在執行時生成類?
在執行時生產新型別的需求通常是由於執行時才知道類屬性,滿足效能要求以及需要在新型別中新增功能。當你嘗試這樣做的時候,你應該考慮的第一件事是:這是否真的是一個明智的解決方案。在深入思考之前,還有很多其他事情可以嘗試,問你自己這樣的問題:
- 我可以使用普通的類嗎
- 我可以使用Dictionary、Tuple或者物件陣列(Array)?
- 我是否可以使用擴充套件物件
- 我確定我不能使用一個普通的類嗎?
如果你認為這仍然是必要的,請繼續閱讀下面的內容。
示例用例
作為一名開發人員,我將大量資料繫結到各種WPF Grids中。大多數時候屬性是固定的,我可以使用預定義的類。有時候,我不得不動態的構建網格,並且能夠在應用程式執行時更改資料。採取以下顯示ID和一些財務資料的類(FTSE和CAC是指數,其屬性代表指數價格):
public class PriceHolderViewModel : ViewModelBase
{
public long Id { get; set; }
public decimal FTSE100 { get; set; }
public decimal CAC40 { get; set; }
}
如果我們僅對其中的屬性感興趣,該類定義的非常棒。但是,如果要使用更多屬性擴充套件此類,則需要在程式碼中新增它,重新編譯並在新版本中進行部署。
相反的,我們可以做的是跟蹤物件所需的屬性,並在執行時構建類。這將允許我們在需要是不斷的新增和刪除屬性,並使用反射來更新它們的值。
// 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
型別。
在你建立了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來幫助您除錯和理解執行時新型別的建立,以及如何更新值。如果您有任何問題或意見,請隨時與我們聯絡。