(转)C#学习入门基础
1 基本语法的学习
与法学习主要包含了以下几个方面:
(1) 基本的数据类型;
(2) 字符串;
(3) 控制语句;
(4) c#特殊的关键字;
(5) 异常处理机制;
1.1 基本的数据类型
比较特殊的几个类型有:decimal;非齐整数组;
定义常量的方式:
(1)常量
public const double gravitationalConstant = 6.673e-11;
不要和直接量混合了。
所谓的直接量其实就是直接给出一个数值,类似于汇编里头的立即数。
(2)变量的作用域;
这点和delphi稍微有点不同,在c#中,变量的作用域是按照大括号来划分的,比如:
public static void main()
{
int x=10;
if (x==10)
{
int y=100; //这个变量只在这个if语句内部有效
}
}
(3)类型转换
类型转换主要分为:自动类型转换和强制类型转换。
强制类型转换采用和java一样的方式;
int I=(int)(10/3);
也就是这个类型一定要用括号括起来。
自动类型转换则必须满足两个条件:格式兼容;目标类型的取值范围大于源类型。
1.2 数组
数组,c#中的数组是作为对象来实现的,所有具备了许多优点
数组的定义格式如下:
int[] testArray = new int[10];
数组可以在创建的时候初始化;
int[] nums = {100,101,102,103,104,105,106,108,109};
等同于
int[] nums = new int[] {100,101,102,103,104,105,106,108,109};
还等同于
int[] nums = new int[10] {100,101,102,103,104,105,106,108,109};
多维数组的声明,感觉有点怪异:
int[,] Two = new int[3,4];
int[,,] Three = new int[3,4,5];
非齐整数组;就是允许每一维的长度不同;
声明的格式如下:
int[][] jagged = new int[3][];
jagged[0] = new int[3];
jagged[1] = new int[4];
jagged[2] = new int[5];
这样的格式我好像也在java里头看到过,不过有点不确定了。
数组的length属性;
这里又分为一般的数组和非齐整数组,非齐整数组其实是数组的数组;
foreach循环;
在循环的过程中,迭代变量是只读的,因为不能通过迭代变量来改变数组的内容。
使用方法如下:
int[] temp = {3,4,5} ;
foreach(int x in temp)
{
Console.WriteLine(“temp[i] is : {0}”,x);
}
1.3 字符串
字符串的格式化输出;
Console.WriteLine(“My Name is {0}”,”Yangguichun”);
在c#中,字符串并不像在其他的语言中一样,是字符的数组,它是对象。我记得java中的字符串也是对象。毫无疑问,c#学的是java的那一套,因为将字符串作为对象在java中已经被验证是可行的,所以为什么不用呢。既然学习的是java的那一套,我估计java中给字符串提供的那一套方法在这里也是有的。
虽然字符串不是字符的数组,但是字符串还是允许使用如下的形式;
string s =” this is a string”;
Console.WriteLine(s[0]); //将输出t
不过却有点例外,就是不能够通过下标来给字符串赋值。因为据说一个字符串一旦创建,就不能修改了,是个常量。
字符串还允许使用==和!=,因为字符串类重载了这两个运算符。其他的运算符则和一般的应用一样,比较的是引用。
字符串一旦创建之后是不可改变的,如果要使用可改变的字符串,那么可以使用System.Text中的StringBuilder类。
在switch中可以使用字符串,这是所学过的其他语言中都不具备的功能,这个功能很棒。
2 面向对象特性的学习
面向对象特性的学习主要包含以下几个方面:
(1) 接口;
(2) 虚方法;
(3) 面向对象方面的特殊的关键字;
2.1 类、对象和方法
2.1.1 重载
首先来看一下重载,c#中的重载和delphi也有些不同,首先它并不需要overload关键字;其次一般方法的重载和构造函数的重载有所不同,重载的构造函数中的一个要调用另外一个的方法是:
class TestConstructor
{
public TestConstructor()
{
Console.WriteLine("Constructor 1");
}
public TestConstructor(string name):this()
{
Console.WriteLine("Constructor 2 "+ name );
}
}
而一般函数的方式则和delphi的相似。
class TestConstructor
{
public TestConstructor()
{
Console.WriteLine("Constructor 1");
}
public void TestOverload()
{
Console.WriteLine("overload 1");
}
public void TestOverload( string name )
{
Console.WriteLine( " before " );
TestOverload();
Console.WriteLine( " after " );
}
public static void Main()
{
TestConstructor test = new TestConstructor("3");
test.TestOverload("test");
}
}
则输出为:
before
Overload 1
after
2.1.2 ref和out关键字。
这两个关键字和delphi的var和out关键字相似,ref和var唯一的不同在于:如果某个方法的参数有ref,那么在调用该方法的时候传送进来的参数也要用ref来修饰。
此外,对象的因用也可以用ref来修饰。
2.1.3 可变元参数
(1)一般函数的变元
有些类似于delphi的format函数的那个参数的声明方式。
在这里采用的声明方式为:
public void TestParams(params int[] args)
{
for (int i = 0; i < args.Length; i++)
{
Console.WriteLine(args[i]);
}
}
这里给出的只是int类型的情况,其实和delphi的format函数的参数还不完全一样,如果要在一个数组中兼容各种类型,那么就要采用如下的声明方式。
class TestParams
{
public void Test(params object[] args)
{
for (int i = 0; i < args.Length; i++)
{
Console.WriteLine(args[i]);
}
}
}
(2)Main函数的变元
这个变元和一般函数的变元有所不同,格式如下:
public static void Main(string[] args)
{
for (int i = 0; i < args.Length; i++)
{
Console.WriteLine(args[i]);
}
}
2.1.4 函数的返回类型可以是数组
以前我所学过的其他的语言的函数都无法返回数组,但是c#可以,因为在c#中,数组是作为对象来实现的。
public int[] TestArray()
{
int[] result = new int[10];
for (int i = 0; i < result.Length; i++)
{
result[i] = i;
}
return result;
}
如果要返回多维数组,则用
public int[,] TestArray()
{
…
}
2.2 索引与属性
(1)索引
索引的主要用途就是支持那种受特殊条件限制的数组的创建。
声明的方式为:
public int this[int index]
{
get
{
if (index< MyArray.Length)
return MyArray[index];
else
return -1 ;
}
set
{
if (index<MyArray.Length)
MyArray[index] = value;
}
}
索引不仅有一维的,还有多维的,这是c#比delphi进步的地方。
(2)属性
这里的属性和delphi的属性其实是一回事,因为c#的设计者也正是delphi的设计者,所以他会把在设计delphi是的许多思想带到这里来。
这里的get和set就类似于delphi中的read和write;
和delphi一样,这里也允许只存在set和get只存在一个,这样就变成了只读或者只写的。
属性的定义格式如下:
class TestProperty
{
string FName;
public string Name
{
get
{
return FName;
}
set
{
FName = value;
}
}
}
2.3 继承
将到继承可能主要会想到以下几个方面的内容:
(1) 子类如何访问父类的成员变量;
(2) 虚方法的使用;
2.3.1 父类成员变量的访问
在c#中要访问父类的成员变量有两种方法:通过属性;通过protected关键字。
这和delphi当然有些类似,可是又有些不同,在delphi中子由于存在友元类型,所以如果父类和子类在同一个单元中,那么不管父类的成员变量是不是私有的,子类都可以访问。而在c#这样是不允许的。
2.3.2 虚方法的使用
在c#中,如果子类override了父类的方法,但是子类又想要调用父类的这个方法,格式如下:
base.MethodNamexxx,和delphi 大同小异,delphi中用的是inherited关键字。
2.3.3 Base关键字的使用
在c#中有this和base,在delphi中有self和inherited。这两个是一一对应的。
2.3.4 Sealed关键字
该关键字用来定义类,以防止该类被继承。这个和java中的finally是一样地,在java中也允许这种不允许被继承的类的声明。
Sealed和abstract是互斥的。
2.3.5 object关键字
理论上object只是System.Object的另一种表达方式,它是所有类的父类。
Object存在一个封箱和拆箱的操作;
所谓封箱指的是:将一个一般数据类型赋值给object变量,因为object是对象,所以如果把一个数值给他,就要经过一个一般类型到类的转换过程;
所谓拆箱指的是:将object类型的变量赋值给一个一般类型数据变量。
另外,由于object的存在,使得一个object类型的数组用来存储多种数据类型成为可能。就和delphi的Variable类型的效果一样。
例子如下:
class TestObject
{
object[] OArray = new object[7];
public TestObject()
{
for (int i = 0; i < 5; i++)
{
OArray[i] = i;
}
OArray[5] = "name";
OArray[6] = "age";
for (int i = 0; i < OArray.Length; i++)
{
Console.WriteLine(OArray[i]);
}
}
}
2.4 接口、结构和枚举
2.4.1 接口
接口定义了签名不定义实现,对我来说java,delphi,c#的接口功能都是大同小异的。我一向很少使用接口,所以也没法看出他们有什么不同。但是基于接口的编程早已经是大势所趋了,我要开始学习了。
在c#中,接口是允许有属性的。这里的属性也只是定义了签名不定义实现。来看一下定义的方式:
interface Person
{
int Age { get;set;}
}
还有更厉害的,c#中的接口还允许有索引。还是一样,只定义签名不定义实现。
interface Person
{
int this[int index]
{
set;
get;
}
}
如果一个类实现了多个接口,那么这些不同的接口可能会有一些方法的名字是相同的,此时就要采用所谓的显式实现 ,所谓显示实现格式如下:
interface ITestInterface
{
int cal(int x,int y);
}
class TestInterface : ITestInterface
{
int ITestInterface.cal(int x,int y ) //显示实现
{
return x * y;
}
}
显示实现的代码在类的实例中是不能直接调用的,因为它是私有的,但是可以通过借口的对象来调用。如以下的代码是合法的。
public static void Main(string[] args)
{
TestInterface temp = new TestInterface();
ITestInterface tempI = temp;
Console.WriteLine(tempI.cal(3,4));
}
在.net的框架中定义了大量的用户可以使用的接口。
2.4.2 结构(struct)
在c#中的结构和delphi的结构的声明就有些不同了,下面给出一个例子,很快就一目了然了。
struct Person
{
public string Name;
public int Age;
public string Tel;
public Person(string name, int age, string tel)
{
this.Name = name;
this.Age = age;
this.Tel = tel;
}
}
是不是很怪,他竟然还有一个构造函数。
而且使用的时候也是采用new的形式。
此外结构也可以使用属性;
这里要说明一下结构和类的区别:和类一样,结构可以有属性,有方法有构造函数,结构可以继承一个或多个接口;但是结构不能被继承也不能继承别的类和结构,结构的成员不能是abstract,virtual,protected。我觉得结构不能继承和被继承是他和类的最大的区别。
2.4.3 枚举
枚举没什么好说的,和delphi的差不多。
2.5 异常处理机制
System.Exception
四个关键字:Try catch throw finally.
捕捉了异常之后会有副作用:避免了程序的异常中止。
异常被处理之后就直接从系统中删除。
要捕捉所有的异常,只要使用一个没有参数的catch语句就可以了。
Try语句是允许嵌套的。
和java一样,throw是用来抛出异常的。
有的时候捕捉到异常不想处理,就可以重新抛出,这时只要在这个catch语句内简单条用一个throw就可以了。
自定义异常类其实没什么,十分的简单,和delphi中的大同小异,而且一般来说自定异常类并不需要附加什么功能,除了名字不同之外,其他基本相同。
C#有两个关于算术计算的关键字:checked和unchecked,所谓的checked就是说要检查某个赋值操作是否溢出,比如把1000赋值给一个byte类型的变量,那么就会产生溢出,而unchecked与checked相反,不检查这类溢出。
默认情况下是要检查溢出的。也就是会抛出溢出的异常。
2.6 事件
大概看了一下,c#中的事件的概念和delphi中的大同小异,其实也就是那天看的那个委托(delegate),还可以说是面向对象中的函数指针,但是c#中的这个函数指针比delphi的更加强大,他提供了多重委托的能力。也就是一个委托可以挂接多个方法实体,这样调用一次这个委托就同时调用了多个方法,我估计这个委托对象的内部有一个数组。
Delegate其实是一个类。我的理解是,当一个委托对象被赋值了一个方法引用之后,其实是把该方法引用存储在该委托对象内部的一个数组中,由于委托对象内部用来存放方法引用的是一个数组,所以可以允许多重委托的存在。
2.7 组织应用程序
(1) C#程序是通过名字空间来组织应用程序的。
(2) 动态连接库的一个用途是:软件产品的国际化,开发人员将依赖于各国语言的资源分离开来,各自放进专门的动态链接库中。各种不同的用户可以在安装和使用的使用调用不同的动态链接库。这样主程序就可以不经改变而在全球范围使用。
(3) 编译单元的using指示符只对本单元的名字空间和成员声明产生影响。
(4) 名字空间提供了一个逻辑上的层次体系结构。
(5) 装配用于应用程序的打包和部署。装配有两种形式:exe,dll。
两种编译的指令如下:
csc /target:library HelloWorld.cs //编译成dll
csc /reference: HelloWorld.cs //编译成exe
2.7.1 名字空间
(1)名字空间是可以嵌套的;
namespace N1
{
namespace N2
{
class Sample
{
}
}
}
等同于
namespace N1.N2
{
class Sample
{
}
}
(2)名字空间不能使用访问修饰符号来修饰,但是他其实默认采用了public修饰符。
(3)名字空间内的类型的使用。
假如存在如下代码:
namespace Myprogram.CSharp.FirstApp
{
class HelloWorld
{
}
}
那么在另外一个单元中要使用该HelloWorld类时,则应该采用Myprogram.CSharp.FirstApp.HelloWorld的形式,不过我们一般采用先导入名字空间的方式,也就是在这个单元的开始先:
using Myprogram.CSharp.FirstApp;
这样在后面要使用HelloWorld的时候,就只要直接使用名字就可以了.
2.7.2 using指示符
(1) 别名;
所谓别名就是为某个名字空间或者类型定义一个别名,这样在这个名字空间但中,凡是遇到这个别名指的就是那个名字空间或者类型。下面是一个简单的例子。
namespace N1.N2
{
class A
{ }
}
namespace N3
{
using A = N1.N2.A;
class B:A
{ }
}
上面的例子的第二部分还等同于:
namespace N3
{
using N = N1.N2;
class B: N.A
{ }
}
使用名别的时候有一点要注意:就是别名不能和本名字空间中的某一个类型或者别名同名。
别名在哪里声明就在哪里有效,离开了就无效。
(2)使用指示符
如果某个名字空间主体中定义的成员和名字空间using指示符导入的成员名字相同,则外部的成员被隐藏。
2.8 学习c#的流
一提起流就不可避免的想起java的流控,那真是。。。。。乱成马了。
和java的类似,c#的流控也牵涉到字节和字符,字节到字符、字符到字节的转换问题。
C#中的标准数据流是字符。
字符数据流逻辑上是独立的,但实际上是基于字节数据流的。他将默认字节数据刘转换为字符数据流,自动处理各种转换过程。
2.8.1 FileStream(用来读取字节的)
FileStream的一些相关信息;
FileMode.Append,FileMode.Create……….
FileAccess.Read ; FileAccess.Write ; FileAccess.ReadWrite
我觉得这样的形式特别好,编写程序的时候不会容易忘记名字。
这种基于字节的文件有一个好处就是可以用来读取任何类型的文件,用来复制拷贝任何类型的文件。
和java中不同,在java中如果要对一个文件进行随机存储,则要新建一个随机存储的类的对象,在c#中没有这么麻烦,直接用FileStream就可以了,该类提供了一个seek方法,使用这个方法就可以定为到文件的任何位置。
2.8.2 基于字符的文件i/o
对于想要存取unicode编码的字符的操作时,使用基于字符的数据流似乎就更加合适一些了。对于这样的文本,一不小心就可能造成乱码。
和java、delphi一样,c#中的流也是允许嵌套的。
比如允许字符流包装字节流,这样输出来的就是字符,当然反过来估计也可以。
2.8.3 重新定向标准数据流
这里提到的主要是console,他主要提供了一下三个方法:
SetIn(TestReader input)
SetOut(TestWriter output);
SetError(TextWriter output);
2.8.4 读写二进制数据
首先需要理解下面一点:
使用数据的内部二进制格式,而不是用户可读的文本格式来读些这些数据。
主要在调用了write之后,一般要调用flush(),才会真正的将数据写入到文件中。
在前面的各种流中,允许用户将各种数字类型的变量直接输出到终端,并且一般是以字符的形式来显示,可是c#并不提供反向操作。
还好,在c#中提供了一系列的类似于delphi中的strToInt这类的函数,不过在c#中组织的更为严密,比如要将一个字符串转换为一个32位的整数,那么就可以采用Int32.Parse来对这个字符串进行转换。
这里还要另外说明一点,由于c#和.net的紧密关系,所有的c#的数据类型都对应着.net框架中的一个类型,而.net中的类型是通过结构的形式来实现的。下面给出一个对应表:
.net |
C# |
Decimal |
decimal |
Double |
double |
Single |
float |
Int16 |
short |
Int32 |
int |
Int64 |
long |
UInt16 |
unshort |
UIint32 |
uint |
UInt64 |
ulong |
Byte |
byte |
SByte |
sbyte |
其实是否了解这个表没有关系,所有的parse都可以通过对应的c#类型来调用。
2.9 委托与事件
把事件作为单独的一章来学习,与他的重要性相比一点也不过分;
代理与事件是c#的第二个革新性质的特征。
所有的代理都是从System.Delegate 类派生出来的。
2.9.1 使用代理的两点好处
(1)代理支持事件;
(2)代理允许程序在运行时执行方法而无需确切知道编译时该方法是什么。
2.9.2 事件
什么事件呢?事件就是出现某种行为的自动通知。
和代理一样,事件也是允许多点传送的,所谓的多点传送就是允许一个事件对象挂接上多个事件处理函数。这样就允许多个对象来相应事件。
如果将一个静态方法作为事件处理方法,那么这个事件发生的时候将把事件通知到该类的所有对象。其实大多数的情况都只是使用实例方法。
除了一般的采用:
delegate void MyEventHandler(int Num);
class MyEvent
{
public event MyEventHandler SomeEvent;
public void OnSomeEvent()
{
if (SomeEvent != null)
SomeEvent(3);
}
}
这样的形式来定义事件以外,还可以采用下面的形式:
delegate void MyEventHandler(int Num);
class MyEvent
{
MyEventHandler[] e = new MyEventHandler[3];
public event MyEventHandler SomeEvent
{
add
{
e[0] = value;
}
remove
{
if (e[0] == value)
e[0] = null;
}
}
public void OnSomeEvent()
{
for (int i = 0; i < 3; i++)
{
if (e[i] != null)
e[i](i);
}
}
}
这就类似于属性的定义方式。
2.9.3 .net事件
void handler(object source, EventArgs arg) {//…}
2.10 运行时类型id、映射和属性
这其实又是.net的三个功能强大的特征。
2.10.1 三个关键字is、 as、 typeof
用as进行类型转换的时候即时无法转换也不会抛出异常,只会返回null而已。
其中typeof关键字要好好关注一下,这方面的东西我在学习java和delphi的时候没有留意。Typeof返回的是一个System.Type类型的对象,用该对象就可以获得许多关于那个类型的信息。
感觉这章好难啊。
2.10.2 使用映射
我尝试使用getMethods()的参数BindingFlags.DeclareOnly,可是一个方法也显示不出来。
阅读了一些关于反射的程序,暂时也不知道这些东西在实际中都有什么用,不过总体感觉他的反射功能比delphi的强大。常用的一些属性和方法有:
Type类的getMethods(),getConstructors()。
MethodInfo类的getParameters()方法。
Type类,ParameterInfo类,MethodInfo类,ConstructorInfo类。
使用不确定数目的参数
通过在声明方法时指定 params 关键字,C# 允许我们发送可变数目的参数。参数列表也可以包含普通参数,但是需要注意,用 params 关键字声明的参数必须放在最后。它采用可变长度数组的形式,并且每个方法只有一个 params 参数。
当编译器尝试解析一个方法调用时,它查找其参数列表与调用的方法相匹配的方法。如果找不到可以与参数列表匹配的方法重载,但是存在与适当类型的 params 参数匹配的版本,则会调用该方法,并将额外的参数放在一个数组中。
下面的示例对此进行了演示:
using System; public class ParamsClass { public static void Main(string[] args) { Average ("List One", 5,10,15); Average ("List Two", 5,10,15,20,25,30); } public static void Average (string title, params int[] values) { int Sum = 0; Console.Write("Average of {0}: ", title); for (int i = 0; i < values.Length; i++) { Sum += values[i]; Console.Write(values[i] + ", "); } Console.WriteLine(": {0}", (float)Sum/values.Length); } }
在上面的示例中,用整型数组中的 params 参数声明了方法 Average,让我们使用任何数目的参数来调用它。输出结果如下:
Average of List One: 5, 10, 15, : 10 Average of List Two: 5, 10, 15, 20, 25, 30, : 17.5
注意,如果我们希望允许不同类型的不确定参数,我们可以指定 Object 类型的 params 参数。
属性
在 C# 中,属性是类、struct,或接口的命名成员,它提供了一种简洁的途径,可以通过所谓的 get 和 set 访问器方法访问私有字段。
下面的代码片断为类 Animal 声明了一个名为 Species 的属性,它抽象了对名为 name 的私有变量的抽象访问:
public class Animal { private string name; public string Species { get { return name; } set { name = value; } } }
通常,属性与它访问的内部成员有相同的名称,但是属性以大写字母开头(例如上面的示例中的 Name),或者内部成员带有“_”前缀。同时还需要注意 set 访问器中所用的名为 value 的隐式参数 — 这种参数具有基础成员变量类型。
实际上,访问器在内部表示成 get_X() 和 set_X() 方法,从而与 .NET 语言保持兼容,因为 .NET 语言并不支持访问器(如本文前面的 typeOf 和 GetType() 部分中的屏幕截图所示)。一旦定义好属性,就可以非常容易地获取或设置它的值:
Animal animal = new Animal() // Set the property animal.Species = "Lion"; // Get the property value string str = animal.Species;
如果属性只有 get 访问器,它就是一个只读属性。如果它只有 set 访问器,它就是一个只写属性。如果两者都有,则它是一个可读写属性。
结构
C# 支持 struct 关键字,它是源于 C 的另一个项,但是不可用于 Java。可以将 struct 看作是一个轻量级类。它可以采用与类大致相同的方式包含构造函数、常量、字段、方法、属性、索引器、运算符和嵌套类型。structs 不同于类,因为它们不能是抽象的,并且不支持实现继承。结构与类还有一点重要的不同,结构是值类型的,而类是引用类型的。在构造函数的工作方式上,结构也有所不同。特别是,编译器总是提供默认的不带参数的构造函数,并且不允许您替换它。
在下面的示例中,我们使用 new 关键字并且通过初始化实例的成员初始化了一个 struct:
using System; public struct CustomerStruct { public int ID; public string name; public CustomerStruct(int customerID, string customerName) { ID = customerID; name = customerName; } } class TestClass { public static void Main(string[] args) { // Declare a CustomerStruct using the default constructor CustomerStruct customer = new CustomerStruct(); Console.WriteLine("Struct values before initialization"); Console.WriteLine("ID = {0}, Name = {1}", customer.ID, customer.name); customer.ID = 100; customer.name = "Robert"; Console.WriteLine("Struct values after initialization"); Console.WriteLine("ID = {0}, Name = {1}", customer.ID, customer.name); } }
当我们编译并运行上面的代码时,它的输出显示,该结构的变量在默认情况下是已初始化的。int 变量初始化为 0,而字符串变量初始化为空字符串:
初始化前的 struct 变量
ID = 0, Name =
初始化后的 truct 值
ID = 100, Name = Robert
注意,当我们使用另一种表示法(CustomerStruct customer)声明 customer 时,它的成员变量将不被初始化,因此,如果试图在为它们赋值前使用它们,将会产生编译时错误。
C# 中的数组
数组是具有相同数据类型的项的有序集合,通过数组名以及所期望的项相对于数组开始位置的偏移量可以访问数组。与 Java 相比,在 C# 中声明和使用数组的方式有一些重要的不同,我将在这一部分中对此进行介绍。
一维数组
一维数组以线性方式存储了固定数目的项,它仅仅需要一个索引值就可以确定任何一项。
在 C# 中,数组声明中的方括号必须紧跟数据类型,而不可以像在 Java 中一样出现在变量名的后面。因此,可以使用下面的语法来声明整数类型的数组:
int[] MyArray;
而下面的声明在 C# 中是无效的:
int MyArray[];
一旦声明了数组,就可以使用新的关键字来设置它的大小,这与 Java 中是完全一样的:
int[] MyArray; // declares the array reference MyArray = new int[5]; // creates a 5 element integer array
然后,我们就可以使用与 Java 完全相同的语法来访问一维数组中的元素,注意 C# 数组的索引也是从零开始的:
MyArray [4] // accesses the last element in the array
初始化
可以使用与 Java 相同的语法在创建时对数组元素进行初始化:
MyArray = new int[5] {1, 2, 3, 4, 5};
与 Java 不同,初始化器的数目必须与数组大小完全匹配。 我们可以利用这一特性在一行中声明和初始化 C# 数组:
int[] TaxRates = {0, 20, 23, 40, 50};
此语法创建了一个大小与初始化器的数目相等的数组。
程序循环中的初始化
C# 中初始化数组的另一种方法就是使用 foreach 循环。下面的循环将数组中的每个元素都设置为零:
int[] MyLittleArray = new int[5]; foreach (int i in MyLittleArray) { MyLittleArray[i] = 0; }
交错数组
C# 和 Java 都支持创建交错或者说非矩形的数组,其中的每一行都包含不同数目的列。例如,下面的交错数组的第一行有四项,而第二行有三项:
int[][] JaggedArray = new int[2][]; JaggedArray[0] = new int[4]; JaggedArray[1] = new int[3];
多维数组
C# 允许我们创建规则的多维数组,它可以看作是相同类型的值的矩阵。虽然 Java 和 C# 都支持交错的数组,但是 C# 还支持多维数组或数组的数组。我们将马上介绍交错数组。
使用下面的语法,我们可以声明一个多维矩形数组:
int[,] My2DIntArray; float[,,,] My4DFloatArray;
其中,My2DintArray 是可以借此访问每个元素的名称。
注意, int[][] My2DintArray; 行在 C# 中有不同的含义,我们很快就会明白这一点。
一旦声明了数组,我们就可以这样给它分配内存:
int[,] My2DIntArray; // declares array reference My2DIntArray = new int[5,4]; // allocates space for 5x4 integers
然后,可以使用下面的语法来访问数组的元素:
My2DIntArray [4,3] = 906;
因为数组是从零开始的,所以这将第四行第五列中的元素(右下角)设置为 906。
初始化
通过下面的任何一种方法,都可以在一行中创建、设置和初始化多维数组:
int[,] intArray = { {1,2,3}, {4,5,6} }; int[,] intArray = new int [2,3] { {1,2,3}, {4,5,6} }; int[,] intArray = new int [,] { {1,2,3}, {4,5,6} };
程序循环中的初始化
数组中所有的元素都可以使用嵌套的循环进行初始化,如下所示:
int[,] intArray = new int[5,4]; foreach (int i in intArray) { foreach (int j in intArray[]) { j = 0; } }
System.Array 类
在 .NET 中,数组是作为 System.Array 类的实例实现的。此类提供了几个有用的方法,例如 Sort() 和 Reverse()。
下面的程序说明了使用这几个方法是多么的容易。首先,我们使用 Array 类的 Reverse() 方法来使数组的元素反向,然后,我们使用 Sort() 方法对它们进行排序:
using System; public class ArrayMethods { public static void Main() { // Create string array of size 5 string[] EmployeeNames = new string[5]; Console.WriteLine("Enter five employee names:"); // Read 5 employee names from user for(int i=0;i<5;i++) { EmployeeNames[i]= Console.ReadLine(); } // Print the array in original order Console.WriteLine("\n** Original Array **"); foreach(string EmployeeName in EmployeeNames) { Console.Write("{0} ", EmployeeName); } //print the array in reverse order. Console.WriteLine("\n\n** Values in Reverse Order **"); System.Array.Reverse(EmployeeNames); foreach(string EmployeeName in EmployeeNames) { Console.Write("{0} ", EmployeeName); } //print the array in sorted order. Console.WriteLine("\n\n** Values in Sorted Order **"); System.Array.Sort(EmployeeNames); foreach(string EmployeeName in EmployeeNames) { Console.Write("{0} ", EmployeeName); } } }
下面是此程序的一些典型输出:
Enter five employee names: Luca Angie Brian Kent Beatriz ** Original Array ** Luca Angie Brian Kent Beatriz ** Values in Reverse Order ** Beatriz Kent Brian Angie Luca ** Values in Sorted Order ** Angie Beatriz Brian Kent Luca
继承和派生类
通过创建一个从现有类派生的新类,我们可以扩展现有类的功能。派生类继承了基类的属性,并且我们可以在需要时添加或重写方法和属性。
在 C# 中,继承和接口实现都通过 : 运算符来定义,这等同于 Java 中的扩展和实现。注意,基类应该一直在类声明的最左边。
同 Java 一样,C# 不支持多重继承,这意味着类不能从多个类中继承。然而,我们可以为此目的而采用与 Java 相同的方式使用接口,正如我们在下一部分中将看到的。
下面的代码定义了一个名为 Point 的类,它有两个私有成员变量 x 和 y,表示点的位置。这些变量可以分别通过名为 X 和 Y 的属性来访问:
public class Point { private int x, y; public Point() { x = 0; y = 0; } public int X { get { return x; } set { x = value; } } public int Y { get { return y; } set { y = value; } } }
我们将从 Point 类派生一个新类(比如说名为 ColorPoint),如下所示:
public class ColorPoint : Point
于是 ColorPoint 继承了基类的所有字段和方法,我们可以根据需要向派生类中添加新的字段和方法来提供额外的特性。在这个示例中,我们添加了一个私有成员和一些访问器来为点增加颜色:
using System.Drawing; public class ColorPoint : Point { private Color screenColor; public ColorPoint() { screenColor = Color.Red; } public Color ScreenColor { get { return screenColor; } set { screenColor = value; } } }
注意,派生类的构造函数隐式调用了基类(或 Java 术语中的超类)的构造函数。在继承中,所有的基类构造函数都是按照这些类出现在类层次中的顺序在派生类的构造函数之前调用的。
将类型强制转换到基类
与在 Java 中一样,我们不能使用对基类的引用来访问派生类的成员和方法,即使基类引用可能包含对派生类型对象的有效引用也同样如此。
我们可以通过隐式地引用派生的类型来引用派生类:
ColorPoint clrpt = new ColorPoint(); Point pt = clrpt;
在这段代码中,基类引用 pt 包含 clrpt 引用的副本。
base 关键字
通过使用 base 关键字,我们可以访问子类中的基类成员,即使这些基类成员在超类中被重写也同样如此。例如,我们可以创建一个派生类,该类所包含的方法具有与基类中相同的签名。如果我们在此方法前加上 new 关键字,就表示这是一个属于派生类的全新方法。通过 base 关键字,我们仍然可以提供方法来访问基类中的原始方法。
例如,我们的 Point 基类有名为 invert() 的方法,它交换 x 和 y 坐标。通过使用下面这样的代码,我们可以在派生类 ColorPoint 中提供此方法的替代方法:
public new void invert() { int holding = X; X = Y; Y = holding; screenColor = Color.Gray; }
正如您所见,该方法交换 x 和 y,然后将点的颜色设置为灰色。通过在 ColorPoint 中创建另一个方法(例如下面的这个方法),我们可以提供对此方法的基实现的访问:
public void baseInvert() { base.invert(); }
然后,我们就可以通过调用 baseInvert() 方法来调用 ColorPoint 对象中的基方法。
ColorPoint clrpt = new ColorPoint();clrpt.baseInvert();
请记住,如果我们将对基类的引用赋值给 ColorPoint 的实例,然后访问它的方法,我们将获得相同的效果:
Point pt = clrpt; pt.invert();
选择构造函数
基类对象总是在任何派生类之前构造的。因此基类的构造函数在派生类的构造函数之前执行。如果基类有多个构造函数,派生类就可以决定要调用的构造函数。例如,我们可以修改我们的 Point 类来添加第二个构造函数:
public class Point { private int x, y; public Point() { x = 0; y = 0; } public Point(int x, int y) { this.x = x; this.y = y; } }
然后,通过使用 base 关键字,我们可以将 ColorPoint 类更改为使用某个特定的可用构造函数:
public class ColorPoint : Point { private Color color; public ColorPoint(int x, int y) : base (x, y) { color = Color.Red; } }
在 Java 中,这项功能是通过 super 关键字来实现的。
方法重写
通过为声明的方法提供新的实现,派生类可以重写基类的方法。Java 和 C# 之间的一个重要区别在于,Java 方法在默认情况下标记为虚方法,而在 C# 中,必须使用 virtual 修饰符才能将方法显式标记为虚方法。可以采用大致相同的方式重写属性访问器以及方法。
虚方法
派生类中要被重写的方法是用 virtual 修饰符声明的。而在派生类中,已被重写的方法用 override 修饰符声明。
override 修饰符表示派生类的方法或属性,这个派生类代替基类中具有相同的名称和签名的类。要被重写的基方法必须声明为 virtual、abstract 或 override:以这种方式重写非虚方法或静态方法是不可能的 — 请参见关于此问题的下一部分。已被重写的方法或属性和重写方法或属性必须具有相同的访问级修饰符。
下面的示例显示了一个称为 StepUp 的虚方法,它是在派生类中用 override 修饰符重写的:
using System; public class CountClass { public int count; // Constructor public CountClass(int startValue) { count = startValue; } public virtual int StepUp() { return ++count; } } class Count100Class : CountClass { // Constructor public Count100Class(int x) : base(x) { } public override int StepUp() { return ((base.count) + 100); } public static void Main() { CountClass counter = new CountClass(10); CountClass bigCounter = new Count100Class(10); Console.WriteLine("Value of count in base class = {0}", counter.StepUp()); Console.WriteLine("Value of count in derived class = {0}", bigCounter.StepUp()); } }
当我们运行这段代码时,会发现派生类的构造函数使用基类中给出的方法体,这使得我们在不复制该代码的情况下就可以初始化 count 成员。下面是我们得到的输出结果:
Value of count in base class = 11 Value of count in derived class = 110
抽象类
抽象类将一个(或多个)方法或属性声明为抽象的。这样的方法并不具有声明它们的类中提供的实现,尽管抽象类也可以包含非抽象方法,也就是说,已经为其方法提供了实现。抽象类不能直接实例化,而只能作为派生类。这样的派生类必须为所有的抽象方法和属性提供实现(使用 override 关键字),除非派生成员本身被声明为抽象的。
下面的示例声明了一个抽象的 Employee 类。我们还创建了一个名为 Manager 的派生类,它提供了定义在 Employee 类中的抽象方法 show() 的实现:
using System; public abstract class Employee { // abstract show method public abstract void show(); } // Manager class extends Employee public class Manager: Employee { string name; public Manager(string name) { this.name = name; } //override the show method public override void show() { Console.WriteLine("Name : " + name); } } public class CreateManager { public static void Main(string[] args) { // Create instance of Manager and assign it to an Employee reference Employee temp = new Manager("John Chapman"); // Call show method. This will call the show method of the Manager class temp.show(); } }
这段代码调用了由 Manager 类提供的 show() 实现,并且在屏幕上打印出雇员的名字。
接口
接口是一种“主干类”,包含方法签名但是没有方法的实现。在这个方面,接口与抽象类一样,只包含抽象方法。C# 接口非常类似于 Java 接口,工作原理基本一样。
接口的所有成员都定义为公共成员,并且接口不能包含常量、字段(私有数据成员)、构造函数、析构函数或任何类型的静态成员。如果为接口的成员指定任何修饰符,编译器将会产生错误。
为了实现接口,我们可以从接口派生类。这样的派生类必须为所有接口的方法提供实现,除非派生类声明为抽象的。
接口的声明与 Java 完全一样。在接口定义中,通过单独使用 get 和 set 关键字,属性仅指示它的类型,以及它是只读的、只写的还是可读写的。下面的接口声明了一个只读属性:
public interface IMethodInterface { // method signatures void MethodA(); int MethodB(float parameter1, bool parameter2); // properties int ReadOnlyProperty { get; } }
用一个冒号来代替 Java 的实现关键字,类就可以继承此接口。实现类必须提供所有方法的定义以及任何必需的属性访问器:
public class InterfaceImplementation : IMethodInterface { // fields private int count = 0; private int ID; // implement methods defined in interface public void MethodA() { ... } public int MethodB(float parameter1, bool parameter2) { ... return integerVariable; } public int ReadOnlyProperty { get { return count; } } // add extra methods if required }
实现多个接口
通过使用下面的语法,一个类可以实现多个接口:
public class MyClass : interfacename1, interfacename2, interfacename3
如果一个类实现多个接口,则成员的名称会存在二义性,通过使用属性或方法名的完全限定符可以解决这个问题。换句话说,通过使用方法的完全限定名来指示它属于哪个接口(例如属于 IMethodInterface.MethodA),派生类可以解决这种冲突。
运算符重载
与 C++ 一样,C# 允许我们重载运算符,以供在我们自己的类中使用。这可能使得用户定义的数据类型看起来很自然,并且可以在逻辑上作为基本数据类型使用。例如,我们可以创建一个新的名为 Complex 的数据类型来表示一个复杂的数字,并且提供一些方法,以使用标准的算术运算符对这样的数字进行算术运算,例如使用 + 运算符来使两个复杂的数字相加。
为了重载一个运算符,我们编写了一个函数,它将需要重载的运算符的符号放在名称 operator 的后面。例如,我们可以这样来重载 + 运算符:
public static complex operator+(complex lhs, complex rhs)
所有的运算符重载都是类的静态方法。同时也需要注意,如果您重载等于运算符 (==),您还必须重载不等于运算符 (!=)。
可以重载的运算符的完整列表如下:
? |
一元运算符: +, -, !, ~, ++, --, true, false |
? |
二元运算符: +, -, *, /, %, &, |, ^, <<, >>, ==, !=, >, <, >=, <= |
下一个示例创建了一个 Complex 类,该类重载 + 和 - 运算符:
using System; public class complex { private float real; private float img; public complex(float p, float q) { real = p; img = q; } public complex() { real = 0; img = 0; } public void Print() { Console.WriteLine("{0} + {1}i", real, img); } // Overloading '+' operator public static complex operator+(complex lhs, complex rhs) { complex sum = new complex(); sum.real = lhs.real + rhs.real; sum.img = lhs.img + rhs.img; return (sum); } // Overloading '-' operator public static complex operator-(complex lhs, complex rhs) { complex result = new complex(); result.real = lhs.real - rhs.real; result.img = lhs.img - rhs.img; return (result); } } 此类允许我们使用代码来创建和操作两个复杂的数字,如下所示: using System; public class ComplexClass { public static void Main(string[] args) { // Set up complex numbers complex A = new complex(10.5f,12.5f); complex B = new complex(8.0f,4.5f); complex C; // Print object A and B Console.Write("Complex Number A: "); A.Print(); Console.Write("Complex Number B: "); B.Print(); // Add A and B, print result C = A + B; Console.Write("\nA + B = "); C.Print(); // Subtract A and B, print result C = A - B; Console.Write("A - B = "); C.Print(); } }
正如程序所演示的,我们现在可以很直观地对属于复杂类的对象使用加减运算符。下面是我们得到的输出:
Complex Number A: 10.5 + 12.5i Complex Number B: 8 + 4.5i A + B = 18.5 + 17i A - B = 2.5 + 8i
虽然 Java 在内部为字符串连接重载了 + 运算符,但是它并不支持运算符重载。
异常
C# 中的异常处理与 Java 非常相似。
在程序执行的过程中,无论什么时候出现了严重错误,.NET 运行库都会创建一个 Exception 对象来处理该错误。在 .NET 中,Exception 是所有异常类的基类。从 Exception 基类派生了两种类别的异常:System.SystemException 和 System.ApplicationException。System 命名空间中的所有类型都是从 System.SystemException 派生的,而用户定义的异常应该从 System.ApplicationException 派生,以便区分运行库错误和应用程序错误。一些常见的 System 异常包括:
? |
IndexOutOfRangeException — 使用了大于数组或集合大小的索引 |
? |
NullReferenceException — 在将引用设置为有效的实例之前使用了引用的属性或方法 |
? |
ArithmeticException — 在操作产生溢出或下溢时引发的异常 |
? |
FormatException — 参数或操作数的格式不正确 |
与 Java 中一样,当我们有容易引起异常的代码时,我们应该将此代码放在 try 块中。紧接其后的是一个或多个提供错误处理的 catch 块,并且我们还可以对任何我们想执行但又不知道是否引发异常的代码使用 finally 块。
注意:当使用多个 catch 块时,捕获异常的代码必须以升序的顺序放置,这样就只有第一个与引发的异常相匹配的 catch 块会被执行。C# 编译器会强制这样做,而 Java 编译器不会这样做。
C# 也与 Java 一样,catch 块并不需要参数;在缺少参数的情况下,catch 块适用于任何 Exception 类。
例如,当从文件中进行读取时,可能会遇到 FileNotFoundException 或 IOException,首先,我们需要放置更具体的 FileNotFoundException 处理程序:
try { // Code to open and read a file } catch (FileNotFoundException fe) { // Handle file not found exception first } catch (IOException ioe) { // Now handle any other IO exceptions } catch { // This block will catch all other exceptions } finally { // Executed whether or not an exception occurs, often to release resources }
通过从 Exception 派生,我们可以创建我们自己的异常类。例如,下面的代码创建了一个 InvalidDepartmentException 类,比方说,当某个部门的一个新雇员记录为无效时,我们可能引发该类。用户定义的异常的类构造函数使用 base 关键字来调用基类构造函数,并发送一个适当的消息:
public class InvalidDepartmentException : System.Exception { public InvalidDepartmentException(string Department) : base( "Invalid Department: " + Department){ } }
那么,我们可以用下面的代码引发这种类型的异常:
if (!(Department == "Sales" | Department == "Marketing")) { throw new InvalidDepartmentException(Department); }
注意,C# 不支持 checked 异常。在 Java 中,这些是使用 throws 关键字进行声明的,以便指定一个方法可能引发一个特殊类型的异常,此异常必须由调用代码进行处理。有关 C# 为什么不支持 checked 异常的更多信息,请参见 interview with Anders Hejlsberg。
高级 C# 技术
索引器
索引器提供了一种以与数组相同的方式访问类或结构的方法。例如,我们可能有表示我们公司内某个部门的类。这个类可以包含该部门中所有雇员的名字,而索引器可以允许我们访问这些名字,如下所示:
myDepartment[0] = "Fred"; myDepartment[1] = "Barney";
等等。在类的定义中,通过定义具有如下签名的属性,可以启用索引器:
public type this [int index]
然后,我们提供 get 和 set 方法作为标准属性,而当使用索引器时,这些访问器指定哪些内部成员被引用。
在下面的简单示例中,我们创建了一个名为 Department 的类,此类使用索引器来访问该部门的雇员(在内部表示为一个字符串数组):
using System; public class Department { private string name; private const int MAX_EMPLOYEES = 10; private string [] employees = new string [MAX_EMPLOYEES]; public Department(string deptName) { name = deptName; } public string this [int index] { get { if (index >= 0 && index < MAX_EMPLOYEES) { return employees[index]; } else { throw new IndexOutOfRangeException(); //return "Error"; } } set { if (index >= 0 && index < MAX_EMPLOYEES) { employees[index] = value; } else { throw new IndexOutOfRangeException(); //return "Error"; } } } // Other methods and properties as usual }
然后,我们可以创建这个类的一个实例,并且对其进行访问,如下所示:
using System; public class SalesDept { public static void Main(string[] args) { Department sales = new Department("Sales"); sales[0] = "Nikki"; sales[1] = "Becky"; Console.WriteLine("The sales team is {0} and {1}", sales[0], sales[1]); } }
有关索引器的更多信息,请参见 Indexer。
属性
为了增加有关类型的声明性信息,C# 引入一种叫做属性的新机制。有关类型的其他信息放在类型定义前面的声明性标记中。下面的示例向您展示了如何利用 .NET 框架属性来修饰一个类或方法。
在下面的示例中,通过添加 WebMethod 属性,GetTime 方法被标记为一个 XML Web 服务。
using System; using System.Web.Services; public class Utilities : WebService { [WebMethod] public string GetTime() { return DateTime.Now.ToShortTimeString(); } }
通过添加 CODE>WebMethod 属性,.NET 框架现在会自动处理调用此函数所必需的 XML/SOAP 交换。可以调用这个 Web 服务来检索下列值:
<?xml version="1.0" encoding="utf-8" ?> <string xmlns="http://tempuri.org/">7:26 PM</string>
在下面的示例中,通过添加 Serializable() 属性,Employee 类被标记为 Serializable。虽然 salary 字段被标记为 public,但是它不会被序列化,因为它是用 NonSerialized() 属性标记的。
using System; [Serializable()] public class Employee { public int ID; public string Name; [NonSerialized()] public int Salary; }
有关创建自定义属性的信息,请参见 Creating Custom Attributes。
委托 (Delegate)
C++、Pascal 和其他一些支持函数指针概念的语言都允许我们在运行时选择我们希望调用的函数。
Java 并没有提供任何具有函数指针功能的结构,但是 C# 却通过 System.Delegate 类提供了此功能。一个委托实例封装一个方法,它是一个可调用实体。
对于实例方法,委托由包含类的实例以及该实例中的方法组成。对于静态方法,可调用实体由一个类和此类中的静态方法组成。因此,委托可用于调用任意对象的函数,并且委托是面向对象、类型安全且可靠的。
在定义和使用委托时有三个步骤:
? |
声明 |
? |
实例化 |
? |
调用 |
我们可以用下面的语法声明一个委托:
delegate void myDelegate();
然后,就可以用这个委托来引用返回 void 而且不带任何参数的所有函数。
类似地,要为带有 string 参数并返回 long 的任何函数创建一个委托,我们应该使用下面的语法:
delegate long myDelegate(string mystring);
然后,我们就可以将该委托指派给具有此签名的任何方法,如下所示:
myDelegate operation = new myDelegate(methodName);
重新指派委托
委托对象是不可改变的,也就是说,与它们匹配的签名一旦设置就不能改变。然而,我们可以指向另一个方法,只要它们都具有相同的签名即可。例如:
delegate myDelegate(int a, int b) myDelegate operation = new myDelegate(Add); operation = new myDelegate(Multiply);
这里,我们将 operation 重新指派给一个新的委托对象,这样 operation 就可以调用 Multiply 方法。只有 Add() 和 Multiply() 都具有相同的签名时,我们才可以这样做。
调用委托
调用委托相当简单,只需要将委托变量的名称替换成方法的名称即可:
delegate long myDelegate(int i, int j); myDelegate operation = new myDelegate(Add); long lresult = operation(10, 20);
这里用值 10 和 20 调用 Add 方法,返回一个长整型结果,并将其赋给变量 lresult。
让我们创建一个程序来快速地演示委托的创建、实例化和调用:
using System; public class DelegateClass { delegate long myDelegate (int i, int j); public static void Main(string[] args) { myDelegate operation = new myDelegate(MathClass.Add); Console.WriteLine("Call to Add method through delegate"); long l = operation(10, 20); Console.WriteLine("Sum of 10 and 20 is " + l); Console.WriteLine("Call to Multiply method thru delegate"); operation = new myDelegate(MathClass.Multiply); l = operation(1639, 1525); Console.WriteLine("1639 multiplied by 1525 equals " + l); } } public class MathClass { public static long Add (int i, int j) { return (i+j); } public static long Multiply (int i, int j) { return (i*j); } }
我们会得到这样的输出:
Call to Add method through delegate Sum of 10 and 20 is 30 Call to Multiply method through delegate 1639 multiplied by 1525 equals 2499475
如前所述,委托实例必须包含对象引用。在上面的示例中,通过将方法声明为静态的(意味着我们自己不需要指定对象引用),我们避免了这样做。然而,如果委托引用一个实例方法,就必须给出对象引用,如下所示:
MathClass obj = new MathClass(); myDelegate operation = new myDelegate(obj.Power);
其中,Power 是 MathClass 方法的一个实例。因此,如果 MathClass 的方法没有声明为静态的,我们就可以通过委托来调用它们,如下所示:
using System; public class DelegateClass { delegate long myDelegate(int i, int j); public static void Main(string[] args) { MathClass mathObj = new MathClass(); myDelegate operation = new myDelegate(mathObj.Add); Console.WriteLine("Call to Add method through delegate"); long l = operation(10, 20); Console.WriteLine("Sum of 10 and 20 is " + l); Console.WriteLine("Call to Multiply method thru delegate"); operation = new myDelegate(mathObj.Multiply); l = operation(1639, 1525); Console.WriteLine("1639 multiplied by 1525 equals " + l); } }
当这些方法声明为 static 时,如果您运行此程序,您就会得到同前面一样的输出。
委托和事件
.NET 框架也将委托广泛应用于事件处理任务(像 Windows 或 Web 应用程序中的按钮单击事件)。虽然在 Java 中事件处理通常通过实现自定义侦听器类来完成,但是 C# 开发人员可以利用委托来进行事件处理。事件被声明为带有委托类型的字段,只是在事件声明前面加上 event 关键字。通常,事件被声明为公共的,但是任何可访问性修饰符都是允许的。下面的代码显示了委托和事件的声明。
public delegate void CustomEventHandler(object sender, EventArgs e); public event CustomEventHandler CustomEvent;
事件委托是多路广播的,这意味着它们可以具有对多个事件处理方法的引用。通过维护事件的注册事件处理程序列表,委托可以担当引发事件的类的事件发送程序。下面的示例向您展示了可以如何给多个函数预订事件。类 EventClass 包含委托、事件和调用事件的方法。注意,只能从声明事件的类中调用事件。然后,类 TestClass 就可以使用 +=/-= 运算符来预订/取消预订事件。当调用 InvokeEvent() 方法时,它会激发此事件,而已经预订此事件的任何函数都会同步激发,如下面的代码所示:
using System; class TestClass { static void Main(string[] args) { EventClass myEventClass = new EventClass(); // Associate the handler with the events myEventClass.CustomEvent += new EventClass.CustomEventHandler(CustomEvent1); myEventClass.CustomEvent += new EventClass.CustomEventHandler(CustomEvent2); myEventClass.InvokeEvent(); myEventClass.CustomEvent -= new EventClass.CustomEventHandler(CustomEvent2); myEventClass.InvokeEvent(); } private static void CustomEvent1(object sender, EventArgs e) { Console.WriteLine("Fire Event 1"); } private static void CustomEvent2(object sender, EventArgs e) { Console.WriteLine("Fire Event 2"); } } public class EventClass { public delegate void CustomEventHandler(object sender, EventArgs e); //Declare the event using the delegate datatype public event CustomEventHandler CustomEvent; public void InvokeEvent() { CustomEvent(this, EventArgs.Empty); } }
此程序的输出如下:
Fire Event 1 Fire Event 2 Fire Event 1
垃圾回收
在 C 和 C++ 中,许多对象一旦声明,就需要编程人员在对象可以安全使用之前给它们分配资源。在对象使用完资源之后,将这些资源释放回自由内存池也是编程人员的责任。如果资源没有释放,当越来越多的资源被不必要地消耗时,就可以说代码泄漏内存。而在另一方面,如果资源过早地释放,则可能会发生数据丢失、其他内存区域破坏和 null 指针异常。
为了防止这些危险的发生,Java 和 C# 都通过一个应用程序来独立地管理所有对象的生命周期。
在 Java 中,JVM 通过跟踪所分配资源的引用来处理未使用的内存的释放。只要 JVM 检测到一个资源不再被一个有效的引用加以引用,该资源就会被当作垃圾回收。
在 C# 中,通过具有与 JVM 类似功能的公共语言运行库 (CLR) 来处理垃圾回收。CLR 垃圾回收器周期性检查内存堆,以查看是否有未引用的对象,并释放这些对象所占用的资源。有关垃圾回收的更多信息,请参见 Garbage Collection:Automatic Memory Management in the Microsoft .NET Framework, 和 Garbage Collection—Part 2: Automatic Memory Management in the Microsoft .NET Framework。
安全代码和不安全代码
C# 的一个特别值得注意的特性是它支持非类型安全代码。通常,CLR 负责监视 IL(Intermediate Language,中间语言)代码的行为,并阻止任何有问题的操作。然而,有时我们希望直接访问低级功能(例如 Win32 API 调用),只要我们负责保证这样的代码操作正确,我们就会被允许这样做。这样的代码必须放在源代码的不安全块中。
unsafe 关键字
进行低级 API 调用、使用指针算法、执行一些其他不安全操作的 C# 代码必须放在用 unsafe 关键字标记的块中。下面任何一种情况都可以标记为 unsafe:
? |
一个完整的方法 |
? |
一段放在括号中的代码块 |
? |
一个单独的语句 |
下面的示例演示了上面所有三种情况中 unsafe 的使用:
using System; class UnsafeClass { unsafe static void PointyMethod() { int i=10; int *p = &i; Console.WriteLine("*p = " + *p); string address = "Pointer p = " + int.Format((int) p, "X"); Console.WriteLine(address); } static void StillPointy() { int i=10; unsafe { int *p = &i; Console.WriteLine("*p = " + *p); string address = "Pointer p = " + int.Format((int) p, "X"); Console.WriteLine(address); } } static void Main() { PointyMethod(); StillPointy(); } }
在这段代码中,整个 PointyMethod() 方法被标记为 unsafe,因为该方法声明并使用了指针。与这段代码一样,一旦某个代码块再次使用指针,StillPointy() 方法就将其标记为 unsafe。
有关不安全代码的更多信息,请参见 Unsafe at the Limit。
fixed 关键字
在安全代码中,垃圾回收器在其生命周期内可以相当自由地移动一个对象,它的任务是组织和压缩可用资源。然而,如果我们的代码使用指针,这种行为就可能很容易导致意外的结果,因此,我们可以使用 fixed 关键字来指示垃圾回收器不要移动特定的对象。
下面的代码展示了如何使用 fixed 关键字来确保在 PointyMethod() 方法中的代码块执行期间系统不会移动数组。注意,fixed 仅仅使用在不安全代码中:
public class FixedClass { public static void PointyMethod(char[] array) { unsafe { fixed (char *p = array) { for (int i=0; i<array.Length; i++) { Console.Write(*(p+i)); } } } } static void Main () { char[] array = { 'H', 'e', 'l', 'l', 'o' }; PointyMethod(array); } }
小结
虽然 Microsoft 和其他厂商都已经为 .NET 平台引入了许多语言,但是 C# 是非常类似 Java 的一种语言,并且它非常适合希望从 J2EE 迁移到 .NET 平台的开发人员。
本文比较和对比了这两种语言。在许多方面,C# 具有 C++ 的强大功能、Java 的简洁优美,以及 Visual Basic 开发的简易性,我希望本文已经说明了这一点。