将一个对象的属性值转移到另一个对象

5
首先我知道“AutoMapper”,但我不想使用它。因为我正在学习C#,想要深入了解它。所以我尝试自己解决这个问题(如下所述)。但是,如果属性的名称和类型相同,并且从源中可读且在目标中可写,则我正在尝试创建一个属性复制程序来复制一种类型的值到另一种类型上。我正在使用“type.GetProperties()”方法。以下是示例方法:
    static void Transfer(object source, object target) {

        var sourceType = source.GetType();
        var targetType = target.GetType();

        var sourceProps = sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance);

        var targetProps = (from t in targetType.GetProperties()
                           where t.CanWrite
                                 && (t.GetSetMethod().Attributes & MethodAttributes.Static) == 0
                           select t).ToList();

        foreach(var prop in sourceProps) {
            var value = prop.GetValue(source, null);
            var tProp = targetProps
                .FirstOrDefault(p => p.Name == prop.Name &&
                    p.PropertyType.IsAssignableFrom(prop.PropertyType));
            if(tProp != null)
                tProp.SetValue(target, value, null);
        }
    }

它可以工作,但我在SO上读到一个答案,使用System.Reflection.Emit和ILGenerator和late-bound delegates更快且性能更高。但是没有更多的解释或任何链接。您能帮助我了解加速此代码的方法吗?或者您能向我建议一些关于Emit、ILGenerator和late-bound delegates的链接吗?或者您认为对主题有所帮助的任何内容?
完整问题:
我从@svick的答案中理解并学到了很多东西。但现在,如果我想将其用作开放式通用方法,该怎么做?像这样的东西:
public TTarget Transfer<TSource, TTarget>(TSource source) where TTarget : class, new() { } 

或者是一个扩展:

public static TTarget Transfer<TSource, TTarget>(this TSource source) where TTarget : class, new() { } 

1
我觉得System.Reflection.Emit在这里对你没有帮助。在你的情况下,源对象和目标对象都存在于编译时,并且你只是将相应属性的值从一个对象复制到另一个对象。如果你想要在运行时创建目标类型,那么Emit可能会有所帮助。 - Paolo Falabella
4个回答

6

你可以使用 Reflection.Emit 来完成这个操作,但通常使用 表达式 要容易得多,并且它可以给你基本相同的性能。请记住,性能优势仅在缓存编译的代码时才存在,例如在 Dictionary<Tuple<Type, Type>, Action<object, object>> 中,这里我没有这样做。

static void Transfer(object source, object target)
{
    var sourceType = source.GetType();
    var targetType = target.GetType();

    var sourceParameter = Expression.Parameter(typeof(object), "source");
    var targetParameter = Expression.Parameter(typeof(object), "target");

    var sourceVariable = Expression.Variable(sourceType, "castedSource");
    var targetVariable = Expression.Variable(targetType, "castedTarget");

    var expressions = new List<Expression>();

    expressions.Add(Expression.Assign(sourceVariable, Expression.Convert(sourceParameter, sourceType)));
    expressions.Add(Expression.Assign(targetVariable, Expression.Convert(targetParameter, targetType)));

    foreach (var property in sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
    {
        if (!property.CanRead)
            continue;

        var targetProperty = targetType.GetProperty(property.Name, BindingFlags.Public | BindingFlags.Instance);
        if (targetProperty != null
                && targetProperty.CanWrite
                && targetProperty.PropertyType.IsAssignableFrom(property.PropertyType))
        {
            expressions.Add(
                Expression.Assign(
                    Expression.Property(targetVariable, targetProperty),
                    Expression.Convert(
                        Expression.Property(sourceVariable, property), targetProperty.PropertyType)));
        }
    }

    var lambda =
        Expression.Lambda<Action<object, object>>(
            Expression.Block(new[] { sourceVariable, targetVariable }, expressions),
            new[] { sourceParameter, targetParameter });

    var del = lambda.Compile();

    del(source, target);
}

如果您拥有这个,编写通用方法就很简单:
public TTarget Transfer<TSource, TTarget>(TSource source)
    where TTarget : class, new()
{
    var target = new TTarget();
    Transfer(source, target);
    return target;
} 

将主要的工作方法也泛化并创建 Action<TSource, TTarget>,甚至可以直接创建对象并使用 Func<TSource, TTarget>,这也许是有意义的。但如果像我建议的那样添加缓存,这意味着您需要使用类似于 Dictionary<Tuple<Type, Type>, Delegate> 这样的东西,并在从缓存中检索委托后将其转换为正确的类型。


我差不多理解了你的代码;但由于我是C#的新手学生,有些地方还存在歧义。你能否解释一下如何在开放泛型中使用它?就像这样:TTarget Transfer<TSource>(TTarget target) where TTarget : class, new() 我把这个问题放在原来的问题里了。非常感谢。 - amiry jd
感谢您的编辑和指导。是的,我理解您所说的内容,并且对此有所了解。但我的问题是如何获取泛型类型的类型和属性,以及如何为它们创建表达式。例如,如果该方法是一个开放式泛型方法,则似乎不需要这些行:var sourceParameter = Expression.Parameter(typeof(object), "source"); var targetParameter = Expression.Parameter(typeof(object), "target");。这是正确的吗? - amiry jd

4

你可以只获取目标上与名称匹配的属性。这将大大简化你的代码。

foreach (var property in sourceType.GetProperties( BindingFlags.Public | BindingFlags.Instance))
{
     var targetProperty = targetType.GetProperty( property.Name, BindingFlags.Public | BindingFlags.Instance );
     if (targetProperty != null
          && targetProperty.CanWrite
          && targetProperty.PropertyType.IsAssignableFrom(property.PropertyType))
     {
         targetProperty.SetValue( target, property.GetValue(source, null), null );
     }
}

我认为如果“source”具有一些只写属性,你的代码将无法正常工作。这种情况非常罕见且是一种不好的实践,但肯定是可能存在的。 - svick
在模型映射的上下文中(参见OP的介绍),我认为可以假设源和目标都没有只写属性,这是可以接受的。 - tvanfosson
@tvanfosson,我认为在几乎任何情况下这都是一个可以接受的假设,但为了安全起见,我仍然会进行检查。 - svick
@svick 这取决于我是设计库还是为自己的代码编写帮助程序。在后一种情况下,我的测试将清楚地表明您不应该使用源上的只写属性调用该方法,因为它会期望抛出异常。 - tvanfosson
我喜欢这个简单的解决方案。只需要做一个小修正:这一行需要进行如下所示的小修正:targetProperty.***PropertyType.***IsAssignableFrom(property.PropertyType)) - Nikola Schou
@NikolaSchou 很好的发现。 - tvanfosson

0

C# 反射 IL - 理解值如何被复制

这个问题是关于克隆的,因此源对象的类型与目标对象的类型相同(虽然据我所知,源和目标可以有不同的类型),但仍然值得分析。


0

我写了一篇关于如何做到这一点的博客文章(仅限葡萄牙语,但您可以阅读代码)

http://elemarjr.net/2012/02/27/um-helper-para-shallow-cloning-emitting-em-c/

您可以从以下位置获取代码:

https://github.com/ElemarJR/FluentIL/blob/master/demos/Cloning/src/Cloning/Cloning/ILCloner.cs

我认为使用Reflection.Emit比必要的更难。因此,我编写了一个名为FluentIL(www.fluentil.org)的开源库,以使其变得更容易。我在这里使用了它。
[]s

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接