读书笔记修炼之道NET开发要

北京治疗白癜风 的医院 https://m-mip.39.net/baidianfeng/mipso_4272326.html
《修炼之道:.NET开发要点精讲》

目录

《修炼之道:.NET开发要点精讲》

第1章另辟蹊径:解读.NET

1.7本章思考位置

第2章高屋建瓴:梳理编程约定

2.2方法与线程的关系位置

2.7线程安全位置

2.8调用与回调位置

2.9托管资源与非托管资源位置

2.13协议位置

第3章编程之基础:数据类型

3.1引用类型与值类型位置

3.3赋值与复制位置

第4章物以类聚:对象也有生命

4.1堆和栈位置

4.2堆中对象的出生与死亡位置

4.3管理非托管资源位置

4.4正确使用IDisposable接口位置

4.6本章思考位置

第5章重中之重:委托与事件

5.1什么是.NET中的委托位置

5.2事件与委托的关系位置

5.3使用事件编程位置

5.4弱委托位置

5.5本章回顾位置

第6章线程的升级:异步编程模型

6.1异步编程的必要性位置

6.2委托的异步调用位置

6.3非委托的异步调用位置

6.6本章思考位置

第7章可复用代码:组件的来龙去脉

7.1.NET中的组件位置

7.2容器–组件–服务模型位置

7.3设计时与运行时位置

7.4控件位置

第8章经典重视:桌面GUI框架揭秘

8.2Win32应用程序的结构位置

8.4WindowsForms框架位置

8.5Winform程序的结构位置

第9章沟通无碍:网络编程

9.1两种Socket通信方式位置

9.3UDP通信的实现位置

9.4异步编程在网络编程中的应用位置

9.6本章思考位置

第10章动力之源:代码中的“泵”

10.2常见的“泵”结构位置

第11章规绳矩墨:模式与原则

11.1软件的设计模式位置

11.5本章思考位置

第12章难免的尴尬:代码依赖

12.1从面向对象开始位置

12.2不可避免的代码依赖位置

12.3降低代码依赖位置

12.4框架的“代码依赖”位置

12.6本章思考位置

作者:周见智;博图轩

第1章 另辟蹊径:解读.NET1.7 本章思考位置

1.简述.NET平台中CTS、CLS以及CLR的含义与作用。

A:CTS指公共类型系统,是.NET平台中各种语言必须遵守的类型规范;CLS指公共语言规范,是.NET平台中各种语言必须遵守的语言规范;CLR指公共语言运行时,它是一个虚拟机,.NET平台中所有的托管代码均需要在CLR中运行,可将其视为另外一个操作系统。

第2章 高屋建瓴:梳理编程约定2.2 方法与线程的关系位置

只要我们确定了两个方法只会运行在同一个线程中,那么这两个方法就不可能同时执行,跟方法所处的位置无关。

只可能一前一后执行,此时我们不需要考虑方法中访问的公共资源的线程是否安全。

2.7 线程安全位置

如果操作一个对象(比如调用它的方法或者给属性赋值)为非原子操作,即可能操作还没完成就暂停了,这个时候如果有另外一个线程开始运行同时也操作这个对象,访问了同样的方法(或属性),那么这时就可能会出现一个问题:前一个操作还未结束,后一个操作就开始了,前后两个操作一起出现混乱。当多个线程同时访问一个对象时,如果每次执行都得到不一样的结果,甚至出现异常,那么这个对象便是“非线程安全”。造成一个对象非线程安全的因素有很多,除了前面提到的由于非原子操作执行到一半就中断以外,还有一种情况是由多个CPU造成的,即就算操作没有中断,由于多个CPU可以真正实现多线程同时运行,所以就有可能出现“对同一对象同时操作出现混乱”的情况,如图2-4所示。

2.7 线程安全位置

