使用反射递归获取私有字段值

Posted

技术标签:

【中文标题】使用反射递归获取私有字段值【英文标题】:Recursively get private field value using reflection 【发布时间】:2021-12-07 17:20:20 【问题描述】:

我有一个深度嵌套的 private 字段链,我想对其进行递归迭代以获取某个目标字段的值。

如何做到这一点?

例如:

public class A

   private B b;
   public A(B b)  this.b = b; 


public class B

   private C[] cItems;
   public B(C[] cItems)  this.cItems = cItems; 


public class C

   private string target; // <-- get this value
   public C(int target)  this.target = val; 

public static void GetFieldValueByPath(object targetObj, string targetFieldPath)

   // how to do it? I self-answer below 

用法如下:

public void DoSomething(A a)

   var val = GetFieldValueByPath(a, "b.cItems[2].target");

注意事项: 有一个关于recursively getting properties 的相关问题,但不是字段。但即便如此,它也不支持数组字段。 获取字段的this one等相关问题不是递归的。

【问题讨论】:

虽然这是可以做到的,但我强烈建议不要这样做。我看到您需要这样做的唯一原因是,如果 A 是在您没有源代码的第三方库中定义的。 @NigelBess,这完全是我的用例,我实际上需要get Serilog's log file path location 进行调试。 【参考方案1】:

这是执行此操作的方法(请注意对其他答案的改进,使用regex 提前准备路径部分):

public static object GetFieldValueByPath(object obj, string fieldPath)

    var flags = BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
    var parts = Regex.Matches(fieldPath, @"([^.\[]+)(?:\[(.*?)\])?").Cast<Match>().Select(match => match.Groups).ToList();
    return GetFieldValueByPathParts(obj, parts, flags);

private static object GetFieldValueByPathParts(object obj, List<GroupCollection> parts, BindingFlags flags)

    if (obj == null || parts.Count == 0) return obj;
    var field = new Field(name: parts[0][1].Value, value: (object)null, index: parts[0][2].Value);
    try
    
        field.Value = obj.GetType().GetField(field.Name, flags).GetValue(obj);
    
    catch (NullReferenceException ex)
    
        throw new Exception($"Wrong path provided: field 'field.Name' does not exist on 'obj'");
    
    field = TrySetEnumerableValue(field);
    return GetFieldValueByPathParts(field.Value, parts.Skip(1).ToList(), flags);

private static Field TrySetEnumerableValue(Field field)

    if (field.Value != null && field.Index != null)
    
        var enumerable = ((IEnumerable)field.Value).Cast<object>();
        field.Value = field.Index <= enumerable.Count() ? enumerable.ElementAt(field.Index.Value) : null;
    
    return field;

这里是 Field 助手类的定义:

public class Field

    public string Name  get; set; 
    public object Value  get; set; 
    public int? Index  get; set; 
    public Field(string name, object value, string index)
    
        Name = name;
        Value = value;
        Index = int.TryParse(index, out int parsed) ? parsed : (int?)null;
    


用法(live demo):

public static void Main(string[] s)

    var a1 =  new A(new B(new C[]  new C(1), new C(2), new C(3)  ) );
    Console.WriteLine(GetFieldValueByPath(a1, "b.cItems[2].target"));
            
    var a2 =  new A(new B(new C[]   ) );
    Console.WriteLine(GetFieldValueByPath(a2, "b.cItems[2].target"));
            
    var a3 =  new A(new B(null) );
    Console.WriteLine(GetFieldValueByPath(a3, "b.cItems[2].target"));

【讨论】:

【参考方案2】:

OfirD 的答案是正确的,但它不会起作用。不仅不编译,C[] 也没有实现IList&lt;object&gt;

它也有很多它没有考虑的场景。 (我没有更新他的代码来解决这些情况)

如果数组没有被整数索引怎么办? 如果是锯齿状数组怎么办? 如果路径指向属性而不是字段怎么办?

我已经更新了他的代码来工作:

    public static object GetFieldValueByPath(object obj, string fieldPath)
    
        var flags =
            BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
        var splitted = fieldPath.Split('.');

        var current = splitted[0];
        int? index = null;

        // Support getting a certain index in an array field
        var match = Regex.Match(current, @"\[([0-9]+)\]");
        if (match.Groups.Count > 1)
        
            current = fieldPath.Substring(0, match.Groups[0].Index);
            index = int.Parse(match.Groups[1].Value);
        

        var value = obj.GetType().GetField(current, flags).GetValue(obj);


        if (value == null)
        
            return null;
        

        if (splitted.Length == 1)
        
            return value;
        

        if (index != null)
        
            value = Index(value, index.Value);
        

        return GetFieldValueByPath(value, string.Join(".", splitted.Skip(1)));
    

    static object Index(object obj, int index)
    
        var type = obj.GetType();
        foreach (var property in obj.GetType().GetProperties())
        
            var indexParams = property.GetIndexParameters();
            if (indexParams.Length != 1) continue;
            return property.GetValue(obj, new object[]  index );
        

        throw new Exception($"type has no getter of the format type[int]");
    

【讨论】:

不要使用违反Code of Conduct 的语言,您可以评论说我忘了用Count 替换Length。毕竟,我的代码显然适用于我的演示。至于你的其他cmets:(1)锯齿状数组:你的代码不起作用,在我的demo上运行看看。 (2) 指向属性的路径:这不是这个问题的主题,因为在我的问题中已经有关于属性的问题。 (3) 我猜你的代码闻起来和我的一样,因为你几乎复制了它。【参考方案3】:

代码适用于您的示例,但如果有字典,您可能需要更改它

public static object GetFieldValueByPath(object targetObj, string targetFieldPath)
        
            var fieldNames = targetFieldPath.Split('.');
            var type = targetObj.GetType();

            foreach (var fieldName in fieldNames)
            
                string name = fieldName;
                int? objectIndex = default;
                if (name.Contains('['))//getting fieldName without indexer
                
                    int indexerStart = name.IndexOf('[');
                    int indexerEnd = name.IndexOf(']');

                    objectIndex = int.Parse(name.Substring(indexerStart + 1, indexerEnd-indexerStart - 1));
                    name = name.Substring(0, indexerStart);
                

                var field = type.GetField(name, BindingFlags.NonPublic | BindingFlags.Instance);
                if (objectIndex.HasValue)//here we know that field is collection
                
                    targetObj=((IList<object>)field.GetValue(targetObj))[0];//getting item by index
                    type = targetObj.GetType();
                
                else
                
                    targetObj = field.GetValue(targetObj);
                    type = field.FieldType;
                
            

            return targetObj;
         

【讨论】:

请尝试使用我在回答中提供的演示运行您的代码,因为它目前有几个问题,最突出的是它不是递归的。

以上是关于使用反射递归获取私有字段值的主要内容,如果未能解决你的问题,请参考以下文章

如何通过反射获取类及其父类的私有字段?

Java反射获取所有私有字段

java field获取值

java 使用反射获取属性名,和值

java 使用反射获取属性名,和值

Java面试题:如何通过反射获取和设置对象私有字段的值?