3

考虑以下代码:

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:container执行后在调试器中悬停变量时,SetMember我看到修改后的字符串字段从nullto更改"Sergiy",而当悬停obj并导航到同一字段时,它仍然存在null

顺便说一句,代码可在此处获得:https ://github.com/rryk/opensim-omp/blob/kiara/KIARA/

更新:我在以下测试中运行时遇到了这种奇怪的行为:https ://github.com/rryk/opensim-omp/blob/kiara/KIARA.Test/FunctionMapingConfigTest.cs

更新:感谢克里斯,我已经意识到问题出在哪里,并重新实现了代码,如下所示:

// 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;
  }
}
4

1 回答 1

1

原因是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;
}

和 path ["name", "first"],它将创建FullNameat "name" 的副本,并设置其 "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您检索 a 的步骤struct,然后确保重新分配您制作的每个副本,它可能会起作用。

于 2013-04-03T02:23:06.187 回答