在Winform编程中,我们之所以经常会遇见“不在创建控件的线程中访问该控件”的异常,原因就是对UI控件的操作几乎都不是线程安全的(部分是)。一般UI控件只能由UI线程操作,其余的所有操作均需要投递到UI线程之中执行,否则就会像前面讲过的那样,程序出现异常或不稳定。

可以使用Control.InvokeRequired属性去判断当前线程是否是创建控件的线程(UI线程),如果是,则该属性返回false,可以直接操作UI控件,否则,返回true,不能直接操作UI控件。

注:Control含有若干个线程安全的方法和属性,常见的主要有Control.InvokeRequired属性、Control.Invoke方法、Control.BeginInvoke方法(Control.Invoke的异步版本)、Control.EndInvoke方法以及Control.CreateGraphics方法。这些属性和方法都可以在非UI线程中使用,并且跨线程访问这些方法和属性时不会引起程序异常。

2.8 调用与回调位置

.NET平台开发中的回调主要是通过委托来实现的。委托是一种代理,专门负责调用方法。

2.9 托管资源与非托管资源位置

.NET中对象使用到的非托管资源主要有I/O流、数据库连接、Socket连接、窗口句柄等各种直接与操作系统相关的资源。

2.13 协议位置

图2-11 网络七层协议

第3章 编程之基础:数据类型3.1 引用类型与值类型位置

通常值类型又分为以下两个部分。

(1)简单值类型:包括类似int、bool、long等.NET内置类型。它们本质上也是一种结构体。

(2)复合值类型:使用Struct关键字定义的结构体,比如System.Drawing.Point等。复合值类型可以由简单值类型和引用类型组成。

3.3 赋值与复制位置

引用类型的赋值、浅复制、深复制的区别,如图3-15所示。

值类型的赋值、浅复制、深复制的区别,如图3-16所示。

对象深复制的过程,如图3-17所示。

3.3 赋值与复制位置

NET中可以使用“序列化和反序列化”的技术实现对象的深复制,只要一个类型及其所有成员的类型都标示为“可序列化”,那么就可以先序列化该类型对象到字节流,然后再将字节流反序列化成源对象的副本。这样一来,源对象与副本之间没有任何关联,从而达到深复制的效果。

第4章 物以类聚:对象也有生命4.1 堆和栈位置

栈主要用来记录程序的运行过程,它有严格的存储和访问顺序;而堆主要存储程序运行期间产生的一些数据,几乎没有顺序的概念

4.1 堆和栈位置

堆跟栈的本质都是一段内存块。

4.2 堆中对象的出生与死亡位置

栈中的对象由系统负责自动存入和移除,正常情况下,跟我们程序开发的关联并不大。

4.2 堆中对象的出生与死亡位置

谨慎使用对象的析构方法。析构方法由CLR调用,不受程序控制,而且容易造成对象重生。析构方法除了用作管理非托管资源外,几乎不能用作其他用途。

4.3 管理非托管资源位置

GC.SuppressFinalize方法请求CLR不要再调用本对象的析构方法,原因很简单,既然非托管资源已经释放完成,那么CLR就没必要再继续调用析构方法。

注:CLR调用对象的析构方法是一个复杂的过程,需要消耗非常大的性能,这也是尽量避免在析构方法中释放非托管资源的一个重要原因, 是彻底地不调用析构方法。

4.4 正确使用IDisposable接口位置

如果一个类型使用了非托管资源,或者它包含使用了非托管资源的成员,那么开发者就应该应用“Dispose模式”:正确地实现(间接或直接)IDisposable接口,正确地重写Dispose(booldisposing)虚方法。

4.6 本章思考位置

调用一个对象的Dispose()方法后,并不意味着该对象已经死亡,只有GC将对象实例占用的内存回收后,才可以说对象已死。但是通常情况下,在调用对象的Dispose()方法后,由于释放了该对象的非托管资源,因此该对象几乎就处于“无用”状态,“等待死亡”是它正确的选择。

第5章 重中之重:委托与事件5.1 什么是.NET中的委托位置

像声明一个普通方法一样,提供方法名称、参数、访问修饰符以及返回值,然后在前面加上Delegate关键字,这样就定义了一个委托类型。

委托类型定义完成后,怎样去实例化一个委托对象呢?其实很简单,跟实例化其他类型对象一样,我们可以通过new关键字创建委托对象。

5.1 什么是.NET中的委托位置

使用委托调用方法时,我们可以直接使用“委托对象(参数列表);”这样的格式,它等效于“委托对象.Invoke(参数列表)”。

给委托赋值的另外一种方式是:委托对象=方法。

怎样让一个委托同时调用两个或者两个以上的方法呢?答案是直接使用加法赋值运算符(=)将多个方法附加到委托对象上。

5.1 什么是.NET中的委托位置

委托内部的“链表”结构跟单向链表的实现原理却不相同。它并不是通过Next引用与后续委托建立关联,而是将所有委托存放在一个数组中,如图5-6所示。

每一个委托类型都有一个公开的GetInvocationList()的方法,可以返回已附加到委托对象上的所有委托,即图5-6中数组列表部分。另外,我们平时不区分委托对象和委托链表,提到委托对象,它很有可能就表示一个委托链表,这跟单向链表只包含一个节点时道理类似。

5.1 什么是.NET中的委托位置

委托跟String类型一样,也是不可改变的。换句话说,一旦委托对象创建完成后,这个对象就不能再被更改,那么我们前面讲到的将一个委托附加到另外一个委托对象上形成一个委托链表又该如何解释呢?其实这个跟String.ToUpper()过程类似,我们对委托进行附加、移除等操作都会产生一个全新的委托,这些操作并不会改变原有委托对象。

5.1 什么是.NET中的委托位置

框架有两种调用框架使用者编写的代码的方式,一种便是面向抽象编程,即框架中尽量不出现某个具体类型的引用,而是使用抽象化的基类引用或者接口引用代替。只要框架使用者编写的类型派生自抽象化的基类或实现了接口,框架均可以正确地调用它们。

框架调用框架使用者代码的另外一种方式就是使用委托,将委托作为参数(变量)传递给框架,框架通过委托调用方法。

5.2 事件与委托的关系位置

问题,.NET中提出了一种介于public和private之间的另外一种访问级别:在定义委托成员的时候给出event关键字进行修饰,前面加了event关键字修饰的public委托成员,只能在类外部进行附加和移除操作,而调用操作只能发生在类型内部。

我们把类中设置了event关键字的委托叫作“事件”。“事件”本质上就是委托对象。事件的出现,限制了委托调用只能发生在一个类型的内部,如图5-12所示。

在图5-12中由于server中的委托使用了event关键字修饰,因此委托只能在server内部调用,对外部也只能进行附加和移除方法操作。当符合某一条件时,server内部会调用委托,这个时间不由我们(client)控制,而是由系统(server)决定。因此大部分时候,事件在程序中起到了回调作用。

调用加了event关键字修饰的委托也称为“激发事件”。其中,调用方(图5-12中的server)被称为“事件发布者”;被调用方(图5-12中的client)被称为“事件注册者”(或“事件观察者”、“事件订阅者”等,本书中统一称之为“事件注册者”);附加委托的过程被称之为“注册事件”(或“绑定事件”、“监听事件”、“订阅事件”等,本书中统一称之为“注册事件”);移除委托的过程被称之为“注销事件”。通过委托调用的方法被称为“事件处理程序”。

5.3 使用事件编程位置

在调用委托链时,如果某一个委托对应的方法抛出了异常,那么剩下的其他委托将不会再调用。这个很容易理解,本来是按先后顺序依次调用方法,如果其中某一个抛出异常,剩下的肯定会被跳过。为了解决这个问题,单单是将激发事件的代码放在try/catch块中是不够的,我们还需要分步调用每个委托,将每一步的调用代码均放在try/catch块中。

