C#使用反射复制基类属性

28
我想使用反射将 MyObject 的所有属性更新到另一个对象上。问题是这个特殊的对象继承自一个基类,而那些基类属性值没有被更新。下面的代码可以复制顶级属性值。
public void Update(MyObject o)
{
    MyObject copyObject = ...

    FieldInfo[] myObjectFields = o.GetType().GetFields(
    BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);

    foreach (FieldInfo fi in myObjectFields)
    {
        fi.SetValue(copyObject, fi.GetValue(o));
    }
}

我想看看是否有其他BindingFlags属性可以帮助,但没有找到。


使用案例的例子是什么? - Matthew
1
我可以考虑:封装一个你喜欢的类的API。如果你返回该类型,它将强制在引用你的封装的任何项目中安装整个基本API。如果你只提取出一两个你喜欢的类,你可以简单地来回复制属性,时间几乎可以忽略不计,并且对于使用你的封装的用户来说,复杂度显著降低 - 现在不需要其他依赖项。 - DFTR
6个回答

41

试试这个:

public void Update(MyObject o)
{
    MyObject copyObject = ...
    Type type = o.GetType();
    while (type != null)
    {
        UpdateForType(type, o, copyObject);
        type = type.BaseType;
    }
}

private static void UpdateForType(Type type, MyObject source, MyObject destination)
{
    FieldInfo[] myObjectFields = type.GetFields(
        BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);

    foreach (FieldInfo fi in myObjectFields)
    {
        fi.SetValue(destination, fi.GetValue(source));
    }
}

这个工作很好。我遇到了一个问题,如果有一些属性是引用类型(其中一些是某些类型的Icollection),那么如果我更新复制实例中的数据,它会反映在源实例中。我能通过某种方式避免这种情况吗? - Novice
我可以建议在UpdateForType中删除“Type type”参数,并在方法体中添加source.GetType()。您已经知道了类型,而且这段代码不适用于泛型类型。 - DFTR
使用对象的扩展方法时要小心。这会对性能和可维护性产生影响。请参阅《CLR via C#》关于扩展方法的章节,作者描述了为什么这是不可取的。最好的做法是将扩展方法进行类型修复或使其成为静态泛型。 - Alexander Troshchenko
您可以使用BindingFlags.FlattenHierarchy,这将在FieldInfo[]数组中包含所有继承类的字段,但不包括私有静态字段。 - marcelo
我刚刚测试了BindingFlags.FlattenHierarchy,它运行正常。 - marcelo

24

我编写了一个扩展方法,可以与不同类型一起使用。我的问题是我有一些绑定到 asp mvc 表单的模型和其他映射到数据库的实体。理想情况下,我只需要一个类,但是该实体是分阶段构建的,而 asp mvc 模型希望一次性验证整个模型。

以下是代码:

public static class ObjectExt
{
    public static T1 CopyFrom<T1, T2>(this T1 obj, T2 otherObject)
        where T1: class
        where T2: class
    {
        PropertyInfo[] srcFields = otherObject.GetType().GetProperties(
            BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty);

        PropertyInfo[] destFields = obj.GetType().GetProperties(
            BindingFlags.Instance | BindingFlags.Public | BindingFlags.SetProperty);

        foreach (var property in srcFields) {
            var dest = destFields.FirstOrDefault(x => x.Name == property.Name);
            if (dest != null && dest.CanWrite)
                dest.SetValue(obj, property.GetValue(otherObject, null), null);
        }

        return obj;
    }
}

10

嗯,我认为GetFields可以获取整个继承链中的成员,如果不想要继承的成员,就必须显式地指定BindingFlags.DeclaredOnly。所以我进行了快速测试,证明我的想法是正确的。

然后我注意到了一些事情:

  

我想使用反射从MyObject更新到另一个对象上的所有属性。 我遇到的问题是该特定对象是从基类继承而来的,那些基类属性值没有被更新。

  

下面的代码只复制顶层的属性值。

