.Net体系结构与C#简介
.Net平台无关性
通用类型系统
公共语言运行时的一个重要组成部分称为通用类型系统(Common Type System,CTS)。
CTS类型主要分成两大类:引用类型和值类型,如图1-5所示。这两种类型之间也可以相互转换,方法是装箱(Boxing)和拆箱(UnBoxing)。
从上图我们可以得出如下几点结论:
- CTS类型不是引用类型就是值类型;
- 引用类型直接继承自Object对象,值类型继承自ValueType对象,而ValueType对象继承自Object对象;
- CTS类型不管是否直接继承自Object对象,最终都继承自Object。
装箱和拆箱
当值类型的数据转换为引用类型时,CLR会先在托管堆配置一块内存,将值类型的数据复制到这块内存,然后再让托管栈上引用类型的变量指向这块内存,这样的过程称为装箱(Boxing),反之则是拆箱(UnBoxing)。
如图所示,托管栈中类型为Int32(值为1023)的变量,装箱后引用类型变量位于栈中,原来值类型变量的值被放入到托管堆的一个对象中,其内容为1023,类型为Object,然后将位于托管栈中的引用类型变量指向堆中的这个Object类型的对象,这就是装箱的整个过程。
一般来说,装箱操作不需要我们主动去做,当将一个值类型的变量赋值给一个引用类型的变量时,.NET框架会自动帮我们做装箱处理,但拆箱操作并非自动的,我们必须知道被拆箱的对象的实际类型,然后明确地去执行拆箱操作。
Int32 BirthdayNumber=1023;//Int32类型变量BirthdayNumber
Object BoxingBirthdayNumber=BirthdayNumber;//系统自动装箱
Int32 UnBoxingBirthdayNumber=(Int32)BoxingBirthdayNumber;//明确地拆箱
有一点需要注意的是,装箱和拆箱对性能是有影响的,因为它花费了更多的时间。
公共语言规范
CTS中定义了大量的类型,要求每种语言都全部实现并不现实,亦无必要,例如Visual Basic.NET就没有完全实现CTS,因此在CTS这个大的集合中,撷取其中的一部分,要求各种语言都支持,否则无法实现互操作,这一小部分称作公共语言规范(Common Language Specification,CLS)。
关键字
C#共有77个关键字,原则上,关键字不可以用做标识符,但有一种情况例外,加上前导字符@就可以用作标识符了,例如:@abstract、@bool、@break等都是合法的标识符,但我们不推荐这样做。
命名约定
.NET类型
数据类型的C#关键字(如int、short和string)从编译器映射到.NET数据类型。例如,在C#中声明一个int类型的数据时,声明的实际上是.NET结构System.Int32的一个实例。这听起来似乎很深奥,但其意义深远:这表示在语法上,可以把所有的基本数据类型看成支持某些方法的类。例如,要把int i转换为string类型,可以编写下面的代码:
string s = i.ToString();
应强调的是,在这种便利语法的背后,类型实际上仍存储为基本类型。基本类型在概念上用.NET结构表示,所以肯定没有性能损失。
有些C#类型的名称与C++和Java类型一致,但定义不同。例如,在C#中,int总是32位有符号的整数。而在C++中,int是有符号的整数,但其位数取决于平台(在Windows上是32位)。在C#中,所有的数据类型都以与平台无关的方式定义,以备将来从C#和.NET迁移到其他平台上。
bool值和整数值不能相互隐式转换。如果变量(或函数的返回类型)声明为bool类型,就只能使用值true或false。如果试图使用0表示false,非0值表示true,就会出错。
object类型
许多编程语言和类层次结构都提供了根类型,层次结构中的其他对象都从它派生而来。C#和.NET也不例外。在C#中,object类型就是最终的父类型,所有内置类型和用户定义的类型都从它派生而来。这样,object类型就可以用于两个目的:
- 可以使用object引用来绑定任何特定子类型的对象。例如,第8章将说明如何使用object类型把堆栈中的值对象装箱,再移动到堆中。object引用也可以用于反射,此时必须有代码来处理类型未知的对象。
- object类型实现了许多一般用途的基本方法,包括Equals()、GetHashCode()、GetType()和ToString()。用户定义的类需要使用一种面向对象技术——重写(见第4章),来提供其中一些方法的替代实现代码。例如,重写ToString()时,要给类提供一个方法,给出类本身的字符串表示。如果类中没有提供这些方法的实现代码,编译器就会使用object类型中的实现代码,它们在类上下文中的执行不一定正确。
可以在字符串字面量的前面加上字符@,在这个字符后的所有字符都看成其原来的含义——它们不会解释为转义字符:string filepath = @"C:\ProCSharp\First.cs";
可空类型(Nullable)
?
: 单问号用于对 int,double,bool 等无法直接赋值为 null 的数据类型进行 null 的赋值,意思是这个数据类型是 NullAble 类型的。
int? i = 3
等同于
Nullable<int> i = new Nullable<int>(3);
int i; //默认值0
int? ii; //默认值null
??
: 双问号 可用于判断一个变量在为 null 时返回一个指定的值。
变量的初始化
基于安全性考虑,C#对变量的初始化有一定要求:
- 所有的局部变量在被显式地初始化之前,都会被编译器当作未初始化,然后抛出编译期异常;
- 所有的字段级变量被编译器初始化为所属类型中等价于0的值。如布尔型的被初始化为false,
- 数值型的被初始化为0或者0.0,所有的引用类型都被初始化为null。
using System;
namespace ProgrammingCSharp4 {
class Person {
private int Age;
public void SayHello(){
string message;
Console.WriteLine(message);
}
}
}
上述代码中有两个变量,一个字段级变量Age,一个局部变量message,当我们编译这段代码的时候会产生如下错误:
使用了未赋值的局部变量"message"
这是因为我们没有给局部变量message赋值初始化,而Age变量是字段级变量,可以被编译器自动初始化为0。
数据类型
C#是一种强类型语言,无论是变量、常量,还是方法的参数、返回值,都需要指定相应的数据类型。从某种意义上来说,数据类型就像数据结构的模板,它包含了很多信息:
- 一种数据类型所需要的内存空间;
- 该数据类型的取值范围,即它可以表示的最大值和最小值;
- 它所继承的基类信息;
- 运行时它在内存中存储的位置;
- 该数据类型本身所支持的操作,比如数字型支持四则运算;
- 它自身的成员,如方法、字段、事件,等等。
内置的值类型
值类型主要由两大类型组成:结构类型和枚举类型。而我们平时常用的数值型、布尔型都是结构类型,其中数值型又包括:整型、浮点型和十进制型.
内置的引用类型
引用类型的变量又称为对象,存储的是对实际数据的引用。引用类型的变量存储在托管栈中,而实际的数据存储在托管堆中,引用类型主要成员如下引用类型的变量又称为对象,存储的是对实际数据的引用。引用类型的变量存储在托管栈中,而实际的数据存储在托管堆中,引用类型主要成员如下。
类型推断
从C#3.0开始,C#引入了一个新的关键字var,它表示一种隐式类型推断,编译器可以通过它的初始值来判断变量的具体类型。尤其需要注意的是,var只能用于局部变量的声明,不能用于字段级变量的声明,并且使用var声明的变量必须有初始值,这样编译器才有判断其是否是真实变量的依据。
类的语法
下面,我们看看类相关的语法。首先,我们使用如下语法声明一个类:
class TestClass
{
//方法,属性,字段,事件,委托
//以及内部类等,这些都是类的主体.
}
例如,我们声明一个SampleObject的类,它有一个字符串类型的sampleValue字段,代码如下:
class SampleObject
{
public string sampleValue;
}
一般情况都需要对类进行实例化,我们可以使用new关键词对类进行实例化,例如:
SampleObject sampleObject=new SampleObject();
sampleObject.sampleValue="sampleValue‘s scope";
接口
一个有关程序设计的最佳实践,就是要求面向接口编程,这样可以降低程序各部分间的耦合度。那么什么是接口呢?接口和类有什么区别?实现了接口的类必须实现接口规定的方法、属性等,可以说接口是一种约定,甚至是一种规定。接口和类的重要区别有如下两点:
- 接口可以继承多个基接口,而类只能继承一个类,但可以实现多个接口;
- 接口只能包含签名,不能含有实现,类无此限制。
接口能包含下列成员的签名:
- 方法
- 属性
- 事件
- 索引器
委托
委托类似C++中的函数指针,但它是类型安全的,也就是说它能够引用函数。每一个委托都有一个签名,可以使用delegate关键字来声明一个委托。
委托的作用相当于同类型函数签名的接口/集合体,通常用于多播。
object类型
C#里的object是.NET框架中Object的别名。在CTS中,所有类型包括:预定义类型、用户自定义类型、引用类型和值类型,都是直接或间接从Object继承的。因此,可以将任何类型的值赋给object类型的变量。
null类型
null关键字代表一个空值,用以表示一个引用没有指向任何对象或数组,它是引用类型变量的默认值。
Nullable类型
一个Nullable类型就是基本类型加上一个“是否为null指示器”的合成类型。对于一个类型,如果既可以给它分配一个值,也可以给它分配null引用(表示没有任何值),我们就说这个类型是可空的。因此,可空类型可表示一个值,或表示不存在任何值。例如,类似String的引用类型就是可空类型,而类似Int32的值类型不是可空类型。由于值类型的容量只够表示适合于该类型的值,因此它不可为空。
int?oneValue=null;
隐式转换
如果编译器认为从类型1(下称T1)到类型2(下称T2)的转换不会产生不良后果,那么T1到T2的转换就是由编译器自动完成的,这就是隐式转换
。
符合以下情况之一者,编译器可以自动实施隐式类型转换,并且不需要运行时类型检查:
- 任意引用类型到object类型的转换;
- 派生类型到基类型的转换;
- 派生类型到其实现的接口类型的转换;
- 派生接口类型到基接口类型的转换;
- 数组类型到System.Array类型的转换;
- 委托类型到System.Delegate类型的转换;
我们对装箱的类型转换做个总结,如下:
- 值类型可隐式转换到object类型或System.ValueType类型;
- 非Nullable值类型可隐式转换到它实现的接口;
- 枚举类型可隐式转换到System.Enum类型。
显式转换
显式类型转换
又叫做显式强制类型转换、强制类型转换,因为不能自动进行转换(和隐式类型转换相比而言),因而需要显式地告知编译器需要类型转换。隐式类型转换往往是由窄向宽的转换,而显式类型转换恰恰相反,是由宽向窄的类型转换。以数值类型为例,从一个取值范围更大的类型向较小的类型转换时,由于可能导致精度损失或引发异常,因此编译器不会自动进行隐式转换,除非明确告知。因此,显式转换也称为收缩转换。
那么,该如何告诉编译器我们确定要做这种显式的转换呢?很简单,只需要在变量前使用一对小括号()运算符,小括号中是目标类型。如果未定义相应的()运算符,则强制转换会失败,还可以使用as运算符进行类型转换。
引用类型
引用类型不同于值类型,它由两部分组成:栈中的变量和堆中的对象。对于引用类型的显式类型转换来说,转换的是栈中变量的类型,而该变量指向的位于堆中的对象则类型和数据都不受影响。一般来说,从基类向派生类的转换需要显式转换,因为基类“窄”而派生类“宽”,故而必须进行显式类型转换。
符合下列情况之一的,需要进行显式类型转换:
- object类型到任何引用类型的转换(任何引用类型都是object类型的子类);
- 基类到派生类的转换;
- 类到其实现的接口的转换;
- 非密封类到其没有实现接口的转换;
- 接口到另一个不是其基接口的转换;
- System.Array类型到数组类型的转换;
- System.Delegate类型到委托类型的转换。
显式类型转换的结果是否成功只有在运行时才能知道,转换失败则会抛出System.InvalidCastException异常。
拆箱
与装箱相反,从引用类型到值类型的转换称为拆箱。符合以下条件之一的进行拆箱操作:
- 从object类型或System.ValueType到值类型的转换;
- 从接口类型到值类型(实现了该接口)的转换;
- 从System.Eum类型到枚举类型的转换。
在执行拆箱操作前,编译器会首先检查引用类型是否是某个值类型或枚举类型的“装箱”版本,如果是就将其值拷贝出来,还原为值类型的变量。
as和is运算符
我们知道,隐式转换是安全的,而显式转换往往是不安全的,有可能造成精度损失,甚至会抛出异常。但类型转换又是不可避免的,例如对于某些集合类型,常常会用到System.Object类型的变量(使用泛型可以避免这种情况),对于非泛型集合,在将数据放入集合时将发生“向上转型”,即当前类型信息丢失,数据的类型成了object类型;而当需要把数据从集合取出时,因为需要恢复数据的本来类型,因此也就需要执行“向下转型”到它本来的类型。因此,如何更安全地进行类型转换就是一个值得探讨的问题了。幸好,C#已经为我们提供了解决方案,我们有两种选择:
- 使用as运算符进行类型转换;
- 先使用is运算符判断类型是否可以转换,再使用()运算符进行显式类型转换。
那么,我们先来介绍一下as和is运算符:
as运算符用于在两个引用类型之间进行转换,如果转换失败则返回null,并不抛出异常,因此转换是否成功可以通过结果是否为null进行判断,并且只能在运行时才能判断。
特别要注意的是,as运算符有一定的适用范围,它只适用于引用类型或可以为null的类型,而无法执行其他转换,如值类型的转换以及用户自定义的类型转换,这类转换应使用强制转换表达式来执行。
is运算符用于检查对象是否与给定类型兼容,并不执行真正的转换。如果判断的对象引用为null,则返回false。由于仅仅判断是否兼容,因此它并不会抛出异常。
同样,也要注意is的适用范围,它只适用于引用类型转换、装箱转换和拆箱转换。而不支持其他的类型转换,如值类型的转换。
现在,我们已经了解了as和is运算符,在实际工作中建议尽量使用as运算符,而少使用()运算符显式转换。理由如下:
- 无论是as还是is运算符,都比直接使用()运算符强制转换更安全;
- 不会抛出异常,免除了使用try……catch进行异常捕获的必要和系统开销,只需要判断是否为null;
- 使用as比使用is性能上更好,
现在我们总结下,什么场合该使用is,什么场合该使用as:如果测试对象的目的是确定它是否属于所需类型,并且如果测试结果为真,就要立即进行转换,这种情况下使用as操作符的效率更高;但有时仅仅只是测试,并不想立即转换,也可能根本就不会转换,只是在对象实现了接口时,要将它加到一个列表中,这时is操作符就是一种更好的选择。
类Class
类的成员
从级别来分,类的成员包括静态成员和实例成员。静态成员是类级别,不属于类的实例,而实例成员则属于类的实例(对象)。
从功能来分,类的成员包括字段、属性、方法、索引器、构造函数等,其中字段属于数据成员,方法属于函数成员。
字段
字段用于存储类所需要的数据。例如,一个类(汽车)可能有三个字段:速度、方向、行驶里程数(当然还可能有很多其他属性,我们此处只是举例说明,尽量将它简化而已)。
声明一个字段时,只需要说明如下3个要素即可:
- 访问级别
- 字段的类型
- 字段名称
class Car
{
public double speed;
protected string direction;
private double distance;
}
静态字段
和前述的实例字段不同的是,静态字段是类级别的,就是说访问它不需要先实例化类,直接使用“类名.静态字段名”即可访问。
//汽车型号
public static string Type;
静态字段的特点如下:
- 它不属于特定对象,而属于某一个类;
- 它不用创建类的实例即可访问(使用点运算符,且满足相应访问级别的情况下)。
字段的初始化
所有的字段级变量被编译器初始化为所属类型中等价于0的值。如布尔型被初始化为false,数值型被初始化为0或者0.0,所有的引用类型都被初始化为null。
当然,也可以在声明时就立即进行初始化,而且我们推荐这种方式,这是一个好的编程习惯。
属性
C#属性是字段的扩展,它配合C#中的字段使用,用以构造一个安全的应用程序。属性提供了灵活的机制来读取、编写或计算私有字段的值,可以像使用公共数据成员一样使用属性,但实际上它们是称做“访问器”的特殊方法,其设计目的主要是为了实现面向对象(Object Oriented,OO)中的封装思想。根据该思想,字段最好设为private,一个设计完善的类最好不要直接把字段声明为公有或受保护的,以阻止客户端直接进行访问,其中一个主要原因是,客户端直接对公有字段进行读写,使得我们无法对字段的访问进行灵活的控制,比如控制字段只读或者只写将很难实现。
属性的声明主要包含以下几个部分:访问修饰符、属性类型、属性名称、访问器。
public string name;
属性访问器包括get访问器和set访问器,分别用于字段的读写操作,但要注意的是,属性本身并不一定和字段相联系。仅包含get访问器的属性为只读属性,仅包含set访问器的属性为只写属性,同时包含两种访问器的属性可读也可写,称做读写属性。
get访问器的责任是返回字段的值,字段就是该属性所封装的字段,那么很自然地,返回值的类型应该和字段的类型一致。
set访问器的责任是为字段赋值,怎么赋值呢?它是通过一个隐式的参数value来实现值的传入。
public class person
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
}
还有一种更简单的方法,自动实现的属性:
public class person
{
public string Name{set;get;}
}
类的实例:对象
前面已经讲了类的成员变量和成员方法,如果要访问到这些成员,必须通过类的实例(除了静态成员以外)。前面讲过了,类是对数据和功能的封装,但封装不是目的,将类进行实例化,并使用对象的数据和服务完成某种任务才是目的。
要得到一个类的实例对象,必须先声明一个该类类型的变量,然后使用new运算符创建一个实例对象,后面会讲到,new运算符还会调用实例对象的构造函数。
Car car=new Car();
class Car
{
//当前行驶速度
public double maxSpeed;
//当前行驶方向
protected string direction;
//已行驶距离
private double distance;
//汽车型号
public static string Type;
public Car()
{
Type="Benz";
}
}
实例化中的内存分配
类是引用类型,我们知道,引用类型是在堆里分配内存的,在栈中保存的是对象的引用。因此,类的实例化涉及两个位置的内存分配。
- 在栈中为对象的引用分配空间;
- 在堆中为对象分配空间。
Car car=new Car();
这行代码看似简单,实则不然,其过程大致可分为两个步骤:
(1)首先,声明类型为Car的变量car,并使用null初始化,此时会在栈中为car变量分配一个内存,它指向null,因为在堆中还没有创建一个car对象实例以供它指向;
(2)其次,使用new运算符在堆中创建一个新的Car实例对象,并将其引用赋予变量car,此时栈中的car变量指向新创建的实例对象。
访问修饰符
访问修饰符的意义在于控制对象的权限,一切皆权限。
访问修饰符用于限制类、结果以及它们的成员的可访问性。访问修饰符包括4个关键字:public、protected、internal、private。
分部类型和分部方法——修饰符:partial
如何理解分部类型和分部方法呢?简单地说,就是将一个类型或方法拆分到两个或多个源文件中,每个源文件只包含类型定义的一部分。类、结构、接口、方法都可以拆分。
那么,为什么要进行拆分呢?或者说什么情况下才需要拆分?以下是几种使用场景:
- 当处理大型项目时,把一个类分布于多个独立文件中可以让多位程序员同时对该类进行处理;
- 使用自动生成的源时,无须重新创建源文件便可将代码添加到类中。Visual Studio在创建Windows窗体、Web服务包装代码等时都使用此方法。无须修改Visual Studio创建的文件,就可创建使用这些类的代码。
分部类具有如下特征:
- 类的定义前加partial修饰符;
- 分部类可以定义在两个不同的.cs文件中,也可以位于同一个.cs文件中;
- 分部类必须同属一个命名空间。
继承
类的继承
C#和C++不同,可以从一个类继承或实现多个接口,但不可以从多个类继承。新定义的派生类的实例可以继承已有的基类的特征和能力。
如果从其他的类继承,语法也很简单,只需在声明类时,在类名后加一个冒号,然后在冒号后指定要继承的类即可。
class Benz:Car
下面我们解释一下与继承性相关的两个概念:
- 基类:被继承的类,又称做父类。实际上,基类和派生类也只是一个相对的概念,Automobile(汽车)在此处是基类,但它同时也是一个派生类,因为任何类都继承自Object类,都是Object类型的派生类型。
- 派生类:继承自基类的类,又称做子类。派生类不但有自身的成员,还包含了基类的成员。我们还是以奔驰和汽车的关系举例,首先,奔驰汽车是汽车的一种,如果不把汽车作为基类抽象出来,那么势必会造成奔驰汽车、宝马汽车、奇瑞汽车等具有重复的性质,既造成无谓的冗余,又不利于数据复用及统一管理。因此,可以先定义一个代表汽车的基类Automobile,Automobile类作为基类,体现了“汽车”这个实体具有的公共性质:这里只拿鸣笛和行驶举例(对应两个方法:Beep()和Run()),然后定义代表奔驰汽车的派生类Benz,它派生自汽车。
使用new修饰符隐藏基类的成员
在这里new不再是运算符,而是修饰符。new修饰符的作用是显式地隐藏从基类继承的成员。你并非一定这样做,不用它也可以达到目的,但会引发一个编译器警告,同样地,如果你没有隐藏任何基类成员时,就不应使用new修饰符,那样也会引发一个编译器警告。
那么,如何隐藏基类的成员呢?
- 若要隐藏继承的数据成员(例如:字段),需要在派生类中声明一个相同名称的成员,并使用new修饰符修饰该成员,注意,这里只需名称相同即可,而不管类型是否相同;
- 若要隐藏继承的方法成员,需要在派生类中声明一个具有相同签名的方法,注意,签名不包括返回值,并使用new修饰符修饰该方法;
- 基类的静态成员也可以被隐藏,方法同上。
访问基类的成员
访问类的当前实例成员使用的是this关键字,访问基类的成员则使用base关键字。base的用法类似于this,在base后使用点运算符即可访问基类成员。base关键字还有另外一个用途,就是可以调用基类的构造函数。
using System;
namespace mytest
{
class Program
{
public class A
{
public A()
{
Console.WriteLine("Build A");
}
}
public class B : A
{
public B() : base()
{
Console.WriteLine("Build B");
}
static void Main()
{
B b = new B();
Console.ReadLine();
}
}
}
}
Build A
Build B
类的初始化顺序
(1)首先,初始化类的实例字段;
(2)其次,调用基类的构造函数,没有明确的基类则调用System.Object的构造函数;
(3)最后,调用类自己的构造函数。
多态
重载方法
重载就是在同一个类中存在多个同名的方法,但签名却不同。因此方法的重载需要以下几个前提条件:
- 在同一个类中;
- 方法名相同;
- 方法签名不同。
虚方法
如果希望基类中某个方法能够在派生类中进一步得到改进,那么可以将这个方法定义为虚方法,虚方法就是可以在派生类中对其实现进一步改进的方法。
定义虚方法要使用virtual关键字。
覆写方法
要在派生类覆写基类的虚方法,要使用override关键字。接上面的例子,我们来为基类Person派生两个子类:Chinese(中国人)和Englishmen(英国人),它们分别覆写父类的Speak方法。
标记为override的方法依然可以被覆写。
抽象类及抽象方法
- 抽象类及抽象方法仅可以被继承,不能被实例化;
- 抽象方法不能包含方法体,并且抽象方法所在的类必须也声明为抽象类;
- 要声明抽象类及抽象方法,可以使用关键字abstract,关键字abstract置于关键字class的前面。
接口
接口定义了一份契约,实现了接口的类或结构就意味着遵守接口定义的契约。接口只允许包含方法、属性、索引器和事件的签名,请注意,这里只允许包含签名,而不能包含实现。另外,接口不能包含常量、字段、运算符、实例构造函数、析构函数以及任何静态成员。
由于接口中不包含成员实现,因此接口中定义的成员必须在实现该接口的类或结构中提供成员实现。因此,一个接口的不同实现类可以有多个不同的实现方式。因此,当外界依赖的是接口,而不是实现类的时候,可以轻易地调换实现该接口的实现类,而无须改变依赖于该接口的代码,这就是业界为什么推崇“面向接口编程”的原因。
实现类不能选择实现接口中的某些成员,而不去实现另外一些成员,它必须全部、毫无保留地实现接口的所有成员,无论是方法、属性、索引器还是事件。除此以外,实现类还可以定义自己的成员,由于这些成员没有在接口中进行定义,因此当通过接口的引用进行访问的时候,这些成员将不可见。
那么,我们来看看接口具有哪些特点:
1.接口可以从一个或多个基接口继承,一个类或结构也可以实现多个接口。
2.接口只包含方法、属性、索引器和事件的签名,不能包含实现;
3.接口不能包含字段、常量、运算符、实例构造函数、析构函数以及任何静态成员;
4.接口的成员默认是公共的(public),且不可以再显式声明为public,否则将产生编译异常:修饰符"public"对该项无效。
5.接口不能被实例化,从接口类型编译后生成的CIL代码可以看到,接口的声明是抽象(abstract)的。
6.实现接口的类必须实现接口的所有成员,不管是方法、属性、索引器还是事件。
定义接口
首先,我们要学习如何定义一个接口。接口的定义非常类似于类的定义,只是把class关键字换成interface关键字,然而,接口的成员只包含签名,不包含内容,且不能包含任何访问修饰符(默认为public级别),需要说明的是必须以分号结尾。
interface Sample
{
void SayHello();//方法签名
int Age{get;set;}//属性签名
EventHandler SizeChanged();//事件签名
int this[int index]{get;set;}//索引器签名
}
声明和实现接口
先来定义两个接口,每个都仅定义了一个方法成员。
interface ISample
{
void Method1();
}
interface ISample2
{
void Method2();
}
我们在上述代码中定义了两个接口:ISample和ISample2。在接口的定义中省略了访问修饰符,那么意味着它们当前的访问级别是internal,即只能被同程序集的类“看到”并实现,而在程序集外根本无法“看到”。大家注意到了,这两个接口名称分别为ISample和ISample2。说到这里,顺便介绍一下接口的命名规则。接口的命名一般使用名词或名词短语,或者描述行为的形容词,尽可能少用缩写,不要使用下划线字符,大小写采用Pascal风格,即首字母和后面的每个单词的首字母都大写,其他字母小写。大家可能注意到了接口名字前的前缀I,它表示这个类型是一个接口。下面是几个合乎规范的例子:
- IComponent(描述性名词)
- IUserService(名词短语)
- IDisposable(形容词)
截至目前,我们已经定义了两个接口,下一步要学习如何实现一个接口。从一定意义上说,接口只是表示其所定义成员的一种存在性,它是一个契约。
我们继续举例说明,先定义两个类,分别实现接口ISample和ISample2。
class ClassOne:ISample
{
public void Method1()
{
System.Console.WriteLine("from ClassOne.Method1()");
}
public void Method2()
{
System.Console.WriteLine("from ClassOne.Method2()");
}
}
class ClassTwo:ISample
{
public void Method1()
{
System.Console.WriteLine("from ClassTwo.Method1()");
}
}
接下来,我们来看看如何调用这两个实现类。
class ClassExample
{
public static void Main()
{
ISample sample1=new ClassOne();
ISample sample2=new ClassTwo();
sample1.Method1();
sample2.Method1();
}
}
这段代码的执行结果如下:
from ClassOne.Method1()
from ClassTwo.Method1()
在上述代码中,要特别关注的是声明了两个类型为ISample的变量sample1和sample2,它们分别使用ClassOne和ClassTwo类进行了初始化。这说明接口类型的变量可以指向任何实现了这个接口的类的实例,这样固然很好,可以灵活地指定某个实现类,而不需要改变依赖这个接口的代码。但这样一来就只能通过这些接口类型的引用调用接口的方法,如果想要调用某个实现类中的自定义的、不在接口中的方法,就需要把引用强制类型为合适的类型。下面通过一个例子进行说明。
在ClassOne类中添加一个Method2方法,该方法的定义并未出现在接口ISample中,因此它属于ClassOne类,通过ISample接口的引用将无法看到访问到它,必须将ISample接口的引用向下转型到ClassOne类型才能进行访问。
interface ISample
{
}
class ClassOne:ISample
{
public void Method()//定义于ISample接口中的方法
{
System.Console.WriteLine("from ClassOne.Method()");
}
}
class ClassExample
{
public static void Main()
{
ISample sample1 = new ClassOne();
//将ISample类型的引用向下转型到ClassOne类型的引用
ClassOne clsOne =(ClassOne)sample1;
clsOne.Method();
}
}
from ClassOne.Method()
此处理解:
c#的类是放在堆上的,栈上放的只是类型,即占据内存的大小。因此向下转化时,只是改变了栈中的数据类型,然后将其重新指向堆中,所以没有数据损失。
相对于C++来说,类是放在栈中的,因此向下转换后数据是损失了的。
这里还需要明确一些概念:基类和派生类是对应的,接口类是抽象类。
实现多个接口
在C#中一个类不能从多个类继承,但可以实现多个基接口,而一个接口可以从多个接口继承。
顾名思义,实现多个接口就意味着,在实现类中要实现所有实现的接口规定的所有成员,也意味着对于同一个类,可以使用它实现的任意接口类型来引用它的实例。
结构
C#中结构类型和类(class)类型在语法上非常相似,它们都是一种数据结构,都可以包含数据成员和方法成员。但结构和类也存在着重要的差别。
- 结构是值类型,它在栈中分配空间;而类是引用类型,它在堆中分配空间,栈中保存的只是引用。
- 结构类型直接存储成员数据,让其他类的数据位于堆中,位于栈中的变量保存的是指向堆中数据对象的引用。
由于结构是值类型,并且直接存储数据,因此,在一个对象的主要成员为数据且数据量不大的情况下,使用结构会带来更好的性能。
委托
委托是C#中一个很棒的特性,它既有一定的灵活性,同时又是类型安全的。委托是事件的基础,而事件的应用非常广泛,因此,委托对于学习C#来说非常重要。
什么是委托
既然委托这么重要,那么究竟什么是委托呢?委托类似于C/C++中的函数指针。它能够引用函数,只不过在C#中,委托是一个对象,并且是类型安全的,避免了函数指针的不安全性。一个委托类型的变量可以引用一个或多个方法,这些方法由委托存放于一个调用列表中,当调用一个委托类型的变量(后文我们会解释为何委托类型的变量可以被“调用”)即相当于依次调用它的“调用列表”中的方法。
委托的定义和方法的定义类似,只是在返回值类型的前面多了一个delegate关键字,虽然形式上很像方法,但委托并不是方法,它是一种引用类型。下面就是一个委托示例:
public delegate void PrintDelegate(string content);
委托能引用的方法也是有限制的,这种限制体现在委托的签名里。
- 方法的签名要和委托一致,比如方法参数的个数和类型;
- 方法的返回值要和委托一致。
委托是引用类型
委托和类一样都是引用类型。事实上,每一个委托都默认继承自System.MulticastDelegate类,而后者是抽象类,它又继承自System.Delegate类。
委托的声明和实例化
既然委托是引用类型,那么要使用它,就必须像其他引用类型一样,需要先声明,然后实例化。需要注意的是:类实例化后叫做对象,而委托实例化后仍然叫做委托,因此,当我们看到“委托”这个词的时候,无法立即判断它是类型还是实例,需要结合上下文来判断。
关于委托的声明,有以下几点需要注意:
1)和类一样,委托的声明可以在类的声明外部,委托也可以声明在类中;
2)委托的声明虽然形式上与方法很像,但它没有方法主体,而是直接以分号结尾;
3)修饰符可以是:new、public、protected、internal、private;
4)delegate关键字后是本委托类型可以匹配的方法签名,尤其需要注意的是,它还包括方法的返回类型,但并非必须与方法名完全匹配,稍后我们会介绍委托中的“协变”和“逆变”。
为委托增删一个方法
public delegate void DoProcess(string msg);
DoProcess p1;
p1=sample.Process1;
p1+=sample.Process2;
p1-=sample.Process1;
委托中的协变和逆变
前面我们讲过委托对于它能引用的方法有一定的要求,但这个要求具有一定的灵活性,那就是协变和逆变,我们先来看看什么是协变和逆变:
- 协变:委托方法的返回值类型直接或间接地继承自委托签名的返回值类型,称为协变;
- 逆变:委托签名中的参数类型继承自委托方法的参数类型,称为逆变。
注意 协变强调的是返回类型,逆变强调的则是方法参数。
协变:
namespace ProgrammingCSharp4
{
//打印机类
public class Printer
{
}
//激光打印机类
public class LaserPrinter:Printer
{
}
//委托签名返回值为Printer类型
public delegate Printer PrintDelegate();
public class DelegateSample
{
//委托方法的返回值类型是Printer类的派生类
public static LaserPrinter PrintWithLaser()
{
return new LaserPrinter();
}
public static void Main()
{
//协变
PrintDelegate pd=PrintWithLaser;
Printer printer=pd();
}
}
}
逆变:
namespace ProgrammingCSharp4
{
//打印机类
public class Printer
{
}
//激光打印机类
public class LaserPrinter:Printer
{
}
//委托签名的参数类型为Printer类的派生类:LaserPrinter类型
public delegate void PrintDelegate(LaserPrinter laserPrinter);
public class DelegateSample
{
//委托方法的参数类型是Printer类
public static void Print(Printer printer)
{
return;
}
public static void Main()
{
PrintDelegate pd=Print;
//逆变
pd(new LaserPrinter());
}
}
}
匿名方法
匿名方法在本质上是一个传递给委托的代码块,它是使用委托的另一种方法。匿名方法的最大优势在于其减少了系统开销,方法仅在委托使用时才定义。在大多数情况下,我们并不希望声明一个仅作为参数传递给委托的独立方法。此时,直接给委托传递一段代码要比先创建一个方法后再把该方法传递给委托简单得多。
使用匿名方法需要遵守如下规则:
- 匿名方法中不能使用跳转语句跳至此匿名方法的外部,反之亦然;匿名方法外部的跳转语句也不能跳到此匿名方法的内部。
- 在匿名方法内部不能访问不安全的代码。另外,也不能访问在匿名方法外部定义的ref和out参数。但可以使用在匿名方法外部定义的其他变量。
namespace ProgrammingCSharp4
{
//委托类型
public delegate void PrintDelegate();
public class DelegateSample
{
public static void Main()
{
//匿名方法
PrintDelegate pd = delegate
{
System.Console.WriteLine("Printing……");
};
pd();
}
}
}
Lambda表达式
λ(Lambda)表达式和匿名方法基本相同,只是语法不同,可以说,λ表达式就是匿名方法。实际上,λ表达式就是从匿名方法演变而来,λ表达式的语法比匿名方法更加简洁:
(param)=>expr
其中,param是一个输入参数列表,expr是一个表达式或者一系列语句。
λ表达式具有的特性如下:
- 在一个具有唯一的显式类型参数的Lambda表达式中,圆括号可以从参数列表中删除,如:(parm)=>expr可以简写为:param=>expr;
- 当输入参数不唯一时,括号则不能省略;
- 输入参数列表中的各参数可以显式指定类型,也可以省略掉参数类型,具体类型通过类型推断机制判断;
- expr可以只包含一个计算表达式,也可以包含一系列语句,只是语句需要包括在大括号内。
expr为表达式的λ表达式
namespace ProgrammingCSharp4
{
//委托类型
public delegate string PrintDelegate(string content);
public class DelegateSample
{
public static void Main()
{
//λ表达式
PrintDelegate pd=(str)=>str+="\ndate:2010-1-1";
string result=pd("The quick brown fox jumps oyer a lazy dog.");
System.Console.WriteLine(result);
}
}
}
The quick brown fox jumps oyer a lazy dog.
date:2010-1-1
expr为语句的λ表达式
namespace ProgrammingCSharp4
{
//委托类型
public delegate void PrintDelegate(string content);
public class DelegateSample
{
public static void Main()
{
//λ表达式
PrintDelegate pd=(string str)=>
{
System.Console.WriteLine("Printing……");
System.Console.WriteLine("Content:{0}",str);
};
pd("The quick brown fox jumps oyer a lazy dog.");
}
}
}
事件
个人理解,事件就是通知数据变更的过程。
什么是事件
事件涉及两类角色:事件发布者和事件订阅者。当某个事件发生后,事件发布者会发布消息,事件订阅者会收到事件发生的通知,并做出相应处理。事件的触发可能源于用户和系统的交互(例如键盘按下、鼠标单击或者窗体加载等),也可能是由程序逻辑触发的。触发事件的对象称为事件发布者,捕获事件并对其做出响应的对象叫做事件订阅者。
具体来说,事件就是一对多的过程,当一个状态改变了,会触发多个函数调用。
事件和委托的关系
事件基于委托。
在事件触发以后,事件发布者要发布消息,通知事件订阅者进行事件处理,但事件发布者并不知道要通知哪些事件订阅者,这就需要在发布者和订阅者之间存在一个中介,这个中介就是委托。我们知道,委托包含一个调用列表,那么,只需要事件发布者有这样一个委托,各个事件订阅者将自己的事件处理程序都加入到该委托的调用列表中,那么,事件触发时,发布者只需要调用委托即可触发订阅者的事件处理程序。
如何声明事件
声明事件的语法和定义一个类的成员非常相似,也非常简单。事实上,事件就是类成员的一种,只是事件定义中包含一个特殊的关键字:event
。
一个事件的声明代码中,确实存在一个委托类型。其中,该“委托类型”部分可以自定义,既可以使用预定义的委托类型EventHandler,也可以使用自定义委托类型。
EventHandler是在BCL中预定义的委托类型,它位于System命名空间,用以处理不包含事件数据的事件。事件如果需要包含事件数据,可以通过派生EventArgs实现
,后文会有专门叙述。先来看下EventHandler委托的签名,可见它带有两个参数:
public delegate void EventHandler(Object sender,EventArgs e);
从EventHandler委托的签名可以得到如下信息:
- 委托的返回类型为void;
- 第一个参数——sender参数,它负责保存触发事件的对象的引用,因为参数的类型是Object类型,因此它可以保存任何类型的实例;
- 第二个参数——e参数,它负责保存事件数据,这里是在BCL中定义的默认的EventArgs类,它位于System命名空间中,它不能保存任何数据。
订阅事件
事件订阅者角色需要订阅事件发布者发布的事件,这样才能在事件发布时接收到消息并做出响应。我们知道,事件事实上是委托类型,因此事件处理方法必须和委托签名相匹配。
有了事件处理方法,就可以订阅事件了,只需要使用加法赋值运算符(+=)即可。假设名为eventSample的对象拥有一个名为PrintComplete的事件,那么订阅事件的代码如下:
eventSample.PrintComplete+=ShowMessage;
另外,还可以使用匿名方法或Lambda表达式进行事件订阅。
使用匿名方法:
eventSample.PrintComplete+=delegate(object sender,EventArgs e)
{
//……
};
使用Lambda表达式:
eventSample.PrintComplete+=(object sender,EventArgs e)=>
{
//……
};
C#和java比较:
java中使用的是接口。
C#使用委托机制,可以用时 + 运算符进行注册,直接多播。 而java中是一般是使用一个集合来保存观察者。
发布者(Publisher)
= 被观察者 (Observable)
= 事件源
(java中的EventObject,C#中的sender)订阅者(Subscriber)
=观察者(Observer)
= 接收者
(java中继承EventLister,接口,或Observer接口, C#由于委托机制,不需要继承接口,直接按EventHandler实现回调方法)
在发生其他类或对象关注的事情时,类或对象可通过事件通知它们。发送(或引发)事件的类称为“发布者”,接收(或处理)事件的类称为“订阅者”。
在典型的 C# Windows 窗体或 Web 应用程序中,可订阅由控件(如按钮和列表框)引发的事件。可使用 Visual C# 集成开发环境 (IDE) 来浏览控件发布的事件,选择要处理的事件。IDE 会自动添加空事件处理程序方法和订阅事件的代码。
EventHandler为C#中的预定义委托,专用于表示不生成数据的事件的事件的处理程序方法。
public delegate void EventHandler(Object sender,EventArgs e)
事件具有以下特点:
1.发行者确定何时引发事件,订户确定执行何种操作来响应该事件。
2.一个事件可以有多个订户。一个订户可处理来自多个发行者的多个事件。
3.没有订户的事件永远不会被调用。
4.事件通常用于通知用户操作(如:图形用户界面中的按钮单击或菜单选择操作)。
5.如果一个事件有多个订户,当引发该事件时,会同步调用多个事件处理程序。要异步调用事件,请参见使用异步方式调用同步方法。
6.可以利用事件同步线程。
7.在 .NET Framework 类库中,事件是基于 EventHandler 委托和 EventArgs 基类的。
using System;
namespace SimpleEvent
{
using System;
/***********发布者***********/
public class EventTest
{
private int value;
public delegate void NumManipulationHandler();
public event NumManipulationHandler ChangeNum;
protected virtual void OnNumChanged()
{
if ( ChangeNum != null )
{
ChangeNum(); /* 事件被触发 */
}else {
Console.WriteLine( "event not fire" );
Console.ReadKey(); /* 回车继续 */
}
}
public EventTest()
{
int n = 5;
SetValue(n);
}
public void SetValue( int n )
{
if ( value != n )
{
value = n;
OnNumChanged();
}
}
}
/***********订阅者***********/
public class subscribEvent
{
public void printf()
{
Console.WriteLine( "event fire" );
Console.ReadKey(); /* 回车继续 */
}
}
/***********主函数***********/
public class MainClass
{
public static void Main()
{
EventTest e = new EventTest(); /* 实例化发布者,第一次没有触发事件 */
subscribEvent v = new subscribEvent(); /* 实例化订阅者 */
e.ChangeNum += new EventTest.NumManipulationHandler( v.printf ); /* 注册 */
e.SetValue( 7 );
e.SetValue( 11 );
}
}
}
上述代码运行结果如下:
打印已完成……
模拟发送短消息……
元数据
所谓元数据(Metadata)就是描述数据的数据(Data about data)。
这些元数据包含了对模块或程序集中定义和引用的每个类型以及成员的说明,也包括引用的所有程序集在内。
访问元数据
要访问元数据,我们先介绍下Type类,Type是一个表示类型声明的抽象基类,Type的实例可表示以下任何类型:
- 类
- 值类型
- 数组
- 接口
- 指针
- 枚举
- 构造泛型类型和泛型类型定义
- 构造泛型类型、泛型类型定义和泛型方法定义的类型实参和类型形参要获取一个Type类型,有三种方法:
- 使用Object类型中定义的GetType()方法,每个类型都有此方法。
- 使用typeof运算符,返回一个Type对象。
- 使用Type抽象类中定义的静态GetType()方法。
要注意的是,typeof运算符不能应用于一个表达式,它只能使用一个类型作为参数。
using System;
namespace ProgrammingCSharp4
{
class MetadataSample
{
private static void Main()
{
//声明并实例化MetadataSample类型的变量
MetadataSample ms=new MetadataSample();
//使用从Object类型继承来的GetType()方法获取Type对象
Console.WriteLine(ms.GetType());
//使用typeof运算符获取Type对象,该操作符不能用于表达式
Console.WriteLine(typeof(MetadataSample));
//使用Type.getType()静态方法获取Type对象
Console.WriteLine(Type.GetType("ProgrammingCSharp4.MetadataSample"));
}
}
}
ProgrammingCSharp4.MetadataSample
ProgrammingCSharp4.MetadataSample
ProgrammingCSharp4.MetadataSample
从运行结果可见,这三种方式是等同的。只是需要注意的是,typeof运算符不能用于表达式,这一点尤其需要注意。
属性(Property)
C#属性是字段的扩展,它配合C#中的字段使用,用以构造一个安全的应用程序。
属性提供了灵活的机制来读取、编写或计算私有字段的值,可以像使用公共数据成员一样使用属性,但实际上它们是称做“访问器”的特殊方法,其设计目的主要是为了实现面向对象(Object Oriented, OO)中的封装思想。
根据该思想,字段最好设为private, 一个设计完善的类最好不要直接把字段声明为公有或受保护的,以阻止客户端直接进行访问,其中一个主要原因是,客户端直接对公有字段进行读写,使得我们无法对字段的访问进行灵活的控制,比如控制字段只读或者只写将很难实现。
—— 姜晓东《C# 4.0权威指南》-【9.4.5 属性】
声明和使用读/写属性(旧)
这种方式是C#中最基础的,也是最早出现的读写属性的方式。本文中我暂时称它为老式的读/写属性。
该方式允许我们在对属性读/写时,进行一些操作/计算。
声明属性
首先要声明一个私有字段,然后使用get访问器读取私有字段的值,使用set访问器为私有字段赋值。
示例代码如下:
class Person
{
private string _name = "N/A";
private int _age = 0;
// Declare a Name property of type string:
public string Name
{
get
{
return _name;
}
set
{
_name = value;
}
}
public int Age
{
get
{
return _age;
}
set
{
_age = value > 120 ? 120 : value;
}
}
}
使用属性
属性的使用很简单。外部可以直接对属性进行读取或赋值,就像使用类的公共字段一样。(注意:这里假设属性都是公共读写的,实际使用中要注意属性的可访问性)
//==========外部访问属性==========
Person person = new Person();
person.Name = "Bob";//为属性赋值
person.Age = 18;//为属性赋值
//读取属性的值
int age = person.Age;
备注
在属性的set方法中,value变量是很特殊的, 它代表用户指定的值。
自动实现的属性(新)
当属性访问器中不需要任何其他逻辑时,我们可以使用自动实现的属性,它会使属性声明更加简洁。
自动实现的属性在编译后,也是生成了老式的读/写属性。
VS中使用快捷键prop可以快速生成自动实现属性。
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
其他
自动实现的属性的本质
自动实现的属性在编译后,也是生成了老式的读/写属性。
这个是编译器自动帮我们做的,可以通过查看编译后生成的IL代码(又称作MSIL或CIL)来验证。
不过本人能力有限,就不分析了。这里推荐大家阅读 《C# 4.0权威指南》 中的【9.4.5 属性】一节,该章节详细分析了自动实现的属性经过编译后生成的IL代码。
属性初始化器
在 C# 6 和更高版本中,你可以像字段一样初始化自动实现属性:
public string FirstName { get; set; } = "Jane";
上述代码经过编译后,是在构造函数中,为属性赋值的。(来源)
只读属性默认初始化
在 C# 6 中,可以去掉set访问器,使属性变为只读属性。
public string Name { get; } = "hello world";
上述代码经过编译后,生成的属性关联字段是readonly的,并且仍然是在构造函数中为属性赋值的:private readonly string kBackingField;。(来源)
这种方式下,生成的属性是没有 setter 的(即使用反射,也无法设置值,setter 根本就不存在)。这个属性只能在构造函数中,或者结合特性赋值。(来源)
表达式体属性
自 C# 6 起,支持方法、运算符和只读属性的表达式主体定义。
自 C# 7.0 起,支持构造函数、终结器、属性和索引器访问器的表达式主体定义。
在 C# 6 中,可以把只读属性改写为表达式体的形式。
在 C# 7.0 中,可以把某个访问器改写为表达式体的形式。
只读属性的表达式体形式和属性(访问器)的表达式体形式是不冲突的,因为它们的使用场景不一样(写法也不一样)。
因为都是表达式体形式,它们具有相同的限制:要求方法体能够改写为lambda表达式(必须是单行代码)。
只读属性的表达式体形式(C#6)
只读属性的表达式体形式有2个限制:
- 只包含 get访问器。
- 要求 get访问器的方法体能够改写为lambda表达式(必须是单行代码)。
示例代码如下:
//C# 5
public string FullName
{
get
{
return FirstName + "" + LastName;
}
}
//C# 6
public string FullName => FirstName + "" + LastName;
我们可以通过VS的智能提示看到:该属性只有get访问器。
属性访问器的表达式体形式(C#7)
在 C# 7.0 中,对于老式的读/写属性,我们可以把get访问器或set访问器改写为表达式体(lambda)。
注意:要求访问器的方法体能够改写为lambda表达式(必须是单行代码)
示例代码如下:
//C# 5
private int _id;
public int Id
{
get
{
return _id;
}
set
{
_id = value;
}
}
//C# 7.0
private int _id;
//全部改写为表达式体
public int Id { get => _id; set => _id = value; }
//只改写set访问器
public int Id { get { return _id; } set => _id = value; }
//只改写get访问器
public int Id { get => _id; set { _id = value; } }
备注
下面的备注,对于老式的读/写属性和自动实现的属性都是通用的。
可以给访问器设置可访问性。例如把set访问器设为private,不允许外部直接赋值。通常是限制set访问器的可访问性。更多请参考:限制访问器可访问性(C# 编程指南)
可以省略掉某个访问器。通常是省略掉set访问器可使属性为只读。
另外,属性的本质是方法,所以接口中可以包含属性。
特性(Attribute)
这里讲的Attribute,翻译成中文也是“属性”的意思,但为了不与之前讲的“属性”相混淆,我们这里使用“特性”的名称以示区别。做个简单对比,例如,代码清单中的Name和Grade就是属性,它的作用是对字段的封装。
public class Student
{
//属性
public string Name
{
get;
set;
}
//属性
public string Grade
{
get;
set;
}
}
而特性的作用是:为类型、方法、属性添加附加的声明信息。在运行时可以使用反射技术获取这些声明信息,这在很多场合非常有用,如文档生成、O/R Mapping(对象关系映射,稍后会举一个O/R Mapping场景中应用的简单例子)。并非只有C#中有特性的概念,在Java中的对应技术为Annotation(JDK1.5时新引入),中文名为注解。
什么是特性
如果你使用过C++,或许对包含关键字(如public和private)的声明比较熟悉,这些关键字提供有关类成员的其他信息。另外,这些关键字通过描述类成员对其他类的可访问性来进一步定义类成员的行为。由于编译器被显式设计为识别预定义关键字,因此传统上您没有机会创建自己的关键字。但是,公共语言运行时允许您添加类似关键字的描述性声明(称为特性)来批注编程元素,如类型、字段、方法和属性。
为运行时编译代码时,该代码被转换为CIL(Common Intermediate Language,公共中间语言),并同编译器生成的元数据一起被放到可迁移可执行(Portable Executable,PE)文件的内部。特性使得我们可以向元数据中放置额外的描述性信息,并可使用运行时反射服务提取该信息。任何特性都是System.Attribute抽象类的直接或间接的派生类。因此,要创建一个自定义的特性,就需要从System.Attribute抽象类继承,或者派生自另一个特性。
.NET Framework出于多种原因使用特性并通过它们解决若干问题。特性描述如何将数据序列化,指定用于强制安全性的特性,并限制实时(Just-In-Time,JIT)编译器的优化,从而使代码易于调试。特性还可以记录文件名或代码作者,或在窗体开发阶段控制控件和成员的可见性。
可使用特性以几乎所有可能的方式描述代码,并以富有创造性的新方式影响运行时行为。使用特性可以向C#、Visual C++、Microsoft Visual Basic 2005或其他任何以运行时为目标的语言添加自己的描述性元素,而不必重新编写编译器。
按照约定,所有特性名都以Attribute结尾。但是,对于C#等以运行时为目标的语言,不要求指定特性的全名。例如,如果要应用HelpAttribute、DocumentAttribute特性,只需使用Help、Document作为特性名称即可。
特性本身也需要说明特性的元特性,可能不容易理解,这么说吧,特性本身可以应用到哪些对象(程序集、类等),特性是否允许在同一个对象多次使用,以及特性是否会在被修饰对象的派生类同样起作用等,这些都是通过元特性进行说明的,最常用的元特性是AttributeUsage。
特性(Attribute)是用于在运行时传递程序中各种元素(比如类、方法、结构、枚举、组件等)的行为信息的声明性标签。您可以通过使用特性向程序添加声明性信息。一个声明性标签是通过放置在它所应用的元素前面的方括号([ ])来描述的。
特性(Attribute)用于添加元数据,如编译器指令和注释、描述、方法、类等其他信息。.Net 框架提供了两种类型的特性:预定义特性和自定义特性。
规定特性(Attribute)
[attribute(positional_parameters, name_parameter = value, ...)]
element
特性(Attribute)的名称和值是在方括号内规定的,放置在它所应用的元素之前。positional_parameters 规定必需的信息,name_parameter 规定可选的信息。
预定义特性(Attribute)
.Net 框架提供了三种预定义特性:
- AttributeUsage
- Conditional
- Obsolete
AttributeUsage
预定义特性 AttributeUsage 描述了如何使用一个自定义特性类。它规定了特性可应用到的项目的类型。
规定该特性的语法如下:
[AttributeUsage(
validon,
AllowMultiple=allowmultiple,
Inherited=inherited
)]
其中:
- 参数 validon 规定特性可被放置的语言元素。它是枚举器 AttributeTargets 的值的组合。默认值是 AttributeTargets.All。
- 参数 allowmultiple(可选的)为该特性的 AllowMultiple 属性(property)提供一个布尔值。如果为 true,则该特性是多用的。默认值是 false(单用的)。
- 参数 inherited(可选的)为该特性的 Inherited 属性(property)提供一个布尔值。如果为 true,则该特性可被派生类继承。默认值是 false(不被继承)。
例如:
[AttributeUsage(AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Field |
AttributeTargets.Method |
AttributeTargets.Property,
AllowMultiple = true)]
Conditional
这个预定义特性标记了一个条件方法,其执行依赖于指定的预处理标识符。
它会引起方法调用的条件编译,取决于指定的值,比如 Debug 或 Trace。例如,当调试代码时显示变量的值。
规定该特性的语法如下:
[Conditional(
conditionalSymbol
)]
例如:
[Conditional("DEBUG")]
下面的实例演示了该特性:
#define DEBUG
using System;
using System.Diagnostics;
public class Myclass
{
[Conditional("DEBUG")]
public static void Message(string msg)
{
Console.WriteLine(msg);
}
}
class Test
{
static void function1()
{
Myclass.Message("In Function 1.");
function2();
}
static void function2()
{
Myclass.Message("In Function 2.");
}
public static void Main()
{
Myclass.Message("In Main function.");
function1();
Console.ReadKey();
}
}
In Main function
In Function 1
In Function 2
Obsolete
这个预定义特性标记了不应被使用的程序实体。它可以让您通知编译器丢弃某个特定的目标元素。例如,当一个新方法被用在一个类中,但是您仍然想要保持类中的旧方法,您可以通过显示一个应该使用新方法,而不是旧方法的消息,来把它标记为 obsolete(过时的)。
规定该特性的语法如下:
[Obsolete(
message
)]
[Obsolete(
message,
iserror
)]
其中:
参数 message,是一个字符串,描述项目为什么过时以及该替代使用什么。
参数 iserror,是一个布尔值。如果该值为 true,编译器应把该项目的使用当作一个错误。默认值是 false(编译器生成一个警告)。
创建自定义特性(Attribute)
.Net 框架允许创建自定义特性,用于存储声明性的信息,且可在运行时被检索。该信息根据设计标准和应用程序需要,可与任何目标元素相关。
创建并使用自定义特性包含四个步骤:
- 声明自定义特性
- 构建自定义特性
- 在目标程序元素上应用自定义特性
- 通过反射访问特性
最后一个步骤包含编写一个简单的程序来读取元数据以便查找各种符号。元数据是用于描述其他数据的数据和信息。该程序应使用反射来在运行时访问特性。我们将在下一章详细讨论这点。
声明自定义特性
一个新的自定义特性应派生自 System.Attribute 类。例如:
// 一个自定义特性 BugFix 被赋给类及其成员
[AttributeUsage(AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Field |
AttributeTargets.Method |
AttributeTargets.Property,
AllowMultiple = true)]
public class DeBugInfo : System.Attribute
在上面的代码中,我们已经声明了一个名为 DeBugInfo 的自定义特性。
构建自定义特性
让我们构建一个名为 DeBugInfo 的自定义特性,该特性将存储调试程序获得的信息。它存储下面的信息:
- bug 的代码编号
- 辨认该 bug 的开发人员名字
- 最后一次审查该代码的日期
- 一个存储了开发人员标记的字符串消息
我们的 DeBugInfo 类将带有三个用于存储前三个信息的私有属性(property)和一个用于存储消息的公有属性(property)。所以 bug 编号、开发人员名字和审查日期将是 DeBugInfo 类的必需的定位( positional)参数,消息将是一个可选的命名(named)参数。
每个特性必须至少有一个构造函数。必需的定位( positional)参数应通过构造函数传递。下面的代码演示了 DeBugInfo 类:
// 一个自定义特性 BugFix 被赋给类及其成员
[AttributeUsage(AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Field |
AttributeTargets.Method |
AttributeTargets.Property,
AllowMultiple = true)]
public class DeBugInfo : System.Attribute
{
private int bugNo;
private string developer;
private string lastReview;
public string message;
public DeBugInfo(int bg, string dev, string d)
{
this.bugNo = bg;
this.developer = dev;
this.lastReview = d;
}
public int BugNo
{
get
{
return bugNo;
}
}
public string Developer
{
get
{
return developer;
}
}
public string LastReview
{
get
{
return lastReview;
}
}
public string Message
{
get
{
return message;
}
set
{
message = value;
}
}
}
应用自定义特性
通过把特性放置在紧接着它的目标之前,来应用该特性:
[DeBugInfo(45, "Zara Ali", "12/8/2012", Message = "Return type mismatch")]
[DeBugInfo(49, "Nuha Ali", "10/10/2012", Message = "Unused variable")]
class Rectangle
{
// 成员变量
protected double length;
protected double width;
public Rectangle(double l, double w)
{
length = l;
width = w;
}
[DeBugInfo(55, "Zara Ali", "19/10/2012",
Message = "Return type mismatch")]
public double GetArea()
{
return length * width;
}
[DeBugInfo(56, "Zara Ali", "19/10/2012")]
public void Display()
{
Console.WriteLine("Length: {0}", length);
Console.WriteLine("Width: {0}", width);
Console.WriteLine("Area: {0}", GetArea());
}
}
定位参数和命名参数
特性可以与方法和属性相同的方式接受参数,它有两种类型的参数:定位参数和命名参数。使用参数可以向特性类的字段或属性赋值,由此改变一个特性实例的状态。但特性并非必须使用参数,它也可以没有任何参数。但是,如果有参数的话,则只能是这两种类型参数中的一种。
以HelpAttribute特性为例:
using System;
[AttributeUsage(AttributeTargets.Class)]
public class HelpAttribute:Attribute
{
public HelpAttribute(string url)
{
……
}
public string Topic{
get{……}
set{……}
}
public string Url{get{……}}
}
(1)定位参数:特性类中的每个公共非静态构造函数定义了一系列有序的定位参数。顺序很重要,定位参数和命名参数之间也是有顺序的,定位参数在前,不需要参数名,直接在参数的对应位置传入参数值即可。如代码清单所示,只有一个定位参数,没有使用命名参数。这里的定位参数对应于特性类HelpAttribute的公共构造函数:HelpAttribute(string url)。
[Help("http://www.mycompany.com/……/Class1.htm")]
class Class1
{
}
说白了,定位参数就是传入构造函数的参数。
(2)命名参数:特性类中的每一个非静态的公共可读写字段或属性都是一个命名参数,命名参数的形式为:“参数名=参数值”。注意这里的关键词——可读写,只读属性不能作为命名参数。可选择性地对命名参数赋值,如果有多个命名参数,它们之间的顺序可以随意调整。如代码清单中的Topic就是一个命名参数,它对应着HelpAttribute类中的Topic属性。
[Help("http://www.mycompany.com/……/Misc.htm",Topic="Class2")]
class Class2
{
}
说白了,就是不通过构造函数给私有变量赋值。
特性的参数类型
无论是定位参数还是命名参数,其参数类型都是有限制的,只能为以下类型:
- bool、byte、char、double、float、int、long、short、string;
- object类型和System.Type类型;
- 公共枚举类型,且枚举类型中内嵌的类型也是公共的;
- 上述类型组成的一维数组。
应用特性
要将一个特性应用到程序元素非常简单,只需以下步骤即可:
(1)在紧邻程序元素之前放置特性,从而将该特性应用于程序元素。在C#中,特性由方括号括起来,并且通过空白(可包括换行符)与元素分隔。
(2)为特性指定位置参数和命名参数。位置参数是必需的,并且必须放在所有命名参数之前;位置参数对应于特性的公共构造函数之中的参数。而命名参数是可选的,对应于特性的公共可读/写属性。在C#中,为每个可选参数指定name=value,其中name是属性的名称。
一般情况下,特性应用于它后面的程序元素。但是,你也可以显式地指定要将特性应用于方法还是参数抑或返回值。若要显式指明特性应用的程序元素,可以使用下面的语法:
[target:attribute-list]
其中target可能的值:
其中,方括号中是以逗号分隔的一个或多个特性的列表。
using System.Diagnostics;
public class App
{
[Sample1("DEBUG"),Sample2("TEST1"), [1] ]
public void Process()
{
}
}
要说明的是,特性列表中的各个特性顺序并不重要。
创建自定义特性
要创建自定义特性,需要以下几个步骤:
- 声明一个特性类,必须为公共类,另外,该类直接或间接地继承自System.Attribute类;
- 为该特性类应用AttributeUsageAttribute特性,由于该特性的作用是专用于定义其他特性,因此也可称之为“元特性”;
- 定义特性类的公共构造函数,构造函数的参数将作为特性的定位参数;
- 定义属性,属性必须可读写,不能为只读或只写。属性将作为特性的命名参数。
我们知道,特性类构造函数中的参数将作为特性的定位参数,一个类的构造函数可以有多个重载,这些重载的构造函数可以适应值的不同组合。如果同时为自定义特性类定义了属性,则在初始化特性时可使用命名参数和定位参数的组合。在通常情况下,将所有必选参数定义为定位参数,将所有可选参数定义为命名参数。在这种情况下,如果没有必选参数,则无法初始化特性。其他所有参数都是可选参数。
其中,我们重点关注AttributeUsageAttribute元特性,它用于指定另一特性类的用法。它有如下三个参数,其中一个定位参数和两个命名参数:
- 定位参数validOn,它是AttributeTargets枚举类型,该枚举包括了该特性可应用的所有可能元素的集。使用按位“或”运算可以组合多个AttributeTargets值,以获取所需的有效程序元素组合。
- 命名参数AllowMultiple,该命名参数指定能否为给定的程序元素多次应用所指示的特性。
- 命名参数Inherited,该命名参数指定所指示的特性能否由派生类和重写成员继承。
定位参数validOn位于特性类的公共构造函数中,每个公共构造函数都为该类定义一个有效的定位参数序列:
public AttributeUsageAttribute(AttributeTargets validOn)
{
this.m_attributeTarget=AttributeTargets.All;
this.m_inherited=true;
this.m_attributeTarget=validOn;
}
这里我们有必要说明一下AttributeTargets枚举类型,它的每个值都表示应用特性的应用程序元素,AttributeTargets枚举类型的值如表所示。
我们继续研究AllowMultiple命名参数,探讨它对于所修饰的特性有何影响。AllowMultiple属性指示元素中是否可存在特性的多个实例。如果设置为true,则允许存在多个实例;如果设置为false(默认值),则只允许存在一个实例。如代码清单所示,我们定义了一个特性MultipleEnableAttribute,设置AllowMultiple为true,意味着它可以在同一个程序元素应用多个实例。
接下来介绍Inherited定位参数,该参数用途为指示特性是否可由应用了该特性的类的派生类来继承。该属性的默认值为true。
反射(Reflection)
反射指程序可以访问、检测和修改它本身状态或行为的一种能力。
程序集包含模块,而模块包含类型,类型又包含成员。反射则提供了封装程序集、模块和类型的对象。
您可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型。然后,可以调用类型的方法或访问其字段和属性。
优点:
1、反射提高了程序的灵活性和扩展性。
2、降低耦合性,提高自适应能力。
3、它允许程序创建和控制任何类的对象,无需提前硬编码目标类。
缺点:
1、性能问题:使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此反射机制主要应用在对灵活性和拓展性要求很高的系统框架上,普通程序不建议使用。
2、使用反射会模糊程序内部逻辑;程序员希望在源代码中看到程序的逻辑,反射却绕过了源代码的技术,因而会带来维护的问题,反射代码比相应的直接代码更复杂。
反射(Reflection)的用途
反射(Reflection)有下列用途:
它允许在运行时查看特性(attribute)信息。
它允许审查集合中的各种类型,以及实例化这些类型。
它允许延迟绑定的方法和属性(property)。
它允许在运行时创建新类型,然后使用这些类型执行一些任务。
查看元数据
我们已经在上面的章节中提到过,使用反射(Reflection)可以查看特性(attribute)信息。System.Reflection
类的 MemberInfo
对象需要被初始化,用于发现与类相关的特性(attribute)。为了做到这点,您可以定义目标类的一个对象,如下:
System.Reflection.MemberInfo info = typeof(MyClass);
示例程序:
using System;
[AttributeUsage(AttributeTargets.All)]
public class HelpAttribute : System.Attribute
{
public readonly string Url;
public string Topic // Topic 是一个命名(named)参数
{
get
{
return topic;
}
set
{
topic = value;
}
}
public HelpAttribute(string url) // url 是一个定位(positional)参数
{
this.Url = url;
}
private string topic;
}
[HelpAttribute("Information on the class MyClass")]
class MyClass
{
}
namespace AttributeAppl
{
class Program
{
static void Main(string[] args)
{
System.Reflection.MemberInfo info = typeof(MyClass);
object[] attributes = info.GetCustomAttributes(true);
for (int i = 0; i < attributes.Length; i++)
{
System.Console.WriteLine(attributes[i]);
}
Console.ReadKey();
}
}
}
HelpAttribute
实例:
using System;
using System.Reflection;
namespace BugFixApplication
{
// 一个自定义特性 BugFix 被赋给类及其成员
[AttributeUsage(AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Field |
AttributeTargets.Method |
AttributeTargets.Property,
AllowMultiple = true)]
public class DeBugInfo : System.Attribute
{
private int bugNo;
private string developer;
private string lastReview;
public string message;
public DeBugInfo(int bg, string dev, string d)
{
this.bugNo = bg;
this.developer = dev;
this.lastReview = d;
}
public int BugNo
{
get
{
return bugNo;
}
}
public string Developer
{
get
{
return developer;
}
}
public string LastReview
{
get
{
return lastReview;
}
}
public string Message
{
get
{
return message;
}
set
{
message = value;
}
}
}
[DeBugInfo(45, "Zara Ali", "12/8/2012",
Message = "Return type mismatch")]
[DeBugInfo(49, "Nuha Ali", "10/10/2012",
Message = "Unused variable")]
class Rectangle
{
// 成员变量
protected double length;
protected double width;
public Rectangle(double l, double w)
{
length = l;
width = w;
}
[DeBugInfo(55, "Zara Ali", "19/10/2012",
Message = "Return type mismatch")]
public double GetArea()
{
return length * width;
}
[DeBugInfo(56, "Zara Ali", "19/10/2012")]
public void Display()
{
Console.WriteLine("Length: {0}", length);
Console.WriteLine("Width: {0}", width);
Console.WriteLine("Area: {0}", GetArea());
}
}//end class Rectangle
class ExecuteRectangle
{
static void Main(string[] args)
{
Rectangle r = new Rectangle(4.5, 7.5);
r.Display();
Type type = typeof(Rectangle);
// 遍历 Rectangle 类的特性
foreach (Object attributes in type.GetCustomAttributes(false))
{
DeBugInfo dbi = (DeBugInfo)attributes;
if (null != dbi)
{
Console.WriteLine("Bug no: {0}", dbi.BugNo);
Console.WriteLine("Developer: {0}", dbi.Developer);
Console.WriteLine("Last Reviewed: {0}",
dbi.LastReview);
Console.WriteLine("Remarks: {0}", dbi.Message);
}
}
// 遍历方法特性
foreach (MethodInfo m in type.GetMethods())
{
foreach (Attribute a in m.GetCustomAttributes(true))
{
DeBugInfo dbi = (DeBugInfo)a;
if (null != dbi)
{
Console.WriteLine("Bug no: {0}, for Method: {1}",
dbi.BugNo, m.Name);
Console.WriteLine("Developer: {0}", dbi.Developer);
Console.WriteLine("Last Reviewed: {0}",
dbi.LastReview);
Console.WriteLine("Remarks: {0}", dbi.Message);
}
}
}
Console.ReadLine();
}
}
}
当上面的代码被编译和执行时,它会产生下列结果:
Length: 4.5
Width: 7.5
Area: 33.75
Bug No: 49
Developer: Nuha Ali
Last Reviewed: 10/10/2012
Remarks: Unused variable
Bug No: 45
Developer: Zara Ali
Last Reviewed: 12/8/2012
Remarks: Return type mismatch
Bug No: 55, for Method: GetArea
Developer: Zara Ali
Last Reviewed: 19/10/2012
Remarks: Return type mismatch
Bug No: 56, for Method: Display
Developer: Zara Ali
Last Reviewed: 19/10/2012
Remarks:
异步编程和多线程编程
在平时的工作中,读者会经常遇到某些操作,诸如打印、下载等操作。这类操作都有一个共同点就是比较费时,都需要花费一定的时间才能完成。在程序中调用这类比较费时的代码时,调用方如果停在那里等待费时的代码执行完毕,无疑会严重影响程序的可操作性。例如,如果当打印或者下载进行时,如果我们只能等待不能做任何其他的事,甚至都不能取消打印或下载操作,那该是多么让人郁闷的事!再举个例子,现在很多应用程序将设置保存在配置文件中,那么当程序启动时,由于需要加载配置,然后利用这些配置数据进行一系列的初始化操作,但因为I/O读取操作稍慢,这将导致程序的主窗体不能立刻显示,给用户一种启动过程十分漫长的感觉,这显然不是一个好的用户体验。如今,实现某个功能已经不是最重要的事,大家都可以实现,但程序的可用性往往被用户给予了更多的关注,有时候甚至会对用户的采购决策起决定性的作用。这类问题,可以借助异步调用或者多线程编程模型轻松解决,如下是可能的解决方案,供大家参考:
- 把整个初始化处理放进一个单独线程,主线程启动此线程后继续执行其他操作,例如窗体绘制操作,当初始化配置数据的进行还在执行的同时,主窗体也快速地展现在用户眼前。虽然当前主窗体可能还不能完全可用,但给用户一种程序飞快运行的感觉,这种感觉无疑棒极了!
- 配置信息初始化线程此刻也在同步执行,将配置文件中的数据读取到内存,并根据配置对当前程序进行初始化。
截至目前,本书所给出的示例都是同步调用。什么是同步调用呢?就是程序按照在代码中既定的顺序,从开始一直顺序执行到结束。这也是有时候会造成之前所述的问题的根源所在,那么本章将致力于解决这一“影响用户心情”的问题。
进程和线程
什么是进程呢?在我们启动一个应用程序后,系统将会给它分配一定的内存以及其他的一些资源,这些划定的内存以及资源的物理分隔叫做进程。进程(process)是一块包含了某些资源的内存区域,操作系统利用进程把它的工作划分为一些功能单元。
线程究竟是什么呢?线程是系统分配处理器时间资源的基本单元,或者说是进程之内的独立执行的一个单元。对于操作系统而言,其调度单元是线程。一个进程至少包括一个线程,通常将该线程称为主线程。从另一个角度来说,线程是由进程创建的,由处理器使用的一个执行序列。
“应用程序”和“进程”分属两个不同的选项卡,这说明它们是不同的,一个应用程序可能包含一个或多个进程,每个进程都拥有自己独立的数据、执行代码以及系统资源。
理解进程和线程是进行异步编程的基础。目前为止,我们使用的都是同步编程的示例,所谓同步编程就是指,从第一条语句直到最后一条一句都是顺序执行。之前也讨论过,这在某些情况下会给用户造成困扰。例如,当在一个应用程序中向打印机发出了打印的指令,如果是同步模式,那么我们就不得不等待慢速的打印机执行完打印任务,此时应用程序是停止响应的,我们无法进行其他操作,也无法取消打印任务——这显然是不可接受的。
改进这一缺憾的方式,就是将同步编程改为异步编程。所谓异步编程,就是合理地利用多线程处理,从理论上讲,这些线程是“同时”执行的。这里的“同时”加了双引号,是因为当应用在一个单核处理器执行的时候,并不是真正地“同时执行”,而是操作系统会定期中断当前正在执行的线程,然后将CPU分配给等待队列中的另一个线程(这意味着任何一个线程都不能独占CPU),而具体每个线程所占用的CPU时间的长短取决于进程和操作系统。因为进程分配给每个线程的时间很短,以至于让我们产生“所有的线程是同时执行”的错觉。
BeginInvoke和EndInvoke
在C#中使用线程的方法很多,使用委托的BeginInvoke和EndInvoke方法就是其中之一。在讲这两个方法之前,先来大致看一下这两个方法的用法:
//声明一个委托类型
public delegate void PrintDelegate(string content);
//定义一个委托要代理的方法
public static void Print(string content)
{
Console.WriteLine(“打印中……\n{0}”,content);
System.Threading.Thread.Sleep(2000);
}
//将目标方法添加到委托的方法列表里去
PrintDelegate printDelegate=AsynchronousSample.Print;
//调用委托的BeginInvoke方法,开始异步调用委托列表中的目标方法
IAsyncResult result=printDelegate.BeginInvoke("Hello World!",null,null);
//调用委托的EndInvoke方法,等待异步调用结束
printDelegate.EndInvoke(result);
BeginInvoke方法可以使用线程异步地执行委托所指向的方法。需要特别强调的是,委托所代理的目标方法只能为1个。然后通过EndInvoke方法获得方法的返回值(EndInvoke方法的返回值就是被调用方法的返回值),或是确定方法已经被成功调用,这取决于采用何种异步编程的方法。
异步编程的4种方法
实现异步编程有4种方法可供选择,这4种方法实际上也对应着4种异步调用的模式,分为“等待”和“回调”两大类。
使用EndInvoke
当使用BeginInvoke异步调用方法时,如果方法未执行完,EndInvoke方法就会一直阻塞,直到被调用的方法执行完毕.
using System;
using System.Threading;
namespace ProgrammingCSharp4
{
class AsynchronousSample
{
//声明一个委托类型
public delegate void PrintDelegate(string content);
public static void Main()
{
int threadId = Thread.CurrentThread.ManagedThreadId;
PrintDelegate printDelegate = AsynchronousSample.Print;
Console.WriteLine("[主线程id:{0}]\t开始调用打印方法--", threadId);
IAsyncResult result = printDelegate.BeginInvoke("Hello World!", null, null);
printDelegate.EndInvoke(result);
}
public static void Print(string content)
{
int threadId = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine("[当前线程id:{0}]\t{1}", threadId, content);
System.Threading.Thread.Sleep(2000);
Console.WriteLine("[当前线程id:{0}]\t打印方法调用完毕.", threadId);
}
}
}
[主线程id:1]开始调用打印方法……
[当前线程id:3]Hello World!
[当前线程id:3]打印方法调用完毕.
多线程编程
Thread类
使用Thread类可以创建一个线程,它位于System.Threading命名空间。我们来看看Thread有哪些构造函数:
public Thread(ParameterizedThreadStart start)
public Thread(ThreadStart start)
public Thread(ParameterizedThreadStart start,int maxStackSize)
public Thread(ThreadStart start,int maxStackSize)
1)ParameterizedThreadStart:是一个委托类型,表示此线程开始执行时要调用的方法,支持向调用的方法传递一个参数。该委托的签名如下:
public delegate void ParameterizedThreadStart(object obj);
2)ThreadStart:是一个委托类型,表示此线程开始执行时要调用的方法没有参数的委托类型,不支持向调用的方法传递一个参数。该委托的签名如下:
public delegate void ThreadStart();
3)maxStackSize(int类型):表示线程要使用的最大堆栈大小,如果为0则使用可执行文件的文件头中指定的默认最大堆栈大小。从.NET Framework 4.0开始,只有完全受信任的代码可以将此值设置为大于默认堆栈大小(1MB)的值。如果在部分信任的情况下运行代码时为此值指定更大的值,则maxStackSize将被忽略,并且使用默认堆栈大小,这并不会引发异常。任何信任级别的代码可以将maxStackSize设置为小于默认堆栈大小的值。
PropertyInfo
属性定义:它提供灵活的机制来读取、编写或计算某个私有字段的值。 可以像使用公共数据成员一样使用属性,但实际上它们是称作“访问器”的特殊方法。 这使得可以轻松访问数据,此外还有助于提高方法的安全性和灵活性。属性通常可以分为常规属性和自动属性。两者之间还是有一点区别的,最开始编程对着两个全无概念。
属性PropertyInfo的使用
案例,如果两个类中有大部分的字段相同,需要将其中一个类的字段赋值给另外一个类:
//定义Person类:
public class Person {
public Person(int id,string name,string address)
{
this.Id = id;
this.Name = name;
this.Address = address;
}
public int Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
}
//定义User类
public class User {
public int Id { get; set; }
public string Name { get; set; }
public string Group { get; set; }
}
//转换方法
public static User ConvertObject(User user,Person person)
{
PropertyInfo[] userPro = user.GetType().GetProperties();
PropertyInfo[] personPro = person.GetType().GetProperties();
if (userPro.Length>0&&personPro.Length>0)
{
for (int i = 0; i < userPro.Length; i++)
{
for (int j = 0; j < personPro.Length; j++)
{<br> //判断User的属性是不是的Person中
if (userPro[i].Name == personPro[j].Name && userPro[i].PropertyType == personPro[j].PropertyType)
{
Object value=personPro[j].GetValue(person, null);
//将Person中属性的值赋值给User<br> userPro[i].SetValue(user,value , null);
}
}
}
}
return user;
}
//方法的调用
static void Main(string[] args)
{
Person person = new Person(1,"FlyElephant","北京");
User user = new User();
user.Id = 20;
user = ConvertObject(user, person);
Console.WriteLine("Id:" + user.Id + "Name:" + user.Name + "角色:" + user.Group);
System.Console.Read();
}