5.3 使用事件编程位置

除了事件本身的命名,事件所属委托类型的命名也同样有标准格式,一般以“事件名EventHandler”这种格式来给委托命名,因此前面提到的NewEmailReceived事件对应的委托类型名称应该是“NewEmailReceivedEventHandler”)。激发事件时会传递一些参数,这些参数一般继承自EventArgs类型(后者为.NET框架预定义类型),以“事件名EventArgs”来命名,比如前面提到的NewEmailReceived事件在激发时传递的参数类型名称应该是“NewEmailReceivedEventArgs”。

5.3 使用事件编程位置

之所以要把激发事件的代码放在一个单独的虚方法中,是为了让从该类型(EmailManager)派生出来的子类能够重写虚方法,从而改变激发事件的逻辑。

虚方法的命名方式一般为“On事件名”。另外,该代码中的虚方法必须定义为“protected”,因为派生类中很可能要调用基类的虚方法。

5.3 使用事件编程位置

图5-13 属性和事件的作用

5.4 弱委托位置

在事件编程中,委托的Target成员,就是对事件注册者的强引用,如果事件注册者没有注销事件,强引用Target便会一直存在,堆中的事件注册者内存就一直不会被CLR回收,这对开发人员来讲,几乎是很难发觉的。

像“Aa=newA();”中的a被称为“显式强引用(ExplicitStrongReference)”,类似委托中包含的不明显的强引用,我们称之为“隐式强引用(ImplicitStrongReference)”。

弱引用与对象实例之间属于一种“弱关联”关系,跟强引用与对象实例的关系不一样,就算程序中有弱引用指向堆中对象实例,CLR还是会把该对象实例当作回收目标。程序中使用弱引用访问对象实例之前必须先检查CLR有没有回收该对象内存。换句话说,当堆中一个对象实例只有弱引用指向它时,CLR可以回收它的内存。使用弱引用,堆中对象能否被访问,同时掌握在程序和CLR手中。

创建一个弱引用很简单,使用WeakReference类型,给它的构造方法传递一个强引用作为参数即可。

5.4 弱委托位置

在编程过程中,由于很难管理好强引用,从而造成不必要的内存开销。尤其前面讲到的“隐式强引用”,在使用过程中不易发觉它们的存在。弱引用特别适合用于那些对程序依赖程度不高的对象,即那些对象生命期主要不是由程序控制的对象。比如事件编程中,事件发布者对事件注册者的存在与否不是很关心,如果注册者在,那就激发事件并通知注册者;如果注册者已经被CLR回收内存,那么就不通知它,这完全不会影响程序的运行。

5.4 弱委托位置

前面讲到过,委托包含两个部分:一个Object类型的Target成员,代表被调用方法的所有者,如果方法为静态方法,则Target为null;另一个是MethodInfo类型的Method成员,代表被调用方法。由于Target成员是一个强引用,所以只要委托存在,那么方法的所有者就会一直在堆中存在而不能被CLR回收。如果我们将委托中的Target强引用换成弱引用的话,那么不管委托存在与否,都不会影响方法的所有者在堆中内存的回收。这样一来,我们在使用委托调用方法之前,需要先判断方法的所有者是否已经被CLR回收。我们称将Target成员换成弱引用之后的委托为“弱委托”,弱委托定义代码如下:

//Code5-26classWeakDelegate{WeakReference_weakRef;// MethodInfo_method;//NO.2publicWeakDelegate(Delegated){_weakRef=newWeakReference(d.Target);_methodInfo=d.Method;}publicobjectInvoke(paramobject[]args){objectobj=_weakRef.Target;if(_weakRef.IsAlive)//NO.3{return_method.Invoke(obj,args);//NO.4}else{returnnull;}}}

弱委托将委托与被调用方法的所有者之间的关系由“强关联”转换成了“弱关联”,方法的所有者在堆中的生命期不再受委托的控制,如图5-16所示,为弱委托的结构。

