第6讲:如何使用.NET开发Windows应用程序
2004.12.21 欧岩亮
课程介绍
深入Microsoft .NET Framework
基础内容
熟悉.NET
课程内容
程序的托管执行(Managed Execution)
程序集(Assemblies)
名称空间(Namespaces)
委托(Delegates)
线程
应用程序域(AppDomains)
Framework类
程序中的属性(Attribute)
数据类型
反射(Reflection)
Framework编程
托管执行
托管代码和非托管代码
托管代码是第一次编译形成中间代码以后的代码,它在进入执行的时候,要进行第二次编译。它不是计算机上的本地代码,而是一种中间形式的代码。
托管代码和非托管代码的本质区别:一个是中间代码来表示;一个是本地的机器语言。
公共语言运行时(Common Language Runtime,CLR)
当托管代码被编译器第一次编译以后,在应用程序被加载的时候,公共语言运行时会提供一些类的加载库。把托管代码中的程序加载到内存之后,它会通过JIT实时地把托管代码编译为本地代码然后执行。
中间语言(Intermediate Language,IL)
经过编译器第一次编译以后形成的应用程序集应该是用中间语言来表示的托管代码,在真正执行的时候,CLR会把托管代码加载到内存当中并且进行实时编译。这种托管代码其实是多平台一致的,但是在真正运行的时候,会根据当前平台上的一些代码的特性进行实时编译形成本地代码。
ILDASM
这个工具可以用来反编译托管代码。
公共类型系统
保证了多语言的互操作。
内存管理
非确定性的内存回收:当我们把指针设置为null的时候,指针这时并没有被释放,因为有垃圾回收机制。
垃圾回收:周期性地回收没有被指针指向的内存空间。
IDispose:给开发人员一定的机会来回收非托管代码中消耗的资源。托管资源的内存由垃圾回收自动回收,而在托管代码中引用的非托管对象,垃圾回收是不负责回收的,例如文件句柄,当我们要使用文件句柄时,我们一定要实现IDispose接口来主动回收资源。
托管代码的执行过程
代码第一次编译形成IL中间语言的托管代码,在运行时被Class Loader装载后进行JIT第二次编译形成托管的本地代码。在执行过程中,它会不断地检查当前我们执行的代码的安全性和规范性。
Class Loader在装载可执行程序exe或者动态链接库dll的时候,它不是把所有的exe和dll当中的类库全部装载到内存里面。它是先装载一部分,即Main函数所在的文件,然后在执行过程中Class Loader会不断地判断当前执行过程中所要调用的方法是否已被装载到内存中了,如果没有,它会实时地去装载一些没有被装载的代码。装载进来之后,被编译为托管的本地代码,然后调用。
公共语言运行时(CLR)
线程支持
类型检查
安全引擎
MSIL到本地编译
代码管理器
垃圾回收
类装载器
COM Marshaller:COM组件的互操作
异常管理
调试器
公共类型系统
创建了一个框架,能够帮助实现不同语言之间的互操作,类型安全和高性能的代码执行
提供了一套统一的面向对象模型,可以完全的支持所有的语言
定义的一套语言规范,能够帮助不同语言之间进行交互
程序集(Assemblies)
一个程序集是一组类型和资源的集合,共同组成一定的逻辑功能
包含一个类型或程序的清单(manifest),类型原数据,MSIL,资源
程序集清单描述了应用程序集里面都有哪些类型,这些程序都存放在什么地方。它规定了我的这些代码都在什么地方,可能在磁盘的某个位置,另外的一部分资源可能在Internet上。Manifest最终被Class Loader所用,Class Loader在动态加载类库的时候,就需要知道类库在哪个位置的哪个文件里。我们在编写应用程序的时候,实际上我们可以把程序集清单里面的Assembly的位置描述为在一个互联网上的Http地址,Class Loader在实时地加载这个类的时候它会从Http这个路径去加载远程服务器上的Library过来,这杨就实现了零部署,我们不用在更新了Dll之后强迫客户端更新。
所有能够部署的单元都是编译过的MSIL(可执行的中间代码)
轻便的可执行文件(PE file)EXE或者是DLL
在.NET Framework程序在执行的时候,它有一个公共类型系统,这些公共类型系统的实现都在CLR里面。我们在运行程序的时候,使用到了大量的Framework中所定义好的类库,而客户端在执行应用程序的时候实际上已经安装了Framework,也就是说它已经帮我们在客户端部署了很多已经存在的功能。我们的程序在完成后,有很多类库不需要包装在我们的执行文件里面,我们只需要把执行文件放到客户端,去调用客户端的Framework里面的类库。这样我们编写的应用程序规模就很小,但它执行起来内存占用量比较大。
可以用ILDASM和反射(Reflection)来检查程序集
可以是单一的文件或多个文件
名称空间
名称空间是一个命名的容器
名称空间可以按照层次的方式来组织类
避免命名冲突
帮助提示类的用途
名称空间可以跨越工程/程序集
推荐:CompanyName.Project<.Module>.Class
举例:Northwind.OrderEntry.Order
演示一
程序集和名称空间
Delegates
Delegate实际上是.NET中的类,是一个强类型的函数指针
主要用于事件处理和回调
多播的Delegate:Combine和Remove方法可以添加或者去除Delegates中的调用列表
可以通过Invoke来调用Delegate指向的方法
可以使用Delegate来完成异步调用,BeginInvoke和EndInvoke方法
Delegate的实现是运行时提供的,用户不用关心
Delegate在运行时决定调用怎样的用户代码,用户需要编写这些代码
演示二
Delegates
运行结果
其中932是界面线程的ID,1380是异步调用时的线程ID。也就是说BeginInvoke为我们创建了一个线程来执行我们函数指针所指向的函数。
这样做有一些好处,SimpleDelegateHandler方法位于MainClass类内部,它能访问MainClass里面的一些私有成员,而DelegateExample类不能访问,因此将SimpleDelegateHandler方法传入DelegateExample类的CallMeBack函数,就可以完成调用。也就是说CallMeBack里面如果想去更新或者设置MainClass里面的私有成员,我们可以在MainClass里创建一个Delegate,通过外部的DelegateExample类里的CallMeBack,做一个回调,回调到SimpleDelegateHandler方法里,在这个方法里就能访问MainClass里的私有成员。
线程
具有优先级的多任务操作系统——“时间片”将时钟周期分配给多个线程
多线程技术可以在工作线程执行长时间计算的同时,相应用户的UI操作
System.Threading.Thread类实际上描述的就是一个系统线程
ThreadPool.QueueUserWorkItem()可以异步地执行一些操作,通过使用系统的线程池来完成
将你使用的线程数量降低到最少!
AppDomains
AppDomain是一个独立的应用程序运行环境
它是一个逻辑空间,是线程与进程之间的一种东西。一个.NET进程出现了以后,这个.NET进程会去装载必要的东西,然后创建一个AppDomain,在AppDomain里面实现多线程。
AppDomain在执行托管代码时提供分离应用程序的能力、卸载应用程序的功能和安全边界
举个例子,当我们的Hello World程序加载的时候,.NET Framework的CLR会为我们创建一个SystemDomain,这个SystemDomain会去装载mscorlib,即微软的核心运行库。这个核心运行库运行在SystemDomain里面,还会创建一个SharedDomain,SharedDomain会去装载GAC中所具有的程序集。最后创建一个Domain0,来装载界面线程,或主线程。
所有的托管代码都在AppDomain中执行
在一个进程中可以执行多个应用程序域
应用程序域与线程之间没有一一对应的关系
一个应用程序域可以拥有多个线程
一个线程可以在一个应用程序域中运行,同时也可以在多个应用程序域之间运行
演示三
AppDomain和线程
在保存按钮被点击的时候,是在主线程内被点击的,在主线程里面创建了一个新的线程,在新的线程里面去执行EmulateReallyLongProcess。在这个方法里面又创建了一个线程,去异步调用DoneDelegateHandler方法。这里一共会出现三个线程。
这个例子同上面的例子类似,当一个窗体里的按钮被按下,我们可以让另外一个线程去工作,但是同时当另外一个线程工作的时候,我们又希望更新当前窗体的界面元素,但是我们这个方法又不在当前界面的内部。即EmulateReallyLongProgress方法想要更改Form中的元素的话,我们只能通过Delegate这种方式来进行回调,回调之后就可以访问类的私有成员。
Framework类库
公共名称空间
System
System.Data
System.Data.Xml
其他的名称空间
System.Windows.Forms
System.Web
System.Configuration
System.IO
System.Security
程序的属性(Attribute)
Attribute是一些.NET的类,它可以在IL中添加一些原数据,来描述程序的一些属性,这些属性是在编译的过程中被识别的
属性可以被用来描述下面任何一种类型:Assembly、Module、Property、Field、Event、Interface、Parameter、Delegate、ReturnValue
Attribute可在运行时通过编写代码进行查找,并且可以通过Attribute来控制代码的执行
在Framework中内嵌了很多Attribute
也可以编写一些自己的Attribute,只需从System.Attribute派生即可
CLSCompliantAttribute
用来指定当前的程序或代码是否符合公共语言运行时的规范(CLS)
这个标签(Attribute)只对程序集、模块、类型或类型的成员起作用
如果不使用CLSCompliantAttribute标签的话,编译器默认:
程序集是CLS不兼容的
如果某个类型是CLS兼容的,那么它所包含的所有类型或程序集都必须是CLS兼容的
如果某个类型的成员是CLS兼容的,那么这个成员的类型必须是CLS兼容类型
数据类型
引用类型
必须被实例化才能使用
类
在托管堆中分配
值类型
没有默认的构造函数(不需要实例化)
简单类型(Primitives)——Integer、float、string、byte
结构
在栈中进行内存分配
演示四
数据类型
C#里面是区分大小写的,第一个函数和第三个函数唯一的区别就是大小写不同。这些方法在C#环境下被编译为IL之后,这三个函数能做有效区分。但是这三个函数如果是在VB环境下编译的话,VB是不区分大小写的,就会报错。所以像这种写法不是CLS兼容的写法。
当我们把AssemblyInfo里面的CLSCompliant标签加上时,也就是我们要求CLS兼容,即使在C#环境里面也会报错。
可以看到出现了四个错误,主要错误有:UInte32类型不是CLS兼容的;两个函数只有大小写不一样不是CLS兼容的,因为有些语言是不区分大小写的。
版本(Versioning)
在程序集中使用强命名
使用工具sn.exe可以生成一个强命名的(strong-name)密钥文件(存有密钥对)
密钥文件里面包含了密钥对(公钥和私钥),当应用程序编译并签名的时候,从密钥文件中拿出私钥,对应用程序集签名实行一定的反列。反列之后,应用程序具有了一定的特征,因为别人没有你的私钥,只有你才能进行签名。然后把sn生成的密钥文件的公钥保存在应用程序集中,当其他程序集来调用这个强命名的程序集时,它会用附带在应用和程序集中的公钥来验证私钥签名的有效性。
使用AssemblyKeyFile标签可以将强命名的密钥编译到应用程序集当中
使用强命名可以用来确认程序集的版本,并将程序集安装到Global Assembly Catch(GAC)当中
演示五
Versioning
使用sn -k命令
生成密钥成功
使用强命名
当一个应用程序集被强命名的时候,它同时要求它所引用的其他程序集也要是强命名的。
强命名实际上是保证了代码不被窜改。一旦代码被窜改,也会保证代码版本的有效性。
当我们把版本号中的*改成固定值时
并且要求主项目对子项目的引用是直接从路径中选择dll文件引用,而不是Project项目引用。(因为如果是Project项目引用,VS也会自动替我们更新整个程序集的版本)
当编译成功之后,我们再只把一个子项目的版本改为1.0.0.1
只编译这个子项目成功之后,不编译主项目,我们把编译好的1.0.0.1版本的子项目的dll替换主项目的bin文件夹下的1.0.0.0版本的dll。
然后我们直接执行应用程序,并使用里面的那个我们更新版本的子项目的功能,这个时候就会出现应用程序集的版本不正确的错误信息。
反射(Reflection)
System.Runtime.Reflection名称空间
反射可以用来在运行时获取元数据信息
找出PE文件(程序集)中的类型和元数据
在运行时动态的使用类型
演示六
反射
这是一个简单的About窗体
Assembly.GetExecutingAssembly可以获得当前执行的应用程序集,然后通过GetCustomAttributes可以获得当前应用程序集的信息。这些应用程序集信息都在AssemblyInfo里面设置。
这样在运行时About窗体就能读出相应程序集信息。
其中列表显示了当前运行的其他程序集信息,这是通过下面的代码获取的
GetEntryAssembly得到当前运行的所有加载的程序集,GetReferenceAssemblies得到当前应用的程序集。
Framework编程
Framework类库
Collections
Arrays
Enums
IEnumerable接口和Foreach(For Each)
只要实现了IEnumerable接口,就能使用Foreach遍历成员
运行时信息
反射
程序集属性、标签
AppDomain
字符串
字符串的内容是固定的,immutable(不可变更)
即字符串一旦形成,它便不可以改变
连接少量的字符串,建议使用String.Format()
若使用大量的+去连接字符串,性能会有损失
使用System.Text.StringBuilder来连接大量的字符串
StringBuilder的AppendFormat()方法
演示七
字符串
带输出的字符串连接,使用System.Diagnostics.Trace.WriteLine可以很方便的输出我们的调试信息,这个尤其是在多线程调试的时候有用,因为多线程在设置断点的时候可能会有些问题。
Main函数
输出面板
总结
.NET Framework包含了一系列有层次的用名称空间划分的可重用组件
MSIL,PE
Attribute可以定制程序集当中各种元素的元数据,或描述信息
反射可以在运行时获得程序集当中的类型,可以方便的实现插件的功能
2010.10.7