使用C#表达式进行以成员命名为锚点的数据转换

使用C#表达式进行以成员命名为锚点的数据转换

在软件开发中,有时因为某些原因如底层数据结构不可更改等需要将数据结构进行转换,这些数据类型之间没有继承关系,只有字段或属性名相同,往往需要手动编写数据转换代码,这样不仅繁琐,而且容易出错.

如果涉及到大量不同的类型转换,我们可以使用C#中的反射机制来转换数据.虽然使用反射很通用,但是还是太吃运行时间了,有没有一种既通用又高效的方法呢?

有的,兄弟有的,在C#中提供了动态创建表达式的功能,我们将基于C#表达式(Expression),设计一个通用的数据转换工具.

结构定义

首先,我们需要明确工具接口和一些结构定义.

我们的转换工具需要一个GetData方法,传入object,输出想要的数据结构;还需要一个标记表示某个类型是需要转换的,这个标记可以使用Attribute或接口表示,我们这里使用一个空的接口IConvertTarget来定义:

public interface IConvertTarget
{
}
public static class DataTransformer
{
 public static T GetData<T>(object source) where T : IConvertTarget, new()
 {
 }
}

传入source时我们需要一个转换器保存source->T的处理,这里定义一个委托类型DataTransformer来保存,不同的类型到类型的转换需要不同的转换器,为了可读性我们定义一个以[Type,Type]为索引的ConvertorDataBase结构:

 public static class DataTransformer
 {
 private delegate IConvertTarget DataConvertor(object source);
 
 private class ConvertorDataBase
 {
 private readonly Dictionary<Type, Dictionary<Type, DataConvertor>> m_dictionary = new();
 public DataConvertor? this[Type targetType, Type sourceType]
 {
 get => m_dictionary.GetValueOrDefault(targetType)?.GetValueOrDefault(sourceType);
 set
 {
 if (value == null) return;
 m_dictionary.TryAdd(targetType, new Dictionary<Type, DataConvertor>());
 m_dictionary[targetType].Add(sourceType, value);
 }
 }
 public void ClearData()
 {
 m_dictionary.Clear();
 }
 }
 }

GetData接口实现

GetData方法如下实现,_dataBase如果能找到convertor则直接调用,否则创建新的convertor,CreateConverter会在下文中实现.为CreateConverter和InvokeConverter添加Try Catch,便于发现接口中的异常.

public static class DataTransformer
{
 private static ConvertorDataBase _dataBase = new();
 private static MethodInfo _getDataMethodInfo;
 static DataTransformer()
 {
 // GetData的Info,递归调用需要
 _getDataMethodInfo = typeof(DataTransformer).GetMethod(nameof(GetData)) ?? throw new InvalidOperationException();
 }
 
 public static T GetData<T>(object source) where T : IConvertTarget, new()
 {
 var targetType = typeof(T);
 var sourceType = source.GetType();
 var convertor = _dataBase[targetType, sourceType];
 if (convertor == null)
 {
 try
 {
 convertor = CreateConverter(targetType, sourceType);
 _dataBase[targetType, sourceType] = convertor;
 }
 catch (Exception e)
 {
 Console.Write("Something wrong when creating converter.\n" +
 $"targetType:{targetType}.\n" +
 $"sourceType:{sourceType}\n" +
 $"{e.Message}");
 throw;
 }
 }
 try
 {
 return (T)convertor(source);
 }
 catch (Exception e)
 {
 Console.Write("Something wrong when invoking converter.\n" +
 $"targetType:{targetType}.\n" +
 $"sourceType:{sourceType}\n" +
 $"{e.Message}");
 throw;
 }
 }
 public static void Reset() => _dataBase.ClearData();
}

CreateConverter方法实现

CreateConverter与具体实例无关,只需要关注输入输出的类型,这里就需要使用Expression来创建表达式.

我们的基本思路时先new一个目标实例,在使用成员初始化器设置字段和属性.

