LINQ基于子项顺序对平面列表进行排序

21

我目前正在尝试使用LINQ和C#找到一种好的方式来对我的元素进行排序,但我似乎做不到。

现在假设你有以下表格:

---TempTable
ID (int)
ParentID (int)
Name (varchar)
SortOrder (int)

ID和ParentID彼此相关,并为我提供了自我分层数据结构。根元素在ID字段中有一个null。 SortOrder仅是整个表的一部分,并基于ParentID,因此共享相同ParentID的元素确实具有其中的1、2、3。

让我们进一步假设以下数据:

ID = 1
ParentID = null
Name = Test 1
SortOrder = 1

ID = 2
ParentID = 1
Name = Test 2
SortOrder = 1

ID = 3
ParentID = 1
Name = Test 3
SortOrder = 2

ID = 4
ParentID = 2
Name = Test 4
SortOrder = 1

我期望的平面列表应按以下顺序排列:

Test 1 //root element with sort order 1 = very top
Test 2 //child element of root with sort order 1
Test 4 //child element of test 2 with sort order 1
Test 3 //child element of root with sort order 2

另外,我希望能够获得对象本身,而不仅仅是通过使用select new ...获取部分信息。

这是我失败尝试之一:

from x in EntityModel.TempTables //DbSet<TempTable> by EntityFramework - which already holds all elements
   orderby x.SortOrder
   from y in x.TempTableChildren //Navigation Property by EntityFramework
   orderby y.SortOrder
   select y

感谢您提前的帮助。

编辑:

根据给定的TestData,使用ParentID排序可能会有所帮助,因为ID和ParentIDs是完美排序的。但在实际应用中,情况并非如此,因为它是数据驱动的。某些人可能会删除一个条目,创建一个新的条目,并将其按照某个父项的顺序放置,这样就会出现以下情况:

ID = 193475037
ParentID = 2
Name = Test 192375937
SortOrder = 25

现在,在应用程序中,可以移动这个项目,ParentID和SortOrder会随机更改为类似以下内容:

ID = 193475037
ParentID = 456798424
Name = Test 192375937
SortOrder = 4

为了进一步解释这个问题,这里有一些代码 - 在没有一个漂亮的Linq查询的情况下,我将如何使用两个yield return进行操作:

public class LinqTestDemo
{
    Random rand = new Random();
    List<TempTable> list = new List<TempTable>();

    public List<TempTable> GetFlatData()
    {
        list = GetTestData();

        var rootElement = (from x in list
                            where x.ParentID == null
                            orderby x.SortOrder
                            select x).ToList();

        var flatList = OrderChilds(rootElement).ToList();

        foreach (var tempTable in flatList)
        {
            Console.WriteLine(string.Format("ID = {0} - ParentID = {1} - Name = {2} - SortOrder = {3}", tempTable.ID, tempTable.ParentID, tempTable.Name, tempTable.SortOrder));
        }

        return flatList;
    }

    private IEnumerable<TempTable> OrderChilds(List<TempTable> enumerable)
    {
        foreach (var tempTable in enumerable)
        {
            yield return tempTable;

            TempTable table = tempTable;
            var childs = OrderChilds((from x in list
                                        where x.ParentID == table.ID
                                        orderby x.SortOrder
                                        select x).ToList());

            foreach (var child in childs)
            {
                yield return child;
            }
        }
    }

    public List<TempTable> GetTestData()
    {
        var returnValue = new List<TempTable>();
        for (int i = 0; i < 50; i++)
        {
            var tempTable = new TempTable();
            tempTable.ID = i;
            if (i == 0)
                tempTable.ParentID = null;
            else
                tempTable.ParentID = rand.Next(0, i);

            var maxSortOrder = (from x in returnValue
                                where x.ParentID == tempTable.ParentID
                                select (int?)x.SortOrder).Max();

            if (maxSortOrder.HasValue)
                tempTable.SortOrder = maxSortOrder.Value + 1;
            else
                tempTable.SortOrder = 1;

            tempTable.Name = string.Format("Test {0:00}", i);
            returnValue.Add(tempTable);
        }

        return returnValue;
    }

    public class TempTable
    {
        public int ID { get; set; }
        public int? ParentID { get; set; }
        public string Name { get; set; }
        public int SortOrder { get; set; }
    }
}

@ 广度优先遍历 vs 深度优先遍历: 经过阅读后,我想选择深度优先遍历,其中在同一深度级别的元素应按SortOrder属性排序。


有多少层深度?如果您可以拥有无限深度,则不可能在单个查询中完成。此外,实体框架的工作方式,在递归性质的查询上会失败。唯一的解决方案是树遍历。 - Akash Kava
它可以具有无限的深度,不应该有任何限制。 - Rand Random
5个回答

