将一个对象的值复制到另一个对象

16

有没有一个好的实用类建议,可以将一个对象的值映射到另一个对象上?我想要一个使用反射机制的实用类,接受两个对象并在第二个对象中复制第一个对象中具有相同名称的公共属性的值。

我有两个实体是从Web服务代理生成的,所以我不能更改父类或实现接口或做任何类似的操作。但我知道这两个对象拥有相同的公共属性。


5
仅供参考 - 接受的答案将比其他答案有更多开销,因为它使用原始反射。像 AutoMapperExpression 这样的方法会在运行时将其预编译成 IL,这可能会带来显着的性能优势。因此,如果您需要频繁进行此操作,请避免使用基本的反射方法。 - Marc Gravell
6个回答

41

应该很简单就能组合在一起...

public static void CopyPropertyValues(object source, object destination)
{
    var destProperties = destination.GetType().GetProperties();

    foreach (var sourceProperty in source.GetType().GetProperties())
    {
        foreach (var destProperty in destProperties)
        {
            if (destProperty.Name == sourceProperty.Name && 
        destProperty.PropertyType.IsAssignableFrom(sourceProperty.PropertyType))
            {
                destProperty.SetValue(destination, sourceProperty.GetValue(
                    source, new object[] { }), new object[] { });

                break;
            }
        }
    }
}

1
这对我有用,但我添加了一行代码来处理只读属性。 - Vincent Dagpin
对我来说运行良好。点赞!好的提示!;) - Nadeem_MK
正如@VincentDagpin所说,在“if”语句中对于只读属性使用“&& destProperty.CanWrite”。 - KregHEk
如果对象包含List<string>属性,它会工作吗? - Waqar Ahmed
有人对“深拷贝”(包含子对象)有解决方案吗? - oo_dev

20

Jon Skeet和Marc Gravell有一个名为MiscUtil的库。在MiscUtil.Reflection中,有一个名为PropertyCopy的类,正好可以做到你所描述的内容。它只适用于.NET 3.5。

它通过遍历SourceType的公共属性并按名称与TargetType的公共属性匹配,确保每个属性都可以从源分配给目标,然后为这两种类型创建和缓存复制函数(这样你就不必每次进行所有这些反射)。我已经在生产代码中使用过它,并且可以证明它的好处。

算了,我想我会直接发布他们简明的代码(带有少于100行的注释)。此代码的许可证可以在此处找到:

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;

namespace MiscUtil.Reflection
{
    /// <summary>
    /// Generic class which copies to its target type from a source
    /// type specified in the Copy method. The types are specified
    /// separately to take advantage of type inference on generic
    /// method arguments.
    /// </summary>
    public static class PropertyCopy<TTarget> where TTarget : class, new()
    {
        /// <summary>
        /// Copies all readable properties from the source to a new instance
        /// of TTarget.
        /// </summary>
        public static TTarget CopyFrom<TSource>(TSource source) where TSource : class
        {
            return PropertyCopier<TSource>.Copy(source);
        }

        /// <summary>
        /// Static class to efficiently store the compiled delegate which can
        /// do the copying. We need a bit of work to ensure that exceptions are
        /// appropriately propagated, as the exception is generated at type initialization
        /// time, but we wish it to be thrown as an ArgumentException.
        /// </summary>
        private static class PropertyCopier<TSource> where TSource : class
        {
            private static readonly Func<TSource, TTarget> copier;
            private static readonly Exception initializationException;

            internal static TTarget Copy(TSource source)
            {
                if (initializationException != null)
                {
                    throw initializationException;
                }
                if (source == null)
                {
                    throw new ArgumentNullException("source");
                }
                return copier(source);
            }

            static PropertyCopier()
            {
                try
                {
                    copier = BuildCopier();
                    initializationException = null;
                }
                catch (Exception e)
                {
                    copier = null;
                    initializationException = e;
                }
            }