public static class DataTransformer
{
 private static DataConvertor CreateConverter(Type targetType, Type sourceType)
 {
 // 参数obj的表达式
 var parameterExpression = Expression.Parameter(typeof(object), "obj");
 // 创建新的目标实例的表达式
 var targetInstanceExpression = Expression.New(targetType);
 // 用来保存确切类型的源数据的局部变量表达式
 var sourceInstanceExpression = Expression.Variable(sourceType, "source");
 // 获取target中所有公共的字段和属性
 var targetMembers = targetType.GetMembers(BindingFlags.Public | BindingFlags.Instance)
 .Where(info => info.MemberType is MemberTypes.Property or MemberTypes.Field).ToArray();
 // 获取source中所有公共的字段和属性
 var sourceMembers = sourceType.GetMembers(BindingFlags.Public | BindingFlags.Instance)
 .Where(info => info.MemberType is MemberTypes.Property or MemberTypes.Field).ToArray();
 //获取成员绑定表达式,具体实现在下文中
 var memberBindings = GetMemberBindings(targetMembers, sourceMembers, sourceInstanceExpression);
 //封装代码块
 var finalBlock = Expression.Block(
 new[]
 {
 //声明的局部变量source
 sourceInstanceExpression
 },
 //将obj强转后赋值给source
 Expression.Assign(sourceInstanceExpression, Expression.Convert(parameterExpression, sourceType)),
 //new Target 的 成员绑定
 Expression.MemberInit(targetInstanceExpression, memberBindings)
 );
 // 将表达式块编译为委托,返回一个 DataConvertor 类型的转换器.
 var lambda = Expression.Lambda<DataConvertor>(finalBlock, parameterExpression);
 return lambda.Compile();
 }
}

获取MemberBindings

简单的遍历,不解释.

public static class DataTransformer
{
 private static IEnumerable<MemberBinding> GetMemberBindings(MemberInfo[] targetMembers, MemberInfo[] sourceMembers, ParameterExpression sourceInstanceExpression)
 {
 var memberBindings = new List<MemberBinding>();
 foreach (var targetMember in targetMembers)
 {
 var sourceMember = sourceMembers.FirstOrDefault(info => info.Name == targetMember.Name);
 if (sourceMember == null) continue;
 var sourceExpression = GetSourceExpression(targetMember, sourceMember, sourceInstanceExpression);
 if (sourceExpression != null)
 memberBindings.Add(Expression.Bind(targetMember, sourceExpression));
 }
 return memberBindings;
 }
}

获取SourceExpression

public static class DataTransformer
{
 private static Expression? GetSourceExpression(MemberInfo targetMember, MemberInfo sourceMember, ParameterExpression sourceInstanceExpression)
 {
 // 目标成员类型
 var targetMemberType = GetMemberType(targetMember);
 // 源成员类型
 var sourceMemberType = GetMemberType(sourceMember);
 // 获取源数据的成员表达式
 var sourceMemberExpression = Expression.PropertyOrField(sourceInstanceExpression, sourceMember.Name);
 // 如果类型可直接赋值(继承目标类型)直接返回成员表达式(直接赋值)
 if (targetMemberType.IsAssignableFrom(sourceMemberType))
 {
 return sourceMemberExpression;
 }
 // 如果目标类型也是IConvertTarget,则递归调用GetData
 if (typeof(IConvertTarget).IsAssignableFrom(targetMemberType))
 {
 return Expression.Call(_getDataMethodInfo.MakeGenericMethod(targetMemberType), sourceMemberExpression);
 }
 return null;
 }
 // 获取成员的类型
 private static Type GetMemberType(MemberInfo memberInfo)
 {
 if (memberInfo is FieldInfo fieldInfo)
 {
 return fieldInfo.FieldType;
 }
 if (memberInfo is PropertyInfo propertyInfo)
 {
 return propertyInfo.PropertyType;
 }
 throw new ArgumentException("MemberInfo must be of type FieldInfo, PropertyInfo or PropertyInfo.");
 }
}

完整代码

数据转换中常常会有List数据需要转换,完整代码中给出了实现,请自行查看,如果有字典类型等数据结构需要转换可以添加类似的处理.(末尾有彩蛋)

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
public interface IConvertTarget
{
}
public static class DataTransformer
{
 private delegate IConvertTarget DataConvertor(object source);
 private class ConvertorDataBase
 {
 private readonly Dictionary<Type, Dictionary<Type, DataConvertor>> m_dictionary = new();
 public DataConvertor? this[Type targetType, Type sourceType]
 {
 get => m_dictionary.GetValueOrDefault(targetType)?.GetValueOrDefault(sourceType);
 set
 {
 if (value == null) return;
 m_dictionary.TryAdd(targetType, new Dictionary<Type, DataConvertor>());
 m_dictionary[targetType].Add(sourceType, value);
 }
 }
 public void ClearData()
 {
 m_dictionary.Clear();
 }
 }
 private static ConvertorDataBase _dataBase = new();
 private static MethodInfo _getDataMethodInfo;
 private static MethodInfo _getListDataMethodInfo;
 static DataTransformer()
 {
 _getDataMethodInfo = typeof(DataTransformer).GetMethod(nameof(GetData)) ?? throw new InvalidOperationException();
 _getListDataMethodInfo = typeof(DataTransformer).GetMethod(nameof(GetDataList)) ?? throw new InvalidOperationException();
 }
 public static T GetData<T>(object source) where T : IConvertTarget, new()
 {
 var targetType = typeof(T);
 var sourceType = source.GetType();
 var convertor = _dataBase[targetType, sourceType];
 if (convertor == null)
 {
 try
 {
 convertor = CreateConverter(targetType, sourceType);
 _dataBase[targetType, sourceType] = convertor;
 }
 catch (Exception e)
 {
 Console.Write("Something wrong when creating converter.\n" +
 $"targetType:{targetType}.\n" +
 $"sourceType:{sourceType}\n" +
 $"{e.Message}");
 throw;
 }
 }
 try
 {
 return (T)convertor(source);
 }
 catch (Exception e)
 {
 Console.Write("Something wrong when invoking converter.\n" +
 $"targetType:{targetType}.\n" +
 $"sourceType:{sourceType}\n" +
 $"{e.Message}");
 throw;
 }
 }
 public static TList? GetDataList<TList, T>(IEnumerable? source) where T : IConvertTarget, new() where TList : IList<T>, new()
 {
 if (source == null)
 return default;
 var targetList = new TList();
 foreach (var obj in source)
 {
 targetList.Add(GetData<T>(obj));
 }
 return targetList;
 }
 public static void Reset() => _dataBase.ClearData();
 private static DataConvertor CreateConverter(Type targetType, Type sourceType)
 {
 var parameterExpression = Expression.Parameter(typeof(object), "obj");
 var targetInstanceExpression = Expression.New(targetType);
 var sourceInstanceExpression = Expression.Variable(sourceType, "source");
 var targetMembers = targetType.GetMembers(BindingFlags.Public | BindingFlags.Instance).Where(info => info.MemberType is MemberTypes.Property or MemberTypes.Field).ToArray();
 var sourceMembers = sourceType.GetMembers(BindingFlags.Public | BindingFlags.Instance).Where(info => info.MemberType is MemberTypes.Property or MemberTypes.Field).ToArray();
 var memberBindings = GetMemberBindings(targetMembers, sourceMembers, sourceInstanceExpression);
 var finalBlock = Expression.Block(
 new[]
 {
 sourceInstanceExpression
 },
 Expression.Assign(sourceInstanceExpression, Expression.Convert(parameterExpression, sourceType)),
 Expression.MemberInit(targetInstanceExpression, memberBindings)
 );
 var lambda = Expression.Lambda<DataConvertor>(finalBlock, parameterExpression);
 return lambda.Compile();
 }
 private static IEnumerable<MemberBinding> GetMemberBindings(MemberInfo[] targetMembers, MemberInfo[] sourceMembers, ParameterExpression sourceInstanceExpression)
 {
 var memberBindings = new List<MemberBinding>();
 foreach (var targetMember in targetMembers)
 {
 var sourceMember = sourceMembers.FirstOrDefault(info => info.Name == targetMember.Name);
 if (sourceMember == null) continue;
 var sourceExpression = GetSourceExpression(targetMember, sourceMember, sourceInstanceExpression);
 if (sourceExpression != null)
 memberBindings.Add(Expression.Bind(targetMember, sourceExpression));
 }
 return memberBindings;
 }
 private static Expression? GetSourceExpression(MemberInfo targetMember, MemberInfo sourceMember, ParameterExpression sourceInstanceExpression)
 {
 var targetMemberType = GetMemberType(targetMember);
 var sourceMemberType = GetMemberType(sourceMember);
 var sourceMemberExpression = Expression.PropertyOrField(sourceInstanceExpression, sourceMember.Name);
 if (targetMemberType.IsAssignableFrom(sourceMemberType))
 {
 return sourceMemberExpression;
 }
 if (typeof(IConvertTarget).IsAssignableFrom(targetMemberType))
 {
 return Expression.Call(_getDataMethodInfo.MakeGenericMethod(targetMemberType), sourceMemberExpression);
 }
 var itemType = targetMemberType.GetGenericArguments().FirstOrDefault();
 if (itemType != null && typeof(IList<>).MakeGenericType(itemType).IsAssignableFrom(targetMemberType) && typeof(IConvertTarget).IsAssignableFrom(itemType) && typeof(IEnumerable).IsAssignableFrom(sourceMemberType))
 {
 return Expression.Call(_getListDataMethodInfo.MakeGenericMethod(targetMemberType, itemType), sourceMemberExpression);
 }
 return null;
 }
 private static Type GetMemberType(MemberInfo memberInfo)
 {
 if (memberInfo is FieldInfo fieldInfo)
 {
 return fieldInfo.FieldType;
 }
 if (memberInfo is PropertyInfo propertyInfo)
 {
 return propertyInfo.PropertyType;
 }
 throw new ArgumentException("MemberInfo must be of type FieldInfo, PropertyInfo or PropertyInfo.");
 }
}

