考虑以下代码:
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
深处的特定字段或属性,这是通过 路径
。我希望 container
变量指向包含该字段的实际对象,并希望 SetMember
修改该字段。由于container是引用,原来的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);
}
...
}
更新:调用站点的代码:
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/
更新:我在运行以下测试时遇到了这种奇怪的行为:https://github.com/rryk/opensim-omp/blob/kiara/KIARA.Test/FunctionMapingConfigTest.cs
更新:感谢 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
时,它会返回原始值的副本。然后,当您最终更改一个值(或深入复制更多值类型成员的兔子洞)时,您正在更改一个副本。
我建议你avoid mutable structs altogether .如果您将类型更改为引用类型,它可能会正常工作。
编辑:给定您使用类型 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
的步骤,然后确保重新分配您制作的每个副本,它可能工作。
关于c# - 为什么要复制对象?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/15777011/