WCF服务使用(IIS+Http)和(Winform宿主+Tcp)两种方式进行发布

1、写在前面
刚接触WCF不久,有很多地方知其然不知其所以然。当我在【创建服务->发布服务->使用服务】这一过程出现过许多问题。如客户端找不到服务引用;客户端只在本机环境中才能访问服务,移植到其他机器上就不能访问服务(权限问题)等问题。所以写下这篇文章把我使用http和tcp这两方式部署服务所出现的问题以及解决方案记录下来,方便自己下次查看,也可以当作初学WCF的一个入门小示例吧。
 
2、建立一个WCF服务
首先要编写一个WCF服务,我在这里提供一个通过名字查询年龄的服务,代码如下:
服务契约:
复制代码
    [ServiceContract]    public interface IPeopleInfo
    {
        [OperationContract]        int GetAge(string name);
    }
复制代码

 数据契约:

复制代码
    [DataContract]    public class People
    {        public string Name;        public int Age;
    }
复制代码

服务实现:

复制代码
    public class PeopleInfo : IPeopleInfo
    {        public int GetAge(string name)
        {
            List<People> peopleList = DataFactory.GetPeopleList();            return peopleList.Find(t =>t.Name==name).Age ;
        }
    }
复制代码

 辅助类:

复制代码
    public class DataFactory
    { 
        public static List<People> GetPeopleList()
        {
            List<People> peopleList = new List<People>();
            peopleList.AddRange(new People[] { 
                new People{Name="tjm",Age=18},                new People{Name="lw",Age=20},                new People{Name="tj",Age=22},
            });            return peopleList;
        }
    }
复制代码

 

2、发布(部署)服务
在这里使用两种方式发布服务。一:利用iis作为宿主,使用http通信协议发布服务;二:利用Winform程序作为宿主,使用tcp通信协议发布服务。
 
2.1、iis作为宿主,使用http通信协议发布服务
把1中已经编写好的服务,可以像部署一个WebService或者是Asp.Net网站一样部署到局域网上去。这里的发布服务的步骤就省略了。发布成功之后在浏览器中进行访问时出现如下的错误:
WCF部署IIS出现“由于扩展配置问题而无法提供您请求的页面。如果该页面是脚本,请添加处理程序。如果应下载文件,请添加 MIME 映射”的解决办法
网上有找了很多资料,大部分的解决方案都是说,系统没有默认iis注册WCF服务的svc文件的MIME映射,于是我为iis注册WCF服务的映射,方法如下:

管理员身份运行C:\Windows\Microsoft.NET\Framework\v3.0\Windows Communication Foundation\ServiceModelReg.exe -i

但是注册完之后,在浏览器中浏览该网站还是出现错误,错误如下:
HTTP 错误 404.3 - Not Found,
于是在网上又找到解决方案如下:
使用管理员注册:C:\Windows\system32>"C:\Windows\Microsoft.NET\Framework\v4.0.30319\ServiceModelReg.exe" -r
注册完之后直接在控制台中出现如下错误,

Microsoft(R) WCF/WF 注册工具版本 4.5.0.0

版权所有(C) Microsoft Corporation。保留所有权利。

用于管理一台计算机上 WCF 和 WF 组件的安装和卸载的管理实用工具。

[错误]此 Windows 版本不支持此工具。管理员应改为使用“打开或关闭 Windows 功能”对话框或 dism 命令行工具来安装/卸载 Windows Communication Foundation 功能。

根据错误的提示,去控制面板->程序->启用或关闭Windows功能,如下图

 

 把我画红线的框内的复选框全部勾选,点击确定,然后再在iis中再进行浏览就能够找到发布后的WCF服务了,原因应该是我没有安装WCF服务的组件而导致的吧。在浏览器中浏览服务如下。

如果上述的方法,还没有在iis中成功发布服务,可以尝试。
在window功能中卸载iis和WCF服务,然后再重新安装配置。
到此,使用iis中利用http协议发WCF服务已经成功了。
 
2.2 利用Winform程序作为宿主,使用tcp通信协议发布服务

首先建立一个Winform项目,界面如下。