public void Update(MyObject o) {
  MyObject copyObject = ...

  FieldInfo[] myObjectFields = o.GetType().GetFields(
  BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
这将仅获取字段(包括此类型上的私有字段),但不会获取属性。因此,如果您有以下继承结构(请原谅名称!):
class L0
{
    public int f0;
    private int _p0;
    public int p0
    {
        get { return _p0; }
        set { _p0 = value; }
    }
}

class L1 : L0
{
    public int f1;
    private int _p1;
    public int p1
    {
        get { return _p1; }
        set { _p1 = value; }
    }
}

class L2 : L1
{
    public int f2;
    private int _p2;
    public int p2
    {
        get { return _p2; }
        set { _p2 = value; }
    }
}

如果您在L2上使用您指定的BindingFlags执行.GetFields,将会获得f0f1f2_p2,但不包括p0p1(它们是属性而不是字段)或者_p0_p1(它们是私有基类字段,因此类型为L2的对象没有这些字段)。

如果您想要复制属性,请尝试使用.GetProperties代替。


2

这并没有考虑带有参数的属性,也没有考虑私有的get/set访问器可能不可访问,也没有考虑只读的枚举类型,所以这里提供了一个扩展解决方案。

我尝试将其转换为C#,但通常的转换工具无法实现,而我又没有时间自己转换。

''' <summary>
''' Import the properties that match by name in the source to the target.</summary>
''' <param name="target">Object to import the properties into.</param>
''' <param name="source">Object to import the properties from.</param>
''' <returns>
''' True, if the import can without exception; otherwise, False.</returns>
<System.Runtime.CompilerServices.Extension()>
Public Function Import(target As Object, source As Object) As Boolean
    Dim targetProperties As IEnumerable(Of Tuple(Of Reflection.PropertyInfo, Reflection.MethodInfo)) =
        (From aPropertyInfo In source.GetType().GetProperties(Reflection.BindingFlags.Public Or Reflection.BindingFlags.NonPublic Or Reflection.BindingFlags.Instance)
         Let propertyAccessors = aPropertyInfo.GetAccessors(True)
         Let propertyMethods = aPropertyInfo.PropertyType.GetMethods()
         Let addMethod = (From aMethodInfo In propertyMethods
                          Where aMethodInfo.Name = "Add" AndAlso aMethodInfo.GetParameters().Length = 1
                          Select aMethodInfo).FirstOrDefault()
         Where aPropertyInfo.CanRead AndAlso aPropertyInfo.GetIndexParameters().Length = 0 _
          AndAlso (aPropertyInfo.CanWrite OrElse addMethod IsNot Nothing) _
          AndAlso (From aMethodInfo In propertyAccessors
                   Where aMethodInfo.IsPrivate _
                    OrElse (aMethodInfo.Name.StartsWith("get_") OrElse aMethodInfo.Name.StartsWith("set_"))).FirstOrDefault() IsNot Nothing
         Select New Tuple(Of Reflection.PropertyInfo, Reflection.MethodInfo)(aPropertyInfo, addMethod))
    ' No properties to import into.
    If targetProperties.Count() = 0 Then Return True

    Dim sourceProperties As IEnumerable(Of Tuple(Of Reflection.PropertyInfo, Reflection.MethodInfo)) =
        (From aPropertyInfo In source.GetType().GetProperties(Reflection.BindingFlags.Public Or Reflection.BindingFlags.NonPublic Or Reflection.BindingFlags.Instance)
         Let propertyAccessors = aPropertyInfo.GetAccessors(True)
         Let propertyMethods = aPropertyInfo.PropertyType.GetMethods()
         Let addMethod = (From aMethodInfo In propertyMethods
                          Where aMethodInfo.Name = "Add" AndAlso aMethodInfo.GetParameters().Length = 1
                          Select aMethodInfo).FirstOrDefault()
         Where aPropertyInfo.CanRead AndAlso aPropertyInfo.GetIndexParameters().Length = 0 _
          AndAlso (aPropertyInfo.CanWrite OrElse addMethod IsNot Nothing) _
          AndAlso (From aMethodInfo In propertyAccessors
                   Where aMethodInfo.IsPrivate _
                    OrElse (aMethodInfo.Name.StartsWith("get_") OrElse aMethodInfo.Name.StartsWith("set_"))).FirstOrDefault() IsNot Nothing
         Select New Tuple(Of Reflection.PropertyInfo, Reflection.MethodInfo)(aPropertyInfo, addMethod))
    ' No properties to import.
    If sourceProperties.Count() = 0 Then Return True

    Try
        Dim currentPropertyInfo As Tuple(Of Reflection.PropertyInfo, Reflection.MethodInfo)
        Dim matchingPropertyInfo As Tuple(Of Reflection.PropertyInfo, Reflection.MethodInfo)

        ' Copy the properties from the source to the target, that match by name.
        For Each currentPropertyInfo In sourceProperties
            matchingPropertyInfo = (From aPropertyInfo In targetProperties
                                    Where aPropertyInfo.Item1.Name = currentPropertyInfo.Item1.Name).FirstOrDefault()
            ' If a property matches in the target, then copy the value from the source to the target.
            If matchingPropertyInfo IsNot Nothing Then
                If matchingPropertyInfo.Item1.CanWrite Then
                    matchingPropertyInfo.Item1.SetValue(target, matchingPropertyInfo.Item1.GetValue(source, Nothing), Nothing)
                ElseIf matchingPropertyInfo.Item2 IsNot Nothing Then
                    Dim isEnumerable As IEnumerable = TryCast(currentPropertyInfo.Item1.GetValue(source, Nothing), IEnumerable)
                    If isEnumerable Is Nothing Then Continue For
                    ' Invoke the Add method for each object in this property collection.
                    For Each currentObject As Object In isEnumerable
                        matchingPropertyInfo.Item2.Invoke(matchingPropertyInfo.Item1.GetValue(target, Nothing), New Object() {currentObject})
                    Next
                End If
            End If
        Next
    Catch ex As Exception
        Return False
    End Try

    Return True
End Function

2
Bogdan Litescu的解决方案非常好,不过我也建议检查一下是否可以写入属性。
foreach (var property in srcFields) {
        var dest = destFields.FirstOrDefault(x => x.Name == property.Name);
        if (dest != null)
            if (dest.CanWrite)
                dest.SetValue(obj, property.GetValue(otherObject, null), null);
    }

1
我有一个对象,它是从基础对象派生的,并为某些情况添加了额外的属性。但我想在派生对象的新实例上设置所有基础对象属性。即使以后向基础对象添加更多属性,我也不必担心在派生对象中添加硬编码行来设置基本属性。
感谢maciejkow,我想到了以下方法:
// base object
public class BaseObject
{
    public int ID { get; set; } = 0;
    public string SomeText { get; set; } = "";
    public DateTime? CreatedDateTime { get; set; } = DateTime.Now;
    public string AnotherString { get; set; } = "";
    public bool aBoolean { get; set; } = false;
    public int integerForSomething { get; set; } = 0;
}

// derived object
public class CustomObject : BaseObject
{
    public string ANewProperty { get; set; } = "";
    public bool ExtraBooleanField { get; set; } = false;

    //Set base object properties in the constructor
    public CustomObject(BaseObject source)
    {
        var properties = source.GetType().GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);

        foreach(var fi in properties)
        {
            fi.SetValue(this, fi.GetValue(source));
        }
    }
}

可以简单地使用,例如:
public CustomObject CreateNewCustomObject(BaseObject obj, string ANewProp, bool ExtraBool)
{
    return new CustomObject(obj)
    {
        ANewProperty = ANewProp,
        ExtraBooleanField = ExtraBool
    };
}

我还有其他想法:

  • 直接转换对象会起作用吗?(CustomObject)baseObject

    (我测试了一下转换,得到了System.InvalidCastException: '无法将类型为'BaseObject'的对象强制转换为类型'CustomObject'。')

  • 将其序列化为JSON字符串,然后反序列化为CustomObject?

    (我测试了序列化/反序列化——效果很好,但是序列化/反序列化有明显的延迟)

因此,在派生对象的构造函数中使用反射设置属性在我的测试案例中是瞬间完成的。我确信JSON序列化/反序列化也在任何情况下都使用反射,但是进行两次操作,而在构造函数中仅使用反射进行转换只发生一次。


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