c#迭代反射

c# Iterative reflection

我想使用反射设置值,但要使设置函数能够访问多个子对象。当子对象是 class 时,我没有遇到任何问题,但是对于结构它不起作用。

例如,我有以下 class 和结构

class MyConfig
{
    Gravity gravity;
}
struct Gravity
{
    Vector2 direction;
}
struct Vector2
{
    float X,Y;
}

我希望能够像这样设置值:

MyConfig cfg=new MyConfig();
setValueViaReflection (cfg,"gravity.direction.X",76.5f);

显然应该将 cfg.gravity.direction.X 设置为 76.5f

我现在的代码是这样的:

void setValueViaReflection (object obj,string fieldName,object value)
{
int i;
TypeInfo baseType=null;
FieldInfo field;

    string []split=fieldName.Split ('.');

    // get one subobject in each iteration
    for (i=0;i<split.Count()-1;i++)
    {
        string fname=split[i];
        baseType=obj.GetType().GetTypeInfo();

        field=baseType.GetDeclaredField (fname);
        if (field==null) return;

        obj=field.GetValue (obj);
        if (obj==null) return;

    }

    // finally you've got the final type, set value
    baseType=obj.GetType().GetTypeInfo();

    field=baseType.GetDeclaredField (split[split.Count()-1]);
    if (field==null) return;

    field.SetValue (obj,value);
}

我知道我应该使用 SetValueDirect,但使用它没有任何区别(值在 "obj" 中被修改,但似乎是一个值类型,因此它没有改变原始对象。

我认为问题出在 field.GetValue 中,它创建了一个值类型,导致最终的 SetValueDirect 无用。

如果将 Gravity 和 Vector2 设置为 classes,代码运行良好。

您的分析基本正确。也就是说,问题源于值类型的使用。基本问题是,当您修改值类型的实例时,这对该实例的原始副本没有影响。它只会更改您从原始存储(在本例中为字段)中检索到的当前副本。要使更改对原始存储产生影响,您必须修改当前副本,然后将该副本存储回该存储。

当然,当遍历字段值的路径时,这意味着在每一步中,您都需要修改当前值并将其存储回去,至少对于值类型而言。对于引用类型字段,这在技术上是没有必要的——修改该存储值中的字段确实更新原始存储——但存储旧值(即对对象的引用)没有坏处) 回到它的存储空间。

对我来说,这个问题似乎更容易递归地思考。 IE。您已经知道解决基本情况很容易,因为框架直接使用 GetValue()SetValue() 方法为您提供了该机制。

因此,如果您能以某种方式逐步将问题减少到该基本情况,那么您就解决了主要问题。请注意,这种减少的一个关键方面是解决了基本情况后,您需要将结果传播到字段链;这意味着中间结果,因此这里不仅涉及递归,它也不能转换为普通的迭代解决方案(即它不是 "tail recursion")。

换句话说,递归不仅是解决问题的更简单方法,迭代解决方案仍然需要递归的某些方面(即行为类似于堆栈的数据结构)。所以你不妨使用递归,因为它更紧凑,更容易编写(恕我直言,这是一种更容易思考问题的方法)。

这是一个递归方法,可以满足您的需求:

static void SetValueByPath(object target, string path, object value)
{
    int dotIndex = path.IndexOf('.');
    string targetProperty = dotIndex > 0 ?
        targetProperty = path.Substring(0, dotIndex) : path;
    FieldInfo fieldInfo = target.GetType().GetTypeInfo().GetDeclaredField(targetProperty);

    if (dotIndex > 0)
    {
        object currentValue = fieldInfo.GetValue(target);

        SetValueByPath(currentValue, path.Substring(dotIndex + 1), value);

        value = currentValue;
    }

    fieldInfo.SetValue(target, value);
}

请注意,当值类型字段发生装箱时,运行时会对它们做正确的事情。您可以修改装箱值类型中的字段,并且不会制作该值类型的新副本;该字段更新为原始引用的装箱值(即上面代码中的 currentValue)。

另请注意,即使在混合中有引用类型字段,上述代码也能正常工作。例如。如果 Gravity 类型是 class。如果您真的关心,可以更改上面的代码以跳过将中间引用类型值复制回其字段,因为在这种情况下字段的值本身不会更改,但这只会使代码复杂化受益。