在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(); } } }
运行结果如下图所示: