目录

介绍

我们试图解决的问题

访问者模式

C#中的访问者模式——第 1 版——经典访问者

C#中的访问者模式——第 2 版——动态访问者

此解决方案的局限性

C#中的访问者模式——第 3 版——反射访问者

此解决方案的局限性

C#中的访问者模式——第 4 版——反射扩展访问者

此解决方案的局限性

C# 中的访问者模式——第 5 版——泛型访问者

此解决方案的局限性

结论


  • 下载 ClassicVisitor 的源代码 - 26.2 KB
  • 下载DynamicVisitor的源代码 - 18.5KB
  • 下载ReflectiveVisitor的源代码 - 19.5 KB
  • 下载ReflectiveExtensionVisitor 的源代码 - 20 KB
  • 下载GenericsVisitor的源代码的源代码 - 19KB

介绍

这是一篇描述C#访问者模式的教程文章。目标受众是中级C#程序员及以上。

访客模式是23种GoF模式中最复杂的模式之一。在C#中,它有多个版本。在这里,我们将用五个版本来描述它:

  1. C#中的访问者模式——第 1 版——经典访问者
  2. C#中的访问者模式——第 2 版——动态访问者
  3. C#中的访问者模式——第 3 版——反射访问者
  4. C#中的访问者模式——第 4 版——反射扩展访问者
  5. C#中的访问者模式——第 5 版——泛型访问者

我们试图解决的问题

首先,让我们尝试了解我们试图用这种模式解决什么问题,以及经典OO设计的局限性是什么。让我们看一下图1-1和代码1-1中的经典OO设计。

public abstract class Element
{public int Attribute1 = 0;public int Attribute2 = 0;abstract public void V1();abstract public void V2();abstract public void V3();
}public class ElementA : Element
{public ElementA(){}public int Attribute3 = 0;public override void V1(){}public override void V2(){}public override void V3(){}
}public class ElementB : Element
{public ElementB(){}public int Attribute3 = 0;public override void V1(){}public override void V2(){}public override void V3(){}
}

我们在这个解决方案中看到的问题,或者更好地说,限制是:

  • 在这种方法中,数据和算法(方法V1、V2等)是耦合的​​。有时尝试将它们分开可能很有用
  • 在不改变现有类结构的情况下,添加新操作(例如V4)并不容易。这与开/关原则相反。希望能够在不改变类结构的情况下添加新的操作(方法)。
  • 在同一个地方有不同的方法(例如,V1和V2),它们可以解决完全不同和不相关的功能/问题。例如,V1可以关注生成.pdf,而V2可以关注生成html。这与关注点分离的原则背道而驰。

访问者模式

访问者模式通过将数据和操作划分为单独的类来解决上述问题/限制。数据保存在Element/Elements类中,而操作保存在Visitor/Visitors类中,每个特定的Visitor都可以解决不同的问题。通过创建新Visitor类可以很容易地实现对, Elements的操作的扩展。

此模式的关键部分是设计解决方案,使Visitor对象能够在Element上执行操。 我们说“Visitor访问Element”是对Element进行操作。

如果我们查看类图Picture1-1,我们会看到对于对象 ElementA,我们有方法V1,所以操作调用看起来像:

ElementA elementa=new ElementA();
elementa.V1();

在访问者模式中,使用V1()方法执行的操作将封装在对象Visitor1中,使用V2()方法执行的操作将封装在object Visitor2中,等等。相同的操作调用现在看起来像:

ElementA elementa=new ElementA();
Visitor1 visitor1=new Visitor1();
visitor1.visit(elementa);

情况并没有到此结束。问题是我们将有几个Element和Visitor对象,我们经常通过基类/接口来处理这些对象。然后,出现了调度适当方法的问题。“Dispatch”是找出调用哪个具体方法的问题。

C#与大多数OO语言一样,以虚函数调用的形式支持“单次调度”。也就是所谓的“动态绑定”。根据所讨论对象的类型,在运行时动态地,C#将从虚拟方法表中调用适当的虚拟函数。

但有时,这还不够,需要“多次调度”。多重分派是根据多个对象的运行时类型找到调用哪个具体方法的问题。

C#中的访问者模式——第 1 版——经典访问者

访问者模式的经典访问者版本最常见于文学作品中。在经典版本的访问者模式中,模式基于C#的“双重调度”机制。此解决方案中使用的双重调度机制基于C#的两个特性:

  1. 基于对象类型动态绑定具体虚方法的能力
  2. 根据参数类型将重载方法解析为具体方法的能力