???

有人说直接调方法也太没有手法了,其实获取List数据转换的还有更复杂的做法,做法如下:

	private Expression GetListMemberAssignment(FieldInfo targetField, PropertyInfo sourceField, MemberExpression sourceList)
	{
	var targetElementType = targetField.FieldType.GetGenericArguments()[0];
	var sourceElementType = sourceField.PropertyType.GetGenericArguments()[0];
	var genericGetItemMethod = GetIndexerMethod(sourceElementType);//此处为获取Generic方法,太麻烦懒得写了
	var genericAddItemMethod = GetAddMethod(targetElementType);
	var targetList = Expression.Variable(targetField.FieldType, "targetList");
	var targetListInit = Expression.Assign(targetList, Expression.New(targetField.FieldType));
	//sourceList.Count
	var countExpression = Expression.Property(sourceList, m_countMethod);
	//var i = 0
	var loopVariable = Expression.Variable(typeof(int), "i");
	var loopVariableInit = Expression.Assign(loopVariable, Expression.Constant(0));
	//sourceList[i]
	var getItemMethod = Expression.Call(sourceList, genericGetItemMethod, loopVariable);
	//GetData<TargetType>(sourceList[i]);
	var getDataExpression = Expression.Call(m_thisInstance, m_getDataMethod.MakeGenericMethod(targetElementType), getItemMethod);
	//targetList.Add(GetData<TargetType>(sourceList[i]))
	var addItemMethod = Expression.Call(targetList, genericAddItemMethod, getDataExpression);
	var targetLabel = Expression.Label();
	var loopBody = Expression.Block(
	Expression.IfThen(
	Expression.Equal(loopVariable, countExpression),
	Expression.Break(targetLabel)
	),
	addItemMethod,
	Expression.PostIncrementAssign(loopVariable) //i++
	);
	var loop = Expression.Loop(loopBody, targetLabel);
	var block = Expression.Block(new[] { loopVariable }, loopVariableInit, targetListInit, loop);
	var blockNull = Expression.Block(Expression.Assign(targetList, Expression.Constant(null, targetField.FieldType)));
	// 检查 sourceList 是否为空
	var ifThenElse = Expression.IfThenElse(
	Expression.Equal(sourceList, Expression.Constant(null)),
	blockNull,
	block
	);
	var ret = Expression.Block(new[] { targetList }, ifThenElse, targetList);
	return ret;
	}

你就说有没有手法把.这种做法虽然工作量大,但其实效果也不如上一种写法,直接调用方法有泛型约束,并且编译的也更快.不过可以手写一下练习Expression的循环和条件的用法.

作者:sunisok原文地址:https://www.cnblogs.com/sunisok/p/18689611

%s 个评论

要回复文章请先登录注册