Трудная задача, связанная с List <T> и литье объектов

Мне дали этот вопрос на собеседовании в последнее время и не могли понять, как это сделать элегантно. С тех пор, это было неприятно для меня, и я не могу разобраться, если у него недостаток знаний о какой-то "современной" технике/технологии, о которой я не знаю, или если я просто глуп. Любые советы были бы очень желанными.

Проблема

Представьте себе простую иерархию классов:

abstract class Person {
    public string Name { get; set; }
}

class Child : Person { }

class Parent : Person {
    public List<Person> Children { get; set; }
}

class Ancestor : Parent { }

Проблема заключается в том, как перемещаться по иерархии таких объектов и распечатывать все встреченные люди. Итак, для следующего сценария:

Ancestor myAncestor = new Ancestor {    
    Name = "GrandDad",
    Children = new List<Person> { 
        new Child { Name = "Aunt" },
        new Child { Name = "Uncle" },
        new Parent {
            Name = "Dad", 
            Children = new List<Person> { 
                new Child { Name = "Me" }, 
                new Child { Name = "Sister" } 
            }
        }
    }
};

вывод должен выглядеть примерно так:

GrandDad  
-    Aunt  
-    Uncle  
-    *Dad  
         -Me  
         -Sister

Вся обработка должна выполняться в рамках одного метода, который принимает единственный параметр типа Ancestor.

Я реализовал, почти не задумываясь, простое рекурсивное решение, но, конечно, из-за того, как связанные объекты связаны друг с другом, все не так просто, как все это.

Попытайтесь, чтобы я не мог придумать чистый способ сделать это, и мой пост-интервью Googlings предположил, что мне нужно делать что-то, что есть (для меня, с только рабочим знанием LINQ и List<T>) что-то значительно более технически совершенное, чем то, что я делал в последнее десятилетие или около того. Это так? Или я должен думать о выходе из разработки программного обеспечения на том основании, что я мусор на нем?

Update

Спасибо всем вам за ваши ответы/предложения. Я принял @Daniel Hilgarth ответ прежде всего потому, что это был единственный, который я мог искренне понять: -o

Ответ 1

Я согласен с комментарием Марка, говоря, что система такого типа не имеет смысла. Тем не менее, вы можете решить его с делегатами. Это немного обман, потому что в основном это не что иное, как методы, но здесь мы идем:

void PrintFamily(Ancestor a)
{
    Action<Parent, int> printParent = null;
    printParent = (parent, level) => 
    {
        var indentation = new string(' ', level * 4);
        var indentationChildren = new string(' ', (level + 1) * 4);
        Console.WriteLine(indentation + parent.Name);
        foreach(var child in parent.Children)
        {
            if(child is Child)
                Console.WriteLine(indentationChildren + child.Name);
            else if(child is Parent)
            {
                printParent((Parent)child, level + 1);
            }
        }
    };

    printParent(a, 0);
}

Ответ 2

Это ужасно, но в рамках вопроса (без дополнительных методов, поэтому нельзя добавлять полиморфизм/дискриминатор, а метод должен принимать Ancestor, поэтому нет рекурсии метода):

static void Write(Ancestor root)
{
    // stack since depth-first
    var stack = new Stack<Tuple<Person,int>>();
    stack.Push(Tuple.Create((Person)root, 0));

    while(stack.Count > 0)
    {
        var pair= stack.Pop();
        var person = pair.Item1;

        // indentation
        Console.Write(new string('\t', pair.Item2));
        Parent parent = person as Parent;

        // node markers aren't fully specified, but this gets somewhere
        // near; maybe * should actually be checking Children != null &&
        // Children.Count > 0             
        if(person == root) {}
        else if (parent != null) { Console.Write("*");}
        else {Console.Write("-");}            

        Console.WriteLine(person.Name);

        // recursion on the stack
        if(parent != null && parent.Children != null) {
            foreach(var next in Enumerable.Reverse(parent.Children)) {
                stack.Push(Tuple.Create(next, pair.Item2 + 1));
            }
        }
    }
}

Ответ 3

Здесь мой переход, используя стек для эмуляции рекурсии:

static void PrintTree(Ancestor ancestor)
{
    Stack<Tuple<int, Person>> PersonStack = new Stack<Tuple<int, Person>>();
    PersonStack.Push(new Tuple<int, Person>(0, ancestor));

    while (PersonStack.Count != 0)
    {
        Tuple<int, Person> NextPerson = PersonStack.Pop();

        Console.WriteLine(new string(' ', NextPerson.Item1) + NextPerson.Item2.Name);

        Parent NextPersonAsParent = NextPerson.Item2 as Parent;
        if (NextPersonAsParent != null && NextPersonAsParent.Children != null)
        {
            foreach (var Child in NextPersonAsParent.Children)
            {
                PersonStack.Push(new Tuple<int, Person>(NextPerson.Item1 + 1, Child));
            }
        }
    }
}

