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
。如果您真的关心,可以更改上面的代码以跳过将中间引用类型值复制回其字段,因为在这种情况下字段的值本身不会更改,但这只会使代码复杂化受益。
我想使用反射设置值,但要使设置函数能够访问多个子对象。当子对象是 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
。如果您真的关心,可以更改上面的代码以跳过将中间引用类型值复制回其字段,因为在这种情况下字段的值本身不会更改,但这只会使代码复杂化受益。