本小节示例代码中的WeakDelegate类型并没有提供类似Delegate.Combine以及Delegate.Remove这样操作委托链表的方法,当然也没有弱委托链表的功能。这些功能可以仿照单向链表的结构去实现,把每个弱委托都当作链表中的一个节点。其方法可参照5.1.2小节中讲到的单向链表。

5.5 本章回顾位置

委托的3个作用: ,它允许把方法作为参数,传递给其他的模块;第二,它允许我们同时调用多个具有相同签名的方法;第三,它允许我们异步调用任何方法。这3个作用奠定了委托在.NET编程中的 重要地位。

第6章 线程的升级:异步编程模型6.1 异步编程的必要性位置

通常情况下,调用一个方法都符合这样一个规律:调用线程开始调用方法A后,在A返回之前,调用线程得不到程序执行的控制权。也就是说,方法A后面的代码是不可能执行的,直到A返回为止,这种调用方式被称之为“同步调用”;相反,如果调用在返回之前,调用线程依旧保留控制权,能够继续执行后面的代码,那么这种调用方式被称为“异步调用”。

同步调用也被一些学者称为“阻塞调用”;一些相对的异步调用则被称为“非阻塞调用”。

6.2 委托的异步调用位置

理论上讲,任何一个方法,通过委托包装后,都可以实现异步调用。

.NET编译器定义的每个委托类型都自动生成了两个方法:BeginInvoke和EndInvoke。这两个方法专门用来负责异步调用委托。

BeginInvoke返回一个IAsyncResult接口类型,它可以 区分一个异步调用过程。BeginInvoke一执行就能马上返回,不会阻塞调用线程。EndInvoke表示结束对委托的异步调用,但这并不意味着它可以中断异步调用过程,如果异步调用还未结束,EndInvoke则只能等待,直到异步调用过程结束。另外,如果委托带有返回值,我们必须通过EndInvoke获得这个返回结果。

6.2 委托的异步调用位置

委托异步调用开始后,系统会在线程池中找到一个空闲的线程去执行委托。

6.2 委托的异步调用位置

异步调用委托时,由于方法实际运行在其他线程中(线程池中的某一线程,非当前调用线程),因此当前线程捕获不了异常,那么我们怎样才能知道异步调用过程中到底是否会有异常呢?答案就在EndInvoke方法上,如果异步调用过程有异常,那么该异常就会在我们在调用EndInvoke方法时抛出。所以我们在调用EndInvoke方法时,一定要把它放在try/catch块中。

6.3 非委托的异步调用位置

.NET中提供异步方法的类型有Stream(或其派生类)、Socket(或其派生类)以及访问数据库的SqlConnection类型等。它们的使用方式跟委托的BeginInvoke和EndInvoke方法类似,只是命名有所差别,基本上都是“Begin操作”和“End操作”这种格式。比如FileStream.BeginRead表示开始一个异步读操作,而FileStream.EndRead则表示结束异步读操作。

6.6 本章思考位置

异步编程与多线程编程的效果类似,都是为了能够并行执行代码,达到同时处理任务的目的。异步编程时,系统自己通过线程池来分配线程,不需要人工干预,异步编程逻辑复杂不易理解,而多线程编程时,完全需要人为去控制,相对较灵活。

第7章 可复用代码:组件的来龙去脉7.1 .NET中的组件位置

在.NET编程中,我们把实现(直接或者间接)System.ComponentModel.IComponent接口的类型称为“组件”,

7.1 .NET中的组件位置

组件和控件不是相等的,组件包含控件,控件只是组件中的一个分类。

7.1 .NET中的组件位置

图7-2 WindowsForms中控件之间的关系

所有的控件均派生自Control类,Control类又属于组件,因此所有控件均具备组件的特性。

不管组件还是控件,它们都是可以重复使用的代码集合,都实现了IDisposable接口,都需要遵循第4章中讲到的Dispose模式。如果一个类型使用了非托管资源,它实现IDisposable接口就可以了,那为什么还要在.NET编程中又提出组件的概念呢?

