Трудная задача, связанная с 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