对象为什么被复制

本文关键字:复制 为什么 对象 | 更新日期: 2023-09-27 18:07:13

考虑以下代码:

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时,它返回原始值的副本。然后,当您最终更改一个值(或进一步深入到复制更多值类型成员的兔子洞中)时,您正在更改副本。

我建议您完全避免使用可变结构。如果您将类型更改为引用类型,则可能会正常工作。

编辑:给定您的测试使用类型FullNameLoginRequest:

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,然后确保将所做的每个副本重新赋值,则可能有效。