这样做可以说完全是为了实现程序的“可视化开发”,也就是我们常说的“所见即所得”。在类似VisualStudio这样的开发环境中,一切“组件”均可被可视化设计,换句话说,只要我们定义的类型实现了IComponent接口,那么在开发阶段,该类型就可以出现在窗体设计器中,我们就可以使用窗体设计器编辑它的属性、给它注册事件。它还能被窗体设计器中别的组件识别。

7.2 容器–组件–服务模型位置

在.NET编程中,把所有实现(直接或间接)System.ComponentModel.IContainer接口的类型都称之为逻辑容器(以下简称“容器”)。

容器是为组件服务的。

.NET框架中有一个IContainer接口的默认实现:System.ComponentModel.Container类型,该类型默认实现了IContainer接口中的方法以及属性。

7.2 容器–组件–服务模型位置

传统容器仅仅是在空间上简单地将数据组织在一起,并不能为数据之间的交互提供支持。而本章讨论的逻辑容器,在某种意义上讲,更为高级。它能为组件(逻辑元素)之间的通信提供支持,组件与组件之间不再是独立存在。此外,它还能直接给组件提供某些服务。物理容器和逻辑容器分别与元素之间的关系,如图7-4所示。

物理容器中的元素之间不能相互通信,物理容器也不可能为其内部元素提供服务;逻辑容器中的组件之间可以通过逻辑容器作为桥梁,进行数据交换;同时,逻辑容器还能给各个组件提供服务。所谓服务,就是指逻辑容器能够给组件提供一些访问支持。比如某个组件需要知道它的所属容器中共包含有多少个组件,那么它就可以向容器发出请求。容器收到请求后会为它返回一个获取组件总数的接口。

在本章7.1.1小节中我们提到过IComponent接口中有一个ISite类型的属性。当时说它是起到一个“定位”的作用。现在看来,组件与容器之间的纽带就是它,组件通过该属性与它所属容器取得了联系。

7.2 容器–组件–服务模型位置

Component、Site以及Container3个类型均包含有获取服务的方法GetService。现在我们可以整理一下组件向容器请求服务的流程,如图7-6所示。

注:容器将组件添加进来时(执行Container.Add),会初始化该组件的Site属性,让该组件与容器产生关联,只有当这一过程发生之后,组件才能获取容器的服务。

7.2 容器–组件–服务模型位置

在我们向窗体设计器中拖动控件时,是会执行类似“newButton();”这样的代码,在内存中实例化一个组件实例。

7.2 容器–组件–服务模型位置

图7-10 窗体设计器中的组件与生成的源代码

在图7-10中,图中左边显示我们拖放到设计器中的一个Button控件。在这个过程中,窗体设计器除了会实例化一个Button控件(图中左边Form2中),还会为我们生成右边的代码。

7.3 设计时与运行时位置

任何组件都有两种状态:设计时和运行时。

判断组件的当前状态有以下两种方法。

(1)判断组件的DesignMode属性。每个组件都有一个Bool类型的DesignMode属性,正如它的字面意思,如果该属性为true,那么代表组件当前处于设计时状态;否则该组件处于运行时状态。

(2)随便请求一个服务,看返回来的服务接口是否为null。前面提到过,当一个组件不属于任何一个容器时,那么它通过GetService方法请求的服务肯定返回为null。

注:(1)(2)方法均不适合嵌套组件,因为窗体设计器只会将最外层组件的DesignMode属性值设置为true。

有一种可以解决嵌套组件中无法判断其子组件状态的方法,那就是通过Process类来检查当前进程的名称。看它是否包含“devenv”这个字符串。如果有,那么说明组件当前处于VisualStudio开发环境中(即组件处于设计时),if(Process.GetCurrentProcess().ProcessName.Contains(”devenv”))为假,说明组件处于运行时。这种方法也有一个弊端,很明显,如果我们使用的不是VisualStudio开发环境(即进程名不包含devenv),或者我们自己的程序进程名称本身就已经包含了devenv,那么该怎么办呢?