            private static Func<TSource, TTarget> BuildCopier()
            {
                ParameterExpression sourceParameter = Expression.Parameter(typeof(TSource), "source");
                var bindings = new List<MemberBinding>();
                foreach (PropertyInfo sourceProperty in typeof(TSource).GetProperties())
                {
                    if (!sourceProperty.CanRead)
                    {
                        continue;
                    }
                    PropertyInfo targetProperty = typeof(TTarget).GetProperty(sourceProperty.Name);
                    if (targetProperty == null)
                    {
                        throw new ArgumentException("Property " + sourceProperty.Name + " is not present and accessible in " + typeof(TTarget).FullName);
                    }
                    if (!targetProperty.CanWrite)
                    {
                        throw new ArgumentException("Property " + sourceProperty.Name + " is not writable in " + typeof(TTarget).FullName);
                    }
                    if (!targetProperty.PropertyType.IsAssignableFrom(sourceProperty.PropertyType))
                    {
                        throw new ArgumentException("Property " + sourceProperty.Name + " has an incompatible type in " + typeof(TTarget).FullName);
                    }
                    bindings.Add(Expression.Bind(targetProperty, Expression.Property(sourceParameter, sourceProperty)));
                }
                Expression initializer = Expression.MemberInit(Expression.New(typeof(TTarget)), bindings);
                return Expression.Lambda<Func<TSource,TTarget>>(initializer, sourceParameter).Compile();
            }
        }
    }
}

1
@Marc Gravell:像光明会一样吗? :) - Jason Punyon
有点像那样,但他们没有♦️(除非Jeff 光明会...) - Marc Gravell
@Marc Gravell: 突然间,他们的整个计划变得清晰明了。 - Jason Punyon
你有没有想过如何更新代码,以便能够将 SQL Server char(1) 列中的数据复制到 POCO 的 char 属性中?现在我遇到了这个错误:“属性 EmailType 具有不兼容的类型…” 谢谢。 - geoff swartz
需要注意的是,如果您尝试在具有相同名称的可空和非可空类型之间混合使用,该函数可能会遇到问题。运行时生成的通用验证异常会提示错误。我花了一段时间才弄清楚问题所在。 - GreatSamps

11
我们使用 Automapper 来实现这个功能。它非常好用。

真是个烦人的问题。我花了很长时间才记住它是“AutoMapper”,而不是“AutoMap”。顺便说一句,在谷歌上搜索“automatically map”真的很糟糕。 - user1228

5
我改进了Robinson的答案,并将其重构为Object类型的扩展方法,非常方便:
    public static void CopyPropertyValues( this object destination, object source )
    {
        if ( !( destination.GetType ().Equals ( source.GetType () ) ) )
            throw new ArgumentException ( "Type mismatch" );
        if ( destination is IEnumerable )
        {
            var dest_enumerator = (destination as IEnumerable).GetEnumerator();
            var src_enumerator = (source as IEnumerable).GetEnumerator();
            while ( dest_enumerator.MoveNext () && src_enumerator.MoveNext () )
                dest_enumerator.Current.CopyPropertyValues ( src_enumerator.Current );
        }
        else
        {
            var destProperties = destination.GetType ().GetRuntimeProperties ();
            foreach ( var sourceProperty in source.GetType ().GetRuntimeProperties () )
            {
                foreach ( var destProperty in destProperties )
                {
                    if ( destProperty.Name == sourceProperty.Name 
                        && destProperty.PropertyType.GetTypeInfo ()
                            .IsAssignableFrom ( sourceProperty.PropertyType.GetTypeInfo () ) )
                    {
                        destProperty.SetValue ( destination, sourceProperty.GetValue (
                            source, new object[] { } ), new object[] { } );
                        break;
                    }
                }
            }
        }
    }

假设我们有一个对象列表。一段时间后,我们会遇到一个异常,dest_enumerator.Current, .......... “枚举尚未开始或已经完成。” 你的代码不起作用。 - x19

4

使用Json.net怎么样呢?

static T CopyPropertiesJson<T>(object source)
{
    string jsonsource = JsonConvert.SerializeObject(source);
    return JsonConvert.DeserializeObject<T>(jsonsource);
}

2

有趣的是,在有人喜欢基于Expression<>比较对象属性的代码之后,我被邀请撰写了这篇文章。 我可能错了,但很可能是这个链接:https://dev59.com/9HNA5IYBdhLWcg3wX8rk#986617。 - Marc Gravell
@jitbit修复了URL。 - Roger Lipscombe

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