首页 > 代码库 > 初学者第二季
初学者第二季
第一章 C#语言基础
本章介绍C#语言的基础知识,希望具有C语言的读者能够基本掌握C#语言,并以此为基础,能够进一步学习用C#语言编写window应用程序和Web应用程序。当然仅靠一章的内容就完全掌握C#语言是不可能的,如需进一步学习C#语言,还需要认真阅读有关C#语言的专著。
1.1 C#语言特点
Microsoft.NET(以下简称.NET)框架是微软提出的新一代Web软件开发模型,C#语言是.NET框架中新一代的开发工具。C#语言是一种现代、面向对象的语言,它简化了C++语言在类、命名空间、方法重载和异常处理等方面的操作,它摒弃了C++的复杂性,更易使用,更少出错。它使用组件编程,和VB一样容易使用。C#语法和C++和JAVA语法非常相似,如果读者用过C++和JAVA,学习C#语言应是比较轻松的。
用C#语言编写的源程序,必须用C#语言编译器将C#源程序编译为中间语言(MicroSoft Intermediate Language,MSIL)代码,形成扩展名为exe或dll文件。中间语言代码不是CPU可执行的机器码,在程序运行时,必须由通用语言运行环境(Common Language Runtime,CLR)中的既时编译器(JUST IN Time,JIT)将中间语言代码翻译为CPU可执行的机器码,由CPU执行。CLR为C#语言中间语言代码运行提供了一种运行时环境,C#语言的CLR和JAVA语言的虚拟机类似。这种执行方法使运行速度变慢,但带来其它一些好处,主要有:
? 通用语言规范(Common Language Specification,CLS):.NET系统包括如下语言:C#、C++、VB、J#,他们都遵守通用语言规范。任何遵守通用语言规范的语言源程序,都可编译为相同的中间语言代码,由CLR负责执行。只要为其它操作系统编制相应的CLR,中间语言代码也可在其它系统中运行。
? 自动内存管理:CLR内建垃圾收集器,当变量实例的生命周期结束时,垃圾收集器负责收回不被使用的实例占用的内存空间。不必象C和C++语言,用语句在堆中建立的实例,必须用语句释放实例占用的内存空间。也就是说,CLR具有自动内存管理功能。
? 交叉语言处理:由于任何遵守通用语言规范的语言源程序,都可编译为相同的中间语言代码,不同语言设计的组件,可以互相通用,可以从其它语言定义的类派生出本语言的新类。由于中间语言代码由CLR负责执行,因此异常处理方法是一致的,这在调试一种语言调用另一种语言的子程序时,显得特别方便。
? 增加安全:C#语言不支持指针,一切对内存的访问都必须通过对象的引用变量来实现,只允许访问内存中允许访问的部分,这就防止病毒程序使用非法指针访问私有成员。也避免指针的误操作产生的错误。CLR执行中间语言代码前,要对中间语言代码的安全性,完整性进行验证,防止病毒对中间语言代码的修改。
? 版本支持:系统中的组件或动态联接库可能要升级,由于这些组件或动态联接库都要在注册表中注册,由此可能带来一系列问题,例如,安装新程序时自动安装新组件替换旧组件,有可能使某些必须使用旧组件才可以运行的程序,使用新组件运行不了。在.NET中这些组件或动态联接库不必在注册表中注册,每个程序都可以使用自带的组件或动态联接库,只要把这些组件或动态联接库放到运行程序所在文件夹的子文件夹bin中,运行程序就自动使用在bin文件夹中的组件或动态联接库。由于不需要在注册表中注册,软件的安装也变得容易了,一般将运行程序及库文件拷贝到指定文件夹中就可以了。
? 完全面向对象:不象C++语言,即支持面向过程程序设计,又支持面向对象程序设计,C#语言是完全面向对象的,在C#中不再存在全局函数、全区变量,所有的函数、变量和常量都必须定义在类中,避免了命名冲突。C#语言不支持多重继承。
1.2 编写控制台应用程序
1.2.1 使用SDK命令行工具编写控制台程序
第一个程序总是非常简单的,程序首先让用户通过键盘输入自己的名字,然后程序在屏幕上打印一条欢迎信息。程序的代码是这样的:
using System;//导入命名空间。//为C#语言新增解释方法,解释到本行结束
class Welcome//类定义,类的概念见下一节
{ /*解释开始,和C语言解释用法相同
解释结束*/
static void Main()//主程序,程序入口函数,必须在一个类中定义
{ Console.WriteLine("请键入你的姓名:");//控制台输出字符串
Console.ReadLine();//从键盘读入数据,输入回车结束
Console.WriteLine("欢迎!");
}
}
可以用任意一种文本编辑软件完成上述代码的编写,然后把文件存盘,假设文件名叫做welcome.cs,C#源文件是以cs作为文件的扩展名。和C语言相同,C#语言是区分大小写的。高级语言总是依赖于许多在程序外部预定义的变量和函数。在C或C++中这些定义一般放到头文件中,用#include语句来导入这个头文件。而在C#语言中使用using语句导入名字空间,using System语句意义是导入System名字空间,C#中的using语句的用途与C++中#include语句的用途基本类似,用于导入预定义的变量和函数,这样在自己的程序中就可以自由地使用这些变量和函数。如果没有导入名字空间的话我们该怎么办呢?程序还能保持正确吗?答案是肯定的,那样的话我们就必须把代码改写成下面的样子:
class Welcome
{ static void Main()
{ System.Console.WriteLine("请键入你的姓名:");
System.Console.ReadLine();
System.Console.WriteLine("欢迎!");
}
}
也就是在每个Console前加上一个前缀System.,这个小原点表示Console是作为System的成员而存在的。C#中抛弃了C和C++中繁杂且极易出错的操作符象::和->等,C#中的复合名字一律通过.来连接。System是.Net平台框架提供的最基本的名字空间之一,有关名字空间的详细使用方法将在以后详细介绍,这里只要学会怎样导入名字空间就足够了。
程序的第二行class Welcome声明了一个类,类的名字叫做Welcome。C#程序中每个变量或函数都必须属于一个类,包括主函数Main(),不能象C或C++那样建立全局变量。C#语言程序总是从Main()方法开始执行,一个程序中不允许出现两个或两个以上的Main()方法。请牢记C#中Main()方法必须被包含在一个类中,Main第一个字母必须大写,必须是一个静态方法,也就是Main()方法必须使用static修饰。static void Main()是类Welcome中定义的主函数。静态方法意义见以后章节。
程序所完成的输入输出功能是通过Console类来完成的,Console是在名字空间System中已经定义好的一个类。Console类有两个最基本的方法WriteLine和ReadLine。ReadLine表示从输入设备输入数据,WriteLine则用于在输出设备上输出数据。
如果在电脑上安装了Visual Studio.Net,则可以在集成开发环境中直接选择快捷键或菜单命令编译并执行源文件。如果您不具备这个条件,那么至少需要安装Microsoft.Net Framework SDK,这样才能够运行C#语言程序。Microsoft.Net Framework SDK中内置了C#的编译器csc.exe,下面让我们使用这个微软提供的命令行编译器对程序welcome.cs进行编译。假设已经将welcome.cs文件保存在d:\Charp目录下,启动命令行提示符,在屏幕上输入一行命令:d:回车,cd Charp回车,键入命令:
C:\WINNT\Microsoft.NET\Framework\v1.0.3705\csc welcome.cs
如果一切正常welcome.cs文件将被编译,编译后生成可执行文件Welcome.exe。可以在命令提示符窗口运行可执行文件Welcome.exe,屏幕上出现一行字符提示您输入姓名:请键入你的姓名:输入任意字符并按下回车键,屏幕将打印出欢迎信息:欢迎!
注意,和我们使用过的绝大多数编译器不同,在C#中编译器只执行编译这个过程,而在C和C++中要经过编译和链接两个阶段。换而言之C#源文件并不被编译为目标文件.obj,而是直接生成可执行文件.exe或动态链接库.dll,C#编译器中不需要包含链接器。
1.2.1 使用Visual Studio.Net建立控制台程序
(1) 运行Visual Studio.Net程序,出现如图1.2.2A界面。
(2) 单击新建项目按钮,出现如图1.2.2B对话框。在项目类型(P)编辑框中选择Visual C#项目,在模板(T)编辑框中选择控制台应用程序,在名称(N)编辑框中键入e1,在位置(L)编辑框中键入D:\csarp,必须预先创建文件夹D:\csarp。也可以单击浏览按钮,在打开文件对话框中选择文件夹。单击确定按钮,创建项目。出现如图1.2.2C界面。编写一个应用程序,可能包含多个文件,才能生成可执行文件,所有这些文件的集合叫做一个项目。
(3) 修改class1.cs文件如下,有阴影部分是新增加的语句,其余是集成环境自动生成的。
using System;
namespace e1
{
/// <summary>
/// Class1 的摘要说明。
/// </summary>
class Class1
{
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main(string[] args)
{
//
// TODO: 在此处添加代码以启动应用程序
//
Console.WriteLine("请键入你的姓名:");
Console.ReadLine();
Console.WriteLine("欢迎!");
}
}
}
(4) 按CTRL+F5键,运行程序,如右图,和1.2.1节运行效果相同。屏幕上出现一行字符,提示您输入姓名:请键入你的姓名:输入任意字符并按下回车键,屏幕将打印出欢迎信息:欢迎!输入回车退出程序。
图1.2.2A
图1.2.2B
图1.2.2C(下载源码就到源码网:www.codepub.com)
1.3 类的基本概念
C#语言是一种现代、面向对象的语言。面向对象程序设计方法提出了一个全新的概念:类,它的主要思想是将数据(数据成员)及处理这些数据的相应方法(函数成员)封装到类中,类的实例则称为对象。这就是我们常说的封装性。
1.3.1 类的基本概念
类可以认为是对结构的扩充,它和C中的结构最大的不同是:类中不但可以包括数据,还包括处理这些数据的函数。类是对数据和处理数据的方法(函数)的封装。类是对某一类具有相同特性和行为的事物的描述。例如,定义一个描述个人情况的类Person如下:
using System;
class Person//类的定义,class是保留字,表示定义一个类,Person是类名
{
private string name="张三";//类的数据成员声明
private int age=12;//private表示私有数据成员
public void Display()//类的方法(函数)声明,显示姓名和年龄
{
Console.WriteLine("姓名:{0},年龄:{1}",name,age);
}
public void SetName(string PersonName)//修改姓名的方法(函数)
{
name=PersonName;
}
public void SetAge(int PersonAge)
{
age=PersonAge;
}
}
Console.WriteLine("姓名:{0},年龄:{1}",name,age)的意义是将第二个参数变量name变为字符串填到{0}位置,将第三个参数变量age变为字符串填到{1}位置,将第一个参数表示的字符串在显示器上输出。
大家注意,这里我们实际定义了一个新的数据类型,为用户自己定义的数据类型,是对个人的特性和行为的描述,他的类型名为Person,和int,char等一样为一种数据类型。用定义新数据类型Person类的方法把数据和处理数据的函数封装起来。类的声明格式如下:
属性 类修饰符 class 类名{类体}
其中,关键字class、类名和类体是必须的,其它项是可选项。类修饰符包括new、public、protected、internal、private、abstract和sealed,这些类修饰符以后介绍。类体用于定义类的成员。
1.3.2 类成员的存取控制
一般希望类中一些数据不被随意修改,只能按指定方法修改,既隐蔽一些数据。同样一些函数也不希望被其它类程序调用,只能在类内部使用。如何解决这个问题呢?可用访问权限控制字,常用的访问权限控制字如下:private(私有),public(公有)。在数据成员或函数成员前增加访问权限控制字,可以指定该数据成员或函数成员的访问权限。
私有数据成员只能被类内部的函数使用和修改,私有函数成员只能被类内部的其它函数调用。类的公有函数成员可以被类的外部程序调用,类的公有数据成员可以被类的外部程序直接使用修改。公有函数实际是一个类和外部通讯的接口,外部函数通过调用公有函数,按照预先设定好的方法修改类的私有成员。对于上述例子,name和age是私有数据成员,只能通过公有函数SetName()和SetAge()修改,既它们只能按指定方法修改。
这里再一次解释一下封装,它有两个意义,第一是把数据和处理数据的方法同时定义在类中。第二是用访问权限控制字使数据隐蔽。
1..3.3 类的对象
Person类仅是一个用户新定义的数据类型,由它可以生成Person类的实例,C#语言叫对象。用如下方法声明类的对象:Person OnePerson=new Person();此语句的意义是建立Person类对象,返回对象地址赋值给Person类变量OnePerson。也可以分两步创建Person类的对象:Person OnePerson;OnePerson=new Person();OnePerson虽然存储的是Person类对象地址,但不是C中的指针,不能象指针那样可以进行加减运算,也不能转换为其它类型地址,它是引用型变量,只能引用(代表)Person对象,具体意义参见以后章节。和C、C++不同,C#只能用此种方法生成类对象。
在程序中,可以用OnePerson.方法名或OnePerson.数据成员名访问对象的成员。例如:OnePerson.Display(),公用数据成员也可以这样访问。注意,C#语言中不包括C++语言中的->符号。
1.3.4 类的构造函数和析构函数
在建立类的对象时,需做一些初始化工作,例如对数据成员初始化。这些可以用构造函数来完成。每当用new生成类的对象时,自动调用类的构造函数。因此,可以把初始化的工作放到构造函数中完成。构造函数和类名相同,没有返回值。例如可以定义Person类的构造函数如下:
public Person(string Name,int Age)//类的构造函数,函数名和类同名,无返回值。
{
name=Name;
age=Age;
}
当用Person OnePerson=new Person(“张五”,20)语句生成Person类对象时,将自动调用以上构造函数。请注意如何把参数传递给构造函数。
变量和类的对象都有生命周期,生命周期结束,这些变量和对象就要被撤销。类的对象被撤销时,将自动调用析构函数。一些善后工作可放在析构函数中完成。析构函数的名字为~类名,无返回类型,也无参数。Person类的析构函数为~ Person()。C#中类析构函数不能显示地被调用,它是被垃圾收集器撤销不被使用的对象时自动调用的。
1.3.5 类的构造函数的重载
在C#语言中,同一个类中的函数,如果函数名相同,而参数类型或个数不同,认为是不同的函数,这叫函数重载。仅返回值不同,不能看作不同的函数。这样,可以在类定义中,定义多个构造函数,名字相同,参数类型或个数不同。根据生成类的对象方法不同,调用不同的构造函数。例如可以定义Person类没有参数的构造函数如下:
public Person()//类的构造函数,函数名和类同名,无返回值。
{
name="张三";
age=12;
}
用语句Person OnePerson=new Person("李四",30)生成对象时,将调用有参数的构造函数,而用语句Person OnePerson=new Person()生成对象时,调用无参数的构造函数。由于析构函数无参数,因此,析构函数不能重载。
1.3.6 使用Person类的完整的例子
下边用一个完整的例子说明Person类的使用:(VisualStudio.Net编译通过)
using System;
namespace e1//定义以下代码所属命名空间,意义见以后章节
{
class Person
{
private String name="张三";//类的数据成员声明
private int age=12;
public void Display()//类的方法(函数)声明,显示姓名和年龄
{
Console.WriteLine("姓名:{0},年龄:{1}",name,age);
}
public void SetName(string PersonName)//指定修改姓名的方法(函数)
{
name=PersonName;
}
public void SetAge(int PersonAge)//指定修改年龄的方法(函数)
{
age=PersonAge;
}
public Person(string Name,int Age)//构造函数,函数名和类同名,无返回值
{
name=Name;
age=Age;
}
public Person()//类的构造函数重载
{
name="田七";
age=12;
}
}
class Class1
{
static void Main(string[] args)
{
Person OnePerson=new Person("李四",30);//生成类的对象
OnePerson.Display();
//下句错误,在其它类(Class1类)中,不能直接修改Person类中的私有成员。
//OnePerson.name="王五";
//只能通过Person类中公有方法SetName修改Person类中的私有成员name。
OnePerson.SetName("王五");
OnePerson.SetAge(40);
OnePerson.Display();
OnePerson=new Person();
OnePerson.Display();
}
}
}
键入CTRL+F5运行后,显示的效果是:
姓名: 李四,年龄:30
姓名: 王五,年龄:40
姓名: 田七,年龄:12
1.4 C#的数据类型
从大的方面来分,C#语言的数据类型可以分为三种:值类型,引用类型,指针类型,指针类型仅用于非安全代码中。本节重点讨论值类型和引用类型。
1.4.1 值类型和引用类型区别
在C#语言中,值类型变量存储的是数据类型所代表的实际数据,值类型变量的值(或实例)存储在栈(Stack)中,赋值语句是传递变量的值。引用类型(例如类就是引用类型)的实例,也叫对象,不存在栈中,而存储在可管理堆(Managed Heap)中,堆实际上是计算机系统中的空闲内存。引用类型变量的值存储在栈(Stack)中,但存储的不是引用类型对象,而是存储引用类型对象的引用,即地址,和指针所代表的地址不同,引用所代表的地址不能被修改,也不能转换为其它类型地址,它是引用型变量,只能引用指定类对象,引用类型变量赋值语句是传递对象的地址。见下例:
using System;
class MyClass//类为引用类型
{
public int a=0;
}
class Test
{
static void Main()
{
f1();
}
static public void f1()
{
int v1=1;//值类型变量v1,其值1存储在栈(Stack)中
int v2=v1;//将v1的值(为1)传递给v2,v2=1,v1值不变。
v2=2;//v2=2,v1值不变。
MyClass r1=new MyClass();//引用变量r1存储MyClass类对象的地址
MyClass r2=r1;//r1和r2都代表是同一个MyClass类对象
r2.a=2;//和语句r1.a=2等价
}
}
存储在栈中的变量,当其生命周期结束,自动被撤销,例如,v1存储在栈中,v1和函数f1同生命周期,退出函数f1,v1不存在了。但在堆中的对象不能自动被撤销。因此C和C++语言,在堆中建立的对象,不使用时必须用语句释放对象占用的存储空间。.NET系统CLR内建垃圾收集器,当对象的引用变量被撤销,表示对象的生命周期结束,垃圾收集器负责收回不被使用的对象占用的存储空间。例如,上例中引用变量r1及r2是MyClass类对象的引用,存储在栈中,退出函数f1,r1和r2都不存在了,在堆中的MyClass类对象也就被垃圾收集器撤销。也就是说,CLR具有自动内存管理功能。
1.4.2 值类型变量分类
C#语言值类型可以分为以下几种:
? 简单类型(Simple types)
简单类型中包括:数值类型和布尔类型(bool)。数值类型又细分为:整数类型、字符类型(char)、浮点数类型和十进制类型(decimal)。
? 结构类型(Struct types)
? 枚举类型(Enumeration types)
C#语言值类型变量无论如何定义,总是值类型变量,不会变为引用类型变量。
1.4.3 结构类型
结构类型和类一样,可以声明构造函数、数据成员、方法、属性等。结构和类的最根本的区别是结构是值类型,类是引用类型。和类不同,结构不能从另外一个结构或者类派生,本身也不能被继承,因此不能定义抽象结构,结构成员也不能被访问权限控制字protected修饰,也不能用virtual和abstract修饰结构方法。在结构中不能定义析构函数。虽然结构不能从类和结构派生,可是结构能够继承接口,结构继承接口的方法和类继承接口的方法基本一致。下面例子定义一个点结构point:
using System;
struct point//结构定义
{
public int x,y;//结构中也可以声明构造函数和方法,变量不能赋初值
}
class Test
{
static void Main()
{
point P1;
P1.x=166;
P1.y=111;
point P2;
P2=P1;//值传递,使P2.x=166,P2.y=111
point P3=new point();//用new生成结构变量P3,P3仍为值类型变量
}//用new生成结构变量P3仅表示调用默认构造函数,使x=y==0。
}
1.4.4 简单类型
简单类型也是结构类型,因此有构造函数、数据成员、方法、属性等,因此下列语句int i=int.MaxValue;string s=i.ToString()是正确的。即使一个常量,C#也会生成结构类型的实例,因此也可以使用结构类型的方法,例如:string s=13.ToString()是正确的。简单类型包括:整数类型、字符类型、布尔类型、浮点数类型、十进制类型。见下表:
保留字 System命名空间中的名字 字节数 取值范围
sbyte System.Sbyte 1 -128~127
byte System.Byte 1 0~255
short System.Int16 2 -32768~32767
ushort System.UInt16 2 0~65535
int System.Int32 4 -2147483648~2147483647
uint System.UInt32 4 0~4292967295
long System.Int64 8 -9223372036854775808~9223372036854775808
ulong System.UInt64 8 0~18446744073709551615
char System.Char 2 0~65535
float System.Single 4 3.4E-38~3.4E+38
double System.Double 8 1.7E-308~1.7E+308
bool System.Boolean (true,false)
decimal System.Decimal 16 正负?1.0???????到7.9?????之间
C#简单类型使用方法和C、C++中相应的数据类型基本一致。需要注意的是:
? 和C语言不同,无论在何种系统中,C#每种数据类型所占字节数是一定的。
? 字符类型采用Unicode字符集,一个Unicode标准字符长度为16位。
? 整数类型不能隐式被转换为字符类型(char),例如char c1=10是错误的,必须写成:char c1=(char)10,char c=‘A‘,char c=‘\x0032‘;char c=‘\u0032‘。
? 布尔类型有两个值:false,true。不能认为整数0是false,其它值是true。bool x=1是错误的,不存在这种写法,只能写成x=true 或x=false。
? 十进制类型(decimal)也是浮点数类型,只是精度比较高,一般用于财政金融计算。
1.4.5 枚举类型
C#枚举类型使用方法和C、C++中的枚举类型基本一致。见下例:
using System;
class Class1
{ enum Days {Sat=1, Sun, Mon, Tue, Wed, Thu, Fri};
//使用Visual Studio.Net,enum语句添加在[STAThread]前边
static void Main(string[] args)
{
Days day=Days.Tue;
int x=(int)Days.Tue;//x=2
Console.WriteLine("day={0},x={1}",day,x);//显示结果为:day=Tue,x=4
}
}
在此枚举类型Days中,每个元素的默认类型为int,其中Sun=0,Mon=1,Tue=2,依此类推。也可以直接给枚举元素赋值。例如:
enum Days{Sat=1,Sun,Mon,Tue,Wed,Thu,Fri,Sat};
在此枚举中,Sun=1,Mon=2,Tue=3,Wed=4,等等。和C、C++中不同,C#枚举元素类型可以是byte、sbyte、short、ushort、int、uint、long和ulong类型,但不能是char类型。见下例:
enum Days:byte{Sun,Mon,Tue,Wed,Thu,Fri,Sat};//元素为字节类型
1.4.6 值类型的初值和默认构造函数
所有变量都要求必须有初值,如没有赋值,采用默认值。对于简单类型,sbyte、byte、short、ushort、int、uint、long和ulong默认值为0,char类型默认值是(char)0,float为0.0f,double为0.0d,decimal为0.0m,bool为false,枚举类型为0,在结构类型和类中,数据成员的数值类型变量设置为默认值,引用类型变量设置为null。
可以显示的赋值,例如int i=0。而对于复杂结构类型,其中的每个数据成员都按此种方法赋值,显得过于麻烦。由于数值类型都是结构类型,可用new语句调用其构造函数初始化数值类型变量,例如:int j=new int()。请注意,用new语句并不是把int变量变为引用变量,j仍是值类型变量,这里new仅仅是调用其构造函数。所有的数值类型都有默认的无参数的构造函数,其功能就是为该数值类型赋初值为默认值。对于自定义结构类型,由于已有默认的无参数的构造函数,不能再定义无参数的构造函数,但可以定义有参数的构造函数。
1.4.7 引用类型分类
C#语言中引用类型可以分为以下几种:
? 类:C#语言中预定义了一些类:对象类(object类)、数组类、字符串类等。当然,程序员可以定义其它类。
? 接口。
? 代表。
C#语言引用类型变量无论如何定义,总是引用类型变量,不会变为值类型变量。C#语言引用类型对象一般用运算符new建立,用引用类型变量引用该对象。本节仅介绍对象类型(object类型)、字符串类型、数组。其它类型在其它节中介绍。
1.4.8 对象类(object类)
C#中的所有类型(包括数值类型)都直接或间接地以object类为基类。对象类(object类)是所有其它类的基类。任何一个类定义,如果不指定基类,默认object为基类。继承和基类的概念见以后章节。C#语言规定,基类的引用变量可以引用派生类的对象(注意,派生类的引用变量不可以引用基类的对象),因此,对一个object的变量可以赋予任何类型的值:
int x =25;
object obj1;
obj1=x;
object obj2= ‘A‘;
object关键字是在命名空间System中定义的,是类System.Object的别名。
1.4.9 数组类
在进行批量处理数据的时候,要用到数组。数组是一组类型相同的有序数据。数组按照数组名、数据元素的类型和维数来进行描述。C#语言中数组是类System.Array类对象,比如声明一个整型数数组:int[] arr=new int[5];实际上生成了一个数组类对象,arr是这个对象的引用(地址)。
在C#中数组可以是一维的也可以是多维的,同样也支持数组的数组,即数组的元素还是数组。一维数组最为普遍,用的也最多。我们先看一个一维数组的例子:
using System;
class Test
{ static void Main()
{ int[] arr=new int[3];//用new运算符建立一个3个元素的一维数组
for(int i=0;i<arr.Length;i++)//arr.Length是数组类变量,表示数组元素个数
arr[i]=i*i;//数组元素赋初值,arr[i]表示第i个元素的值
for (int i=0;i<arr.Length;i++)//数组第一个元素的下标为0
Console.WriteLine("arr[{0}]={1}",i,arr[i]);
}
}
这个程序创建了一个int类型3个元素的一维数组,初始化后逐项输出。其中arr.Length表示数组元素的个数。注意数组定义不能写为C语言格式:int arr[]。程序的输出为:
arr[0] = 0
arr[1] = 1
arr[2] = 4
上面的例子中使用的是一维数组,下面介绍多维数组:
string[] a1;//一维string数组类引用变量a1
string[,] a2;//二维string数组类引用变量a2
a2=new string[2,3];
a2[1,2]="abc";
string[,,] a3;//三维string数组类引用变量a3
string[][] j2;//数组的数组,即数组的元素还是数组
string[][][][] j3;
在数组声明的时候,可以对数组元素进行赋值。看下面的例子:
int[] a1=new int[]{1,2,3};//一维数组,有3个元素。
int[] a2=new int[3]{1,2,3};//此格式也正确
int[] a3={1,2,3};//相当于int[] a3=new int[]{1,2,3};
int[,] a4=new int[,]{{1,2,3},{4,5,6}};//二维数组,a4[1,1]=5
int[][] j2=new int[3][];//定义数组j2,有三个元素,每个元素都是一个数组
j2[0]=new int[]{1,2,3};//定义第一个元素,是一个数组
j2[1]=new int[]{1, 2, 3, 4, 5, 6};//每个元素的数组可以不等长
j2[2]=new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9};
1.4.10 字符串类(string类)
C#还定义了一个基本的类string,专门用于对字符串的操作。这个类也是在名字空间System中定义的,是类System.String的别名。字符串应用非常广泛,在string类的定义中封装了许多方法,下面的一些语句展示了string类的一些典型用法:
? 字符串定义
string s;//定义一个字符串引用类型变量s
s="Zhang";//字符串引用类型变量s指向字符串"Zhang"
string FirstName="Ming";
string LastName="Zhang";
string Name=FirstName+" "+LastName;//运算符+已被重载
string SameName=Name;
char[] s2={‘计‘,‘算‘,‘机‘,‘科‘,‘学‘};
string s3=new String(s2);
? 字符串搜索
string s="ABC科学";
int i=s.IndexOf("科");
搜索"科"在字符串中的位置,因第一个字符索引为0,所以"A"索引为0,"科"索引为3,因此这里i=3,如没有此字符串i=-1。注意C#中,ASCII和汉字都用2字节表示。
? 字符串比较函数
string s1="abc";
string s2="abc";
int n=string.Compare(s1,s2);//n=0
n=0表示两个字符串相同,n小于零,s1<s2,n大于零,s1>s2。此方法区分大小写。也可用如下办法比较字符串:
string s1="abc";
string s="abc";
string s2="不相同";
if(s==s1)//还可用!=。虽然String是引用类型,但这里比较两个字符串的值
s2="相同";
? 判断是否为空字符串
string s="";
string s1="不空";
if(s.Length==0)
s1="空";
? 得到子字符串或字符
string s="取子字符串";
string sb=s.Substring(2,2);//从索引为2开始取2个字符,Sb="字符",s内容不变
char sb1=s[0];//sb1=‘取‘
Console.WriteLine(sb1);//显示:取
? 字符串删除函数
string s="取子字符串";
string sb=s.Remove(0,2);//从索引为0开始删除2个字符,Sb="字符串",s内容不变
? 插入字符串
string s="计算机科学";
string s1=s.Insert(3,"软件");//s1="计算机软件科学",s内容不变
? 字符串替换函数
string s="计算机科学";
string s1=s.Replace("计算机","软件");//s1="软件科学",s内容不变
? 把String转换为字符数组
string S="计算机科学";
char[] s2=S.ToCharArray(0,S.Length);//属性Length为字符类对象的长度
? 其它数据类型转换为字符串
int i=9;
string s8=i.ToString();//s8="9"
float n=1.9f;
string s9=n.ToString();//s8="1.9"
其它数据类型都可用此方法转换为字符类对象
? 大小写转换
string s="AaBbCc";
string s1=s.ToLower();//把字符转换为小写,s内容不变
string s2=s.ToUpper();//把字符转换为大写,s内容不变
? 删除所有的空格
string s="A bc ";
s.Trim();//删除所有的空格
string类其它方法的使用请用帮助系统查看,方法是打开Visual Studio.Net的代码编辑器,键入string,将光标移到键入的字符串string上,然后按F1键。
1.4.11 类型转换
在编写C#语言程序中,经常会碰到类型转换问题。例如整型数和浮点数相加,C#会进行隐式转换。详细记住那些类型数据可以转换为其它类型数据,是不可能的,也是不必要的。程序员应记住类型转换的一些基本原则,编译器在转换发生问题时,会给出提示。C#语言中类型转换分为:隐式转换、显示转换、加框(boxing)和消框(unboxing)等三种。
一. 隐式转换
隐式转换就是系统默认的、不需要加以声明就可以进行的转换。例如从int类型转换到long类型就是一种隐式转换。在隐式转换过程中,转换一般不会失败,转换过程中也不会导致信息丢失。例如:
int i=10;
long l=i;
二. 显示转换
显式类型转换,又叫强制类型转换。与隐式转换正好相反,显式转换需要明确地指定转换类型,显示转换可能导致信息丢失。下面的例子把长整形变量显式转换为整型:
long l=5000;
int i=(int)l;//如果超过int取值范围,将产生异常
三. 加框(boxing)和消框(unboxing)
加框(boxing)和消框(unboxing)是C#语言类型系统提出的核心概念,加框是值类型转换为object(对象)类型,消框是object(对象)类型转换为值类型。有了加框和消框的概念,对任何类型的变量来说最终我们都可以看作是object类型。
1 加框操作
把一个值类型变量加框也就是创建一个object对象,并将这个值类型变量的值复制给这个object对象。例如:
int i=10;
object obj=i;//隐式加框操作,obj为创建的object对象的引用。
我们也可以用显式的方法来进行加框操作,例如:
int i =10;
object obj=object(i);//显式加框操作
值类型的值加框后,值类型变量的值不变,仅将这个值类型变量的值复制给这个object对象。我们看一下下面的程序:
using System
class Test
{ public static void Main()
{ int n=200;
object o=n;
o=201;//不能改变n
Console.WriteLine("{0},{1}",n,o);
}
}
输出结果为:200,201。这就证明了值类型变量n和object类对象o都独立存在着。
2. 消框操作
和加框操作正好相反,消框操作是指将一个对象类型显式地转换成一个值类型。消框的过程分为两步:首先检查这个object对象,看它是否为给定的值类型的加框值,如是,把这个对象的值拷贝给值类型的变量。我们举个例子来看看一个对象消框的过程:
int i=10;
object obj=i;
int j=(int)obj;//消框操作
可以看出消框过程正好是加框过程的逆过程,必须注意加框操作和消框操作必须遵循类型兼容的原则。
3. 加框和消框的使用
定义如下函数:
void Display(Object o)//注意,o为Object类型
{ int x=(int)o;//消框
System.Console.WriteLine("{0},{1}",x,o);
}
调用此函数:int y=20;Display(y);在此利用了加框概念,虚参被实参替换:Object o=y,也就是说,函数的参数是Object类型,可以将任意类型实参传递给函数。
1.5 运算符
C#语言和C语言的运算符用法基本一致。以下重点讲解二者之间不一致部分。
1.5.1 运算符分类
与C语言一样,如果按照运算符所作用的操作数个数来分,C#语言的运算符可以分为以下几种类型:
? 一元运算符:一元运算符作用于一个操作数,例如:-X、++X、X--等。
? 二元运算符:二元运算符对两个操作数进行运算,例如:x+y。
? 三元运算符:三元运算符只有一个:x? y:z。
C#语言运算符的详细分类及操作符从高到低的优先级顺序见下表。
类别 操作符
初级操作符 (x) x.y f(x) a[x] x++ x-- new type of sizeof checked unchecked
一元操作符 + - ! ~ ++x –x (T)x
乘除操作符 * / %
加减操作符 + -
移位操作符 << >>
关系操作符 < > <= >= is as
等式操作符 == !=
逻辑与操作符 &
逻辑异或操作符 ^
逻辑或操作符 |
条件与操作符 &&
条件或操作符 ||
条件操作符 ?:
赋值操作符 = *= /= %= += -= <<= >>= &= ^= |=
1.5.2 测试运算符is
is操作符用于动态地检查表达式是否为指定类型。使用格式为:e is T,其中e是一个表达式,T是一个类型,该式判断e是否为T类型,返回值是一个布尔值。例子:
using System;
class Test
{ public static void Main()
{ Console.WriteLine(1 is int);
Console.WriteLine(1 is float);
Console.WriteLine(1.0f is float);
Console.WriteLine(1.0d is double);
}
}
输出为:
True
False
True
True
1.5.3 typeof运算符
typeof操作符用于获得指定类型在system名字空间中定义的类型名字,例如:
using System;
class Test
{
static void Main()
{
Console.WriteLine(typeof(int));
Console.WriteLine(typeof(System.Int32));
Console.WriteLine(typeof(string));
Console.WriteLine(typeof(double[]));
}
}
产生如下输出,由输出可知int和System.int32是同一类型。
System.Int32
System.Int32
System.String
System.Double[]
1.5.4 溢出检查操作符checked和unchecked
在进行整型算术运算(如+、-、*、/等)或从一种整型显式转换到另一种整型时,有可能出现运算结果超出这个结果所属类型值域的情况,这种情况称之为溢出。整型算术运算表达式可以用checked或unchecked溢出检查操作符,决定在编译和运行时是否对表达式溢出进行检查。如果表达式不使用溢出检查操作符或使用了checked操作符,常量表达式溢出,在编译时将产生错误,表达式中包含变量,程序运行时执行该表达式产生溢出,将产生异常提示信息。而使用了unchecked操作符的表达式语句,即使表达式产生溢出,编译和运行时都不会产生错误提示。但这往往会出现一些不可预期的结果,所以使用unchecked操作符要小心。下面的例子说明了checked和unchecked操作符的用法:
using System;
class Class1
{ static void Main(string[] args)
{ const int x=int.MaxValue;
unchecked//不检查溢出
{ int z=x*2;//编译时不产生编译错误,z=-2
Console.WriteLine("z={0}",z);//显示-2
}
checked//检查溢出
{ int z1=(x*2);//编译时会产生编译错误
Console.WriteLine("z={0}",z1);
}
}
}
1.5.5 new运算符
new操作符可以创建值类型变量、引用类型对象,同时自动调用构造函数。例如:
int x=new int();//用new创建整型变量x,调用默认构造函数
Person C1=new Person ();//用new建立的Person类对象。Person 变量C1对象的引用
int[] arr=new int[2];//数组也是类,创建数组类对象,arr是数组对象的引用
需注意的是,int x=new int()语句将自动调用int结构不带参数的构造函数,给x赋初值0,x仍是值类型变量,不会变为引用类型变量。
1.5.6 运算符的优先级
当一个表达式包含多种操作符时,操作符的优先级控制着操作符求值的顺序。例如,表达式x+y*z按照x+(y*z)顺序求值,因为*操作符比+操作符有更高的优先级。这和数学运算中的先乘除后加减是一致的。1.5.1节中的表总结了所有操作符从高到低的优先级顺序。
当两个有相同优先级的操作符对操作数进行运算时,例如x+y-z,操作符按照出现的顺序由左至右执行,x+y-z按(x+y)-z进行求值。赋值操作符按照右接合的原则,即操作按照从右向左的顺序执行。如x=y=z按照x=(y=z)进行求值。建议在写表达式的时候,如果无法确定操作符的实际顺序,则尽量采用括号来保证运算的顺序,这样也使得程序一目了然,而且自己在编程时能够思路清晰。
1.6 程序控制语句
C#语言控制语句和C基本相同,使用方法基本一致。C#语言控制语句包括:if语句、swith语句、while语句、do…while语句、for语句、foreach语句、break语句、continue语句、goto语句、return语句、异常处理语句等,其中foreach语句和异常语句是C#语言新增加控制语句。本节首先介绍一下这些语句和C语言的不同点,然后介绍C#语言新增的控制语句。
1.6.1 和C语言的不同点
? 与C不同,if语句、while语句、do…while语句、for语句中的判断语句,一定要用布尔表达式,不能认为0为false,其它数为true。
? switch语句不再支持遍历,C和C++语言允许switch语句中case标签后不出现break语句,但C#不允许这样,它要求每个case标签项后使用break语句或goto跳转语句,即不允许从一个case自动遍历到其它case,否则编译时将报错。switch语句的控制类型,即其中控制表达式的数据类型可以是sbyte、byte、short、ushort、uint、long、ulong、char、string或枚举类型。每个case标签中的常量表达式必须属于或能隐式转换成控制类型。如果有两个或两个以上case标签中的常量表达式值相同,编译时将会报错。执行switch语句,首先计算switch表达式,然后与case后的常量表达式的值进行比较,执行第一个与之匹配的case分支下的语句。如果没有case常量表达式的值与之匹配,则执行dafault分支下的语句,如果没有dafault语句,则退出switch语句。switch语句中可以没有dafault语句,但最多只能有一个dafault语句。见下例:
using System;
class class1
{ static void Main()
{ System.Console.WriteLine("请输入要计算天数的月份");
string s=System.Console.ReadLine();
string s1="";
switch(s)
{ case "1": case "3": case "5":
case "7": case "8": case "10":
case "12"://共用一条语句
s1="31";break;
case "2":
s1="28";break;
case "4": case "6": case "9":
goto case "11";//goto语句仅为说明问题,无此必要
case "11":
s1="30";break;
default:
s1="输入错误";break;
}
System.Console.WriteLine(s1);
}
}
1.6.2 foreach语句
foreach语句是C#语言新引入的语句,C和C++中没有这个语句,它借用Visual Basic中的foreach语句。语句的格式为:
foreach(类型 变量名 in 表达式) 循环语句
其中表达式必须是一个数组或其它集合类型,每一次循环从数组或其它集合中逐一取出数据,赋值给指定类型的变量,该变量可以在循环语句中使用、处理,但不允许修改变量,该变量的指定类型必须和表达式所代表的数组或其它集合中的数据类型一致。例子:
using System;
class Test()
{ public static void Main()
{ int[] list={10,20,30,40};//数组
foreach(int m in list)
Console.WriteLine("{0}",m);
}
}
对于一维数组,foreach语句循环顺序是从下标为0的元素开始一直到数组的最后一个元素。对于多维数组,元素下标的递增是从最右边那一维开始的。同样break和continue可以出现在foreach语句中,功能不变。
1.6.3 异常语句
在编写程序时,不仅要关心程序的正常操作,还应该考虑到程序运行时可能发生的各类不可预期的事件,比如用户输入错误、内存不够、磁盘出错、网络资源不可用、数据库无法使用等,所有这些错误被称作异常,不能因为这些异常使程序运行产生问题。各种程序设计语言经常采用异常处理语句来解决这类异常问题。
C#提供了一种处理系统级错误和应用程序级错误的结构化的、统一的、类型安全的方法。C#异常语句包含try子句、catch子句和finally子句。try子句中包含可能产生异常的语句,该子句自动捕捉执行这些语句过程中发生的异常。catch子句中包含了对不同异常的处理代码,可以包含多个catch子句,每个catch子句中包含了一个异常类型,这个异常类型必须是System.Exception类或它的派生类引用变量,该语句只扑捉该类型的异常。可以有一个通用异常类型的catch子句,该catch子句一般在事先不能确定会发生什么样的异常的情况下使用,也就是可以扑捉任意类型的异常。一个异常语句中只能有一个通用异常类型的catch子句,而且如果有的话,该catch子句必须排在其它catch子句的后面。无论是否产生异常,子句finally一定被执行,在finally子句中可以增加一些必须执行的语句。
异常语句捕捉和处理异常的机理是:当try子句中的代码产生异常时,按照catch子句的顺序查找异常类型。如果找到,执行该catch子句中的异常处理语句。如果没有找到,执行通用异常类型的catch子句中的异常处理语句。由于异常的处理是按照catch子句出现的顺序逐一检查catch子句,因此catch子句出现的顺序是很重要的。无论是否产生异常,一定执行finally子句中的语句。异常语句中不必一定包含所有三个子句,因此异常语句可以有以下三种可能的形式:
? try –catch语句,可以有多个catch语句
? try -finally语句
? try -catch-finally语句,可以有多个catch语句
请看下边的例子:
1. try–catch-finally语句
using System
using System.IO//使用文件必须引用的名字空间
public class Example
{ public static void Main()
{ StreamReader sr=null;//必须赋初值null,否则编译不能通过
try
{ sr=File.OpenText("d:\\csarp\\test.txt");//可能产生异常
string s;
while(sr.Peek()!=-1)
{ s=sr.ReadLine();//可能产生异常
Console.WriteLine(s);
}
}
catch(DirectoryNotFoundException e)//无指定目录异常
{ Console.WriteLine(e.Message);
}
catch(FileNotFoundException e)//无指定文件异常
{ Console.WriteLine("文件"+e.FileName+"未被发现");
}
catch(Exception e)//其它所有异常
{ Console.WriteLine("处理失败:{0}",e.Message);
}
finally
{ if(sr!=null)
sr.Close();
}
}
}
2. try -finally语句
上例中,其实可以不用catch语句,在finally子句中把文件关闭,提示用户是否正确打开了文件,请读者自己完成。
3. try -catch语句
请读者把上例修改为使用try-catch结构,注意在每个catch语句中都要关闭文件。
1.7 类的继承
在1.3节,定义了一个描述个人情况的类Person,如果我们需要定义一个雇员类,当然可以从头开始定义雇员类Employee。但这样不能利用Person类中已定义的函数和数据。比较好的方法是,以Person类为基类,派生出一个雇员类Employee,雇员类Employee继承了Person类的数据成员和函数成员,既Person类的数据成员和函数成员成为Employee类的成员。这个Employee类叫以Person类为基类的派生类,这是C#给我们提出的方法。C#用继承的方法,实现代码的重用。
1.7.1 派生类的声明格式
派生类的声明格式如下:
属性 类修饰符 class 派生类名:基类名 {类体}
雇员类Employee定义如下:
class Employee:Person//Person类是基类
{ private string department;//部门,新增数据成员
private decimal salary;//薪金,新增数据成员
public Employee(string Name,int Age,string D,decimal S):base(Name,Age)
{//注意base的第一种用法,根据参数调用指定基类构造函数,注意参数的传递
department=D;
salary=S;
}
public new void Display()//覆盖基类Display()方法,注意new,不可用override
{ base.Display();//访问基类被覆盖的方法,base的第二种用法
Console.WriteLine("部门:{0} 薪金:{1}",department,salary);
}
}
修改主函数如下:
class Class1
{ static void Main(string[] args)
{ Employee OneEmployee=new Employee("李四",30,"计算机系",2000);
OneEmployee.Display();
}
}
Employee类继承了基类Person的方法SetName()、SetAge(),数据成员name和age,即认为基类Person的这些成员也是Employee类的成员,但不能继承构造函数和析构函数。添加了新的数据成员department和salary。覆盖了方法Display()。请注意,虽然Employee类继承了基类Person的name和age,但由于它们是基类的私有成员,Employee类中新增或覆盖的方法不能直接修改name和age,只能通过基类原有的公有方法SetName()和SetAge()修改。如果希望在Employee类中能直接修改name和age,必须在基类中修改它们的属性为protected。
1.7.2 base 关键字
base关键字用于从派生类中访问基类成员,它有两种基本用法:
? 在定义派生类的构造函数中,指明要调用的基类构造函数,由于基类可能有多个构造函数,根据base后的参数类型和个数,指明要调用哪一个基类构造函数。参见上节雇员类Employee构造函数定义中的base的第一种用法。
? 在派生类的方法中调用基类中被派生类覆盖的方法。参见上节雇员类Employee的Display()方法定义中的base的第二种用法。
1.7.3 覆盖基类成员
在派生类中,通过声明与基类完全相同新成员,可以覆盖基类的同名成员,完全相同是指函数类型、函数名、参数类型和个数都相同。如上例中的方法Display()。派生类覆盖基类成员不算错误,但会导致编译器发出警告。如果增加new修饰符,表示认可覆盖,编译器不再发出警告。请注意,覆盖基类的同名成员,并不是移走基类成员,只是必须用如下格式访问基类中被派生类覆盖的方法:base.Display()。
1.7.4 C#语言类继承特点
C#语言类继承有如下特点:
? C#语言只允许单继承,即派生类只能有一个基类。
? C#语言继承是可以传递的,如果C从B派生,B从A派生,那么C不但继承B的成员,还要继承A中的成员。
? 派生类可以添加新成员,但不能删除基类中的成员。
? 派生类不能继承基类的构造函数、析构函数和事件。但能继承基类的属性。
? 派生类可以覆盖基类的同名成员,如果在派生类中覆盖了基类同名成员,基类该成员在派生类中就不能被直接访问,只能通过base.基类方法名访问。
? 派生类对象也是其基类的对象,但基类对象却不是其派生类的对象。例如,前边定义的雇员类Employee是Person类的派生类,所有雇员都是人类,但很多人并不是雇员,可能是学生,自由职业者,儿童等。因此C#语言规定,基类的引用变量可以引用其派生类对象,但派生类的引用变量不可以引用其基类对象。
1.8 类的成员
由于C#程序中每个变量或函数都必须属于一个类或结构,不能象C或C++那样建立全局变量,因此所有的变量或函数都是类或结构的成员。类的成员可以分为两大类:类本身所声明的以及从基类中继承来的。
1.8.1 类的成员类型
类的成员包括以下类型:
? 局部变量:在for、switch等语句中和类方法中定义的变量,只在指定范围内有效。
? 字段:即类中的变量或常量,包括静态字段、实例字段、常量和只读字段。
? 方法成员:包括静态方法和实例方法。
? 属性:按属性指定的get方法和Set方法对字段进行读写。属性本质上是方法。
? 事件:代表事件本身,同时联系事件和事件处理函数。
? 索引指示器:允许象使用数组那样访问类中的数据成员。
? 操作符重载:采用重载操作符的方法定义类中特有的操作。
? 构造函数和析构函数。
包含有可执行代码的成员被认为是类中的函数成员,这些函数成员有方法、属性、索引指示器、操作符重载、构造函数和析构函数。
1.8.2 类成员访问修饰符
访问修饰符用于指定类成员的可访问性,C#访问修饰符有private、protected、public和internal4种。Private声明私有成员,私有数据成员只能被类内部的函数使用和修改,私有函数成员只能被类内部的函数调用。派生类虽然继承了基类私有成员,但不能直接访问它们,只能通过基类的公有成员访问。protected声明保护成员,保护数据成员只能被类内部和派生类的函数使用和修改,保护函数成员只能被类内部和派生类的函数调用。public声明公有成员,类的公用函数成员可以被类的外部程序所调用,类的公用数据成员可以被类的外部程序直接使用。公有函数实际是一个类和外部通讯的接口,外部函数通过调用公有函数,按照预先设定好的方法修改类的私有成员和保护成员。internal声明内部成员,内部成员只能在同一程序集中的文件中才是可以访问的,一般是同一个应用(Application)或库(Library)。
1.9 类的字段和属性
一般把类或结构中定义的变量和常量叫字段。属性不是字段,本质上是定义修改字段的方法,由于属性和字段的紧密关系,把它们放到一起叙述。
1.9.1 静态字段、实例字段、常量和只读字段
用修饰符static声明的字段为静态字段。不管包含该静态字段的类生成多少个对象或根本无对象,该字段都只有一个实例,静态字段不能被撤销。必须采用如下方法引用静态字段:类名.静态字段名。如果类中定义的字段不使用修饰符static,该字段为实例字段,每创建该类的一个对象,在对象内创建一个该字段实例,创建它的对象被撤销,该字段对象也被撤销,实例字段采用如下方法引用:实例名.实例字段名。用const修饰符声明的字段为常量,常量只能在声明中初始化,以后不能再修改。用readonly修饰符声明的字段为只读字段,只读字段是特殊的实例字段,它只能在字段声明中或构造函数中重新赋值,在其它任何地方都不能改变只读字段的值。例子:
public class Test
{ public const int intMax=int.MaxValue;//常量,必须赋初值
public int x=0;//实例字段
public readonly int y=0;//只读字段
public static int cnt=0;//静态字段
public Test(int x1,int y1)//构造函数
{ //intMax=0;//错误,不能修改常量
x=x1;//在构造函数允许修改实例字段
y=y1;//在构造函数允许修改只读字段
cnt++;//每创建一个对象都调用构造函数,用此语句可以记录对象的个数
}
public void Modify(int x1,int y1)
{ //intMax=0;//错误,不能修改常量
x=x1;
cnt=y1;
//y=10;//不允许修改只读字段
}
}
class Class1
{ static void Main(string[] args)
{ Test T1=new Test(100,200);
T1.x=40;//引用实例字段采用方法:实例名.实例字段名
Test.cnt=0;//引用静态字段采用方法:类名.静态字段名
int z=T1.y;//引用只读字段
z=Test.intMax;//引用常量
}
}
1.9.2 属性
C#语言支持组件编程,组件也是类,组件用属性、方法、事件描述。属性不是字段,但必然和类中的某个或某些字段相联系,属性定义了得到和修改相联系的字段的方法。C#中的属性更充分地体现了对象的封装性:不直接操作类的数据内容,而是通过访问器进行访问,借助于get和set方法对属性的值进行读写。访问属性值的语法形式和访问一个变量基本一样,使访问属性就象访问变量一样方便,符合习惯。
在类的基本概念一节中,定义一个描述个人情况的类Person,其中字段name和age是私有字段,记录姓名和年龄,外部通过公有方法SetName和SetAge修改这两个私有字段。现在用属性来描述姓名和年龄。例子如下:
using System;
public class Person
{ private string P_name="张三";//P_name是私有字段
private int P_age=12;//P_age是私有字段
public void Display()//类的方法声明,显示姓名和年龄
{ Console.WriteLine("姓名:{0},年龄:{1}",P_name,P_age);
}
public string Name//定义属性Name
{ get
{ return P_name;}
set
{ P_name=value;}
}
public int Age//定义属性Age
{ get
{ return P_age;}
set
{ P_age=value;}
}
}
public class Test
{ public static void Main()
{ Person OnePerson= new Person();
OnePerson.Name="田七";//value="http://www.mamicode.com/田七",通过set方法修改变量P_Name
string s=OnePerson.Name;//通过get方法得到变量P_Name值
OnePerson.Age=20;//通过定义属性,既保证了姓名和年龄按指定方法修改
int x=OnePerson.Age;//语法形式和修改、得到一个变量基本一致,符合习惯
OnePerson.Display();
}
}
在属性的访问声明中,只有set访问器表明属性的值只能进行设置而不能读出,只有get访问器表明属性的值是只读的不能改写,同时具有set访问器和get访问器表明属性的值的读写都是允许的。
虽然属性和字段的语法比较类似,但由于属性本质上是方法,因此不能把属性当做变量那样使用,也不能把属性作为引用型参数或输出参数来进行传递。
1.10 类的方法
方法是类中用于执行计算或其它行为的成员。所有方法都必须定义在类或结构中。
1.10.1 方法的声明
方法的声明格式如下:
属性 方法修饰符 返回类型 方法名(形参列表){方法体}
方法修饰符包括new、public、protected、internal、private、static、virtual、sealed、override、abstract和extern。这些修饰符有些已经介绍过,其它修饰符将逐一介绍。返回类型可以是任何合法的C#数据类型,也可以是void,即无返回值。形参列表的格式为:(形参类型 形参1,形参类型 形参2,...),可以有多个形参。不能使用C语言的形参格式。
1.10.2 方法参数的种类
C#语言的方法可以使用如下四种参数(请注意和参数类型的区别):
? 值参数,不含任何修饰符。
? 引用参数,以ref修饰符声明。
? 输出参数,以out修饰符声明。
? 数组参数,以params修饰符声明。
1. 值参数
当用值参数向方法传递参数时,程序给实参的值做一份拷贝,并且将此拷贝传递给该方法,被调用的方法不会修改实参的值,所以使用值参数时,可以保证实参的值是安全的。如果参数类型是引用类型,例如是类的引用变量,则拷贝中存储的也是对象的引用,所以拷贝和实参引用同一个对象,通过这个拷贝,可以修改实参所引用的对象中的数据成员。
2. 引用参数
有时在方法中,需要修改或得到方法外部的变量值,C语言用向方法传递实参指针来达到目的,C#语言用引用参数。当用引用参数向方法传递实参时,程序将把实参的引用,即实参在内存中的地址传递给方法,方法通过实参的引用,修改或得到方法外部的变量值。引用参数以ref修饰符声明。注意在使用前,实参变量要求必须被设置初始值。
3. 输出参数
为了把方法的运算结果保存到外部变量,因此需要知道外部变量的引用(地址)。输出参数用于向方法传递外部变量引用(地址),所以输出参数也是引用参数,与引用参数的差别在于调用方法前无需对变量进行初始化。在方法返回后,传递的变量被认为经过了初始化。值参数、引用参数和输出参数的使用见下例:
using System;
class g{public int a=0;}//类定义
class Class1
{ public static void F1(ref char i)//引用参数
{ i=‘b‘;}
public static void F2(char i)//值参数,参数类型为值类型
{ i=‘d‘;}
public static void F3(out char i)//输出参数
{ i=‘e‘;}
public static void F4(string s)//值参数,参数类型为字符串
{ s="xyz";}
public static void F5(g gg)//值参数,参数类型为引用类型
{ gg.a=20;}
public static void F6(ref string s)//引用参数,参数类型为字符串
{ s="xyz";}
static void Main(string[] args)
{ char a=‘c‘;
string s1="abc";
F2(a);//值参数,不能修改外部的a
Console.WriteLine(a);//因a未被修改,显示c
F1(ref a);//引用参数,函数修改外部的a的值
Console.WriteLine(a);//a被修改为b,显示b
Char j;
F3(out j);//输出参数,结果输出到外部变量j
Console.WriteLine(j);//显示e
F4(s1);//值参数,参数类型是字符串,s1为字符串引用变量
Console.WriteLine(s1);//显示:abc,字符串s1不被修改
g g1=new g();
F5(g1);//值参数,但实参是一个类引用类型变量
Console.WriteLine(g1.a.ToString());//显示:20,修改对象数据
F6(ref s1);//引用参数,参数类型是字符串,s1为字符串引用变量
Console.WriteLine(s1);//显示:xyz,字符串s1被修改
}
}
4. 数组参数
数组参数使用params说明,如果形参表中包含了数组参数,那么它必须是参数表中最后一个参数,数组参数只允许是一维数组。比如string[]和string[][]类型都可以作为数组型参数。最后,数组型参数不能再有ref和out修饰符。见下例:
using System;
class Class1
{ static void F(params int[] args)//数组参数,有params说明
{ Console.Write("Array contains {0} elements:",args.Length);
foreach (int i in args)
Console.Write(" {0}",i);
Console.WriteLine();
}
static void Main(string[] args)
{ int[] a = {1,2,3};
F(a);//实参为数组类引用变量a
F(10, 20, 30, 40);//等价于F(new int[] {60,70,80,90});
F(new int[] {60,70,80,90});//实参为数组类引用
F();//等价于F(new int[] {});
F(new int[] {});//实参为数组类引用,数组无元素
}
}
程序输出
Array contains 3 elements: 1 2 3
Array contains 4 elements: 10 20 30 40
Array contains 4 elements: 60,70,80,90
Array contains 0 elements:
Array contains 0 elements:
方法的参数为数组时也可以不使用params,此种方法可以使用一维或多维数组,见下例:
using System;
class Class1
{ static void F(int[,] args)//值参数,参数类型为数组类引用变量,无params说明
{ Console.Write("Array contains {0} elements:",args.Length);
foreach (int i in args)
Console.Write(" {0}",i);
Console.WriteLine();
}
static void Main(string[] args)
{ int[,] a = {{1,2,3},{4,5,6}};
F(a);//实参为数组类引用变量a
//F(10, 20, 30, 40);//此格式不能使用
F(new int[,] {{60,70},{80,90}});//实参为数组类引用
//F();//此格式不能使用
//F(new int[,] {});//此格式不能使用
}
}
程序输出
Array contains 3 elements: 1 2 3 4 5 6
Array contains 4 elements: 60,70,80,90
1.10.3 静态方法和实例方法
用修饰符static声明的方法为静态方法,不用修饰符static声明的方法为实例方法。不管类生成或未生成对象,类的静态方法都可以被使用,使用格式为:类名.静态方法名。静态方法只能使用该静态方法所在类的静态数据成员和静态方法。这是因为使用静态方法时,该静态方法所在类可能还没有对象,即使有对象,由于用类名.静态方法名方式调用静态方法,静态方法没有this指针来存放对象的地址,无法判定应访问哪个对象的数据成员。在类创建对象后,实例方法才能被使用,使用格式为:对象名.实例方法名。实例方法可以使用该方法所在类的所有静态成员和实例成员。例子如下:
using System;
public class UseMethod
{ private static int x=0;//静态字段
private int y=1;//实例字段
public static void StaticMethod()//静态方法
{ x=10;//正确,静态方法访问静态数据成员
//y=20;//错误,静态方法不能访问实例数据成员
}
public void NoStaticMethod()//实例方法
{ x=10;//正确,实例方法访问静态数据成员
y=20;//正确,实例方法访问实例数据成员
}
}
public class Class1
{ public static void Main()
{ UseMethod m=new UseMethod();
UseMethod.StaticMethod();//使用静态方法格式为:类名.静态方法名
m.NoStaticMethod();//使用实例方法格式为:对象名.实例方法名
}
}
1.10.4 方法的重载
在C#语言中,如果在同一个类中定义的函数名相同,而参数类型或参数个数不同,认为是不相同的函数,仅返回值不同,不能看作不同函数,这叫做函数的重载。前边Person类中定义了多个构造函数就是重载的例子。在C语言中,若计算一个数据的绝对值,则需要对不同数据类型求绝对值方法使用不同的方法名,如用abc()求整型数绝对值,labs()求长整型数绝对值,fabs()求浮点数绝对值。而在C#语言中,可以使用函数重载特性,对这三个函数定义同样的函数名,但使用不同的参数类型。下面是实现方法:
using System;
public class UseAbs
{ public int abs(int x)//整型数求绝对值
{ return(x<0 ? -x:x);}
public long abs(long x)//长整型数求绝对值
{return(x<0 ? -x:x);}
public double abs(double x)//浮点数求绝对值
{return(x<0 ? -x:x);}
}
class Class1
{ static void Main(string[] args)
{ UseAbs m=new UseAbs();
int x=-10;
long y=-123;
double z=-23.98d;
x=m.abs(x);
y=m.abs(y);
z=m.abs(z);
Console.WriteLine("x={0},y={1},z={2}",x,y,z);
}
}
类的对象调用这些同名方法,在编译时,根据调用方法的实参类型决定调用那个同名方法,计算不同类型数据的绝对值。这给编程提供了极大方便。
1.10.5 操作符重载
操作符重载是将C#语言中的已有操作符赋予新的功能,但与该操作符的本来含义不冲突,使用时只需根据操作符出现的位置来判别其具体执行哪一种运算。操作符重载,实际是定义了一个操作符函数,操作符函数声明的格式如下:
static public 函数返回类型 operator 重新定义的操作符(形参表)
C#语言中有一些操作符是可以重载的,例如:+ - ! ~ ++ -- true false * / % & | ^ << >> == != > < >= <=等等。但也有一些操作符是不允许进行重载的,例如:=, &&, ||, ?:, new, typeof, sizeof, is等。
下边的例子,定义一个复数类,并且希望复数的加减乘除用符号+,-.*,/来表示。
using System;
class Complex//复数类定义
{ private double Real;//复数实部
private double Imag;//复数虚部
public Complex(double x,double y)//构造函数
{ Real=x;
Imag=y;
}
static public Complex operator - (Complex a)//重载一元操作符负号,注意1个参数
{ return (new Complex(-a.Real,-a.Imag));}
static public Complex operator +(Complex a,Complex b)//重载二元操作符加号
{ return (new Complex(a.Real+b.Real,a.Imag+b.Imag));}
public void Display()
{ Console.WriteLine("{0}+({1})j",Real,Imag);}
}
class Class1
{ static void Main(string[] args)
{ Complex x=new Complex(1.0,2.0);
Complex y=new Complex(3.0,4.0);
Complex z=new Complex(5.0,7.0);
x.Display();//显示:1+(2)j
y.Display();//显示:3+(4)j
z.Display();//显示:5+(7)j
z=-x;//等价于z=opeator-(x)
z.Display();//显示:-1+(-2)j
z=x+y;//即z=opeator+(x,y)
z.Display();//显示:4+(6)j
}
}
1.10.6 this关键字
每个类都可以有多个对象,例如定义Person类的两个对象:
Person P1=new Person("李四",30);
Person P2=new Person("张三",40);
因此P1.Display()应显示李四信息,P2.Display()应显示张三信息,但无论创建多少个对象,只有一个方法Display(),该方法是如何知道显示那个对象的信息的呢?C#语言用引用变量this记录调用方法Display()的对象,当某个对象调用方法Display()时,this便引用该对象(记录该对象的地址)。因此,不同的对象调用同一方法时,方法便根据this所引用的不同对象来确定应该引用哪一个对象的数据成员。this是类中隐含的引用变量,它是被自动被赋值的,可以使用但不能被修改。例如:P1.Display(),this引用对象P1,显示李四信息。P2.Display(),this引用对象P2,显示张三信息。
1.11 类的多态性
在面向对象的系统中,多态性是一个非常重要的概念。C#支持两种类型的多态性,第一种是编译时的多态性,一个类的对象调用若干同名方法,系统在编译时,根据调用方法的实参类型及实参的个数决定调用那个同名方法,实现何种操作。编译时的多态性是通过方法重载来实现的。C#语言的方法重载以及操作符重载和C++语言的基本一致。
第二种是运行时的多态性,是在系统运行时,不同对象调用一个名字相同,参数的类型及个数完全一样的方法,会完成不同的操作。C#运行时的多态性通过虚方法实现。在类的方法声明前加上了virtual修饰符,被称之为虚方法,反之为非虚方法。C#语言的虚方法和C++语言的基本一致。下面的例子说明了虚方法与非虚方法的区别:
using System;
class A
{ public void F()//非虚方法
{ Console.Write(" A.F");}
public virtual void G()//虚方法
{ Console.Write(" A.G");}
}
class B:A//A类为B类的基类
{ new public void F()//覆盖基类的同名非虚方法F(),注意使用new
{ Console.Write(" B.F");}
public override void G()//覆盖基类的同名虚方法G(),注意使用override
{ Console.Write(" B.G");}
}
class Test
{ static void F2(A aA)//注意,参数为A类引用变量
{ aA.G();}
static void Main()
{ B b=new B();
A a1=new A();
A a2=b;//允许基类引用变量引用派生类对象,a2引用派生类B对象b
a1.F();//调用基类A的非虚方法F(),显示A.F
a2.F();//F()为非虚方法,调用基类A的F(),显示A.F
b.F();//F()为非虚方法,调用派生类B的F(),显示B.F
a1.G();//G()为虚方法,因a1引用基类A对象,调用基类A的G(),显示A.G
a2.G();//G()为虚方法,因a2引用派生类B对象,调用派生类B的G(),显示B.G
F2(b);//实参为派生类B对象,由于A aA=b,调用派生类B的函数G(),显示B.G
F2(a1);//实参为基类A对象,调用A类的函数G(),显示A.G
}
}
那么输出应该是:
A.F A.F B.F A.G B.G B.G A.G
注意例子中,不同对象调用同名非虚方法F()和同名虚方法G()的区别。a2虽然是基类引用变量,但它引用派生类对象b。由于G()是虚方法,因此a2.G()调用派生类B的G(),显示G.F。但由于F()是非虚方法,a2.F()仍然调用基类A的F(),显示A.F。或者说,如果将基类引用变量引用不同对象,或者是基类对象,或者是派生类对象,用这个基类引用变量分别调用同名虚方法,根据对象不同,会完成不同的操作。而非虚方法则不具备此功能。
方法F2(A aA)中,参数是A类类型,F2(b)中形参和实参的关系是:A aA=b,即基类引用变量aA引用派生类对象b,aA.G()调用派生类B的函数G(),显示B.G。同理,F2(a1)实参为基类A对象,调用A类的函数G(),显示A.G。
在类的基本概念一节中,定义一个描述个人情况的类Person,其中公有方法Display()用来显示个人信息。在派生雇员类Employee中,覆盖了基类的公有方法Display(),以显示雇员新增加的信息。我们希望隐藏这些细节,希望无论基类还是派生类,都调用同一个显示方法,根据对象不同,自动显示不同的信息。可以用虚方法来实现,这是一个典型的多态性例子。例子
using System;
public class Person
{ private String name="张三";//类的数据成员声明
private int age=12;
protected virtual void Display()//类的虚方法
{ Console.WriteLine("姓名:{0},年龄:{1}",name,age);
}
public Person(string Name,int Age)//构造函数,函数名和类同名,无返回值
{ name=Name;
age=Age;
}
static public void DisplayData(Person aPerson)//静态方法
{ aPerson.Display();//不是静态方法调用实例方法,如写为Display()错误
}
}
public class Employee:Person//Person类是基类
{ private string department;
private decimal salary;
public Employee(string Name,int Age,string D,decimal S):base(Name,Age)
{ department=D;
salary=S;
}
protected override void Display()//重载虚方法,注意用override
{ base.Display();//访问基类同名方法
Console.WriteLine("部门:{0} 薪金:{1} ", department,salary);
}
}
class Class1
{ static void Main(string[] args)
{ Person OnePerson=new Person("李四",30);
Person.DisplayData(OnePerson);//显示基类数据
Employee OneEmployee=new Employee("王五",40,"财务部",2000);
Person.DisplayData(OneEmployee); //显示派生类数据
}
}
运行后,显示的效果是:
姓名: 李四,年龄:30
姓名: 王五,年龄:40
部门:财务部 薪金:2000
1.12 抽象类和抽象方法
抽象类表示一种抽象的概念,只是希望以它为基类的派生类有共同的函数成员和数据成员。抽象类使用abstract修饰符,对抽象类的使用有以下几点规定:
? 抽象类只能作为其它类的基类,它不能直接被实例化。
? 抽象类允许包含抽象成员,虽然这不是必须的。抽象成员用abstract修饰符修饰。
? 抽象类不能同时又是密封的。
? 抽象类的基类也可以是抽象类。如果一个非抽象类的基类是抽象类,则该类必须通过覆盖来实现所有继承而来的抽象方法,包括其抽象基类中的抽象方法,如果该抽象基类从其它抽象类派生,还应包括其它抽象类中的所有抽象方法。
请看下面的示例:
abstract class Figure//抽象类定义
{ protected double x=0,y=0;
public Figure(double a,double b)
{ x=a;
y=b;
}
public abstract void Area();//抽象方法,无实现代码
}
class Square:Figure///类Square定义
{ public Square(double a,double b):base(a,b)
{}
public override void Area()//不能使用new,必须用override
{ Console.WriteLine("矩形面积是:{0}",x*y);}
}
class Circle:Figure///类Square定义
{ public Circle(double a):base(a,a)
{}
public override void Area()
{ Console.WriteLine("园面积是:{0}",3.14*x*y);}
}
class Class1
{ static void Main(string[] args)
{ Square s=new Square(20,30);
Circle c=new Circle(10);
s.Area();
c.Area();
}
}
程序输出结果为:
矩形面积是:600
园面积是:314
抽象类Figure提供了一个抽象方法Area(),并没有实现它,类Square和Circle从抽象类Figure中继承方法Area(),分别具体实现计算矩形和园的面积。
在类的基本概念一节中,定义一个描述个人情况的类Person,它只是描述了一个人最一般的属性和行为,因此不希望生成它的对象,可以定义它为抽象类。
注意:C++程序员在这里最容易犯错误。C++中没有对抽象类进行直接声明的方法,而认为只要在类中定义了纯虚函数,这个类就是一个抽象类。纯虚函数的概念比较晦涩,直观上不容易为人们接受和掌握,因此C#抛弃了这一概念。
1.13 密封类和密封方法
有时候,我们并不希望自己编写的类被继承。或者有的类已经没有再被继承的必要。C#提出了一个密封类(sealed class)的概念,帮助开发人员来解决这一问题。
密封类在声明中使用sealed修饰符,这样就可以防止该类被其它类继承。如果试图将一个密封类作为其它类的基类,C#编译器将提示出错。理所当然,密封类不能同时又是抽象类,因为抽象总是希望被继承的。
C#还提出了密封方法(sealed method)的概念。方法使用sealed修饰符,称该方法是一个密封方法。在派生类中,不能覆盖基类中的密封方法。
1.14 接口
与类一样,在接口中可以定义一个和多个方法、属性、索引指示器和事件。但与类不同的是,接口中仅仅是它们的声明,并不提供实现。因此接口是函数成员声明的集合。如果类或结构从一个接口派生,则这个类或结构负责实现该接口中所声明的所有成员。一个接口可以从多个接口继承,而一个类或结构可以实现多个接口。由于C#语言不支持多继承,因此,如果某个类需要继承多个类的行为时,只能使用多个接口加以说明。
1.14.1 接口声明
接口声明是一种类型声明,它定义了一种新的接口类型。接口声明格式如下:
属性 接口修饰符 interface 接口名:基接口{接口体}
其中,关键字interface、接口名和接口体时必须的,其它项是可选的。接口修饰符可以是new、public、protected、internal和private。例子:
public interface IExample
{//所有接口成员都不能包括实现
string this[int index] {get;set;}//索引指示器声明
event EventHandler E;//事件声明
void F(int value);//方法声明
string P { get; set;}//属性声明
}
声明接口时,需注意以下内容:
? 接口成员只能是方法、属性、索引指示器和事件,不能是常量、域、操作符、构造函数或析构函数,不能包含任何静态成员。
? 接口成员声明不能包含任何修饰符,接口成员默认访问方式是public。
1.14.2 接口的继承
类似于类的继承性,接口也有继承性。派生接口继承了基接口中的函数成员说明。接口允许多继承,一个派生接口可以没有基接口,也可以有多个基接口。在接口声明的冒号后列出被继承的接口名字,多个接口名之间用分号分割。例子如下:
using System;
interface IControl
{ void Paint();
}
interface ITextBox:IControl//继承了接口Icontrol的方法Paint()
{ void SetText(string text);
}
interface IListBox:IControl//继承了接口Icontrol的方法Paint()
{ void SetItems(string[] items);
}
interface IComboBox:ITextBox,IListBox
{//可以声明新方法
}
上面的例子中,接口ITextBox和IListBox都从接口IControl中继承,也就继承了接口IControl的Paint方法。接口IComboBox从接口ITextBox和IListBox中继承,因此它应该继承了接口ITextBox的SetText方法和IListBox的SetItems方法,还有IControl的Paint方法。
1.14.3 类对接口的实现
前面已经说过,接口定义不包括函数成员的实现部分。继承该接口的类或结构应实现这些函数成员。这里主要讲述通过类来实现接口。类实现接口的本质是,用接口规定类应实现那些函数成员。用类来实现接口时,接口的名称必须包含在类声明中的基类列表中。
在类的基本概念一节中,定义一个描述个人情况的类Person,从类Person可以派生出其它类,例如:工人类、公务员类、医生类等。这些类有一些共有的方法和属性,例如工资属性。一般希望所有派生类访问工资属性时用同样变量名。该属性定义在类Person中不合适,因为有些人无工资,如小孩。如定义一个类作为基类,包含工资属性,但C#不支持多继承。可行的办法是使用接口,在接口中声明工资属性。工人类、公务员类、医生类等都必须实现该接口,也就保证了它们访问工资属性时用同样变量名。例子如下:
using System;
public interface I_Salary//接口
{ decimal Salary//属性声明
{ get;
set;
}
}
public class Person
{…//见1.9.2属性节Person类定义,这里不重复了。
}
public class Employee:Person,I_Salary//Person类是基类,I_Salary是接口
{//不同程序员完成工人类、医生类等,定义工资变量名称可能不同
private decimal salary;
public new void Display()
{ base.Display();
Console.WriteLine("薪金:{0} ",salary);
}
//工人类、医生类等都要实现属性Salary,保证使用的工资属性同名
public decimal Salary
{ get
{ return salary;}
set
{ salary=value;}
}
}
public class Test
{ public static void Main()
{ Employee S=new Employee();
S.Name="田七";//修改属性Name
S.Age=20;//修改属性Age
S.Salary=2000;//修改属性Salary
S.Display();
}
}
如果类实现了某个接口,类也隐式地继承了该接口的所有基接口,不管这些基接口有没有在类声明的基类表中列出。因此,如果类从一个接口派生,则这个类负责实现该接口及该接口的所有基接口中所声明的所有成员。
1.15 代表
在这里要介绍的是C#的一个引用类型----代表(delegate),也翻译为委托。它实际上相当于C语言的函数指针。与指针不同的是C#中的代表是类型安全的。代表类声明格式如下:
属性集 修饰符 delegate 函数返回类型 定义的代表标识符(函数形参列表);
修饰符包括new、public、protected、internal和private。例如我们可以声明一个返回类型为int,无参数的函数的代表MyDelegate:
public delegate int MyDelegate();//只能代表返回类型为int,无参数的函数
声明了代表类MyDelegate,可以创建代表类MyDelegate的对象,用这个对象去代表一个静态方法或非静态的方法,所代表的方法必须为int类型,无参数。看下面的例子:
using System;
delegate int MyDelegate();//声明一个代表,注意声明的位置
public class MyClass
{ public int InstanceMethod()//非静态的方法,注意方法为int类型,无参数
{ Console.WriteLine("调用了非静态的方法。");
return 0;
}
static public int StaticMethod()//静态方法,注意方法为int类型,无参数
{ Console.WriteLine("调用了静态的方法。");
return 0;
}
}
public class Test
{ static public void Main ()
{ MyClass p = new MyClass();
//用new建立代表类MyDelegate对象,d中存储非静态的方法InstanceMethod的地址
MyDelegate d=new MyDelegate(p.InstanceMethod);//参数是被代表的方法
d();//调用非静态方法
//用new建立代表类MyDelegate对象,d中存储静态的方法StaticMethod的地址
d=new MyDelegate(MyClass.StaticMethod);//参数是被代表的方法
d();//调用静态方法
}
}
程序的输出结果是:
调用了非静态的方法。
调用了静态的方法。
1.16 事件
事件是C#语言内置的语法,可以定义和处理事件,为使用组件编程提供了良好的基础。
1.16.1 事件驱动
Windows操作系统把用户的动作都看作消息,C#中称作事件,例如用鼠标左键单击按钮,发出鼠标单击按钮事件。Windows操作系统负责统一管理所有的事件,把事件发送到各个运行程序。各个程序用事件函数响应事件,这种方法也叫事件驱动。
C#语言使用组件编制Windows应用程序。组件本质上是类。在组件类中,预先定义了该组件能够响应的事件,以及对应的事件函数,该事件发生,将自动调用自己的事件函数。例如,按钮类中定义了单击事件Click和单击事件函数。一个组件中定义了多个事件,应用程序中不必也没必要响应所有的事件,而只需响应其中很少事件,程序员编制相应的事件处理函数,用来完成需要响应的事件所应完成的功能。现在的问题是,第一,如何把程序员编制的事件处理函数和组件类中预先定义的事件函数联系起来。第二,如何使不需响应的事件无动作。这是本节要节的解决问题。
1.16.2 事件的声明
在C#中,事件首先代表事件本身,例如按钮类的单击事件,同时,事件还是代表类引用变量,可以代表程序员编制的事件处理函数,把事件和事件处理函数联系在一起。下面的例子定义了一个Button组件,这个例子不完整,只是说明问题。实际在C#语言类库中已预定义了Button组件,这里的代码只是想说明Button组件中是如何定义事件的。例子如下:
public delegate void EventHandler(object sender,EventArgs e);//代表声明
//EventHandler可以代表没有返回值,参数为(object sender,EventArgs e)的函数
public class Button:Control//定义一个按钮类Button组件
{…//按钮类Button其它成员定义
public event EventHandler Click;//声明一个事件Click,是代表类引用变量
protected void OnClick(EventArgs e)//Click事件发生,自动触发OnClick方法
{ if(Click!=null)//如果Click已代表了事件处理函数,执行这个函数
Click(this,e);
}
public void Reset()
{ Click=null;}
}
在这个例子中,Click事件发生,应有代码保证(未列出)自动触发OnClick方法。Click是类Button的一个事件,同时也是代表EventHandler类的引用变量,如令Click代表事件处理函数,该函数完成Click事件应完成的功能,Click事件发生时,执行事件处理函数。
1.16.3 事件的预订和撤消
在随后的例子中,我们声明了一个使用Button类的登录对话框类,对话框类含有两个按钮:OK和Cancel按钮。
public class LoginDialog: Form//登录对话框类声明
{ Button OkButton;
Button CancelButton;
public LoginDialog()//构造函数
{ OkButton=new Button();//建立按钮对象OkButton
//Click代表OkButtonClick方法,注意+=的使用
OkButton.Click+=new EventHandler(OkButtonClick);
CancelButton=new Button();//建立按钮对象OkButton
CancelButton.Click += new EventHandler(CancelButtonClick);
}
void OkButtonClick(object sender, EventArgs e)
{…//处理OkButton.Click事件的方法
}
void CancelButtonClick(object sender, EventArgs e)
{…//处理CancelButton.Click事件的方法
}
}
在例子中建立了Button类的两个实例,单击按钮事件Click通过如下语句和事件处理方法联系在一起:OkButton.Click+=new EventHandler(OkButtonClick),该语句的意义是使OkButton.Click代表事件处理方法OkButtonClick,这样只要Click事件被触发,事件处理方法OkButtonClick就会被自动调用。撤消事件和事件处理方法OkButtonClick的联系采用如下语句实现:OkButton.Click-=new EventHandler(OkButtonClick),这时,OkButton.Click就不再代表事件处理方法,Click事件被触发,方法OkButtonClick就不会被调用了。务必理解这两条语句的用法。使用Visual Studio.Net集成环境可以自动建立这种联系,在自动生成的代码中包括这两条语句。
1.17 索引指示器
在C#语言中,数组也是类,比如我们声明一个整型数数组:int[] arr=new int[5],实际上生成了一个数组类对象,arr是这个对象的引用(地址),访问这个数组元素的方法是:arr[下标],在数组类中,使用索引访问元素是如何实现的呢?是否可以定义自己的类,用索引访问类中的数据成员?索引指示器(indexer)为我们提供了通过索引方式方便地访问类的数据成员的方法。
首先看下面的例子,用于打印出小组人员的名单:
using System
class Team
{ string[] s_name = new string[2];//定义字符串数组,记录小组人员姓名
public string this[int nIndex]//索引指示器声明,this为类Team类的对象
{ get//用对象名[索引]得到记录小组人员姓名时,调用get函数
{ return s_name[nIndex];
}
set//用对象名[索引]修改记录小组人员姓名时,调用set函数
{ s_name[nIndex] =value;//value为被修改值
}
}
}
class Test
{ public static void Main()
{ Team t1 = new Team();
t1[0]="张三";
t1[1]="李斯";
Console.WriteLine("{0},{1}",t1[0], t1[1]);
}
}
显示结果如下:张三,李斯
1.18 名字空间
一个应用程序可能包含许多不同的部分,除了自己编制的程序之外,还要使用操作系统或开发环境提供的函数库、类库或组件库,软件开发商处购买的函数库、类库或组件库,开发团队中其它人编制的程序,等等。为了组织这些程序代码,使应用程序可以方便地使用这些程序代码,C#语言提出了名字空间的概念。名字空间是函数、类或组件的容器,把它们按类别放入不同的名字空间中,名字空间提供了一个逻辑上的层次结构体系,使应用程序能方便的找到所需代码。这和C语言中的include语句的功能有些相似,但实现方法完全不同。
1.18.1 名字空间的声明
用关键字namespace声明一个名字空间,名字空间的声明要么是源文件using语句后的第一条语句,要么作为成员出现在其它名字空间的声明之中,也就是说,在一个名字空间内部还可以定义名字空间成员。全局名字空间应是源文件using语句后的第一条语句。在同一名字空间中,不允许出现同名名字空间成员或同名的类。在声明时不允许使用任何访问修饰符,名字空间隐式地使用public修饰符。例子如下:
using System;
namespace N1//N1为全局名字空间的名称,应是using语句后的第一条语句
{ namespace N2//名字空间N1的成员N2
{ class A//在N2名字空间定义的类不应重名
{ void f1(){};}
class B
{ void f2(){};}
}
}
也可以采用非嵌套的语法来实现以上名字空间:
namespace N1.N2//类A、B在名字空间N1.N2中
{ class A
{ void f1(){};}
class B
{ void f2(){};}
}
也可以采用如下格式:
namespace N1.N2//类A在名字空间N1.N2中
{ class A
{ void f1(){};}
}
namespace N1.N2//类B在名字空间N1.N2中
{ class B
{ void f2(){};}
}
1.18.2 名字空间使用
如在程序中,需引用其它名字空间的类或函数等,可以使用语句using,例如需使用上节定义的方法f1()和f2(),可以采用如下代码:
using N1.N2;
class WelcomeApp
{ A a=new A();
a.f1();
}
using N1.N2实际上是告诉应用程序到哪里可以找到类A。请读者重新看一下1.2.1节中的例子。
1.19 非安全代码
在C和C++的程序员看来,指针是最强有力的工具之一,同时又带来许多问题。因为指针指向的数据类型可能并不相同,比如你可以把int类型的指针指向一个float类型的变量,而这时程序并不会出错。如果你删除了一个不应该被删除的指针,比如Windows中指向主程序的指针,程序就有可能崩溃。因此滥用指针给程序带来不安全因素。正因为如此,在C#语言中取消了指针这个概念。虽然不使用指针可以完成绝大部分任务,但有时在程序中还不可避免的使用指针,例如调用Windows操作系统的API函数,其参数可能是指针,所以在C#中还允许使用指针,但必须声明这段程序是非安全(unsafe)的。可以指定一个方法是非安全的,例如:unsafe void F1(int * p){…}。可以指定一条语句是非安全的,例如:unsafe int* p2=p1;还可以指定一段代码是非安全的,例如:unsafe{ int* p2=p1;int* p3=p4;}。在编译时要采用如下格式:csc 要编译的C#源程序 /unsafe。
习题
1. 从键盘输入姓名,在显示器中显示对输入姓名的问候。(提示:string为字符串类型,用语句string s=Console.ReadLine()输入姓名)
2. 构造函数和析购函数的主要作用是什么?它们各有什么特性?
3. 定义点类,数据成员为私有成员,增加有参数和无参数构造函数,在主函数中生成点类对象,并用字符显示点类对象的坐标。
4. 定义矩形类,数据成员为私有成员,增加有参数和无参数构造函数,在主函数中生成矩形类对象,并用字符显示矩形类对象的长、宽和矩形左上角的坐标。
5. 设计一个计数器类,统计键入回车的次数,数据成员为私有成员,在主程序中使用此类统计键入回车的次数。
6. 说明值类型和引用类型的区别,并和C语言相应类型比较。
7. 定义点结构,在主函数中生成点结构变量,从键盘输入点的位置,并重新显示坐标。
8. 定义整型一维数组,从键盘输入数组元素数值后,用循环语句显示所有元素的值。
9. 输入字符串,将字符串第一个字母和每个空格后的字母变为大写,其余字母为小写后输出。
10. 输入5个数,在每两个数之间增加3个空格后输出。
11. 编一个猜数程序,程序设定一个1位十进制数,允许用户猜3次,错了告诉比设定数大还是小,用switch语句实现。
12. C#语言for语句可以这样使用:for(int i;i<10;i++),请问,i的有效使用范围。
13. 用字符*在CRT上显示一个矩形。
14. 输入一个字符串,用foreach语句计算输入的字符串长度,并显示长度。
15. 输入两个数相加,并显示和。用异常语句处理输入错误。
16. 将1.6.3节中try–catch-finally语句例子改为try-finally和try–catch语句。
17. 定义点类,从点类派生矩形类,数据成员为私有成员,增加有参数和无参数构造函数,在主函数中生成矩形类对象,并用字符显示矩形类对象的长、宽和矩形左上角的坐标。
18. 重做12题,将数据成员用属性表示。
19. 定义一个类,将类外部的char数组元素都变为大写。主程序输入一个字符串,将其变为char数组,变为大写后输出每一个char数组元素。分别用类对象和静态函数实现。
20. 定义分数类,实现用符号+,-,*,/完成分数的加减乘除。在主函数中输入两个数,完成运算后输出运算结果。
21. 建立一个sroot()函数,返回其参数的二次根。重载它,让它能够分别返回整数、长整数和双精度参数的二次根。
22. 重新设计complex类,完成复数的+、-、*、/四则运算。
23. 定义点类,从点类派生矩形类和园类,主程序实现用同一个方法显示矩形和园的面积。
24. 重做19题,将点类定义为抽象类。
25. 重做19题,改为接口实现,即将点类改为接口。
初学者第二季