如何通过TypeBuilder动态创建带Key属性的匿名类型以用于Linq GroupBy分组
如何为动态创建的匿名类型添加Key属性以支持Linq GroupBy?
你现在的代码已经能动态创建一个包含指定字段的类型,但要让它像VB中带Key修饰的匿名类型那样支持GroupBy,核心是要让这个动态类型正确实现相等性比较和哈希码计算——因为Linq的GroupBy依赖于这两个逻辑来判断两个实例是否属于同一分组。
VB里的Key关键字本质上是告诉编译器:基于这些属性生成Equals、GetHashCode方法,并实现IEquatable(Of T)接口。我们需要手动在动态类型中实现这些逻辑,下面是修改后的完整实现:
Imports System.Reflection Imports System.Reflection.Emit Imports System.Runtime.CompilerServices Public Module DynamicTypeHelper ' 全局模块,用于动态类型生成(确保单例,避免重复创建) Private ReadOnly dynamicModule As ModuleBuilder = CreateDynamicModule() Private Function CreateDynamicModule() As ModuleBuilder Dim assemblyName As New AssemblyName("DynamicAnonymousTypes") Dim assemblyBuilder As AssemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run) Return assemblyBuilder.DefineDynamicModule("DynamicAnonymousTypesModule") End Function Public Shared Function CreateAnonymousType(fieldNames As List(Of String), fieldTypes As Type()) As Type ' 生成唯一的类型名称,避免冲突 Dim anonymousTypeName As String = GetAnonymousTypeName(fieldTypes) Dim dynamicTypeBuilder As TypeBuilder = dynamicModule.DefineType(anonymousTypeName, TypeAttributes.Public Or TypeAttributes.Class Or TypeAttributes.Serializable) ' 1. 添加编译器生成特性,模拟匿名类型的标识 dynamicTypeBuilder.SetCustomAttribute(New CustomAttributeBuilder( GetType(CompilerGeneratedAttribute).GetConstructor(Type.EmptyTypes), New Object() {})) ' 2. 定义字段(如果需要属性的话,可以改成DefineProperty,这里先保持和你原代码一致用字段) Dim fields As New List(Of FieldInfo)() For i = 0 To fieldNames.Count - 1 Dim fieldName = fieldNames(i) Dim fieldType = fieldTypes(i) Dim field = dynamicTypeBuilder.DefineField(fieldName, fieldType, FieldAttributes.Public) fields.Add(field) Next ' 3. 实现IEquatable(Of T)接口,T就是我们的动态类型 Dim iEquatableType = GetType(IEquatable(Of )).MakeGenericType(dynamicTypeBuilder) dynamicTypeBuilder.AddInterfaceImplementation(iEquatableType) ' 4. 实现IEquatable(Of T).Equals(T)方法 ImplementEqualsMethod(dynamicTypeBuilder, fields, iEquatableType) ' 5. 重写Object.Equals(Object)方法 ImplementObjectEqualsOverride(dynamicTypeBuilder, fields) ' 6. 重写Object.GetHashCode()方法 ImplementGetHashCodeOverride(dynamicTypeBuilder, fields) ' 创建并返回动态类型 Return dynamicTypeBuilder.CreateType() End Function Private Sub ImplementEqualsMethod(typeBuilder As TypeBuilder, fields As List(Of FieldInfo), iEquatableType As Type) ' 定义方法签名:Public Function Equals(other As [动态类型]) As Boolean Dim equalsMethod = typeBuilder.DefineMethod("Equals", MethodAttributes.Public Or MethodAttributes.Virtual Or MethodAttributes.Final, GetType(Boolean), {typeBuilder}) Dim il = equalsMethod.GetILGenerator() ' 如果other为Nothing,返回False il.Emit(OpCodes.Ldarg_1) il.Emit(OpCodes.Ldnull) il.Emit(OpCodes.Ceq) il.Emit(OpCodes.Ldc_I4_0) il.Emit(OpCodes.Ceq) Dim notNullLabel = il.DefineLabel() il.Emit(OpCodes.Brtrue_S, notNullLabel) il.Emit(OpCodes.Ldc_I4_0) il.Emit(OpCodes.Ret) il.MarkLabel(notNullLabel) ' 逐个比较所有Key字段(这里我们把所有字段当作Key) For Each field In fields ' 加载this.field il.Emit(OpCodes.Ldarg_0) il.Emit(OpCodes.Ldfld, field) ' 加载other.field il.Emit(OpCodes.Ldarg_1) il.Emit(OpCodes.Ldfld, field) ' 如果是值类型,直接用Ceq;如果是引用类型,调用Object.Equals If field.FieldType.IsValueType Then il.Emit(OpCodes.Ceq) Else il.Emit(OpCodes.Call, GetType(Object).GetMethod("Equals", {GetType(Object), GetType(Object)})) End If ' 如果比较结果为False,直接返回False il.Emit(OpCodes.Ldc_I4_0) il.Emit(OpCodes.Ceq) Dim fieldEqualLabel = il.DefineLabel() il.Emit(OpCodes.Brtrue_S, fieldEqualLabel) il.Emit(OpCodes.Ldc_I4_0) il.Emit(OpCodes.Ret) il.MarkLabel(fieldEqualLabel) Next ' 所有字段都相等,返回True il.Emit(OpCodes.Ldc_I4_1) il.Emit(OpCodes.Ret) ' 将方法映射到IEquatable接口的Equals方法 typeBuilder.DefineMethodOverride(equalsMethod, iEquatableType.GetMethod("Equals")) End Sub Private Sub ImplementObjectEqualsOverride(typeBuilder As TypeBuilder, fields As List(Of FieldInfo)) ' 重写Object.Equals(Object) Dim equalsMethod = typeBuilder.DefineMethod("Equals", MethodAttributes.Public Or MethodAttributes.Virtual Or MethodAttributes.Final, GetType(Boolean), {GetType(Object)}) Dim il = equalsMethod.GetILGenerator() ' 检查是否是同一类型的实例 il.Emit(OpCodes.Ldarg_1) il.Emit(OpCodes.Isinst, typeBuilder) Dim isSameTypeLabel = il.DefineLabel() il.Emit(OpCodes.Brfalse_S, isSameTypeLabel) ' 如果是同一类型,调用我们实现的IEquatable.Equals方法 il.Emit(OpCodes.Ldarg_0) il.Emit(OpCodes.Ldarg_1) il.Emit(OpCodes.Castclass, typeBuilder) il.Emit(OpCodes.Callvirt, typeBuilder.GetMethod("Equals", {typeBuilder})) il.Emit(OpCodes.Ret) il.MarkLabel(isSameTypeLabel) ' 类型不同,返回False il.Emit(OpCodes.Ldc_I4_0) il.Emit(OpCodes.Ret) End Sub Private Sub ImplementGetHashCodeOverride(typeBuilder As TypeBuilder, fields As List(Of FieldInfo)) ' 重写Object.GetHashCode() Dim hashCodeMethod = typeBuilder.DefineMethod("GetHashCode", MethodAttributes.Public Or MethodAttributes.Virtual Or MethodAttributes.Final, GetType(Integer), Type.EmptyTypes) Dim il = hashCodeMethod.GetILGenerator() ' 初始哈希码设为17 il.Emit(OpCodes.Ldc_I4_S, 17) For Each field In fields ' 加载当前哈希码 il.Emit(OpCodes.Ldc_I4_S, 23) il.Emit(OpCodes.Mul) ' 加载字段值 il.Emit(OpCodes.Ldarg_0) il.Emit(OpCodes.Ldfld, field) ' 如果是值类型,直接调用GetHashCode;如果是引用类型,先检查是否为Nothing If field.FieldType.IsValueType Then il.Emit(OpCodes.Callvirt, field.FieldType.GetMethod("GetHashCode", Type.EmptyTypes)) Else ' 如果字段为Nothing,用0作为哈希码;否则调用其GetHashCode Dim notNullLabel = il.DefineLabel() il.Emit(OpCodes.Dup) il.Emit(OpCodes.Ldnull) il.Emit(OpCodes.Ceq) il.Emit(OpCodes.Ldc_I4_0) il.Emit(OpCodes.Ceq) il.Emit(OpCodes.Brtrue_S, notNullLabel) il.Emit(OpCodes.Pop) il.Emit(OpCodes.Ldc_I4_0) il.Emit(OpCodes.Br_S, nextLabel) il.MarkLabel(notNullLabel) il.Emit(OpCodes.Callvirt, GetType(Object).GetMethod("GetHashCode", Type.EmptyTypes)) nextLabel = il.DefineLabel() il.MarkLabel(nextLabel) End If ' 合并哈希码:current = current * 23 + field.GetHashCode() il.Emit(OpCodes.Add) Next ' 返回最终哈希码 il.Emit(OpCodes.Ret) End Sub Private Function GetAnonymousTypeName(fieldTypes As Type()) As String ' 生成唯一名称,基于字段类型的全名 Dim typeNames = fieldTypes.Select(Function(t) t.FullName.Replace(".", "_").Replace("`", "_")).ToArray() Return $"AnonymousType_{String.Join("_", typeNames)}_{Guid.NewGuid():N}" End Function End Module
关键逻辑说明
- 添加
CompilerGeneratedAttribute:让动态类型和VB原生匿名类型保持一致的标识,虽非强制,但更贴近原生行为。 - 实现
IEquatable(Of T):提供强类型的相等比较,比Object.Equals性能更优,也是原生匿名类型的标准实现。 - 重写
Equals和GetHashCode:这是GroupBy正常工作的核心——GroupBy会通过这两个方法判断实例是否属于同一分组。示例中默认把所有字段当作Key属性处理,如果你需要区分Key/非Key字段,只需修改比较和哈希计算的逻辑,仅针对指定字段操作即可。 - 字段 vs 属性:原代码使用
DefineField,而VB原生匿名类型用的是属性。如果需要完全贴近原生,可将字段改为属性(使用DefineProperty),但示例中的字段实现也能正常工作。
使用示例
' 创建一个包含Name(String)和Age(Integer)的动态Key类型 Dim fieldNames = New List(Of String) From {"Name", "Age"} Dim fieldTypes = {GetType(String), GetType(Integer)} Dim keyType = CreateAnonymousType(fieldNames, fieldTypes) ' 在Linq GroupBy中使用 Dim data = New List(Of Object) From { New With {.Name = "Alice", .Age = 30, .Score = 90}, New With {.Name = "Alice", .Age = 30, .Score = 85}, New With {.Name = "Bob", .Age = 25, .Score = 95} } Dim grouped = data.GroupBy(Function(item) ' 为每个数据项创建对应的动态Key实例 Return Activator.CreateInstance(keyType, item.Name, item.Age) End Function)
这样GroupBy就能正确按照Name和Age分组,效果和使用VB原生的New With{Key .Name=..., Key .Age=...}完全一致。
内容的提问来源于stack exchange,提问作者Dionisis Hoholis
相关产品推荐
相关产品推荐