Ответ 4

internal void ToString(Ancestor root)
{
    Trace.WriteLine(root.Name);
    Trace.Indent();
    foreach(var child in root.Children)
    {
         if(child is Parent)
             ToString(new Ancestor(){Name = child.Name, 
                                     Children = ((Parent)child).Children});
         else
             Trace.WriteLine(child.Name);
    }
    Trace.Unindent();
}

Ответ 5

С небольшим количеством "обмана" (спасибо даниэлю для ides), это использует рекурсию и остается в ограничениях задачи.

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ, это еще раз доказывает, что предложенный класс hierachi является нечетным. Специально для задачи, и я бы не использовал эту технику в производстве

  internal static string ToString(Ancestor root){
        Func<Parent, string, string> inner = (x, y) => string.Empty;
        inner = (p, indentation) =>{
                    var parents = p.Children.OfType<Parent>();
                    var children = p.Children.OfType<Child>();
                    var childString =
                        string.Concat
                            (children.Select
                                 (c => indentation + "-" + c.Name + Environment.NewLine));
                    return indentation + "-" + p.Name + Environment.NewLine +
                           childString +
                           string.Concat
                               (parents.Select(par => inner(par, " " + indentation)));
                };
        return inner(root, string.Empty);
    }

Сначала мы объявляем функтор и инициализируем его фиктивным значением.

Затем мы строим лямбда-выражение, которое может называть его саморекурсивным. Тело выражения делает то же, что и метод ниже.

Если для метода signatur не нужен предок, мы могли бы сделать что-то вроде:

    internal string ToString(Parent parent, string indentation){
        var parents = parent.Children.OfType<Parent>();
        var children = parent.Children.OfType<Child>();
        var childString = children.Select(c => indentation + "-" + c.Name + Environment.NewLine);
        return indentation + "-" + parent.Name + Environment.NewLine + childString +
               string.Concat(parents.Select(par => ToString(par, " " + indentation)));
    }

сначала создайте список всех родителей и одного из всех детей. Затем создайте строку для всех детей (где не требуется рекурсия), а повтор для всех родителей

Ответ 6

Edit:

Полиморфное решение, скорее всего, является самым простым, но вы должны иметь возможность изменять объекты. Имейте Person реализовать виртуальный метод: Print(), например, который будет переопределен в Parent, чтобы распечатать данные. Отступ может обрабатываться, например, путем подачи аргумента уровня отступов. Как вы заметили, ограничения проблемы запрещают этот.

Структура объекта, представленная в вопросе, довольно бессмысленна, а ограничения довольно узкие. Кроме того, тот факт, что вам нужно реализовать метод, который принимает Ancestor и ограничен только этим телом метода, приводит меня к мысли, что вопрос был задан специально, чтобы привести вас к подходу к стеку. Кроме того, последний имеет важные преимущества в производительности по сравнению с рекурсией.

Уже есть несколько хороших примеров стека, поэтому я бы предложил альтернативный метод рекурсии, который в принципе должен соответствовать правилу "не объявлять никаких дополнительных методов" и гораздо читабельнее (если вы знаете свою lambda:)

Action<IEnumerable<Person>, int> traverseAndPrint = null;
traverseAndPrint =
    (ps, i) =>
    {
        foreach (var p in ps)
        {
            Console.WriteLine("{0}-{1}", new string(' ', i), p.Name);

            if (p is Parent) traverseAndPrint(((Parent)p).Children, i + 1);
        }
    };

traverseAndPrint(new [] {ancestor}, 0);

Ответ 7

Если вам разрешено (повторно) создавать объекты и использовать их простоту, вы можете использовать это:

    private static void PrintAncestor(Ancestor myAncestor)
    {
        Console.WriteLine(myAncestor.Name);
        foreach (var child in myAncestor.Children)
        {
            string intend = new string(myAncestor.Name.TakeWhile(c => c == '\t').ToArray()) + '\t';

            if (child is Ancestor)
                PrintAncestor(new Ancestor {
                    Children = (child as Ancestor).Children,
                    Name = intend + child.Name
                });

            if (child is Parent)
                PrintAncestor(new Ancestor {
                    Children = (child as Parent).Children,
                    Name = intend + "- *" + child.Name
                });

            if (child is Child)
                Console.WriteLine(intend + "-  " + child.Name);
        }
    }

печатает:

GrandDad
        -  Aunt
        -  Uncle
        - *Dad
                -  Me
                -  Sister