前言
Microsoft .net平台组成
底层操作系统:
windows
:
--Microsoft
.NET Passport XML Web服务支持
--即时消息通知应用程序
--.NET 企业服务器:
l
Microsoft Application Center 2000
l
Microsoft BizTalk Server 2000
l
Microsoft Commerce Server 2000
l
Microsoft Exchange 2000
l
Microsoft Host Integration Server 200
l
Microsoft Internet Security and Acceleration(ISA) Server 2000
l
Microsoft Mobile Information Server 2002
l
Microsoft SQL Server 2000
--Microsoft XML Web 服务:.NET My Services
http://www.Microsoft.com/MyServices/
开发平台:.NET FrameWork
有两个部分:通用语言运行时(Common Language Runtime:CLR)和.NET 框架类库(Framework Class Library:FCL)
特征如下:
l 一致的编程模型。完全面向对象机制和丰富的FCL使编程模型一致
l 简化的编程方式。简化Win32和COM环境下所需的复杂基础构造(注册表、GUID、IUnknown、AddRef、Release、HRESULT等等)
l 可靠的版本机制。彻底消除“DLL hell”的大门
l 轻便的部署管理。安装程序不再使用注册表,而是直接进行拷贝
l 广泛的平台支持。对ECMA的CLR和FCL兼容的机器都可以运行
l 无缝的语言集成。遵循通用语言规范(Common Language Specification:CLS)书写的类型可以在不同语言之间互用
l 简便的代码复用。
l 自动化的内存管理(垃圾回收)。对内存以及诸如文件、屏幕空间、网络连接、数据库等资源的管理
l 坚实的类型安全
l 丰富的调试支持。CLR完全支持跨语言调试
l 统一的错误报告。强大的异常处理机制;CLR中的异常具有跨模块和跨语言特性
l 全新的安全策略。CLR中的代码访问安全(CAS)为我们提供了以代码为中心的安全控制方式
l 强大的互操作能力。对访问现有COM组件以、传统DLL以及WIN32函数提供支持
集成开发环境:Visual Studio .NET
对所有的开发语言,有统一的IDE。
第一章??
Microsoft .NET
框架开发平台体系结构
一、????????????
将源代码编译为托管模块:
?
1、? CLR的存在是得我们可以选择适合表达逻辑的语言,只要存在相应得编译器将代码编译成为面向CLR的代码即可,结果成为托管模块。
2、? 托管模块(managed module):是一个需要CLR才能执行的标准windows可移植可执行文件(portable executable:PE),组成:
1)??????? PE表头:指出文件类型,文件的时间标记
2)??????? CLR表头:CLR版本,托管模块入口、元数据、资源、强命名、标记等信息
3)??????? 元数据:源代码中定义、引用的类型和成员
4)??????? 中间语言(IL)代码
关于元数据:
l???????? 总与IL代码同步
l???????? 省去源代码编译时对头文件和库文件的需求
l???????? Visual Studio .NET利用之进行智能感知,辅助编码
l???????? 用于CLR的代码验证
l???????? 序列化及反序列化对象
l???????? 垃圾收集器可以追踪对象的生存期
二、????????????
将托管模块组合为程序集
关于程序集的理解:
暂歇
三、加载通用语言运行时
1、? 通过在%window%/system32目录下查找MSCorEE.dll文件来判断一个机器中是否安装了.NET框架;而框架的版本可从注册表下的子键:
HKEY_LOCAL_MACHINE / SOFTWARE / Microsoft / .NETFramework / policy中查看
2、
当生成一个
EXE
程序集时,编译器/链接器会产生一些特殊的信息,并将它们嵌入到结果程序集的
PE
文件表头及其各个组成文件的
.text
部分。当
EXE
文件被调用时,这些特殊的信息将导致
CLR
被加载并初始化。
CLR
随后会定位至应胜程序的入口点方法,从面以此来启动应用程序。
?
类似地,如果是一个非托管应用程序通过调用
LoadLibrary
来加载一个托管程序集,那么该托管程序集
DLL
的入口点函数也会知道去加载
CLR
来处理包含在其中的代码
3、?
其他诸如
EXE
程序集或
DLL
程序集如何被执行的过程,可在考察普通
Windows PE
文件的执行过程后进行比较以明确执行过程
四、执行程序集代码
1、? IL代码:一种面向对象的机器语言。可以理解对象类型,包含高级指令:创建和初始化对象、调用对象上的虚方法、直接操作数组元素、抛出和捕获异常;
IL汇编语言能获取CLR的所有功能,其他高级语言智能获取其中的一个子集;
IL代码独立于CPU平台,执行时要先被即时编译器(Just In Time Compiler)编译;
微软提供的IL汇编器:ILAsm.exe 反汇编器:ILDasm.exe。
2、? IL代码执行过程:
以下面代码段为例:
??????? static void Main()
??????? {
?????????????? Console.WriteLine(“Hello”);
?????????????? Console.WriteLine(“GoolBye”);
}
?0)代码被执行之前:
???? CLR首先检测Main中代码所引用到的所有类型,并分配一个数据结构记录该类型,数据结构为类型的每个方法分配一个对应条目,记录该方法实现代码的地址;初始化该结构时,各方法被设置为CLR内部的一个函数(未定义的函数)
1)??????? 第一次被执行时
当上述结构中某函数被调用时,JITComplier由该函数的类型及地址信息在程序集的元数据中找到其IL代码的位置,把这些IL代码验证并编译成本地CPU指令这些CPU指令被保存在一个动态分配的内存中,而a)步骤中数据结构中被调用方法的地址则被替换为包含本地CPU指令的地址。
2)??????? 第二次被执行时
若被调用的函数已经被验证并编译过,则直接从内存中调用之
若没有被调用则重复第一次被执行时的情况
3、? 性能
托管代码可能比非托管代码效率更好的理由:
l???????? JIT编译器可检测到新型CPU并产生为这些CPU提供的特殊指令,以优化执行效率
l???????? JIT编译器可检测到总是返回错误的布尔测试,对于其所办函的代码段不产生CPU指令,以使得代码量更小,执行效率更高。
l???????? CLR在运行时有针对性地重新编译某些IL代码,重新组织以提高分支预测的成功率。
4、? NGen.exe提供折中方案,为IL代码提供预编译版本,以避免运行时编译。
5、? IL代码验证:
--被验证为“不安全”的代码将抛出System.Security.VerificationException异常
--代码验证使得可以在一个windows进程中可以运行多个 托管应用程序
更多的讨论在
20
章
五、.NET框架类库(FCL)
CLR和FCL允许开发人员创建以下几种应用程序:
l???????? XML Web服务
l???????? Web窗体
l???????? Windows窗体
l???????? Windows控制台应用程序
l???????? Window服务
l???????? 组件库
制作特定的应用程序,关键是对相应得FCL的熟悉,那么首先对.NET Framework熟悉了吧。
六、通用类型系统(Common Type System)
描述了CLR中类型的规范
类型的成员组成:
l???????? 字段:表明对象的状态
l???????? 方法:用来改变状态
l???????? 属性:提供对状态访问时输入参数验证、状态有效性检验及必要的求值运算等功能
l???????? 事件:对象间的通知机制
类型可见性和访问类型成员的一些规则:
l???????? Private:只能被同一类型中的代码访问
l???????? Family:可以被派生类中的代码调用,而不管是否位于同一个程序集中
l???????? Family与Assembly:只能被位于同一个程序集中的派生类代码调用
l???????? Assembly:只能被同一个程序集中的代码调用
l???????? Family或Assembly:能被任何程序集中的派生类代码调用,也可以被同一程序集中的任何类型调用
l???????? Public:可被任何程序集中的任何代码调用
七、通用语言规范(Common Language Specification)
为使一种语言创建的类型能被其他语言无缝访问,微软定义了通用语言规范(CLS)。CLR/CTS支持的特性比CLS定义的丰富得多,实际上不同的语言实现了CLR/CTS特性的一个子集,而CLS则是所有语言特性的交集(一个最小特性集合)。
因此只有遵循CLS的类型才能被其他语言访问、应用,C#中使用:
[assembly:CLSCompliant(true)]
迫使编译器确保在公共导出类型中,剔除不符合CLS的部分。
八、与非托管代码互操作
为避免重新实现所有现有代码,CLR被设计成包含托管部分和非托管部分,CLR支持三种互操作情形:
l???????? 托管代码调用DLL中的非托管函数
l???????? 托管代码使用现存的COM组件
l???????? 非托管代码使用托管类型
第七章
类型成员及其访问限定
一、类型成员的可能组成:
ü???????? 常数,总是静态不可变的
ü???????? 字段,分为静态和实例字段两种
ü???????? 实例构造器,初始化实例对象
ü???????? 类型构造器,初始化类型的静态字段
ü???????? 方法,分为静态和实例方法两种
ü???????? 重载操作符,并非CLS的一部分,因为并非所有语言均支持之
ü???????? 转换操作符,不是CLS的一部分,部分语言不支持
ü???????? 属性,分静态和实例属性
ü???????? 事件,分为静态和实例事件两种
ü???????? 类型
二、访问限定修饰符:[
仅列出c#
中的]
C#
术语
|
描述
|
private
|
仅可被所定义的类型(或其嵌套类型)访问
|
protected
|
仅可以被所定义的类型(或嵌套类型)或继承的类型访问
|
Internal
|
仅可以被锁定义的程序集访问
|
protected internal
|
仅可以被所定义的类型(或嵌套类型)、派生类型以及同一程序集访问
|
public
|
可被任何程序集的任何类型访问
|
注:1、默认的访问方式为Internal
?????? 2、只能选择上述的一种修饰符,而不可同时指定两个
三、类型预定义特性:
C#
术语
|
描述
|
abstract
|
不可被实例化,可用作基类型
|
sealed
|
不能用作基类型
|
注:不可同时使用上述限定符,可通过为sealed修饰的类型提供private构造函数来达到“不可被实例化并不可被继承”的目的
四、字段预定义特性:
C#
术语
|
描述
|
static
|
字段为 类型字段
|
readonly
|
仅可在构造器中被赋值
|
注:关于常数和静态只读字段的区别,将在第八章详述
五、方法预定义特性
C#
术语
|
描述
|
static
|
类型方法,不能访问实例字段或方法,只能访问类型方法或类型字段
|
默认(CLR中称为Instance)
|
实例方法,可以访问实例方法或字段,也可以访问类型方法或字段
|
virtual
|
多态的实现,总调用继承链最末端的实现
|
new
|
仅用于虚方法,隐藏基类型的方法实现
|
override
|
仅用于虚方法,显示声明重写基类型方法
|
abstract
|
仅用于虚方法,派生类必须提供和该抽象方法匹配的实现,含有抽象方法的类型为抽象类型
|
sealed
|
仅用于虚方法,派生类不能重写该方法
|
注:sealed和abstract不能同时使用
第八章
常数与字段
一、常数
1、可被定义为常数的类型有:
2 基元类型:Boolean, Char, Byte, SByte, Decimal, Int16, Int32, UInt16, UInt32, Int64, UInt64, Single, Double
2 字符串:String
2 枚举类型
2、常数在编译后直接嵌入IL代码中,因此一个模块中的常数不能在运行时被另一模块获取,前者对常数进行的修改无法被另一个模块在运行时感知,因此要想在运行时获取“不变数值”应该使用只读字段
二、字段
1、字段(包括静态、实例或只读字段)均在运行时分配内存
2、只读字段只能在构造器中被符值(也可在声明时被直接赋值,其他地方均不允许,实际上同于在构造器中赋值),静态只读字段在类型构造器中赋值,实例只读字段在实例构造器中被符值。
3、静态只读字段只能使用类型名访问,而不能使用实例引用访问
三、静态只读字段和常数的区别:
1、常数直接编译时刻嵌入IL代码,在运行时不可重新读取,静态只读字段则可以在运行时重新读取
2、常数只能在声明处赋值,而静态只读字段则可在构造器中赋值
第九章
方法
一、????????????
实例构造器
1、? 前面提到用new操作符创建对象时的三部曲:
l???????? 为对象分配内存
l???????? 初始化对象的附加成员(方法表指针和SyncBlockIndex)
l???????? 调用实例构造器初始化实例状态
在分配内存时,系统将所有内存位置均置为0值,这就是为什么字段初始化而未赋值时均为0或null值。
不
调用实例构造器的情况:
l???????? 调用Object.MemberwiseClone()方法创建实例(分配内存;初始化附加成员;将源对象字节拷贝到新创建的对象)
l???????? 反序列化对象时
2、? 为避免为实例字段产生过多的构造器代码,应避免在声明字段时为字段符初值,而是在无参构造器中为它们符初值,在其他重载的构造器中调用无参构造器。
3、? 值类型实例构造器
l???????? C#编译器不会自动调用其构造器,必须显式调用构造器才能起作用
l???????? C#编译器不允许为值类型定义无参实例构造器(下面会介绍可以定义无参类型构造器)
l???????? 不能为结构中的字段在声明的同时赋初值,可通过定义带参构造器的方式进行
l???????? 必须在结构的构造器中为所有字段赋初值
二、????????????
类型构造器
1、? 类型构造器的一些限制:
l???????? 不能带任何参数
l???????? 类型构造器总为私有的,不能用其他访问修饰符
2、类型构造器被调用的时机:
l???????? 第一个实例被创建,或者类型的第一个字段或成员第一次被访问之前
l???????? 非继承静态字段被第一次访问之前
类型构造器在类型的生命周期中只被调用一次;
3、一些限制:
l???????? 若类型构造器中抛出异常,则该类型变成不可访问,访问其中的任何字段或方法均会抛出System.TypeInitializationException异常
l???????? 类型构造器只能访问类型的静态字段
l???????? 类型构造器不应该调用基类型的类型构造器,因静态字段并非继承而是编译时静态绑定
三、????????????
操作符重载
1、? 操作符重载
C#中对操作符重载的一些限制:
l???????? 必须声明为public static
l???????? 必须有一个参数为操作符所属类型
l???????? 不能改变操作符原始定义的引数个数
l???????? 若定义了true操作符也必须同时定义false操作符,二者都必须返回bool值
l???????? ++、--操作符必须返回其所隶属之类型的一个实例
l???????? 可被重载的一元操作符:+、-、!、~、++、--、true、false
l???????? 可被重载的二元操作符:+、-、*、/、%、!、^(异或)、<、>、<<、>>、==、!=、<=、>=
l???????? 不允许被重载的操作符:&&、||、=、?:、+=、-=、/=、%=、|=、^=、<<=、>>=,实际上其中一些“复式操作符”在二元操作符被重载后自动生成,而不能显式定义
l???????? 必须成对重载的操作符:(== ,!=)、(<,>)、(<=,>=)
l???????? ++、--操作符重载时不能区分其为前置或后置的
2、? 操作符重载与语言互操作性
编译器会为重载的操作符生成一个特殊名称的方法,如+(加)操作符生成op_Addition()方法,并为该方法的定义条目上加上specialname标记。当某种语言不能进行操作符重载时,可以直接定义具有该特殊名称的方法,以在其他语言中调用;或直接调用具有该特殊名称的方法以适应某种语言不能解析操作符的限制。如:vb中不能重载操作符,可显式定义op_Addition()方法以在C#中调用;C#中定义的+操作符不能被VB识别,可显式调用op_Addition()方法获得同样的功能。
四、????????????
转换操作符
转换操作符的一些限制:
l???????? 必须为public static
l???????? 必须指定关键字implicit或explicit,原则为:从本类型转换为其他类型使用implicit,将其他类型转换为本类型用explicit,不能都使用implicit
五、????????????
方法参数
1、? 引用参数
l???????? 缺省情况下为值传递
l???????? 标志为out的参数,在调用方法前
不必初始化,但返回之前必须赋值,没有被初始化的参数是不能被使用
l???????? 标志为ref的参数,在调用方法前
必须初始化,否则触发编译错误
l???????? 可以使用ref或out来进行方法的重载,但不能通过区分ref和out来重载方法
l???????? 按引用方式传递的变量(实参)必须和方法声明的参数(形参)类型完全相同,否则触发编译错误。
2、? 可变数目参数
使用params关键字及对象数组的方式指定可变参数序列。一些限制:
l???????? 只有方法的最后一个参数才能使用可变数目参数
六、????????????
虚方法
1、? 虚方法的调用机理
CLR使用以下两个IL指令调用方法:
u?????? call
? 根据类型(即引用的静态类型、声明类型)来调用一个方法
u?????? callvirt
???? 根据对象(即引用的动态类型、实际类型)来调用一个方法
对于虚方法使用call来调用的情况有:
l???????? base.虚方法(),
l???????? 密封类型引用虚方法,因为没有必要检验密封类型的实际类型
l???????? 值类型,避免被装箱
使用callvirt调用非虚方法的情况:
l???????? 应用变量为null时,使用callvirt才会抛出System.NullReferenceException异常,而call不会抛出
无论call或callvirt调用方法,均会有一个隐含的this指针作为方法的第一个参数,它指向正在操作的对象
2、? 虚方法的版本控制:
用下面的例子说明:
using System;
class BaseClass
{
?????? public void NonVirtualFunc()
?????? {
????????????? Console.WriteLine("Non virtual func in base class");
?????? }
??????
?????? public virtual void VirtualFunc()
?????? {
????????????? Console.WriteLine("Virtual func in base class");??
?????? }
}
class DevicedClass : BaseClass
{
?????? //若不使用new 关键字则编译器会有warning:
?????? //“DevicedClass.NonVirtualFunc()”上要求关键字
?????? //new,因为它隐藏了继承成员“BaseClass.NonVirtualFunc()”
?????? public new void NonVirtualFunc()
?????? {
????????????? Console.WriteLine("Non virtual func in deviced class");?????
?????? }
?????? //若不添加关键字override或new,则编译器会有warning:
?????? //“DevicedClass.VirtualFunc()”将隐藏继承的成员“BaseClass.VirtualFunc()
?????? //”。若要使当前成员重写该实现,请添加关键字 override。否则,添加关键字
?????? //new。
?????? public override void VirtualFunc()
?????? {
????????????? Console.WriteLine("Virtual func in deviced class");?????
?????? }
}
class TestClass
{
?????? public static void Main()
?????? {
????????????? //派生类实例调用 非虚 及 虚函数
????????????? DevicedClass dc = new DevicedClass();
????????????? dc.NonVirtualFunc();
????????????? dc.VirtualFunc();
????????????? //基类实例调用 非虚 及 虚函数
?????????????
????????????? BaseClass bc = new BaseClass();
????????????? bc.NonVirtualFunc();
????????????? bc.VirtualFunc();
?????????????
????????????? //指向派生类实例的基类引用 调用 非虚 及 虚函数
????????????? BaseClass bc1 = dc;
????????????? bc1.NonVirtualFunc();
????????????? bc1.VirtualFunc();
?????? }
}
/*
在虚函数上使用关键字override的运行结果:
Non virtual func in deviced class
Virtual func in deviced class
Non virtual func in base class
Virtual func in base class
Non virtual func in base class
Virtual func in deviced class
*/
/*
在虚函数上使用关键字new的运行结果
Non virtual func in deviced class
Virtual func in deviced class
Non virtual func in base class
Virtual func in base class
Non virtual func in base class
Virtual func in base class
*/
由上可见:new 和 override在派生类中协调版本的控制,在第七章中已经看到oeverride只能用于virtual方法,new则可用于非虚或虚方法,以实现隐藏基类中的同名方法。在虚函数上使用override,重写了基类的方法,并无隐藏,这也就实现了多态。我们可设想这样的结论:
new
使用
call
指令调用静态类型的方法,而
override
使用
callvirt
指令调用动态类型的方法。
希望这个例子对您的理解有所帮助。
第十章
属性
摘要:
本章讨论C#中的 属性 及 索引器
一、属性
分为
静态属性、实例属性和虚属性
l 避免
直接访问类型字段或使用烦琐的访问器方法进行访问
l 很好的实现了类型的
数据封装,如:改变字段而维持属性的意义对用户是透明的
l 代码量小,运算量小的操作才使用属性,否则使用方法调用更合适
二、索引器
l 可有多个重载的索引器,只要参数列表不同即可
l 可通过应用System.Runtime.CompilerServices.IndexerNameAttribute特性改变编译器为索引器生成的方法名(缺省使用get_Item(…),set_Item(...))
l 不能通过上述改变方法名的办法来定义多个参数列相同而仅名称不同的索引器
l 没有所谓“静态索引器”
注:在属性或索引器中添加对参数或value值得判定有助于保证程序的完整性
一个简单的示例:
using System;
class IndexerTest
{
private static string[] strArr = new string[5];
IndexerTest()
{
for(int i = 0; i < 5; i ++)
{
strArr[i] = i.ToString();
}
}
public string this[Int32 nIndex]
{
get{
return strArr[nIndex];
}
set{
strArr[nIndex] = value;
}
}
//提供不同的参数列进行重载索引器
public string this[byte bIndex]
{
get{
return strArr[bIndex];
}
set{
strArr[bIndex] = (string)value;
}
}
//只读属性
public string[] StrArr
{
get{
return strArr;
}
}
public static void Main()
{
IndexerTest it = new IndexerTest();
it[1] = "Hello"; //利用索引器进行写操作
foreach(string str in it.StrArr)
{
Console.WriteLine(str);
}
}
}
/*
运行结果:
0
Hello
2
3
4
*/
第十一章
事件
摘要:
?????? 本章讲述事件的应用,包括:
n???????? 发布事件设计模式
n???????? 侦听事件的方法
n???????? 显式控制事件注册
n???????? 一个类型定义多个事件并减少内存资源
一、????????????
发布事件
1、发布事件的类型提供的功能:
l???????? 允许其他对象登记事件
l???????? 允许其他对象注销事件
l???????? 维护一个登记对象列表,在事件发生时通知相应的登记对象
2、发布事件步骤:
l???????? 定义事件附加信息类型
l???????? 定义事件触发时被调用的委托类型(Delegate回调函数)
l???????? 定义事件成员。形如:public event [EventName]EventHandler Msg;
l???????? 定义一个受保护的虚方法(protected virtual),负责通知事件的登记对象
l???????? 定义一个将输入转化为事件的方法
3、.NET框架的一些约定:
l???????? .NET框架建议附加信息类型名以EventArgs结尾([EventName]EventArgs);无需传递附加信息的事件使用EventArgs.Empty静态只读字段
l???????? .NET框架建议委托原形为:void [EventName]EventHandler(Object sender, [EventName]EventArgs e);无需附加信息的直接用System.EventHandler委托类型,并使用EventArgs.Emtpy静态只读字段作为第2个参数
4、对事件定义的剖析:
若发布事件的类型中有如下事件定义:
?
public event EventNameEventHandler EventMsg;
?
则被编译器翻译为:
?
private EventNameEventHandler EventMsg = null;
[MethodImplAttribute(MeghodImplOptions.Synchronized)]
?????? //用于线程安全,需要开销
public void add_EventMsg(EventNameEventHandler handler)
{
?????? EventMsg = (EventNameEventHandler)Delegate.Combine(EventMsg, handler);
}
[MethodImplAttribute(MeghodImplOptions.Synchronized)]
?????? //用于线程安全,需要开销
public void remove_EventMsg(EventNameEventHandler handler)
{
?????? EventMsg = (EventNameEventHandler)Delegate.Remove(EventMsg, handler);
}
?
联系显式定义事件的方法,有异曲同工之妙。
二、????????????
侦听事件
侦听过程分为以下几个步骤:
l???????? 定义事件通知回调函数,在其中处理到达的事件
l???????? 登记本对象到发布事件的类型
l???????? 注销本对象
三、????????????
显式控制事件注册
显示控制事件注册是为了在单线程应用中消除由于线程同步带来的开销(见上面对事件定义的剖析),因此仅当无需线程同步时,显示发布事件才有用。
参考发布事件中的描述,显式控制事件注册只是将“定义事件成员”分解为:
l???????? 定义委托类型
l???????? 显式定义事件及访问器方法。注意必须同时定义add和remove方法
l???????? 修改通知事件登记对象的方法,使用刚定义的委托类型
四、????????????
定义多个事件
定义多个事件主要为了公开大量的事件但不为每个事件分配字段,使用哈希表或链表(如FCL中的
System.ComponentModel.EventHandlerList)存储这些事件实例。这只有在有非常多的事件且预期大部分事件都不实现时才有用。
理解这部分内容要抓住以下几个方面:
l???????? 上面对事件定义的剖析,其中的方法也是在集合中添加事件委托的方法
l???????? 为每种事件建立一个唯一键(通过建立一个静态只读对象来标识)用以标识事件
l???????? 其余同单一事件的发布
五、????????????
示例
请参阅另一篇文章“
第十一章多事件示例[一个男人和三个女人的故事]”
本章牵涉到的其他知识点:
ü???????? 委托的应用(第十七章详述)
ü???????? 线程安全的保障
ü???????? 散列表(Hashtable)的应用
在后续笔记中会有详述。
?
第十一章
多事件示例
[
一个男人和三个女人的故事
]
摘要:
应用
FCL
中的
System.ComponentModel.EventHandlerList示例一个类型中发布多事件的应用
场景:一个男生有三个女朋友,各自有不同的爱好,女朋友A爱好音乐,女朋友B爱好美食,女朋友C爱好XXX,为满足各个女朋友,此男生必须进行唱歌、烹饪食物、xxx。
以此制作程序演示单类型多事件的应用,并假设此男同时只能干一件事情(即排除一边xxx一边唱歌或一边xxx一边烹饪的可能
J)
如下为源代码:
using System;
using System.ComponentModel;
//男朋友的源代码
public class BoyFriend
{
protected EventHandlerList eventList
= new EventHandlerList();
//
//满足女朋友A定义音乐喜好
//使用自定义的音乐事件及回调函数
protected static readonly object musicEventKey = new object();
public class MusicEventArgs : EventArgs
{
private string strMusicName;
public string MusicName
{
get{
return strMusicName;
}
}
public MusicEventArgs(string strMusicName)
{
this.strMusicName = strMusicName;
}
}
public delegate void MusicEventHandler(object sender, MusicEventArgs args);
public event MusicEventHandler MusicMsg
{
add
{
eventList.AddHandler(musicEventKey, value);
}
remove
{
eventList.RemoveHandler(musicEventKey, value);
}
}
protected virtual void OnMusic(MusicEventArgs e)
{
Delegate d = eventList[musicEventKey];
if(d != null)
{
d.DynamicInvoke(new Object[]{this, e});
}
}
public void SimulateMusic(string strName)
{
Console.WriteLine("男朋友:好的,我给你唱支{0}吧!", strName);
OnMusic(new MusicEventArgs(strName));
}
//
//满足女朋友B的美食欲望
//
protected static readonly object cateEventKey = new object();
public class CateEventArgs : EventArgs
{
private string strCateName;
public string CateName
{
get
{
return strCateName;
}
}
public CateEventArgs(string strCateName)
{
this.strCateName = strCateName;
}
}
public delegate void CateEventHandler(Object sender, CateEventArgs args);
public event CateEventHandler CateMsg
{
add
{
eventList.AddHandler(cateEventKey, value);
}
remove
{
eventList.RemoveHandler(cateEventKey, value);
}
}
protected void OnCate(CateEventArgs e)
{
Delegate d = eventList[cateEventKey];
if(d != null)
{
d.DynamicInvoke(new Object[]{this, e});
}
}
public void SimulateCate(string strCateName)
{
Console.WriteLine("男朋友:请吃一点我做的{0}", strCateName);
OnCate(new CateEventArgs(strCateName));
}
//
//满足女朋友C的xxx欲望
//使用EventArgs.Empty事件及System.EventHandler回调函数
protected static readonly object xxxEventKey = new object();
public event EventHandler XXXMsg
{
add
{
eventList.AddHandler(xxxEventKey, value);
}
remove
{
eventList.RemoveHandler(xxxEventKey, value);
}
}
protected virtual void OnXXX()
{
Delegate d = eventList[xxxEventKey];
if(d != null)
{
d.DynamicInvoke(new Object[]{this, EventArgs.Empty});
}
}
public void SimulateXXX()
{
Console.WriteLine("男朋友:你今天真漂亮呵!");
OnXXX();
}
public static void Main()
{
BoyFriend bf = new BoyFriend();
//
Console.WriteLine("上午 女朋友A来玩:");
GF_A gfa = new GF_A(bf);
bf.SimulateMusic("恋曲");
gfa.Unregister(bf);
//
Console.WriteLine();
Console.WriteLine("下午 女朋友B来玩");
GF_B gfb = new GF_B(bf);
bf.SimulateCate("祖传小甜点");
gfb.Unregister(bf);
//
Console.WriteLine();
Console.WriteLine("晚上 女朋友C来玩");
GF_C gfc = new GF_C(bf);
bf.SimulateXXX();
gfc.Unregister(bf);
}
}
//女朋友A的源代码
public class GF_A
{
public GF_A(BoyFriend bf)
{
bf.MusicMsg += new BoyFriend.MusicEventHandler(MusicMsg);
Console.WriteLine("女朋友A:老公!我要听歌");
}
private void MusicMsg(Object sender, BoyFriend.MusicEventArgs args)
{
switch(args.MusicName)
{
case "恋曲":
case "清歌":
Console.WriteLine("女朋友A:哇,是{0}耶,好喜欢啊!", args.MusicName);
break;
default:
Console.WriteLine("女朋友A:这首歌没听过耶,好好听奥!");
break;
}
}
public void Unregister(BoyFriend bf)
{
BoyFriend.MusicEventHandler bfe = new BoyFriend.MusicEventHandler(MusicMsg);
bf.MusicMsg -= bfe;
Console.WriteLine("女朋友A: 休息了,别吵!");
}
}
//女朋友B的源代码
public class GF_B
{
public GF_B(BoyFriend bf)
{
bf.CateMsg += new BoyFriend.CateEventHandler(CateMsg);
Console.WriteLine("女朋友B: 老公!我饿了!");
}
private void CateMsg(Object sender, BoyFriend.CateEventArgs args)
{
switch(args.CateName)
{
case "祖传小甜点":
Console.WriteLine("女朋友B: 哇!老公你真能干,{0}好好吃耶!", args.CateName);
break;
case "饼干":
case "方便面":
Console.WriteLine("女朋友B: 刚认识你时,给人家做小点心,现在让人家吃方便食品了,555555");
break;
default:
Console.WriteLine("女朋友B: 这是什么东东,没吃过耶");
break;
}
}
public void Unregister(BoyFriend bf)
{
BoyFriend.CateEventHandler e = new BoyFriend.CateEventHandler(CateMsg);
bf.CateMsg -= e;
Console.WriteLine("女朋友B: 吃饱了,谢谢你噢!");
}
}
//女朋友C的源代码
public class GF_C
{
public GF_C(BoyFriend bf)
{
bf.XXXMsg += new EventHandler(XXXMsg);
Console.WriteLine("女朋友C: 老公!你今天真帅哦!");
}
private void XXXMsg(Object sender, EventArgs args)
{
Console.WriteLine("女朋友C: R...O...O...M...");
}
public void Unregister(BoyFriend bf)
{
EventHandler e = new EventHandler(XXXMsg);
bf.XXXMsg -= e;
Console.WriteLine("女朋友C:累了,想休息了!");
}
}
/*运行结果:
注:
1
、因上例使用
FCL
中的
System.ComponentModel.EventHandlerList
,因此不具备线程安全性。
2
、上述代码中
xxx
部分未定义事件参数而是使用
System.EventArgs.Emtpy
,也没有定义回调函数而是使用
System.EventHandler;
其他两个事件都是自定义的。可以修改其他两个事件
3
、关于发布事件、自定义事件、多事件定义的详细信息,请参考《.net框架程序设计》读书笔记_第十一章事件
第十三章
枚举类型与位标记
一、
枚举类型
1、 使用枚举类型的理由:
l 枚举类型是得程序更容易编写、阅读、维护,在代码中使用符号名称代替数字是程序设计的一贯主张。
l 强类型的,便于类型检验
2、 注意事项:
l 枚举类型继承自System.Enum,System.Enum又继承自System.ValurType
l 枚举类型不能定义方法、属性、事件
l 枚举类型为常数而非只读字段,因此可能引入版本问题(见
第八章的相关讨论)
l 将枚举类型与引用它的类型定义在同一层次上,可减少代码录入的工作量
3、 System.Enum中方法的应用:
l public static Type GetUnderlyingType(TypeenumType);
获取用于保存枚举类型实例值得基础类型。声明某枚举类型使用的基础类型语法如下:
enum Human : byte
{
Male,
Female
}
则调用上述方法Enum.GetUnderlyingType(typeof(Human));将返回System.Byte;
l public override string ToString();
public string ToString(string); //参数为格式字符串
public static string Format(TypeenumType,objectvalue,stringformat);
//Value – 要转换的值,format – 格式字符串(G,g,X,x,D,d,F,f)
l public static Array GetValues(TypeenumType);
获取枚举中常数值的数组
l public static string GetName(Type enumType,object value);
在指定枚举中检索具有指定值的常数的名称
l public static string[] GetNames(TypeenumType);
检索指定枚举中常数名称的数组。
l public static object Parse(Type, string);
public static object Parse(Type, string, bool);
将一个或多个枚举常数的名称或数字值的字符串表示转换成等效的枚举对象
l public static bool IsDefined(Type enumType,object value);
返回指定枚举中是否存在具有指定值的常数的指示,value为常数的值或名称
l 系列ToObject方法
返回设置为指定值的、指定枚举类型的实例
二、
位标记
l 使用
System.FlagsAttributes定制特性,使得ToString或Format方法可以查找枚举数值中的每个匹配符号,将它们连接为一个字符串,并用逗号分开;Parse方法可用该特性拆分字符串并得到复合的枚举类型
l 使用格式字符串F或f 也有同样的效果
下面的示例说明上述情况
using System;
[Flags]
//定制特性
public enum Human : byte
//定制基本类型
{
Male = 0x01,
Female = 0x10
}
public class EnumTest
{
public static void Main()
{
Human human = Human.Male | Human.Female; //人妖?
Console.WriteLine(human.ToString()); //使用Flags定制特性的情况
//Console.WriteLine(human.ToString("F")); //没有使用Flags定制特性的情况
Console.WriteLine(Enum.Format(typeof(Human), human, "G"));//使用Flags定制特性的情况
//Console.WriteLine(Enum.Format(typeof(Human), human, "F"));//没有使用Flags定制特性的情况
human = (Human)Enum.Parse(typeof(Human), "17");
Console.WriteLine(human.ToString()); //使用Flags定制特性的情况
//Console.WriteLine(human.ToString("F")); //没有使用Flags定制特性的情况
}
}
/*运行结果
Male, Female
Male, Female
Male, Female
*/
注: 上述程序中的注释为不使用Flags特性时的语法
第十四章
数组
.
内容摘要:
本章讨论了数组的方方面面,对于这种常用类型进行深入研究。
一、
数组简介
三种类型:一维数组、多维数组、交错数组(jagged aray)
l一维数组:
Int32[] myIntegers;
myIntegers = new Int32[100];
l 多维数组:
Int32[,] myIntegers;
myIntegers = new Int32[100,100];
l 交错数组:交错数组不受CLS支持
Point[][] myPolygons = new Point[3][];
myPolygons[0] = new Point[10];
myPolygons[1] = new Point[20];
myPolygons[2] = new Point[30];
二、
System.Array
请参考.net framework sdk中相关描述
三、
数组转型
l 两数组必须有同样的维数
l 两数组中元素类型间存在隐式或显式转换
l 除使用Array.Copy()方法外,不允许将值类型数组转换为其他类型数组(Array.Copy方法会根据需要进行强制类型转换或装箱操作)
Array.Copy()
方法能够执行的类型转换如下:
l将值类型转换为引用类型,将Int32转换为Object
l 将引用类型转换为值类型,将Object转换为Int32
l拓宽(widen)CLR基类型,如将Int32转换为Double
下面这个示例提供了一个
反面教材(切勿效仿,后果自负!)了值类型的装换:
using System;
//自定义一个值类型,该类型可以与Int32进行互转换
struct MyAge
{
private Int32 m_nAge;
public MyAge(Int32 nAge)
{
this.m_nAge = nAge;
}
//转换操作符
public static implicit operator MyAge(Int32 nAge)
{
return new MyAge(nAge);
}
public static explicit operator Int32(MyAge age)
{
return age.ToInt32();
}
public Int32 ToInt32()
{
return m_nAge;
}
public override string ToString()
{
return "Age : " + m_nAge;
}
}
public class ArrayTest
{
public static void Main()
{
Int32[] arrInt = new Int32[10];
for(int i = 0; i < arrInt.Length; i ++)
{
arrInt[i] = i;
}
MyAge[] arrAge = new MyAge[arrInt.Length];
Array.Copy(arrInt, arrAge, arrInt.Length);
foreach(MyAge age in arrAge)
{
Console.WriteLine(age.ToString());
}
}
}
/*运行结果
未处理的异常: System.ArrayTypeMismatchException: 不能将源数组类型分配给目标数组
类型。
at System.Array.Copy(Array sourceArray, Int32 sourceIndex, Array destinationA
rray, Int32 destinationIndex, Int32 length)
at System.Array.Copy(Array sourceArray, Array destinationArray, Int32 length)
at ArrayTest.Main()
*/
注:1、
上述代码是不能运行的。虽然为值类型定义了转换操作,但仍不满足上述转换条件,可见自定义的转换操作不被
Array.Copy()
认可。
2、上述代码中涉及到类型转换操作符的应用,请参考读书笔记
第九章方法
四、
数组传递与返回
注意事项:
l 为获得对数组元素的深拷贝,要求每个元素都实现ICloneable接口。需要注意的是应在适当时候使用深拷贝操作(详情请参见《c# primer》p185)
l 返回数组引用的方法若不含数组元素应该返回一个包含0个元素的数组而不是null
l 不在方法中返回类型内部的数组引用,而是重新构造一个
深拷贝的数组返回。
五、
创建下限非0
的数组
使用Array.CreateInstance()方法:
public static Array CreateInstance(Type elementType, int[] lengths, int[] lowerBounds);
elementType : 要创建的 Array 的 Type。
lengths : 一维数组,它包含要创建的 Array 的每个维度的大小
lowerBounds : 一维数组,它包含要创建的 Array 的每个维度的下限(起始索引)。
六、
快速数组访问
要点:
l 使用非安全的代码
l 使用指针访问数组
l 可进行非安全数组操作的元素为数值基元类型、Boolean、枚举类型或字段为上述类型的结构类型,或字段为上述结构类型的结构类型……(递归定义的结构类型)
如下面例程所示:
using System;
public class ArrSafe
{
unsafe public static void Main() //此处表明使用非安全代码
{
Int32[] arr = new Int32[]{1,2,3,4,5};
fixed(Int32 *element = arr)??? //使用指针访问
{
for(Int32 i = 0; i < arr.Length; i ++)
{
Console.WriteLine(element[i]);
}
}
}
}
七、
重新调整数组的长度
l 使用如下方法获取创建新的数组长度(请参见.net framework sdk中的详细描述)
创建使用从零开始的索引、具有指定 Type 和长度的一维 Array。
[C#] public static Array CreateInstance(Type, int);
创建使用从零开始的索引、具有指定 Type 和维长的多维 Array。维的长度在一个 32 位整数数组中指定。
[C#] public static Array CreateInstance(Type, params int[]);
创建使用从零开始的索引、具有指定 Type 和维长的多维 Array。维的长度在一个 64 位整数数组中指定。
[C#] public static Array CreateInstance(Type, params long[]);
创建使用从零开始的索引、具有指定 Type 和维长的二维 Array。
[C#] public static Array CreateInstance(Type, int, int);
创建具有指定下限、指定 Type 和维长的多维 Array。
[C#] public static Array CreateInstance(Type, int[], int[]);
创建使用从零开始的索引、具有指定 Type 和维长的三维 Array。
[C#] public static Array CreateInstance(Type, int, int, int);
l 然后使用Array.Copy()方法,将原来的数组拷贝到新数组
第十五章
接口
摘要:
接口的应用及完全限定名方式定义接口的应用。
一、
接口与继承
lC#支持单实现继承和多接口继承
l接口中可以定义:事件、无参属性(属性)、含参属性(索引器);C#不允许接口定义任何静态成员(CLR却允许定义静态成员);CLR不允许接口定义实例字段和构造器。
l缺省为public abstract 方法,但不可用任何修饰符进行修饰(包括public)
l将值类型转换为接口类型(假设其实现了某个接口),则值类型被装箱为引用类型,以调用其中的接口方法。
实现接口继承或实现继承的原则:
l存在
IS-A关系使用实现继承,存在
CAN-DO关系使用接口继承
l实现继承可继承父类型的实现,由于接口中没有定义方法的实现,因此继承后必须实现方法
l考虑到每个人对接口实现的不同,使用实现继承可同一部分功能的实现
l为父类型添加方法可能不影响使用继承自该类型实现的用户,而为接口添加方法导致用户必须为新方法添加实现
二、
利用接口改变已装箱类型中的字段
实际上是将已装箱类型转型为接口类型,然后通过调用方法进行值改变。
因为对已装箱类型进行拆箱到原值类型将在堆栈上声称新的值类型,调用这个新的值类型的方法不能改变以装箱类型的值。
三、
实现多个有相同方法的接口
使用 接口名.方法名 的形式声明实现了那个接口的方法。成为
完全限定接口名,这样定义的方法被认为是私有方法,不能使用类型本身调用;当将类型转换为接口类型时,完全限定接口名定义的方法又变成一个公有方法。(比较奇特,也很有用);使用完全限定接口名定义的方法不能使用任何访问修饰符。
四、
显示接口成员实现
问题的提出:通常接口接受的参数为Object,这样的参数非强类型安全的,可能需要在我们自定义类型的接口实现中定义强类型安全的方法,同时也需要实现“接口合同”
问题解决:通过上面的完全限定接口名方式定义接口方法,可达到这样的效果。
优点:获得强类型支持,可在编译期发现类型不匹配问题,而不是到运行期。同时,调用自定义的方法不必进行装箱操作,提高了效率。
缺点:由于我们在类型中隐藏了接口实现(必须将转型为接口才能暴露出接口定义的方法),因此可能对使用造成不便,因此应该慎用该方法。
第十七章
委托
一、
委托的使用
静态委托和实例委托,使用方法类似,这里给出一个使用可变参数委托的例子:
using System;
public class DelCls
{
public delegate void DelDef(params string[] strParams);
public static void CallDel(DelDef dd)
{
if(dd != null) //请务必在此处进行判断,这是个好习惯
{
dd("Hello", "world");
}
}
}
public class DelIns
{
//声明为private(私有)成员并不影响在类型内部使用委托
private static void ClsCallStr(params string[] strParams) //类型方法
{
//将字符串数组并顺序输出
foreach(string str in strParams)
{
Console.Write("{0} ", str);
}
Console.WriteLine();
}
public void InsCallStr(params string[] strParams) //实例方法
{
//将字符串数组并反序输出
for(int i = strParams.Length - 1; i >= 0; i --)
{
Console.Write("{0} ", strParams[i]);
}
Console.WriteLine();
}
public static void Main()
{
DelIns di = new DelIns();
DelCls.DelDef dd = null;
Console.WriteLine("combine two delegate:");
dd += new DelCls.DelDef(DelIns.ClsCallStr);
dd += new DelCls.DelDef(di.InsCallStr);
DelCls.CallDel(dd);
Console.WriteLine("remove the first delegate:");
dd -= new DelCls.DelDef(DelIns.ClsCallStr);
DelCls.CallDel(dd);
}
}
/*运行结果
combine two delegate:
Hello world
world Hello
remove the first delegate:
world Hello
*/
在C#中使用委托方法:
l 创建委托所使用的方法必须和委托声明相一致(参数列表、返回值都一致)
l 利用 +=、-=来进行委托的链接或取消链接或直接使用Delegate.Combine和Delegate.Remove方法来实现
l 使用MulticastDelegate的实例方法GetInvocationList()来获取委托链中所有的委托
二、
委托揭秘
所有的委托都继承自MulticastDelegate,编译器在编译时刻为委托的声明生成了一个完整的委托类,重点注意其中的一些成员:
ü 构造函数,传入委托的目标对象(实例)及指向回调方法的整数
ü 继承自MulticastDelegate的_target(System.Object)字段
ü 继承自MulticastDelegate的_methodPtr(System.Int32)字段
ü 继承自MulticastDelegate的_prev(System.MulticastDelegaet)字段
ü 生成的与方法声明相一致Invoke函数用以调用方法
可利用MulticastDelegate中的Method及Target属性来考察_methodPtr及_target字段的性质。
关于编译器生成的委托类及Invoke方法的调用情况,可通过使用ILDAsm.exe查看执行文件的IL代码获得
将上例中类型DelIns中的Main方法作如下修改,以实验GetInvocationList及MulticastDelegate中属性的使用:
public class DelIns
{
…
public static void Main()
{
…
Delegate[] arrDel = dd.GetInvocationList();
foreach(DelCls.DelDef d in arrDel)
{
Console.WriteLine("Object type: {0}, Method name: {1}",
(d.Target != null) ? d.Target.GetType().ToString() : "null",
d.Method.Name);
}
…
}
…
}
/*运行结果
…
Object type: null, Method name: ClsCallStr
Object type: DelIns, Method name: InsCallStr
…
*/
三、
委托判等
首先判断_methodPtr及_target字段是否相等,若不等则返回false;
若相等,继续判断_prev是否为null(指向委托链头部的委托),若为null,则相等返回true;
若不等,继而判断委托链上所有委托对象,重复上述步骤。
可见牵涉到委托链的时候是个递归判断的过程。
四、
委托链
l 首先被加入到委托链中的委托位于委托链的尾部,但首先被调用,这是因为Invoke中利用
递归对委托函数进行调用,这样位于头部的委托最后被调用。
l 委托调用后的返回值,只是最后一次被调用方法的返回值,即委托链头部委托的返回值
l 每调用一次Remove方法只删除匹配的第一个委托链
五、
委托与反射
以下是.net framework sdk文档提供的Delegate.CreateDelegate方法列表:
创建指定类型的委托以表示指定的静态方法。
[C#] public static Delegate CreateDelegate(Type, MethodInfo);
创建指定类型的委托,该委托表示要对指定的类实例调用的指定实例方法。
[C#] public static Delegate CreateDelegate(Type, object, string);
创建指定类型的委托,该委托表示指定类的指定静态方法。
[C#] public static Delegate CreateDelegate(Type, Type, string);
创建指定类型的委托,该委托表示要按指定的大小写敏感度对指定类实例调用的指定实例方法。
[C#] public static Delegate CreateDelegate(Type, object, string, bool);
下面的示例演示了创建静态方法委托、实例方法委托以及动态调用委托:
using System;
using System.Reflection;
public class DelReflection
{
public delegate void GoGo(string strPam, Int32 nPam);
public static void ClsGo(string strPam, Int32 nPam)
{
Console.WriteLine("In class, String:{0}, Int32:{1}", strPam, nPam);
}
public void InsGo(string strPam, Int32 nPam)
{
Console.WriteLine("In instance, String:{0}, Int32:{1}", strPam, nPam);
}
public static void Main()
{
Delegate d = null;
d = Delegate.CreateDelegate(typeof(GoGo), typeof(DelReflection), "ClsGo");
if(d != null)
d.DynamicInvoke(new Object[]{"Hello", 45});
DelReflection dr = new DelReflection();
d = Delegate.CreateDelegate(typeof(GoGo), dr, "InsGo");
if(d != null)
d.DynamicInvoke(new Object[]{"Hello", 45});
}
}
/*运行结果
In class, String:Hello, Int32:45
In instance, String:Hello, Int32:45
*/