在开发一些需要授权的组件时,就可以用到组件的两种状态。这些需要授权的组件收费对象一般是开发者。因此,在开发者使用这些组件开发系统的时候(处于开发阶段),就应该有授权入口,而当程序运行之后,就不应该出现授权的界面。

7.4 控件位置

无论是复合控件、扩展控件还是自定义控件,我们均可以重写控件的窗口过程:WndProc虚方法,从根源上接触到Windows消息,这个做法并不是自定义控件的专利。

第8章 经典重视:桌面GUI框架揭秘8.2 Win32应用程序的结构位置

程序是无法直接识别用户键盘或者鼠标等设备的输入信息,这些输入必须先由操作系统转换成固定格式数据之后,才能被程序使用。

在Windows编程中,我们把由操作系统转换之后的固定格式数据称为Windows消息。Windows消息是一种预定义的数据结构(比如C中的Struct),它包含有消息类型、消息接收者以及消息参数等信息。我们还把程序中获取Windows消息的结构称之为Windows消息循环。Windows消息循环在代码中就是一个循环结构(比如while循环),它不停地从操作系统中获取Windows消息,然后交给程序去处理。

8.2 Win32应用程序的结构位置

在Windows中,其实将消息分成了两类,一类需要存入消息队列,然后由消息循环取出来之后才能被窗口过程处理,这类消息被称之为“队列消息”(QueuedMessage)。这类消息主要包括用户的鼠标键盘输入消息、绘制消息WM_PAINT、退出消息WM_QUIT以及时间消息WM_TIMER。另一类是不需要存入消息队列,也不经过消息循环,它们直接传递给窗口过程,由窗口过程直接处理,这类消息被称之为“非队列消息”(NonqueuedMessage)。当操作系统想要告诉窗口发生了某件事时,它会给窗口发送一个非队列消息,比如当我们使用SetWindowPosAPI移动窗口后,系统自动会发送一个WM_WINDOWPOSCHANGED消息给该窗口的窗口过程,告诉它位置发生变化了。

8.4 WindowsForms框架位置

在WindowsForms框架中,以Control为基类,其他所有与窗体显示有关的控件几乎都派生自它;Control类中的WndProc虚方法就是我们在Win32开发模式中所熟悉的窗口过程。另外,前面也讲到过,窗体和控件本质上是一个东西,只是它们有着不同的属性,所以我们可以看到,窗体类Form间接派生自Control类。

Winform程序中包含3个部分:消息队列、UI线程以及控件。

8.5 Winform程序的结构位置

在每个Winform程序的Program.cs文件中,都有一个Main方法,该Main方法就是程序的入口方法。每个程序启动时都会以Main方法为入口,创建一个线程,这个线程就是UI线程。可能你会问UI线程怎么没有消息循环呢?那是因为Main方法中总是会出现一个类似Application.Run的方法,而消息循环就隐藏在了该方法内部(具体参见下一小节内容)。一个程序理论上可以有多个UI线程,且每个线程都有自己的消息队列(由操作系统维护)、消息循环、窗体等元素,如图8-13所示。

由于UI线程之间的数据交换比较复杂,因此在实际开发中,在没有特殊需求的情况下,一个程序一般只包含有一个UI线程。

8.5 Winform程序的结构位置

对UI线程的认识:

①一个程序可以包含有多个UI线程,我们完全可以通过System.Threading.Thread类新创建一个普通线程,然后在该线程中调用Application.Run方法来开启消息循环;

②一个UI线程结束(该线程中的消息循环个数为“0”)后,将会激发Application.ThreadExit事件,告知有UI线程结束,只有当所有UI线程都结束(程序中消息循环总数为“0”),才会激发Application.Exit事件,告知应用程序退出。

