考虑以下代码:
class ObjectAccessor {
...
static public void SetValueAtPath(ref object obj, List<PathEntry> path,
object value)
{
if (path.Count == 0)
obj = value;
object container = obj;
for (int i = 0; i < path.Count - 1; i++)
container = GetMember(container, path[i]);
SetMember(container, path[path.Count - 1], value);
}
...
}
当调用SetValueAtPath
时,我打算将value
分配给obj
内部的特定字段或属性,该字段或属性是通过跟随path
找到的。我希望container
变量指向包含字段的实际对象,SetMember
修改该字段。因为容器是一个引用,所以原始的obj
也应该被修改。然而,根据调试器,只有container
被修改,obj
保持不变。在哪里以及为什么创建副本?
class PathEntry
{
public enum PathEntryKind
{
Index,
Name
}
public PathEntryKind Kind;
public int Index; // Kind == Index
public string Name; // Kind == Name
}
class ObjectAccessor {
...
static public object GetMember(object obj, PathEntry member)
{
if (member.Kind == PathEntry.PathEntryKind.Index)
return ((IList)obj)[member.Index];
else
return GetFieldOrPropertyValue(obj, member.Name);
}
static public object GetFieldOrPropertyValue(object obj, string name)
{
FieldInfo fieldInfo = obj.GetType().GetField(name, BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance);
PropertyInfo propertyInfo = obj.GetType().GetProperty(name, BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance);
if (fieldInfo != null)
return fieldInfo.GetValue(obj);
else if (propertyInfo != null)
return propertyInfo.GetValue(obj, null);
throw new IncompatibleNativeTypeException();
}
static public void SetMember(object obj, PathEntry member, object value)
{
if (member.Kind == PathEntry.PathEntryKind.Index)
((IList)obj)[member.Index] = value;
else
SetFieldOrPropertyValue(obj, member.Name, value);
}
static public void SetFieldOrPropertyValue(object obj, string name, object value)
{
FieldInfo fieldInfo = obj.GetType().GetField(name, BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance);
PropertyInfo propertyInfo = obj.GetType().GetProperty(name, BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance);
if (fieldInfo != null)
fieldInfo.SetValue(obj, value);
else if (propertyInfo != null)
propertyInfo.SetValue(obj, value, null);
}
...
}
UPDATE:调用站点代码:
object obj = ObjectConstructor.ConstructObject(encoding, objectType);
...
ObjectAccessor.SetValueAtPath(ref obj, encodingEntry.ValuePath, value);
@Kirk:当执行SetMember
后,在调试器中悬停container
变量时,我看到修改的字符串字段从null
更改为"Sergiy"
,而悬停obj
并导航到同一字段时,它仍然是null
。
顺便说一句,代码在这里:https://github.com/rryk/opensim-omp/blob/kiara/KIARA/
UPDATE:我在以下测试中运行时会遇到这种奇怪的行为:https://github.com/rryk/opensim-omp/blob/kiara/KIARA.Test/FunctionMapingConfigTest.cs
UPDATE:感谢Chris,我已经意识到问题是什么,并重新实现了代码如下:
// Supports empty path in which case modifies passed obj as it's passed by reference.
static public void SetValueAtPath(ref object obj, List<PathEntry> path, object value)
{
if (path.Count == 0)
{
obj = value;
return;
}
// List of value types (structs) to be reassigned.
List<KeyValuePair<object, PathEntry>> valueTypeContainers = new List<KeyValuePair<object,PathEntry>>();
object container = obj;
for (int i = 0; i < path.Count - 1; i++)
{
object newContainer = GetMember(container, path[i]);
// Keep the trail of the value types (struct) or clear it if next container is non-value type.
if (newContainer.GetType().IsValueType)
valueTypeContainers.Add(new KeyValuePair<object, PathEntry>(container, path[i]));
else
valueTypeContainers.Clear();
container = newContainer;
}
SetMember(container, path[path.Count - 1], value);
// Reassign the value types (structs).
for (int i = valueTypeContainers.Count - 1; i >= 0; i--)
{
object valueContainer = valueTypeContainers[i].Key;
PathEntry pathEntry = valueTypeContainers[i].Value;
SetMember(valueContainer, pathEntry, container);
container = valueContainer;
}
}
原因是您的对象成员路径中有struct
值类型。
在值类型上调用ObjectAccessor.GetFieldOrPropertyType
时,它返回原始值的副本。然后,当您最终更改一个值(或进一步深入到复制更多值类型成员的兔子洞中)时,您正在更改副本。
我建议您完全避免使用可变结构。如果您将类型更改为引用类型,则可能会正常工作。
编辑:给定您的测试使用类型FullName
和LoginRequest
:
struct FullName
{
public string first;
public string last;
}
struct LoginRequest
{
FullName name;
string pwdHash;
string start;
string channel;
string version;
string platform;
string mac;
string[] options;
string id0;
string agree_to_tos;
string read_critical;
string viewer_digest;
}
路径["name", "first"]
,它将在"name"处创建FullName
的副本,并设置其"first"字段值。但是这个副本最终被扔掉了。
就像这样写:
LoginRequest login = new LoginRequest();
FullName name = login.name;
name.first = "My name!";
Console.WriteLine(name.first); //My name!
Console.WriteLine(login.name.first); //null
EDITx2:如果无法避免嵌套的值类型(考虑到库的性质,我怀疑是这样),那么可以做的就是设置每个检索到的值类型。因此,如果您在循环/堆栈中确定遍历ValuePath
的步骤中检索到struct
,然后确保将所做的每个副本重新赋值,则可能有效。