第1章:.NET体系结构
C#编写的所有代码总是在.NET Framework中运行。对于C#语言来说,可以得出两个重要的结论:
n C#的结构和方法反映了.NET基础方法论。
n 在许多情况下,C#的特定语言功能取决于.NET的功能,或依赖于.NET基类。
一、 C#与.NET的关系
C#的重要性体现在以下两个方面:
它是专门为与Microsoft的.NET Framework一起使用而设计的。
它是一种基于现代面向对象设计方法的语言,在设计它时,Microsoft还吸取了其他类似语言的经验,这些语言是近20年来面向对象规则得到广泛应用后才开发出来的。
二、 公共语言运行库(CLR)
.NET Framework的核心是其运行库的执行环境,称为公共语言运行库(CLR)或.NET运行库。在CLR的控制下运行的代码常常称为托管代码(managed code)。但是,在CLR执行开发的源代码之前,需要编译它们。
在.NET中,编译分为两个阶段:
n 把源代码编译为Microsoft中间语言(IL Intermediate Language)。
n CLR把IL编译为平台专用的代码。
1、 CLR的作用:
n 内存管理
n 跨语言支持
n 和windows资源集成
n 编译IL成机器码
2、 托管代码的优点:
Microsoft中间语言与Java字节代码共享一种理念:它们都是一种低级语言,语法很简单(使用数字代码,而不是文本代码),可以快速地转换为内部机器码。
(1) 平台无关性
编译成中间语言(IL)就可以获得.NET平台无关性,这与编译为Java字节代码的不会得到Java平台无关性是一样的,.NET目前只能用于Windows平台。
(2) 提高性能
实际上,IL比Java字节代码的作用还要大。IL总是即时编译的(称为JIT编译),而Java字节码常常是解释性的,Java的一个缺点是,在运行应用程序时,把Java字节代码转换为内部可执行代码的过程会导致性能损失(但最近Java在某些平台上能进行JIT编译)。
JIT编译器并不是把整个应用程序一次编译完,这样会有很长的启动时间,而只是编译这调用的那部分代码。代码编译一次后,得到的内部可执行代码就存储起来,直到退出程序为止。
(3) 语言互操作性
将任何一种编译为中间代码,编译好的代码可以与从其他语言通过编译过来的代码进行交互操作。除C#以外,包括语言例如,VB.NET、Visual C++.NET、Visual J#、脚本语言、COM和COM+。
三、 中间语言(Intermediate Language)
中间语言的主要特征:
n 面向对象和使用接口
n 值类型和引用类型之间的巨大差别
n 强数据类型
n 使用异常来处理错误
n 使用特性(attribute)
1、 面向对象和使用接口
Microsoft IL也是传统的面向对象编程,带有类的单一继承性。同时中间语言还引入了接口的概念。
2、 值类型和引用类型之间的巨大差别
与其他编程语言一样,中间语言提供了许多预定义的基本数据类型。对于值类型,变量直接保存其数据,而对于引用类型,变量仅保存地址,对应的数据可以在该地址中找到。
3、 强数据类型
中间语言的一个重要方面是它是基于强数据类型。所有的变量都清晰地标记为属于某个特定的数据类型。一般不允许对模糊的数据类型执行任何操作。
A. 语言互操作性中强数据类型的重要性
如果类派生其他类,或包含其他类的实例,它就需要知道其他类使用的所有数据类型,这就是强数据类型重要性的原因。
例如VB.NET类中一个方法返回一个值,而C#中没有与其匹配的数据类型,在.NET中如何解决?
A) 通用类型系统(CTS,Type System)
以上数据类型问题在.NET中使用CTS解决,CTS定义了可以在中间语言中使用的预定义数据类型,所有用于.NET Framework的语言都可以生成最终基于这些类型的编译代码。例如在VB.NET中的整型和C#中的int型,在中间语言中用类型Int32来表示。
B) 公共语言规范(CLS, Common Language Specification)
CLS和CTS一起确保语言的互操作性。CLS是一个最低标准集,所有面向.NET的编译器都必须支持它。
B. 垃圾收集
到目前为止,Windows平台已经使用了两种技术来释放进程向系统动态请求的内存:
n 完全以手工方式使应用程序代码完成这些工作。
例如C++就是采用此技术,但是最大的缺点就是频繁出现错误。请求内存的代码必须明确通知系统它什么时候不再需要该内存,很容易被遗漏,从而导致内存泄漏。
n 让对象维护引用计数。
维护引用计数是COM对象采用的一种技术,其方法是每个COM组件都保留一个计数,记录客户机目前对它的引用数。当这个数下降到0时,组件会删除自己,并用释放内存和资源。但是只要有一个客户机在引用,对象仍驻留在内存中。
.NET运行库采用的方法是垃圾收集器,这是一个程序,其目的是清理内存,方法是所有动态请求的内存都分配到堆上(CLR维护它自己的托管堆),当.NET检测到给定进程的托管堆已满,就调用垃圾收集器进行清理。
C. 安生性
.NET很好地补足了Windows提供了安全机制,因为它提供的安全机制是基于代码的安全性,而Windows仅提供了基于角色的安全性。基于角色的安全性建立在运行进程的账户的身分基础上,换言之,就是谁拥有和运行进程。另一方面,基于代码的安全性建立在代码实际执行的任务和代码的可信程度上。
D. 应用程序域
4、 使用异常来处理错误
5、 使用特性(attribute)
四、 程序集(assembly)
程序集是包含编译好的、面向.NET Framework的代码的逻辑单元。类似于DLL。程序集完全是自我描述性的,是一个逻辑单元而不是物理单元,它可以存储在多个文件中(动态程序集的确存在内存中,而不是文件中)。程序集的一个重要特性是它们包含的元数据描述了对应代码中定义的类型和方法。程序集也包含描述程序集本身的元数据,这种程序集元数据包含在一个称为程序集清单的区域中,可以检查程序集的版本及其完整性。
程序集包含程序元数据,表示调用给定程序集中的代码的应用程序或其他程序集不需要指定注册表或其他数据源,以便确定如何使用该程序集。这与以前的COM有很大的不同,以前,组件的GUID和接口必须从注册表中获取,在某些情况下,方法和属性的说细信息也需要从类型库中读取。
1、 私有程序集
私有程序集是最简单的一种程序集类型。私有程序集一般附带在某些软件上,而只能用于该软件中。附带私有程序集的常见情况是,以可执行文件或许多库的方法给应用程序提供私有程序集,这些库包含的代码只能用于该应用程序。
因为私有程序集完全是自含式的,所以安装它的过程就很简单。只需把相应的文件放在文件系统的对应文件夹中即可(不需要注册表项),这个过程称为“0影响(xcopy)安装”。
2、 共享程序集
共享程序集是其他应用程序可以使用的公共库。
共享程序集存在风险:
n 名称冲突
n 程序集被同一个程序集的不同版本覆盖——新版本与某些已有的客户机代码不兼容。
这些问题的解决方法是把共享程序集放在文件系统的一个特定的子目录树上,称为全局程序缓冲区(GAC)。与私有程序集不同,不能简单地把共享程序集复制到对应的文件夹中,而需要专门安装到缓冲区中。
3、 反射
因为程序集存储了元数据,包括在程序集中定义的所有类型和这些类型的成员的细节,所以可以编程访问这些元数据。这个技术称为反射。
五、 .NET Framework类
编写托管代码的最大好处是可以使用.NET基类库。
.NET基类包括:
u IL提供的核心功能,例如,通用类型系统中的基本数据类型。
u Windows GUI支持和控件。
u Web窗体(ASP.NET)。
u 数据访问(ADO.NET)。
u 目录访问
u 文件系统和注册表访问
u 网络和Web浏览
u .NET特性和反射
u 访问Windows操作系统的各个方面(例如环境变量)
u COM互操作性
1、 命名空间
Microsoft建议在大多数情况下,都至少要提供两上嵌套的命名空间名,第一个是公司名,第二个是技术名或软件包的名称,而类是其中一个成员。例如:Accp.Web.UserManager。
如果没有显示提供命名空间,类型就添加到一个没有名称的全局命名空间中。
六、 用C#创建.NET应用程序
1、 创建ASP.NET应用程序
n ASP.NET的特性
ASP.NET的页面是结构化的。这就是说,每个页面都是一个继承了.NET类System.Web.UI.Page的类。
可使用MS VS.NET来创建页面、页面使用的业务逻辑和数据访问组件。
n Web窗体
n XML Web服务
2、 创建Windows窗体
3、 Windows服务
七、 C#在.NET企业体系结构中的作用
C#和.NET至少提供了四个优点:
n 组件冲突很少见,部署工作将更容易,因为同一组件的不同版本可以在同一机器上并行运行,而不会发生冲突。
n ASP代码不再很难读懂。
n 可以在.NET基类中使用许多功能。
n 对于需要Windows窗体用户界面的应用程序来说,利用C#可以容易编写这类应用程序。
八、 总结
.NET的下述特性在编译和执行过程中的作用:
n 程序集和.NET基类
n COM组件
n JIT编译
n 应用程序域
n 垃圾收集
下图简要说明这些特性在编译和执行过程中是如何发挥作用的。
通过CTS和CLS进行语言交互 |
包含IL代码的程序集 |
编译 |
C#源代码 |
包含IL代码的程序集 |
VB.NET源代码 |
.NET基类 |
执行 |
加载程序集 |
JIT编译 许可的安全权限 内存类型安全检查 创建应用程序域 |
CLR组织: |
垃圾收集器 简化资源 |
COM交互服务 |
进程
应用程序域 在些执行代码 |
原COM组件 |
第2章:C#基础
一、C#程序的构造
1、 代码
using System;
namespace XMBC.MyPro.MyNS
{
class MyFirstClass
{
static void Main()
{
Console.WriteLine(“Hello!ASP.NET!”);
}
}
}
2、 解析
n 典型的C#源文件以”.CS“作为扩展名。
n C#语言对大小写是敏感的。
n C#中已经抛弃C++中的“::”,只统一用“.”来连接,如:System.Console。
n 命名空间用来避免名称冲突问题,可以在两个不同的Namespace中创建同名的类。
n C#中,程序的执行总是从Main()方法开始,C#中可以出现两个以上的Main(),在编译的时候必须指定应该从哪一个Main()启动。
n System是NET框架提供的最基本的名称空间
n System.Console常用的方法:
A. Read
B. Write
C. ReadLine
D. WriteLine
3、 C#程序的运行
CSC只有编译过程(C和C++有两个过程,编译和连接),所有C#不会产生OBJ文件而直接生成EXE文件或DLL文件,所有C#不需要连接器
csc First.cs
注:csc在一般的DOS提示符下无法运行,需要运行“NET运行工具—Visual Studio .NET命令提示符”。
二、变量(Variable)
1、变量和常量设计原则
n 必须先定义后使用。
n 变量以字母开始可以有数字、下划线,不可以和关键字、库函数同名。
n 变量前面可以加@。
n 可以在一条语句中命名多个类型相同的变量。
例:int a,b,c=50,d;
2、 变量的初始化
n 变量在类或结构中,如果没有显式进行初始化,在默认状态下当创建这些变量时,其值是0。
n 方法的局部变量必须在代码中显式初始化,之后才能使用。如果未初始化,就会产生错误。
3、 变量的类型
n 静态变量(static variables)
在类的运行过程中一直存在,有默认值,最好在定义时被赋值。
n 非静态变量(instance variables)
n 数组元素(array elements)
n 值参数(value parameter)
n 引用参数(reference parameter)
n 输出参数(output parameter)
n 局部变量(local parameter)
4、 变量声明
AccessModifier DataType VariableName
例如:private int a;
AccessModifier(作用域修饰符):
Public:公共的
Private:私有的
Protected:受保护的
5、 常量声明
AccessModifier const DataType Name=Value
例如:Private const int a=10;
6、变量命名规则:
n 内联变量(在方法内声明的变量)必须以骆驼命名法声明;而类变量(成员变量)也必须以骆驼命名法声明,但应以一个下划线开头。
n 不应使用匈牙利命名法(如strName)命名变量。
n 避免使用单个字符作为变量名,但for循环中除外。
7、常量命名规则:
n 带有private的常量必须以骆驼命名法声明,并以一个下划线开头。
n 带有public or protected的常量必须以帕斯卡命名法声明。
l 骆驼命名法:name,time,stuName
l 匈牙利(hungarian)命名法:strName ,intCount(变量名带有数据描述)
l 帕斯卡命名法:UserName,BookPrice
三、预定义数据类型
1、C#中数据类型的分类
C#中的数据类型分为两个基本类型,即值类型和引用类型。值类型的数据存储在内存的堆栈中,从堆栈中可以快速访问这些数据,因此,值类型表示实际数据。引用类型表示指向存储在内存堆(托管堆)中的数据的指针或引用。
n 值类型只将值存放在内存中,这些值类型数据都存储在堆栈中。原始数据类型(如int和bool)都属于此类。
n 引用类型的内存单元只存放内存堆中对象的地址,而对象本身存放在内存堆中。
特点 |
值类型 |
引用类型 |
变量存放的内容 |
实际值 |
引用 |
内存单元 |
内联(堆栈) |
堆 |
默认值 |
0 |
空 |
传递给方法的参数 |
复制值 |
复制引用 |
2、 预定义值类型
n 整型
名称 |
CTS类型(.NET类型) |
说明 |
sbyte |
System.SByte |
8位有符号的整数 |
short |
System.Int16 |
16位有符号的整数 |
int |
System.Int32 |
32位有符号的整数 |
long |
System.Int64 |
64位有符号的整数 |
byte |
System.Byte |
8位无符号的整数 |
ushort |
System.Uint16 |
16位无符号的整数 |
uint |
System.Uint32 |
32位无符号的整数 |
ulong |
System.Uint64 |
64位无符号的整数 |
n 浮点类型
名称 |
CTS类型(.NET类型) |
说明 |
位数 |
float |
System.Single |
32位单精度浮点数 |
7 |
double |
System.Double |
64位双精度浮点数 |
15/16 |
n decimal类型
名称 |
CTS类型(.NET类型) |
说明 |
位数 |
decimal |
System.Decimal |
128位高精度十进制数表示法 |
28 |
n bool类型
名称 |
CTS类型(.NET类型) |
值 |
bool |
System.Boolean |
true或false |
n 字符类型
名称 |
CTS类型(.NET类型) |
值 |
char |
System.Char |
表示一个16位的(Unicode)字符 |
转义序列 |
字符 |
转义序列 |
字符 |
"’ |
单引号 |
"f |
换页 |
"’’ |
双引号 |
"n |
换行 |
"" |
反斜杠 |
"r |
回车 |
"0 |
空 |
"t |
水平制表符 |
"a |
警告 |
"v |
垂直制表符 |
"b |
退格 |
|
|
3、 预定义引用类型
名称 |
CTS类型(.NET类型) |
说明 |
object |
System.Object |
根类型,CTS中的基他类型都是从它派生而来的,包括值类型 |
string |
System.String |
Unicode字符串 |
四、流控制
1、 条件语句
n if…else语句
语法:if(condition)
{……}
else
{……}
n switch语句
语法:
switch(variable)
{
case value:
……
break;
case value:
……
break;
default:
……
break;
}
2、 循环
n for循环
语法:
for ( initializer;condition;iterator)
{……}
n while循环
语法:
while (condition)
{……}
n do…while循环
语法:
do
{……}while (condition)
n foreach循环
语法:
foreach (Type identifier in expression )
{……}
3、跳转语句
n goto语句:跳转
goto Lable;
Label:
n break语句:退出for,foreach,while,do…while,switch
n continue语句:类似于break,但只是退出本次循环
n return语句:返回、
五、枚举
1、 枚举值的定义
public enum TimeOfDay
{
Morning=0,
Afternoon=1,
Evening=2
}
2、 枚举特点
默认情况下枚举的第一个元素被指定为0值,后续的各个元素值依次递增。
六、数组
数组声明:DataType[] ArrayName=new DataType[ElementCount]
例:int[] integers=new int[10]
注:数组元素下标是从0开始。
可以通过属性值Length获得数组元素个数。
七、命名空间
命名空间是一种逻辑组合,而不是物理组合。不允许在另一个嵌套命名空间中声明多部分的命名空间。
命名空间跟程序集无关。同一程序集中可以有不同的命名空间,也可以在不同的程序集中定义同一个命名空间中的类型。
1、using语句(导入命名空间)
示例:using System;
如上所述,所有的C#源代码都以using System;开始,这仅是因为Microsoft提供的许多有用的类都包含在System命名空间中。
n 导入资源之间冲突时,指定完全的限制名
n 导入资源与本地资源冲突时,本地资源优化
2、类别名与命名空间别名
语法如下:
using alias=NamespaceName;
八、Main()方法
1. C#程序从Main()方法开始执行,这个方法必须是类或结构的静态方法,并且返回类型必须是int或void。
2. 如果程序中包括多个Main()方法,可以使用/main选项,其后跟Main()方法所属类的全名(包括命名空间),显式地告诉编译器把哪个方法作为程序的入口点。
csc FileName.cs / main:NamespaceName.ClassName
3. 给Main()方法传送参数
在调用程序时,CLR将任何命令行都作为一个参数传递给程序.这个参数是一个字符串数组,传统称为args(但C#可以接受任何名称)。在启动程序时,可以使用这个数组,访问通过命令行传送过来的选项。
using System;
namespace Wrox.ProCSharp.Basics
{
class AragsExample
{
public static int Main(string[] args)
{
for(int i=0;i<args.Length;i++)
{
Console.WriteLine(args[i]);
}
return 0;
}
}
}
通过使用命令行就可以编译这段代码。在运行编译好的可执行文件时,可以在程序名的后面加上参数,例如:
AragsExample /a /b /c
/a
/b
/c
九、 有关编译C#文件的更多内容
1、 要将C#文件编译成一种文件类型,需使用/target选项(常简写为/t)来指定要创建的文件类型。文件类型可是下表中的一种。
选项 |
输出 |
/t:exe |
控制台应用程序(默认) |
/t:library |
带有清单的类库 |
/t:module |
没有清单的组件 |
/t:winexe |
Windows应用程序(没有控制台窗口) |
示例:
1) 将MathLibrary.cs文件编译成类库:
csc /t:library MathLibrary.cs
2) 在其它文件中引用编译好的类库MathLibrary.dll
csc MathClient.cs/r:MathLibrary.dll
注:一个文件中引用其他的类库使用:/reference或/r选项,后跟程序集的路径和文件名。
十、 控制台I/O
Console类的几个静态方法:
n Console.Write():将指定的值写入控制台窗口。
n Console.WriteLine():与Write()类似,但输出结果最后添加一个换行符。
使用输出方法时,可使用占位符:Console.WriteLine(“this is a {0}!”,name)。其中{0}就是占位符,后续的占位符依次递增。
n Console.Read():读取输入的字符。
n Console.ReadLine():与Read()类似,但读取结果最后添加了一个换行符。
十一、注释
1、 源文件内部注释
// 表示单行注释
/* */ 表示多行注释
2、 XML文档说明
C#中,使用XML格式文档说明类与方法。(///) 3斜杠开头,会自导入注释代码。
标识符 |
说明 |
<c> |
把行中的文本标记为代码,例如<c>int i=10;</c> |
<code> |
把多行标记为代码 |
<example> |
标记为一个代码示例 |
<exception> |
说明一个异常类(编译器要验证其语法) |
<include> |
包含其他文档说明文件的注释(编译器要验证其语法) |
<list> |
把列表插入到文档说明中 |
<param> |
标记方法的参数(编译器要验证其语法) |
<paramref> |
表示一单词是方法的参数(编译器要验证其语法) |
<permission> |
说明对成员的访问(编译器要验证其语法) |
<remarks> |
给成员添加描述 |
<returns> |
说明方法的返回值 |
<see> |
提供对另一个参数的交叉引用(编译器要验证其语法) |
<seealso> |
提供描述中的“参见”部分(编译器要验证其语法) |
<summary> |
提供类型或成员的简短小结(类或方法) |
<value> |
描述属性 |
如果要将XML注解,生成一个XML文档说明,需在编译时指定/doc选项。
示例:
csc /t:library /doc:Math.xml Math.cs
十二、C#预处理器指令
预处理器指令就是标识命令从来不会被转化为可执行代码中的命令,但会影响编译过程的各个方面。
1、#define和#undef
声明与取消一个变量标记
2、#if , #elif, #else, #endif
告诉编译器是否要编译某个代码块
示例:
#define DEBUG
int DoSomeWork(double x)
{
#if DEBUG
Console.WriteLine(“x is”,x);
#endif
}
注解:Console.WriteLine命令被包含在#if块中,只有#define声明了符号DEBUG后才执行。当编译器遇到#if语句后,将选检查有关的符号是否存在,如果符号存在,就只编译#if块中的代码,否则会忽略,直至遇到#endif。(调试中使用)
3、#warning和#error
当编译器遇见它们时会产生生警告或错误。后面的文本被显示出来。相当于显示一条警告或错误信息。
4、#region和#endregion
把一段代码标记为有给定名称的一个块。
5、#line
用于改变编译器在警告和错误信息中显示的文件名和行号信息。、
十三、C#编程规则
1、变量、类、方法命名规则
n 它们必须以一个字母或下划线开头,但可以包含数字字符
n 不能把关键字用作标识符,如果一定要使用,可以在标识符的前面加上前缀@符号
2、命名约定(camel:骆驼命名法Pascal:帕斯卡命名法)
n 类中所有私有成员的名称都是camel(骆驼命名法)大小写形式,但需用下划线做为前缀:private int studentCode;
n 传递给方法的所有参数都应是camel大小写形式:public void AddStudent(string studentCode);
十四、代表(引用类型)
n C#中允许使用指针,但必须生命程序是非安全(unsafe)的,使用C#中的引用类型:代表(delegate)
n 代表申明时,只要指定代表指向的原形的类型,不能有返回值,也不能带有输出参数
n 代表也是在System中定义(System.delegate);
例://申明一个指向int类型函数的代表
delegate int MyDelegate();
using System;
delegate int MyDelegate(); //申明一个指向int型函数原形的代表
namespace MyProg
{
class MyClass
{
public int InstanceMethod()
{
Console.Write("正在调用InstanceMethod方法!");
return 0;
}
public int StaticMethod()
{
Console.Write("正在调用StaticMethod方法!");
return 0;
}
}
class Test
{
static void Main(string[] args)
{
MyClass M=new MyClass();
MyDelegate d=new MyDelegate(M.InstanceMethod);
d();
Console.WriteLine(""n执行第一个代表完成!");
d=new MyDelegate(M.StaticMethod);
d();
Console.ReadLine();
}
}
}
十五、装箱和拆箱
1、 在C#类型系统中,任何值类型、引用类型和Object类型之间进行转换;
n 装箱:将一个值类型隐含转换成Object类型
例:int i=10;
object obj=i;
注意:装箱的时候,被装箱的值拷贝一个副本赋给对象
n 拆箱:将对象类型显式地转换为一个值类型
例:i=(int)obj;
第3章:对象和类
一、 类和结构的区别
1、类是存储在堆(heap)上的引用类型,而结构是存储在堆栈(stack)上的值类型。结构生存期的确限制与简单的数据类型一样。
2、结构不支持继承。
3、结构必须提供一个不带参数的构造函数。
4、使用结构可以指定字段如何在内存中布局。
二、 属性
对于类来说,属性是其私有的。因此提供了属性访问器来访问。Get访问器获得属性值。Set访问器设置属性值。
例:
class Student
{
private string _name;
private int _age;
public string Name
{
get
{
return this._name;
}
set
{
this._name=value;
}
}
}
在属性定义中省略set访问器,就中以创建只读属性。同样省略get访问器就可创建只写属性。
三、 按值和引用传递参数
n 值类型的对象包括的是实际数据,所以传递给方法的是数据本身的副本。对值类型对象做修改而不会影响原值类型对象的值。
n 引用类型的对象只包含对象的引用,它们只给方法传递这个引用,而不是对象本身,所以底层对象的修改会保留下来。
n 关键字ref可以迫使值参数通过引用传递给方法。
例如:
static void SomeFunction(int a,ref int b)
{}
四、 方法重载
1、方法重载条件,方法名相同,方法返回类型相同,参数列表个数不同或参数类型不同。以下是参数列表个数不同的一种方法重载。
using System;
public class Area
{
private int areaVal;
public void AreaCal(int radius)
{
areaVal = (22/7)* radius*radius;
}
public void AreaCal(int length, int breadth)
{
areaVal = length*breadth;
}
public void AreaCal(int length, int breadth, int height)
{
areaVal = length*breadth*height;
}
…
}
2、类的成员申明中,可以申明与继承来的成员同名成员(派生隐藏 ),但编译器不会报错误,但会产生警告信息,如果在派生类中使用new,则无警告。
步骤:
1、在继承类中声明同名的新方法
2、在该方法名前附加new关键字
示例:
using System;
class IntAddition
{
public void add()
{
int firstNum=1;
int secondNum=2;
Console.WriteLine("两数之和是:{0}", firstNum+secondNum);
}
}
class StringAddition : IntAddition
{
new public void add()
{
string firstStr="a";
string secondStr="b";
Console.WriteLine("两字符串之和是:{0}", firstStr+secondStr);
}
}
此结果输出:ab。因为子类的方法重载了父类的方法。子类方法前使用了new关键字。
重载与覆盖的区别:
覆盖(override):父类的方法会被子类中相同名称的方法覆盖,覆盖是真正改写了父类的方法。override要遵守一下的规则:
n 方法的名称一定要与父类一样
n 返回的数据类型也要一样
n 所使用的参数也要一样,包括参数的个数和类型
n 限定词只能是越来越开放
重载(overloading):重载时方法名相同,但参数个数不同或数据类型不同,需要遵守一下的规则:
n 方法的名称一定要一样
n 传递的参数个数或类型一定要不一样
重载是发现在一个类之中,而覆盖是子类覆盖父类的方法。重载的方法在编译时处理,覆盖的方法在运行时处理。
五、构造函数
构造函数的声明与Java和C++一样。
n 构造函数名与类名相同。
n 构造函数没有返回类型(有访问修饰符,但一般情况下是public,如果不是,则表明此类不能实例化)。
n 其任务是在创建对象时初始化其内部状态。
一般情况下,如果没有显示的提供构造函数,编译器会在后台创建一个默认的构造构数。构造函数提供任意多的重载。
类的(static)静态方法或静态变量是属于类,而不属于某个特定的对象,因此无需创建对象。
六、 只读字段(readonly字段)
常量的概念就是一个包含不能修改的值的变量。有时可能需要一些变,其值不应该改变,但在运行之前是未知的。C#为这种情形提供了另一个类型的变量:readonly字段。
例如:一个程序,需注册才能使用,因此需制可以打开的次数。每次运行或安装时,从注册表键或其他文件存储中读取。代码如下所示。
public class DocumenEditor
{
public static readonly uint MaxDocuments;
static DocumentEditor
{
MaxDocuments=DosomethingToFindOutMaxNumber();
}
}
七、 Object类
所有的.NET类都派生于System.Object。
1、 System.Object类的方法
方法 |
访问修饰符 |
作用 |
string ToString() |
public virtual |
返回对象的字符串表示 |
int GetHashTable() |
public virtual |
在实现字典(散列表)时使用 |
bool Equals(object obj) |
public virtual |
对对象的实例进行相等比较 |
Bool Equals(object a,object b) |
public static |
对对象的实例进行相等比较 |
Bool ReferenceEquals(object a,object b) |
public static |
比较两个引用是否指向同一个对象 |
Type GetType() |
public |
返回对象类型的详细信息 |
void Finalize() |
protected virtual |
该方法是析构函数的.NET版本 |
object MemberwiseClone() |
protected |
进行对象的浅表复制 |
第4章:继承
一、 继承的类型
n 实现继承:表示一个类型派生于一个基类型,拥有该基类型的所有成员字段和函数。派生类的每个函数采用基类的实现,除非在派生类型的定义中指定重写该函数的实现。
n 接口继承:表示一个类型只继承了函数的签名,没有继承任何实现。
备注:C#不支持多重继承。
二、 实现的继承
实现继承的语法:
class MyDervedClass:MyBaseClass
{
//functions and data members here
}
1、 虚方法(Virtual)
把一个基类函数声明为virtual,该函数就可以在任何派生类中重写(覆盖)了:
class MyBaseClass
{
public virtual string VirtualMethod()
{
return “This Method is virtual and defined in MyBaseClass”;
}
}
在C#中,派生类中重写了虚函数,在调用方法时,会调用对象类型合适的方法。在C#中,函数在默认情况下不是虚拟的,(除了构造函数以外)但可以显示地声明为虚拟。这与C++类似,从性能的角度来看,除非显示指定,否则函数就不是虚拟的,在Java中所有函数都是虚拟的。但C#与C++有些不同,C#要求在派生类的函数重写另一个函数时,要使用override关键字显示声明:
class MyDerivedClass:MyBaseClass
{
public override string VirtualMethod()
{
return “This method is an override defined in MyDerivedClass”;
}
}
2、 隐藏方法
如果签名相同的方法在基类和派生类中都进行了声明,但基类方法没有声明为virtual和派生类方法没有声明为override,派生类方法就会隐藏基类方法。结果是调用哪个类的方法取决于引用实例的变量类型,而不是实例本身的类型。
例如:
class MyBaseClass
{
public int MyMethod()
{
return “This Method in MyBaseClass”;
}
}
class MyDerivedClass:MyBaseClass
{
public new int MyMethod()
{
return “This Method in MyDerivedClass”;
}
}
如果子类中的MyMethod方法没有声明为new,编译器会认为隐藏了基类的同名方法。并且发出一个警告,是否需要将基类的MyMethod方法声明为vritual。或者给子类的方法重命名。
3、 抽象类和抽象函数(abstract)
C#允许把类和函数声明为abstract,抽象类不能实例化,而抽象函数没有执行代码,必须在非抽象的派生类中重写。
abstract class MyClass
{
public abstract int MyMethod();
}
注意:在C++中,抽象函数常常声明为纯虚函数(纯虚函数是虚函数的一种特别形式,没有方法体),而在C#中,仅使用抽象这个术语。
4、 密封类和密封方法(sealed)
C#允许把类和方法声明为sealed,对于类来说,这表示不能继承该类,对于方法来说,表示不能重写该方法。
因商业原因把类或方法标记为sealed,以防第三方以违返注册协议的方式扩展该类。.NET基类库大量使用了密封类,使希望从这些类中派生出自己的类的第三方开发人员无法访问这些类。例如string就是一个密封类。
注意:Java开发人员可以把C#中的sealed当作Java中的final。
5、 派生类的构造函数
abstract class Color
{
private string name;
}
class Red:Color
{
private int rColor;
}
构造过程解释:
编译器首先找到试图实例化的类的构造构函数,本例中是Red()。这个默认构造函数首先要做的是为其直接基类Color类运行默认构造函数,然后Color类构造函数Color()为其直接基类System.Object类运行默认构造函数。System.Object没有任何基类,所以它的构造函数就执行,并把控制返回给Color类的构造函数。现在执行Color类的构造函数,把name初始化为null。再把控制权返回给Red类的构造函数,然后执行Red()构造函数,把rColor初始化为0,并退出。此时Red实例就已经成功地构造和初始化了。
构造函数的调用顺序是先调用System.Object,再按照层次结构由上向下进行,直到到达编译器要实例化的类为止。
(1)构造函数(指定继承)
class Vehicle
{
public Vehicle(){}
public Vehicle(int w,int h){}
}
class Car:Vehicle
{
//注:Car类在启动时,首先运行的是
父类的Vehicle(int w,int h);
public Car(w,h):base(w,h)
{
//函数体
}
}
三、 修饰符
1、 可见性修饰符
修饰符 |
应用于 |
说明 |
public |
所有类或成员 |
任何代码均可访问该方法 |
protected |
类和内嵌类的所有成员 |
只有派生的类能访问该方法 |
internal |
类和内嵌类的所有成员 |
只能在包含它的程序集中访问该方法 |
private |
所有的类或成员 |
只能在它所属的类中访问该方法 |
protected internal |
类和内嵌类的所有成员 |
只能在包含它的程序集和派生类的代码中访问该方法 |
2、 其他修饰符
修饰符 |
应用于 |
说明 |
new |
函数成员 |
成员用相同的签名隐藏继承的成员(即父类的成员) |
static |
所有的成员 |
成员不在类的具体实例上执行 |
virtual |
仅函数成员 |
成员可以由派生类重写 |
abstract |
仅类和函数成员 |
只是定义了成员的签名,没有实现 |
override |
仅函数成员 |
成员重写了被继承的虚拟或抽象成员 |
sealed |
类 |
该类的不能被重写。 |
extern |
仅静态[DllImport]方法 |
成员在外部用另一种语言实现 |
四、组件与接口
1、Window编程的优势
n 组件对象模型(COM)
n Microsoft事务服务器(MTS)
n 应用程序编程接口(Applicaton Programming Interface)
2、应用结构技术
n C/S模式问题:客户连接开销、服务器数据格式限制、可扩展性、维护性
n 三层结构:
表现层:提供数据,展现用户接口
商业层:实施商业逻辑,表示层使用商业层提供服务
数据层:执行具体的数据访问服务,包含检索和存储
n 优势:
1)商业层不关心连接个数,不关心数据的存储方式
2)层次的独立性,任何一层的修改对其他层次不影响
3、组件(Component)
n 工程的模块分割,每个模块完成独立的功能,模块间协同工作,这样的模块就称为“组件”
n 特征:可以单独开发、单独编译、单独测试
4、采用组件装配需要考虑的问题
n 采用标准方式来规范组件的定位和使用,提高通用性和减少培训成本
n 提供对象化交互操作的标准方式
n 提供灵活的版本控制能力
n 提供用户需要的安全性
n 组件的交换性、开放性、版本控制能力和稳定性需要接口来实现
5、接口设计原则
n 接口描述组件对外提供的服务,在组件之间、组件和客户之间都是通过接口进行交互
n 组件一旦发布,就只能通过预定义的接口来提供合理的,一致的服务(接口稳定性)
n 组件的接口必须能够自我描述,不依赖于具体实现,来测定消除接口的使用者和实现者之间的相关性,增加组件封装程序(要求接口必须和组件实现无关的语言,多用IDL语言)
n 接口是组件协议,接口一旦发布,组件就应该尽量保持接口不变
n 如果组件具有自主性,通过接口和外界通信;如果组件提供新的服务,应该增加新的接口而不修改旧的接口,保证已有客户的稳定性
6、组件化程序设计原则
n 强调可重用性和高度互操作性;侧重组件的产生和装配
n 组件是一种规范,不管采用任何语言设计组件,都必须遵循;C#可以很好的设计组件
7、接口的实现
n 接口是纯抽象基类
n 它只包含抽象方法,而不包含任何方法实现
n 实现特定接口的类必须实现该接口列出的成员
n 可用修饰符:new / public / protected / internal / private
示例:
public interface IFile
{
int delFile();
void disFile();
}
8、接口的成员
n 包括一个或多个成员,可以是方法、属性、索引器和时间
n 不可以是常量、域、操作符、构造函数和析构函数
n 不可以包含任何静态成员
n 接口成员默认访问方式为public,不可以包含任何修饰符
n 接口成员不可以同名,但继承时候可以覆盖继承来的成员(使用new可以避免警告错误)
9、接口演示
//申明代表
public delegrate void EventHandler(object sender,EventArgs e);
interface IExample
{
//索引器接口
string this[int index]
{get;
set;}
//事件接口
event EventHandler E;
//方法接口
void F(int V);
//属性接口
string P
{get;
set; }
}
10、接口的继承
n 接口的继承中,派生接口只继承了父接口的成员方法的说明,而没有继承父接口的实现
n C#中只允许单继承,但接口允许多继承,一个子接口可以有多个父接口(多个接口用“,”分开,继承符为“:”)
n private/Internal类型的接口中的继承是不允许的
n 接口不能直接或间接从自己继承
11、接口的继承示例
interface IControl
{ void Paint(); }
interface ITextBox:IControl
{ void setTexts(string text); }
interface IListBox:IControl
{ void setItems(string[] items); }
interface IComboBox:ITextBox,IListBox
{
//其他的接口部分
}
注:IComboBox同时继承了IControl.Paint、ITextBox.SetText、IListBox.SetItem三个接口元素
12、接口的实现
n 接口定义不包括方法的实现部分,只能通过类或结构来实现
n 接口的实现和类的继承可以同时产生,派生类同时实现接口和获得继承特性
public class BaseforInterface
{
public void open()
{
System.Console.WriteLine("这是 BaseforInterface 的 open 方法");
}
}
13、单接口实现演示
public interface IFile
{
int delFile();
void disFile();
}
public class MyFile : IFile
{
public int delFile()
{
System.Console.WriteLine("DelFile 实现!");
return 0;
}
public void disFile()
{
System.Console.WriteLine("DisFile 实现!");
}
}
class Test
{
static void main()
{
MyFile objMyFile = new MyFile();
objMyFile.disFile();
int retValue = objMyFile.delFile();
}
}
注意:类MyFile继承了接口IFile并实现了接口中定义的方法delFile()和disFile()
14、单接口和继承的结合演示
public class MyFile : BaseforInterface, IFile
{
public int delFile()
{
System.Console.WriteLine("DelFile 实现!");
return 0;
}
public void disFile()
{
System.Console.WriteLine("DisFile 实现!");
}
}
class Test
{
static void main()
{
MyFile objMyFile = new MyFile();
objMyFile.disFile();
int retValue = objMyFile.delFile();
objMyFile.open();
}
}
注意:定义一个继承类BaseforInterface和接口Ifile的新类MyFile
15、多接口实现演示
public interface IFileTwo
{
void applySecondInterface();
}
public interface IFile
{
int delFile();
void disFile();
}
public class MyFile : IFile, IFileTwo
{
public int delFile()
{
System.Console.WriteLine("DelFile 实现!");
return 0;
}
public void disFile()
{
System.Console.WriteLine("DisFile 实现!");
}
public void applySecondInterface()
{
System.Console.WriteLine(“ApplySecondInterface 实现!”);
}
}
class Test
{
static void main()
{
MyFile objMyFile = new MyFile();
objMyFile.disFile();
int retValue = objMyFile.delFile();
objMyFile.open();
objMyFile.applySecondInterface();
}
}
16、多接口实现的二义性问题
n 接口支持多继承,如果多个父接口有同名成员,会产生二义性——需要显示申明
n 如果层次继承中,下层接口出现上层接口同名成员,则隐藏
17、多接口实现的二义性问题示例
interface IInteger
{
void Add(int i);
}
interface IDouble
{
void Add(double d);
}
interface INumber:IInteger,IDouble{}
class MyClass
{
void Test(INumber n)
{
n.add(1); //错误,因为可以使用两个重载函数,产生二义性
n.add(1.0); //正确,因为只有Add(double d)可以
((IInteger)n).Add(1); //可以,显式指派
((IDouble)n).Add(1); //可以
}
}
18、接口继承的二义性问题
interface IBase
{ void F(int i); }
interface ILeft:IBase
{ new void F(int i); }
interface IRight:IBase
{ void G(); }
interface IDerived:ILeft,IRight { }
class MyClass
{
void Test(IDerived d)
{
d.F(1); //执行的是ILeft.F,因为继承时被拦截
((IBase)d).F(1); //IBase.F
((ILeft)d).F(1); //ILeft.F
((IRight)d).F(1); //IBase.F,因为IRight并没有覆盖IBase.F();
}
}
注意:一旦成员被覆盖后,所有对其的访问都被覆盖后的成员拦截
19、显式接口成员
n 当两个接口中具有同名的方法时,可以使用显式接口成员
n 为了便于编写,可以使用接口成员的全权名:接口名.成员名
20、显式接口成员的实现
public interface IFile
{
int delFile();
void disFile();
}
public interface IFileTwo
{
void applySecondInterface();
void disFile();
}
public class MyFile : BaseforInterface, IFile, IFileTwo
{...
void IFile.disFile()
{
System.Console.WriteLine("DisFile 的 IFile 实现");
}
void IFileTwo.disFile()
{
System.Console.WriteLine("DisFile 的 IFileTwo 实现");
}
...
}
21、显式接口成员的调用
n 不能在方法调用的时候直接通过全权名访问显式借口成员执行体,只能创建接口的实例,引用接口实例的成员名来访问
n 目的:
1)将共有接口中把接口的实现单据分离(如果一个类在期内部使用接口,而类的继承者不会直接只用该接口)
2)避免接口成员同名产生混淆
3)类继承时,接口成员的全权名必须对应在接口中申明的成员
22、显式接口成员的调用
class TextBox:IControl,ITextBox
{
public/private void IControl.Paint() { //执行部分 }
public/private void ITextBox.SetText(string T) { //执行部分 }
}
class Test
{
static void Main()
{
TextBox T=new TextBox();
T.Paint(); //错误,没有这方法
IControl C=T; //对象上传
C.Paint(); //正常
}
}
五、一个示例(关于new、override、virtual)
using System;
namespace MyProject
{
/// <summary>
/// 接口类MyInterface
/// </summary>
interface MyInterface
{
void beep();
void show();
void station();
}
/// <summary>
/// 抽象类MyAbstract
/// </summary>
abstract class MyAbstract:MyInterface
{
public abstract void beep();
public abstract void show();
public void station()
{
}
}
/// <summary>
/// 车的基类Vechile
/// </summary>
class Vechile:MyAbstract
{
private int _wheels;
private float _weight;
public int Wheels
{
get{return _wheels;}
set{_wheels=value;}
}
public float Weight
{
get{return _weight;}
set{_weight=value; }
}
/// <summary>
/// 车基类不带参数的构造函数
/// </summary>
public Vechile()
{
}
/// <summary>
/// 车基类带参数的构造函数
/// </summary>
/// <param name="wheels">车的轮子数</param>
/// <param name="weight">车的重量</param>
public Vechile(int wheels,float weight)
{
this.Weight=weight;
this.Wheels=wheels;
}
/// <summary>
/// 描述车发音的方法
/// </summary>
public override void beep()
{
Console.WriteLine("车会发出声音");
}
/// <summary>
/// 显示车的详细信息
/// </summary>
public override void show()
{
Console.WriteLine("车轮有{0}个",this.Weight);
Console.WriteLine("车的重量{0}千克",this.Wheels);
}
}
/// <summary>
/// 小汽类Car,继承自车基类Vechile
/// </summary>
class Car:Vechile
{
private int _passengers;
private int _compartment;
public int Passengers
{
get{return _passengers; }
set{_passengers=value; }
}
public int Compartment
{
get{return _compartment; }
set{_compartment=value; }
}
/// <summary>
/// 小汽车不带参数的构造函数
/// </summary>
public Car()
{
}
/// <summary>
/// 小汽车带参数的构造函数
/// </summary>
/// <param name="passengers">小汽车的载人数</param>
/// <param name="compartment">小汽车的车箱数</param>
/// <param name="weight">小汽车的重量</param>
/// <param name="wheels">小汽车的车轮数</param>
public Car(int wheels,float weight,int passengers,int compartment)
{
this.Passengers=passengers;
this.Compartment=compartment;
this.Wheels=wheels;
this.Weight=weight;
}
/// <summary>
/// 描述小汽车发声音的方法
/// </summary>
public override void beep()
{
Console.WriteLine("小汽车发出嘀嘀声");
}
/// <summary>
/// 显示小汽车详细信息
/// </summary>
public override void show()
{
Console.WriteLine("小汽车的重量数为{0}千克",this.Weight);
Console.WriteLine("小汽车的车轮为{0}个",this.Wheels);
Console.WriteLine("小汽车的车箱数为{0}个",this.Compartment);
Console.WriteLine("小汽车的乘客数为{0}人",this.Passengers);
}
}
class Train:Vechile
{
private int _passengers;
private int _compartment;
public int Passengers
{
get{return _passengers; }
set{_passengers=value; }
}
public int Compartment
{
get{return _compartment; }
set{_compartment=value; }
}
/// <summary>
/// 火车不带参数的构造函数
/// </summary>
public Train()
{
}
/// <summary>
/// 火车带参数的构造函数
/// </summary>
/// <param name="passengers">火车的乘客数</param>
/// <param name="compartment">火车的车箱数</param>
/// <param name="weight">火车的重量</param>
/// <param name="wheels">火车的车轮数</param>
public Train(int wheels,float weight,int passengers,int compartment)
{
this.Weight=weight;
this.Wheels=wheels;
this.Passengers=passengers;
this.Compartment=compartment;
}
/// <summary>
/// 火车的发声描述
/// </summary>
public override void beep()
{
Console.WriteLine("火车发出嘟嘟声");
}
/// <summary>
/// 显示火车的详细信息
/// </summary>
public override void show()
{
Console.WriteLine("火车的车轮数为{0}个",this.Wheels);
Console.WriteLine("火车的重量为{0}千克",this.Weight);
Console.WriteLine("火车的车箱数为{0}个",this.Compartment);
Console.WriteLine("火车的乘客数为{0}人",this.Passengers);
}
/// <summary>
/// 显示火车站名的方法
/// </summary>
public new void station()
{
Console.WriteLine("厦门火车站");
Console.ReadLine();
}
}
/// <summary>
/// Class1 的摘要说明。
/// </summary>
class Class1
{
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main(string[] args)
{
Car car=new Car(4,100000,5,1);
car.beep();
car.show();
Train train=new Train(100,9999999,1000,15);
train.beep();
train.show();
train.station();
}
}
}
显示结果:
小汽车发出嘀嘀声
小汽车的重量数为100000千克
小汽车的车轮为4个
小汽车的车箱数为1个
小汽车的乘客数为5人
火车发出嘟嘟声
火车的车轮数为100个
火车的重量为9999999千克
火车的车箱数为15个
火车的乘客数为1000人
厦门火车站
第五章:运算符和类型强制转换
一、 运算符一览表
类别 |
运算符 |
算术运算符 |
+ - * / % |
逻辑运算符 |
& | ^ ~ && || ! |
字符串连接运算符 |
+ |
增量和减量运算符 |
++ -- |
移位运算符 |
<< >> |
比较运算符 |
= = != <> <= >= |
赋值运算符 |
= += -= *= /= %= &= |= ^= <<= >>= |
成员访问运算符(用于对象和结构) |
. |
索引运算符(用于数组和索引器) |
[] |
数据类型转换运算符 |
() |
条件运算符(三元运算符) |
?: |
对象创建运算符 |
new |
类型信息运算符 |
sizeof(只用于不安全的代码) is typeof as |
益出异常控制运算符 |
checked unchecked |
间接寻址运算符 |
* -> &(只用于不安全代码) [] |
1、 checked和unchecked运算符
考虑下面的代码:
byte b=255;
b++;
Console.WriteLine(b.ToString());
byte数据类型只能包含0~255的数,所以b值的增量会导致溢出。
如果把一个代码块标记为checked,CLR就会执行溢出检查,如果发生溢出,就会抛出异常。
byte b=255;
checked
{
b++;
}
Console.WriteLine(b.ToString());
注意:用/checked选项进行编译,就可以检查程序中所有未标记代码中的溢出。
如果要禁止检查,可以把代码标记为unchecked(默认值):
byte b=255;
unchecked
{
b++;
}
Console.WriteLine(b.ToString());
2、 is运算符
is运算符可以检查对象是否与特定的类型兼容。“兼容”是指对象是该类型,或者派生于该类型。
示例:
int i=10;
if ( i is object)
{
Console.WriteLine (“i is an object”);
}
3、 as运算符
as运算符用于执行引用类型的显式类型转换。如果要转换的类型与指定的类型兼容,转换就会成功进行;如果类型不兼容,as运算符就会返回值null。如下面的代码所示,如果object引用不指向string实例,把object引用转换为string就会返回null。
object obj1=”Some String”;
object obj2=5;
string s1=obj1 as string; //s1=”Some String”
string s2=obj2 as string; //s2=null
4、 sizeof运算符
使用sizeof运算符可以确定堆栈中值类型需要的长度(单位是字节)。
String s=”A string”;
Console.WriteLine(sizeof(int));
其结果是显示数字4,因为int有4个字节。
5、 typeof运算符
typeof运算符返回一个表示特定类型的Type对象(系统原形对象)。
例:Console.WriteLine(typeof(int))
结果:Int32
二、 运算符重载
n 所有数据属于某个类,希望给已经定义的操作符赋予新的含义,在特定的类的实例上进行新的解释
n 使用操作符可以使方程式简单易懂
n 语法:
type operator operator-name(formal-param-list)
n 可重载的操作符
+、-、!、~、++、true、false、*、/、%、&、|、^、<<、>>、==、!=、>、<、>=、<=
n 不可重载的操作符
=、&&、||、?:、new、typeof、sizeof、is
示例:
using System;
namespace MyProg
{
//一元操作符重载
public class Game
{
private int tl;
private int nl;
private int mf;
public Game()
{
tl=100;
nl=100;
mf=100;
}
public static Game operator ++(Game g)
{
g.tl=g.tl+50;
g.nl=g.nl+50;
g.mf=g.mf+50;
return g;
}
public void Showinfo(Game g)
{
Console.WriteLine("体力:{0},智力:{1},魔法{2}",g.tl,g.nl,g.mf);
Console.ReadLine();
}
}
//二元操作符重载
public class Js
{
public int a;
public int b;
public int c;
public Js()
{
this.a=10;
this.b=10;
this.c=10;
}
public static Js operator +(Js j1,Js j2)
{
Js j=new Js();
j.a=j1.a+j2.a;
j.b=j1.b+j2.b;
j.c=j1.c+j2.c;
return j;
}
public void Showinfo(Js g)
{
Console.WriteLine("A:{0},B:{1},C:{2}",g.a,g.b,g.c);
Console.ReadLine();
}
}
class Test
{
static public void Main()
{
Game G=new Game();
G++;
G.Showinfo(G);
Console.WriteLine("测试二元操作符重载......"n");
Js a1=new Js();
a1.a=100;
a1.b=15;
a1.c=25;
Js b1=new Js();
Js c1=a1+b1;
c1.Showinfo(c1);
}
}
}
第六章:委托和事件
回调函数(call back)函数是Windows编程的一个重要部分。回调函数实际上是方法调用的指针,也称为函数指针,是一个非常强大的编程特性。.NET以委托的形式实现了函数指针的概念。它们的特殊之处是,与C函数指针不同,.NET委托是类型安全的。这说明,C中的函数指针只不过是一个指向存储单元的指针,我们无法说出这个指针实际指向什么,像参数和返回类型等就更无从知晓了。
一、委托
委托只是一种特殊的对象类型,其特殊之处在于,我们以前定义的所有对象都包含数据,而委托包含的只是方法的细节。
1、在C#中使用委托
在C#中使用一个类时,分两个阶段。首先需要定义这个类,即告诉编译器这个类由什么字段和方法组成。然后(静态方法除外)实例化类的一个对象。
使用委托时,也需要经过这两个步骤。首先定义要使用的委托,对于委托,定义它就是告诉编译器这种类型代表了哪种类型的方法,然后创建委托的一个或多个实例。
定义委托的语法如下:
delegate void VoidOperation(uint x);
在这个示例中,定义了一个委托VoidOperation,并指定该委托的每个实例都包含一个方法的细节,该方法带有一个uint参数,并返回Void。理解委托的一种好方式是把委托的作用当作是给方法签名指定名称。
注意:实际上,“定义一个委托”是指“定义一个新类”。委托实现为派生于基类System.MulticastDelegate的类,Sytem.MulticastDelegate又派生于基类System.Delegate。C#编译器知道这个类,使用其委托语法,因此我们不需要了解这个类的具体执行情况。
定义好委托后,就可以创建它的一个实例,以存储特定方法的细节。
注意:此处,在术语方面有一个问题。类有两个不同的术语:“类”表示较广义的定义,“对象”表示类的实例。但委托只有一个术语。在创建委托的实例时,所创建的委托的实例仍称为委托。你需要从上下文中确定委托的确切含义。
2、多播委托
委托可以包含多个方法。这种委托称为多播委托。如果调用多播委托,就可以按顺序连续调用多个方法。为此,委托的签名就必须返回void(否则,返回值应送到何处?)。多播委托实例化会产生一个委托数组。
多播委托可识别运算符+和+=,也识别-和-=(以从委托中删除方法调用)。
注意:多播委托是一个派生于System.MulticastDelegate的类,这派生于基类System.Delegate。System.MulticastDelegate的其他成员允许把多个方法调用链接在一起,成为一个列表。
示例:
using System;
//定义委托
public delegate void MyDel(int a,int b);
namespace MyProg
{
class JJ
{
public void Add(int a,int b)
{
Console.WriteLine("执行Add方法结果为:a+b={0}",a+b);
}
public void Sub(int a,int b)
{
Console.WriteLine("执行Sub方法结果为:a-b={0}",a-b);
}
}
class QQ
{
public void Mul(int a,int b)
{
Console.WriteLine("执行Mul方法结果为:a*b={0}",a*b);
}
public void Div(int a,int b)
{
Console.WriteLine("执行Div方法结果为:a/b={0}",a/b);
}
}
class Test
{
static void Main(string[] args)
{
JJ jj=new JJ();
QQ qq=new QQ();
MyDel mydel=new MyDel(jj.Add);
mydel(2,1);
Console.WriteLine("重定向方法");
Console.ReadLine();
mydel=new MyDel(jj.Sub);
mydel(2,1);
Console.ReadLine();
//委托的批处理性
Console.WriteLine("批处理:类内传播");
mydel+=new MyDel(jj.Add);
mydel(2,1);
Console.ReadLine();
Console.WriteLine("批处理:类间传播");
mydel+=new MyDel(qq.Mul);
mydel+=new MyDel(qq.Div);
mydel(2,1);
Console.ReadLine();
Console.WriteLine("批处理:方法去除");
mydel-=new MyDel(jj.Add);
mydel(2,1);
Console.ReadLine();
}
}
}
运行结果如下:
执行Add方法结果为:a+b=3
重定向方法
执行Sub方法结果为:a-b=1
批处理:类内传播
执行Sub方法结果为:a-b=1
执行Add方法结果为:a+b=3
批处理:类间传播
执行Sub方法结果为:a-b=1
执行Add方法结果为:a+b=3
执行Mul方法结果为:a*b=2
执行Div方法结果为:a/b=2
批处理:方法去除
执行Sub方法结果为:a-b=1
执行Mul方法结果为:a*b=2
执行Div方法结果为:a/b=2