我们再来看一下WindowsForms中消息循环的结构图,如图8-14所示。

8.5 Winform程序的结构位置

WindowsForms框架将窗体和窗口过程封装在了一起,Control(或其派生类,下同)类中的WndProc虚方法就是控件的窗口过程。之所以将窗口过程声明为虚方法,这是因为WindowsForms框架是面向对象的,它充分地利用了面向对象编程中的“多态”特性。因此,所有Control类的派生类均可以重写它的窗口过程,从而从源头上拦截到Windows消息,处理自己想要处理的Windows消息。

窗口过程做了一个非常重要的事:将Window消息转换成了.NET中的事件。

第9章 沟通无碍:网络编程9.1 两种Socket通信方式位置

(TCP)数据是按顺序走在建立的一条隧道中,那么数据就应该遵循“先走先到达”的规则,并且隧道中的数据以“流”的形式传输。发送方发送的前后两次数据之间没有边界,需要接收方自己根据事先规定好的“协议”去判断数据边界。

9.1 两种Socket通信方式位置

UDP通信中,数据是以“数据报”的形式传输,以一个整体发送、以一个整体接收,因此UDP存在数据边界。但是UDP接收到的数据是无序的,先发送的可能后接收,后发送的可能先接收,甚至有的接收不到。

9.1 两种Socket通信方式位置

在.NET中有关Socket通信编程的类型主要有5种,见表9-1。

TcpListener和TcpClient的关系如图9-9所示

图9-9中,TcpListener侦听来自客户端的“连接”请求,返回一个代理TcpClient,该代理与客户端的TcpClient进行数据交换。

UdpClient在UDP通信中所处的角色如图9-10所示:

NetworkStream类型是System.IO.Stream类的一个派生类。NetworkStream只能用于TCP通信中。

Socket类型是一个相对较基础的通信类型。它既能实现TCP通信也能实现UDP通信,可以认为TcpListener、TcpClient以及UdpClient进一步封装了Socket类型。

9.3 UDP通信的实现位置

之所以将TCP通信中应用层协议的数据结构设计成字节流的形式,是因为TCP通信中数据是以流的形式传输,以字节流的形式格式化数据后,更方便程序判断数据边界。

而在UDP通信中,数据以数据报的形式传输,每次接收到的数据是完整的,对于当前局域网即时通信实例来讲,协议使用文本的形式更方便程序处理数据,当然,我们完全也可以将UDP通信中应用层协议的数据结构设计成字节流的形式。

9.4 异步编程在网络编程中的应用位置

使用Socket.BeginSendTo()方法开始一个异步发送过程,并为该方法提供一个AsyncCallback的回调参数。该方法的调用不会阻塞调用线程。我们在回调方法OnSend中可使用Socket.EndSendTo()方法,结束异步发送过程,该方法返回实际发送的数据长度。

9.4 异步编程在网络编程中的应用位置

异步编程也能实现循环接收数据,但却看不到显式地创建的线程,也看不到类似while这样的循环语句。

9.6 本章思考位置

所有的通信协议本质上都是一种数据结构。通信双方都必须按照这种数据结构规定的形式去发送或接收(解析)数据。

基于TCP协议的通信在进行数据交互之前需要先建立连接,类似打电话。这种通信方式保证了数据传输的正确性、可靠性。基于UDP协议的通信在进行数据传输之前不需要建立连接,类似发短信。这种通信方式不能保证数据传输的正确性。

第10章 动力之源:代码中的“泵”10.2 常见的“泵”结构位置

桌面程序的UI线程中包含一个消息循环(确切地说,应该是While循环)。该循环不断地从消息队列中获取Windows消息,最终通过调用对应的窗口过程,将Windows消息传递给窗口过程进行处理。

10.2 常见的“泵”结构位置

浏览器每次发送


转载请注明:http://www.xxcyfilter.com/zytd/zytd/11221.html