Search code examples
c#reflectionrecursionstack-overflow

Prevent stack overflow while crawling inside objects via reflection in c#


I have this method called MatchNodes: IEnumerable<bool> MatchNodes<T>(T n1, T n2)

Which basically gets every property and field from both T objects (via reflection, and not including properties/fields from base classes) and compares them, returning the result as a IEnumerable of bools.

When it finds a primitive type or string, if just returns the == between them.

When it finds a type derived from a collection, it iterates each member and calls MatchNodes for each of them (ouch).

When it finds any other type, it calls MatchNodes for each property/field.

My solution is obviously asking for a stack overflow exception, but I don't have a clue on how make it better, because I have no idea how deep the objects will go.

Code (try not to cry please, it's ugly as hell):

public static IEnumerable<bool> MatchNodes<T>(T n1, T n2)
    {
        Func<PropertyInfo, bool> func= null;

        if (typeof(T) == typeof(String))
        {
            String str1 = n1 as String;
            String str2 = n2 as String;
            func = new Func<PropertyInfo, bool>((property) => str1 == str2);
        }
        else if (typeof(System.Collections.IEnumerable).IsAssignableFrom(typeof(T)))
        {
            System.Collections.IEnumerable e1 = (System.Collections.IEnumerable)n1;
            System.Collections.IEnumerable e2 = (System.Collections.IEnumerable)n2;
            func = new Func<PropertyInfo, bool>((property) =>
            {
                foreach (var v1 in e1)
                {
                    if (e2.GetEnumerator().MoveNext())
                    {
                        var v2 = e2.GetEnumerator().Current;
                        if (((IEnumerable<bool>)MatchNodes(v1, v2)).All(b => b == true))
                        {
                            return false;
                        }
                    }
                    else
                    {
                        return false;
                    }
                }
                if (e2.GetEnumerator().MoveNext())
                {
                    return false;
                }
                else return true;
            });
        }
        else if (typeof(T).IsPrimitive || typeof(T) == typeof(Decimal))
        {
            func = new Func<PropertyInfo, bool>((property) => property.GetValue(n1, null) == property.GetValue(n2, null)); 
        }
        else
        {
            func = new Func<PropertyInfo, bool>((property) =>
                    ((IEnumerable<bool>)MatchNodes(property.GetValue(n1, null),
                    property.GetValue(n2, null))).All(b => b == true));
        }

        foreach (PropertyInfo property in typeof(T).GetProperties().Where((property) => property.DeclaringType == typeof(T)))
        {
            bool result =func(property);
            yield return result;
        }

    }

What I'm looking at is a way to crawl into the objects without calling my method recursively.

EDIT

To clarify, example:

public class Class1 : RandomClassWithMoreProperties{
    public string Str1{get;set;}
    public int Int1{get;set;}
}

public class Class2{
    public List<Class1> MyClassProp1 {get;set;}
    public Class1 MyClassProp2 {get;set;}
    public string MyStr {get;set;}
}

MatchNodes(n1,n2) where n1.GetType() and n2.GetType() are Class2 would return true if:

  • Every Class1 object inside MyClassProp1 has the same Str1,Int1 for both objects
  • MyClassProp2 has the same Str1,Int1 for both objects
  • MyStr is equal for both objects

And I won't compare any properties from RandomClassWithMoreProperties.


Solution

  • You can use a stack or queue to store the properties you want to compare. It goes along these lines:

     var stack = new Stack<Tuple<object, object>>();
    
     // prime the stack
     foreach (var prop in n1.GetType().GetProperties())
     {
         stack.Push(Tuple.Create(prop.GetValue(n1), prop.GetValue(n2));
     }
    
     while (stack.Count > 0)
     {
         var current = stack.Pop();
    
         // if current is promitive: compare
         // if current is enumerable: push all elements as Tuples on the stack
         // else: push all properties as tuples on the stack
     }
    

    If you use a Queue instead of a Stack you get a BFS instead of a DFS. Also you should probably keep track of already visited nodes in a HashSet. You also might want to add a check to make sure the types of n1 and n2 are the same.