在執行時生成C# .NET類

yc98zp發表於2020-06-23

​本文譯自​:​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

為什麼我需要在執行時生成類?

在執行時生產新型別的需求通常是由於執行時才知道類屬性,滿足效能要求以及需要在新型別中新增功能。當你嘗試這樣做的時候,你應該考慮的第一件事是:這是否真的是一個明智的解決方案。在深入思考之前,還有很多其他事情可以嘗試,問你自己這樣的問題:

  1. 我可以使用普通的類嗎
  2. 我可以使用Dictionary、Tuple或者物件陣列(Array)?
  3. 我是否可以使用擴充套件物件
  4. 我確定我不能使用一個普通的類嗎?

如果你認為這仍然是必要的,請繼續閱讀下面的內容。

示例用例

作為一名開發人員,我將大量資料繫結到各種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();
}

在定義型別時,你可以提供一種型別,從中派生新的型別。如果你的基類具有要包含在新型別中的某些功能或屬性,這將非常有用。之前,我曾使用它在執行時擴充套件ViewModelSerializable型別。

在你建立了TypeBuilder後,你可以使用下面提供的程式碼開始新增屬性。它建立了支援欄位和所需的中間語言,以便通過GetterSetter訪問它們。為每個屬性完成此操作後,可以使用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來幫助您除錯和理解執行時新型別的建立,以及如何更新值。如果您有任何問題或意見,請隨時與我們聯絡。

相關文章