在Visual Studio .NET中,一个解决方案可以包含多个项目,一个项目可以引用若干其它项目。编译的时候,VS会自动确定每个项目的编译顺序。VS究竟是如何计算出这个顺序的呢?
如果学习过数据结构,可以很容易回答出这个问题:拓扑排序(Topological Sort)。
什么是拓扑排序?让我们来温习一下。百度百科上的介绍如下:
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若<u,v> ∈E(G),则u在线性序列中出现在v之前。
上述介绍抽象,不如用实际案例来解释一下。假如在VS中创建一个MVC的解决方案XMedia,该解决方案包含的项目,以及项目之间的引用关系如下表所示:
项目 | 引用 |
XMedia | XMedia.Controllers、XMedia.Models、XMedia.Logics、XMedia.Commons |
XMedia.Controllers | XMedia.Models、XMedia.Logics、XMedia.Commons |
XMedia.Models |
|
XMedia.Logics | XMedia.Models、XMedia.Commons |
XMedia.Commons |
|
项目之间的引用关系,是一种依赖关系。如果项目A引用项目B,则表示A依赖B。所以,必须先编译项目B,再编译项目A。
根据经验,我们可以得出上述项目的编译顺序依次是:XMedia.Commons、XMedia.Models、XMedia.Logics、XMedia.Controllers、XMedia。当然,也可以把前两项对调一下。
项目和引用关系构成了一张有向图图,项目相当于有向图中的顶点(Vertex),引用关系相当于有向图中的边(Edge),而项目的编译顺序就是一个拓扑序列,产生该序列的算法称为拓扑排序算法。
以下是项目引用关系的有向图展示:
拓扑排序算法的简要描述:
(1) 从有向图中选择一个出度为0的顶点并且输出它。
(2) 从图中删去该顶点,并且删去该顶点的所有边。
(3) 重复上述两步,直到剩余的图中没有出度为0的顶点。
按照上述算法,运行过程演示如下:
第一步 选择 XMedia.Commons节点
第二步 选择XMedia.Models节点
第三步 选择XMedia.Logics节点
第四步 选择XMedia.Controllers节点
第五步 选择XMedia节点
接下来我们用C#实现代码实现这个算法。
由于拓扑排序是一个应用很多的算法,所以,我们将实现一个通用的排序算法。在这个通用的算法中,我们将顶点之间的关系作为依赖关系。代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
namespace ConsoleApplication1 {
/// <summary>
/// 拓扑排序类。
/// </summary>
public class TopologicSort {
/// <summary>
/// 拓扑顺序。
/// </summary>
/// <typeparam name="TKey">节点的键值类型。</typeparam>
/// <param name="nodes">一组节点。</param>
/// <returns>拓扑序列。</returns>
/// <exception cref="InvalidOperationException">如果存在双向引用或循环引用,则抛出该异常。</exception>
public IEnumerable<string> OrderBy(IEnumerable<TopologicNode> nodes) {
if (nodes == null) yield break;
//复制一份,便于操作
List<TopologicNode> list = new List<TopologicNode>();
foreach (var item in nodes) {
TopologicNode node = new TopologicNode() { Key = item.Key };
if (item.Dependences != null)
node.Dependences = new List<string>(item.Dependences);
list.Add(node);
}
while (list.Count > 0) {
//查找依赖项为空的节点
var item = list.FirstOrDefault(c => c.Dependences == null || c.Dependences.Count == 0);
if (item != null) {
yield return item.Key;
//移除用过的节点,以及与其相关的依赖关系
list.Remove(item);
foreach (var otherNode in list) {
if (otherNode.Dependences != null)
otherNode.Dependences.Remove(item.Key);
}
} else if (list.Count > 0) {
//如果发现有向环,则抛出异常
throw new InvalidOperationException("存在双向引用或循环引用。");
}
}
}
}
/// <summary>
/// 拓扑节点类。
/// </summary>
public class TopologicNode {
/// <summary>
/// 获取或设置节点的键值。
/// </summary>
public string Key { get; set; }
/// <summary>
/// 获取或设置依赖节点的键值列表。
/// </summary>
public List<string> Dependences { get; set; }
}
}
测试代码如下:
using System;
using System.Collections.Generic;
namespace ConsoleApplication1 {
class Program {
static void Main(string[] args) {
List<TopologicNode> nodes = new List<TopologicNode>()
{
new TopologicNode(){ Key = "XMedia",
Dependences = new List<string>(){ "XMedia.Controllers", "XMedia.Models", "XMedia.Logics", "XMedia.Commons" } },
new TopologicNode(){ Key = "XMedia.Controllers",
Dependences = new List<string>(){"XMedia.Models","XMedia.Logics","XMedia.Commons"}},
new TopologicNode(){ Key = "XMedia.Logics",
Dependences = new List<string>(){ "XMedia.Models","XMedia.Commons"}},
new TopologicNode(){ Key = "XMedia.Models" },
new TopologicNode(){ Key = "XMedia.Commons" }
};
//输出拓扑排序的结果
TopologicSort sort = new TopologicSort();
foreach (var key in sort.OrderBy(nodes)) {
Console.WriteLine(key);
}
Console.ReadLine();
}
}
}
运行结果如下图所示: