【问题标题】:Why is object copied?为什么要复制对象?
【发布时间】:2013-04-03 00:46:16
【问题描述】:

考虑以下代码:

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);
  }
  ...
}

更新:调用现场的代码:

object obj = ObjectConstructor.ConstructObject(encoding, objectType);
...
ObjectAccessor.SetValueAtPath(ref obj, encodingEntry.ValuePath, value);

@Kirk:在执行SetMember 后,在调试器中悬停container 变量时,我看到修改后的字符串字段从null 更改为"Sergiy",而在悬停obj 并导航到同一字段时,它仍然是@ 987654341@.

顺便说一句,代码在这里可用: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;
  }
}

【问题讨论】:

  • 你能告诉我们你是怎么打电话给SetValueAtPath(和周围的代码)吗?
  • 除了Simon的要求,还请详细说明“但是,根据调试器,只有container被修改,obj保持不变”是什么意思。容器在什么意义上被修改?
  • 旁注:我无法用任何正常的思维过程重现这一点。所以发生了一些奇怪的事情(我怀疑它是你的呼叫站点..)。
  • 也许这只是调试器中的陈旧数据,或者您正在查看错误的断点,或者如果为发布模式编译时进行了一些优化。您可以尝试注销或将值写入控制台而不是使用调试器吗?编辑:此外,沿着ValuePath,是否有any 成员访问值类型(struct)作为属性 索引器访问? EDITx2:既然您已经发布了源代码的链接,那么您的测试用例中使用的objectType 和确切的encodingEntry.ValuePath 是什么?
  • 我已经尝试打印到控制台 - 与调试器中的结果相同。始终在调试模式下编译。是的,有结构在路上 - 请参阅github.com/rryk/opensim-omp/blob/kiara/KIARA.Test/… 中的实际数据类型。 objectType 将是 LoginRequestValuePath 将是 ["name", "first"]

标签: c# reflection copy


【解决方案1】:

原因是因为您的对象成员路径中有struct 值类型。

当您在值类型上调用ObjectAccessor.GetFieldOrPropertyType 时,它会返回原始值的副本。然后,当您最终更改一个值(或进一步深入兔子洞复制更多值类型成员)时,您就是在更改一个副本。

我建议你avoid mutable structs altogether。如果您将类型更改为引用类型,它可能会正常工作。

编辑:给定您使用类型 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"],它将在“名称”处创建FullName的副本,并设置其“第一个”字段值。但这个副本最终被扔掉了。

这和写作一样:

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 的步骤,然后确保重新分配您制作的每个副本,它可能会起作用。

【讨论】:

  • 至 EDITx2:查看最终代码的问题。我也有同样的想法,但我决定为此保留一个额外的数组。
  • @SergiyByelozyorov 当路径有多种类型时,您可能想尝试测试(比如class -> struct -> struct -> class -> struct ->struct)。我怀疑在这种情况下你的算法不会起作用;我认为它只会考虑值类型的最后次运行,同时在引用类型之前出现的链上更高的值类型将无法正确重新分配并且复制链被破坏。编辑:我认为您可能需要做的就是避免在找到引用类型时清除valueTypeContainers。不过只是猜测。
  • 我已经考虑过了。事实上,在您提供第二个结构的情况下,路径中将包含对第二个类的引用。由于我只修改最深对象中的值,因此只需将第 4 个结构(最后一个)重新分配给第 3 个结构的字段,将第 3 个结构重新分配给第 2 类的字段。
猜你喜欢
  • 1970-01-01
  • 2014-01-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-12
  • 2018-04-14
  • 2018-09-26
相关资源
最近更新 更多