在这里使用两种方式来宿主WCF服务,第一用代码的方式,第二使用配置文件的方式。
注意:在部署之前需要把第一步中编写服务的dll引用进来。
1)使用代码部署WCF服务
代码如下:
复制代码
  btnStart_Click(
            Uri tcpa =  Uri(=  ServiceHost(= = (IMetadataExchange), MetadataExchangeBindings.CreateMexTcpBinding(),   btnStop_Click( (host != .label1.Text = .btnStart.Enabled =
复制代码

2)使用配置文件

其实使用配置文件和使用代码理论是差不多的,可以根据代码编写配置文件,上面的代码分析可知道,服务宿主对象host添加了一个ServiceMetadataBehavior 对象和两个Endpoint,因此App.config配置文件中的内容如下。
复制代码
<?xml version="1.0" encoding="utf-8" ?><configuration>
  <system.serviceModel>
    <services>
      <service name="FirstWCFService.PeopleInfo">
        <!--客户端用来解释服务,这个端点如果缺少服务会发布失败-->
        <endpoint contract="IMetadataExchange"
           binding="mexTcpBinding" address ="net.tcp://172.21.212.54:8090/peoleinfo/mex"></endpoint>
        <!--提供服务的端点-->
        <endpoint address="net.tcp://172.21.212.54:8090/peoleinfo" binding="netTcpBinding"
            contract="FirstWCFService.IPeopleInfo">

        </endpoint>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior>
          <serviceMetadata/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel></configuration>
复制代码

 虽然是用来配置文件,但是开启服务的代码还是需要写的。代码如下。

复制代码
        ServiceHost host;        private void btnStart_Click(object sender, EventArgs e)
        {
            host = new ServiceHost(typeof(FirstWCFService.PeopleInfo));
            host.Open();            this.btnStart.Enabled = false;            this.label1.Text = "Service Running";
        }        private void btnStop_Click(object sender, EventArgs e)
        {            if (host != null)
            {
                host.Close();                this.label1.Text = "Service Closed";                this.btnStart.Enabled = true;
            }
        }
复制代码

 运行程序,点击Start按钮,到此使用Winform作为宿主来发布WCF服务已经成功了。

 
3、编写客户端调用服务。
客户端使用Winform程序,界面设计如下:
 
然后分别添加使用http和tcp方式发布的wcf服务。
1)引用iis+http协议发布的服务
2)引用Winform宿主+tcp协议发布的服务
服务添加完成之后,在项目中会自动生成一个应用程序的配置文件,里面记录了调用WCF服务的信息。配置文件内容如下:
复制代码
<?xml version="1.0" encoding="utf-8" ?><configuration>
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="BasicHttpBinding_IPeopleInfo" />
            </basicHttpBinding>
            <netTcpBinding>
                <binding name="NetTcpBinding_IPeopleInfo">
                </binding>
            </netTcpBinding>
        </bindings>
        <client>
            <endpoint address="http://172.21.212.54/PeopleInfo.svc" binding="basicHttpBinding"
                bindingConfiguration="BasicHttpBinding_IPeopleInfo" contract="HttpService.IPeopleInfo"
                name="BasicHttpBinding_IPeopleInfo" />
            <endpoint address="net.tcp://172.21.212.54:8090/peoleinfo" binding="netTcpBinding"
                bindingConfiguration="NetTcpBinding_IPeopleInfo" contract="TcpService.IPeopleInfo"
                name="NetTcpBinding_IPeopleInfo" />
        </client>
    </system.serviceModel></configuration>
复制代码

 调用服务操作的代码非常简单,就类似于操作本地的类一样,代码如下: 

复制代码
        private void bntGet_Click(object sender, EventArgs e)
        {            string name = this.txtName.Text;            int age;            if (rdbHttp.Checked)
            {
                HttpService.PeopleInfoClient httpClient = new HttpService.PeopleInfoClient();
                age = httpClient.GetAge(name);
            }            else
            {
                TcpService.PeopleInfoClient tcpClient = new TcpService.PeopleInfoClient();
                age = tcpClient.GetAge(name);
            }            this.txtAge.Text = age.ToString();
        }
复制代码

 到此,wcf的客户端编写完成,结果如下。

 
注意:把wcf的客户端部署到其他机器上去运行,选择http进行调用没有问题,而选择tcp调用服务的时候会出现如下错误:

System.ServiceModel.Security.SecurityNegotiationException: 服务器已拒绝客户端凭据。 --->

System.Security.Authentication.InvalidCredentialException: 服务器已拒绝客户端凭据。 ---> 

System.ComponentModel.Win32Exception: 登录没有成功
其中http方式是由iis进行托管的,里面权限已经是配置好的,允许匿名用户进行访问,因此在远程访问时不会出现问题的。而tcp方式是由我们自己编写winform程序管理的,当客户端在本机时候,是因为它本身就具有window的用户访问的权限,因此可以访问,当部署到其他其他机器上去的时候,已经不具备本机的这种环境,所以当我们使用winform作为宿主的时候,要在配置文件中设置权限模式,在这里使用无需客户端验证的方式。在Winform宿主项目的配置文件添加如下内容,红色加粗标示为新增的。
复制代码
<?xml version="1.0" encoding="utf-8" ?><configuration>
  <system.serviceModel>
    <services>
      <service name="FirstWCFService.PeopleInfo">
        <!--客户端用来解释服务,这个端点如果缺少服务会发布失败-->
        <endpoint contract="IMetadataExchange"
           binding="mexTcpBinding" address ="net.tcp://172.21.212.54:8090/peoleinfo/mex"></endpoint>
        <!--提供服务的端点-->
        <endpoint address="net.tcp://172.21.212.54:8090/peoleinfo" binding="netTcpBinding"
            contract="FirstWCFService.IPeopleInfo" bindingConfiguration="NoSecurity">
        </endpoint>
      </service>
    </services>    <bindings>
      <netTcpBinding>
        <binding name="NoSecurity">
          <security mode="None"/>
        </binding>
      </netTcpBinding>
    </bindings>    <behaviors>
      <serviceBehaviors>
        <behavior>
          <serviceMetadata/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel></configuration>
复制代码

 设置完成之后,重新开启服务。然后在客户端删除winform宿主发布的服务,并且再重新添加,以便在配置文件中重新生成访问tcp方式的服务端点的配置。更新后的配置文件如下,红色加粗部分为更新的。

复制代码
<?xml version="1.0" encoding="utf-8" ?><configuration>
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="BasicHttpBinding_IPeopleInfo" />
            </basicHttpBinding>
            <netTcpBinding>
                <binding name="NetTcpBinding_IPeopleInfo">
                    <security mode="None" />                </binding>
            </netTcpBinding>
        </bindings>
        <client>
            <endpoint address="http://172.21.212.54/PeopleInfo.svc" binding="basicHttpBinding"
                bindingConfiguration="BasicHttpBinding_IPeopleInfo" contract="HttpService.IPeopleInfo"
                name="BasicHttpBinding_IPeopleInfo" />
            <endpoint address="net.tcp://172.21.212.54:8090/peoleinfo" binding="netTcpBinding"
                bindingConfiguration="NetTcpBinding_IPeopleInfo" contract="TcpService.IPeopleInfo"
                name="NetTcpBinding_IPeopleInfo" />
        </client>
    </system.serviceModel></configuration>
复制代码

 

4、结论
虽然WCF服务的两种部署方式已经成功了,但是其中有许多的原理还不是太明白,毕竟接触WCF还不久。希望后面继续深入的学习,达到知其然并且知其所以然境界。


半导体行业通信标准-SECS / GEM

  • 第一章 介绍

  • 第二章 GEM 收集事件

  • 第三章 数据轮询

  • 第四章 GEM 工厂应用支持

  • 第五章 报警

  • 第六章 配方管理

  • 第七章 文档

  • 第八章 设备终端服务

  • 第九章 用户界面

  • 第十章 GEM消息假脱机功能

  • 第十一章 协议层

  • 第十二章 消息日志

  • 第十三章GEM 控制状态

  • 第十四章 总结





第一章 介绍

SECS/GEM指的是一组用于管理制造设备和工厂主机系统之间通信的半导体行业标准。消息层标准SEMI E5 SECS-II定义了一个通用的消息结构和一个包含许多标准化消息的库。协议层标准SEMI E37高速消息服务(HSMS)定义了使用TCP/IP传输SECS-II消息的二进制结构。SEMI E30 GEM定义了一组最低要求、附加(可选)功能、用例和部分SECS-II消息的用户场景。

SECS/GEM是在设备上实现的,工厂使用它来实现命令和控制功能。由于它是一个行业标准,任何符合SECS/GEM的主机软件都可以与任何符合SECS/GEM设备进行通信。该标准在设备上全面实施后,工厂软件可通过其SECS/GEM接口对设备进行全面控制和监控。这些标准为设备制造商和工厂提供了许多好处。本文重点介绍了其中的几个好处。

SECS/GEM降低了设备集成成本

工厂通常由跨国企业拥有和经营,这些企业从各种设备制造商购买设备。尽管每种设备的控制软件都不一样,但要求工厂对设备进行整合,使设备协调运行。虽然可以独立地将每个设备与定制的软件集成在一起,但是这样做既不节约成本,也不节省时间。

设备制造商的情况也类似,他们的产品销往全球各地的不同工厂。每个工厂的数据收集和应用软件都是不同的。设备制造商需要帮助工厂整合采购的设备。虽然为每个工厂开发定制的集成解决方案是可能的,但这仍然不是成本有效的。每当工厂要求定制集成特性时,这些成本就会转嫁到工厂本身。

定制化软件,无论是由设备制造商还是工厂开发的,创建和维护起来都很昂贵,而且往往质量低于预期。相比之下,SECS/GEM标准定义了如何在任何制造设备上创建标准化接口。设备制造商受益于为所有客户开发一个接口。工厂通过为他们购买的所有设备重用相同的集成软件而获益。工厂和设备制造商对该软件和技术的重用提高了软件质量,降低了成本,并允许更多的功能。设备制造商和工厂不仅可以在所需的最低需求功能上投资,还可以实现在其他方面无法负担的高级功能。如果他们只需要支持SECS/GEM,那么设备制造商就可以发布更多的数据,支持更先进的控制。反过来,工厂可以利用这些额外的数据来提高产品质量和生产率。

SECS/GEM适用于所有制造设备

因为SECS/GEM被划分为基本需求和附加功能,它可以在任何制造设备上实现,而不考虑其大小和复杂性。额外的功能是可选的,因为它们并不总是被需要的。例如,一些设备没有配方,因此不需要实现配方管理这项附加功能。

SECS/GEM也可以很好地根据设备数据的大小进行规模的缩放。例如,一个非常简单的设备或设备可能会发布10个不同的采集事件,而一个复杂的设备可能会发布5000个不同的采集事件; 然而,两者都可以使用相同的SECS/GEM技术。

使用SECS/GEM接口可以支持无数的应用程序

SECS/GEM使得设备上发生的一切都可以被追踪到。支持任何远程控制功能和系统配置。设备发布的数据越多,工厂可以实现的软件应用程序就越多。SECS/GEM接口使统计工艺控制、故障排除、预测性维护、前馈/反馈工艺控制、设备利用率、材料跟踪、配方验证以及更多应用程序的实现成为可能。这些应用程序通常减少了对设备上人机操作界面的需要,从而减少了工厂中操作员的数量。配方管理允许工厂最小化报废材料。例如,使用SECS/GEM接口将黄金配方存储在工厂的中央存储器,并确保在材料上使用正确的配方。

SECS/GEM非常有效地使用网络带宽

有几个特性使SECS/GEM自然高效。首先,每个SECS/GEM接口都充当消息代理。由于代理在设备上运行,未订阅的数据不会在网络上发布。如果主机软件要接收警报、收集事件或跟踪数据消息,必须先订阅。由于每个对警报、收集事件和跟踪数据的订阅都是单独管理的,因此设备可以实现单个SECS/GEM接口,该接口发布所有工厂应用程序请求的所有警报、收集事件和跟踪数据,而不会因为不必要的数据浪费网络带宽。此外,当主机订阅跟踪数据时,它可以指定数据收集速率,这使得SECS/GEM比以硬编码速率发布数据的协议更有效和有用。

另外,所有SECS/GEM消息总是以高效率的二进制格式传输。这比ASCII格式的协议使用更少的带宽。尽管使用二进制格式,SECS/GEM消息也很容易和标准的XML符号进行互转。

SECS/GEM获得业界的大力支持

多年来,SECS/GEM一直是半导体行业工厂/设备通信和控制系统的支柱。这意味着今天所有的半导体制造完全依赖于SECS/GEM通信。自90年代末以来,300mm半导体工厂一直在以SECS/GEM通信为基础的全自动化运行——像台积电、三星、美光、英特尔、东芝等大公司在每个工厂7*24小时的使用SECS/GEM。平板显示器、高亮度LED和光伏等其他行业也正式开始使用SECS/GEM,因为它们认识到SECS/GEM特性可以应用于任何制造设备,以支持关键任务的应用。

SECS/GEM是自描述的

虽然该标准要求GEM文档随设备一起提供,但是SECS/GEM仍支持多种方法让主机软件自动适应设备的SECS/GEM接口。主机软件可以通过一些消息请求可用报警、状态变量以及设备常量的列表,对于较新的SECS/GEM实现,主机软件还可以请求可用采集事件和数据变量的列表。这些消息使得SECS/GEM接口即插即用。此外,设备制造商还可以提供一个标准化的提供SECS/GEM接口及其特性的完整描述的XML文件。

总结

对于工厂和设备制造商来说,使用SECS/GEM技术有许多好处,以上只是其中的一部分。SECS/GEM是当今可用的成熟技术。

第二章 GEM 收集事件

在开始我们的SECS/GEM系列之前,让我们先来解释GEM标准的一个关键特性,即Collection Events。我们将从解释它们如何工作开始,然后进一步说明为什么它们在从制造设备收集数据方面如此有效。

什么是收集事件?

“收集事件”名称中的两个单词是描述性的。

如“事件”一词所示,收集事件是通知。它的目的是在设备上发生感兴趣的事情时通知主机。“主机”是指连接到设备GEM接口的工厂客户端软件。例如,收集事件可以在物料到达时报告,耗材不足时报告,出现硬件问题时报告,摄像机对物料进行检查时报告,物料准备取出时报告,燃烧室达到目标真空压力时报告,加工完成时报告等等。设备可以使用收集事件特性来报告任何感兴趣的事件。创建GEM接口的人准确地定义了主机可以使用哪些收集事件;因此,不同设备类型的可用收集事件集是不同的。

收集事件功能也能够将数据与收集事件消息一起发布。这是一种非常有效的数据收集形式,在消息可用时异步提供信息。例如,当物料到达时报告的收集事件也可以报告到达物料的条码和位置。GEM接口中有三种类型的数据; 关于收集事件的数据(称为数据变量)、关于状态的信息(称为状态变量)和设备设置信息(称为设备常量)。无论谁创建GEM接口,都将准确地定义每个收集事件将提供哪些信息。因此,不同设备类型的收集事件的可用信息集是不同的。只有在主机设置报表时,设备才会将可用数据发送到主机。

综上所述,收集事件不仅可以告诉主机发生了什么事情,还可以提供关于发生了什么事情以及设备状态的更详细的信息。

一个小的类比


打个比方,把工厂想象成老板,把他们购买的设备想象成员工。有许多不同的管理风格,就像有不同类型的工厂和运行工厂的风格一样。你不想被迫按照别人的工厂的方式经营自己的工厂。你想按你自己的方式去做。

此外,每个员工都是独特的,需要独特的关注度。每个员工都在做独特的事情。一般来说,所有的管理者都想知道员工的基本情况以及他们的员工在做什么。他们想要知道员工什么时候开始一个项目,什么时候完成一个项目。有些员工即使在极少的监督和汇报下也非常高效。一些员工需要大量的监督和报告。GEM允许工厂以独特的方式处理每台设备。具体来说,GEM收集事件为设备提供了一种报告其正在做什么的方式。

主机必须为报告建立规则,并适当地调整规则。例如,有时经理并不关心员工什么时候去洗手间。对于某些员工,经理可能想知道。在GEM接口中,主机可以选择哪些通知会发生,哪些不会发生。

有时经理只需要知道员工什么时候来,什么时候走,什么时候休息,什么时候下班。有时经理需要更多的细节,比如你完成了什么项目,花了多长时间,项目的关键结果。类似同样的,GEM允许主机不仅仅跟踪事件发生的时间,还要提供关于活动的详细信息。GEM报告非常有效地满足了这一需求。

为什么需要这个功能?

简单地说,收集事件允许您实时跟踪设备在做什么。如果一家工厂想要进行某种程度的智能制造,或者只是想提高生产率,那么首先需要的是能够跟踪设备在做什么。收集事件提供了这一点。您可以跟踪设备利用率、材料移动、处理里程碑、活动周期计数,以便进行预测性维护、消耗品使用,以及与发布的收集事件相关的任何其他内容。这些信息的应用是无止境的。

有时,收集事件还用于实现以下场景:设备在继续或获得继续的许可之前需要来自主机的信息。当设备准备好接受主机指令或权限时,可以通过收集事件通知主机。

收集事件通知如何工作?

设备的GEM接口可以发布许多不同的收集事件。通常情况下,主机不希望一次得到所有这些信息的通知,也不需要这样做。收集事件以两种方式使用发布/订阅设计模式。

基本的发布/订阅通知

主机订阅特定的收集事件,以便在事件发生时接收通知。订阅允许主机启用或禁用GEM接口中可用的每个收集事件的报告。设备在会该收集事件发生时发布它们。

事件报告发布/订阅数据收集

默认情况下,收集事件消息不包含任何数据。订阅还允许主机决定在每个启用的收集事件的消息中包含哪些数据。主机定义报表并将报表链接到收集事件;从而订阅数据。每个收集事件可以有不同的报告。还可以多个收集事件共享一个报表。报表里可以包含与收集事件关联的任何数据变量、任何状态变量和任何设备常量。设备会在发布收集事件是将请求的数据一并带出。

识别

设备发布的每个收集事件都有一个惟一的ID号进行标识。主机软件在启用或禁用收集事件时使用ID号。设备在发送收集事件消息时使用ID号。每个可用的数据变量、状态变量和设备常量都有一个惟一的ID号。当主机定义报表时,它也需要为报表分配惟一的ID号。

代理

代理构建在设备的GEM接口中,负责处理所有收集事件发布/订阅。它是设备系统的一部分。主机(客户端)和GEM接口之间的通信使用SECS/GEM通信进行标准化。GEM接口与设备的其余硬件和软件(设备收集事件和数据的源)之间的通信可以是任何适当的技术,只要GEM接口功能正常并且性能足够好就可以了。

这意味着消息只会在主机订阅之后才从设备发送到主机。将代理作为设备和GEM接口的一部分可以使GEM接口变得非常有效,并且比使用外部代理的协议使用更少的带宽,因为在外部代理中,所有消息和数据都必须一直发送到代理。



持久性

收集事件订阅持久化在GEM接口中。因此,如果主机断开并重新连接,或者设备重新启动,GEM接口仍旧记得所有订阅的设置。

GEM使用哪些消息?

下面是与收集事件相关的每个主要消息的摘要。注意,“S”表示“流”,“F”表示“函数”。流和函数号一起惟一地标识一个消息。

消息编号方向描述
S2F37主机->设备启用或禁用一组收集事件的报告。 空列表将启用或禁用所有收集事件的报告。在描述GEM接口时,启用所有收集事件报告非常有用。在启用所需收集事件的报告之前,禁用所有收集事件是非常有用的。
S2F33主机->设备定义一个或多个报告。 空列表将删除所有报告以及指向收集事件的报告链接。当试图重置订阅或首次连接到GEM接口并覆盖默认订阅时,删除所有报告功能非常有用。
S2F35主机->设备将一个或多个报告链接到一组收集事件。 如果报表已链接到一个收集事件,则必须移除链接,然后在一条消息中链接所有收集事件。空列表将从收集事件中删除报表链接。
S1F23主机->设备请求获取可用收集事件列表以及每个收集事件的可用数据列表。
S6F11设备->主机收集事件消息。 如果没有链接任何报告,则消息将只包含收集事件的ID。如果一个或多个报告链接到集合事件,则消息中将包含每个链接报告的报告数据。

关乎收集事件的常见问题

收集事件需要多少带宽?

这取决于几个因素。

  1. 主机启用的收集事件的数量。

  2. 连接到收集事件的数据报告的大小。

  3. 设备触发启用的收集事件的频率。这取决于收集事件的含义。

收集事件触发的速度有多快?

GEM标准使用标准通信硬件,并不限制收集事件的频率。换句话说,通过改进硬件,可以加快收集事件的速度。

GEM支持两种协议:SECS-I和HSMS。SECS-I基于RS-232串行通信,因此目前很少使用。这样的实现无法非常快地触发收集事件。

HSMS基于网络通信。由于串行通信很慢,所以目前大多数GEM实现都使用HSMS。GEM可以非常有效地使用TCP/IP。收集事件的可能频率取决于网络硬件的速度、设备计算机性能和主机性能。与大多数协议一样,使用消息通常比生成消息需要更多的计算机资源。

生成收集事件的速度还取决于链接到收集事件的数据报告。例如,如果数据报告很大,比如10mb,这将影响性能。

为什么我无法收到收集事件消息?

主机无法收到收集事件消息的可能原因有几个。

  1. 主机和设备必须使用成功的S1F13/S1F14交换建立GEM通信。

  2. GEM控制状态必须在线。它不能处于主机-脱机或设备-脱机状态。

  3. GEM Spooling 必须是非活跃的。在Spooling已经活跃时时禁用该功能并不会使Spooling变为活跃。如果不需要Spooling消息,则需要使用消息S6F23清除Spooling内容。如果需要假脱机消息,那么使用S6F23逐个获取它们,直到Spooling机状态变为非活跃状态。

  4. 必须启用收集事件。使用S1F3检查“EventsEnabled”状态变量以确认收集事件已启用。使用消息S2F37启用收集事件。

  5. 需要发生收集事件活动。例如,如果材料没有实际到达,那么当材料到达时将永远不会发生收集事件报告。如果活动发生且满足上述条件,则设备GEM接口存在缺陷。

如果设备的GEM接口没有发布我需要的收集事件,该怎么办?

请设备供应商添加所需的收集事件。设备供应商很难准确预测工厂需要的所有收集事件。设备供应商需要升级其在工厂中的GEM接口软件。

当链接到收集事件时,数据报告的大小是多少?

GEM允许单个数据变量值或状态变量的值为任意数据类型的数组或结构体,包括浮点数、字符串或整数。单个数组的大小限制为16.777215 MB,消息的总大小限制为4.294967295 GB。

第三章 数据轮询

GEM是一种工业标准,它定义了工艺设备和工厂主机软件之间, 为了达到监视和控制目的所建立通信的标准方法。通过连接GEM设备,工厂可以立即体验到运营效益。工厂主机可以通过多种方式收集数据。之前的一篇博客文章讨论了使用收集事件报告收集数据,其中数据基于设备状态的变化被推送到主机。除了事件报告之外,工厂主机通常还需要轮询设备的当前数据值。数据值可以由主机直接请求,也可以在跟踪报告中定期采样。这就是所谓的数据轮询,也是今天讨论的主题。

数据的类型

GEM接口中有三种类型的数据:

数据变量(DV) —— 设备事件发生时可以收集的数据项。此数据只保证在事件上下文中有效。例如,GEM接口可能提供一个名为PPChanged的事件(当配方发生更改时触发)。该接口还可以提供一个名为changed recipe的数据变量。此数据变量(DV)的值仅在PPChanged事件的上下文中有效。在其他的时间轮询该值可能会有无效或意外的数据。

状态变量(SV) —— 包含设备信息的数据项。该数据保证在任何时候都是有效的。例如,该设备可能在流程模块中有一个温度传感器。GEM接口可以提供模块温度状态变量。主机可以在任何时候请求这个状态变量(SV)的值,并期望这个值是准确的。

设备常数(EC) —— 包含设备设置的数据项。设备常数决定设备的行为。例如,GEM接口可能有一个名为MaxSimultaneousTraces的设备常量(EC),该常量指定可以同时从主机请求的最大跟踪数。设备常数的值总是保证是有效的和最新的。

数据属性

上面列出的三种数据类型都有一些类似的有助于定义数据的属性。设备供应商需要在GEM手册中提供这些属性,以便工厂主机能够与数据进行交互。一些重要的数据属性有:

ID —— 在该GEM接口中唯一的数字ID。这些ID可以按数据类型分为SVID(状态变量ID), DVID(数据变量ID)和ECID(设备常量ID)。

名称 —— 数据项的可读名称。
格式 —— 数据项的数据类型。

数据格式可以是简单类型(数字、ASCII、布尔值),也可以是复杂类型(数组、列表、结构)。例如,数字类型可以是I1、I2、I4、I8(不同字节长度的带符号整数类型)、U1、U2、U4、U8(无符号整数类型)和F4或F8(浮点类型)。列表和数组类型会在数据项中包含多个值。例如,图像数据通常采用字节数组作为数据格式。

结构类型包含特定类型的数据。例如,一个变量可以表示一个Slot map,其中包含Carrier信息,Slot列表和晶圆的存放信息。

值 —— 数据项的实际值。数据值采用精确、高效、自描述的二进制格式,从而主机知道如何解释数据。数据格式允许更有效地收集更多的数据。

收集事件(CE)和警报也有ID和名称。这些内容将在其他博客文章中讨论,但是了解本文的提到的这些属性非常重要,因为这些属性也可以被Host所查询。

数据轮询

如前所述,工厂主机经常通过它所定义的跟踪报告和事件报告定期获得数据。GEM还为工厂主机提供了一种可以根据他的需要去轮询设备数据的方法。

状态变量

主机可以在任何时候通过发送一条包含状态变量ID(SVID)列表的消息来查询这些状态变量(SV)的当前值。如果该列表的长度为1,则只返回单个状态变量(SV)的值。如果列表的长度为零,则返回GEM接口中定义的所有状态变量(SV)的值。这些值会包含在在设备回复的S1F4消息中。

主机还可以通过向设备发送S1F11消息向设备请求状态变量(SV)名称列表。上面提到的列表规则也适用于此消息。返回消息将包含每个状态变量(SV)的条目,包括状态变量ID(SVID)、名称和单位。

设备常量

状态变量(SV)的工作方式类似,主机还可以通过发送一条S2F13消息来查询GEM接口中定义的设备常量的值。这些值会包含在在设备回复的S2F14消息中。

与状态变量(SV)类似,可以使用S2F29消息查询设备常量的名称列表。

数据变量

由于数据变量只在集合事件的上下文中有效,因此没有轮询数据变量值的方法。数据变量的值只能在收集事件报告中上报给主机。

其他

除了上面讨论的数据轮询方法外,还可以通过轮询从设备获取以下信息:

收集事件(CE) —— 主机可以查询GEM接口上可用的收集事件,以及与每个CE关联的DVs。这些是使用S1F23消息请求的。
警报 —— 主机可以通过发送一个列有所期望的警报ID列表的S5F5消息来查询设备上有哪些警报是可用的。返回的消息将列出与ALID关联的警报ID和警报文本。每个GEM接口都需要有两个状态变量。AlarmEnabled包含设备上所有启用警报的ID列表。AlarmsSet包含设备商所有当前处于设置状态的警报ID。由于这些值是状态变量,所以主机可以在任何时候查询它们的值。
MDLN和SOFTREV —— 对S1F1(你在吗?)消息的回复将包含设备模型类型(MDLN)和软件修订版本(SOFTREV)。
DateTime ——主机可以使用S2F17消息请求设备的日期和时间,也可以使用S2F31消息同步设备的时间。GEM要求设备维护一个包含当前时间的时钟状态变量。允许主机查询和同步时间使得可以对系统上几乎同时发生的事件进行排序。

跟踪数据收集

跟踪数据收集提供了一种定期采样数据的方法。这种基于时间的数据收集方法对于跟踪一段时间内的数据变化趋势或重复的应用,或者监视连续的数据非常有用。在创建跟踪的定义时,主机需要提供以下内容:

采样周期 —— 样本之间的时间。以百分之一秒为最小单位,因此可以使用跟踪非常快速地收集数据。能够支持10赫兹的采样间隔数据的设备很常见。
数据组大小 —— 跟踪报告中包含的样本数量。
状态变量 —— 跟踪报告中包含的状态变量列表。
总样本数 —— 在跟踪的整个生命周期内要采集的样本数量。
跟踪请求ID —— 跟踪请求的标识符(GEM只允许整数类型的跟踪ID)。

主机使用S2F23消息定义跟踪请求。跟踪报告使用S6F1消息从设备发送到主机。

跟踪报告样本

假设一个设备正在处理一个晶圆片,这个过程需要5分钟。重要的是要保持卡盘温度在一定的可接受范围内,并确保腔室压力保持在指定的水平以下。每隔15秒采样一次这些状态变量值就足够了,但是我们可以创建数据组,实现每分钟只接收一次报告。主机可以发送一条包含如下跟踪配置的S2F23消息:

跟踪ID : 100 (ID必须是整数)
采样周期 : 00001500(每15秒采样一次)
总样本:75个(每15秒采样5英寸)
数据组大小: 4
SVID列表 :
300(包含卡盘温度信息的状态变量的ID)
301(包含室压信息的状态变量ID)

一分钟后,第一个跟踪报告将来自设备发出的的S6F1消息。这条消息将包含以下信息:

100(跟踪ID)
4(最后一个样本号)
2018-01-22T14:20:34.8(日期格式取决于时间格式设备常数)
状态变量列表:(长度为8 : 2 个状态变量,数据组大小为4)

219.96(第一次采样卡盘温度)
0.0112(第一次采样压力)
219.97(第二次采样卡盘温度)
0.0122(第二次采样压力)
219.97(第三次采样卡盘温度)
0.0120(第三次采样压力)
219.96(第四次采样卡盘温度)
0.0119(第四次采样压力)

再过一分钟,跟踪报告可能如下所示:

100(跟踪ID)
8(最后一个样本号)
2018-01-22T14:21:34.8(日期时间显示比第一个跟踪晚一分钟)
状态变量列表 : (长度为8: 2 个状态,数据组大小为4)

219.96(第五次采样卡盘温度)
0.0112(第五次采样压力)
219.97(第六次采样卡盘温度)
0.0122(第六次采样压力)
220.01(第七次采样卡盘温度)
0.0120(第七次采样压力)
220.00(第八次采样卡盘温度)
0.0119(第八次采样压力)

以一分钟为间隔,后续还将收到三份报告。主机可以检查报告中返回的值,并在值超出预期范围时做出相应的处理。

结论

如果主机想在特定的时间点检查一个值,它可以使用S1F3轮询状态数据。如果希望在给定的时间内连续收集数据,则可以设置跟踪报告。

使用本博客中概述的数据采样方法,将允许主机应用程序在需要时轮询所需的数据。GEM提供了从设备请求数据的灵活性,允许主机在给定的时间点查询值,或者使用跟踪定期采样样。

第四章 GEM 工厂应用支持

工厂如何处理这些数据?

与本系列中其他涉及SEMI E30 GEM(通用设备模型)标准的特定特性和功能的文章不同,本篇博客阐述了许多使用设备上收集到的数据的工厂应用程序。

此外,由于我们经常听到这样的问题:“工厂实际如何使用哪些希望我们提供的各种类型的设备信息?”这篇文章将总结出支持这些应用程序所需的具体数据。这个列表并没有涵盖所有的内容,但是应该可以让您了解由GEM数据收集支持其目标的工厂受益者。下图说明了关键性能指标(kpi)、负责优化它们的受益者、用于实现此目的的应用程序以及这些应用程序所需的数据之间的关系。



分享这类信息最有效的方式是表格形式。在一组相关的应用程序中(例如,调度、预防性维护),应用程序通常按照复杂性递增的顺序列出,这也是工厂应用程序开发人员实现的可能顺序。

工厂应用需要的设备数据
OEE(设备总体效率)足够对所有时间段设备状态进行分类的转换事件和状态代码。
Intra-equipment material flow设备内部物料流物料跟踪时间,物料位置状态指示和状态变化时间。
Process execution tracking工艺执行跟踪所有工艺模块的开始/结束事件;所有支持多步骤配方的工艺模块里每个步骤的指示和步骤变化时间
WTW (wait time waste) analysis等待时间损耗分析设备内部物料流和工艺执行跟踪应用程序所需要的事件以及划分所有时间段物料状态所需要的上下文数据的组合。参见SEMI E168产品时间管理标准作进一步解释)
Time-based PM (PreventiveMaintenance)基于时间的预防性保养现场可替换部件级别的使用次数。
Usage-based PM基于用法的预防性保养每个可替换部件使用参数及累计值,比如状态内市场,执行次数,流体流量,耗材流量,功耗等等。
Condition-based PM基于条件的预防性保养每个可替换部件的有意义的健康指标
FDC (错误检测分类)特定错误模块所要求的设备/工艺参数及上下文信息(这里想要做到完整比较困难,因为多数FDC系统都有)
Automated equipment interdiction自动设备停止远程停止命令(例如当FDC应用感知到以及存在或者将要发生的错误时)
Equipment configurationMonitoring设备配置监控重要设备常量的矢量,包括期望值和可接受范围。如果值是依赖于设置的期望值和可接受范围会有多套。这套系统是为了能够捕获因为操作人员手工修改参数导致的人为错误。
Component fingerprinting组件识别设备关键机制的性能参数,包括传感器/驱动器级别的命令/响应信号。
Static job scheduling静态作业调度每个产品/配方组合的计划和执行时间,以及当前计划的信息。
Real-time job dispatching实时作业分配预估当前作业完成时间;预估设备上所有排队等待的物料的完成时间。
Factory cycle time optimization工厂周期优化物料缓存内容,作业队列信息
Operator notification操作员通知非自动化/半自动化环境下一些操作员频繁操作的通知代码,比如加载/卸载物料, 选择/确定配方,一旦设备卡住提供的一些手动协助等等。
Real-time dashboard实时仪表盘设备/部件生产状态指标
Equipment failure analysis设备失败分析有意义的报警/错误代码,或者最近的历史记录/数据
Run-to-run process control批次工艺控制配方可调整参数识别以及远程更新这些参数的命令。

在某种程度上,在上面的表中描述的应用程序数据可以跨设备类型进行标准化,这样的话就有可能创建通用的工厂的应用程序, 只需要一个从供方定义的GEM ID (收集事件id、状态/数据变量,常量,设备等)到通用应用方的对应列表。但是这是另一个关于GEM上下文中“即插即用”概念的主题。

我们希望这个解释能够帮助您理解设备信息对于使用它的工厂是多么有价值,从而明白在您将来设计的GEM接口中提供一组丰富的事件、变量和其他详细信息是多么重要。

第五章 报警

以前的文章已经讨论了允许通过GEM接口收集数据的功能,以便在最近的文章中描述的工厂应用程序能够分析这些数据。在这篇文章中,我们将回到对SEMI E30 GEM(通用设备模型)标准的特定特性和功能的讨论,特别是对设备错误情况的管理。

在一个完美的世界里,一切都按计划进行,但在现实中,事情总是会出错。成功的秘诀是能够知道什么时候出了问题,然后做出适当的反应。

就像家庭报警系统一样,半导体晶圆厂也想知道什么时候发生了不好的事情。他们想防止正在加工的材料被报废。报警管理使设备能够在出错时通知主机,并提供出错信息。GEM标准将报警管理定义为设备能够通知主机对设备上发生的报警情况和对报警情况进行管理的能力。

在GEM中,一个报警可以是指设备上的任何可能危及正在加工的人员、设备或材料的异常情况。例如,如果技术人员打开一个盖板来替换组件,设备应该发出报警,通知主机在当前状态下操作设备是不安全的。另一个例子可能是,如果一个设备需要高温进行加工,但是传感器检测到低温条件,它应该触发报警,因为在这些条件下运行过程可能会损坏正在加工的材料。当出现报警情况时,设备制造商也有责任制止设备上的不安全活动。设备制造商最清楚设备上需要什么样的特殊报警,以确保人员、设备和材料的安全。

通常,在报警条件发生时,能得到更多的设备中的状况信息是有用的。向传达额外的信息是非常有价值的,但是没法通过正常的报警报告发送/确认消息。为了提供一种途径获取额外信息,GEM要求为设备上每种可能的报警条件定义两个收集事件——一个事件用于设置报警,另一个事件用于清除报警。这些收集事件使得GEM事件数据收集机制可以被用于在报警更改状态时向主机发送额外的相关信息。

除了提供报警状态更改的时间外,设备上的报警管理必须允许主机获取所有报警id和相关报警文本的列表。主机还必须能够启用/禁用设备上的单个报警的报告,并查询设备以获得当前启用报告的报警列表。

报警的状态图不是很令人兴奋,但是它满足了一个至关重要的需求。下图为报警状态图:



GEM报警只有两种状态: 每个报警要么处于设置状态,要么处于清除状态。它简单但是有效。

报警管理不是复杂的事情,但通过有效地使用报警管理,晶圆厂可以仔细的监控其工艺设备的健康状况,并将其对生产良率的负面影响降到最低。

第六章 配方管理

在几篇SECS/GEM系列博客文章(包括收集事件、数据轮询和警报)之后,我们现在讨论GEM特性的特性和优点,称为配方管理。我们将介绍配方的定义, 配方管理是什么意思,,以及为什么需要这个功能!

什么是配方?

配方是一组描述设备应如何处理其材料的指令。配方内容由设备供应商定义。

什么是配方管理?

配方管理允许工厂主机在设备之间传输配方。它还要求设备在设备上的配方发生变化时通知工厂主机。

为什么需要这个特性?

几乎所有的半导体工厂都需要这个特性来确保配方的完整性并支持可追溯性。主机将设备上已批准的配方上传并保存下来供以后使用,以确保菜谱不会被更改。为了可跟踪性,配方通常与工艺数据一起保存。

配方管理是如何工作的?

配方通过SECS消息在主机和设备之间传递。有几组SECS消息被用于这个功能。E30 GEM列举了格式化、非格式化和大型配方的消息集。这里将不讨论大型配方的消息集。

当操作人员在设备上修改配方时,设备还需要通知主机。生成的PPChange收集事件还需要伴有两个数据变量:载有被更改的配方ID的变量PPChangeName以及载有包含更改类型(创建、删除和编辑)的变量PPChangeStatus。当配方被传到设备上时,设备应对内容进行验证。如果配方无效,则应该生成一个PPVerificationFailed的收集事件,伴有包含验证失败信息的PPError数据变量,以将问题通知主机。如果验证失败,该配方将不被使用。

识别

每个配方都由一个名为process program ID或PPID的ASCII名称标识。工厂主机和设备GEM接口在配方操作中使用该名称。

持续性

在GEM 接口中, 配方是持续性的。如果主机断开并重新连接,或者设备重新启动,GEM接口仍将记得配方。此外,大多数工厂主机会将配方保存在工厂端。

使用哪些消息?

下面是与集合事件相关的每个主要消息的总结。注意,“S”表示“流”,“F”表示“函数”。S和F一起唯一地标识消息。

所有配方

消息编号方向描述
S7F17主机 -> 设备从设备上删除一个配方。空列表将删除设备上的所有配方。
S7F19主机 -> 设备请求设备上所有可用的配方列表

非格式化配方

消息编号方向描述
S7F1主机 <- 设备设备要求上传一个配方
S7F3主机 <- 设备设备上传一个配方到主机
S7F5主机 <- 设备设备请求从主机获取一个配方
S7F1主机 -> 设备主机要求下传一个配方到设备
S7F3主机 -> 设备主机下传一个配方到设备
S7F5主机 -> 设备主机请求从设备获取一个配方

格式化配方

消息编号方向描述
S7F1主机 <- 设备设备要求上传一个配方
S7F23主机 <- 设备设备上传一个配方到主机
S7F25主机 <- 设备设备请求从主机获取一个配方
S7F1主机 -> 设备主机要求下传一个配方到设备
S7F3主机 -> 设备主机下传一个配方到设备
S7F5主机 -> 设备主机请求从设备获取一个配方
S7F29主机 <- 设备设备请求发送配方验证结果
S7F27主机 <- 设备设备发送配方验证结果

有关配方管理的常见问题

可以传送一个多大的配方?

对于未格式化的菜谱消息,菜谱要么是单个ASCII字符串,要么是二进制数组值。单个数组值被限制为16.777215 MB。

格式化的配方消息,将配方分解为一个项目列表。单个数组值被限制为16.777215 MB。消息的总大小被限制为4.294967295 GB。

第七章 文档

正如SECS/GEM系列的特性和优点的第一篇文章所指出的,SECS/GEM标准定义了一个可以在任何设备上使用的标准化接口。GEM接口通过状态变量、数据变量、收集事件、警报、数据格式、错误代码、SECS-II消息和其他可选的GEM功能公开设备的功能。GEM标准要求每台设备都附带文档; 确保工厂拥有使用设备GEM接口所需的信息。该文档通常称为GEM手册。

GEM手册可以以多种形式分发。目前,大多数GEM手册都是以Word、Excel或PDF文档的形式提供的。GEM手册中的大量信息被用于做出购买决策、开发主机软件和测试设备。对于一个功能完整的GEM接口,GEM手册必须包含以下主题: 状态模型、场景、数据收集、警报管理、远程控制、设备常量、流程配方管理、物料移动、终端服务、错误消息、时钟、假脱机、控制、支持的SECS-II消息、GEM遵从性声明和数据项格式。为了使这篇文章保持一个合理的长度,我们将只讨论一些必要的主题。

GEM遵从性声明

遵从性声明是要审阅的第一个主题之一。它是一种快速、简单地了解设备接口特性的方法。制造商需要标记在设备上实现了哪些GEM功能,以及这些功能是否以符合GEM标准的方式实现。



状态模型

状态模型是GEM的基本功能,因此可以在每个设备上实现。该功能定义了设备的通信、控制和假脱机行为。设备必须提供处理状态模型。但是,定义出一个适用于所有设备的工艺状态的状态机是不可能的。本标准规定了所有设备应具有相同的加工行为。每个状态模型都必须用状态模型图、转换表和每个状态的文本描述来记录归档。关于每个状态模型的一致和详细信息使工厂能够在得到GEM手册之后立即开始编写主机应用程序。



警报、收集事件、设备常数、数据变量和状态变量

设备收集数据的一大部分都是警报、收集事件和变量。所以需要将他们包含在GEM手册中并不意外。设备上的每个警报都应该在GEM手册中有其ID、名称、描述和相关的Set/Clear事件。每个集合事件的文档应该包括ID、名称、描述和关联变量列表。所有变量的文档将包括ID、名称、描述和数据类型。还应该在适当的时候提供变量默认值或值范围的信息。虽然不是必需的,但通常将所有这些信息显示在五个容易找到的表中。对于以下每一类都有一个表: 警报、收集事件、设备常量、数据变量和状态变量。参见下面的示例。




远程控制

一旦工厂能够从设备中收集数据,他们就开始研究如何控制设备。远程控制是GEM功能,它允许主机应用程序请求设备执行一个操作。所有远程命令都应该包含在手册中,包括它的名称、描述以及可能随命令一起发送的每个命令参数的详细信息。命令参数的详细信息应该包括名称、格式和描述。下面显示了一个示例。



SMN 和SEDD

GEM手册很少采用易于在软件中解析的格式。这通常会导致重复代码,并进行一些小的更改,以便与其他设备通信。SEMI E172 SECS设备数据字典(SEDD)和E173 SECS消息表示法(SMN)是两个标准,它们可以极大地提高主机应用程序的灵活性和可重用性。SEDD是一个易于在软件中分布和解析的xml文件。SEDD可以被认为是一个现代化的GEM手册,因为它包含了与GEM手册中相同的信息。例如,SEDD文件包含关于每个变量、收集事件、警报和受支持的SECS-II消息的详细信息。SEDD文件使用SMN表示数据项、变量和SECS-II消息。SMN也是XML格式,它是第一个定义表达数据项和SECS-II消息的表示法的标准。这意味着单个应用程序可以读取SEDD文件,进行一个简短的配置过程,然后立即开始使用设备的GEM接口。这些特性允许单个应用程序用于多个设备,而不是为每个设备创建略有不同的变体。

总结

GEM手册是GEM标准所要求的与每个设备一起提供的重要文档。当遇到关于设备的GEM接口的问题时,GEM手册应该是寻找答案的首选。SEMI也在通过更新现有标准和创建新标准,继续改进GEM手册的内容和灵活性。

第八章 设备终端服务

在本系列的几篇文章讨论了数据收集、事件、警报、配方管理和文档之后,本文重点讨论GEM标准的Twitter - 设备终端服务(Terminal Services)。我们将研究什么是终端服务(Terminal Services),为什么需要它们,以及它们的工作机制。

什么是终端服务(Terminal Services)?

设备终端服务允许工厂操作员从设备工作站与主机交换信息。主机可以在设备的显示设备上显示信息。它还允许设备的操作员向主机发送信息。设备必须能够显示主机传递给它的信息,供操作员注意。

为什么需要这个特性?

使用终端服务的例子如下:

  1. FDC软件通知主机进程模块有偏移需要处理。

  2. 主机打开信号灯塔上的操作员通知灯。通知灯亮起时需要说明灯亮起的原因。

  3. 主机发送一条终端消息说FDC软件检测到偏移,操作员应该解决这个问题。

  4. 与信号塔灯一起,终端服务通知在工具上处于活动状态。

  5. 操作员看到并确认消息。

  6. 可选:有不同的恢复方法,但是操作员可以在问题解决后向主机发送终端消息。



终端服务功能如何工作?

当主机向设备发送终端消息时,需要设备向操作员显示该消息。该显示器必须能够显示最多160个字符(甚至比使用Twitter在一条tweet中发送的字符还要多),但也有可能会显示更多字符。设备的显示设备必须具有一种机制,用于通知操作员一条消息已被接收,但尚未被操作员确认。该消息将继续显示,直到操作员确认该消息为止。设备必须提供一个方法,例如一个按钮,让操作员确认消息。操作员的消息识别将导致一个收集事件,该事件会通知主机操作员已接收到信息。设备应用程序不需要翻译从主机发送的数据。它只是为操作员提供信息显示。

如果主机发送的新消息是在操作员确认前一条消息之前发送的,则新消息将覆盖前一条消息。主机可以通过发送零长度的消息清除未确认消息(包括指示符)。零长度的消息不会被认为是待确定的消息。设备还必须允许操作员将输入的信息从操作员的设备控制台发送到主机。

使用哪些消息?

消息ID方向描述
S10F3H->E主机向设备发送文本信息,以供显示。
S10F1H<-E操作员向主机发送文本信息。
S10F5H->E(可选)主机发送多块显示的消息。如果不支持多区块,设备将回复S10F7表示不允许多区块消息。
S6F11H<-E设备向主机发送收集事件通知终端服务消息已被确认。

第九章 用户界面

我记得作为一个新的童子军,我们计划去我家附近的一个原始山区远足。我们从地图上学到的第一件事就是在哪里可以找到图例。地图图例包含了阅读地图所需的重要信息,比如指出哪个方向是北。既然我们知道在哪里可以找到图例,我们就可以确定地图的方向,这样在我们计划徒步旅行时就能找到它了。

在典型的半导体或电子装配工厂中,大多数设备都有一个用户界面,其中包含许多关于设备的信息。大多数设备还包含许多用于控制或操作设备的屏幕。利用GEM,一个工厂主机系统可以对设备进行控制,以及采集工艺过程中生成的重要数据。

就像地图一样,在一个设备的用户界面上有很多可用的信息。有时很难找到那些主机系统需要的,用来控制和与设备通信的重要信息。GEM标准提供了关于如何将这些关键项目在用户界面上呈现和控制的指南。例如,如果主机向设备操作员发送关于他们需要执行的任务的信息,GEM终端消息指南规定,这些信息必须保留在设备的用户界面上,直到操作员确认他们已经阅读了它。

SEMI E30标准定义了制造设备通信和控制通用模型(GEM)的规范。除了提供制造自动化所需的通用设备行为和通信功能集的定义外,该标准还提供了关于哪些项必须出现在设备用户界面上以及如何表示这些项的要求。用户界面要求由标准定义的通信状态、终端服务新消息指示、终端服务消息确认按钮、通信状态默认值和通信状态选择说明。

这似乎是一件小事,但就像知道地图上找到图例就能理解地图上的线条和符号的一样,GEM为如何理解设备界面上所展示的那些对与工厂主机之间通信尤为重要的信息提供了帮助。

第十章 GEM消息假脱机功能

假脱机消息的目的

即使是最健壮的计算机网络也会经历通信失败。不管原因是什么,一个小故障都可能导致大量的关键任务数据丢失。GEM通过提供消息假脱机功能来调停数据的丢失

假脱机的定义

假脱机是这样一种功能,设备可以在通信失败时对发送给主机的消息进行排队缓存,然后在通信恢复时发送这些消息。

假脱机的好处

自动化工厂是数据驱动的。对数据进行提取和分析,以做出影响工程和管理团队如何应对的决策,以确保产品产量高而废品率低。

这些数据的缺失可能导致错误的判断甚至猜测。假脱机是一种备份系统,它可以确保存储和恢复这些数据,从而降低丢失有价值数据的风险。

GEM功能需求

然而,假脱机并不是GEM的要求,如果要实现这个附加的功能,就必须正确地实现。下面是实现兼容假脱机接口的一些要求。

设备必须通过设备常量“EnableSpooling”给主机提供启用和禁用假脱机的能力。该EC由设备发布,主机可以选择所需的状态。

在实现假脱机时,它必须对所有相关的主要消息都有效,并且能够使用S2、F43/F44交互进行访问。Stream 1消息要被排除在外。主机试图为Stream 1 “设置spool”的指令将被拒绝。

非易失性存储(NVS)

设备负责分配足够的非易失性存储器,可以存储该设备的至少一个工艺周期需要假脱机的所有消息。NVS还将包含所有与假脱机相关的状态变量。NVS用于此数据,因此,如果发生断电,数据将被持久化。

假脱机实现中主机端的责任

消息假脱机功能也需要主机的参与,以在通信中断后成功恢复。在主机程序能够正确的处理整个状态机中可能发生的所有情况之前,最好将假脱机设置为禁用状态。禁用假脱机比管理不当的假脱机要好。

一旦重新建立了通信,主机必须管理对假脱机消息的请求。主机还可以在必要时从设备中清除假脱机消息文件。

结论

虽然假脱机不是GEM的基本需求,但是如果实现了它,就必须正确地执行。当启用假脱机时,主机和设备软件都有责任确保遵守GEM。GEM假脱机避免了潜在的有价值数据丢失,并为设备和主机软件提供了一个易于遵守的标准。

第十一章 协议层

协议层的用途

协议层封装数据,并在工厂主机和设备GEM接口之间可靠地传输数据。

协议层定义

协议层实现了通过工厂主机和设备GEM接口之间的连线发送消息所用到的传输技术和数据打包算法,。

SEMI E5标准,半导体设备通信标准II 消息内容(SECSII), 定义了用作数据的SECS消息,以及如何将它们打包到二进制缓冲区中进行传输。

SEMI E37和E37.1标准高速SECS消息服务(HSMS)定义了一种协议,用于在TCP/IP连接上交换SECS消息。这是SECS/GEM中使用最多的传输技术。



HSMS协议栈

SEMI E4标准,即半导体设备通信协议标准I 消息传输 (SECS- I),定义了在RS-232上交换SECS消息的机制。这通常用于较旧的设备或设备内部的某些硬件,例如EFEM控制器。

本文的其余部分将重点讨论通过HSMS传递的SECS消息。

协议层的好处

GEM中的协议层维护连接并检测连接丢失,因此任何一方都可以采取适当的操作,比如激活假脱机。
协议层定义握手机制,以确保在需要时传递消息。

协议层连接是工厂主机和设备之间的点对点连接。它是一个没有广播功能的专用连接。这使得预测网络负载变得更加容易。

数据密度

SECS/GEM传输数据开销小、密度高。这意味着给定数据集的网络带宽使用更少。
为了便于说明,我们来看一个典型的事件报告示例,并将SECS/GEM消息传递与某种程度上等价的XML和JSON消息进行比较。

以一个典型的GEM接口为例,该接口为id使用无符号的4字节整数,以及一个包含8字节浮点数和4字节整数的事件报告。下表以SECS/GEM E5格式以及等效的JSON和XML格式显示了此消息的一个示例。



和XML数字可以根据键/元素名称进行一些更改,上面的只是许多可能的表示之一。



下图显示了示例消息的数据密度比较。实际数据大小为2个4字节整数+ 2个8字节浮点数+ 1个4字节事件id + 1个4字节报告id = 32字节的实际数据。开销是通过从消息的总字节数中减去实际数据大小来计算的。



对于示例消息SECS的数据密度,数据密度百分比如下图所示。数据密度百分比由(实际数据)/开销*100计算。



现在,如果我们将示例消息更改为包含100个8字节浮点数,则数据密度百分比图将改变为如下图。注意JSON和XML相对相同,但是SECS/GEM数据密度增加到78%。数据密度百分比由(实际数据)/开销*100计算。



SECS/GEM编码的开销非常小。消息的开销是描述消息的头部的10个字节,加上消息体大小的1到4个字节。对于SECS消息中的任何4字节整数或浮点数,都将通过网络发送6个字节,4个字节表示整数值+ 1个字节表示类型+ 1个字节表示数据的长度(以字节为单位)。同样,对于任何8字节的整数或浮点数,都将发送10字节。对于字符串值,长度将是字符数加上2到4字节。在SECS消息中出现列表(上面可读示例中的L)时,将向消息添加2到4个字节。

在SECS/GEM数据中,数字数组尤为高效。数组的开销是类型为1字节,数组长度为1到4字节,加上数据的本身大小。例如:一个由10个4字节整数组成的数组将占用42字节,即数据密度为95%!

在JSON示例中,一个4字节的整数需要16个字节加上表示该整数所需的字符数,因此需要17到28个字节。浮点数的开销相同,但可能需要更多字符来表示值。

在XML中,开销基于XML元素名称的大小。使用上面示例中的元素名,对于任何4字节整数,跨连接的字节数将是9 +表示该整数所需的字符数,所以是10到21字节。浮点数取决于用来表示值的字符串格式。

总而言之,通过查看每项数据所需字节大小,SECS/GEM非常密集。以4字节整数为例,其中SECS/GEM是6个字节,JSON示例是17到28个字节,XML示例是10到21个字节,随着参数数量的增加,您会发现开销确实很重要。300mm半导体设备预计每秒每个工艺模块向主机传输1000个参数。对于2个模块的设备,这将导致仅用于数据的字节数如下: SECS/GEM 12K字节, JSON 34K-56K, 如实例的XML 需要20K-42K。这些数字不考虑消息其余部分的大小,只考虑与参数值相关的实际部分。如果数据在大量消息中传输,而每个消息的值很少,那么网络负载就更糟了。在所有情况下,更少、更大的消息总是更好。

根据使用的传输协议,XML和JSON也可能增加开销。例如,XML通常使用SOAP通过HTTP传输,这就为每个消息增加了额外的两层开销和更多的字节。SECS/GEM所显示的字节数是实际通过TCP/IP上的网络传输的。

无数据翻译

在SECS/GEM中传输数值数据时不需要转换。数字以其原始格式传输。例如:8字节浮点数以其8字节表示形式传输,没有经过任何转换、截断或舍入。

任何协议,如JSON或XML,都必须将这些8字节浮点数转换为文本表示形式。这需要计算用于编码和解码的资源,并且需要更多的字节。IEEE754要求17位十进制数字将8字节浮点数精确地表示为字符串。将符号、小数点、指数和指数符号的字符相加,得到21个字符。这是SECS/GEM通过网络发送的两倍多。

环路保证

HSMS定义了一种称为Link Test的环路保证机制。如果没有活动的消息交换,协议层将启动一个计时器。每次计时器过期时,都会交换一条协议消息以确保连接仍然是打开的。

安全

HSMS没有定义安全性。没有连接方的验证,连接不需要凭证或证书。数据未使用任何普通加密算法加密; 然而,数据在数据打包过程中是模糊的,通常是人类无法读懂的。由于工厂网络与外部世界隔离,安全通常不会被视为一个问题。

结论

使用HSMS的SECS/GEM协议层提供了在工厂主机和设备之间交换准确数据的非常有效的方法。

第十二章 消息日志

1977年,经典电影《第三类接触》上映。在电影的最后,有一段外星人和人类之间的戏剧性的 “对话”。其中一位科学家说:“我希望有人把这一切都记下来。”

他们真正想要的是消息日志!

就像软件日志对于应用程序的故障诊断很重要一样,记录工厂主机和生产设备之间的详细消息交互对于故障诊断也很重要。

例如,主机发送一个命令,设备根据消息进行操作,但是有些事情并没有如预期的那样工作。将发送给设备的消息和设备的回复的消息与来自设备的其他日志一起查看,非常有助于确定问题位于在哪儿。

用于显示/表示已记录消息的格式也非常重要。SECS消息格式的最新行业标准是SEMI - E173,即XML SECS- ii消息符号规范(SMN)。

举个例子:

<?xml version="1.0" encoding="utf-8"?>
<SECSMessageScenario xmlns="urn:semi-org:xsd.SMN">
<Comment time="2018-02-05T18:19:20.365Z">State Change NotConnected</Comment>
<Comment time="2018-02-05T18:19:20.400Z">State Change NotSelected</Comment>
<HSMSMessage time="2018-02-05T18:19:20.394Z" sType="Select.req" direction="H to E" txid="1">
<Header>FFFF0000000100000001</Header>
</HSMSMessage>
<HSMSMessage time="2018-02-05T18:19:20.417Z" sType="Select.rsp" direction="E to H" txid="1">
<Header>FFFF0000000200000001</Header>
<Description>Communication Established</Description>
</HSMSMessage>

这是一个S5,F5的例子:

<SECSMessage s="5" f="5" direction="H to E" replyBit="true" txid="7" time="2018-02-05T18:19:20.507Z">
<SECSData>
<UI4 />
</SECSData>
</SECSMessage>
<SECSMessage s="5" f="6" direction="E to H" replyBit="false" txid="7" time="2018-02-05T18:19:20.507Z">
<SECSData>
<LST>
<LST>
<BIN>0</BIN>
<UI4>1</UI4>
<ASC>Alarm 1 Text</ASC>
</LST>
</LST>
</SECSData>
</SECSMessage>

SMN格式非常适合:

  • 以清晰的方式捕获HSMS头信息

  • 以精确的二进制格式记录消息

  • 使用软件读取日志

  • 创建主机或设备模拟器,因为很容易从软件应用程序读取日志并回放

  • 从SMN日志中提取数据

日志可以被设备、主机甚至像Cimetrix的CIMSniffer实用程序这样的“网络嗅探器”捕获。

Cimetrix的Logviewer实用程序也支持SMN日志:



有了这些标准和工具,就没有理由像《亲密接触》中的科学家那样,希望这些信息被记录下来。去打开日志!

Cimetrix的CIMConnect、HostConnect和SECSConnect都提供SMN格式的消息日志记录。

第十三章GEM 控制状态

什么是GEM 控制状态?

GEM板的控制状态是E30 GEM的基本要求之一。它定义了主机和设备之间的协作级别,并指定了操作员如何在不同级别的主机控制状态下进行交互。

在半导体工厂中,主机或操作员可以控制设备的加工。双方同时控制设备会带来问题。所以当一方控制设备时,另一方所能进行的操作将受到限制。例如,如果操作员暂停了工艺处理,则不应允许主机发送恢复处理或启动新作业的命令。GEM控制状态就是为了防止此类问题的发生而被建立的。



控制状态如何工作?

控制状态提供三个基本级别的控制。每个级别都描述了主机和设备端可以执行哪些操作。

远程

  • 主机可以最大限度地控制设备。

  • 设备可能会限制当地操作员控制设备的能力,但这不是标准的要求。主机必须能够处理操作员在设备上调用的意外命令。

  • 主机使用GEM远程命令来调用设备上的命令。
    本地

本地

  • 操作者可以尽可能地控制设备。

  • 主机可以完全访问信息。主机可以使用其他GEM特性(如收集事件、跟踪和状态数据收集)收集数据。

  • 限制主机如何影响设备操作:

    • 禁止启动处理(例如START)或导致物理移动的远程命令。在处理期间,还禁止影响处理的远程命令(停止、中止、暂停、恢复)。

    • 允许使用其他不启动处理、不会导致物理移动或影响处理的远程命令。

    • 在处理期间,禁止主机修改任何影响该进程的设备常数。

    • 不影响当前运行进程的设备常数可以更改。

    • 当不处理时,所有的设备常数都是可变的。

离线

  • 操作者对设备有完全的控制。

  • 主机对设备操作没有控制,信息收集能力非常有限。

  • 设备将从主机接收的唯一消息是:

    • 用于建立GEM通信的消息(S1F13/F14)。

    • 请求激活联机控制状态(S1F17),但仅限于当前活动状态为主机脱机时(控件状态模型上的转换#11)。

    • 在尝试在线时收到的S1F2“Are You There Response”

  • 设备可能发送给主机的唯一主要消息是:

    • 用于建立通信的消息(S1F13)。

    • S9Fx消息,但仅响应设备离线时通常响应的消息(即S1F13和S1F17)。

    • 当进入“Attempt ON-LINE”子状态时,S1F1 “Are You There Request” 会被发送到主机。此消息用于从主机获得进入在线状态的权限(转换#5)。

  • 离线时没有不会对消息做假脱机处理。

控制状态模型的设计使设备操作员对状态机的控制多于对主机的控制。这将保护操作员不被主机发起的意料之外的状态更改影响。

  • 设备操作员可以通过操作界面选择哪个在线子状态处于活动状态。主机端不能选择哪个在线子状态是活动的。

  • 设备端可以将控制状态模型放入设备离线状态(转换#6)。当处于这种状态时,主机无法请求设备进入在线状态。

  • 主机端可以将控制状态放入主机离线状态(转换#10),但是设备端可以拒绝此请求。当处于主机离线状态时,设备端总是可以通过先切换到设备离线状态(转换#12),然后尝试在线(转换#3),来进入在线状态。

操作界面需求

设备必须提供一种显示当前控制状态的方法,以便操作者知道谁控制着设备。

设备必须提供一个瞬时开关来启动到设备离线状态的转换,另一个开关来尝试从设备离线状态切换到在线状态。这可以是前面板上的一个硬件开关,但通常在软件中使用按钮控件实现。

设备必须提供一个离散的双位置开关,操作员可以使用该开关指示所需的在线子状态(本地或远程)。这可以是前面板上的一个硬件开关,但通常在软件中使用按钮控件实现。如果在软件中实现,该设置必须保存在非易失性存储中。

有条件的状态转换

在控件状态模型中,转换#1、#2、#4和#7是有条件状态转换。设备应用程序必须提供一种方法来配置要转换到哪种状态。设备常量可以用于该配置。

条件转换#1和#2决定了启动期间控制状态模型的初始状态。控制这些转换的配置可以设置为以下状态之一:

  • 在线

  • 设备离线

  • 尝试在线

  • 主机离线

条件转换#4用于决定设备尝试在线失败后要转换到哪个状态。该配置可以设置为以下状态之一:

  • 设备离线

  • 主机离线

条件转换#7用于确定当控件状态变为在线时,应该进入哪个在线子状态(本地或远程)。该配置可以设置为以下在线子状态之一:

  • 当地的

  • 远程

控制状态功能用会用到的消息

消息编号方向描述
S1F1主机 <- 设备当设备尝试在线时(处于“尝试在现在”状态),此消息被发送到主机。主机通过发送S1F2应答消息授予权限。主机可以通过发送S1F0或允许消息事务超时来拒绝权限。
S1F15主机 -> 设备主机发送此消息请求从“主机离线”到在线的转换(转换#11)。
S1F17主机 -> 设备主机发送此消息请求从在线到“主机离线”的转换(转换#10)。

第十四章 总结

我们希望您能喜欢这一系列与GEM标准相关的主题。

显然,GEM标准是最适合工厂自动化各个方面的行业标准,具有广泛的特性,支持智能制造和工业4.0。GEM标准可以在最简单和最复杂的设备上实现。GEM将继续在未来许多行业的制造业中发挥关键作用。


23种设计模式全解析

一、设计模式的分类

总体来说设计模式分为三大类:

创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式,共七种:适配器模式装饰器模式代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式,共十一种:策略模式模板方法模式观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

其实还有两类:并发型模式和线程池模式。用一个图片来整体描述一下:

 

二、设计模式的六大原则

总原则:开闭原则(Open Close  Principle

开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,而是要扩展原有代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类等,后面的具体设计中我们会提到这点。

1、单一职责原则

不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的职责,如若不然,就应该把类拆分。

2、里氏替换原则(Liskov  Substitution Principle

里氏代换原则(Liskov Substitution Principle  LSP)面向对象设计的基本原则之一。里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

里氏替换原则中,子类对父类的方法尽量不要重写和重载。因为父类代表了定义好的结构,通过这个规范的接口与外界交互,子类不应该随便破坏它。

3、依赖倒转原则(Dependence  Inversion Principle

这个是开闭原则的基础,具体内容:面向接口编程,依赖于抽象而不依赖于具体。写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。

4、接口隔离原则(Interface  Segregation Principle

这个原则的意思是:每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。

5、迪米特法则(最少知道原则)(Demeter  Principle

就是说:一个类对自己依赖的类知道的越少越好。也就是说无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。

最少知道原则的另一个表达方式是:只与直接的朋友通信。类之间只要有耦合关系,就叫朋友关系。耦合分为依赖、关联、聚合、组合等。我们称出现为成员变量、方法参数、方法返回值中的类为直接朋友。局部变量、临时变量则不是直接的朋友。我们要求陌生的类不要作为局部变量出现在类中。

6、合成复用原则(Composite Reuse  Principle

合成复用原则是尽量首先使用合成/聚合的方式,而不是使用继承。


三、23种设计模式

我们详细介绍Java23种设计模式的概念,应用场景等情况,并结合他们的特点及设计模式的原则进行分析。

A、创建模式

0、简单工厂模式

首先,简单工厂模式不属于23种设计模式,简单工厂一般分为:普通简单工厂、多方法简单工厂、静态方法简单工厂。

01、普通简单工厂

建立一个工厂类,对实现了同一接口的一些类进行实例的创建。首先看下关系图:

举例如下:(我们举一个发送邮件和短信的例子)

首先,创建二者的共同接口:

public interface Sender {
    public void Send();
}

其次,创建实现类:

public class MailSender implements Sender {  
    @Override
    public void Send() {
        System.out.println("This is mail sender!");
    }
}
public class SmsSender implements Sender {
    @Override
    public void Send() {
        System.out.println("This is sms sender!");
    }
}

最后,建工厂类:

public class SendFactory {
    public Sender produce(String type) {
        if ("mail".equals(type)) {
            return new MailSender();
        } else if ("sms".equals(type)) {
            return new SmsSender();
        } else {
            System.out.println("请输入正确的类型!");
            return null;
        }
    }
}

我们来测试下:

public class FactoryTest {
    public static void main(String[] args) {
        SendFactory factory = new SendFactory();
        Sender sender = factory.produce("sms");
        sender.Send();
    }
}

输出:This is sms sender!


02、多个方法

是对普通工厂方法模式的改进,在普通工厂方法模式中,如果传递的字符串出错,则不能正确创建对象,而多个工厂方法模式是提供多个工厂方法,分别创建对象。关系图:

将上面的代码做下修改,改动下SendFactory类就行,如下:

public class SendFactory {
    public Sender produceMail(){
         return new MailSender();
     }
     
     public Sender produceSms(){
         return new SmsSender();
     }
 }

测试类如下:

public class FactoryTest {
    public static void main(String[] args) {
        SendFactory factory = new SendFactory();
        Sender sender = factory.produceMail();
        sender.Send();
    }
}

输出:This is mail sender!


03、多个静态方法

将上面的多个工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。

public class SendFactory {
    public static Sender produceMail(){
        return new MailSender();
    }

    public static Sender produceSms(){
        return new SmsSender();
    }
}
public class FactoryTest {
    public static void main(String[] args) {
        Sender sender = SendFactory.produceMail();
        sender.Send();
    }
}

输出:This is mail sender!


总体来说,工厂模式适合:凡是出现了大量的产品需要创建,并且具有共同的接口时,可以通过工厂方法模式进行创建。在以上的三种模式中,第一种如果传入的字符串有误,不能正确创建对象,第三种相对于第二种,不需要实例化工厂类,所以,大多数情况下,我们会选用第三种——静态工厂方法模式。

 

1、工厂方法模式(Factory Method

简单工厂模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑,有一定的问题,如何解决?就用到工厂方法模式,创建一个工厂接口和创建多个工厂实现类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。

请看例子:

interface Sender {
    void Send();
}


两个实现类:

public class MailSender implements Sender {
    @Override
    public void Send() {
        System.out.println("This is mail sender");
    }
}
public class SmsSender implements Sender {
    @Override
    public void Send() {
        System.out.println("This is sms sender!");
    }
}

两个工厂类:

public class SendMailFactory implements Provider {
    @Override
    public Sender produce(){
        return new MailSender();
    }
}
public class SendSmsFactory implements Provider{
    @Override
    public Sender produce() {
        return new SmsSender();
    }
}

再提供一个接口:

public interface Provider {
    public Sender produce();
}

测试类:

public class FactoryMethodTest {
    public static void main(String[] args) {
        Provider provider = new SendMailFactory();
        Sender sender = provider.produce();
        sender.Send();
    }
}

输出:This is mail sender!


其实这个模式的好处就是,如果你现在想增加一个功能:发即时信息,则只需做一个实现类,实现Sender接口,同时做一个工厂类,实现Provider接口,就OK了,无需去改动现成的代码。这样做,拓展性较好!

 

2、抽象工厂模式

抽象工厂是所有形态的工厂模式中最为抽象和最具一般性的一种形态。抽象工厂是指当有多个抽象角色时使用的一种工厂模式。抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体情况下,创建多个产品族中的产品对象。

由于业务发展,当前的本地消息发送器无法胜任所有的业务,需要增加了网络发送器,而网络发送器也需要支持Mail和Sms的信息发送。

interface Sender {
    void Send();
}
public class LocalMailSender implements Sender {
    @Override
    public void Send() {
        System.out.println("This is local mail sender");
    }
}
public class LocalSmsSender implements Sender {
    @Override
    public void Send() {
        System.out.println("This is local sms sender!");
    }
}
public class WebMailSender implements Sender {
    @Override
    public void Send() {
        System.out.println("This is web mail sender");
    }
}
public class WebSmsSender implements Sender {
    @Override
    public void Send() {
        System.out.println("This is web sms sender!");
    }
}
public interface Provider {
    public Sender createMailSender();
    public Sender createSmsSender();
}
public class LocalProvider implements Provider {
    public Sender createMailSender() {
        return new LocalMailSender();
    }
    public Sender createSmsSender() {
        return new LocalSmsSender();
    }
}
public class WebProvider implements Provider {
    public Sender createMailSender() {
        return new WebMailSender();
    }
    public Sender createSmsSender() {
        return new WebSmsSender();
    }
}

测试代码:

public class AbstractFactoryTest {
    public static void main(String[] args) {
        Provider localProvider = new LocalProvider();
        Sender mailSender = localProvider.createMailSender();
        mailSender.Send();

        Provider webProvider = new WebProvider();
        Sender smsSender = webProvider.createSmsSender();
        smsSender.Send();
    }
}

输出结果:
This is local mail sender!
This is web sms sender!


工厂方法模式和抽象工厂模式不好分清楚,他们的区别如下:

工厂方法模式:

   一个抽象产品类,可以派生出多个具体产品类。    
   
一个抽象工厂类,可以派生出多个具体工厂类。    
   
每个具体工厂类只能创建一个具体产品类的实例。

抽象工厂模式:
   
多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。    
   
一个抽象工厂类,可以派生出多个具体工厂类。    
   
每个具体工厂类可以创建多个具体产品类的实例,也就是创建的是一个产品线下的多个产品。    
     
区别:
   
工厂方法模式只有一个抽象产品类,而抽象工厂模式有多个。    
   
工厂方法模式的具体工厂类只能创建一个具体产品类的实例,而抽象工厂模式可以创建多个。


工厂方法创建 "一种" 产品,他的着重点在于"怎么创建",也就是说如果你开发,你的大量代码很可能围绕着这种产品的构造,初始化这些细节上面。也因为如此,类似的产品之间有很多可以复用的特征,所以会和模版方法相随。 

抽象工厂需要创建一些列产品,着重点在于"创建哪些"产品上,也就是说,如果你开发,你的主要任务是划分不同差异的产品线,并且尽量保持每条产品线接口一致,从而可以从同一个抽象工厂继承。


对于java来说,你能见到的大部分抽象工厂模式都是这样的:

   它的里面是一堆工厂方法,每个工厂方法返回某种类型的对象。

比如说工厂可以生产鼠标和键盘。那么抽象工厂的实现类(它的某个具体子类)的对象都可以生产鼠标和键盘,但可能工厂A生产的是罗技的键盘和鼠标,工厂B是微软的。

这样AB就是工厂,对应于抽象工厂;
  每个工厂生产的鼠标和键盘就是产品,对应于工厂方法;

用了工厂方法模式,你替换生成键盘的工厂方法,就可以把键盘从罗技换到微软。但是用了抽象工厂模式,你只要换家工厂,就可以同时替换鼠标和键盘一套。如果你要的产品有几十个,当然用抽象工厂模式一次替换全部最方便(这个工厂会替你用相应的工厂方法)

所以说抽象工厂就像工厂,而工厂方法则像是工厂的一种产品生产线

 

 3、单例模式(Singleton

单例对象(Singleton)是一种常用的设计模式。在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:

1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。

2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。

3、有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。

首先我们写一个简单的单例类:

public class Singleton {  
  
    /* 持有私有静态实例,防止被引用,此处赋值为null,目的是实现延迟加载 */  
    private static Singleton instance = null;  
  
    /* 私有构造方法,防止被实例化 */  
    private Singleton() {  
    }  
  
    /* 静态工程方法,创建实例 */  
    public static Singleton getInstance() {  
        if (instance == null) {  
            instance = new Singleton();  
        }  
        return instance;  
    }  
  
    /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */  
    public Object readResolve() {  
        return instance;  
    }  
}

这个类可以满足基本要求,但是,像这样毫无线程安全保护的类,如果我们把它放入多线程的环境下,肯定就会出现问题了,如何解决?我们首先会想到对getInstance方法加synchronized关键字,如下:

public static synchronized Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
}

但是,synchronized关键字锁住的是这个对象,这样的用法,在性能上会有所下降,因为每次调用getInstance(),都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了,所以,这个地方需要改进。我们改成下面这个:

public static Singleton getInstance() {  
    if (instance == null) {  
        synchronized (instance) {  
            if (instance == null) {  
                instance = new Singleton();  
            }  
        }  
    }  
    return instance;  
}

似乎解决了之前提到的问题,将synchronized关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在instancenull,并创建对象的时候才需要加锁,性能有一定的提升。但是,这样的情况,还是有可能有问题的,看下面的情况:在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例。这样就可能出错了,我们以AB两个线程为例:

a>AB线程同时进入了第一个if判断

b>A首先进入synchronized块,由于instancenull,所以它执行instance = new Singleton();

c>由于JVM内部的优化机制,JVM先画出了一些分配给Singleton实例的空白内存,并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。

d>B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。

e>此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。

所以程序还是有可能发生错误,其实程序在运行过程是很复杂的,从这点我们就可以看出,尤其是在写多线程环境下的程序更有难度,有挑战性。我们对该程序做进一步优化:

private static class SingletonFactory{           
    private static Singleton instance = new Singleton();           
}           
public static Singleton getInstance(){           
    return SingletonFactory.instance;           
}

实际情况是,单例模式使用内部类来维护单例的实现,JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。这样我们暂时总结一个完美的单例模式:

public class Singleton {  
  
    /* 私有构造方法,防止被实例化 */  
    private Singleton() {  
    }  
  
    /* 此处使用一个内部类来维护单例 */  
    private static class SingletonFactory {  
        private static Singleton instance = new Singleton();  
    }  
  
    /* 获取实例 */  
    public static Singleton getInstance() {  
        return SingletonFactory.instance;  
    }  
  
    /* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */  
    public Object readResolve() {  
        return getInstance();  
    }  
}

其实说它完美,也不一定,如果在构造函数中抛出异常,实例将永远得不到创建,也会出错。所以说,十分完美的东西是没有的,我们只能根据实际情况,选择最适合自己应用场景的实现方法。也有人这样实现:因为我们只需要在创建类的时候进行同步,所以只要将创建和getInstance()分开,单独为创建加synchronized关键字,也是可以的:

public class SingletonTest {  
  
    private static SingletonTest instance = null;  
  
    private SingletonTest() {  
    }  
  
    private static synchronized void syncInit() {  
        if (instance == null) {  
            instance = new SingletonTest();  
        }  
    }  
  
    public static SingletonTest getInstance() {  
        if (instance == null) {  
            syncInit();  
        }  
        return instance;  
    }  
}

考虑性能的话,整个程序只需创建一次实例,所以性能也不会有什么影响。

补充:采用"影子实例"的办法为单例对象的属性同步更新

public class SingletonTest {  
  
    private static SingletonTest instance = null;  
    private Vector properties = null;  
  
    public Vector getProperties() {  
        return properties;  
    }  
  
    private SingletonTest() {  
    }  
  
    private static synchronized void syncInit() {  
        if (instance == null) {  
            instance = new SingletonTest();  
        }  
    }  
  
    public static SingletonTest getInstance() {  
        if (instance == null) {  
            syncInit();  
        }  
        return instance;  
    }  
  
    public void updateProperties() {  
        SingletonTest shadow = new SingletonTest();  
        properties = shadow.getProperties();  
    }  
}

通过单例模式的学习告诉我们:

1、单例模式理解起来简单,但是具体实现起来还是有一定的难度。

2synchronized关键字锁定的是对象,在用的时候,一定要在恰当的地方使用(注意需要使用锁的对象和过程,可能有的时候并不是整个对象及整个过程都需要锁)。

到这儿,单例模式基本已经讲完了,结尾处,笔者突然想到另一个问题,就是采用类的静态方法,实现单例模式的效果,也是可行的,此处二者有什么不同?

首先,静态类不能实现接口。(从类的角度说是可以的,但是那样就破坏了静态了。因为接口中不允许有static修饰的方法,所以即使实现了也是非静态的)

其次,单例可以被延迟初始化,静态类一般在第一次加载是初始化。之所以延迟加载,是因为有些类比较庞大,所以延迟加载有助于提升性能。

再次,单例类可以被继承,他的方法可以被覆写。但是静态类内部方法都是static,无法被覆写。

最后一点,单例类比较灵活,毕竟从实现上只是一个普通的Java类,只要满足单例的基本需求,你可以在里面随心所欲的实现一些其它功能,但是静态类不行。从上面这些概括中,基本可以看出二者的区别,但是,从另一方面讲,我们上面最后实现的那个单例模式,内部就是用一个静态类来实现的,所以,二者有很大的关联,只是我们考虑问题的层面不同罢了。两种思想的结合,才能造就出完美的解决方案,就像HashMap采用数组+链表来实现一样,其实生活中很多事情都是这样,单用不同的方法来处理问题,总是有优点也有缺点,最完美的方法是,结合各个方法的优点,才能最好的解决问题!

 

4、建造者模式(Builder

 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。


四个要素

  • 产品类:一般是一个较为复杂的对象,也就是说创建对象的过程比较复杂,一般会有比较多的代码量。在本类图中,产品类是一个具体的类,而非抽象类。实际编程中,产品类可以是由一个抽象类与它的不同实现组成,也可以是由多个抽象类与他们的实现组成。

  • 抽象建造者:引入抽象建造者的目的,是为了将建造的具体过程交与它的子类来实现。这样更容易扩展。一般至少会有两个抽象方法,一个用来建造产品,一个是用来返回产品。

  • 建造者:实现抽象类的所有未实现的方法,具体来说一般是两项任务:组建产品;返回组建好的产品。

  • 导演类:负责调用适当的建造者来组建产品,导演类一般不与产品类发生依赖关系,与导演类直接交互的是建造者类。一般来说,导演类被用来封装程序中易变的部分。

代码实现

class Product {
    private String name;
    private String type;
    public void showProduct(){
        System.out.println("名称:"+name);
        System.out.println("型号:"+type);
    }
    public void setName(String name) {
        this.name = name;
    }
    public void setType(String type) {
        this.type = type;
    }
}

abstract class Builder {
    public abstract void setPart(String arg1, String arg2);
    public abstract Product getProduct();
}
class ConcreteBuilder extends Builder {
    private Product product = new Product();

    public Product getProduct() {
        return product;
    }

    public void setPart(String arg1, String arg2) {
        product.setName(arg1);
        product.setType(arg2);
    }
}

public class Director {
    private Builder builder = new ConcreteBuilder();
    public Product getAProduct(){
        builder.setPart("宝马汽车","X7");
        return builder.getProduct();
    }
    public Product getBProduct(){
        builder.setPart("奥迪汽车","Q5");
        return builder.getProduct();
    }
}
public class Client {
    public static void main(String[] args){
        Director director = new Director();
        Product product1 = director.getAProduct();
        product1.showProduct();

        Product product2 = director.getBProduct();
        product2.showProduct();
    }
}

建造者模式的优点

首先,建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在导演类中对整体而言可以取得比较好的稳定性。

其次,建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。

建造者模式与工厂模式的区别

我们可以看到,建造者模式与工厂模式是极为相似的,总体上,建造者模式仅仅只比工厂模式多了一个"导演类"的角色。在建造者模式的类图中,假如把这个导演类看做是最终调用的客户端,那么图中剩余的部分就可以看作是一个简单的工厂模式了。

与工厂模式相比,建造者模式一般用来创建更为复杂的对象,因为对象的创建过程更为复杂,因此将对象的创建过程独立出来组成一个新的类——导演类。也就是说,工厂模式是将对象的全部创建过程封装在工厂类中,由工厂类向客户端提供最终的产品;而建造者模式中,建造者类一般只提供产品类中各个组件的建造,而将具体建造过程交付给导演类。由导演类负责将各个组件按照特定的规则组建为产品,然后将组建好的产品交付给客户端。

总结

建造者模式与工厂模式类似,他们都是建造者模式,适用的场景也很相似。一般来说,如果产品的建造很复杂,那么请用工厂模式;如果产品的建造更复杂,那么请用建造者模式。


5、原型模式(Prototype

原型模式虽然是创建型的模式,但是与工程模式没有关系,从名字即可看出,该模式的思想就是将一个对象作为原型,对其进行复制、克隆,产生一个和原对象类似的新对象。在Java中,复制对象是通过clone()实现的,先创建一个原型类:

public class Prototype implements Cloneable {  
  
    public Object clone() throws CloneNotSupportedException {  
        Prototype proto = (Prototype) super.clone();  
        return proto;  
    }  
}

很简单,一个原型类,只需要实现Cloneable接口,覆写clone方法,此处clone方法可以改成任意的名称,因为Cloneable接口是个空接口,你可以任意定义实现类的方法名,如cloneA或者cloneB,因为此处的重点是super.clone()这句话,super.clone()调用的是Objectclone()方法,而在Object类中,clone()native的,具体怎么实现,此处不再深究。在这儿,将结合对象的浅复制和深复制来说一下,首先需要了解对象深、浅复制的概念:

浅复制:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,指向的还是原对象所指向的。

深复制:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。简单来说,就是深复制进行了完全彻底的复制,而浅复制不彻底。

此处,写一个深浅复制的例子:

public class Prototype implements Cloneable, Serializable {  
  
    private static final long serialVersionUID = 1L;  
    private String string;  
  
    private SerializableObject obj;  
  
    /* 浅复制 */  
    public Object clone() throws CloneNotSupportedException {  
        Prototype proto = (Prototype) super.clone();  
        return proto;  
    }  
  
    /* 深复制 */  
    public Object deepClone() throws IOException, ClassNotFoundException {  
  
        /* 写入当前对象的二进制流 */  
        ByteArrayOutputStream bos = new ByteArrayOutputStream();  
        ObjectOutputStream oos = new ObjectOutputStream(bos);  
        oos.writeObject(this);  
  
        /* 读出二进制流产生的新对象 */  
        ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());  
        ObjectInputStream ois = new ObjectInputStream(bis);  
        return ois.readObject();  
    }  
  
    public String getString() {  
        return string;  
    }  
  
    public void setString(String string) {  
        this.string = string;  
    }  
  
    public SerializableObject getObj() {  
        return obj;  
    }  
  
    public void setObj(SerializableObject obj) {  
        this.obj = obj;  
    }  
  
}
class SerializableObject implements Serializable {  
    private static final long serialVersionUID = 1L;  
}

要实现深复制,需要采用流的形式读入当前对象的二进制输入,再写出二进制数据对应的对象。

 

 

B、结构模式(7种)

 

结构型模式共有7种模式类型:适配器模式、装饰模式、代理模式、外观模式、桥接模式、组合模式、享元模式。其中对象的适配器模式是各种模式的起源,看下图:

3d7ddf5a521a234d.jpg

  

6、适配器模式

 适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。主要分为三类:类的适配器模式、对象的适配器模式、接口的适配器模式。

01、类的适配器模式

核心思想就是:有一个Source类,拥有一个方法,待适配,目标接口是Targetable,通过Adapter类,将Source的功能扩展到Targetable里,看代码:

public class Source {  
    public void method1() {  
        System.out.println("this is original method!");  
    }  
}
public interface Targetable {  
    /* 与原类中的方法相同 */  
    public void method1();  
  
    /* 新类的方法 */  
    public void method2();  
}
public class Adapter extends Source implements Targetable {  
    @Override  
    public void method2() {  
        System.out.println("this is the targetable method!");  
    }  
}

Adapter类继承Source类,实现Targetable接口,下面是测试类:

public class AdapterTest {  
  
    public static void main(String[] args) {  
        Targetable target = new Adapter();  
        target.method1();  
        target.method2();  
    }  
}

输出:

this is original method!

this is the targetable method!

这样Targetable接口的实现类就具有了Source类的功能。

02、对象的适配器模式

基本思路和类的适配器模式相同,只是将Adapter类作修改,这次不继承Source类,而是持有Source类的实例,以达到解决兼容性的问题。看图:

 

只需要修改Adapter类的源码即可:

public class Wrapper implements Targetable {  
    private Source source;  
      
    public Wrapper(Source source){  
        super();  
        this.source = source;  
    }  
    @Override  
    public void method2() {  
        System.out.println("this is the targetable method!");  
    }  
  
    @Override  
    public void method1() {  
        source.method1();  
    }  
}

测试类:

public class AdapterTest {  
    public static void main(String[] args) {  
        Source source = new Source();  
        Targetable target = new Wrapper(source);  
        target.method1();  
        target.method2();  
    }  
}

输出与第一种一样,只是适配的方法不同而已。

03、接口的适配器模式

第三种适配器模式是接口的适配器模式,接口的适配器是这样的:有时我们写的一个接口中有多个抽象方法,当我们写该接口的实现类时,必须实现该接口的所有方法,这明显有时比较浪费,因为并不是所有的方法都是我们需要的,有时只需要某一些,此处为了解决这个问题,我们引入了接口的适配器模式,借助于一个抽象类,该抽象类实现了该接口,实现了所有的方法,而我们不和原始的接口打交道,只和该抽象类取得联系,所以我们写一个类,继承该抽象类,重写我们需要的方法就行。看一下类图:

这个很好理解,在实际开发中,我们也常会遇到这种接口中定义了太多的方法,以致于有时我们在一些实现类中并不是都需要。看代码:

public interface Sourceable {  
    public void method1();  
    public void method2();  
}

抽象类Wrapper2

public abstract class Wrapper2 implements Sourceable{  
    public void method1(){}  
    public void method2(){}  
}
public class SourceSub1 extends Wrapper2 {  
    public void method1(){  
        System.out.println("the sourceable interface's first Sub1!");  
    }  
}
public class SourceSub2 extends Wrapper2 {  
    public void method2(){  
        System.out.println("the sourceable interface's second Sub2!");  
    }  
}
public class WrapperTest {  
    public static void main(String[] args) {  
        Sourceable source1 = new SourceSub1();  
        Sourceable source2 = new SourceSub2();  
          
        source1.method1();  
        source1.method2();  
        source2.method1();  
        source2.method2();  
    }  
}

测试输出:

the sourceable interface's first Sub1!

the sourceable interface's second Sub2!

达到了我们的效果!

总结一下三种适配器模式的应用场景:

类的适配器模式:当希望将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可。

对象的适配器模式:当希望将一个对象转换成满足另一个新接口的对象时,可以创建一个Wrapper类,持有原类的一个实例,在Wrapper类的方法中,调用实例的方法就行。

接口的适配器模式:当不希望实现一个接口中所有的方法时,可以创建一个抽象类Wrapper,实现所有方法,我们写别的类的时候,继承抽象类即可。

 

7、装饰模式(Decorator

顾名思义,装饰模式就是给一个对象增加一些新的功能,而且是动态的,要求装饰对象和被装饰对象实现同一个接口,装饰对象持有被装饰对象的实例,关系图如下:

Source类是被装饰类,Decorator类是一个装饰类,可以为Source类动态的添加一些功能,代码如下:

public interface Sourceable {  
    public void method();  
}
public class Source implements Sourceable {
    @Override
    public void method() {
        System.out.println("the original method!");
    }
}
public class Decorator implements Sourceable {  
    private Sourceable source;  
      
    public Decorator(Sourceable source){  
        super();  
        this.source = source;  
    }  
    @Override  
    public void method() {  
        System.out.println("before decorator!");  
        source.method();  
        System.out.println("after decorator!");  
    }  
}

测试类:

public class DecoratorTest {
    public static void main(String[] args) {  
        Sourceable source = new Source();  
        Sourceable obj = new Decorator(source);  
        obj.method();  
    }  
}

输出:

before decorator!

the original method!

after decorator!

装饰器模式的应用场景:

1、需要扩展一个类的功能。

2、动态的为一个对象增加功能,而且还能动态撤销。(继承不能做到这一点,继承的功能是静态的,不能动态增删。)

缺点:产生过多相似的对象,不易排错!

 

8、代理模式(Proxy

代理模式就是多一个代理类出来,替原对象进行一些操作,比如我们在租房子的时候会去找中介,为什么呢?因为你对该地区房屋的信息掌握的不够全面,希望找一个更熟悉的人去帮你做,此处的代理就是这个意思。再如我们有的时候打官司,我们需要请律师,因为律师在法律方面有专长,可以替我们进行操作,表达我们的想法。先来看看关系图:

 

根据上文的阐述,代理模式就比较容易的理解了,我们看下代码:

public interface Sourceable {  
    public void method();  
}
public class Source implements Sourceable {  
    @Override  
    public void method() {  
        System.out.println("the original method!");  
    }  
}
public class Proxy implements Sourceable {  
    private Source source;  
    public Proxy(){  
        super();  
        this.source = new Source();  
    }  
    @Override  
    public void method() {  
        before();  
        source.method();  
        atfer();  
    }  
    private void atfer() {  
        System.out.println("after proxy!");  
    }  
    private void before() {  
        System.out.println("before proxy!");  
    }  
}

测试类:

public class ProxyTest {  
    public static void main(String[] args) {  
        Sourceable source = new Proxy();  
        source.method();  
    }  
}

输出:

before proxy!

the original method!

after proxy!

代理模式的应用场景:

如果已有的方法在使用的时候需要对原有的方法进行改进,此时有两种办法:

1、修改原有的方法来适应。这样违反了“对扩展开放,对修改关闭”的原则。

2、就是采用一个代理类调用原有的方法,且对产生的结果进行控制。这种方法就是代理模式。

使用代理模式,可以将功能划分的更加清晰,有助于后期维护!

 

9、外观模式(Facade

外观模式是为了解决类与类之间的依赖关系的,像spring一样,可以将类和类之间的关系配置到配置文件中,而外观模式就是将他们的关系放在一个Facade类中,降低了类与类之间的耦合度,该模式中没有涉及到接口,看下类图:(我们以一个计算机的启动过程为例)

我们先看下实现类:

public class CPU {  
    public void startup(){  
        System.out.println("cpu startup!");  
    }  
      
    public void shutdown(){  
        System.out.println("cpu shutdown!");  
    }  
}
public class Memory {  
    public void startup(){  
        System.out.println("memory startup!");  
    }  
      
    public void shutdown(){  
        System.out.println("memory shutdown!");  
    }  
}
public class Disk {  
    public void startup(){  
        System.out.println("disk startup!");  
    }  
      
    public void shutdown(){  
        System.out.println("disk shutdown!");  
    }  
}
public class Computer {  
    private CPU cpu;  
    private Memory memory;  
    private Disk disk;  
      
    public Computer(){  
        cpu = new CPU();  
        memory = new Memory();  
        disk = new Disk();  
    }  
      
    public void startup(){  
        System.out.println("start the computer!");  
        cpu.startup();  
        memory.startup();  
        disk.startup();  
        System.out.println("start computer finished!");  
    }  
      
    public void shutdown(){  
        System.out.println("begin to close the computer!");  
        cpu.shutdown();  
        memory.shutdown();  
        disk.shutdown();  
        System.out.println("computer closed!");  
    }  
}

User类如下:

public class User {  
    public static void main(String[] args) {  
        Computer computer = new Computer();  
        computer.startup();  
        computer.shutdown();  
    }  
}

输出:

start the computer!

cpu startup!

memory startup!

disk startup!

start computer finished!

begin to close the computer!

cpu shutdown!

memory shutdown!

disk shutdown!

computer closed!

如果我们没有Computer类,那么,CPUMemoryDisk他们之间将会相互持有实例,产生关系,这样会造成严重的依赖,修改一个类,可能会带来其他类的修改,这不是我们想要看到的,有了Computer类,他们之间的关系被放在了Computer类里,这样就起到了解耦的作用,这,就是外观模式!

 

10、桥接模式(Bridge

桥接模式就是把事物和其具体实现分开,使他们可以各自独立的变化。桥接的用意是:将抽象化与实现化解耦,使得二者可以独立变化,像我们常用的JDBCDriverManager一样,JDBC进行连接数据库的时候,在各个数据库之间进行切换,基本不需要动太多的代码,甚至丝毫不用动,原因就是JDBC提供统一接口,每个数据库提供各自的实现,用一个叫做数据库驱动的程序来桥接就行了。我们来看看关系图:

实现代码:

先定义接口:

public interface Sourceable {  
    public void method();  
}

分别定义两个实现类:

public class SourceSub1 implements Sourceable {  
    @Override  
    public void method() {  
        System.out.println("this is the first sub!");  
    }  
}
public class SourceSub2 implements Sourceable {  
    @Override  
    public void method() {  
        System.out.println("this is the second sub!");  
    }  
}

定义一个桥,持有Sourceable的一个实例:

public abstract class Bridge {  
    private Sourceable source;  
  
    public void method(){  
        source.method();  
    }  
      
    public Sourceable getSource() {  
        return source;  
    }  
  
    public void setSource(Sourceable source) {  
        this.source = source;  
    }  
}
public class MyBridge extends Bridge {  
    public void method(){  
        getSource().method();  
    }  
}

测试类:

public class BridgeTest {  
    public static void main(String[] args) {  
        Bridge bridge = new MyBridge();  
          
        /*调用第一个对象*/  
        Sourceable source1 = new SourceSub1();  
        bridge.setSource(source1);  
        bridge.method();  
          
        /*调用第二个对象*/  
        Sourceable source2 = new SourceSub2();  
        bridge.setSource(source2);  
        bridge.method();  
    }  
}

output

this is the first sub!

this is the second sub!

这样,就通过对Bridge类的调用,实现了对接口Sourceable的实现类SourceSub1SourceSub2的调用。接下来我再画个图,大家就应该明白了,因为这个图是我们JDBC连接的原理,有数据库学习基础的,一结合就都懂了。

 

11、组合模式(Composite

组合模式有时又叫部分-整体模式在处理类似树形结构的问题时比较方便,看看关系图:

直接来看代码:

public class TreeNode {  
    private String name;  
    private TreeNode parent;  
    private Vector children = new Vector();  
      
    public TreeNode(String name){  
        this.name = name;  
    }  
  
    public String getName() {  
        return name;  
    }  
  
    public void setName(String name) {  
        this.name = name;  
    }  
  
    public TreeNode getParent() {  
        return parent;  
    }  
  
    public void setParent(TreeNode parent) {  
        this.parent = parent;  
    }  
      
    //添加孩子节点  
    public void add(TreeNode node){  
        children.add(node);  
    }  
      
    //删除孩子节点  
    public void remove(TreeNode node){  
        children.remove(node);  
    }  
      
    //取得孩子节点  
    public Enumeration getChildren(){  
        return children.elements();  
    }  
}
public class Tree {  
    TreeNode root = null;  
  
    public Tree(String name) {  
        root = new TreeNode(name);  
    }  
  
    public static void main(String[] args) {  
        Tree tree = new Tree("A");  
        TreeNode nodeB = new TreeNode("B");  
        TreeNode nodeC = new TreeNode("C");  
          
        nodeB.add(nodeC);  
        tree.root.add(nodeB);  
        System.out.println("build the tree finished!");  
    }  
}

使用场景:将多个对象组合在一起进行操作,常用于表示树形结构中,例如二叉树,树等。

  

12、享元模式(Flyweight

享元模式的主要目的是实现对象的共享,即共享池,当系统中对象多的时候可以减少内存的开销,通常与工厂模式一起使用。

FlyWeightFactory负责创建和管理享元单元,当一个客户端请求时,工厂需要检查当前对象池中是否有符合条件的对象,如果有,就返回已经存在的对象,如果没有,则创建一个新对象,FlyWeight是超类。一提到共享池,我们很容易联想到Java里面的JDBC连接池,想想每个连接的特点,我们不难总结出:适用于作共享的一些个对象,他们有一些共有的属性,就拿数据库连接池来说,urldriverClassNameusernamepassworddbname,这些属性对于每个连接来说都是一样的,所以就适合用享元模式来处理,建一个工厂类,将上述类似属性作为内部数据,其它的作为外部数据,在方法调用时,当做参数传进来,这样就节省了空间,减少了实例的数量。

看个例子:

看下数据库连接池的代码:

public class ConnectionPool {  
    private Vector pool;  
      
    /*公有属性*/  
    private String url = "jdbc:mysql://localhost:3306/test";  
    private String username = "root";  
    private String password = "root";  
    private String driverClassName = "com.mysql.jdbc.Driver";  
  
    private int poolSize = 100;  
    private static ConnectionPool instance = null;  
    Connection conn = null;  
  
    /*构造方法,做一些初始化工作*/  
    private ConnectionPool() {  
        pool = new Vector(poolSize);  
  
        for (int i = 0; i < poolSize; i++) {  
            try {  
                Class.forName(driverClassName);  
                conn = DriverManager.getConnection(url, username, password);  
                pool.add(conn);  
            } catch (ClassNotFoundException e) {  
                e.printStackTrace();  
            } catch (SQLException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
  
    /* 返回连接到连接池 */  
    public synchronized void release(Connection conn) {  
        pool.add(conn);  
    }  
  
    /* 返回连接池中的一个数据库连接 */  
    public synchronized Connection getConnection() {  
        if (pool.size() > 0) {  
            Connection conn = pool.get(0);  
            pool.remove(conn);  
            return conn;  
        } else {  
            return null;  
        }  
    }  
}

通过连接池的管理,实现了数据库连接的共享,不需要每一次都重新创建连接,节省了数据库重新创建的开销,提升了系统的性能!

 

 C、关系模式(11种)

先来张图,看看这11中模式的关系:

第一类:通过父类与子类的关系进行实现。

第二类:两个类之间。

第三类:类的状态。

第四类:通过中间类

 

父类与子类关系

13、策略模式(strategy

策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。需要设计一个接口,为一系列实现类提供统一的方法,多个实现类实现该接口,设计一个抽象类(可有可无,属于辅助类),提供辅助函数,关系图如下:

图中ICalculator提供同意的方法,

AbstractCalculator是辅助类,提供辅助方法,接下来,依次实现下每个类:

首先统一接口:

public interface ICalculator {  
    public int calculate(String exp);  
}

辅助类:

public abstract class AbstractCalculator {  
      
    public int[] split(String exp,String opt){  
        String array[] = exp.split(opt);  
        int arrayInt[] = new int[2];  
        arrayInt[0] = Integer.parseInt(array[0]);  
        arrayInt[1] = Integer.parseInt(array[1]);  
        return arrayInt;  
    }  
}

三个实现类:

public class Plus extends AbstractCalculator implements ICalculator {  
  
    @Override  
    public int calculate(String exp) {  
        int arrayInt[] = split(exp,"\\+");  
        return arrayInt[0]+arrayInt[1];  
    }  
}
public class Minus extends AbstractCalculator implements ICalculator {  
  
    @Override  
    public int calculate(String exp) {  
        int arrayInt[] = split(exp,"-");  
        return arrayInt[0]-arrayInt[1];  
    }  
  
}
public class Multiply extends AbstractCalculator implements ICalculator {  
  
    @Override  
    public int calculate(String exp) {  
        int arrayInt[] = split(exp,"\\*");  
        return arrayInt[0]*arrayInt[1];  
    }  
}

简单的测试类:

public class StrategyTest {  
  
    public static void main(String[] args) {  
        String exp = "2+8";  
        ICalculator cal = new Plus();  
        int result = cal.calculate(exp);  
        System.out.println(result);  
    }  
}

输出:10

策略模式的决定权在用户,系统本身提供不同算法的实现,新增或者删除算法,对各种算法做封装。因此,策略模式多用在算法决策系统中,外部用户只需要决定用哪个算法即可。

 

14、模板方法模式(Template Method

解释一下模板方法模式,就是指:一个抽象类中,有一个主方法,再定义1...n个方法,可以是抽象的,也可以是实际的方法,定义一个类,继承该抽象类,重写抽象方法,通过调用抽象类,实现对子类的调用,先看个关系图:

就是在AbstractCalculator类中定义一个主方法calculatecalculate()调用spilt()等,PlusMinus分别继承AbstractCalculator类,通过对AbstractCalculator的调用实现对子类的调用,看下面的例子:

public abstract class AbstractCalculator {  
      
    /*主方法,实现对本类其它方法的调用*/  
    public final int calculate(String exp,String opt){  
        int array[] = split(exp,opt);  
        return calculate(array[0],array[1]);  
    }  
      
    /*被子类重写的方法*/  
    abstract public int calculate(int num1,int num2);  
      
    public int[] split(String exp,String opt){  
        String array[] = exp.split(opt);  
        int arrayInt[] = new int[2];  
        arrayInt[0] = Integer.parseInt(array[0]);  
        arrayInt[1] = Integer.parseInt(array[1]);  
        return arrayInt;  
    }  
}
public class Plus extends AbstractCalculator {  
  
    @Override  
    public int calculate(int num1,int num2) {  
        return num1 + num2;  
    }  
}

测试类:

public class StrategyTest {  
  
    public static void main(String[] args) {  
        String exp = "8+8";  
        AbstractCalculator cal = new Plus();  
        int result = cal.calculate(exp, "\\+");  
        System.out.println(result);  
    }  
}

我跟踪下这个小程序的执行过程:首先将exp"\\+"做参数,调用AbstractCalculator类里的calculate(String,String)方法,在calculate(String,String)里调用同类的split(),之后再调用calculate(int  ,int)方法,从这个方法进入到子类中,执行完return num1 + num2后,将值返回到AbstractCalculator类,赋给result,打印出来。正好验证了我们开头的思路。

 

类之间的关系

15、观察者模式(Observer

包括这个模式在内的接下来的四个模式,都是类和类之间的关系,不涉及到继承,学的时候应该  记得归纳,记得本文最开始的那个图。观察者模式很好理解,类似于邮件订阅和RSS订阅,当我们浏览一些博客或wiki时,经常会看到RSS图标,就这的意思是,当你订阅了该文章,如果后续有更新,会及时通知你。其实,简单来讲就一句话:当一个对象变化时,其它依赖该对象的对象都会收到通知,并且随着变化!对象之间是一种一对多的关系。先来看看关系图:

我解释下这些类的作用:MySubject类就是我们的主对象,Observer1Observer2是依赖于MySubject的对象,当MySubject变化时,Observer1Observer2必然变化。AbstractSubject类中定义着需要监控的对象列表,可以对其进行修改:增加或删除被监控对象,且当MySubject变化时,负责通知在列表内存在的对象。我们看实现代码:

一个Observer接口:

public interface Observer {  
    public void update();  
}

两个实现类:

public class Observer1 implements Observer {  
  
    @Override  
    public void update() {  
        System.out.println("observer1 has received!");  
    }  
}
public class Observer2 implements Observer {  
  
    @Override  
    public void update() {  
        System.out.println("observer2 has received!");  
    }  
  
}

Subject接口及实现类:

public interface Subject {  
      
    /*增加观察者*/  
    public void add(Observer observer);  
      
    /*删除观察者*/  
    public void del(Observer observer);  
      
    /*通知所有的观察者*/  
    public void notifyObservers();  
      
    /*自身的操作*/  
    public void operation();  
}
public abstract class AbstractSubject implements Subject {  
  
    private Vector vector = new Vector();  
    @Override  
    public void add(Observer observer) {  
        vector.add(observer);  
    }  
  
    @Override  
    public void del(Observer observer) {  
        vector.remove(observer);  
    }  
  
    @Override  
    public void notifyObservers() {  
        Enumeration enumo = vector.elements();  
        while(enumo.hasMoreElements()){  
            enumo.nextElement().update();  
        }  
    }  
}
public class MySubject extends AbstractSubject {  
  
    @Override  
    public void operation() {  
        System.out.println("update self!");  
        notifyObservers();  
    }  
  
}

测试类:

public class ObserverTest {  
  
    public static void main(String[] args) {  
        Subject sub = new MySubject();  
        sub.add(new Observer1());  
        sub.add(new Observer2());  
          
        sub.operation();  
    }  
  
}

输出:

update self!

observer1 has received!

observer2 has received!

 这些东西,其实不难,只是有些抽象,不太容易整体理解,建议读者:根据关系图,新建项目,自己写代码(或者参考我的代码),按照总体思路走一遍,这样才能体会它的思想,理解起来容易!

 

 

16、迭代子模式(Iterator

顾名思义,迭代器模式就是顺序访问聚集中的对象,一般来说,集合中非常常见,如果对集合类比较熟悉的话,理解本模式会十分轻松。这句话包含两层意思:一是需要遍历的对象,即聚集对象,二是迭代器对象,用于对聚集对象进行遍历访问。我们看下关系图:

 


这个思路和我们常用的一模一样,MyCollection中定义了集合的一些操作,MyIterator中定义了一系列迭代操作,且持有Collection实例,我们来看看实现代码:

两个接口:

public interface Collection {  
      
    public Iterator iterator();  
      
    /*取得集合元素*/  
    public Object get(int i);  
      
    /*取得集合大小*/  
    public int size();  
}
public interface Iterator {  
    //前移  
    public Object previous();  
      
    //后移  
    public Object next();  
    public boolean hasNext();  
      
    //取得第一个元素  
    public Object first();  
}

两个实现:

public class MyCollection implements Collection {  
  
    public String string[] = {"A","B","C","D","E"};  
    @Override  
    public Iterator iterator() {  
        return new MyIterator(this);  
    }  
  
    @Override  
    public Object get(int i) {  
        return string[i];  
    }  
  
    @Override  
    public int size() {  
        return string.length;  
    }  
}
public class MyIterator implements Iterator {  
  
    private Collection collection;  
    private int pos = -1;  
      
    public MyIterator(Collection collection){  
        this.collection = collection;  
    }  
      
    @Override  
    public Object previous() {  
        if(pos > 0){  
            pos--;  
        }  
        return collection.get(pos);  
    }  
  
    @Override  
    public Object next() {  
        if(pos<collection.size()-1){  
            pos++;  
        }  
        return collection.get(pos);  
    }  
  
    @Override  
    public boolean hasNext() {  
        if(pos<collection.size()-1){  
            return true;  
        }else{  
            return false;  
        }  
    }  
  
    @Override  
    public Object first() {  
        pos = 0;  
        return collection.get(pos);  
    }  
  
}

测试类:

public class Test {  
  
    public static void main(String[] args) {  
        Collection collection = new MyCollection();  
        Iterator it = collection.iterator();  
          
        while(it.hasNext()){  
            System.out.println(it.next());  
        }  
    }  
}

输出:A B C D  E

此处我们貌似模拟了一个集合类的过程,感觉是不是很爽?其实JDK中各个类也都是这些基本的东西,加一些设计模式,再加一些优化放到一起的,只要我们把这些东西学会了,掌握好了,我们也可以写出自己的集合类,甚至框架!

 

 

 

17、责任链模式(Chain of Responsibility

接下来我们将要谈谈责任链模式,有多个对象,每个对象持有对下一个对象的引用,这样就会形成一条链,请求在这条链上传递,直到某一对象决定处理该请求。但是发出者并不清楚到底最终那个对象会处理该请求,所以,责任链模式可以实现,在隐瞒客户端的情况下,对系统进行动态的调整。先看看关系图:

 

 

Abstracthandler类提供了getset方法,方便MyHandle类设置和修改引用对象,MyHandle类是核心,实例化后生成一系列相互持有的对象,构成一条链。

public interface Handler {  
    public void operator();  
}
public abstract class AbstractHandler {  
      
    private Handler handler;  
  
    public Handler getHandler() {  
        return handler;  
    }  
  
    public void setHandler(Handler handler) {  
        this.handler = handler;  
    }  
      
}
public class MyHandler extends AbstractHandler implements Handler {  
  
    private String name;  
  
    public MyHandler(String name) {  
        this.name = name;  
    }  
  
    @Override  
    public void operator() {  
        System.out.println(name+"deal!");  
        if(getHandler()!=null){  
            getHandler().operator();  
        }  
    }  
}
public class Test {  
  
    public static void main(String[] args) {  
        MyHandler h1 = new MyHandler("h1");  
        MyHandler h2 = new MyHandler("h2");  
        MyHandler h3 = new MyHandler("h3");  
  
        h1.setHandler(h2);  
        h2.setHandler(h3);  
  
        h1.operator();  
    }  
}

输出:

h1deal!

h2deal!

h3deal!

此处强调一点就是,链接上的请求可以是一条链,可以是一个树,还可以是一个环,模式本身不约束这个,需要我们自己去实现,同时,在一个时刻,命令只允许由一个对象传给另一个对象,而不允许传给多个对象。

 

 18、命令模式(Command

命令模式很好理解,举个例子,司令员下令让士兵去干件事情,从整个事情的角度来考虑,司令员的作用是,发出口令,口令经过传递,传到了士兵耳朵里,士兵去执行。这个过程好在,三者相互解耦,任何一方都不用去依赖其他人,只需要做好自己的事儿就行,司令员要的是结果,不会去关注到底士兵是怎么实现的。我们看看关系图:

Invoker是调用者(司令员),Receiver是被调用者(士兵),MyCommand是命令,实现了Command接口,持有接收对象,看实现代码:

public interface Command {  
    public void exe();  
}
public class MyCommand implements Command {  
  
    private Receiver receiver;  
      
    public MyCommand(Receiver receiver) {  
        this.receiver = receiver;  
    }  
  
    @Override  
    public void exe() {  
        receiver.action();  
    }  
}
public class Receiver {  
    public void action(){  
        System.out.println("command received!");  
    }  
}
public class Invoker {  
      
    private Command command;  
      
    public Invoker(Command command) {  
        this.command = command;  
    }  
  
    public void action(){  
        command.exe();  
    }  
}
public class Test {  
  
    public static void main(String[] args) {  
        Receiver receiver = new Receiver();  
        Command cmd = new MyCommand(receiver);  
        Invoker invoker = new Invoker(cmd);  
        invoker.action();  
    }  
}

输出:command  received!

这个很哈理解,命令模式的目的就是达到命令的发出者和执行者之间解耦,实现请求和执行分开,熟悉Struts的同学应该知道,Struts其实就是一种将请求和呈现分离的技术,其中必然涉及命令模式的思想!

其实每个设计模式都是很重要的一种思想,看上去很熟,其实是因为我们在学到的东西中都有涉及,尽管有时我们并不知道,其实在Java本身的设计之中处处都有体现,像AWTJDBC、集合类、IO管道或者是Web框架,里面设计模式无处不在。因为我们篇幅有限,很难讲每一个设计模式都讲的很详细,不过我会尽我所能,尽量在有限的空间和篇幅内,把意思写清楚了,更好让大家明白。本章不出意外的话,应该是设计模式最后一讲了,首先还是上一下上篇开头的那个图:

本章讲讲第三类和第四类。

类的状态

19、备忘录模式(Memento

主要目的是保存一个对象的某个状态,以便在适当的时候恢复对象,个人觉得叫备份模式更形象些,通俗的讲下:假设有原始类AA中有各种属性,A可以决定需要备份的属性,备忘录类B是用来存储A的一些内部状态,类C呢,就是一个用来存储备忘录的,且只能存储,不能修改等操作。做个图来分析一下:

Original类是原始类,里面有需要保存的属性value及创建一个备忘录类,用来保存value值。Memento类是备忘录类,Storage类是存储备忘录的类,持有Memento类的实例,该模式很好理解。直接看源码:

public class Original {  
      
    private String value;  
      
    public String getValue() {  
        return value;  
    }  
  
    public void setValue(String value) {  
        this.value = value;  
    }  
  
    public Original(String value) {  
        this.value = value;  
    }  
  
    public Memento createMemento(){  
        return new Memento(value);  
    }  
      
    public void restoreMemento(Memento memento){  
        this.value = memento.getValue();  
    }  
}
public class Memento {  
      
    private String value;  
  
    public Memento(String value) {  
        this.value = value;  
    }  
  
    public String getValue() {  
        return value;  
    }  
  
    public void setValue(String value) {  
        this.value = value;  
    }  
}
public class Storage {  
      
    private Memento memento;  
      
    public Storage(Memento memento) {  
        this.memento = memento;  
    }  
  
    public Memento getMemento() {  
        return memento;  
    }  
  
    public void setMemento(Memento memento) {  
        this.memento = memento;  
    }  
}

测试类:

public class Test {  
  
    public static void main(String[] args) {  
          
        // 创建原始类  
        Original origi = new Original("egg");  
  
        // 创建备忘录  
        Storage storage = new Storage(origi.createMemento());  
  
        // 修改原始类的状态  
        System.out.println("初始化状态为:" + origi.getValue());  
        origi.setValue("niu");  
        System.out.println("修改后的状态为:" + origi.getValue());  
  
        // 回复原始类的状态  
        origi.restoreMemento(storage.getMemento());  
        System.out.println("恢复后的状态为:" + origi.getValue());  
    }  
}

输出:

初始化状态为:egg

修改后的状态为:niu

恢复后的状态为:egg

简单描述下:新建原始类时,value被初始化为egg,后经过修改,将value的值置为niu,最后倒数第二行进行恢复状态,结果成功恢复了。其实我觉得这个模式叫“备份-恢复”模式最形象。

 

20、状态模式(State

核心思想就是:当对象的状态改变时,同时改变其行为,很好理解!就拿QQ来说,有几种状态,在线、隐身、忙碌等,每个状态对应不同的操作,而且你的好友也能看到你的状态,所以,状态模式就两点:1、可以通过改变状态来获得不同的行为。2、你的好友能同时看到你的变化。看图:


State类是个状态类,Context类可以实现切换,我们来看看代码:

package com.xtfggef.dp.state;  
  
/** 
 * 状态类的核心类 
 * 2012-12-1 
 * @author erqing 
 * 
 */  
public class State {  
      
    private String value;  
      
    public String getValue() {  
        return value;  
    }  
  
    public void setValue(String value) {  
        this.value = value;  
    }  
  
    public void method1(){  
        System.out.println("execute the first opt!");  
    }  
      
    public void method2(){  
        System.out.println("execute the second opt!");  
    }  
}
package com.xtfggef.dp.state;  
  
/** 
 * 状态模式的切换类   2012-12-1 
 * @author erqing 
 *  
 */  
public class Context {  
  
    private State state;  
  
    public Context(State state) {  
        this.state = state;  
    }  
  
    public State getState() {  
        return state;  
    }  
  
    public void setState(State state) {  
        this.state = state;  
    }  
  
    public void method() {  
        if (state.getValue().equals("state1")) {  
            state.method1();  
        } else if (state.getValue().equals("state2")) {  
            state.method2();  
        }  
    }  
}

测试类:

public class Test {  
  
    public static void main(String[] args) {  
          
        State state = new State();  
        Context context = new Context(state);  
          
        //设置第一种状态  
        state.setValue("state1");  
        context.method();  
          
        //设置第二种状态  
        state.setValue("state2");  
        context.method();  
    }  
}

输出:

execute the first opt!

execute the second opt!

根据这个特性,状态模式在日常开发中用的挺多的,尤其是做网站的时候,我们有时希望根据对象的某一属性,区别开他们的一些功能,比如说简单的权限控制等。

 

 通过中间类

21、访问者模式(Visitor

 

访问者模式把数据结构和作用于结构上的操作解耦合,使得操作集合可相对自由地演化。访问者模式适用于数据结构相对稳定算法又易变化的系统。因为访问者模式使得算法操作增加变得容易。若系统数据结构对象易于变化,经常有新的数据对象增加进来,则不适合使用访问者模式。访问者模式的优点是增加操作很容易,因为增加操作意味着增加新的访问者。访问者模式将有关行为集中到一个访问者对象中,其改变不影响系统数据结构。其缺点就是增加新的数据结构很困难。—— From 百科

简单来说,访问者模式就是一种分离对象数据结构与行为的方法,通过这种分离,可达到为一个被访问者动态添加新的操作而无需做其它的修改的效果。简单关系图:

来看看原码:一个Visitor类,存放要访问的对象,

public interface Visitor {  
    public void visit(Subject sub);  
}
public class MyVisitor implements Visitor {  
  
    @Override  
    public void visit(Subject sub) {  
        System.out.println("visit the subject:"+sub.getSubject());  
    }  
}

Subject类,accept方法,接受将要访问它的对象,getSubject()获取将要被访问的属性,

public interface Subject {  
    public void accept(Visitor visitor);  
    public String getSubject();  
}
public class MySubject implements Subject {  
  
    @Override  
    public void accept(Visitor visitor) {  
        visitor.visit(this);  
    }  
  
    @Override  
    public String getSubject() {  
        return "love";  
    }  
}

测试:

public class Test {  
  
    public static void main(String[] args) {  
          
        Visitor visitor = new MyVisitor();  
        Subject sub = new MySubject();  
        sub.accept(visitor);      
    }  
}

输出:visit the subjectlove

 

该模式适用场景:如果我们想为一个现有的类增加新功能,不得不考虑几个事情:1、新功能会不会与现有功能出现兼容性问题?2、以后会不会再需要添加?3、如果类不允许修改代码怎么办?面对这些问题,最好的解决方法就是使用访问者模式,访问者模式适用于数据结构相对稳定的系统,把数据结构和算法解耦,

 

22、中介者模式(Mediator

 

中介者模式也是用来降低类类之间的耦合的,因为如果类类之间有依赖关系的话,不利于功能的拓展和维护,因为只要修改一个对象,其它关联的对象都得进行修改。如果使用中介者模式,只需关心和Mediator类的关系,具体类类之间的关系及调度交给Mediator就行,这有点像spring容器的作用。先看看图:

User类统一接口,User1User2分别是不同的对象,二者之间有关联,如果不采用中介者模式,则需要二者相互持有引用,这样二者的耦合度很高,为了解耦,引入了Mediator类,提供统一接口,MyMediator为其实现类,里面持有User1User2的实例,用来实现对User1User2的控制。这样User1User2两个对象相互独立,他们只需要保持好和Mediator之间的关系就行,剩下的全由MyMediator类来维护!基本实现:

public interface Mediator {  
    public void createMediator();  
    public void workAll();  
}
public class MyMediator implements Mediator {  
  
    private User user1;  
    private User user2;  
      
    public User getUser1() {  
        return user1;  
    }  
  
    public User getUser2() {  
        return user2;  
    }  
  
    @Override  
    public void createMediator() {  
        user1 = new User1(this);  
        user2 = new User2(this);  
    }  
  
    @Override  
    public void workAll() {  
        user1.work();  
        user2.work();  
    }  
}
public abstract class User {  
      
    private Mediator mediator;  
      
    public Mediator getMediator(){  
        return mediator;  
    }  
      
    public User(Mediator mediator) {  
        this.mediator = mediator;  
    }  
  
    public abstract void work();  
}
public class User1 extends User {  
  
    public User1(Mediator mediator){  
        super(mediator);  
    }  
      
    @Override  
    public void work() {  
        System.out.println("user1 exe!");  
    }  
}
public class User2 extends User {  
  
    public User2(Mediator mediator){  
        super(mediator);  
    }  
      
    @Override  
    public void work() {  
        System.out.println("user2 exe!");  
    }  
}

测试类:

public class Test {  
  
    public static void main(String[] args) {  
        Mediator mediator = new MyMediator();  
        mediator.createMediator();  
        mediator.workAll();  
    }  
}

输出:

user1 exe!

user2 exe!

 

23、解释器模式(Interpreter

解释器模式是我们暂时的最后一讲,一般主要应用在OOP开发中的编译器的开发中,所以适用面比较窄。

 


Context类是一个上下文环境类,PlusMinus分别是用来计算的实现,代码如下:

public interface Expression {  
    public int interpret(Context context);  
}
public class Plus implements Expression {  
  
    @Override  
    public int interpret(Context context) {  
        return context.getNum1()+context.getNum2();  
    }  
}
public class Minus implements Expression {  
  
    @Override  
    public int interpret(Context context) {  
        return context.getNum1()-context.getNum2();  
    }  
}
public class Context {  
      
    private int num1;  
    private int num2;  
      
    public Context(int num1, int num2) {  
        this.num1 = num1;  
        this.num2 = num2;  
    }  
      
    public int getNum1() {  
        return num1;  
    }  
    public void setNum1(int num1) {  
        this.num1 = num1;  
    }  
    public int getNum2() {  
        return num2;  
    }  
    public void setNum2(int num2) {  
        this.num2 = num2;  
    } 
}
public class Test {  
  
    public static void main(String[] args) {  
  
        // 计算9+2-8的值  
        int result = new Minus().interpret((new Context(new Plus()  
                .interpret(new Context(9, 2)), 8)));  
        System.out.println(result);  
    }  
}

最后输出正确的结果:3

基本就这样,解释器模式用来做各种各样的解释器,如正则表达式等的解释器等等!

 

来自 <https://www.cnblogs.com/geek6/p/3951677.html>

里氏代换原则原文

Data Abstraction and Hierarchy

    * This research was supported by the NEC Professorship of Software Science and Engineering.

Barbara Liskov

Affiliation: MIT Laboratory for Computer Science

Cambridge, MA  , 02139


ABSTRACT

Data abstraction is a valuable method for organizing programs to make them easier to modify and maintain.  Inheritance allows one implementation of a data abstraction to be related to another hierarchically. This paper investigates the usefulness of hierarchy in program development, and concludes that although data abstraction is the more important idea, hierarchy does extend its usefulness in some situations.

1.Introduction

An important goal in design is to identify a program structure that simplifies both program maintenance and program modifications made to support changing requirements. Data abstractions are a good way of achieving this goal. They allow us to abstract from the way data structures are implemented to the behavior they provide that other programs can rely on. They permit the representation of data to be changed locally without affecting programs that use the data.  They are particularly important because they hide complicated things (data structures) that are likely to change in the future. They also simplify the structure of programs that use them because they present a higher level interface.  For example, they reduce the number of arguments to procedures because abstract objects are communicated instead of their representations.

Object-oriented programming is primarily a data abstraction technique, and much of its power derives from this.  However, it elaborates this technique with the notion of "inheritance."

Inheritance can be used in a number of ways, some of which enhance the power of data abstraction. In these cases, inheritance provides a useful addition to data abstraction.

This paper discusses the relationship between data abstraction and object-oriented programming. We begin in Section 2 by defining data abstraction and its role in the program development process. Then in Section 3 we discuss inheritance and identify two ways that it is used, for implementation hierarchy and for type hierarchy. Of the two methods, type hierarchy really adds something to data abstraction, so in Section 4 we discuss uses of type hierarchy in program design and development. Next we discuss some issues that arise in implementing type hierarchy. We conclude with a summary of our results.

2.Data Abstraction

The purpose of abstraction in programming is to separate behavior from implementation.  The first programming abstraction mechanism was the procedure. A procedure performs some task or function; other parts of the program call the procedure to accomplish the task. To use the procedure, a programmer cares only about what it does and not how it is implemented. Any implementation that provides the needed function will do, provided it implements the function correctly and is efficient enough.

Procedures are a useful abstraction mechanism, but in the early seventies some researchers realized that they were not enough [15], [16], [7] and proposed a new way of organizing programs around the "connections" between modules. The concept of data abstraction or abstract data type arose from these ideas [5], [12].

Data abstractions provide the same benefits as procedures, but for data. Recall that the main idea is to separate what an abstraction is from how it is implemented so that implementations of the same abstraction can be substituted freely.  The implementation of a data object is concerned with how that object is represented in the memory of a computer; this information is called the representation, or rep for short.  To allow changing implementations without affecting users, we need a way of changing the representation without having to change all using programs.  This is achieved by encapsulating the rep with a set of operations that manipulate it and by restricting using programs so that they cannot manipulate the rep directly, but instead must call the operations. Then, to implement or reimplement the data abstraction, it is necessary to define the rep and implement the operations in terms of it, but using code is not affected by a change.

Thus a data abstraction is a set of objects that can be manipulated directly only by a set of operations. An example of a data abstraction is the integers: the objects are 1, 2, 3, and so on and there are operations to add two integers, to test them for equality, and so on. Programs using integers manipulate them by their operations, and are shielded from implementation details such as whether the representation is 2's complement.  Another example is character strings, with objects such as "a" and "xyz", and operations to select characters from strings and to concatenate strings. A final example is sets of integers, with objects such as { } (the empty set) and {3, 7}, and operations to insert an element in a set, and to test whether an integer is in a set. Note that integers and strings are built-in data types in most programming languages, while sets and other application-oriented data abstractions such as stacks and symbol tables are not. Linguistic mechanisms that permit user-defined abstract data types to be implemented are discussed in Section 2.2.

A data or procedure abstraction is defined by a specification and implemented by a program module coded in some programming language.  The specification describes what the abstraction does, but omits any information about how it is implemented.  By omitting such detail, we permit many different implementations An implementation is correct if it provides the behavior defined by the specification. Correctness can be proved mathematically if the specification is written in a language with precise semantics; otherwise we establish correctness by informal reasoning or by the somewhat unsatisfactory technique of testing.  Correct implementations differ from one another in how they work, i.e., what algorithms they use, and therefore they may have different performance. Any correct implementation is acceptable to the caller provided it meets the caller's performance requirements.  Note that correct implementations need not be identical to one another; the whole point is to allow implementations to differ, while ensuring that they remain the same where this is important. The specification describes what is important.

For abstraction to work, implementations must be encapsulated. If an implementation is encapsulated, then no other module can depend on its implementation details. Encapsulation guarantees that modules can be implemented and reimplemented independently; it is related to the principle of "information hiding" advocated by Parnas [15].

2.1.  Locality

Abstraction when supported by specifications and encapsulation provides locality within a program. Locality allows a program to be implemented, understood, or modified one module at a time:

1.The implementer of an abstraction knows what is needed because this is described in the specification. Therefore, he or she need not interact with programmers of other modules (or at least the interactions can be very limited).

2.Similarly, the implementer of a using module knows what to expect from an abstraction, namely the behavior described by the specification.

3.Only local reasoning is needed to determine what a program does and whether it does the right thing. The program is studied one module at a time.  In each case we are concerned with whether the module does what it is supposed to do, that is, does it meet its specification. However, we can limit our attention to just that module and ignore both modules that use it, and modules that it uses.  Using modules can be ignored because they depend only on the specification of this module, not on its code. Used modules are ignored by reasoning about what they do using their specifications instead of their code. There is a tremendous saving of effort in this way because specifications are much smaller than implementations.  For example, if we had to look at the code of a called abstraction, we would be concerned not only with its code, but also with the code of any modules it uses, and so on.

4.Finally, program modification can be done module by module. If a particular abstraction needs to be reimplemented to provide better performance or correct an error or provide extended facilities, the old implementing module can be replaced by a new one without affecting other modules.

Locality provides a firm basis for fast prototyping.  Typically there is a tradeoff between the performance of an algorithm and the speed with which it is designed and implemented.  The initial implementation can be a simple one that performs poorly.  Later it can be replaced by another implementation with better performance.  Provided both implementations are correct, the calling program's correctness will be unaffected by the change.

Locality also supports program evolution.  Abstractions can be used to encapsulate potential modifications.  For example, suppose we want a program to run on different machines.  We can accomplish this by inventing abstractions that hide the differences between machines so that to move the program to a different machine only those abstractions need be reimplemented. A good move the program to a different machine only those abstractions need be reimplemented. A good design principle is to think about expected modifications and organize the design by using abstractions that encapsulate the changes.

The benefits of locality are particularly important for data abstractions.  Data structures are often complicated and therefore the simpler abstract view provided by the specification allows the rest of the program to be simpler. Also, changes to storage structures are likely as programs evolve; the effects of such changes can be minimized by encapsulating them inside data abstractions.

2.2.  Linguistic Support for Data Abstraction

Data abstractions are supported by linguistic mechanisms in several languages.  The earliest such language was Simula 67 [3]. Two major variations, those in CLU and Smalltalk, are discussed below.

CLU [8][11] provides a mechanism called a cluster for implementing an abstract type. A template for a cluster is shown in Figure 2-1. The header identifies the data type being implemented and also lists the operations of the type; it serves to identify what procedure definitions inside the cluster can be called from the outside. The "rep = " line defines how objects of the type are represented; in the example, we are implementing sets as linked lists.  The rest of the cluster consists of procedures; there must be a procedure for each operation, and in addition, there may be some procedures that can be used only inside the cluster.

int_set = cluster is create, insert, is_in, size,...

    rep = int_list      create = proc ... end create      insert = proc ... end insert      ... end int_set

Figure 2-1: Template of a CLU Cluster.

In Smalltalk [4], data abstractions are implemented by classes. Classes can be arranged hierarchically, but we ignore this for now. A class implements a data abstraction similarly to a cluster. Instead of the "rep =" line, the rep is described by a sequence of variable declarations; these are the instance variables.     * We ignore the class variables here since they are not important for the distinctions

we are trying to make. CLU has an analogous mechanism; a cluster can have some "own" variables [9].  The

remainder of the class consists of methods, which are procedure definitions. There is a method for each operation of the data type implemented by the class.  (There cannot be any internal methods in Smalltalk classes because it is not possible to preclude outside use of a method.) Methods are called by "sending messages," which has the same effect as calling operations in CLU.

Both CLU and Smalltalk enforce encapsulation but CLU uses compile-time type checking, while Smalltalk uses runtime checking. Compile-time checking is better because it allows a class of errors to be caught before the program runs, and it permits more efficient code to be generated by a compiler. (Compile-time checking can limit expressive power unless the programming language has a powerful type system; this issue is discussed further in Section 5.) Other object- oriented languages, e.g., [1], [13], do not enforce encapsulation at all.  It is true that in the absence of language support encapsulation can be guaranteed by manual procedures such as code reading, but these techniques are error-prone, and although the situation may be somewhat manageable for a newly-implemented program, it will degrade rapidly as modifications are made. Automatic checking, at either runtime or compile-time, can be relied on with confidence and without the need to read any code at all.

Another difference between CLU and Smalltalk is found in the semantics of data objects. In Smalltalk, the operations are part of the object and can access the instance variables that make up the object's rep since these variables are part of the object too.  In CLU, operations do not belong to the object but instead belong to a type. This gives them special privileges with respect to their type's objects that no other parts of the program have, namely, they can see the reps of these objects.  (This view was first described by Morris [14].) The CLU view works better for operations that manipulate several objects of the type simultaneously because an operation can see the reps of several objects at once.  Examples of such operations are adding two integers or forming the union of two sets. The Smalltalk view does not support such operations as well, since an operation can be inside of only one object. On the other hand, the Smalltalk view works better when we want to have several implementations of the same type running within the same program. In CLU an operation can see the rep of any object of its type, and therefore must be coded to cope with multiple representations explicitly.  Smalltalk avoids this problem since an operation can see the rep of only one object.

3.Inheritance and Hierarchy

This section discusses inheritance and how it supports hierarchy. We begin by talking about what it means to construct a program using inheritance.  Then we discuss two major uses of inheritance, implementation hierarchy and type hierarchy; see [18] for a similar discussion.  Only one of these, type hierarchy, adds something new to data abstraction.

3.1.  Inheritance

In a language with inheritance, a data abstraction can be implemented in several pieces that are related to one another. Although various languages provide different mechanisms for putting the pieces together, they are all similar.  Thus we can illustrate them by examining a single mechanism, the subclass mechanism in Smalltalk.

In Smalltalk a class can be declared to be a subclass of another class     * We ignore multiple inheritance to simplify the discussion.

mechanism is what code results from such a definition. This question is important for understanding what a subclass does. For example, if we were to reason about its correctness, we would need to look at this code.

From the point of view of the resulting code, saying that one class is a subclass of another is simply a shorthand notation for building programs. The exact program that is constructed depends on the rules of the language, e.g., such things as when methods of the subclass override methods of the superclass. The exact details of these rules are not important for our discussion (although they clearly are important if the language is to be sensible and useful).  The point is that the result is equivalent to directly implementing a class containing the instance variables and methods that result from applying the rules.

For example, suppose class T has operations o1 and o2 and instance variable v1 and class S, which is declared to be a subclass of T, has operations o1 and o3 and instance variable v2.  Then the result in Smalltalk is effectively a class with two instance variables, v1 and v2, and three operations, o1, o2, o3, where the code of o2 is supplied by T, and the code of the other two operations is supplied by S. It is this combined code that must be understood, or modified if S is reimplemented, unless S is restricted as discussed further below.

One problem with almost all inheritance mechanisms is that they compromise data abstraction to an extent. In languages with inheritance, a data abstraction implementation (i.e., a class) has two kinds of users. There are the "outsiders" who simply use the objects by calling the operations. But in addition there are the "insiders." These are the subclasses, which are typically permitted to violate encapsulation. There are three ways that encapsulation can be violated [18]: the subclass might access an instance variable of its superclass, call a private operation of its superclass, or refer directly to superclasses of its superclass. (This last violation is not possible in Smalltalk.)

When encapsulation is not violated, we can reason about operations of the superclass using their specifications and we can ignore the rep of the superclass. When encapsulation is violated, we lose the benefits of locality. We must consider the combined code of the sub- and superclass in reasoning about the subclass, and if the superclass needs to be reimplemented, we may need to reimplement its subclasses too.  For example, this would be necessary if an instance variable of the superclass changed, or if a subclass refers directly to a superclass of its superclass T and then T is reimplemented to no longer have this superclass.

Violating encapsulation can be useful in bringing up a prototype quickly since it allows code to be produced by extension and modification of existing code.  It is unrealistic, however, to expect that modifications to the implementation of the superclass can be propagated automatically to the subclass. Propagation is useful only if the resulting code works, which means that all expectations of the subclass about the superclass must be satisfied by the new implementation. These expectations can be captured by providing another specification for the superclass; this is a different specification from that for outsiders, since it contains additional constraints. Using this additional specification, a programmer can determine whether a proposed change to the superclass can usefully propagate to the subclass. Note that the more specific the additional specification is about details of the previous superclass implementation, the less likely that a new superclass implementation will meet it. Also, the situation will be unmanageable if each subclass relies on a different specification of the superclass. One possible approach is to define a single specification for use by all subclasses that contains more detail than the specification for outsiders but still abstracts from many implementation details. Some work in this direction is described in [17].

3.2.  Implementation Hierarchy

The first way that inheritance is used is simply as a technique for implementing data types that are similar to other existing types. For example, suppose we want to implement integer sets, with operations (among others) to tell whether an element is a member of the set and to determine the current size of the set. Suppose further that a list data type has already been implemented, and that it provides a member operation and a size operation, as well as a convenient way of representing the set.  Then we could implement set as a subclass of list; we might have the list hold the set elements without duplication, i.e., if an element were added to the set twice, it would be appear in the list only once. Then we would not need to provide implementations for member and size, but we would need to implement other operations such as one that inserts a new element into the set. Also, we should suppress certain other operations, such as car, to make them unavailable since they are not meaningful for sets.  (This can be done in Smalltalk by providing implementations in the subclass for the suppressed operations; such an implementation would signal an exception if called.)

Another way of doing the same thing is to use one (abstract) type as the rep of another. For example, we might implement sets by using list as the rep. In this case, we would need to implement the size and member operations; each of these would simply call the corresponding operation on lists. Writing down implementations for these two operations, even though the code is very simple, is more work than not writing anything for them.  On the other hand, we need not do anything to take away undesirable operations such as car.

Since implementation hierarchy does not allow us to do anything that we could not already do with data abstraction, we will not consider it further. It does permit us to violate encapsulation, with both the benefits and problems that ensue. However, this ability could also exist in the rep approach if desired.

3.3.  Type Hierarchy

A type hierarchy is composed of subtypes and supertypes. The intuitive idea of a subtype is one whose objects provide all the behavior of objects of another type (the supertype) plus something extra. What is wanted here is something like the following substitution property [6]: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T. (See also [2], [17] for other work in this area.)

We are using the words "subtype" and "supertype" here to emphasize that now we are talking about a semantic distinction.  By contrast, "subclass" and "superclass" are simply linguistic concepts in programming languages that allow programs to be built in a particular way. They can be used to implement subtypes, but also, as mentioned above, in other ways.

We begin with some examples of types that are not subtypes of one another. First, a set is not a subtype of a list nor is the reverse true. If the same element is added to a set twice, the result is the same as if it had been added only once and the element is counted only once in computing the size of the set. However, if the same element is added twice to a list, it occurs in the list twice.  Thus a

program expecting a list might not work if passed a set; similarly a program expecting a set might not work if passed a list.  Another example of non-subtypes are stacks and queues.  Stacks are LIFO; when an element is removed from a stack, the last item added (pushed) is removed. By contrast, queues are FIFO. A using program is likely to notice the difference between these two types.

The above examples ignored a simple difference between the pairs of types, namely related operations. A subtype must have all the operations     * It needs the instance methods but not the class

methods. of its supertype since otherwise the using program could not use an operation it depends on. However, simply having operations of the right names and signatures is not enough. (A operation's signature defines the numbers and types of its input and output arguments.) The operations must also do the same things. For example, stacks and queues might have operations of the same names, e.g., add_ el to push or enqueue and rem_el to pop or dequeue, but they still are not subtypes of one another because the meanings of the operations are different for them.

Now we give some examples of subtype hierarchies.  The first is indexed collections, which have operations to access elements by index; e.g., there would be a fetch operation to fetch the ith element of the collection.  All subtypes have these operations too, but, in addition, each would provide extra operations.  Examples of subtypes are arrays, sequences, and indexed sets; e.g., sequences can be concatenated, and arrays can be modified by storing new objects in the elements.

The second example is abstract devices, which unify a number of different kinds of input and output devices. Particular devices might provide extra operations. In this case, abstract device operations would be those that all devices support, e.g., the end-of-file test, while subtype operations would be device specific. For example, a printer would have modification operations such as put_char but not reading operations such as get_char.  Another possibility is that abstract devices would have all possible operations, e.g., both put_char and get_char, and thus all subtypes would have the same set of operations.  In this case, operations that not all real devices can do must be specified in a general way that allows exceptions to be signalled. For example, get_char would signal an exception when called on a printer.

An inheritance mechanism can be used to implement a subtype hierarchy. There would be a class to implement the supertype and another class to implement each subtype. The class implementing a subtype would declare the supertype's class as its superclass.

4.Benefits of Type Hierarchy

Data abstraction is a powerful tool in its own right. Type hierarchy is a useful adjunct to data abstraction.  This section discusses how subtypes can be used in the development of the design for a program.  (A detailed discussion of design based on data abstraction can be found in [11].) It also discusses their use in organizing a program library.

4.1.  Incremental Design

Data abstractions are usually developed incrementally as a design progresses. In early stages of a design, we only know some of a data abstraction's operations and a part of its behavior. Such a stage of design is depicted in Figure 4-1a.  The design is depicted by a graph that illustrates how a program is subdivided into modules.  There are two kinds of nodes; a node with a single bar on top represents a procedure abstraction, and a node with a double bar on top represents a data abstraction.  An arrow pointing from one node to another means that the abstraction of the first node will be implemented using the abstraction of the second node.  Thus the figure shows two procedures, P and Q, and one data abstraction, T. P will be implemented using Q (i.e., its code calls Q) and T (i.e., its code uses objects of type T). (Recursion is indicated by cycles in the graph. Thus if we expected the implementation of P to call P, there would be an arrow from P to P.)

This figure represents an early stage of design, in which the designer has thought about how to implement P and has invented Q and T. At this point, some operations of T have been identified, and the designer has decided that an object of type T will be used to communicate between P and Q.

The next stage of design is to investigate how to implement Q. (It would not make sense to look at T's implementation at this point, because we do not know all its operations yet.) In studying Q we are likely to define additional operations for T. This can be viewed as refining T to a subtype S as is shown in Figure 4-2. Here a double arrow points from a supertype to a subtype; double arrows can only connect data abstractions (and there can be no cycles involving only double arrows).


图片1.png

Figure 4-1: The Start of a Design.

图片2.png


The kind of refinement illustrated in the figures may happen several times; e.g., S in turn may have a subtype R and so on. Also, a single type may have several subtypes, representing the needs of different subparts of the program.

Keeping track of these distinctions as subtypes is better than treating the group of types as a single type, for several reasons.  First it can limit the effect of design errors.  For example, suppose further investigation indicates a problem with S's interface. When a problem of this sort occurs, it is necessary to look at every abstraction that uses the changed abstraction. For the figure, this means we must look at Q. However, provided T's interface is unaffected, we need not look at P. If S and T had been treated as one type, then P would have had to be examined too.

Another advantage of distinguishing the types is that it may help in organizing the design rationale. The design rationale describes the decisions made at particular points in the design, and discusses why they were made and what alternatives exist. By maintaining the hierarchy to represent the decisions as they are made over time, we can avoid confusion and be more precise. If an error is discovered later, we can identify precisely at what point in the design it occurred.

Finally, the distinction may help during implementation, for example, if S, but not T, needs to be reimplemented.  However, it may be that the hierarchy is not maintained in the implementation.

Frequently, the end of the design is just a single type, the last subtype invented, because implementing a single module is more convenient than having separate modules for the supertype and subtypes. Even so, however, the distinction remains useful after implementation, because the effects of specification changes can still be localized, even if implementation changes cannot. For example, a change to the specification of S but not T means that we need to reimplement Q but not P. However, if S and T are implemented as a single module, we must reimplement both of them, instead of just reimplementing S.

4.2.  Related Types

The second use of subtypes is for related types. The designer may recognize that a program will use several data abstractions that are similar but different.  The differences represent variants of the same general idea, where the subtypes may all have the same set of operations, or some of them may extend the supertype. An example is the generalized abstract device mentioned earlier.  To accommodate related types in design, the designer introduces the supertype at the time the whole set of types is conceived, and then introduces the subtypes as they are needed later in design.

Related types arise in two different ways. Sometimes the relationship is defined in advance, before any types are invented; this is the situation discussed above.  Alternatively, the relationship may not be recognized until several related types already exist. This happens because of the desire to define a module that works on each of the related types but depends on only some small common part of them.  For example, the module might be a sort routine that relies on its argument "collection" to allow it to fetch elements, and relies on the element type itself to provide a "<" operation.

When the relationship is defined in advance, hierarchy is a good way to describe it, and we probably do want to use inheritance as an implementation mechanism. This permits us to implement just once (in the supertype) whatever can be done in a subtype-independent way. The module for a subtype is concerned only with the specific behavior of that subtype, and is independent of modules implementing other subtypes.  Having separate modules for the super- and subtypes gives better modularity than using a single module to implement them all. Also, if a new subtype is added later, none of the existing code need be changed.

When the relationship is recognized after the types have been defined, hierarchy may not be the right way to organize the program. This issue is discussed in Section 5.1.

4.3.  Organizing a Type Library

There is one other way in which hierarchy is useful, and this is to aid in the organization of a type library.  It has long been recognized that programming is more effective if it can be done in a context that encourages the reuse of program modules implemented by others. However, for such a context to be usable, it must be possible to navigate it easily to determine whether the desired modules exists. Hierarchy is useful as a way of organizing a program library to make searching easier, especially when combined with the kind of browsing tools present, e.g., in the Smalltalk environment.

Hierarchy allows similar types to be grouped together.  Thus, if a user want a particular kind of "collection" abstraction, there is a good chance that the desired one, if it exists at all, can be found with the other collections. The hierarchy in use is a either a subtype hierarchy, or almost a subtype hierarchy a subtype differs from an extension of the supertype in a fairly minor way). The point is that types are grouped based on their behavior rather than how they are used to implement one another.

The search for collection types, or numeric types, or whatever, is aided by two things: The first is considering the entire library as growing from a single root or roots, and providing a browsing tool that allows the user to move around in the hierarchy.  The second is a wise choice of names for the major categories, so that a user can recognize that "collection" is the part of the hierarchy of interest.

Using hierarchy as a way of organizing a library is a good idea, but need not be coupled with a subclass mechanism in a programming language.  Instead, an interactive system that supports construction and browsing of the library could organize the library in this way.

5.Type Hierarchy and Inheritance

Not all uses of type hierarchy require language support. No support is needed for the program library; instead all that is needed is to use the notion of type hierarchy as an organizing principle. Support is also usually not needed for hierarchy introduced as a refinement technique. As mentioned earlier, the most likely outcome of this design technique is a single type at the end (the last subtype introduced), which is most conveniently implemented as a unit.  Therefore any language that supports data abstraction is adequate here, although inheritance can be useful for introducing additional operations discovered later.

Special language support may be needed for related types, however. This support is discussed in Section 5.1. Section 5.2 discusses the relationship of inheritance to multiple implementations of a type.

5.1.  Polymorphism

A polymorphic procedure or data abstraction is one that works for many different types. For example, consider a procedure that does sorting. In many languages, such a procedure would be implemented to work on an array of integers; later, if we needed to sort an array of strings, another procedure would be needed. This is unfortunate. The idea of sorting is independent of the particular type of element in the array, provided that it is possible to compare the elements to determine which ones are smaller than which other ones. We ought to be able to implement one sort procedure that works for all such types. Such a procedure would be polymorphic.

Whenever there are related types in a program there is likely to be polymorphism. This is certainly the case when the relationship is indicated by the need for a polymorphic module.  Even when the relationship is identified in advance, however, polymorphism is likely.  In such a case the supertype is often virtual: it has no objects of its own, but is simply a placeholder in the hierarchy for the family of related types. In this case, any module that uses the supertype is polymorphic. On the other hand, if the supertype has objects of its own, some modules might use just it and none of its subtypes.

Using hierarchy to support polymorphism means that a polymorphic module is conceived of as using a supertype, and every type that is intended to be used by that module is made a subtype of the supertype. When supertypes are introduced before subtypes, hierarchy is a good way to capture the relationships. The supertype is added to the type universe when it is invented, and subtypes are added below it later.

If the types exist before the relationship, hierarchy does not work as well. In this case, introducing the supertype complicates the type universe: a new type (the supertype) must be added, all types used by the polymorphic module must be made its subordinates, and classes implementing the subtypes must be changed to reflect the hierarchy (and recompiled in a system that does compilation). For example, we would have to add a new type, "sortable," to the universe and make every element type be a subtype of it.  Note that each such supertype must be considered whenever a new type is invented: each new type must be made a subtype of the supertype if there is any chance that we may want to use its objects in the polymorphic module.  Furthermore, the supertype may be useless as far as code sharing is concerned, since there may be nothing that can be implemented in it.

An alternative approach is to simply allow the polymorphic module to use any type that supplies the needed operations. In this case no attempt is made to relate the types. Instead, an object belonging to any of the related types can be passed as an argument to the polymorphic module. Thus we get the same effect, but without the need to complicate the type universe.  We will refer to this approach as the grouping approach.

The two approaches differ in what is required to reason about program correctness. In either case we require the argument objects to have operations with the right signature and behavior. Signature requirements can be checked by type-checking, which can happen at runtime or compile time. Runtime checking requires no special mechanism; objects are simply passed to the polymorphic module, and type errors will be found if a needed operation is not present. Compile-time checking requires an adequate type system.  If the hierarchy approach is used, then the language must combine compile-time type checking with inheritance; an example of such a language is discussed in [17]. If the grouping approach is used, then we need a way to express constraints on operations at compile-time. For example, both CLU and Ada [19] can do this. In CLU, the header of a sort procedure might be

    sort = proc [T: type] (a: array[T])

            where T has lt: proctype (T, T) returns (bool)

This header constrains parameter T to be a type with an operation named lt with the indicated signature; the specification of sort would explain that lt must be a "less than" test. An example of a

call of sort is

    sort [int] (x)

In compiling such a call, the compiler checks that the type of x is array[int] and furthermore that int has an operation named lt with the required signature. We could even define a more polymorphic sorting routine in CLU that would work for all collections that are "array-like," i.e., whose elements can be fetched and stored by index.

The behavior requirements must be checked by some form of program verification.  The required behavior must be part of the specification, and the specification goes in different places in the two methods. With hierarchy, the specification belongs to the supertype; with related types, it is part of the specification of the polymorphic module.  Behavior checking also happens at different times. With hierarchy, checking happens whenever a programmer makes a type a subtype of the supertype; with grouping, it happens whenever a programmer writes code that uses the polymorphic module.

Both grouping and hierarchy have limitations. More flexibility in the use of the polymorphic module is desirable. For example, if sort is called with an operation that does a "greater than" test, this will lead to a different sorting of the array, but that different sorting may be just what is wanted. In addition, there may be conflicts between the types intended for use in the polymorphic module:

1.Not all types provide the required operation.

2.Types use different names for the operation.

3.Some type uses the name of the required operation for some other operation; e.g., the name lt is used in type T to identify the "length" operation.

One way of achieving more generality is to simply pass the needed operations as procedure arguments, e.g., sort actually takes two arguments, the array, and the routine used to determine ordering.     * Of course, this solution would not work well in Smalltalk because procedures cannot conveniently be

defined as individual entities nor treated as objects. This method is general, but can be inconvenient.

Methods that avoid the inconvenience in conjunction with the grouping approach exist in Argus One way of achieving more generality is to simply pass the needed operations as procedure arguments, e.g., sort actually takes two arguments, the array, and the routine used to determine ordering. This method is general, but can be inconvenient. Methods that avoid the inconvenience in conjunction with the grouping approach exist in Argus [10] and Ada.

In summary, when related types are discovered early in design, hierarchy is a good way to express the relationship. Otherwise, either the grouping approach (with suitable language support) or procedures as arguments may be better.

5.2.  Multiple Implementations

It is often useful to have multiple implementations of the same type. For example, for some matrices we use a sparse representation and for others a nonsparse representation. Furthermore, it is sometimes desirable to use objects of the same type but different representations within the same program.

Object-oriented languages appear to allow users to simulate multiple implementations with inheritance. Each implementation would be a subclass of another class that implements the type. This latter class would probably be virtual; for example, there would be a virtual class implementing matrices, and subclasses implementing sparse and nonsparse matrices.

Using inheritance in this way allows us to have several implementations of the same type in use within the same program, but it interferes with type hierarchy. For example, suppose we invent a subtype of matrices called extended-matrices.  We would like to implement extended-matrices with a class that inherits from matrices rather than from a particular implementation of matrices, since this would allow us to combine it with either matrix implementation.  This is not possible, however.  Instead, the extended-matrix class must explicitly state in its program text that it is a subclass of sparse or nonsparse matrices.

The problem arises because inheritance is being used for two different things: to implement a type and to indicate that one type is a subtype of another. These uses should be kept separate. Then we could have what we really want: two types (matrix and extended-matrix), one a subtype of the other, each having several implementations, and the ability to combine the implementations of the subtype with those of the supertype in various ways.

6.Conclusions

Abstraction, and especially data abstraction, is an important technique for developing programs that are reasonably easy to maintain and to modify as requirements change. Data abstractions are particularly important because they hide complicated things (data structures) that are likely to change in the future. They permit the representation of data to be changed locally without affecting programs that use the data.

Inheritance is an implementation mechanism that allows one type to be related to another hierarchically.  It is used in two ways: to implement a type by derivation from the implementation of another type, and to define subtypes.  We argued that the first use is uninteresting because we can achieve the same result by using one type as the rep of the other. Subtypes, on the other hand, do add a new ability. Three uses for subtypes were identified. During incremental design they provide a way to limit the impact of design changes and to organize the design documentation. They also provide a way to group related types, especially in the case where the supertype is invented before any subtypes. When the relationship is discovered after several types have already been defined, other methods, such as grouping or procedure arguments, are probably better than hierarchy.  Finally, hierarchy is a convenient and sensible way of organizing a library of types. The hierarchy is either a subtype hierarchy, or almost one; the subtypes may not match our strict definition, but are similar to the supertype in some intuitive sense.

Inheritance can be used to implement a subtype hierarchy. It is needed primarily in the case of related types when the supertype is invented first, because here it is convenient to implement common features just once in the superclass, and then implement the extensions separately for each subtype.

We conclude that although data abstraction is more important, type hierarchy does extend its usefulness.  Furthermore, inheritance is sometimes needed to express type hierarchy and is therefore a useful mechanism to provide in a programming language.

Acknowledgments

Many people made comments and suggestions that improved the content of this paper.  The author gratefully acknowledges this help, and especially the efforts of Toby Bloom and Gary Leavens.


REFERENCES

1.Bobrow, D., et al. "CommonLoops: Merging Lisp and Object-Oriented Programming". Proc. of the ACM Conference on Object-Oriented Programming Systems, Languages and Applications, SIGPLAN Notices 21, 11 (November 1986).

2.Bruce, K., and Wegner, P. "An Algebraic Model of Subtypes in Object-Oriented Languages (Draft)". SIGPLAN Notices 21, 10 (October 1986).

3.Dahl, O.-J., and Hoare, C.A.R. Hierarchical Program Structures. In Structured Programming, Academic Press, 1972.

4.Goldberg, A., and Robson, D. Smalltalk-80: The Language and its Implementation. Addison-Wesley, Reading, Ma., 1983.

5.Hoare, C. A. R. "Proof of correctness of data representations". Acta Informatica 4 (1972), 271-281.

6.Leavens, G. Subtyping and Generic Invocation: Semantic and Language Design. Ph.D. Th., Massachusetts Institute of Technology, Department of Electrical Engineering and Computer Science, forthcoming.

7.Liskov, B. A Design Methodology for Reliable Software Systems. In Tutorial on Software Design Techniques, P. Freeman and A. Wasserman, Eds., IEEE, 1977. Also published in the Proc. of the Fall Joint Computer Conference, 1972.

8.Liskov, B., Snyder, A., Atkinson, R. R., and Schaffert, J. C. "Abstraction mechanisms in CLU". Comm. of the ACM 20, 8 (August 1977), 564-576.

9.Liskov, B., et al.. CLU Reference Manual. Springer-Verlag, 1984.

10.Liskov, B., et al. Argus Reference Manual. Technical Report MIT/LCS/TR-400, M.I.T. Laboratory for Computer Science, Cambridge, Ma., 1987.

11.Liskov, B., and Guttag, J.. Abstraction and Specification in Program Development. MIT Press and McGraw Hill, 1986.

12.Liskov, B., and Zilles, S. "Programming with abstract data types". Proc. of ACM SIGPLAN Conference on Very High Level Languages, SIGPLAN Notices 9 (1974).

13.Moon, D. "Object-Oriented Programming with Flavors". Proc. of the ACM Conference on Object-Oriented Programming Systems, Languages, and Applications, SIGPLAN Notices 21, 11 (November 1986).

14.Morris, J. H. "Protection in Programming Languages". Comm. of the ACM 16, 1 (January 1973).

15.Parnas, D. Information Distribution Aspects of Design Methodology. In Proceedings of IFIP Congress, North Holland Publishing Co., 1971.

16.Parnas, D. "On the Criteria to be Used in Decomposing Systems into Modules". Comm. of the ACM 15, 12 (December 1972).

17.Schaffert, C., et al. "An Introduction to Trellis/Owl". Proc. of the ACM Conference on Object-Oriented Programming Systems, Languages, and Applications, SIGPLAN Notices 21, 11 (November 1986).

18.Snyder, A. "Encapsulation and Inheritance in Object-Oriented Programming Languages". Proc. of the ACM Conference on Object-Oriented Programming Systems, Languages, and Applications, SIGPLAN Notices 21, 11 (November 1986).

19.19. U. S. Department of Defense. Reference manual for the Ada programming language.  1983. ANSI standard Ada.

NSIS完整实例(含服务的安装,运行前卸载)

# NetHalt - NSIS installer script # Copyright (C) 2008 Daniel Collins <solemnwarning@solemnwarning.net> # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # #    * Redistributions of source code must retain the above copyright #      notice, this list of conditions and the following disclaimer. # #    * Redistributions in binary form must reproduce the above copyright #      notice, this list of conditions and the following disclaimer in the #      documentation and/or other materials provided with the distribution. # #    * Neither the name of the author nor the names of its contributors may #      be used to endorse or promote products derived from this software #      without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. !include MUI2.nsh !include LogicLib.nsh !include nsDialogs.nsh Name "NetHalt" OutFile "nhclient.exe" InstallDir $PROGRAMFILES\NetHalt InstallDirRegKey HKLM "SOFTWARE\NetHalt" "InstallDir" !define MUI_ABORTWARNING !insertmacro MUI_PAGE_WELCOME !insertmacro MUI_PAGE_LICENSE "COPYING" !insertmacro MUI_PAGE_DIRECTORY !insertmacro MUI_PAGE_INSTFILES !insertmacro MUI_LANGUAGE "English" Function .onInit    nsisos::osversion    ${If} $0 < 5        MessageBox MB_OK "Windows 2000 (NT 5.0) or greater is required"        Abort    ${EndIf} FunctionEnd # Do local install # Section "NetHalt"    SetShellVarContext all    SetOutPath $INSTDIR    # Stop all running nhtray.exe processes    #    StrCpy $0 "nhtray.exe"    KillProc::KillProcesses    # Check if the NetHalt service is installed    #    SimpleSC::ExistsService "nhclient"    Pop $0    # Stop+Remove the NetHalt service if it's installed    #    ${If} $0 == 0        DetailPrint "Stopping NetHalt Client service..."        SimpleSC::StopService "nhclient"        DetailPrint "Removing NetHalt Client service..."        SimpleSC::RemoveService "nhclient"    ${EndIf}    WriteRegStr HKLM "SOFTWARE\NetHalt" "InstallDir" $INSTDIR    cinst::reg_write "dword" "SOFTWARE\NetHalt" "use_server" "0"    cinst::reg_write "string" "SOFTWARE\NetHalt" "server_name" ""    cinst::reg_write "dword" "SOFTWARE\NetHalt" "server_port" "0"    cinst::reg_write "dword" "SOFTWARE\NetHalt" "server_refresh" "1800"    cinst::reg_write "dword" "SOFTWARE\NetHalt" "warning" "300"    cinst::reg_write "dword" "SOFTWARE\NetHalt" "abort" "0"    cinst::reg_write "dword" "SOFTWARE\NetHalt" "delay" "0"    cinst::reg_write "string" "SOFTWARE\NetHalt" "sdtimes" ""    WriteUninstaller "$INSTDIR\uninstall.exe"    File "src\nhclient.exe"    File "src\evlog.dll"    File "src\nhtray.exe"    File "src\nhconfig.exe"    # Add the event log source (evlog.dll) to the registry    #    WriteRegStr HKLM "SYSTEM\CurrentControlSet\Services\Eventlog\Application\NetHalt Client" "EventMessageFile" "$INSTDIR\evlog.dll"    WriteRegDWORD HKLM "SYSTEM\CurrentControlSet\Services\Eventlog\Application\NetHalt Client" "TypesSupported" 0x00000007    # Add the uninstaller to Add/Remove programs    #    WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\NetHalt" "DisplayName" "NetHalt"    WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\NetHalt" "UninstallString" "$INSTDIR\uninstall.exe"    WriteRegDWORD HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\NetHalt" "NoModify" 1    WriteRegDWORD HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\NetHalt" "NoRepair" 1    # Install and start the NetHalt service    # TODO: Check for errors    #    DetailPrint "Installing NetHalt Client service..."    SimpleSC::InstallService "nhclient" "NetHalt Client" "16" "2" "$INSTDIR\nhclient.exe" "" "" ""    DetailPrint "Starting NetHalt Client service..."    SimpleSC::StartService "nhclient"    # Add nhtray.exe to the registry to run at login    #    WriteRegStr HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" "NetHalt" "$INSTDIR\nhtray.exe"    # Launch nhtray.exe    #    Exec '"$INSTDIR\nhtray.exe"'    # Create shortcuts    #    CreateDirectory "$SMPROGRAMS\NetHalt"    CreateShortCut "$SMPROGRAMS\NetHalt\Tray Icon.lnk" "$INSTDIR\nhtray.exe"    CreateShortCut "$SMPROGRAMS\NetHalt\Configuration.lnk" "$INSTDIR\nhconfig.exe"    CreateShortCut "$SMPROGRAMS\NetHalt\Uninstall.lnk" "$INSTDIR\uninstall.exe" SectionEnd Function un.onInit    SetShellVarContext all    ReadRegStr $INSTDIR HKLM "SOFTWARE\NetHalt" "InstallDir"    MessageBox MB_YESNO "This will uninstall NetHalt, continue?" IDYES NoAbort    Abort    NoAbort: FunctionEnd Section "Uninstall"    # Stop and remove the NetHalt service    #    DetailPrint "Stopping NetHalt Client service..."    SimpleSC::StopService "nhclient"    DetailPrint "Removing NetHalt Client service..."    SimpleSC::RemoveService "nhclient"    # Stop all running nhtray.exe processes    #    StrCpy $0 "nhtray.exe"    KillProc::KillProcesses    # Delete shortcuts    #    Delete "$SMPROGRAMS\NetHalt\Tray Icon.lnk"    Delete "$SMPROGRAMS\NetHalt\Configuration.lnk"    Delete "$SMPROGRAMS\NetHalt\Un-Install.lnk"    Delete "$SMPROGRAMS\NetHalt"    DeleteRegValue HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Run" "NetHalt"    DeleteRegKey HKLM "SYSTEM\CurrentControlSet\Services\Eventlog\Application\NetHalt Client"    DeleteRegKey HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\NetHalt"    Delete "$INSTDIR\nhclient.exe"    Delete "$INSTDIR\evlog.dll"    Delete "$INSTDIR\nhtray.exe"    Delete "$INSTDIR\uninstall.exe"    RMDir $INSTDIR SectionEnd

从命令行测量.NET程序性能

可以使用命令行工具收集有关应用程序的性能信息。

在本文所述的示例中,收集 Microsoft Notepad 的性能信息,但可以使用相同的方法来分析任何进程。

先决条件

  • Visual Studio 2019 或更高版本

  • 熟悉命令行工具

  • 若要在未安装 Visual Studio 的远程计算机上收集性能信息,请在此远程计算机上安装 Visual Studio 远程工具。 工具版本必须与 Visual Studio 版本匹配。

收集性能数据

使用 Visual Studio 诊断 CLI 工具进行性能分析的工作原理是将性能分析工具与其中某个收集器代理一起附加到进程。 附加性能分析工具时,将开始诊断捕获并存储分析数据的会话,直到该工具停止,此时数据将导出到 .diagsession 文件中。 然后,可以在 Visual Studio 中打开此文件以分析结果。

  1. 启动 Notepad,并打开任务管理器来获取其进程 ID (PID)。 在任务管理器中,找到“详细信息”选项卡中的 PID。

  2. 打开命令提示符,切换到包含集合代理可执行文件的目录,通常在此处(对于 Visual Studio Enterprise)。

    <Visual Studio installation folder>\2019\Enterprise\Team Tools\DiagnosticsHub\Collector\

  3. 通过键入以下命令,启动 VSDiagnostics.exe。

    cmd
    VSDiagnostics.exe start <id> /attach:<pid> /loadConfig:<configFile>

    必须包含的参数是:

    例如,可以将以下命令用于 CPUUsageBase 代理,方法是按之前所述替换 pid。

    cmd
    VSDiagnostics.exe start 1 /attach:<pid> /loadConfig:AgentConfigs\CPUUsageLow.json
    • <id> 标识收集会话。 ID 必须为介于 1 - 255 之间的数字。

    • <pid>,要分析的进程的 PID 在本例中是在步骤 1 中找到的 PID。

    • <configFile>,要启动的集合代理的配置文件。 有关详细信息,请参阅代理的配置文件

  4. 重设 Notepad 大小,或在其中键入内容,以确保收集一些有趣的分析信息。

  5. 通过键入以下命令,停止收集会话并将输出发送到文件。

    cmd
    VSDiagnostics.exe stop <id> /output:<path to file>
  6. 找到上一个命令的 .diagsession 文件输出,并在 Visual Studio 中打开它(“文件” > “打开”)以检查收集的信息 。

    要分析结果,请参阅相应的性能工具文档。 例如,这可能是 CPU 使用情况.NET 对象分配工具数据库工具。

代理配置文件

集合代理是可互换的组件,可根据要测量的内容收集不同类型的数据。

为方便起见,建议将该信息存储在代理配置文件中。 配置文件是至少包含 .dll 的名称及其 COM CLSID 的 .json 文件 。 以下是可以在以下文件夹中找到的示例配置文件:

<Visual Studio installation folder>Team Tools\DiagnosticsHub\Collector\AgentConfigs\

请参阅以下链接以下载和查看代理配置文件:

CpuUsage 配置(基本/高/低),对应于为 CPU 使用情况分析工具收集的数据。 DotNetObjectAlloc 配置(基本/低),对应于为 .NET 对象分配工具收集的数据。

基本/低/高配置是指采样率。 例如,低为 100 样本/秒,高为 4000 样本/秒。

要使 VSDiagnostics.exe 工具可用于集合代理,需要用于相应代理的 DLL 和 COM CLSID。 代理可能还具有其他配置选项,这可以是配置文件中指定的任何选项,格式为正确转义的 JSON。

权限

要分析需要提升权限的应用程序,必须从提升的命令提示符执行操作。

工具下载

下载 Visual Studio Tools - 免费安装 Windows、Mac、Linux (microsoft.com)

引用资料

从命令行测量性能 - Visual Studio (Windows) | Microsoft Docs

.NET程序的性能要领和优化建议

本文提供了一些性能优化的建议,这些经验来自于使用托管代码重写C# 和 VB编译器,并以编写C# 编译器中的一些真实场景作为例子来展示这些优化经验。.NET 平台开发应用程序具有极高的生产力。.NET 平台上强大安全的编程语言以及丰富的类库,使得开发应用变得卓有成效。但是能力越大责任越大。我们应该使用.NET框架的强大能力,但同时如果我们需要处理大量的数据比如文件或者数据库也需要准备对我们的代码进行调优。

为什么来自新的编译器的性能优化经验也适用于您的应用程序

微软使用托管代码重写了C#和Visual Basic的编译器,并提供了一些列新的API来进行代码建模和分析、开发编译工具,使得Visual Studio具有更加丰富的代码感知的编程体验。重写编译器,并且在新的编译器上开发Visual Studio的经验使得我们获得了非常有用的性能优化经验,这些经验也能用于大型的.NET应用,或者一些需要处理大量数据的APP上。你不需要了解编译器,也能够从C#编译器的例子中得出这些见解。

Visual Studio使用了编译器的API来实现了强大的智能感知(Intellisense)功能,如代码关键字着色,语法填充列表,错误波浪线提示,参数提示,代码问题及修改建议等,这些功能深受开发者欢迎。Visual Studio在开发者输入或者修改代码的时候,会动态的编译代码来获得对代码的分析和提示。

当用户和App进行交互的时候,通常希望软件具有好的响应性。输入或者执行命令的时候,应用程序界面不应该被阻塞。帮助或者提示能够迅速显示出来或者当用户继续输入的时候停止提示。现在的App应该避免在执行长时间计算的时候阻塞UI线程从而让用户感觉程序不够流畅。

想了解更多关于新的编译器的信息,可以访问 .NET Compiler Platform ("Roslyn")

基本要领

在对.NET 进行性能调优以及开发具有良好响应性的应用程序的时候,请考虑以下这些基本要领:

要领一:不要过早优化

编写代码比想象中的要复杂的多,代码需要维护,调试及优化性能。 一个有经验的程序员,通常会对自然而然的提出解决问题的方法并编写高效的代码。 但是有时候也可能会陷入过早优化代码的问题中。比如,有时候使用一个简单的数组就够了,非要优化成使用哈希表,有时候简单的重新计算一下可以,非要使用复杂的可能导致内存泄漏的缓存。发现问题时,应该首先测试性能问题然后再分析代码。

要领二:没有评测,便是猜测

剖析和测量不会撒谎。测评可以显示CPU是否满负荷运转或者是存在磁盘I/O阻塞。测评会告诉你应用程序分配了什么样的以及多大的内存,以及是否CPU花费了很多时间在垃圾回收上。

应该为关键的用户体验或者场景设置性能目标,并且编写测试来测量性能。通过使用科学的方法来分析性能不达标的原因的步骤如下:使用测评报告来指导,假设可能出现的情况,并且编写实验代码或者修改代码来验证我们的假设或者修正。如果我们设置了基本的性能指标并且经常测试,就能够避免一些改变导致性能的回退(regression),这样就能够避免我们浪费时间在一些不必要的改动中。

要领三:好工具很重要

好的工具能够让我们能够快速的定位到影响性能的最大因素(CPU,内存,磁盘)并且能够帮助我们定位产生这些瓶颈的代码。微软已经发布了很多性能测试工具比如:Visual Studio ProfilerWindows Phone Analysis Tool, 以及 PerfView.

PerfView是一款免费且性能强大的工具,他主要关注影响性能的一些深层次的问题(磁盘 I/O,GC 事件,内存),后面会展示这方面的例子。我们能够抓取性能相关的 Event Tracing for Windows(ETW)事件并能以应用程序,进程,堆栈,线程的尺度查看这些信息。PerfView能够展示应用程序分配了多少,以及分配了何种内存以及应用程序中的函数以及调用堆栈对内存分配的贡献。这些方面的细节,您可以查看随工具下载发布的关于PerfView的非常详细的帮助,Demo以及视频教程(比如Channel9 上的视频教程)

要领四:所有的都与内存分配相关

你可能会想,编写响应及时的基于.NET的应用程序关键在于采用好的算法,比如使用快速排序替代冒泡排序,但是实际情况并不是这样。编写一个响应良好的app的最大因素在于内存分配,特别是当app非常大或者处理大量数据的时候。

在使用新的编译器API开发响应良好的IDE的实践中,大部分工作都花在了如何避免开辟内存以及管理缓存策略。PerfView追踪显示新的C# 和VB编译器的性能基本上和CPU的性能瓶颈没有关系。编译器在读入成百上千甚至上万行代码,读入元数据活着产生编译好的代码,这些操作其实都是I/O bound 密集型。UI线程的延迟几乎全部都是由于垃圾回收导致的。.NET框架对垃圾回收的性能已经进行过高度优化,他能够在应用程序代码执行的时候并行的执行垃圾回收的大部分操作。但是,单个内存分配操作有可能会触发一次昂贵的垃圾回收操作,这样GC会暂时挂起所有线程来进行垃圾回收(比如 Generation 2型的垃圾回收)

常见的内存分配以及例子

这部分的例子虽然背后关于内存分配的地方很少。但是,如果一个大的应用程序执行足够多的这些小的会导致内存分配的表达式,那么这些表达式会导致几百M,甚至几G的内存分配。比如,在性能测试团队把问题定位到输入场景之前,一分钟的测试模拟开发者在编译器里面编写代码会分配几G的内存。

装箱

装箱发生在当通常分配在线程栈上或者数据结构中的值类型,或者临时的值需要被包装到对象中的时候(比如分配一个对象来存放数据,活着返回一个指针给一个Object对象)。.NET框架由于方法的签名或者类型的分配位置,有些时候会自动对值类型进行装箱。将值类型包装为引用类型会产生内存分配。.NET框架及语言会尽量避免不必要的装箱,但是有时候在我们没有注意到的时候会产生装箱操作。过多的装箱操作会在应用程序中分配成M上G的内存,这就意味着垃圾回收的更加频繁,也会花更长时间。

在PerfView中查看装箱操作,只需要开启一个追踪(trace),然后查看应用程序名字下面的GC Heap Alloc 项(记住,PerfView会报告所有的进程的资源分配情况),如果在分配相中看到了一些诸如System.Int32和System.Char的值类型,那么就发生了装箱。选择一个类型,就会显示调用栈以及发生装箱的操作的函数。

例1 string方法和其值类型参数

下面的示例代码演示了潜在的不必要的装箱以及在大的系统中的频繁的装箱操作。

public class Logger
{
    public static void WriteLine(string s)
    {
        /*...*/
    }
}
public class BoxingExample
{
    public void Log(int id, int size)
    {
        var s = string.Format("{0}:{1}", id, size);
        Logger.WriteLine(s);
    }
}

这是一个日志基础类,因此app会很频繁的调用Log函数来记日志,可能该方法会被调用millons次。问题在于,调用string.Format方法会调用其重载的接受一个string类型和两个Object类型的方法:

String.Format Method (String, Object, Object)

该重载方法要求.NET Framework 把int型装箱为object类型然后将它传到方法调用中去。为了解决这一问题,方法就是调用id.ToString()size.ToString()方法,然后传入到string.Format 方法中去,调用ToString()方法的确会导致一个string的分配,但是在string.Format方法内部不论怎样都会产生string类型的分配。

你可能会认为这个基本的调用string.Format 仅仅是字符串的拼接,所以你可能会写出这样的代码:

var s = id.ToString() + ':' + size.ToString();

实际上,上面这行代码也会导致装箱,因为上面的语句在编译的时候会调用:

string.Concat(Object, Object, Object);

这个方法,.NET Framework 必须对字符常量进行装箱来调用Concat方法。

解决方法:

完全修复这个问题很简单,将上面的单引号替换为双引号即将字符常量换为字符串常量就可以避免装箱,因为string类型的已经是引用类型了。

var s = id.ToString() + ":" + size.ToString();

例2 枚举类型的装箱

下面的这个例子是导致新的C# 和VB编译器由于频繁的使用枚举类型,特别是在Dictionary中做查找操作时分配了大量内存的原因。

public enum Color { Red, Green, Blue }
public class BoxingExample
{
    private string name;
    private Color color;
    public override int GetHashCode()
    {
        return name.GetHashCode() ^ color.GetHashCode();
    }
}

问题非常隐蔽,PerfView会告诉你enmu.GetHashCode()由于内部实现的原因产生了装箱操作,该方法会在底层枚举类型的表现形式上进行装箱,如果仔细看PerfView,会看到每次调用GetHashCode会产生两次装箱操作。编译器插入一次,.NET Framework插入另外一次。

解决方法:

通过在调用GetHashCode的时候将枚举的底层表现形式进行强制类型转换就可以避免这一装箱操作。

((int)color).GetHashCode()

另一个使用枚举类型经常产生装箱的操作时enum.HasFlag。传给HasFlag的参数必须进行装箱,在大多数情况下,反复调用HasFlag通过位运算测试非常简单和不需要分配内存。

要牢记基本要领第一条,不要过早优化。并且不要过早的开始重写所有代码。 需要注意到这些装箱的耗费,只有在通过工具找到并且定位到最主要问题所在再开始修改代码。

字符串

字符串操作是引起内存分配的最大元凶之一,通常在PerfView中占到前五导致内存分配的原因。应用程序使用字符串来进行序列化,表示JSON和REST。在不支持枚举类型的情况下,字符串可以用来与其他系统进行交互。当我们定位到是由于string操作导致对性能产生严重影响的时候,需要留意string类的Format(),Concat(),Split(),Join(),Substring()等这些方法。使用StringBuilder能够避免在拼接多个字符串时创建多个新字符串的开销,但是StringBuilder的创建也需要进行良好的控制以避免可能会产生的性能瓶颈。

例3 字符串操作

在C#编译器中有如下方法来输出方法前面的xml格式的注释。

public void WriteFormattedDocComment(string text)
{
    string[] lines = text.Split(new[] {"\r\n", "\r", "\n"},
        StringSplitOptions.None);
    int numLines = lines.Length;
    bool skipSpace = true;
    if (lines[0].TrimStart().StartsWith("///"))
    {
        for (int i = 0; i < numLines; i++)
        {
            string trimmed = lines[i].TrimStart();
            if (trimmed.Length < 4 || !char.IsWhiteSpace(trimmed[3]))
            {
                skipSpace = false;
                break;
            }
        }
        int substringStart = skipSpace ? 4 : 3;
        for (int i = 0; i < numLines; i++)
            Console.WriteLine(lines[i].TrimStart().Substring(substringStart));
    }
    else
    {
        /* ... */
    }
}

可以看到,在这片代码中包含有很多字符串操作。代码中使用类库方法来将行分割为字符串,来去除空格,来检查参数text是否是XML文档格式的注释,然后从行中取出字符串处理。

WriteFormattedDocComment方法每次被调用时,第一行代码调用Split()就会分配三个元素的字符串数组。编译器也需要产生代码来分配这个数组。因为编译器并不知道,如果Splite()存储了这一数组,那么其他部分的代码有可能会改变这个数组,这样就会影响到后面对WriteFormattedDocComment方法的调用。每次调用Splite()方法也会为参数text分配一个string,然后在分配其他内存来执行splite操作。

WriteFormattedDocComment方法中调用了三次TrimStart()方法,在内存环中调用了两次,这些都是重复的工作和内存分配。更糟糕的是,TrimStart()的无参重载方法的签名如下:

namespace System
{ 
    public class String 
    { 
        public string TrimStart(params char[] trimChars);
    }
}

该方法签名意味着,每次对TrimStart()的调用都回分配一个空的数组以及返回一个string类型的结果。

最后,调用了一次Substring()方法,这个方法通常会导致在内存中分配新的字符串。

解决方法:

和前面的只需要小小的修改即可解决内存分配的问题不同。在这个例子中,我们需要从头看,查看问题然后采用不同的方法解决。比如,可以意识到WriteFormattedDocComment()方法的参数是一个字符串,它包含了方法中需要的所有信息,因此,代码只需要做更多的index操作,而不是分配那么多小的string片段。

下面的方法并没有完全解,但是可以看到如何使用类似的技巧来解决本例中存在的问题。C#编译器使用如下的方式来消除所有的额外内存分配。

private int IndexOfFirstNonWhiteSpaceChar(string text, int start)
{
    while (start < text.Length && char.IsWhiteSpace(text[start])) 
        start++;
    return start;
}

private bool TrimmedStringStartsWith(string text, int start, string prefix)
{
    start = IndexOfFirstNonWhiteSpaceChar(text, start); 
    int len = text.Length - start; 
    if (len < prefix.Length) return false;
    for (int i = 0; i < len; i++)
    {
        if (prefix[i] != text[start + i]) 
            return false;
    }
    return true;
}

WriteFormattedDocComment() 方法的第一个版本分配了一个数组,几个子字符串,一个trim后的子字符串,以及一个空的params数组。也检查了”///”。修改后的代码仅使用了index操作,没有任何额外的内存分配。它查找第一个非空格的字符串,然后逐个字符串比较来查看是否以”///”开头。和使用TrimStart()不同,修改后的代码使用IndexOfFirstNonWhiteSpaceChar方法来返回第一个非空格的开始位置,通过使用这种方法,可以移除WriteFormattedDocComment()方法中的所有额外内存分配。

例4 StringBuilder

本例中使用StringBuilder。下面的函数用来产生泛型类型的全名:

public class Example 
{ 
    // Constructs a name like "SomeType<T1, T2, T3>" 
    public string GenerateFullTypeName(string name, int arity) 
    { 
        StringBuilder sb = new StringBuilder();
        sb.Append(name);
        if (arity != 0)
        { 
            sb.Append("<");
            for (int i = 1; i < arity; i++)
            {
                sb.Append("T"); sb.Append(i.ToString()); sb.Append(", ");
            } 
            sb.Append("T"); sb.Append(i.ToString()); sb.Append(">");
        }
        return sb.ToString(); 
    }
}

注意力集中到StringBuilder实例的创建上来。代码中调用sb.ToString()会导致一次内存分配。在StringBuilder中的内部实现也会导致内部内存分配,但是我们如果想要获取到string类型的结果化,这些分配无法避免。

解决方法:

要解决StringBuilder对象的分配就使用缓存。即使缓存一个可能被随时丢弃的单个实例对象也能够显著的提高程序性能。下面是该函数的新的实现。除了下面两行代码,其他代码均相同

// Constructs a name like "Foo<T1, T2, T3>" 
public string GenerateFullTypeName(string name, int arity)
{
    StringBuilder sb = AcquireBuilder(); /* Use sb as before */ 
    return GetStringAndReleaseBuilder(sb);
}

关键部分在于新的 AcquireBuilder()GetStringAndReleaseBuilder()方法:

[ThreadStatic]
private static StringBuilder cachedStringBuilder;

private static StringBuilder AcquireBuilder()
{
    StringBuilder result = cachedStringBuilder;
    if (result == null)
    {
        return new StringBuilder();
    } 
    result.Clear(); 
    cachedStringBuilder = null; 
    return result;
}

private static string GetStringAndReleaseBuilder(StringBuilder sb)
{
    string result = sb.ToString(); 
    cachedStringBuilder = sb; 
    return result;
}

上面方法实现中使用了thread-static字段来缓存StringBuilder对象,这是由于新的编译器使用了多线程的原因。很可能会忘掉这个ThreadStatic声明。Thread-static字符为每个执行这部分的代码的线程保留一个唯一的实例。

如果已经有了一个实例,那么AcquireBuilder()方法直接返回该缓存的实例,在清空后,将该字段或者缓存设置为null。否则AcquireBuilder()创建一个新的实例并返回,然后将字段和cache设置为null 。

当我们对StringBuilder处理完成之后,调用GetStringAndReleaseBuilder()方法即可获取string结果。然后将StringBuilder保存到字段中或者缓存起来,然后返回结果。这段代码很可能重复执行,从而创建多个StringBuilder对象,虽然很少会发生。代码中仅保存最后被释放的那个StringBuilder对象来留作后用。新的编译器中,这种简单的的缓存策略极大地减少了不必要的内存分配。.NET Framework 和MSBuild 中的部分模块也使用了类似的技术来提升性能。

简单的缓存策略必须遵循良好的缓存设计,因为他有大小的限制cap。使用缓存可能比之前有更多的代码,也需要更多的维护工作。我们只有在发现这是个问题之后才应该采缓存策略。PerfView已经显示出StringBuilder对内存的分配贡献相当大。

LINQ和Lambdas表达式

使用LINQ 和Lambdas表达式是C#语言强大生产力的一个很好体现,但是如果代码需要执行很多次的时候,可能需要对LINQ或者Lambdas表达式进行重写。

例5 Lambdas表达式,List<T>,以及IEnumerable<T>

下面的例子使用LINQ以及函数式风格的代码来通过编译器模型给定的名称来查找符号。

class Symbol 
{ 
    public string Name { get; private set; } /*...*/
}
class Compiler 
{ 
    private List<Symbol> symbols; 
    public Symbol FindMatchingSymbol(string name) 
    { 
        return symbols.FirstOrDefault(s => s.Name == name); 
    }
}

新的编译器和IDE 体验基于调用FindMatchingSymbol,这个调用非常频繁,在此过程中,这么简单的一行代码隐藏了基础内存分配开销。为了展示这其中的分配,我们首先将该单行函数拆分为两行:

Func<Symbol, bool> predicate = s => s.Name == name; 
return symbols.FirstOrDefault(predicate);

第一行中,lambda表达式 “s=>s.Name==name” 是对本地变量name的一个闭包。这就意味着需要分配额外的对象来为委托对象predict分配空间,需要一个分配一个静态类来保存环境从而保存name的值。编译器会产生如下代码:

// Compiler-generated class to hold environment state for lambda 
private class Lambda1Environment 
{ 
    public string capturedName; 
    public bool Evaluate(Symbol s) 
    { 
        return s.Name == this.capturedName;
    } 
}

// Expanded Func<Symbol, bool> predicate = s => s.Name == name; 
Lambda1Environment l = new Lambda1Environment() 
{ 
    capturedName = name
}; 
var predicate = new Func<Symbol, bool>(l.Evaluate);

两个new操作符(第一个创建一个环境类,第二个用来创建委托)很明显的表明了内存分配的情况。

现在来看看FirstOrDefault方法的调用,他是IEnumerable<T>类的扩展方法,这也会产生一次内存分配。因为FirstOrDefault使用IEnumerable<T>作为第一个参数,可以将上面的展开为下面的代码:

// Expanded return symbols.FirstOrDefault(predicate) ... 
IEnumerable<Symbol> enumerable = symbols;
IEnumerator<Symbol> enumerator = enumerable.GetEnumerator(); 
while (enumerator.MoveNext())
{ 
    if (predicate(enumerator.Current)) 
        return enumerator.Current; 
} 
return default(Symbol);

symbols变量是类型为List<T>的变量。List<T>集合类型实现了IEnumerable<T>即可并且清晰地定义了一个迭代器List<T>的迭代器使用了一种结构体来实现。使用结构而不是类意味着通常可以避免任何在托管堆上的分配,从而可以影响垃圾回收的效率。枚举典型的用处在于方便语言层面上使用foreach循环,他使用enumerator结构体在调用推栈上返回。递增调用堆栈指针来为对象分配空间,不会影响GC对托管对象的操作。

在上面的展开FirstOrDefault调用的例子中,代码会调用IEnumerabole<T>接口中的GetEnumerator()方法。将symbols赋值给IEnumerable<Symbol>类型的enumerable 变量,会使得对象丢失了其实际的List<T>类型信息。这就意味着当代码通过enumerable.GetEnumerator()方法获取迭代器时,.NET Framework 必须对返回的值(即迭代器,使用结构体实现)类型进行装箱从而将其赋给IEnumerable<Symbol>类型的(引用类型) enumerator变量。

解决方法:

解决办法是重写FindMatchingSymbol方法,将单个语句使用六行代码替代,这些代码依旧连贯,易于阅读和理解,也很容易实现。

public Symbol FindMatchingSymbol(string name) 
{ 
    foreach (Symbol s in symbols)
    { 
        if (s.Name == name) 
            return s; 
    } 
    return null; 
}

代码中并没有使用LINQ扩展方法,lambdas表达式和迭代器,并且没有额外的内存分配开销。这是因为编译器看到symbol 是List<T>类型的集合,因为能够直接将返回的结构性的枚举器绑定到类型正确的本地变量上,从而避免了对struct类型的装箱操作。原先的代码展示了C#语言丰富的表现形式以及.NET Framework 强大的生产力。该着后的代码则更加高效简单,并没有添加复杂的代码而增加可维护性。

Aync异步

接下来的例子展示了当我们试图缓存一部方法返回值时的一个普遍问题:

例6 缓存异步方法

Visual Studio IDE 的特性在很大程度上建立在新的C#和VB编译器获取语法树的基础上,当编译器使用async的时候仍能够保持Visual Stuido能够响应。下面是获取语法树的第一个版本的代码:

class Parser 
{
    /*...*/ 
    public SyntaxTree Syntax
    { 
        get; 
    } 
    
    public Task ParseSourceCode() 
    {
        /*...*/ 
    } 
}
class Compilation 
{ 
    /*...*/ 
    public async Task<SyntaxTree> GetSyntaxTreeAsync() 
    { 
        var parser = new Parser(); // allocation 
        await parser.ParseSourceCode(); // expensive 
        return parser.Syntax;
    } 
}

可以看到调用GetSyntaxTreeAsync() 方法会实例化一个Parser对象,解析代码,然后返回一个Task<SyntaxTree>对象。最耗性能的地方在为Parser实例分配内存并解析代码。方法中返回一个Task对象,因此调用者可以await解析工作,然后释放UI线程使得可以响应用户的输入。

由于Visual Studio的一些特性可能需要多次获取相同的语法树, 所以通常可能会缓存解析结果来节省时间和内存分配,但是下面的代码可能会导致内存分配:

class Compilation 
{ /*...*/
    private SyntaxTree cachedResult;
    public async Task<SyntaxTree> GetSyntaxTreeAsync() 
    { 
        if (this.cachedResult == null) 
        { 
            var parser = new Parser(); // allocation 
            await parser.ParseSourceCode(); // expensive 
            this.cachedResult = parser.Syntax; 
        } 
        return this.cachedResult;
    }
}

代码中有一个SynataxTree类型的名为cachedResult的字段。当该字段为空的时候,GetSyntaxTreeAsync()执行,然后将结果保存在cache中。GetSyntaxTreeAsync()方法返回SyntaxTree对象。问题在于,当有一个类型为Task<SyntaxTree> 类型的async异步方法时,想要返回SyntaxTree的值,编译器会生出代码来分配一个Task来保存执行结果(通过使用Task<SyntaxTree>.FromResult())。Task会标记为完成,然后结果立马返回。分配Task对象来存储执行的结果这个动作调用非常频繁,因此修复该分配问题能够极大提高应用程序响应性。

解决方法:

要移除保存完成了执行任务的分配,可以缓存Task对象来保存完成的结果。

class Compilation 
{ /*...*/
    private Task<SyntaxTree> cachedResult;
    public Task<SyntaxTree> GetSyntaxTreeAsync() 
    { 
        return this.cachedResult ?? (this.cachedResult = GetSyntaxTreeUncachedAsync()); 
    }
    private async Task<SyntaxTree> GetSyntaxTreeUncachedAsync() 
    {
        var parser = new Parser(); // allocation 
        await parser.ParseSourceCode(); // expensive 
        return parser.Syntax; 
    } 
}

代码将cachedResult 类型改为了Task<SyntaxTree> 并且引入了async帮助函数来保存原始代码中的GetSyntaxTreeAsync()函数。GetSyntaxTreeAsync函数现在使用 null操作符,来表示当cachedResult不为空时直接返回,为空时GetSyntaxTreeAsync调用GetSyntaxTreeUncachedAsync()然后缓存结果。注意GetSyntaxTreeAsync并没有await调用GetSyntaxTreeUncachedAsync。没有使用await意味着当GetSyntaxTreeUncachedAsync返回Task类型时,GetSyntaxTreeAsync 也立即返回Task, 现在缓存的是Task,因此在返回缓存结果的时候没有额外的内存分配。

其他一些影响性能的杂项

在大的app或者处理大量数据的app中,还有几点可能会引发潜在的性能问题。

字典

在很多应用程序中,Dictionary用的很广,虽然字非常方便和高校,但是经常会使用不当。在Visual Studio以及新的编译器中,使用性能分析工具发现,许多dictionay只包含有一个元素或者干脆是空的。一个空的Dictionay结构内部会有10个字段在x86机器上的托管堆上会占据48个字节。当需要在做映射或者关联数据结构需要事先常量时间查找的时候,字典非常有用。但是当只有几个元素,使用字典就会浪费大量内存空间。相反,我们可以使用List<KeyValuePair<K,V>>结构来实现便利,对于少量元素来说,同样高校。如果仅仅使用字典来加载数据,然后读取数据,那么使用一个具有N(log(N))的查找效率的有序数组,在速度上也会很快,当然这些都取决于的元素的个数。

类和结构

不甚严格的讲,在优化应用程序方面,类和结构提供了一种经典的空间/时间的权衡(trade off)。在x86机器上,每个类即使没有任何字段,也会分配12 byte的空间 (译注:来保存类型对象指针和同步索引块),但是将类作为方法之间参数传递的时候却十分高效廉价,因为只需要传递指向类型实例的指针即可。结构体如果不撞向的话,不会再托管堆上产生任何内存分配,但是当将一个比较大的结构体作为方法参数或者返回值得时候,需要CPU时间来自动复制和拷贝结构体,然后将结构体的属性缓存到本地便两种以避免过多的数据拷贝。

缓存

性能优化的一个常用技巧是缓存结果。但是如果缓存没有大小上限或者良好的资源释放机制就会导致内存泄漏。在处理大数据量的时候,如果在缓存中缓存了过多数据就会占用大量内存,这样导致的垃圾回收开销就会超过在缓存中查找结果所带来的好处。

结论

在大的系统,或者或者需要处理大量数据的系统中,我们需要关注产生性能瓶颈症状,这些问题再规模上会影响app的响应性,如装箱操作、字符串操作、LINQ和Lambda表达式、缓存async方法、缓存缺少大小限制以及良好的资源释放策略、使用Dictionay不当、以及到处传递结构体等。在优化我们的应用程序的时候,需要时刻注意之前提到过的四点:

  1. 不要进行过早优化——在定位和发现问题之后再进行调优。

  2. 专业测试不会说谎——没有评测,便是猜测。

  3. 好工具很重要。——下载PerfView,然后去看使用教程。

  4. 内存分配决定app的响应性。——这也是新的编译器性能团队花的时间最多的地方。

参考资料

C#基础知识之事件和委托

本文中,我将通过两个范例由浅入深地讲述什么是委托、为什么要使用委托、委托的调用方式、事件的由来、.Net Framework中的委托和事件、委托和事件对Observer设计模式的意义,对它们的中间代码也做了讨论。

一、委托是什么

来看下面这两个最简单的方法,它们不过是在屏幕上输出一句问候的话语:

public void GreetPeople(string name)

{

  EnglishGreeting(name);

}

public void EnglishGreeting(string name)

{

   Console.WriteLine("Morning, " + name);

}

现在假设这个程序需要进行全球化,哎呀,不好了,我是中国人,我不明白“Morning”是什么意思,怎么办呢?好吧,我们再加个中文版的问候方法:

1

2

3

4

public void ChineseGreeting(string name)

{

   Console.WriteLine("早上好, " + name);

}

这时候,GreetPeople也需要改一改了,不然如何判断到底用哪个版本的Greeting问候方法合适呢?在进行这个之前,我们最好再定义一个枚举作为判断的依据:

public enum Language

{

   English, Chinese

}

public void GreetPeople(string name, Language lang)

  swith(lang){

     case Language.English:

      EnglishGreeting(name);

      break;

    case Language.Chinese:

      ChineseGreeting(name);

      break;

   }

}

这个解决方案的可扩展性很差,如果日后我们需要再添加韩文版、日文版,就不得不反复修改枚举和GreetPeople()方法,以适应新的需求。

在考虑新的解决方案之前,我们先看看 GreetPeople的方法签名:

1

public void GreetPeople(string name,   Language lang)

如果你再仔细想想,假如GreetPeople()方法可以接受一个参数变量,这个变量可以代表另一个方法,当我们给这个变量赋值 EnglishGreeting的时候,它代表着 EnglsihGreeting() 这个方法;当我们给它赋值ChineseGreeting 的时候,它又代表着ChineseGreeting()方法。我们将这个参数变量命名为 MakeGreeting,那么不是可以如同给name赋值时一样,在调用 GreetPeople()方法的时候,给这个MakeGreeting 参数也赋上值么(ChineseGreeting或者EnglsihGreeting等)?然后,我们在方法体内,也可以像使用别的参数一样使用MakeGreeting。好了,有了思路了,我们现在就来改改GreetPeople()方法,那么它应该是这个样子了:

1

public void GreetPeople(string name, ***   MakeGreeting) {    MakeGreeting(name); }

注意到 *** ,这个位置通常放置的应该是参数的类型,但到目前为止,我们仅仅是想到应该有个可以代表方法的参数,并按这个思路去改写GreetPeople方法,现在就出现了一个大问题:这个代表着方法的MakeGreeting参数应该是什么类型的?

聪明的你应该已经想到了,现在是委托该出场的时候了,但讲述委托之前,我们再看看MakeGreeting参数所能代表的 ChineseGreeting()和EnglishGreeting()方法的签名:

1

public void EnglishGreeting(string name) public void ChineseGreeting(string name)  

MakeGreeting的 参数类型定义 应该能够确定 MakeGreeting可以代表的方法种类,于是,委托出现了:它定义了MakeGreeting参数所能代表的方法的种类,也就是MakeGreeting参数的类型。

本例中委托的定义:

1

public delegate void GreetingDelegate(string name);

可以与上面EnglishGreeting()方法的签名对比一下,除了加入了delegate关键字以外,其余的是不是完全一样?

现在,让我们再次改动GreetPeople()方法,如下所示:

1

public void GreetPeople(string name,   GreetingDelegate MakeGreeting) {    MakeGreeting(name); }

如你所见,委托GreetingDelegate出现的位置与 string相同,string是一个类型,那么GreetingDelegate应该也是一个类型,或者叫类(Class)。但是委托的声明方式和类却完全不同,这是怎么一回事?实际上,委托在编译的时候确实会编译成类。因为Delegate是一个类,所以在任何可以声明类的地方都可以声明委托。看看这个范例的完整代码:

using System;

 using System.Collections.Generic;

 using System.Text;

 

 namespace Delegate {

   public delegate void GreetingDelegate(string name);

     class Program {

      private static void EnglishGreeting(string name) {

        Console.WriteLine("Morning, " + name);

      }

 

      private static void ChineseGreeting(string name) {

        Console.WriteLine("早上好, " + name);

      }

 

      //注意此方法,它接受一个GreetingDelegate类型的方法作为参数

      private static void GreetPeople(string name, GreetingDelegate MakeGreeting) {

        MakeGreeting(name);

       }

 

      static void Main(string[] args) {

        GreetPeople("Jimmy Zhang", EnglishGreeting);

        GreetPeople("张子阳", ChineseGreeting);

        Console.ReadKey();

      }

     }

   }

输出如下:

Morning, Jimmy Zhang

早上好, 张子阳

我们现在对委托做一个总结:

委托是一个类,它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法,可以避免在程序中大量使用If-Else(Switch)语句,同时使得程序具有更好的可扩展性。

二、委托的多播

委托可以将多个方法赋给同一个委托,或者叫将多个方法绑定到同一个委托,当调用这个委托的时候,将依次调用其所绑定的方法。在这个例子中,语法如下:

static void Main(string[] args) {

   GreetingDelegate delegate1;

   delegate1 = EnglishGreeting; // 先给委托类型的变量赋值

   delegate1 += ChineseGreeting;  // 给此委托变量再绑定一个方法

   // 将先后调用 EnglishGreeting ChineseGreeting 方法

   delegate1 ("Jimmy Zhang"); 

   Console.ReadKey();

}

NOTE:第一次用的“=”,是赋值的语法;第二次,用的是“+=”,是绑定的语法。如果第一次就使用“+=”,将出现“使用了未赋值的局部变量”的编译错误。我们也可以使用下面的代码来这样简化这一过程:

1

2

GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting);

delegate1 += ChineseGreeting;   // 给此委托变量再绑定一个方法

看到这里,应该注意到,这段代码第一条语句与实例化一个类是何其的相似,你不禁想到:上面第一次绑定委托时不可以使用“+=”的编译错误,或许可以用这样的方法来避免:代码如下:

1

2

3

GreetingDelegate delegate1 = new GreetingDelegate();

delegate1 += EnglishGreeting;   // 这次用的是 “+=”,绑定语法。

delegate1 += ChineseGreeting;   // 给此委托变量再绑定一个方法

但实际上,这样会出现编译错误: “GreetingDelegate”方法没有采用“0”个参数的重载。尽管这样的结果让我们觉得有点沮丧,但是编译的提示:“没有0个参数的重载”再次让我们联想到了类的构造函数。我知道你一定按捺不住想探个究竟,但再此之前我们需要先把基础知识和应用介绍完。既然给委托可以绑定一个方法,那么也应该有办法取消对方法的绑定,很容易想到,这个语法是“-=”:

static void Main(string[] args) {

   GreetingDelegate delegate1 = new GreetingDelegate(EnglishGreeting);

   delegate1 += ChineseGreeting;  // 给此委托变量再绑定一个方法

 

  // 将先后调用 EnglishGreeting ChineseGreeting 方法

   GreetPeople("Jimmy Zhang", delegate1);

   Console.WriteLine();

 

   delegate1 -= EnglishGreeting; //取消对EnglishGreeting方法的绑定

  // 将仅调用 ChineseGreeting

  GreetPeople("张子阳", delegate1);

  Console.ReadKey();

}

输出为:

Morning, Jimmy Zhang

早上好, Jimmy Zhang

早上好, 张子阳

 

三、事件的由来

我们继续思考上面的程序:上面的三个方法都定义在Program类中,这样做是为了理解的方便,实际应用中,通常都是 GreetPeople 在一个类中,ChineseGreeting和 EnglishGreeting 在另外的类中。现在你已经对委托有了初步了解,是时候对上面的例子做个改进了。假设我们将GreetingPeople()放在一个叫GreetingManager的类中,那么新程序应该是这个样子的:

namespace Delegate {

   //定义委托,它定义了可以代表的方法的类型

  public delegate void GreetingDelegate(string name);   

   //新建的GreetingManager

  public class GreetingManager{

    public void GreetPeople(string name, GreetingDelegate MakeGreeting) {

      MakeGreeting(name);

    }

   }

 

   class Program {

    private static void EnglishGreeting(string name) {

      Console.WriteLine("Morning, " + name);

    }

 

    private static void ChineseGreeting(string name) {

      Console.WriteLine("早上好, " + name);

    }

 

    static void Main(string[] args) {

       GreetingManager gm = new GreetingManager();

       gm.GreetPeople("Jimmy Zhang", EnglishGreeting);

       gm.GreetPeople("张子阳", ChineseGreeting);

     }

   }

}

 输出结果:

  Morning, Jimmy Zhang

  早上好, 张子阳

现在,假设我们需要使用上一节学到的知识,将多个方法绑定到同一个委托变量,该如何做呢?让我们再次改写代码:

static void Main(string[] args) {

   GreetingManager gm = new GreetingManager();

   GreetingDelegate delegate1;

   delegate1 = EnglishGreeting;

   delegate1 += ChineseGreeting;

   gm.GreetPeople("Jimmy Zhang", delegate1);

 }

输出:

Morning, Jimmy Zhang

早上好, Jimmy Zhang

到了这里,我们不禁想到:面向对象设计,讲究的是对象的封装,既然可以声明委托类型的变量,我们何不将这个变量封装到 GreetManager类中?在这个类的客户端中使用不是更方便么?于是,我们改写GreetManager类,像这样:

public class GreetingManager{

   //GreetingManager类的内部声明delegate1变量

  public GreetingDelegate delegate1; 

   public void GreetPeople(string name, GreetingDelegate MakeGreeting) {

    MakeGreeting(name);

   }

 }

现在,我们可以这样使用这个委托变量:

static void Main(string[] args) {

   GreetingManager gm = new GreetingManager();

   gm.delegate1 = EnglishGreeting;

   gm.delegate1 += ChineseGreeting;

   gm.GreetPeople("Jimmy Zhang", gm.delegate1);

 }

 

输出为:

Morning, Jimmy Zhang

早上好, Jimmy Zhang

尽管这样做没有任何问题,但我们发现这条语句很奇怪。在调用gm.GreetPeople方法的时候,再次传递了gm的delegate1字段。既然如此,我们何不修改 GreetingManager 类成这样:

public class GreetingManager{

   //GreetingManager类的内部声明delegate1变量

   public GreetingDelegate delegate1;

 

   public void GreetPeople(string name) {

     if(delegate1!=null){   //如果有方法注册委托变量

     delegate1(name);   //通过委托调用方法

    }

   }

 }

在客户端,调用看上去更简洁一些:

static void Main(string[] args) {

   GreetingManager gm = new GreetingManager();

   gm.delegate1 = EnglishGreeting;

   gm.delegate1 += ChineseGreeting;

 

   gm.GreetPeople("Jimmy Zhang");   //注意,这次不需要再传递 delegate1变量

}

 

输出为:

Morning, Jimmy Zhang

早上好, Jimmy Zhang

尽管这样达到了我们要的效果,但是还是存在着问题:

在这里,delegate1和我们平时用的string类型的变量没有什么分别,而我们知道,并不是所有的字段都应该声明成public,合适的做法是应该public的时候public,应该private的时候private。

我们先看看如果把 delegate1 声明为 private会怎样?结果就是:这简直就是在搞笑。因为声明委托的目的就是为了把它暴露在类的客户端进行方法的注册,你把它声明为private了,客户端对它根本就不可见,那它还有什么用?

再看看把delegate1 声明为 public 会怎样?结果就是:在客户端可以对它进行随意的赋值等操作,严重破坏对象的封装性。

最后,第一个方法注册用“=”,是赋值语法,因为要进行实例化,第二个方法注册则用的是“+=”。但是,不管是赋值还是注册,都是将方法绑定到委托上,除了调用时先后顺序不同,再没有任何的分别,这样不是让人觉得很别扭么?

于是,Event出场了,它封装了委托类型的变量,使得:在类的内部,不管你声明它是public还是protected,它总是private的。在类的外部,注册“+=”和注销“-=”的访问限定符与你在声明事件时使用的访问符相同。

我们改写GreetingManager类,它变成了这个样子:

public class GreetingManager{

   //这一次我们在这里声明一个事件

   public event GreetingDelegate MakeGreet;

 

   public void GreetPeople(string name) {

     MakeGreet(name);

   }

 }

很容易注意到:MakeGreet 事件的声明与之前委托变量delegate1的声明唯一的区别是多了一个event关键字。看到这里,在结合上面的讲解,你应该明白到:事件其实没什么不好理解的,声明一个事件不过类似于声明一个进行了封装的委托类型的变量而已。

为了证明上面的推论,如果我们像下面这样改写Main方法:

static void Main(string[] args) {

   GreetingManager gm = new GreetingManager();

   gm.MakeGreet = EnglishGreeting;     // 编译错误1

   gm.MakeGreet += ChineseGreeting;

   gm.GreetPeople("Jimmy Zhang");

 }

会得到编译错误:事件“Delegate.GreetingManager.MakeGreet”只能出现在 += 或 -= 的左边(从类型“Delegate.GreetingManager”中使用时除外)。

事件和委托的编译代码

这时候,我们注释掉编译错误的行,然后重新进行编译,再借助Reflactor来对 event的声明语句做一探究,看看为什么会发生这样的错误:

public event GreetingDelegate MakeGreet;

 

 可以看到,实际上尽管我们在GreetingManager里将 MakeGreet 声明为public,但是,实际上MakeGreet会被编译成 私有字段,难怪会发生上面的编译错误了,因为它根本就不允许在GreetingManager类的外面以赋值的方式访问,从而验证了我们上面所做的推论。

我们再进一步看下MakeGreet所产生的代码:

private GreetingDelegate MakeGreet; //对事件的声明 实际是 声明一个私有的委托变量

[MethodImpl(MethodImplOptions.Synchronized)]

public void add_MakeGreet(GreetingDelegate value){

   this.MakeGreet = (GreetingDelegate) Delegate.Combine(this.MakeGreet, value);

}

[MethodImpl(MethodImplOptions.Synchronized)]

public void remove_MakeGreet(GreetingDelegate value){

   this.MakeGreet = (GreetingDelegate) Delegate.Remove(this.MakeGreet, value);

}

现在已经很明确了:MakeGreet事件确实是一个GreetingDelegate类型的委托,只不过不管是不是声明为public,它总是被声明为private。另外,它还有两个方法,分别是add_MakeGreet和remove_MakeGreet,这两个方法分别用于注册委托类型的方法和取消注册。实际上也就是: “+= ”对应 add_MakeGreet,“-=”对应remove_MakeGreet。而这两个方法的访问限制取决于声明事件时的访问限制符。

在add_MakeGreet()方法内部,实际上调用了System.Delegate的Combine()静态方法,这个方法用于将当前的变量添加到委托链表中。我们前面提到过两次,说委托实际上是一个类,在我们定义委托的时候:

public delegate void GreetingDelegate(string name);

当编译器遇到这段代码的时候,会生成下面这样一个完整的类:

public sealed class GreetingDelegate:System.MulticastDelegate{

   public GreetingDelegate(object @object, IntPtr method);

   public virtual IAsyncResult BeginInvoke(string name, AsyncCallback callback, object @object);

   public virtual void EndInvoke(IAsyncResult result);

   public virtual void Invoke(string name);

 }

 

关于这个类的更深入内容,可以参阅《CLR Via C#》等相关书籍,这里就不再讨论了。

四、委托、事件与Observer设计模式

假设我们有个高档的热水器,我们给它通上电,当水温超过95度的时候:1、扬声器会开始发出语音,告诉你水的温度;2、液晶屏也会改变水温的显示,来提示水已经快烧开了。

现在我们需要写个程序来模拟这个烧水的过程,我们将定义一个类来代表热水器,我们管它叫:Heater,它有代表水温的字段,叫做temperature;当然,还有必不可少的给水加热方法BoilWater(),一个发出语音警报的方法MakeAlert(),一个显示水温的方法,ShowMsg()。

namespace Delegate {

   class Heater {

   private int temperature; // 水温

   // 烧水

   public void BoilWater() {

     for (int i = 0; i <= 100; i++) {

      temperature = i;

 

      if (temperature > 95) {

        MakeAlert(temperature);

        ShowMsg(temperature);

       }

     }

   }

 

   // 发出语音警报

  private void MakeAlert(int param) {

    Console.WriteLine("Alarm:嘀嘀嘀,水已经 {0} 度了:" , param);

   }

   

   // 显示水温

  private void ShowMsg(int param) {

    Console.WriteLine("Display:水快开了,当前温度:{0}度。" , param);

   }

 }

 

class Program {

   static void Main() {

    Heater ht = new Heater();

    ht.BoilWater();

   }

 }

}

Observer设计模式简介

上面的例子显然能完成我们之前描述的工作,但是却并不够好。现在假设热水器由三部分组成:热水器、警报器、显示器,它们来自于不同厂商并进行了组装。那么,应该是热水器仅仅负责烧水,它不能发出警报也不能显示水温;在水烧开时由警报器发出警报、显示器显示提示和水温。这时候,上面的例子就应该变成这个样子:

// 热水器

public class Heater {

   private int temperature;

     

   // 烧水

   private void BoilWater() {

    for (int i = 0; i <= 100; i++) {

      temperature = i;

     }

   }

}

 

// 警报器

public class Alarm{

   private void MakeAlert(int param) {

    Console.WriteLine("Alarm:嘀嘀嘀,水已经 {0} 度了:" , param);

   }

}

 

// 显示器

public class Display{

   private void ShowMsg(int param) {

    Console.WriteLine("Display:水已烧开,当前温度:{0}度。" , param);

   }

}

这里就出现了一个问题:如何在水烧开的时候通知报警器和显示器?在继续进行之前,我们先了解一下Observer设计模式,Observer设计模式中主要包括如下两类对象:

1.Subject:监视对象,它往往包含着其他对象所感兴趣的内容。在本范例中,热水器就是一个监视对象,它包含的其他对象所感兴趣的内容,就是temprature字段,当这个字段的值快到100时,会不断把数据发给监视它的对象。

2.Observer:监视者,它监视Subject,当Subject中的某件事发生的时候,会告知Observer,而Observer则会采取相应的行动。在本范例中,Observer有警报器和显示器,它们采取的行动分别是发出警报和显示水温。

在本例中,事情发生的顺序应该是这样的:

1.警报器和显示器告诉热水器,它对它的温度比较感兴趣(注册)。

2.热水器知道后保留对警报器和显示器的引用。

3.热水器进行烧水这一动作,当水温超过95度时,通过对警报器和显示器的引用,自动调用警报器的MakeAlert()方法、显示器的ShowMsg()方法。

类似这样的例子是很多的,GOF对它进行了抽象,称为Observer设计模式:Observer设计模式是为了定义对象间的一种一对多的依赖关系,以便于当一个对象的状态改变时,其他依赖于它的对象会被自动告知并更新。Observer模式是一种松耦合的设计模式。

实现范例的Observer设计模式

我们之前已经对委托和事件介绍很多了,现在写代码应该很容易了,现在在这里直接给出代码,并在注释中加以说明。

using System;

using System.Collections.Generic;

using System.Text;

 

namespace Delegate {

   // 热水器

  public class Heater {

     private int temperature;

     public delegate void BoilHandler(int param);  //声明委托

    public event BoilHandler BoilEvent;    //声明事件

 

    // 烧水

    public void BoilWater() {

      for (int i = 0; i <= 100; i++) {

        temperature = i;

        if (temperature > 95) {

          if (BoilEvent != null) { //如果有对象注册

           BoilEvent(temperature); //调用所有注册对象的方法

         }

        }

      }

    }

   }

 

   // 警报器

  public class Alarm {

    public void MakeAlert(int param) {

      Console.WriteLine("Alarm:嘀嘀嘀,水已经 {0} 度了:", param);

    }

   }

 

   // 显示器

  public class Display {

    public static void ShowMsg(int param) { //静态方法

      Console.WriteLine("Display:水快烧开了,当前温度:{0}度。", param);

    }

   }

   

   class Program {

    static void Main() {

      Heater heater = new Heater();

      Alarm alarm = new Alarm();

 

      heater.BoilEvent += alarm.MakeAlert;  //注册方法

      heater.BoilEvent += (new Alarm()).MakeAlert;  //给匿名对象注册方法

      heater.BoilEvent += Display.ShowMsg;    //注册静态方法

      heater.BoilWater();  //烧水,会自动调用注册过对象的方法

    }

   }

 }

输出为:

Alarm:嘀嘀嘀,水已经 96 度了:

Alarm:嘀嘀嘀,水已经 96 度了:

Display:水快烧开了,当前温度:96度。

// 省略...

 

五、.Net Framework中的委托与事件

尽管上面的范例很好地完成了我们想要完成的工作,但是我们不仅疑惑:为什么.Net Framework 中的事件模型和上面的不同?为什么有很多的EventArgs参数?

在回答上面的问题之前,我们先搞懂 .Net Framework的编码规范:

·         委托类型的名称都应该以EventHandler结束。

·         委托的原型定义:有一个void返回值,并接受两个输入参数:一个Object 类型,一个 EventArgs类型(或继承自EventArgs)。

·         事件的命名为 委托去掉 EventHandler之后剩余的部分。

·         继承自EventArgs的类型应该以EventArgs结尾。

1、委托声明原型中的Object类型的参数代表了Subject,也就是监视对象,在本例中是 Heater(热水器)。回调函数(比如Alarm的MakeAlert)可以通过它访问触发事件的对象(Heater)。

2、EventArgs 对象包含了Observer所感兴趣的数据,在本例中是temperature。

上面这些其实不仅仅是为了编码规范而已,这样也使得程序有更大的灵活性。比如说,如果我们不光想获得热水器的温度,还想在Observer端(警报器或者显示器)方法中获得它的生产日期、型号、价格,那么委托和方法的声明都会变得很麻烦,而如果我们将热水器的引用传给警报器的方法,就可以在方法中直接访问热水器了。

现在我们改写之前的范例,让它符合 .Net Framework 的规范:

using System;

using System.Collections.Generic;

using System.Text;

 

namespace Delegate {

   // 热水器

  public class Heater {

    private int temperature;

    public string type = "RealFire 001";    // 添加型号作为演示

    public string area = "China Xian";     // 添加产地作为演示

    //声明委托

    public delegate void BoiledEventHandler(Object sender, BoiledEventArgs e);

    public event BoiledEventHandler Boiled; //声明事件

 

    // 自定义EventArgs 定义BoiledEventArgs类,传递给Observer所感兴趣的信息

    public class BoiledEventArgs : EventArgs {

      public readonly int temperature;

      public BoiledEventArgs(int temperature) {

        this.temperature = temperature;

      }

    }

 

    // 可以供继承自 Heater 的类重写,以便继承类拒绝其他对象对它的监视

    protected virtual void OnBoiled(BoiledEventArgs e) {

      if (Boiled != null) { // 如果有对象注册

       Boiled(this, e); // 调用所有注册对象的方法

      }

    }

    

    // 烧水。

    public void BoilWater() {

      for (int i = 0; i <= 100; i++) {

        temperature = i;

        if (temperature > 95) {

          //建立BoiledEventArgs 对象。

          BoiledEventArgs e = new BoiledEventArgs(temperature);

          OnBoiled(e); // 调用 OnBolied方法

       }

      }

    }

   }

 

   // 警报器

  public class Alarm {

    public void MakeAlert(Object sender, Heater.BoiledEventArgs e) {

      Heater heater = (Heater)sender;   //这里是不是很熟悉呢?

      //访问 sender 中的公共字段

      Console.WriteLine("Alarm{0} - {1}: ", heater.area, heater.type);

      Console.WriteLine("Alarm: 嘀嘀嘀,水已经 {0} 度了:", e.temperature);

      Console.WriteLine();

    }

   }

 

   // 显示器

  public class Display {

    public static void ShowMsg(Object sender, Heater.BoiledEventArgs e) {  //静态方法

      Heater heater = (Heater)sender;

      Console.WriteLine("Display{0} - {1}: ", heater.area, heater.type);

      Console.WriteLine("Display:水快烧开了,当前温度:{0}度。", e.temperature);

      Console.WriteLine();

    }

   }

 

   class Program {

    static void Main() {

      Heater heater = new Heater();

      Alarm alarm = new Alarm();

 

      heater.Boiled += alarm.MakeAlert;  //注册方法

      heater.Boiled += (new Alarm()).MakeAlert;   //给匿名对象注册方法

      heater.Boiled += new Heater.BoiledEventHandler(alarm.MakeAlert);  //也可以这么注册

      heater.Boiled += Display.ShowMsg;    //注册静态方法

 

      heater.BoilWater();  //烧水,会自动调用注册过对象的方法

    }

   }

 }

 

输出为:

AlarmChina Xian - RealFire 001:

 Alarm: 嘀嘀嘀,水已经 96 度了:

AlarmChina Xian - RealFire 001:

 Alarm: 嘀嘀嘀,水已经 96 度了:

AlarmChina Xian - RealFire 001:

 Alarm: 嘀嘀嘀,水已经 96 度了:

DisplayChina Xian - RealFire 001:

 Display:水快烧开了,当前温度:96度。

// 省略 ...

 

六、泛型委托 

简单的泛型委托示例:

class Program 

    { 

        // 泛型委托,与普通委托类似,不同之处只在于使用泛型委托要指定泛型参数         

        public delegate T MyGenericDelegate<T>(T obj1,T obj2);  

        int AddInt(int x, int y) 

        { 

            return x + y; 

        }   

        string AddString(string s1, string s2) 

        { 

            return s1 + s2; 

        }          

        static void Main(string[] args) 

        { 

            Program p = new Program();              

            MyGenericDelegate<int> intDel; 

            intDel = p.AddInt; 

            Console.WriteLine("int代理的值是{0}", intDel(100, 200));  

            MyGenericDelegate<string> stringDel; 

            stringDel = p.AddString; 

            Console.WriteLine("string代理的值是{0}", stringDel("aaa", "bbb")); 

        } 

    } 

为了方便开发,.NET基类库针对在实际开发中最常用的情形提供了几个预定义好的委托,这些预定义委托用得很广,比如在编写lambda表达式和开发并行计算程序时经常要用到他们。.NET提供的泛型委托包括Action、Func和Predicate 

1、Action<T> 泛型委托

Action是无返回值的泛型委托,Action至少0个参数,至多16个参数,无返回值。

·         Action 表示无参,无返回值的委托 

·         Action<int,string> 表示有传入参数int,string无返回值的委托

·         Action<int,string,bool> 表示有传入参数int,string,bool无返回值的委托 

·         Action<int,int,int,int> 表示有传入4个int型参数,无返回值的委托 

泛型委托与直接显示声明自定义委托的示例比较:

显示声明自定义委托:

delegate void DisplayMessage(string message);

public class TestCustomDelegate

{

   public static void Main()

   {

      DisplayMessage messageTarget;

      messageTarget = ShowWindowsMessage;

      messageTarget("Hello, World!");  

   }     

   private static void ShowWindowsMessage(string message)

   {

      MessageBox.Show(message);     

   }

}

Action<T> 用法。比起自定义委托,明显可以看出代码简洁了。

public class TestAction1

{

   public static void Main()

   {

      Action<string> messageTarget;

      messageTarget = ShowWindowsMessage;

      messageTarget("Hello, World!");  

   }     

   private static void ShowWindowsMessage(string message)

   {

      MessageBox.Show(message);     

   }

}

2、Func<T, TResult> 委托

Func是有返回值的泛型委托,Func至少0个参数,至多16个参数,根据返回值泛型返回。必须有返回值,不可void

·         Func<int> 表示无参,返回值为int的委托

·         Func<object,string,int> 表示传入参数为object, string 返回值为int的委托

·         Func<object,string,int> 表示传入参数为object, string 返回值为int的委托

·         Func<T1,T2,,T3,int> 表示传入参数为T1,T2,,T3(泛型)返回值为int的委托   

下面是一个简单的普通委托来传方法的示例。

private delegate string Say();

public static string SayHello()

{

    return "Hello";

}

 

static void Main(string[] args)

{

    Say say = SayHello;

    Console.WriteLine(say());

    Console.ReadKey();

}

为了更方便,我们再来试试.Net默认带的委托。

public static string SayHello()

{

    return "Hello";

}

static void Main(string[] args)

{

    Func<string> say = SayHello;

    Console.WriteLine(say());

    Console.ReadKey();

}

如果需要参数的,还可以这样传一份。

public static string SayHello(string str)

{

    return str + str;

}

static void Main(string[] args)

{

    Func<string, string> say = SayHello;

    string str = say("abc");  

    Console.WriteLine(str);     //输出abcabc

    Console.ReadKey();

}

 3、Predicate

predicate 是返回bool型的泛型委托, Predicate有且只有一个参数,返回值固定为bool

·         predicate<int> 表示传入参数为int 返回bool的委托  

先来一个普通示例:

using System;

using System.Drawing;

public class Example

{

   public static void Main()

   {

      // Create an array of Point structures.

      Point[] points = { new Point(100, 200),

                         new Point(150, 250), new Point(250, 375),

                         new Point(275, 395), new Point(295, 450) };

 

      // Find the first Point structure for which X times Y 

      // is greater than 100000.

      Point first = Array.Find(points, x => x.X * x.Y > 100000 );

 

      // Display the first structure found.

      Console.WriteLine("Found: X = {0}, Y = {1}", first.X, first.Y);

   }

}

上边的示例只不过使用 lambda 表达式来表示Predicate<T>委托,使用predicate 改造上边的示例:

using System;

using System.Drawing;

public class Example

{

   public static void Main()

   {

      // Create an array of Point structures.

      Point[] points = { new Point(100, 200),

                         new Point(150, 250), new Point(250, 375),

                         new Point(275, 395), new Point(295, 450) };

 

      // Define the Predicate<T> delegate.

      Predicate<Point> predicate = FindPoints;     

      // Find the first Point structure for which X times Y 

      // is greater than 100000.

      Point first = Array.Find(points, predicate);

      // Display the first structure found.

      Console.WriteLine("Found: X = {0}, Y = {1}", first.X, first.Y);

   }

   private static bool FindPoints(Point obj)

   {

      return obj.X * obj.Y > 100000;

   }

}

4、Func与Action都支持Lambda的形式调用

一般的需求下,我们就使用微软定义的委托就足够了,这样减少了我们对委托的重复定义,可能有部分初学者见到Func<>,Action<>这样的代码肯定会很懵比,这只是你对新东西陌生罢了,多结合实例敲几遍,自然就会用了,它们其实就是微软封装定义好了的委托,没有什么特别的。我们上面的代码也可以使用Lambda的形式:

class Program

{

    static void Main(string[] args)

    {

        Func<int,int,int> add = (a, b) => a + b;

        Calculate(add, 10, 25);

        Console.ReadKey();

    }

    static void Calculate<T, Y, U>(Func<T, Y, U> ex, T a, Y b)

    {

        Console.WriteLine(ex(a, b) + "\n");

    }

}

这样写用Func就省去了定义委托这一步。

同样,其实在我们的webform,winform框架中,微软也给我们规范了一个委托的定义:

1

delegate void EvenHandler(object sender,   EventArgs e);

七、事件的详解

什么是事件?事件涉及两类角色:事件的发布者和事件的订阅者。触发事件的对象称为事件发布者,捕获时间并对其作出响应的对象叫做事件订阅者。 个人理解事件和委托的关系,相当于属性和字段的关系。

事件和委托的关系:在事件触发以后,事件发布者需要发布消息,通知事件订阅者进行事件处理,但事件发布者并不知道要通知哪些事件订阅者,这就需要在发布者和订阅者之间存在一个中介,这个中介就是委托。我们知道,委托都有一个调用列表,那么,只需要事件发布者有这样一个委托,各个事件订阅者将自己的事件处理程序都加入到该委托的调用列表中,那么事件触发时,发布者只需要调用委托即可触发订阅者的事件处理程序。 

事件的声明:事件就是类成员的一种,只是事件定义中包含一种特殊的关键字:event。事件的声明有两种方式: 

1、采用自定义委托类型。

2、采用EventHandler预定义委托类型,如下面代码:

1

2

//关键字  委托类型   时间名

public event  EventHandler   PrintComplete;

这两种方式基本相同,只不过第二种是.Net Framework中普遍采用的一种形式,因此建议尽量采用第二种方式。我们先来看看EventHandler委托的签名:

1

public delegate void EventHandler(Object   sender,EventArgs e);

从上面的签名可以看出:

1、EventHandler委托的返回类型为void;

2、第一个参数--sender参数,它负责保存触发事件的对象的引用,因为参数的类型是Object类型,因此它可以保存任何类型的实例;

3、第二个参数--e参数,它负责保存事件数据,这里是在BCL中定义的默认的EventArgs类,它位于System命名空间中,他不能保存任何数据。 

订阅事件:事件订阅者角色需要订阅事件发布者发布的事件,这样才能在事件发布时接受到消息并作出响应,事件事实上是委托类型,因此事件处理方法必须和委托签名相匹配。如果事件使用预定义的委托类型:EventHandler,那么匹配它的事件处理方法如下:

public void SomeEventHandler(object sender, EventArgs e)

{

    //..

}

有了事件处理方法,就可以订阅事件了,只需要使用加法赋值运算符(+=)即可。 

触发事件代码如下:

//检查事件是否为空

if (PrintComplete != null)

{

    //像调用方法一样触发事件,参数

    PrintComplete(this,new EventArgs();

}

 完整的一个事件例子:

namespace ConsoleApplication1

{

    public class Program

    {

        static void Main(string[] args)

        {

             Console.WriteLine("该做的东西做完,然后触发事件!");

             EventSample es = new EventSample();

             es.ShowComplete += es.MyEventHandler;

            es.OnShowComplete();

            Console.ReadKey();

        }

    }

    public class EventSample

    {

     //定义一个事件

        public event EventHandler ShowComplete;

    

     //触发事件

        public void OnShowComplete()

        {

            //判断是否绑定了事件处理方法,null表示没有事件处理方法

            if (ShowComplete != null)

            {

          //像调用方法一样触发事件

                ShowComplete(this, new EventArgs());

            }

        }

 

        //事件处理方法

        public void MyEventHandler(object sender, EventArgs e)

        {

            Console.WriteLine("谁触发了我?" + sender.ToString());

        }

    }

}

八、事件引起的内存泄露

1、不手动注销事件也不发生内存泄露的情况

我们经常会写EventHandler += AFunction; 如果没有手动注销这个Event handler类似:EventHandler –= AFunction 有可能会发生内存泄露。

public class Program

{

    static void ShowMemory()

    {

        Console.WriteLine("共用内存:{0}M", GC.GetTotalMemory(true) / 1024 / 1024);

    }

    static void Main(string[] args)

    {

        ShowMemory();

        for (int i = 0; i < 5; i++)

        {

            EventSample es = new EventSample();

            es.ShowComplete += es.MyEventHandler;

            GC.Collect();

            GC.WaitForPendingFinalizers();

            GC.Collect();

            ShowMemory();

        }

        Console.ReadKey();

    }

}

 

public class EventSample

{

    byte[] m_ExtraMemory = new byte[1024 * 1024 * 12];

    //定义一个事件

    public event EventHandler ShowComplete;

    //触发事件

    public void OnShowComplete()

    {

        //判断是否绑定了事件处理方法,null表示没有事件处理方法

        if (ShowComplete != null)

        {

            //像调用方法一样触发事件

            ShowComplete(this, new EventArgs());

        }

    }

    //事件处理方法

    public void MyEventHandler(object sender, EventArgs e)

    {

        Console.WriteLine("谁触发了我?" + sender.ToString());

    }

}

 上述代码输出如下:  

 

 从输出来看,内存被GC正常地回收,没有问题。

2、内存泄露的情况

我们来将代码改动一下

public class Program

{

    static void ShowMemory()

    {

        Console.WriteLine("共用内存:{0}M", GC.GetTotalMemory(true) / 1024 / 1024);

    }

    static void Main(string[] args)

    {

        ShowMemory();

       for (int i = 0; i < 5; i++)

       {

           Microsoft.Win32.SystemEvents.DisplaySettingsChanged += new EventHandler(new MyMethod().SystemEvents_DisplaySettingsChanged);

           GC.Collect();

           GC.WaitForPendingFinalizers();

           GC.Collect();

           ShowMemory();

       }

       Console.ReadKey();

   }

}

public class MyMethod

{

    byte[] m_ExtraMemory = new byte[1024 * 1024 * 12];

    public void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e){ }

}

输出结果如下:  

 

 从输出结果来看,内存已不能被GC正常回收。为什么会出现这种情况呢?我们来看看Microsoft.Win32.SystemEvents.DisplaySettingsChanged的源代码(省略前后部分):

public sealed class SystemEvents

{

    ... ...

    public static event EventHandler DisplaySettingsChanged

    ... ...

为什么会有差别,根本区别在于后者有个SystemEvents.DisplaySettingsChanged事件,而这个事件是静态的。

3、释放资源

如果我们希望释放资源,则我们需要在某个地方实现-=AFunction操作

public class Program

{

    static void ShowMemory()

    {

        Console.WriteLine("共用内存:{0}M", GC.GetTotalMemory(true) / 1024 / 1024);

    }

    static void Main(string[] args)

    {

        ShowMemory();

        for (int i = 0; i < 5; i++)

        {

            using (MyMethod myMethod = new MyMethod())

            {

                Microsoft.Win32.SystemEvents.DisplaySettingsChanged += new EventHandler(myMethod.SystemEvents_DisplaySettingsChanged);

            }

            GC.Collect();

            GC.WaitForPendingFinalizers();

            GC.Collect();

            ShowMemory();

        }

        Console.ReadKey();

   }

}

 

public class MyMethod : IDisposable

{

    byte[] m_ExtraMemory = new byte[1024 * 1024 * 12];

    public void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e) { }

    public void Dispose()

    {

        Microsoft.Win32.SystemEvents.DisplaySettingsChanged -= new EventHandler(SystemEvents_DisplaySettingsChanged);

    }

}

输出如下:

  

增加了一个Dispose来实现 "-="功能就OK了。

4、注意静态、单例

静态对象生命周期很长,永远不会被GC回收,一旦被他给引用上了,那就不可能释放了。上面的例子就是被静态的DisplaySettingsChanged 引用导致不能被回收。  另外一个要注意的是Singleton单例模式实现的类,他们也是static的生命周期很长,要注意引用链,你的类是否被它引用上,如果在它的引用链上,就内存泄露了。

九、委托的三种调用 

本文将主要通过同步调用、异步调用、异步回调三个示例来讲解在用委托执行同一个加法类的时候的的区别和利弊

首先,通过代码定义一个委托和下面三个示例将要调用的方法:

public delegate int AddHandler(int a,int b);

public class 加法类

{

   public static int Add(int a, int b)

   {

       Console.WriteLine("开始计算:" + a + "+" + b);

       Thread.Sleep(3000); //模拟该方法运行三秒

       Console.WriteLine("计算完成!");

       return a + b;

   }
}

1、同步调用

委托的Invoke方法用来进行同步调用。同步调用也可以叫阻塞调用,它将阻塞当前线程,然后执行调用,调用完毕后再继续向下进行。代码如下

public class 同步调用

{

        static void Main()

        {

            Console.WriteLine("===== 同步调用 SyncInvokeTest =====");

            AddHandler handler = new AddHandler(加法类.Add);

            int result = handler?.Invoke(1, 2);

            Console.WriteLine("继续做别的事情。。。");

            Console.WriteLine(result);

            Console.ReadKey();

        }     

}

同步调用会阻塞线程,如果是要调用一项繁重的工作(如大量IO操作),可能会让程序停顿很长时间,造成糟糕的用户体验,这时候异步调用就很有必要了。

2、异步调用

异步调用不阻塞线程,而是把调用塞到线程池中,程序主线程或UI线程可以继续执行。委托的异步调用通过BeginInvoke和EndInvoke来实现。代码如下:

public class 异步调用

{

        static void Main()

        {

            Console.WriteLine("===== 异步调用 AsyncInvokeTest =====");

            AddHandler handler = new AddHandler(加法类.Add);

            //IAsyncResult: 异步操作接口(interface)

            //BeginInvoke: 委托(delegate)的一个异步方法的开始

            IAsyncResult result = handler.BeginInvoke(1, 2, null, null);

            Console.WriteLine("继续做别的事情。。。");

            //异步操作返回

            Console.WriteLine(handler.EndInvoke(result));

            Console.ReadKey();

        }

}

可以看到,主线程并没有等待,而是直接向下运行了。但是问题依然存在,当主线程运行到EndInvoke时,如果这时调用没有结束(这种情况很可能出现),这时为了等待调用结果,线程依旧会被阻塞。

异步委托,也可以参考如下写法:

Action<object> action=(obj)=>method(obj);

action.BeginInvoke(obj,ar=>action.EndInvoke(ar),null);

简简单单两句话就可以完成一部操作。

3、异步回调

用回调函数,当调用结束时会自动调用回调函数,解决了为等待调用结果,而让线程依旧被阻塞的局面。代码如下:

public class 异步回调

{

        static void Main()

        {

            Console.WriteLine("===== 异步回调 AsyncInvokeTest =====");

            AddHandler handler = new AddHandler(加法类.Add);

            //异步操作接口(注意BeginInvoke方法的不同!)

            IAsyncResult result = handler.BeginInvoke(1,2,new AsyncCallback(回调函数),"AsycState:OK");

            Console.WriteLine("继续做别的事情。。。");

            Console.ReadKey();

        }

 

        static void 回调函数(IAsyncResult result)

        {    

             //result 加法类.Add()方法的返回值

            //AsyncResult IAsyncResult接口的一个实现类,空间:System.Runtime.Remoting.Messaging

            //AsyncDelegate 属性可以强制转换为用户定义的委托的实际类。

            AddHandler handler = (AddHandler)((AsyncResult)result).AsyncDelegate;

            Console.WriteLine(handler.EndInvoke(result));

            Console.WriteLine(result.AsyncState);

        }

}

我定义的委托的类型为AddHandler,则为了访问 AddHandler.EndInvoke,必须将异步委托强制转换为 AddHandler。可以在异步回调函数(类型为 AsyncCallback)中调用 MAddHandler.EndInvoke,以获取最初提交的 AddHandler.BeginInvoke 的结果。

4、需要注意的地方

(1)int result = handler.Invoke(1,2);

  为什么Invoke的参数和返回值和AddHandler委托是一样的呢?

  答:Invoke方法的参数很简单,一个委托,一个参数表(可选),而Invoke方法的主要功能就是帮助你在UI线程上调用委托所指定的方法。Invoke方法首先检查发出调用的线程(即当前线程)是不是UI线程,如果是,直接执行委托指向的方法,如果不是,它将切换到UI线程,然后执行委托指向的方法。不管当前线程是不是UI线程,Invoke都阻塞直到委托指向的方法执行完毕,然后切换回发出调用的线程(如果需要的话),返回。

所以Invoke方法的参数和返回值和调用他的委托应该是一致的。

(2)IAsyncResult result = handler.BeginInvoke(1,2,null,null);

  BeginInvoke : 开始一个异步的请求,调用线程池中一个线程来执行,

  返回IAsyncResult 对象(异步的核心). IAsyncResult 简单的说,他存储异步操作的状态信息的一个接口,也可以用他来结束当前异步。

  注意: BeginInvoke和EndInvoke必须成对调用.即使不需要返回值,但EndInvoke还是必须调用,否则可能会造成内存泄漏。

(3)IAsyncResult.AsyncState 属性:

  获取用户定义的对象,它限定或包含关于异步操作的信息。

 十、异步调用与多线程的区别 

随着拥有多个硬线程CPU(超线程、双核)的普及,多线程和异步操作等并发程序设计方法也受到了更多的关注和讨论。本文主要是想探讨一下如何使用并发来最大化程序的性能。

多线程和异步操作的异同 

多线程和异步操作两者都可以达到避免调用线程阻塞的目的,从而提高软件的可响应性。甚至有些时候我们就认为多线程和异步操作是等同的概念。但是,多线程和异步操作还是有一些区别的。而这些区别造成了使用多线程和异步操作的时机的区别。多线程是实现异步的一个重要手段,但不是唯一手段,对以一个单线程程序也可以是异步执行的。 

异步操作的本质

所有的程序最终都会由计算机硬件来执行,所以为了更好的理解异步操作的本质,我们有必要了解一下它的硬件基础。 熟悉电脑硬件的朋友肯定对DMA这个词不陌生,硬盘、光驱的技术规格中都有明确DMA的模式指标,其实网卡、声卡、显卡也是有DMA功能的。DMA就是直 接内存访问的意思,也就是说,拥有DMA功能的硬件在和内存进行数据交换的时候可以不消耗CPU资源。只要CPU在发起数据传输时发送一个指令,硬件就开 始自己和内存交换数据,在传输完成之后硬件会触发一个中断来通知操作完成。这些无须消耗CPU时间的I/O操作正是异步操作的硬件基础。所以即使在DOS 这样的单进程(而且无线程概念)系统中也同样可以发起异步的DMA操作。异步编程的目的就是为了能够是实现并行,但不仅是提高多处理器间的并行度,同时也是提高处理器与I/O处理器的并行度。非阻塞模式一般特指异步的I/O 操作,可以算是异步编程的一种类型。

线程的本质 

线程不是一个计算机硬件的功能,而是操作系统提供的一种逻辑功能,线程本质上是进程中一段并发运行的代码,所以线程需要操作系统投入CPU资源来运行和调度。

异步操作的优缺点 

因为异步操作无须额外的线程负担,并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以不必使用共享变量(即使无法完全不用,最起码可以减少 共享变量的数量),减少了死锁的可能。当然异步操作也并非完美无暇。编写异步操作的复杂程度较高,程序主要使用回调方式进行处理,与普通人的思维方式有些初入,而且难以调试。

多线程的优缺点 

多线程的优点很明显,线程中的处理程序依然是顺序执行,符合普通人的思维习惯,所以编程简单。但是多线程的缺点也同样明显,线程的使用(滥用)会给系统带来上下文切换的额外负担。并且线程间的共享变量可能造成死锁的出现。

适用范围 

在了解了线程与异步操作各自的优缺点之后,我们可以来探讨一下线程和异步的合理用途。我认为:当需要执行I/O操作时,使用异步操作比使用线程+同步 I/O操作更合适。I/O操作不仅包括了直接的文件、网络的读写,还包括数据库操作、Web Service、HttpRequest以及.net Remoting等跨进程的调用。而线程的适用范围则是那种需要长时间CPU运算的场合,例如耗时较长的图形处理和算法执行。但是往 往由于使用线程编程的简单和符合习惯,所以很多朋友往往会使用线程来执行耗时较长的I/O操作。这样在只有少数几个并发操作的时候还无伤大雅,如果需要处 理大量的并发操作时就不合适了。

对于CPU来说以下意味着什么

线程:意味了CPU的一组寄存器

进程:意味着CPU的页目录寄存器

IO:意味着一些端口或内存地址空间中一些地址

 


原来这就是网络

你是一台电脑,你的名字叫 A

很久很久之前,你不与任何其他电脑相连接,孤苦伶仃。

图片

直到有一天,你希望与另一台电脑 B 建立通信,于是你们各开了一个网口,用一根网线连接了起来。

图片

用一根网线连接起来怎么就能"通信"了呢?我可以给你讲 IO、讲中断、讲缓冲区,但这不是研究网络时该关心的问题。

如果你纠结,要么去研究一下操作系统是如何处理网络 IO 的,要么去研究一下包是如何被网卡转换成电信号发送出去的,要么就仅仅把它当做电脑里有个小人在开枪吧~

图片

反正,你们就是连起来了,并且可以通信。






第一层




有一天,一个新伙伴 C 加入了,但聪明的你们很快发现,可以每个人开两个网口,用一共三根网线,彼此相连。


图片

随着越来越多的人加入,你发现身上开的网口实在太多了,而且网线密密麻麻,混乱不堪。(而实际上一台电脑根本开不了这么多网口,所以这种连线只在理论上可行,所以连不上的我就用红色虚线表示了,就是这么严谨哈哈~)

图片

于是你们发明了一个中间设备,你们将网线都插到这个设备上,由这个设备做转发,就可以彼此之间通信了,本质上和原来一样,只不过网口的数量和网线的数量减少了,不再那么混乱。

图片

你给它取名叫集线器,它仅仅是无脑将电信号转发到所有出口(广播),不做任何处理,你觉得它是没有智商的,因此把人家定性在了物理层


由于转发到了所有出口,那 BCDE 四台机器怎么知道数据包是不是发给自己的呢?

首先,你要给所有的连接到集线器的设备,都起个名字。原来你们叫 ABCD,但现在需要一个更专业的,全局唯一的名字作为标识,你把这个更高端的名字称为 MAC 地址

你的 MAC 地址是 aa-aa-aa-aa-aa-aa,你的伙伴 b 的 MAC 地址是 bb-bb-bb-bb-bb-bb,以此类推,不重复就好。

这样,A 在发送数据包给 B 时,只要在头部拼接一个这样结构的数据,就可以了。

图片

B 在收到数据包后,根据头部的目标 MAC 地址信息,判断这个数据包的确是发给自己的,于是便收下

其他的 CDE 收到数据包后,根据头部的目标 MAC 地址信息,判断这个数据包并不是发给自己的,于是便丢弃

图片

虽然集线器使整个布局干净不少,但原来我只要发给电脑 B 的消息,现在却要发给连接到集线器中的所有电脑,这样既不安全,又不节省网络资源。






第二层





如果把这个集线器弄得更智能一些,只发给目标 MAC 地址指向的那台电脑,就好了。


虽然只比集线器多了这一点点区别,但看起来似乎有智能了,你把这东西叫做交换机。也正因为这一点点智能,你把它放在了另一个层级,数据链路层

图片

如上图所示,你是这样设计的。

交换机内部维护一张 MAC 地址表,记录着每一个 MAC 地址的设备,连接在其哪一个端口上。


MAC 地址端口
bb-bb-bb-bb-bb-bb1
cc-cc-cc-cc-cc-cc3
aa-aa-aa-aa-aa-aa4
dd-dd-dd-dd-dd-dd5


假如你仍然要发给 B 一个数据包,构造了如下的数据结构从网口出去。

图片

到达交换机时,交换机内部通过自己维护的 MAC 地址表,发现目标机器 B 的 MAC 地址 bb-bb-bb-bb-bb-bb 映射到了端口 1 上,于是把数据从 1 号端口发给了 B,完事~

你给这个通过这样传输方式而组成的小范围的网络,叫做以太网

当然最开始的时候,MAC 地址表是空的,是怎么逐步建立起来的呢?

假如在 MAC 地址表为空是,你给 B 发送了如下数据

图片

由于这个包从端口 4 进入的交换机,所以此时交换机就可以在 MAC地址表记录第一条数据:

MAC:aa-aa-aa-aa-aa-aa-aa
端口:4

交换机看目标 MAC 地址(bb-bb-bb-bb-bb-bb)在地址表中并没有映射关系,于是将此包发给了所有端口,也即发给了所有机器。

之后,只有机器 B 收到了确实是发给自己的包,于是做出了响应,响应数据从端口 1 进入交换机,于是交换机此时在地址表中更新了第二条数据:

MAC:bb-bb-bb-bb-bb-bb
端口:1

过程如下

图片

经过该网络中的机器不断地通信,交换机最终将 MAC 地址表建立完毕~


随着机器数量越多,交换机的端口也不够了,但聪明的你发现,只要将多个交换机连接起来,这个问题就轻而易举搞定~

图片

你完全不需要设计额外的东西,只需要按照之前的设计和规矩来,按照上述的接线方式即可完成所有电脑的互联,所以交换机设计的这种规则,真的很巧妙。你想想看为什么(比如 A 要发数据给 F)。

但是你要注意,上面那根红色的线,最终在 MAC 地址表中可不是一条记录呀,而是要把 EFGH 这四台机器与该端口(端口6)的映射全部记录在表中。

最终,两个交换机将分别记录 A ~ H 所有机器的映射记录

左边的交换机


MAC 地址端口
bb-bb-bb-bb-bb-bb1
cc-cc-cc-cc-cc-cc3
aa-aa-aa-aa-aa-aa4
dd-dd-dd-dd-dd-dd5
ee-ee-ee-ee-ee-ee
6
ff-ff-ff-ff-ff-ff
6
gg-gg-gg-gg-gg-gg
6
hh-hh-hh-hh-hh-hh
6


右边的交换机


MAC 地址端口
bb-bb-bb-bb-bb-bb1
cc-cc-cc-cc-cc-cc1
aa-aa-aa-aa-aa-aa1
dd-dd-dd-dd-dd-dd1
ee-ee-ee-ee-ee-ee
2
ff-ff-ff-ff-ff-ff
3
gg-gg-gg-gg-gg-gg
4
hh-hh-hh-hh-hh-hh
6


这在只有 8 台电脑的时候还好,甚至在只有几百台电脑的时候,都还好,所以这种交换机的设计方式,已经足足支撑一阵子了。

但很遗憾,人是贪婪的动物,很快,电脑的数量就发展到几千、几万、几十万。






第三层





交换机已经无法记录如此庞大的映射关系了。

此时你动了歪脑筋,你发现了问题的根本在于,连出去的那根红色的网线,后面不知道有多少个设备不断地连接进来,从而使得地址表越来越大。

那我可不可以让那根红色的网线,接入一个新的设备,这个设备就跟电脑一样有自己独立的 MAC 地址,而且同时还能帮我把数据包做一次转发呢?

这个设备就是路由器,它的功能就是,作为一台独立的拥有 MAC 地址的设备,并且可以帮我把数据包做一次转发你把它定在了网络层。

图片

注意,路由器的每一个端口,都有独立的 MAC 地址

好了,现在交换机的 MAC 地址表中,只需要多出一条 MAC 地址 ABAB 与其端口的映射关系,就可以成功把数据包转交给路由器了,这条搞定。

那如何做到,把发送给 C 和 D,甚至是把发送给 DEFGH.... 的数据包,统统先发送给路由器呢?

不难想到这样一个点子,假如电脑 C 和 D 的 MAC 地址拥有共同的前缀,比如分别是


C 的 MAC 地址:FFFF-FFFF-CCCC
D 的 MAC 地址:FFFF-FFFF-DDDD


那我们就可以说,将目标 MAC 地址为 FFFF-FFFF-?开头的,统统先发送给路由器。

这样是否可行呢?答案是否定的。

我们先从现实中 MAC 地址的结构入手,MAC地址也叫物理地址、硬件地址,长度为 48 位,一般这样来表示

00-16-EA-AE-3C-40

它是由网络设备制造商生产时烧录在网卡的EPROM(一种闪存芯片,通常可以通过程序擦写)。其中前 24 位(00-16-EA)代表网络硬件制造商的编号,后 24 位(AE-3C-40)是该厂家自己分配的,一般表示系列号。只要不更改自己的 MAC 地址,MAC 地址在世界是唯一的。形象地说,MAC地址就如同身份证上的身份证号码,具有唯一性。

那如果你希望向上面那样表示将目标 MAC 地址为 FFFF-FFFF-?开头的,统一从路由器出去发给某一群设备(后面会提到这其实是子网的概念),那你就需要要求某一子网下统统买一个厂商制造的设备,要么你就需要要求厂商在生产网络设备烧录 MAC 地址时,提前按照你规划好的子网结构来定 MAC 地址,并且日后这个网络的结构都不能轻易改变。

这显然是不现实的。

于是你发明了一个新的地址,给每一台机器一个 32 位的编号,如:

11000000101010000000000000000001

你觉得有些不清晰,于是把它分成四个部分,中间用点相连。

11000000.10101000.00000000.00000001

你还觉得不清晰,于是把它转换成 10 进制。

192.168.0.1

最后你给了这个地址一个响亮的名字,IP 地址。现在每一台电脑,同时有自己的 MAC 地址,又有自己的 IP 地址,只不过 IP 地址是软件层面上的,可以随时修改,MAC 地址一般是无法修改的。

这样一个可以随时修改的 IP 地址,就可以根据你规划的网络拓扑结构,来调整了。

图片

如上图所示,假如我想要发送数据包给 ABCD 其中一台设备,不论哪一台,我都可以这样描述,"将 IP 地址为 192.168.0 开头的全部发送给到路由器,之后再怎么转发,交给它!",巧妙吧。

那交给路由器之后,路由器又是怎么把数据包准确转发给指定设备的呢?

别急我们慢慢来。

我们先给上面的组网方式中的每一台设备,加上自己的 IP 地址

图片

现在两个设备之间传输,除了加上数据链路层的头部之外,还要再增加一个网络层的头部。

假如 A 给 B 发送数据,由于它们直接连着交换机,所以 A 直接发出如下数据包即可,其实网络层没有体现出作用。

图片

但假如 A 给 C 发送数据,A 就需要先转交给路由器,然后再由路由器转交给 C。由于最底层的传输仍然需要依赖以太网,所以数据包是分成两段的。

A ~ 路由器这段的包如下:

图片

路由器到 C 这段的包如下:

图片

好了,上面说的两种情况(A->B,A->C),相信细心的读者应该会有不少疑问,下面我们一个个来展开。



A 给 C 发数据包,怎么知道是否要通过路由器转发呢?


答案:子网

如果源 IP 与目的 IP 处于一个子网,直接将包通过交换机发出去。

如果源 IP 与目的 IP 不处于一个子网,就交给路由器去处理。

好,那现在只需要解决,什么叫处于一个子网就好了。

  • 192.168.0.1 和 192.168.0.2 处于同一个子网

  • 192.168.0.1 和 192.168.1.1 处于不同子网

这两个是我们人为规定的,即我们想表示,对于 192.168.0.1 来说:

192.168.0.xxx 开头的,就算是在一个子网,否则就是在不同的子网。

那对于计算机来说,怎么表达这个意思呢?于是人们发明了子网掩码的概念

假如某台机器的子网掩码定为 255.255.255.0

这表示,将源 IP 与目的 IP 分别同这个子网掩码进行与运算,相等则是在一个子网,不相等就是在不同子网,就这么简单。

比如

  • A电脑:192.168.0.1 & 255.255.255.0 = 192.168.0.0

  • B电脑:192.168.0.2 & 255.255.255.0 = 192.168.0.0

  • C电脑:192.168.1.1 & 255.255.255.0 = 192.168.1.0

  • D电脑:192.168.1.2 & 255.255.255.0 = 192.168.1.0

那么 A 与 B 在同一个子网,C 与 D 在同一个子网,但是 A 与 C 就不在同一个子网,与 D 也不在同一个子网,以此类推。

图片

所以如果 A 给 C 发消息,A 和 C 的 IP 地址分别 & A 机器配置的子网掩码,发现不相等,则 A 认为 C 和自己不在同一个子网,于是把包发给路由器,就不管了,之后怎么转发,A 不关心



A 如何知道,哪个设备是路由器?


答案:在 A 上要设置默认网关

上一步 A 通过是否与 C 在同一个子网内,判断出自己应该把包发给路由器,那路由器的 IP 是多少呢?

其实说发给路由器不准确,应该说 A 会把包发给默认网关

对 A 来说,A 只能直接把包发给同处于一个子网下的某个 IP 上,所以发给路由器还是发给某个电脑,对 A 来说也不关心,只要这个设备有个 IP 地址就行。

所以默认网关,就是 A 在自己电脑里配置的一个 IP 地址,以便在发给不同子网的机器时,发给这个 IP 地址。

图片

仅此而已!



路由器如何知道C在哪里?


答案:路由表

现在 A 要给 C 发数据包,已经可以成功发到路由器这里了,最后一个问题就是,路由器怎么知道,收到的这个数据包,该从自己的哪个端口出去,才能直接(或间接)地最终到达目的地 C 呢。

路由器收到的数据包有目的 IP 也就是 C 的 IP 地址,需要转化成从自己的哪个端口出去,很容易想到,应该有个表,就像 MAC 地址表一样。

这个表就叫路由表

至于这个路由表是怎么出来的,有很多路由算法,本文不展开,因为我也不会哈哈~

不同于 MAC 地址表的是,路由表并不是一对一这种明确关系,我们下面看一个路由表的结构。


目的地址子网掩码下一跳端口
192.168.0.0255.255.255.0
0
192.168.0.254255.255.255.255
0
192.168.1.0255.255.255.0
1
192.168.1.254255.255.255.255
1


我们学习一种新的表示方法,由于子网掩码其实就表示前多少位表示子网的网段,所以如 192.168.0.0(255.255.255.0) 也可以简写为 192.168.0.0/24


目的地址下一跳端口
192.168.0.0/24
0
192.168.0.254/32
0
192.168.1.0/24
1
192.168.1.254/32
1


这就很好理解了,路由表就表示,192.168.0.xxx 这个子网下的,都转发到 0 号端口,192.168.1.xxx 这个子网下的,都转发到 1 号端口。下一跳列还没有值,我们先不管

配合着结构图来看(这里把子网掩码和默认网关都补齐了)图中 & 笔误,结果应该是 .0





刚才说的都是 IP 层,但发送数据包的数据链路层需要知道 MAC 地址,可是我只知道 IP 地址该怎么办呢?


答案:arp

假如你(A)此时不知道你同伴 B 的 MAC 地址(现实中就是不知道的,刚刚我们只是假设已知),你只知道它的 IP 地址,你该怎么把数据包准确传给 B 呢?

答案很简单,在网络层,我需要把 IP 地址对应的 MAC 地址找到,也就是通过某种方式,找到 192.168.0.2 对应的 MAC 地址 BBBB

这种方式就是 arp 协议,同时电脑 A 和 B 里面也会有一张 arp 缓存表,表中记录着 IP 与 MAC 地址对应关系。


IP 地址MAC 地址
192.168.0.2BBBB


一开始的时候这个表是空的,电脑 A 为了知道电脑 B(192.168.0.2)的 MAC 地址,将会广播一条 arp 请求,B 收到请求后,带上自己的 MAC 地址给 A 一个响应。此时 A 便更新了自己的 arp 表。

这样通过大家不断广播 arp 请求,最终所有电脑里面都将 arp 缓存表更新完整。




总结一下



好了,总结一下,到目前为止就几条规则

从各个节点的视角来看

电脑视角

  • 首先我要知道我的 IP 以及对方的 IP

  • 通过子网掩码判断我们是否在同一个子网

  • 在同一个子网就通过 arp 获取对方 mac 地址直接扔出去

  • 不在同一个子网就通过 arp 获取默认网关的 mac 地址直接扔出去

交换机视角:

  • 我收到的数据包必须有目标 MAC 地址

  • 通过 MAC 地址表查映射关系

  • 查到了就按照映射关系从我的指定端口发出去

  • 查不到就所有端口都发出去

路由器视角:

  • 我收到的数据包必须有目标 IP 地址

  • 通过路由表查映射关系

  • 查到了就按照映射关系从我的指定端口发出去(不在任何一个子网范围,走其路由器的默认网关也是查到了)

  • 查不到则返回一个路由不可达的数据包

如果你嗅觉足够敏锐,你应该可以感受到下面这句话

网络层(IP协议)本身没有传输包的功能,包的实际传输是委托给数据链路层(以太网中的交换机)来实现的。

涉及到的三张表分别是

  • 交换机中有 MAC 地址表用于映射 MAC 地址和它的端口

  • 路由器中有路由表用于映射 IP 地址(段)和它的端口

  • 电脑和路由器中都有 arp 缓存表用于缓存 IP 和 MAC 地址的映射关系

这三张表是怎么来的

  • MAC 地址表是通过以太网内各节点之间不断通过交换机通信,不断完善起来的。

  • 路由表是各种路由算法 + 人工配置逐步完善起来的。

  • arp 缓存表是不断通过 arp 协议的请求逐步完善起来的。

知道了以上这些,目前网络上两个节点是如何发送数据包的这个过程,就完全可以解释通了!



那接下来我们就放上本章 最后一个 网络拓扑图吧,请做好 战斗 准备!

图片

这时路由器 1 连接了路由器 2,所以其路由表有了下一条地址这一个概念,所以它的路由表就变成了这个样子。如果匹配到了有下一跳地址的一项,则需要再次匹配,找到其端口,并找到下一跳 IP 的 MAC 地址。

也就是说找来找去,最终必须能映射到一个端口号,然后从这个端口号把数据包发出去。


目的地址下一跳端口
192.168.0.0/24
0
192.168.0.254/32
0
192.168.1.0/24
1
192.168.1.254/32
1
192.168.2.0/24192.168.100.5
192.168.100.0/24
2
192.168.100.4/32
2


这时如果 A 给 F 发送一个数据包,能不能通呢?如果通的话整个过程是怎样的呢?

图片


思考一分钟...


详细过程动画描述:


详细过程文字描述:


1. 首先 A(192.168.0.1)通过子网掩码(255.255.255.0)计算出自己与 F(192.168.2.2)并不在同一个子网内,于是决定发送给默认网关(192.168.0.254)
2. A 通过 ARP 找到 默认网关 192.168.0.254 的 MAC 地址。
3. A 将源 MAC 地址(AAAA)与网关 MAC 地址(ABAB)封装在数据链路层头部,又将源 IP 地址(192.168.0.1)和目的 IP 地址(192.168.2.2)(注意这里千万不要以为填写的是默认网关的 IP 地址,从始至终这个数据包的两个 IP 地址都是不变的,只有 MAC 地址在不断变化)封装在网络层头部,然后发包

图片

4. 交换机 1 收到数据包后,发现目标 MAC 地址是 ABAB,转发给路由器1
5. 数据包来到了路由器 1,发现其目标 IP 地址是 192.168.2.2,查看其路由表,发现了下一跳的地址是 192.168.100.5
6. 所以此时路由器 1 需要做两件事,第一件是再次匹配路由表,发现匹配到了端口为 2,于是将其封装到数据链路层,最后把包从 2 号口发出去。
7. 此时路由器 2 收到了数据包,看到其目的地址是 192.168.2.2,查询其路由表,匹配到端口号为 1,准备从 1 号口把数据包送出去。
8. 但此时路由器 2 需要知道 192.168.2.2 的 MAC 地址了,于是查看其 arp 缓存,找到其 MAC 地址为 FFFF,将其封装在数据链路层头部,并从 1 号端口把包发出去。
9. 交换机 3 收到了数据包,发现目的 MAC 地址为 FFFF,查询其 MAC 地址表,发现应该从其 6 号端口出去,于是从 6 号端口把数据包发出去。
10. F 最终收到了数据包!并且发现目的 MAC 地址就是自己,于是收下了这个包
更详细且精准的过程:


读到这相信大家已经很累了,理解上述过程基本上网络层以下的部分主流程就基本疏通了

图片

每一步包的传输都会有各层的原始数据,以及专业的过程描述

图片

同时在此基础之上你也可以设计自己的网络拓扑结构,进行各种实验,来加深网络传输过程的理解。





后记





至此,经过物理层、数据链路层、网络层这前三层的协议,以及根据这些协议设计的各种网络设备(网线、集线器、交换机、路由器),理论上只要拥有对方的 IP 地址,就已经将地球上任意位置的两个节点连通了。

图片

本文经过了很多次的修改,删减了不少影响主流程的内容,就是为了让读者能抓住网络传输前三层的真正核心思想。同时网络相关的知识也是多且杂,我也还有很多搞不清楚的地方,非常欢迎大家与我交流,共同进步。