以下是示例代码的类图的样子:

这是这个的代码:

public abstract class Element
{public abstract void Accept(IVisitor visitor);
}public class ElementA : Element
{public int Id = 0;public ElementA(int Id){this.Id = Id;}public override void Accept(IVisitor visitor)     //(2){visitor.Visit(this);}
}public class ElementB : Element
{public int Id = 0;public ElementB(int Id){this.Id = Id;}public override void Accept(IVisitor visitor){visitor.Visit(this);}
}public interface IVisitor
{void Visit(ElementA ElemA);void Visit(ElementB ElemB);
}public class Visitor1 : IVisitor    //(3)
{public virtual void Visit(ElementA ElemA)  //(4){Console.WriteLine("{0} with Id={1} visited by {2}",ElemA.GetType().Name, ElemA.Id, this.GetType().Name);}public virtual void Visit(ElementB ElemB){Console.WriteLine("{0} with Id={1} visited by {2}",ElemB.GetType().Name, ElemB.Id, this.GetType().Name);}
}public class Visitor2 : IVisitor
{public virtual void Visit(ElementA ElemA){Console.WriteLine("{0} with Id={1} visited by {2}",ElemA.GetType().Name, ElemA.Id, this.GetType().Name);}public virtual void Visit(ElementB ElemB){Console.WriteLine("{0} with Id={1} visited by {2}",ElemB.GetType().Name, ElemB.Id, this.GetType().Name);}
}class Client
{static void Main(string[] args){//--single element, explicit call-------------------------ElementA element0 = new ElementA(0);Visitor1 vis0 = new Visitor1();vis0.Visit(element0);  //(0) works//--single element, base class call-----------------------Element element = new ElementA(1);IVisitor visitor = new Visitor1();//visitor.Visit(element);   //(5) will not compileelement.Accept(visitor);  //(1)//--collection of elements-----------------List<IVisitor> listVis = new List<IVisitor>();listVis.Add(new Visitor1());listVis.Add(new Visitor2());List<Element> list = new List<Element>();list.Add(new ElementA(2));list.Add(new ElementB(3));list.Add(new ElementA(4));list.Add(new ElementB(5));foreach (IVisitor vis in listVis)foreach (Element elem in list){elem.Accept(vis);}Console.ReadLine();}
}

以下是示例执行的结果:

请注意,在(0)中,当Visitor通过显式类调用时,一切正常。我们说“Visitor访问Element”是对Element进行操作。

但是,在(5)中,当我们尝试通过基类/接口调用visitor时,我们无法编译。编译器无法解析调用哪个方法。这就是为什么我们需要使用“双重调度”来正确解决调用哪个具体方法的所有这些魔法。

在(1)中,我们有适当的调用。正在发生的事情是:

  1. 在(1)中,我们动态绑定到(2)
  2. 在(2)中,我们动态绑定到(3)
  3. 在(2)中,我们对(4)有重载决议

因为在(2)中,我们有双重分辨率,这就是它被称为“双重调度”的原因。

此解决方案的局限性。作为任何解决方案,这将有一些限制/不需要的副作用:

  • 类层次结构Elements和Visitor之间有很强的循环依赖性。 如果需要经常更新层次结构,这可能是一个问题。
  • 请注意,在(4)中,Visitor要访问Element的数据属性Id,该属性必须是public。这有点打破了封装原则。例如,在我们的第一个解决方案类图Picture1-1 中,方法V1()可以对类Element的private成员进行操作。在C++中,这可以通过使用“friend类”范式来解决,但在C#中并非如此。

C#中的访问者模式——第 2 版——动态访问者

访问者模式的动态访问者版本基于对动态调度的C#支持。这就是语言动态调度的能力,即在运行时做出具体的调用决策。我们将变量转换为“dynamic”,这样,将调度决策推迟到运行时。我们再次有“双重分派”,因为我们是根据两个对象的类型分派到具体方法,只是使用的语言机制不同。

以下是示例代码的类图的样子:

这是这个的代码:

public abstract class AElement
{
}public class ElementA : AElement
{public int Id = 0;public ElementA(int Id){this.Id = Id;}
}public class ElementB : AElement
{public int Id = 0;public ElementB(int Id){this.Id = Id;}
}public interface IVisitor
{void Visit(ElementA ElemA);void Visit(ElementB ElemB);
}public class Visitor1 : IVisitor    //(2)
{public virtual void Visit(ElementA ElemA)  //(3){Console.WriteLine("{0} with Id={1} visited by {2}",ElemA.GetType().Name, ElemA.Id, this.GetType().Name);}public virtual void Visit(ElementB ElemB){Console.WriteLine("{0} with Id={1} visited by {2}",ElemB.GetType().Name, ElemB.Id, this.GetType().Name);}
}public class Visitor2 : IVisitor
{public virtual void Visit(ElementA ElemA){Console.WriteLine("{0} with Id={1} visited by {2}",ElemA.GetType().Name, ElemA.Id, this.GetType().Name);}public virtual void Visit(ElementB ElemB){Console.WriteLine("{0} with Id={1} visited by {2}",ElemB.GetType().Name, ElemB.Id, this.GetType().Name);}
}class Client
{static void Main(string[] args){//--single element-------------------------AElement element = new ElementA(1);IVisitor visitor = new Visitor1();visitor.Visit((dynamic)element); //(1)//--collection of elements-----------------List<IVisitor> listVis = new List<IVisitor>();listVis.Add(new Visitor1());listVis.Add(new Visitor2());List<AElement> list = new List<AElement>();list.Add(new ElementA(2));list.Add(new ElementB(3));list.Add(new ElementA(4));list.Add(new ElementB(5));foreach (IVisitor vis in listVis)foreach (AElement elem in list){vis.Visit((dynamic)elem);}Console.ReadLine();}
}

以下是示例执行的结果:

在(1)中,我们有新的调用。由于动态对象如何工作的性质,解决方案被推迟到运行时。然后,我们首先基于Visitor到(2)的类型进行动态绑定,然后根据在运行时动态发现的Element类型动态解析到(3) 。

此解决方案的局限性

作为任何解决方案,这有一些限制/不需要的副作用:

  • 类层次结构Elements和Visitor之间有很强的循环依赖性。如果需要经常更新层次结构,这可能是一个问题。
  • 请注意,Visitor要访问Element的数据属性ID ,该属性必须是public。这有点打破了封装原则。例如,在我们的第一个解决方案类图Picture1-1中,方法V1()可以对Element类的private成员进行操作。
  • “dynamic”对象的使用给我们带来了性能影响。

C#中的访问者模式——第 3 版——反射访问者

访问者模式的反射访问者版本基于使用C#反射技术在运行时发现对象类型并执行基于发现的类型的显式方法分派。我们再次有“双重分派”,因为我们是根据两个对象的类型分派到具体方法,只是使用的语言机制不同。

以下是示例代码的类图的样子:

这是这个的代码:

public abstract class AElement
{
}public class ElementA : AElement
{public int Id = 0;public ElementA(int Id){this.Id = Id;}
}public class ElementB : AElement
{public int Id = 0;public ElementB(int Id){this.Id = Id;}
}public abstract class AVisitor
{public void Visit(AElement Elem)  //(2){if (Elem is ElementA){Visit((ElementA)Elem);};if (Elem is ElementB){Visit((ElementB)Elem);};}public abstract void Visit(ElementA ElemA);public abstract void Visit(ElementB ElemB);
}public class Visitor1 : AVisitor
{public override void Visit(ElementA ElemA)  //(3){Console.WriteLine("{0} with Id={1} visited by {2}",ElemA.GetType().Name, ElemA.Id, this.GetType().Name);}public override void Visit(ElementB ElemB){Console.WriteLine("{0} with Id={1} visited by {2}",ElemB.GetType().Name, ElemB.Id, this.GetType().Name);}
}public class Visitor2 : AVisitor
{public override void Visit(ElementA ElemA){Console.WriteLine("{0} with Id={1} visited by {2}",ElemA.GetType().Name, ElemA.Id, this.GetType().Name);}public override void Visit(ElementB ElemB){Console.WriteLine("{0} with Id={1} visited by {2}",ElemB.GetType().Name, ElemB.Id, this.GetType().Name);}
}class Client
{static void Main(string[] args){//--single element-------------------------AElement element = new ElementA(1);AVisitor visitor = new Visitor1();visitor.Visit(element); //(1)//--collection of elements-----------------List<AVisitor> listVis = new List<AVisitor>();listVis.Add(new Visitor1());listVis.Add(new Visitor2());List<AElement> list = new List<AElement>();list.Add(new ElementA(2));list.Add(new ElementB(3));list.Add(new ElementA(4));list.Add(new ElementB(5));foreach (AVisitor vis in listVis)foreach (AElement elem in list){vis.Visit(elem);}Console.ReadLine();}
}

以下是示例执行的结果:

在(1)中,我们有新的调用。即使在编译时,它也被解析为方法(2)。然后在运行时,使用反射,解析参数类型并将调用传递给(3)。

此解决方案的局限性

作为任何解决方案,这有一些限制/不需要的副作用:

  • 类层次结构Elements和Visitor之间有很强的循环依赖性。如果需要经常更新层次结构,这可能是一个问题。
  • 请注意,Visitor要访问Element的数据属性Id,该属性必须是public。这有点打破了封装原则。例如,在我们的第一个解决方案类图Picture1-1中,方法V1()可以对Element类的private成员进行操作。
  • 请注意,在(2)中,每个继承自AElement的类都被明确提及并检查类型。缺少某些类型可能是实现的问题。一种可能的解决方案是通过使用反射来发现程序集中的所有类型并自动调度到所有继承自AElement。但是,我们不会在这里这样做。

C#中的访问者模式——第 4 版——反射扩展访问者

访问者模式的Reflective-Extension访问者版本基于:1)使用C#反射技术在运行时发现对象类型,并根据发现的类型执行显式方法分派;2)扩展方法的使用。此版本与“Reflective Visitor”版本非常相似,但由于在其他文献中提到过,我们在此也将其列为单独的变体。我们再次有“双重分派”,因为我们是根据两个对象的类型分派到具体方法,只是使用的语言机制不同。

以下是示例代码的类图的样子:

这是此代码:

public abstract class AElement
{
}public class ElementA : AElement
{public int Id = 0;public ElementA(int Id){this.Id = Id;}
}public class ElementB : AElement
{public int Id = 0;public ElementB(int Id){this.Id = Id;}
}public abstract class AVisitor
{public abstract void Visit(ElementA ElemA);public abstract void Visit(ElementB ElemB);
}public static class AVisitorExtensions
{public static void Visit<T>(this T vis, AElement Elem)where T : AVisitor               //(2){if (Elem is ElementA){vis.Visit((ElementA)Elem);    //(3)};if (Elem is ElementB){vis.Visit((ElementB)Elem);};}
}public class Visitor1 : AVisitor
{public override void Visit(ElementA ElemA)  //(4){Console.WriteLine("{0} with Id={1} visited by {2}",ElemA.GetType().Name, ElemA.Id, this.GetType().Name);}public override void Visit(ElementB ElemB){Console.WriteLine("{0} with Id={1} visited by {2}",ElemB.GetType().Name, ElemB.Id, this.GetType().Name);}
}public class Visitor2 : AVisitor
{public override void Visit(ElementA ElemA){Console.WriteLine("{0} with Id={1} visited by {2}",ElemA.GetType().Name, ElemA.Id, this.GetType().Name);}public override void Visit(ElementB ElemB){Console.WriteLine("{0} with Id={1} visited by {2}",ElemB.GetType().Name, ElemB.Id, this.GetType().Name);}
}class Client
{static void Main(string[] args){//--single element-------------------------AElement element = new ElementA(1);AVisitor visitor = new Visitor1();visitor.Visit(element);      //(1)//--collection of elements-----------------List<AVisitor> listVis = new List<AVisitor>();listVis.Add(new Visitor1());listVis.Add(new Visitor2());List<AElement> list = new List<AElement>();list.Add(new ElementA(2));list.Add(new ElementB(3));list.Add(new ElementA(4));list.Add(new ElementB(5));foreach (AVisitor vis in listVis)foreach (AElement elem in list){vis.Visit(elem);}Console.ReadLine();}
}

以下是示例执行的结果:

在(1)中,我们有新的调用。即使在编译时,它也被解析为方法(2)。然后在运行时,使用反射,参数类型在(3)中解析,调用传递给(4)。

此解决方案的局限性

作为任何解决方案,这有一些限制/不需要的副作用:

  • 类层次结构Elements和Visitor之间有很强的循环依赖性。如果需要经常更新层次结构,这可能是一个问题。
  • 请注意,Visitor要访问Element的数据属性Id,该属性必须是public。这有点打破了封装原则。例如,在我们的第一个解决方案类图Picture1-1中,方法V1()可以对Element类的private成员进行操作。
  • 请注意,在(2)中,每个继承自AElement的类都被明确提及并检查类型。缺少某些类型可能是实现的问题。一种可能的解决方案是使用反射发现程序集中的所有类型,并自动分派给所有继承自AElement。但是,我们不会在这里这样做。

C# 中的访问者模式——第 5 版——泛型访问者

访问者模式的泛型访问者版本类似于反射访问者模式,因为它依赖于1)反射在运行时动态发现类型;2) C#泛型来指定接口Visitor实现。我们再次有“双重分派”,因为我们是根据两个对象的类型分派到具体方法,只是使用的语言机制不同。

以下是示例代码的类图的样子:

这是此的代码:

public abstract class Element
{public abstract void Accept(IVisitor visitor);
}public class ElementA : Element
{public int Id = 0;public ElementA(int Id){this.Id = Id;}public override void Accept(IVisitor visitor)     //(2){if (visitor is IVisitor<ElementA>){((IVisitor<ElementA>)visitor).Visit(this);}}
}public class ElementB : Element
{public int Id = 0;public ElementB(int Id){this.Id = Id;}public override void Accept(IVisitor visitor)     {if (visitor is IVisitor<ElementB>){((IVisitor<ElementB>)visitor).Visit(this);}}
}public interface IVisitor { }; // marker interfacepublic interface IVisitor<TVisitable>
{void Visit(TVisitable obj);
}public class Visitor1 : IVisitor,IVisitor<ElementA>, IVisitor<ElementB>
{public void Visit(ElementA ElemA)   //(3){Console.WriteLine("{0} with Id={1} visited by {2}",ElemA.GetType().Name, ElemA.Id, this.GetType().Name);}public void Visit(ElementB ElemB){Console.WriteLine("{0} with Id={1} visited by {2}",ElemB.GetType().Name, ElemB.Id, this.GetType().Name);}
}public class Visitor2 : IVisitor,IVisitor<ElementA>, IVisitor<ElementB>
{public void Visit(ElementA ElemA){Console.WriteLine("{0} with Id={1} visited by {2}",ElemA.GetType().Name, ElemA.Id, this.GetType().Name);}public void Visit(ElementB ElemB){Console.WriteLine("{0} with Id={1} visited by {2}",ElemB.GetType().Name, ElemB.Id, this.GetType().Name);}
}class Client
{static void Main(string[] args){//--single element, base class call-----------------------Element element = new ElementA(1);IVisitor visitor = new Visitor1();element.Accept(visitor);  //(1)//--collection of elements-----------------List<IVisitor> listVis = new List<IVisitor>();listVis.Add(new Visitor1());listVis.Add(new Visitor2());List<Element> list = new List<Element>();list.Add(new ElementA(2));list.Add(new ElementB(3));list.Add(new ElementA(4));list.Add(new ElementB(5));foreach (IVisitor vis in listVis)foreach (Element elem in list){elem.Accept(vis);}Console.ReadLine();}
}

以下是示例执行的结果:

在(1)中,我们有一个新的调用。在运行时,它动态绑定到(2)。然后在(2)中,我们使用反射将其显式解析为(3)。

此解决方案的局限性

作为任何解决方案,这有一些限制/不需要的副作用:

  • 请注意,Visitor要访问Element的数据属性Id,该属性必须是public。这有点打破了封装原则。例如,在我们的第一个解决方案类图Picture1-1中,方法V1()可以对Element类的private成员进行操作。

结论

首先,我们讨论了我们的动机和我们试图解决的问题。我们正在努力实现的目标很重要,因为解决问题的方法可能不止一种。

然后我们展示了“经典访客”,这是由GoF提出并在文献中经常提到的版本。我认为由于创建它时语言(C++。Smalltalk)的限制,这被提议为唯一和最终的解决方案。

现代OO语言,如C#,具有“动态对象”和“反射”等新功能,可以通过不同的方式实现相同的目标。这在访问者模式的其他四个版本中得到了展示。如果您愿意,可以将它们视为“启用现代C#”的访问者模式的替代版本。

https://www.codeproject.com/Articles/5326263/Visitor-Pattern-in-Csharp-5-Versions

C#中的访问者模式——5个版本相关推荐

  1. 设计模式学习之访问者模式

    访问者模式,是行为型设计模式之一.访问者模式是一种将数据操作与数据结构分离的设计模式,它可以算是 23 中设计模式中最复杂的一个,但它的使用频率并不是很高,大多数情况下,你并不需要使用访问者模式,但是 ...

  2. Vistor(访问者模式)行为型

    访问者模式 一.概述 二.结构 三.实例 四.优缺点 五.适用场景 一.概述 描述:顾客在超超市购买商品时,如苹果.图书等将放在购物车中,然后到收银员付款.在购物过程中,顾客需要对这些商品访问,以便确 ...

  3. 设计模式之访问者模式

    作者:zuoxiaolong8810(左潇龙),转载请注明出处,特别说明:本博文来自博主原博客,为保证新博客中博文的完整性,特复制到此留存,如需转载请注明新博客地址即可. 本次LZ和各位分享一下访问者 ...

  4. 二十二:访问者模式(伪动态双分配)

    定义(源于GoF<Design Pattern>):表示一个作用于某对象结构中的各元素的操作.它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作. 初次接触,定义会显得晦涩并且难 ...

  5. (二十二)访问者模式详解(伪动态双分派) - 转

    作者:zuoxiaolong8810(左潇龙),转载请注明出处. 本次LZ和各位分享一下访问者模式,从场景.设计初衷以及实现方面来说,访问者模式算是LZ即将写到的24种设计模式当中,最复杂也是最难理解 ...

  6. 设计模式之访问者模式、例子分析

    1. 定义 访问者模式( Visitor):表示一个作用于某对象结构中的各元素的操作.它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作 2. 使用前提 这个模式是很复杂的模式,一般情况下 ...

  7. 《研磨设计模式》chap25 访问者模式Visitor(3)联合组合模式+总结

    1. 联合组合模式 //抽象的组件对象,相当于访问者模式中的元素对象 public abstract class Component {//接受访问者的访问 public abstract void ...

  8. 23种设计模式C++源码与UML实现--访问者模式

    访问者模式 Vistor模式也叫访问者模式,是行为模式之一,它分离对象的数据和行为,使用Vistor模式,可以不修改已有类的情况下,增加新的操作角色和职责. 抽象访问者(Visitor)角色:声明了一 ...

  9. C++设计模式之访问者模式

    访问者模式 在GOF的<设计模式:可复用面向对象软件的基础>一书中对访问者模式是这样说的:表示一个作用于某对象结构中的各元素的操作.它使你可以在不改变各元素的类的前提下定义作用于这些元素的 ...

最新文章

  1. malloc、calloc、realloc的区别
  2. CYPRESS USB芯片win10驱动
  3. 解决vc2008 utf8中文字符串报错 C2001常量中有换行符
  4. 周志华《机器学习》课后习题(第三章):线性模型
  5. Codeforces 1479A. Searching Local Minimum(注意输入+二分)
  6. spring autoproxy by annotation
  7. : 无法获得锁 /var/lib/dpkg/lock-frontend - open (11: 资源暂时不可用) E: 无法获取 dpkg 前端锁 (/var/lib/dpkg/lock-fronte
  8. leetcode 17. 电话号码的字母组合 思考分析
  9. OpenStack Nova核心组件和RabbitMQ通信流程分析
  10. 工作线程AfxBeginThread的使用
  11. Table阿里云mysql_数据同步-从MySQL到Tablestore-阿里云开发者社区
  12. 昆仑通态如何连接sqlserver数据库_sqlserver数据库怎么开启远程连接,给到别人访问...
  13. 如何让网页首屏更具视觉吸引力?
  14. Mybatis中Mapper.xml文件sql中动态获取----#{}和${}区别
  15. 【C#小知识】C#中一些易混淆概念总结(八)---------解析接口
  16. 18.docker top
  17. 苹果cms播放器html,苹果cms 全局播放器dplayer带后台Dplayer播放器苹果CMSV10插件
  18. 演出节目名字_【时空文化集团演出公司承制】山东港口首届道德模范、最美家庭颁奖典礼成功举办!...
  19. 关于AndroidStudio3.0以上(3.2bate)创建.9patch图片出错的问题
  20. 《自控力》分享一些减缓压力小妙招

热门文章

  1. 大学计算机入学教育心得1000,大学课程心得体会范文大全1000-大学入学教育心得体会1000字,怎么写?...
  2. 网络游戏客户端同步方法
  3. 【sql】查询每日活跃用户数/每日新增用户数/每日累计用户数
  4. shell 脚本符号介绍
  5. python jieba库不存在_Python入门:jieba库的使用
  6. 从炉石传说卡组推荐理解ASO搜索优化,aso搜索优化怎么用
  7. Android Gradle 基本模板
  8. 计算机网络---万维网(WWW)
  9. 智联招聘发布2013IT行业招聘需求报告
  10. centos7网卡启动失败