使用 Linq 对具有路径和深度字段的层次结构进行排序
Sort hierarchy with path and depth fields using Linq
我实现了以下层级接口
interface ITree<T>
{
// Incremential unique key
int Id { get; set; }
string Name { get; set; }
// Hierarchy classical pattern
T Parent { get; set; }
ICollection<T> Children { get; set; }
// Computed values
int Depth { get; }
// Hierarchy path with dotted notation (e.g.: 12.45.554.23,
// where each path segment is an Id)
string Path { get; set; }
}
class TreeItem : ITree<TreeItem>
{
public int Id { get; set; }
public string Name { get; set; }
public TreeItem Parent { get; set; }
public ICollection<TreeItem> Children { get; set; }
public string Path { get; set; }
public int Depth { get { return Path.Split('.').Length - 1; } }
}
这些项目是通过Entity Framework存储和检索的,因此我们可以假设所有关系字段都不为空且一致:
Path
和 Depth
总是最新的
- 链接作品(例如:
item.Parent?.Parent?.Parent
)
- 遍历
Children
字段也是递归的。
- 使用
Path
和 Depth
是首选方法,因为它不需要计算关系字段。
考虑我有以下层次结构:
- A (Depth = 0)
-- B (Depth = 1)
-- C (Depth = 1)
- D (Depth = 0)
-- E (Depth = 1)
我所有的元素都在一个无序的平面数组中,比方说 [D,C,B,E,A]。
我想使用 Linq 表达式按以下方式对它们进行排序:
- 第一级0,根据其名称字段
- 之前的所有级别 1 children,按名称排序
- 第二级0(仍根据其名称字段)
- 所有级别 1 children 的前...
该示例针对 2 级深度给出,但我希望表达式遍历层次结构,无论其深度如何。
请注意,我的数据结构的级别和路径字段可用于实现此目的,因为每当添加、移动或删除项目时都会重建树的所有路径,并且深度字段是使用简单的 Split('.') 在路径上。
测试样本:
var A = new TreeItem { Id = 1, Name = "A", Path = "1" };
var B = new TreeItem { Id = 2, Name = "B", Path = "1.2", Parent = A };
var C = new TreeItem { Id = 3, Name = "C", Path = "1.3", Parent = A };
var D = new TreeItem { Id = 4, Name = "D", Path = "4" };
var E = new TreeItem { Id = 5, Name = "E", Path = "4.5", Parent = D };
// populate children for the example.
// My actual code is automatic thanks to EF Inverse Relationship.
A.Children = new List<TreeItem> { B, C };
D.Children = new List<TreeItem> { E };
var listToSortHierarchically = new List<TreeItem> { D, C, B, E, A };
// I want the result of the hierarchical sort to be A B C D E
好的,首先你真的应该添加以下约束
interface ITree<T>
where T : class, ITree<T>
{
// ...
}
因此我们可以使用 Parent
和 Children
属性 w/o 转换安全地导航层次结构。
其次,我将重用我对 How to flatten tree via LINQ?(以及其他几个)的回答中的通用树顺序遍历辅助方法,而不是重新发明轮子:
public static partial class TreeUtils
{
public static IEnumerable<T> Expand<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector)
{
var stack = new Stack<IEnumerator<T>>();
var e = source.GetEnumerator();
try
{
while (true)
{
while (e.MoveNext())
{
var item = e.Current;
yield return item;
var elements = elementSelector(item);
if (elements == null) continue;
stack.Push(e);
e = elements.GetEnumerator();
}
if (stack.Count == 0) break;
e.Dispose();
e = stack.Pop();
}
}
finally
{
e.Dispose();
while (stack.Count != 0) stack.Pop().Dispose();
}
}
}
有了那个帮手,问题的方法就这么简单:
partial class TreeUtils
{
public static IEnumerable<T> Ordered<T>(this IEnumerable<T> source, Func<IEnumerable<T>, IEnumerable<T>> order = null)
where T : class, ITree<T>
{
if (order == null) order = items => items.OrderBy(item => item.Name);
return order(source.Where(item => item.Parent == null))
.Expand(item => item.Children != null && item.Children.Any() ? order(item.Children) : null);
}
}
示例用法:
List<TreeItem> flatList = ...;
var orderedList = flatList.Ordered().ToList();
更新: 这里是相同的,只使用 Path
和 Id
属性:
public static partial class TreeUtils
{
public static IEnumerable<T> Ordered<T>(this IEnumerable<T> source, Func<IEnumerable<T>, IEnumerable<T>> order = null)
where T : class, ITree<T>
{
if (order == null) order = items => items != null && items.Any() ? items.OrderBy(item => item.Name) : null;
var chldrenByParentId = source
.Select(item => new { item, path = item.Path.Split('.') })
.ToLookup(e => e.path.Length >= 2 ? int.Parse(e.path[e.path.Length - 2]) : (int?)null, e => e.item);
return (order(chldrenByParentId[null]) ?? Enumerable.Empty<T>())
.Expand(item => order(chldrenByParentId[item.Id]));
}
}
我实现了以下层级接口
interface ITree<T>
{
// Incremential unique key
int Id { get; set; }
string Name { get; set; }
// Hierarchy classical pattern
T Parent { get; set; }
ICollection<T> Children { get; set; }
// Computed values
int Depth { get; }
// Hierarchy path with dotted notation (e.g.: 12.45.554.23,
// where each path segment is an Id)
string Path { get; set; }
}
class TreeItem : ITree<TreeItem>
{
public int Id { get; set; }
public string Name { get; set; }
public TreeItem Parent { get; set; }
public ICollection<TreeItem> Children { get; set; }
public string Path { get; set; }
public int Depth { get { return Path.Split('.').Length - 1; } }
}
这些项目是通过Entity Framework存储和检索的,因此我们可以假设所有关系字段都不为空且一致:
Path
和Depth
总是最新的- 链接作品(例如:
item.Parent?.Parent?.Parent
) - 遍历
Children
字段也是递归的。 - 使用
Path
和Depth
是首选方法,因为它不需要计算关系字段。
考虑我有以下层次结构:
- A (Depth = 0)
-- B (Depth = 1)
-- C (Depth = 1)
- D (Depth = 0)
-- E (Depth = 1)
我所有的元素都在一个无序的平面数组中,比方说 [D,C,B,E,A]。 我想使用 Linq 表达式按以下方式对它们进行排序:
- 第一级0,根据其名称字段
- 之前的所有级别 1 children,按名称排序
- 第二级0(仍根据其名称字段)
- 所有级别 1 children 的前...
该示例针对 2 级深度给出,但我希望表达式遍历层次结构,无论其深度如何。
请注意,我的数据结构的级别和路径字段可用于实现此目的,因为每当添加、移动或删除项目时都会重建树的所有路径,并且深度字段是使用简单的 Split('.') 在路径上。
测试样本:
var A = new TreeItem { Id = 1, Name = "A", Path = "1" };
var B = new TreeItem { Id = 2, Name = "B", Path = "1.2", Parent = A };
var C = new TreeItem { Id = 3, Name = "C", Path = "1.3", Parent = A };
var D = new TreeItem { Id = 4, Name = "D", Path = "4" };
var E = new TreeItem { Id = 5, Name = "E", Path = "4.5", Parent = D };
// populate children for the example.
// My actual code is automatic thanks to EF Inverse Relationship.
A.Children = new List<TreeItem> { B, C };
D.Children = new List<TreeItem> { E };
var listToSortHierarchically = new List<TreeItem> { D, C, B, E, A };
// I want the result of the hierarchical sort to be A B C D E
好的,首先你真的应该添加以下约束
interface ITree<T>
where T : class, ITree<T>
{
// ...
}
因此我们可以使用 Parent
和 Children
属性 w/o 转换安全地导航层次结构。
其次,我将重用我对 How to flatten tree via LINQ?(以及其他几个)的回答中的通用树顺序遍历辅助方法,而不是重新发明轮子:
public static partial class TreeUtils
{
public static IEnumerable<T> Expand<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> elementSelector)
{
var stack = new Stack<IEnumerator<T>>();
var e = source.GetEnumerator();
try
{
while (true)
{
while (e.MoveNext())
{
var item = e.Current;
yield return item;
var elements = elementSelector(item);
if (elements == null) continue;
stack.Push(e);
e = elements.GetEnumerator();
}
if (stack.Count == 0) break;
e.Dispose();
e = stack.Pop();
}
}
finally
{
e.Dispose();
while (stack.Count != 0) stack.Pop().Dispose();
}
}
}
有了那个帮手,问题的方法就这么简单:
partial class TreeUtils
{
public static IEnumerable<T> Ordered<T>(this IEnumerable<T> source, Func<IEnumerable<T>, IEnumerable<T>> order = null)
where T : class, ITree<T>
{
if (order == null) order = items => items.OrderBy(item => item.Name);
return order(source.Where(item => item.Parent == null))
.Expand(item => item.Children != null && item.Children.Any() ? order(item.Children) : null);
}
}
示例用法:
List<TreeItem> flatList = ...;
var orderedList = flatList.Ordered().ToList();
更新: 这里是相同的,只使用 Path
和 Id
属性:
public static partial class TreeUtils
{
public static IEnumerable<T> Ordered<T>(this IEnumerable<T> source, Func<IEnumerable<T>, IEnumerable<T>> order = null)
where T : class, ITree<T>
{
if (order == null) order = items => items != null && items.Any() ? items.OrderBy(item => item.Name) : null;
var chldrenByParentId = source
.Select(item => new { item, path = item.Path.Split('.') })
.ToLookup(e => e.path.Length >= 2 ? int.Parse(e.path[e.path.Length - 2]) : (int?)null, e => e.item);
return (order(chldrenByParentId[null]) ?? Enumerable.Empty<T>())
.Expand(item => order(chldrenByParentId[item.Id]));
}
}