14
  public lEnumerable<TempTable> GetList( int? parentID = null){

     foreach ( var item in Context.TempTables
        .Where( x => x.ParentID == parentID )
        .OrderBy( x=> x.SortOrder)
        .ToList() {

        yield return item;

        foreach( var child in GetList( item.ID))
        {
            yield return child;
        }

     }
  }


  var sortedList = GetList();

它与您的方法类似,但更小且递归。并且适用于许多深度级别。我更喜欢调用ToList,因为它会在查询下一个查询之前关闭结果集。

目前没有一种方法可以在单个查询中完成此操作。

按要求使用单个查询

Entity Framework将自动填充所有子项。

 public IEnumerable<TempTable> PrepareList(IEnumerable<TempTable> list){
     list = list.OrderBy( x=> x.SortOrder);
     foreach(var item in list){
         yield return item;
         foreach(var child in PrepareList(item.ChildTempTables)){
             yield return child;
         }
     }
 }

 // since EF will automatically fill each children on fetch
 // all we need is just a top level nodes
 // which we will pass to PrepareList method
 var list = Context.TempTables.ToList().Where(x=> x.ParentID == null);
 var sortedList = PrepareList(list).ToList();

 // it is good to create list at the end if you are going to 
 // iterate it many times and logic will not change.

请问您能否修改您的代码,使得初始列表(在您的情况下为Context.TempTables)可以是已经查询并按需求排序的IEnumerable,正如您可能会注意到Thomas B.和Roman Pekar提供的另外两个“新”答案都有解决此场景的方案。您会如何做到这一点?感谢您的时间。 - Rand Random
@RandRandom 我已经添加了答案,它将使用预加载的所有内容完成相同的任务。 - Akash Kava

4
这里有一个非递归版本。它不会一遍又一遍地迭代初始列表。相反,它维护了一个父节点到子节点关系的字典,并在枚举器中存储正在进行的前序遍历树的当前位置。
public static IEnumerable<TempTable> PreorderForest(IEnumerable<TempTable> list)
{
    var nodesByParent = list.GroupBy(x => x.ParentID.GetValueOrDefault(-1))
        .ToDictionary(xs => xs.Key, 
                      xs => xs.OrderBy(x => x.SortOrder).GetEnumerator());

    var stack = new Stack<IEnumerator<TempTable>>();
    stack.Push(nodesByParent[-1]);

    while (stack.Count > 0)
    {
        var nodes = stack.Peek();
        if (nodes.MoveNext())
        {
            yield return nodes.Current;
            IEnumerator<TempTable> children;
            if (nodesByParent.TryGetValue(nodes.Current.ID, out children))
                stack.Push(children);
        }
        else
            stack.Pop();
    }
}

你能解释一下为什么你使用了非递归版本吗?难道栈不会造成不必要的开销吗? - Rand Random
1
.NET应用程序的默认(函数调用)堆栈大小为64位4MB和32位1MB。这里使用的堆栈数据集合驻留在堆上,并受到2GB的.NET对象堆大小限制。因此,对于非常深的层次结构,递归版本会比非递归版本更早地抛出异常。此外:由于调用开销,递归实现通常较慢。(尽管如此:非递归更复杂,缺乏可读性) - Thomas B.

4

实际上我不知道是否能够通过优雅的LINQ查询来实现。这里是DFS的递归版本,它构建了查找表以加速按ParentID搜索。

public static IEnumerable<TempTable> SortedList(IEnumerable<TempTable> list = null, int? ParentID = null, ILookup<int?, TempTable> lookup = null)
{
    if (lookup == null)
        lookup = list.ToLookup(x => x.ParentID, x => x);

    foreach (var p in lookup[ParentID].OrderBy(x => x.SortOrder))
    {
        yield return p;
        foreach (var c in SortedList(lookup: lookup, ParentID: p.ID))
            yield return c;
    }
}

不确定在Entity Framework环境中是否需要查找,因为EF是否已经提供了内置的主/外键值搜索?如果不是这种情况,在什么情况下最好执行查找而不是“普通”的LINQ查询,或者如果您持续查询INT值,则更多地使用查找? - Rand Random
这是一个更通用的解决方案,使用列表而不是表格,无法确定实体框架中是否有内置键。因此,在此处查找可帮助避免为每个ID迭代整个列表。 - Roman Pekar

2

试试这个:

public class Item
{
    public int ID { get; set; }
    public int? ParentID { get; set; }
    public string Name { get; set; }
    public int SortOrder { get; set; }
}

public void DoWork()
    {
        Item[] data = new Item[] {
            new Item() { ID = 2, ParentID = 1, Name = "Test 2", SortOrder = 1},
            new Item() { ID = 3, ParentID = 1, Name = "Test 3", SortOrder = 2},
            new Item() { ID = 4, ParentID = 2, Name = "Test 4", SortOrder = 1},
            new Item() { ID = 1, ParentID = null, Name = "Test 1", SortOrder = 1},
        };

        var result = from x in data
                     orderby x.SortOrder, x.ParentID
                     select x;

        foreach (var row in result.ToArray())
        {
            Console.WriteLine(row.Name);
        }
    }

我想这一切都与正确的排序有关。

请看一下我的编辑,不知道您是否收到了没有带有此评论的通知。 - Rand Random

2
这里有一个简单的解决方案:
public class TempTable
{
    public int ID {get;set;}
    public int? ParentID {get;set;}
    public String Name {get;set;}
    public int SortOrder {get;set;}
}

public List<TempTable> GetTempData()
{
    var temp = new List<TempTable>();
    temp.Add(new TempTable { ID = 1, ParentID = null, Name = "Test 1", SortOrder = 1 });
    temp.Add(new TempTable { ID = 2, ParentID = 1, Name = "Test 2", SortOrder = 1 });
    temp.Add(new TempTable { ID = 3, ParentID = 1, Name = "Test 3", SortOrder = 3 });
    temp.Add(new TempTable { ID = 4, ParentID = 2, Name = "Test 4", SortOrder = 1 });
    temp.Add(new TempTable { ID = 5, ParentID = 1, Name = "Test 5", SortOrder = 2 });
    return temp;
}

使用方法:

var data = GetTempData();
var result = data.OrderBy(d => d.SortOrder).ThenBy(d => d.ParentID);
//Do something with result

请查看我的编辑,不知道您是否在没有此评论的情况下收到了编辑通知。 - Rand Random

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接