C#语法笔记(一)

来源:互联网 发布:java开源报表框架 编辑:程序博客网 时间:2024/05/16 06:39

C#中变量的命名:

在C#中,标识符是区分大小写的。推荐的命名约定建议如下:

对于变量名和参数名,使用Camel大小写规则,该规则要求除了第一个单词外,其他单词的首字母大写,如bookTitle;

对于方法名和其他标识符,使用Pascal大小写规则,该规则要求每个单词的首字母都大写,如GetBookTitle;


C#数据类型:

1、值类型

(1)简单类型,如int  double  char 

(2)枚举类型,enum E

(3)结构类型,struct  S

(4)可以为null的类型,其他所有具有null值的类型的扩展

2、引用类型

(1)类类型

A、其他所有类型的最终基类,object

        B、Unicode字符串类型 string

        C、class  C形式的用户自定义类型

(2)接口类型  Interface

(3)数组类型  一维数组和多维数组

(4)委托类型  delegate  int  D


实现值类型和引用类型之间的转换: 装箱和拆箱

装箱:将一个值类型数据隐式或显示转换为object类型数据,或者把这个值类型转换成一个被该值类型应用的接口类型。

拆箱:将一个对象类型显示转换成一个值类型,或将一个接口类型显示的转换成可执行该接口的值类型。


C#数据的输入输出(在Console程序中),使用函数Console.ReadLine(), Console.WriteLine()等。

int  a = Convert.ToInt32(Console.ReadLine());  //将console输入的字符串,转换为int32形式

Console.WriteLine(a + "hello"); //将a转换为字符串,同时加上另一个字符串,输出


可以为null的类型:  T?

可以为null的类型表示可赋值为null值的类型变量,其取值范围为其基础值类型正常范围内加一个null值。如bool? (Nulllable<bool>  )的值包括true,  false,  null。

可以为null类型的赋值, int?  A = 10


类型转换:

隐式转换、显式转换、Convert.Toxx函数


在C#代码中,如果在未检查的上下文中执行,整型间的显式数字转换或算术运算可能造成溢出,从而导致错误。使用checked关键字进行类型检验。

int  j = checked(i + 1);  //检测是否会溢出

可以在编译器中设置/checked 或/unchecked 进行全局检验。


关系和类型测试运算符

==, >=, <= , >, <, != 等传统的比较运算符, x is T,  x  as  T

(1)关系运算符的优先级相同;

(2)对于两个预定义的数值类型,关系运算符按照操作数的大小进行比较

(3)对于string类型,关系运算符比较字符串的值,即按照ascii从左到右一一比较,直到出现不同的字符,进行比较大小。

(4)对于string类型之外的引用类型,如果两个操作数引用同一个对象,则==返回相等;否则返回  !=

(5)int和System.Int32是相同的数据类型。

x  is  T: 判断数据x是否属于类型T

x  as  T: 尝试将x转换为类型T, 如果成功,返回转换为T类型的x, 否则返回null.


逻辑运算符

!,  &,  | , ^, &&, || 

! 逻辑非, & 逻辑与, | 逻辑或

&& 短路与:  A&&B 如果A 为false,则不必计算B 的布尔值,直接判定为false

|| 短路或:   A || B, 如果A 为 true, 则不必计算B的布尔值,直接判定为true


字符串运算符

字符串运算符只有一个  +, 表示将两个字符串连接,当其中一个操作数是字符串类型或两个操作数都是字符串类型时,则+ 运算符执行字符串连接操作。


位运算

~  按位求补,  <<  操作数左移, >> 操作数右移, & 按位逻辑与, | 按位逻辑或, ^ 按位逻辑异或

(1)按位求补运算~ 是为 int、uint、long和ulong类型预定义的,对操作数执行求补操作,其效果相当于反转每一位。

(2)左右移操作符的第二个操作数必须为int 类型


程序for循环结构

可以使用foreach 进行循环操作;

foreach( 类型名称   变量名称  in  数组或集合名称)

{   操作}


异常处理

try{  尝试执行可能触发异常的代码段 }    

catch( xxException e)

{  在异常发生时执行的代码}

finally{ 最终必须执行的代码(即时发生异常), 如释放资源等}


数组和指针

C#中数组的属性;

(1)数组使用类型声明,通过数组下标(或索引)来访问数组中的数据元素

(2)数组可以是一维数组、多维数组或交错数组

(3)数组元素可以为任意类型,包括数组类型

(4)数组下标(索引)从0开始

(5)数组元素的默认值为0, 引用类型的默认值为null

(6)交错数组是数组的数组,因此它的元素是引用类型,默认值为null

(7)数组中的元素总数是数组中各维度长度的乘积

(8)通过.NET框架中的System.Array类来支持数组。因此,可以利用该类的属性和方法来操作数组

使用数组时需要注意;

(1)数组必须先声明,因为数组为引用类型,数组变量的声明只是为数组实例的引用留出空间。

   声明一维数组:int [] A;

(2)数组在声明后必须实例化才能使用。数组实例在运行时使用new 运算符动态创建,new运算符指定新数组实例的长度,new运算符自动将数组的元素初始化为相应的默认值。

(3)初始化例子:int []A = new int[3]{1, 3, 5} ,简化为   int [] A = {1, 3, 5}; 一维数组,含有三个元素,

一旦要为数组指定初始化值,就必须为数组的所有元素指定初始化值,指定值的个数必须严格等于数组的长度。

一维数组: int [] A;  A = new  int[4];    int []A = new int[3]{1, 3, 5} ,简化为   int [] A = {1, 3, 5}; 

多维数组 : int [ , ]  A = new int[4, 3];   int [ , ,] B = new int[3, 5, 2] //三维数组 

int [, ]array = new int[2, 2]{ {1, 2},  {3, 4}};                int [, ]array = new int[ , ] {{1, 2}, {3, 4}};    int [ , ] array = {{1, 2}, {3, 4}}

交错数组:   交错数组是元素为数组的数组,数组的数组。交错数组的元素的维度和大小可以不同,交错数组同样需要声明、实例化并且初始化后才能使用。

int [][] A = new int[3][];  A[0] = new int[5];   A[1] = new int[3];   A[2]  = new  int[4];

int [][] A = new int[] [] 

{  new int[]{1, 2, 3},  new int[]{5, 3, 2},  new int[] {3, 2}}

例:混合使用多维数组和交错数组 int [] [ , ] A =  new int[3] [ , ] { new int[ , ]{{1, 2}, {2, 3}},  new int [ , ]{{1, 1}, {2, 2}},  new int [ , ] {{ 3, 4}, {5,6}}}

作为对象的数组::

C#中,数组实际上是对象,数组类型是从Array类派生而来的引用类型。 Array类是所有数组类型的抽象基类型。System.Array类提供了许多方法和属性,可用户数组的复制、排序等操作处理。如Clear(),  Clone(), CopyTo(), Copy() , GetLength()等


为了保证类型安全,默认情况下,C#不支持指针,从而避免了程序的复杂性。但在某些情况下,可能需要通过指针调用操作系统API函数、访问内存映射设备,或实现一些以时间为关键的算法。C#中使用unsafe关键字,可以定义不安全上下文,然后在不安全上下文中使用指针。

在公共语言运行时(CLR)中,不安全代码是无法验证的代码,其安全性无法由CLR进行 验证,使用不安全代码可能会引起安全风险和稳定性风险,程序员必须确保代码不会引起安全风险或指针错误。  CLR只对在完全受信任的程序集中的不安全代码执行操作,在C#中,为了编译不安全代码,必须使用/unsafe 编译应用程序。 可以将一个类、一个方法、代码块或字段标记为 unsafe, 如 

public unsafe void A()

int *  p: p是指向整数的指针;  int **p:p是指向整数的指针的指针;  int * [] p p是指向整数的指针的一维数组 ,但没有指向数组的指针。

(1)指针不能指向对象引用或会包含引用的结构,因为即使有指针指向对象引用,gia对象引用也可能会被执行垃圾回收,从而造成指针指向的内存空间包含的数据类型无效。

(2)void *类型表示指向未知类型的指针。因为目标类型是未知的,所以不能对void*类型的指针应用间接寻址运算符*, 也不能对这样的指针执行任何算术运算。但是void* 类型可以强制转化为其他任何类型的指针,反之亦然。

(3)指针可以为null,值为null 的指针表示指针没有指向有效的内存地址,针对null指针的操作将会出现异常。

(4)指针类型是单独的类型,与值类型或引用类型不同,指针类型不从object继承,而且不存在指针类型和object之间的转换。具体而言,指针不支持装箱和拆箱操作,但是允许不同指针类型之间以及指针和整型之间进行转换。

(5)指针分配内存,使用stackalloc。 stackalloc关键字用于不安全的代码上下文中,以便在堆栈上分配内存块 int *fib = stackalloc   int[100];

(6)关于fixed语句。

a) fixed语句只能出现在不安全的上下文中。

b)fixed语句禁止垃圾回收器重定位可移动的变量

c)fixed语句还可用于创建固定大小的缓冲区

d)fixed语句设置指向托管变量的指针并在statement执行期间钉住该变量。如果没有fixed语句,则指向可移动托管变量的指针的作用很小,因为垃圾回收可能不可预知的重定位变量,C#编译器值允许在fixed语句中分配指向托管变量的指针

e)无法修改在fixed语句中初始化的指针。


类和对象

类是C#语言的核心,C#中的一切类型都是类,所有的语句必须在类内。.NETFramework类库包含大量解决通用问题的类,一般可以通过创建自定义类和使用.NETFramework类库来解决实际问题。

创建对象:使用new运算符创建类的实例,该运算符为新的实例分配内存,调用构造函数初始化该实例,并返回对该实例的引用。类名    对象名    =  new   类名([参数表])

对象使用:类的对象使用 ”."操作符来引用类的成员。引用时,必须符合权限。

对象比较:用new创建一个类的对象时,将在托管堆中为对象分配一块内存,每一个对象都有不同的内存。代表对象的变量存储的是存放对象内存的地址。因此两个不同的对象,即时他们的所有成员的值或代码都相同,他们也是不相等的。但是,如果将一个对象赋值给另一个对象,则他们的变量都将保存同一块内存的地址,即两个对象是相同的。如果改变其中一个对象的状态(成员的值),那么也会影响另一个对象。

访问修饰符

1)public: 访问不受限制

2)protected:访问仅限于此类或从此类派生的类

3)internal:访问仅限于此程序(类所在的程序,即同一个编译单元,dll或exe中)

4)protected internal:protected或者internal,即访问仅限于此程序或从此类派生的类。

5)private:访问仅限于此类

当没有定义访问修饰符时,类成员默认访问修饰符为internal,即此类只能被定义它的程序使用。

嵌套类:

类的定义是可以嵌套的,在内部定义其他的类,类内声明的类称为内部类或者嵌套类。在编译单元或命名空间内声明的类成为顶级类,也成为包含类或者非嵌套类。嵌套类型默认为private。嵌套类也可以设置为public,internal等。

在理想情况下,嵌套类型仅由其包含类型进行实例化和使用。如果需要在其他类型中使用该嵌套类型,则建议定义单独的顶级类,避免使用嵌套类。

如果类A是类B的内部类,当需要在内部类A的内部访问类B的实例成员时,可以在类B中将代表类B的实例的this指针作为一个参数传递给内部类A的构造函数。这样类B中含有成员类A, 同时类A中保存类B的引用。

嵌套类的访问:内部类可以访问包含它的那个类可访问的所有成员,包括该类自己的具有private和protected声明的可访问性成员。这样内部类对象中可以new一个外部类对象,但实例化之后,该外部类对象和该内部类对象所属的外部类对象不是同一个。

分部类:

分部类型(partial   type)可以将类(以及接口和结构)划分为多个部分,存储在不同的源文件中,以便于开发维护。

使用类修饰符partial 实现通过多个部分来定义一个类,partial修饰符必须直接放在class关键字的前面,分部类声明的每个部分都必须包含partial修饰符,并且其声明必须与其他部分位于同一命名空间。当分部类型声明指定了可访问性(public, protected等修饰符)时,它必须与所有其他部分所指定的可访问性一致。

partial修饰符说明在其他位置可能还有同一个类型声明的其他部分,但是这些其他部分并非必须存在;如果只有一个类型声明,包含partial修饰符也是有效的。

分部类型的所有部分必须一起编译,以使这些部分可在编译时被合并,注意分部类型不允许用于扩展已经编译的类型。

partial修饰符可以用于在多个部分声明嵌套类型,通常其包含类型也使用partial声明,并且嵌套类型的每个部分均在该包含类的不同部分中声明。在类的多个部分中声明同一个成员将一起编译的错误,除非该成员是带有partial修饰符的分部类型。


类成员

类成员,通常含有 常量、字段(变量)、方法、属性(定义一些命名特性以及与读取和写入这些特性相关的操作)、索引器(与数组方式索引类的实例相关联的操作)、事件(可有类生成的通知)、运算符(类所支持的转化和表达式运算符)、构造函数、析构函数、类型(类所声明的嵌套类型)。

成员分为:数据成员(常量、变量(字段)、事件)和函数成员(方法、属性、构造函数、析构函数、索引器、运算符); 

静态成员(static ,必须通过类名来引用。当一个类创建多个对象时,静态字段在内存中占用同一个存储区域,只有一个副本)和实例成员(由每个对象实例拥有,实例成员必须通过对象实例来引用。实例函数成员(方法、属性、构造析构函数等)作用于该类的给定实例,故在其代码体中既可以使用实例成员,也可以直接引用类的静态成员)。


字段:

静态字段:声明     [修饰符]  static  类型   字段名  [ = 初始化 ];

若要访问类的静态字段,使用 类名.静态字段名

常量字段:声明    [修饰符]  const  类型   字段名  =  初始化。常量是静态成员,但声明常量时既不要求也不允许使用static 修饰符,否则将产生编译错误。一个常量可以依赖同一个程序内的其他常量,只要这种依赖关系不是循环的,编译器会自动安排适当的顺序来计算各个常量声明。

只读字段:只读字段只能在声明字段时赋值或在类的构造函数中被赋值,在其他位置,只读字段的值不能更改。[修饰符]   readonly  类型  字段名 [= 初始化]

常量和只读字段的区别:常量只能在声明时赋值,只读字段可以在声明时赋值或在构造函数内赋值;常量的值是在编译时就得到,只读字段的值是在运行时确定;常量的值只能是下列类型之一{sbyte, byte, short, ushort, int , uint,  long, ulong, char, float, double, decimal, bool, string或者枚举类型}, 而只读字段可以为任何类型。

可变字段:可变字段不受编译器优化的限制(假定由单个线程访问),可变字段可以由多个同时执行的线程修改,可以确保该字段在任何时间呈现的都是最新的值。

[修饰符]  volatile  类型 字段名 [= 初始化]

方法:

参数的传递: 方法的声明可以包含一个[形参列表], 而方法调用时则通过传递[实参列表], 以允许方法体中的代码引用这些参数变量。形参可以在方法声明空间直接使用,故在方法体中不能定义同名的局部变量。

1)值形参:声明时不含任何修饰符,在方法代码体中,可以将新值赋给值形参,但赋值只影响方法声明空间的局部存储位置,对值形参的修改不会影响的方法调用时由调用方给出的实参。

2)引用形参:用ref 修饰符声明,用于输入输出参数的传递。为引用参数传递的实参必须是变量,引用形参并不创建新的存储位置,其存储位置就是方法调用中作为实参给出的那个变量所表示的存储位置。故当控制权传递回调用方法时,在方法中对参数的任何更改都将反映在该变量中。

3)输出形参:用out 修饰符声明 , 输出形参不创建新的存储位置,其存储位置是方法调用中作为实参给出的那个变量所表示的存储位置。输出参数主要用于当控制权传递回调用方法(调用该函数的那个方法或对象)时,把输出值传递给相应的变量(当希望方法返回多个值时)。

当形参为输出形参时,方法定义和调用方法都必须显式使用out关键字,方法调用中的对应实参必须为与形参相同的变量,变量在作为输出形参传递之前不需要明确赋值,但是在将变量作为输出形参传递的调用之后,必须明确赋值。

4)形参数组:用params 修饰符声明。用params修饰符声明的形参是形参数组,允许向方法传递可变数量的实参。如果形参表包含一个形参数组,则该形参数组必须位于该列表的最后,且必须是一维数组类型。类型string[]  和 string[ ] [ ]可用做形参数组的类型,但是类型string[ , ]不能。形参数组主要用于传递可变数量的参数。params不能和ref和out修饰符组合起来一块使用。

如  static  void Fun(params   int[]  args)

{

Console.WriteLine(args.Length);

foreach (int   i  in  args)

Console.WriteLine(  i  );

}


C#支持方法的重载。即函数名相同,但参数不同。

静态方法:static声明静态方法,静态方法不对特定的实例继续操作,并且只能够访问静态成员。在静态方法中使用this会造成编译错误。

[方法修饰符] static  返回值类型  方法名 ([形参列表])

{

方法体;

}

使用类的静态方法:  类名.静态方法名([实参列表])

实例方法:不适用static修饰符修饰的方法为实例方法。实例方法对类的某个给定的实例进行操作,并且能够访问静态成员和实例成员。在调用实例方法的基础上,可以通过this显式该实例。


分部方法在分部类型中,可以使用partial修饰符定义分部方法。分部方法在分部类的一个部分中声明分部方法定义,而在分部类的另一个部分中声明分部方法实现。这两个声明必须具有相同的修饰符、类型、方法名和形参列表。

外部方法:当方法声明中包含extern修饰符时,称该方法为外部方法。外部方法是在外部实现的(通常为dll库函数),故外部方法声明不提供任何实现,其方法体只由一个分号组成。外部方法不可以是泛型。

extern修饰符通常与DllImport特性一起使用,以引用由DLL实现的外部函数。当外部方法包括DllImport特性时,该方法声明必须同时包含一个static修饰符。


属性:

面向对象编程的封装性原则要求不能直接访问类中的数据成员,在C#中,数据成员的访问方式一般设置为私有的,然后定义了相应属性的访问器。

属性是一种用于访问对象或类的特性的成员。属性的示例包括字符串的长度、字体的大小,窗口的标题和客户的名称等。属性是字段的自然扩展,两者都是具有关联类型的命名成员,而且访问字段和属性的语法相同。与字段不同的是,属性不存在存储位置,属性通过访问器指定在他们的值被读取或写入时需要执行的语句。属性的读写一般与私有的字段紧密相连。属性的声明基本形式如下:

[属性修饰符]   类型    属性名

{

[get { get 访问器体}]

[set { set 访问器体}]

}

属性修饰符指定方法的可访问性,类型指定该属性值的类型,属性名是一种标识符,其首字母通常大写;访问器指定与属性的读取和写入相关联的可执行语句。

属性的访问类似于字段的访问,但本质上属性是方法,而不是数据成员。属性的访问可以通过  对象.属性名 来进行。

对 对象.属性名 进行赋值, 则调用set访问器; 直接使用 对象.属性名  则调用 get访问器。


C#中的属性通过get和set访问器来对属性的值进行读写。通过set get访问器是否存在,属性分为:

(1)读写属性:同时包含set访问器和get访问器

(2)只读属性:只含有get访问器的属性。

(3)只写属性:只具有set访问器的属性。

属性主要用于以下几种情况:

(1)允许封装私有的成员字段

(2)允许更改前验证数据,即进行错误检查

(3)允许更改数据时进行其他操作,例如,进行数据转换,更改其他字段的值,引发事件等。

(4)允许透明的公开某个类的数据,而数据源来自其他源。

当属性声明时包含static修饰符时,称该属性为 静态属性,类似于静态字段,静态属性不属于某一个对象实例,访问也只能通过类名访问,类名.属性名,且静态属性中不能使用this;

      属性声明中不包含static修饰符时,为实例属性,和实例字段一样,专属于某一个对象,可以使用this。

自动实现的属性,如果属性访问器不需要其他额外逻辑,使用自动实现的属性可以使属性声明变得简洁。

public class Point

{

public  int  X{get ;  set ; } //自动实现的属性

public  int  Y { get;  set ; }  //自动实现的属性

}

等效于声明

public class Point

{

private int X;

private int Y;

public  int  X

{

get { return  X;}

set  {x = value;

}

public  int Y

{

get { return Y ;}

set { Y = value;}

}

}

索引器

  索引器允许对象像数组一样进行索引,并通过索引来操作对象中集合里的元素。

索引器的声明:

[修饰符]  类型  this [参数表]

{

[get {get  访问体}]

[set { set 访问体}]

}

索引器的名称固定为this,且 必须指定索引的参数表

索引器的访问方式为采用数组类似的使用元素访问操作符 [  ]:   对象[索引参数]

class TempRecord

{

private float[]  temps = new float[4] { 1, 2, 2.3, 4};

public int Length//属性

{

get { return temps.Length; }

}

public float  this[ int  index]//索引器

{

get { return temps[index];}

set { temps[index] = value;}

}

}

运算符重载

通过使用static关键字定义静态成员函数来重载运算符,运算符可以使类实例像基本类型一样进行表达式操作运算。重载运算符声明:

[ 修饰符 ] static  类型 operator 运算符  (参数表)

{

转换代码体;

}

比较运算符必须成对重载,如果重载 ==, 必须重载 != ,反之亦然; 如果重载 >= , 必须重载 <= .

[ ] 和 ( )不能重载,因为C#可以通过其他方式得到同样效果,如使用索引器代替 [ ]重载, 使用用户定义的数据类型转换代替 ( ) 重载。

构造函数和析构函数

实例构造函数用于执行类的实例的初始化工作,创建对象时根据传入的参数列表将调用相应的构造函数。每个类都有构造函数,如果没有显式声明构造函数,则编译器会自动生成一个默认的构造函数(无参数),默认构造函数实例化对象,将未赋初值的字段设置为默认值(例如字符串为空,数值为0, bool设置为false)

一般构造函数总是public类型的,创建对象时,自动调用对应的构造函数,不能显式调用构造函数。 在构造函数中不能做对类的实例进行初始化之外的事情。

静态构造函数,static 类名() { 静态构造函数体}。 静态构造函数用于实现初始化类(而不是初始化实例或对象)所需的操作。静态构造函数用于初始化任何静态数据,或用于执行仅需执行一次的操作。在创建第一个实例或引用任何静态成员之前,将自动调用静态构造函数。类的静态构造函数在给定的程序中至多执行一次。

析构函数用于实现实例所需的操作,如释放对象所占用的非托管资源(例如打开文件、网络连接等)。析构函数声明的基本形式如下:

 ~ 类名

{

析构函数方法体;

}

析构函数既没有修饰符,也没有返回值(甚至不能使用void), 也没有参数。

一个类只能由一个析构函数。

不能显式调用析构函数,当公共语言运行时的垃圾回收销毁不再使用的对象时,自动调用其析构函数;程序退出时也调用析构函数。注:如果某种资源需要在对象无效时立即被释放,那么释放这种资源的代码就不应该放在析构函数中。

析构函数隐式的调用对象基类的Finalize()方法,即对继承递归调用Finalize()方法,故不应该使用空析构函数,因为空的析构函数只会导致不必要的性能损失。


继承和多态

继承使得允许重用现有类去创建新类,子类将获得基类的所有非私有数据和行为,可以定义其他数据或行为。子类具有两个有效类型:子类的类型和它继承的基类的类型。

多态是指对象可以表示多个类型的能力。

将子类对象可以强制转换为父类对象,之后可以再强制转化为子类对象;只有实际上是子类实例的那些实例(暂时成父类实例状态),才能强制转换为子类实例。

C#提供两种继承:实现继承和接口继承

实现继承表示一个类型派生于一个基类型,派生类具有基类的所有非私有数据和行为。在实现继承中,派生类型的每个方法采用基类型的实现代码,除非在派生类型的定义中指定重写该方法的实现代码。实现继承一般用于增加现有类型的功能,或许多相关的类型共享一组重要的公共功能的场合。

接口继承表示一个类型实现若干个接口,接口仅包含方法的签名,故接口继承不继承任何实现代码。接口继承一般用于指定该类型具有某类可用的特性。接口即契约,类型派生于接口,即保证该类提供该接口规定的功能。

派生类

C#不支持多重继承,即一个派生类只能继承于一个基类。

访问关键字this和base。

this关键字引用类的当前实例,静态成员方法中不能使用this关键字,this关键字只能在实例构造函数、实例方法或实例访问器中使用。

base关键字用于从派生类中访问基类的成员:

(1)指定创建派生类实例时应调用的基类构造函数

class Child

{

public Child(int a, int b) : base ( a,  b)

{

this.A = a * 2;

this.B = b * 2;

}

}

(2)调用基类上已被其他方法重写的方法

虚方法、重写方法和隐藏方法

C#允许派生类中的方法和基类的方法具有相同的签名:在基类中使用关键字 virtual 定义虚方法,在派生类中使用 override 来重写方法, 或使用关键字new 来覆盖方法(隐藏方法)。

调用虚方法时,将首先检查该对象的运行时类型,并调用派生类中的该重写成员。如果没有派生类重写该方法,则调用其原始成员。

默认情况下,C#方法是非虚拟的,不能重写非虚拟方法,否则将导致编译错误。

除了类方法外,还可以使用virtual关键字修饰的其他类成员以定义虚成员,包括属性、索引器或事件声明。虚拟成员的实现可在派生类中使用关键字override来重写;或使用关键字new来覆盖。

virtual修饰符不能和 static abstract   private  override修饰符一块使用。

抽象类和抽象方法

将关键字abstract置于关键字 class前面可以声明抽象类,抽象类不能实例化,一般用于提供多个派生类可共享的基类的公共定义。当从抽象类派生非抽象类时,这些非抽象类必须实现所继承的所有抽象成员,从而重写那些抽象成员。

抽象类中可以有成员方法、成员变量等的声明和定义。

抽象类中通过将关键字abstract添加到实例方法的返回类型之前可以定义抽象方法,抽象方法声明引入一个新的虚方法,但不提供该方法的任何实现,所以抽象方法的方法体只有一个分号构成。非抽象类的派生类必须重写抽象方法以提供他们自己的实现。

只允许在抽象类中使用抽象方法声明,在派生类中进行override重写实现。

除了类方法外,还可以使用abstract关键字修饰的其他类成员以定义虚成员,包括字段、属性、事件等。

密封类和密封方法

通过将关键字sealed置于关键字class前,可以将类声明为密封类。密封类不能用作基类,因此他也不能是抽象类。密封类主要用于防止非有意的派生,由于密封类从不用做基类,因此调用密封类成员的效率可能会更高。

当实例方法声明包含sealed修饰符时,成该方法为密封方法,如果实例方法声明包含sealed修饰符,它必须包含override修饰符,使用sealed修饰符可以防止派生类进一步重写该方法。

可以将密封类和密封方法看成抽象类和抽象方法的对立,把类或方法声明为抽象,表示该类或方法必须被重写或继承;把类或方法声明为密封,则表示该类或方法不能被重写或继承。

接口

一个接口定义一个协定,接口本身不提供它所定义的成员的实现,只指定实现该接口的类或结构必须提供的成员,继承接口的任何非抽象类型都必须实现接口的所有成员。

接口使用interface关键字定义。接口不能实例化,接口的成员隐式为 public 和abstract。接口可以包含事件、索引器、方法和属性,但不能包含字段。

虽然C#不支持基类的多重继承,但类和结构可以从多个接口继承,接口自身可从多个接口继承。

接口声明:

[接口修饰符] [ partial ]  interface  接口名  [类型形参]  [ : 基接口 [  类型形参约束 ] ]

{

接口体;

}

接口成员,在接口中只能包含其成员的签名,接口成员包括从基接口继承的成员和由接口本身声明的成员,接口成员只能包含方法、属性、索引器和事件的声明。接口成员不能包含常量、字段、运算符、实例构造函数、析构函数或类型,不允许包含运算符重载,不能包含任何类型的静态成员。所有的接口成员都隐式的具有public访问属性。

接口可以由类或结构实现,类中对应的成员必须是公共的public、非静态的,并且与接口成员具有相同的名称和签名。类的属性和索引器可以为接口上定义的属性或索引器定义额外的访问器。例如,接口可以声明一个带有get访问器的属性,而实现该接口的类可以声明同时带有get和set访问器的同一属性。

接口继承: 接口可以从零个或多个及诶口类型继承,被继承的接口为该接口的显式基接口。当接口具有一个会哦多个显式及接口时,在接口声明中,接口标识符后面就要紧跟一个冒号以及一个由逗号分隔的基接口类型列表。


委托和事件

委托用来处理其他语言(如C/C++、pascal和 Modula)需用函数指针来处理的情况。与C/C++指针不同,委托是完全面向对象的,是类型安全的;而C/C++中的函数指针只是一个指向存储单元的指针,不能保证实际指向内容为正确类型的函数,即C/C++中的函数指针是不安全的。C/C++指针只指向成员函数,而委托同时封装了对象实例和方法。

委托是可保存对方法的引用的,委托具有一个签名,并且它只能对与其签名相匹配的方法进行引用,这样委托就相当于一个类型安全函数指针或一个回调。

委托声明:

委托声明定义一个从System.Delegate类派生的类,委托实例封装了一个调用列表,该列表列出了一个或多个方法,每个方法称为一个可调用实体。对于静态方法,可调用实体仅由一个方法组成,用一个适当的参数来调用一个委托实体,就是用此给定的参数集来调用该委托实例的每个可调用实体。

委托声明定义一个从System.Delegate 类的派生类 System.MulticastDelegate 派生的类。委托声明基本形式:

[ 委托修饰符 ]  delegate  返回值类型  委托名  ([ 形参列表 ]);

delegate  int  D1 (int  a,  double b);

public class  Cls

{

delegate void D2(int   a);

public  int  A(int, double)  {     xxx;}

public  void B(int) { xxx; }

}

 Cls  testCls = new testCls;

 D1  d1 = new D1(testCls.A);

D2 d2 = testCls.B;

int result = d1(1, 2.0);


在上例中, 委托D1和方法A匹配, 委托D2和方法B匹配,他们具有相同的返回类型和参数列表。

委托实例化和调用:

委托名   委托实例名 = new  委托名(匹配方法);

匹配方法是与委托的签名(由返回类型和参数构成)匹配的任何可访问类或结构中的任何方法。方法可以是静态方法或实例方法,如果是静态方法,使用名称“ 类名.静态方法名", 如果是实例方法,则需要先创建类的实例对象,然后使用名称 ”实例方法名.实例方法名".

也可以直接将方法名赋值给委托实例名来创建一个委托实例:   委托名   委托实例名  =  匹配方法;

委托实例的调用与实例方法的调用类似:   委托实例名( 实参列表)

委托一种主要用途是将类型安全的函数指针(方法)作为其他方法的参数进行传递,从而实现函数回调方法的功能。

例如;

delegate  void  D(int []  A);

public class ArraySort

{

public static void GeneralSort(int  []  A,   D sort) //通用排序程序

{

sort (A);

foreach( int   i   in  A)  Console.Write("{0, 5} }, i);

}

public  static  void BubbleSort(int [] A)

{

冒泡函数体;

}

public static void  SelectSort(int  []  A)

{

选择排序函数体;

}

static  void  Main()

{

int  []  A = {1, 4, 2, 9,5, 3};

D  d1 = new D(ArraySort.BubbleSort);

GeneralSort(A,  d1);

D  d2 = new D(ArraySort.SelectSort);

GeneralSort(A, d2);

}

}

C#中提供了一种简单的方法来创建委托的实例——匿名方法。无须先声明类或结构以及与委托匹配的方法,而是在创建委托的实例时直接声明与委托匹配的方法的代码块(匿名方法)。

委托名 委托实例名  =  new  delegate( [ 形参列表 ] )

{

方法体;

}

多播委托: 委托也可以包含多个方法,称为多播委托。如果调用多播委托实例,则按顺序依次调用多播委托实例封装的调用列表中的多个方法。

声明多播委托时,返回类型必须为void,因为无法处理多次调用的返回值,而且不能带输出参数,但可以带引用参数。

多播委托通过“+”或“+=”向多播委托实例封装的调用列表中添加方法,通过“-”或“-=”从多播委托实例封装的调用列表删除方法。

delegate   void  D(int x);

class C

{

public static void M1(int i)

{

Console.WriteLine("this is M1 " + i);

}

public static  void M2(int i)

{

Console.WriteLine("this is M2 “ + i);

}

public   void  M3(int i)

{

Console.WriteLine("this is M3 " + i);

}

}

class Test

{

static void Main()

{

D cd1 = new D(C.M1);

cd1(-1);

D cd2 = new D(C.M2);

D cd3 = cd1 + cd2;//组播委托,先调用M1,在调用M2

cd3 += cd1; //调用M1, M2,  M1

cd3 -= cd1;

C  c1 = new C();

D cd4 = c1.M3;

cd3 += cd4;//调用 M1, M2, M3

}

}

委托的兼容性:与委托相对应的方法不必与委托签名完全匹配,只要满足如下条件,则方法M和委托D兼容:

(1) D和M的参数数目相同,且各自对应参数具有相同的ref或out修饰符

(2)对于每个ref或out参数,D中的参数类型与M中的参数类型相同

(3)存在从M的返回类型到D的返回类型的标识或隐式引用的转换。即允许方法具有的派生返回类型比委托中定义的更多。

(4)每一个值参数(没有ref或out修饰符的参数)都存在从D中的参数类型奥M中对应参数的标识或隐式引用转换。允许方法具有的派生参数类型比委托类型中的更少。

事件

类或对象可以通过事件向其他类或对象通知发生的相关事情。发送(或引发)事件的类成为发行者(生产者),接收(或处理)事件的类称为订户(消费者)。

消费者可以通过添加事件处理程序为相应的事件添加可执行代码。

事件是对象发送的消息,以发信号通知操作的发生,操作可能是由用户交互(例如鼠标单击引起),也可能是由其他的程序逻辑引起。

事件的特点:

(1)发行者确定何时引发事件,订户确定执行何种操作响应事件

(2)一个事件可以有多个订户。一个订户可处理来自多个发行者的多个事件

(3)没有订户的事件永远不会被调用

(4)事件通常用于通知用户操作,例如图形界面中按钮单击

(5)如果一个事件有多个订户,当引发该事件时,会同步调用多个事件处理程序

(6)可以利用事件同步线程

(7)在.NETFramework类库中,事件是基于EventHandler委托和EventArgs基类的

在C#中事件是委托的一种特殊形式,处理机制如下:

(1)在事件生产者类中声明一个事件成员,即某种事件处理委托(简称为事件委托)的实例(多播事件委托实例)

(2)在事件消费者类中声明与事件委托相匹配的事件处理方法

(3)通过”+=“向多播事件委托实例封装的调用列表中添加事件处理方法,或通过”-=”从多播事件委托实例封装的调用列表中删除事件处理方法。

(4)在事件生产者类中添加有关发生事件的代码,即当满足某种条件时(发生事件),则调用委托,即调用多播事件委托实例封装的调用列表中添加的事件处理方法。如果没有订阅,即事件委托实例为NULL,则不作任何处理。

事件的声明: 

在C#中使用关键字event在事件生成者类中声明事件,其基本形式如下:

[修饰符 ]  event  事件委托名  事件名

事件委托名为事件处理委托的名称,需要在声明事件前进行定义(也可以使用.NETFramework事件模型中预定义的事件处理委托,如EventHandler等)。 通过关键字event,C# 编译器将自动生成所有事件处理机制所需要的成员和处理代码。

public class Publisher

{

public delegate void SampleEventHandler(object  sender, SampleEventArg  e);//声明事件处理委托

public event  SampleEventHandler    SampleEvent;//声明事件    [修饰符 ]  event  事件委托名  事件名

protected virtual void RaiseSampleEvent()

{

SampleEvent(this,  new SampleEventArgs("hello"));

}

}

事件订阅和取消:

使用“+=”来为事件附加事件处理程序,用“-=”取消事件的相应处理程序。

基本形式如下:  对象.事件名  +=  委托实例;    对象.事件名 -= 委托实例

public class Subscriber

{

public static void Method1(object sender,  SampleEventArgs  e);

{

Console.WriteLine("To do something……“);

}

public static  void  Method2(object  sender,  SampleEventArgs  e)

{

Console.WriteLine("To  do something");

}

SampleEventHandler d1 = new SampleEventHandler(Subscriber.Method1);//创建委托实例

Publisher p = new Publisher();

p.SampleEvent += d1;

p.SampleEvent += new SampleEventHandler(Subscriber.Method2);//订阅事件

p.SampleEvent -= d1;//取消事件

}

事件分为静态事件和实例事件,含有修饰符static为静态事件,静态事件不属于任何一个实例,不可以使用this;实例事件没有static修饰符,可以使用this。

.NETFramework事件模型:在.NETFramework中国,事件模型由三个互相联系的元素提供:提供事件数据的类、事件委托、引发事件的类。

提供事件数据的类必须从System.EventArgs派生,命名为  事件名EventArgs

事件的委托,命名为 事件名EventHandler

引发事件的类,该类必须提供事件声明和引发事件的方法。

.NETFramework类库中定义了大量事件数据类和事件委托类,可以根据需要直接使用,而不需定义这些类。例如如果事件不使用自定义数据,则可以使用System.EventArgs作为事件数据,并使用System.EventHandler作为委托。

按照.NETFramework模型,在C#中实现事件处理的基本步骤如下:

(1)声明提供事件数据的类,从System.EventArgs派生提供事件数据的类

(2)声明事件处理委托

(3)声明引发事件的类(事件生产类)

(4)在事件生产类中声明事件

(5)在事件生产类中实现产生事件的代码

(6)声明处理事件的类(事件消费类)

(7)在事件消费类中声明事件处理方法

(8)在事件消费类中订阅或取消事件

using System;

using System.Collections;

namespace  SampleEvent

{

//(1)声明提供事件数据的类

public class NameListEventArgs: EventArgs

{

public string Name{ get; set;}

public int Count{get;  set; }

public NameListEventArgs(string name, int count)

{

Name = name;

Count = count;

}

}

//(2)声明事件委托处理

public delegate  void NameListEventHandler(object  sender,  NameListEventArgs  args);

//(3)声明引发事件的类

public  class NameList

{

ArrayList list;

//(4)在事件生成类中,声明事件

public  event  NameListEventHandler  nameListEvent;

public NameList()

{

list = new ArrayList();

}

public void Add(string name)

{

list.Add(name);

//(5)在事件生产类中,实现产生事件的代码

if (nameListEvent != null)

{

nameListEvent(this,  new NameListEventArgs(name, list.Count);

}

}

}

//(6)声明处理事件的类(消费类)

public class EventDemo 

{

//(7)在事件消费类中声明事件处理方法

public static void Method1(object  source,  NameListEventArgs  args)

{

Console.WriteLine("列表中添加了项目:{0}", args.Name);

}

//(7)在事件消费类中声明事件处理方法

public static void Method2(object source, NameListEventArgs args)

{

Console.WriteLine("列表中的项目数:{0}",  args.Count);

}

public static void Main()

{

NameList  nl = new NameList();

//(8)在事件消费类中订阅或取消事件

nl.nameListEvent += new NameListEventHandler(EventDemo.Method1);

nl.nameListEvent += new NameListEventHandler(EventDemo.Method2);

nl.Add("jason”);

nl.Add("toread");

}

}

}

结构和枚举

结构和类相似,均为包含数据成员和函数成员的数据结构,结构可视为轻量级类,是创建用于存储少量数据的数据类型的理想选择。

类是存储在堆(heap)上的引用类型,而结构是存储在堆栈(stack)上的值类型。

复制包含结构的变量,将复制所有的数据,对新副本作出的任何修改都不会改变旧副本的数据,但是结构可以通过ref或out形参以引用方式传递给函数成员。

结构为值类型,故可以通过装箱/拆箱操作实现与object类型或由该结构实现的接口类型之间的转换。

结构不存在继承,结构类型永远不会是抽象的,始终是隐式密封的。但允许实现接口。

结构中不允许声明无形参实例构造函数,每个结构隐式的具有一个无形参的实例构造函数,该构造函数将所有的值类型设置为默认值,所有的引用类型设置为null。结构可以声明具有形参的实例构造函数。 

结构中不允许声明析构函数。

结构声明: [ 结构修饰符 ] [ partial ] struct  结构名 [类型 形参] [ : 基接口 [类型形参约束

结构创建:  结构名   结构变量名  =  new  结构名([ 参数表 ] )  。 与类不同,结构的实例化也可以不使用new运算符,如果不使用new, 则在初始化所有字段之前,字段都保持未赋值状态且对象不可用。结构名    结构变量名;

结构可以使用嵌套结构,也可以使用分部结构。

结构的成员:可以为常量、字段、方法、属性、事件、索引器、运算符重载、带参数构造函数、嵌套结构

由于结构不支持竭诚,因此结构成员的声明可访问性不能是protected 或 protected internal。结构中的函数成员不能是abstract或virtual,override修饰符只适用于重写从System.ValueType继承的方法。

结构不允许它的实例字段声明中含有变量初始化设定项,但在结构的静态字段声明中可以含有变量初始值设定项。

枚举

声明:

[ 枚举修饰符 ] enum  枚举名 [ : 基础类型]

{

枚举体;

}

基础类型,每种枚举类型都有基础类型,可以声明为byte, sbyte, short,  ushort, int, uint, long ,ulong类型作为对应的基础类型,char不能用作基础类型,默认的基础类型为int。

枚举类型中的每种类型的使用   枚举名.枚举中的类型名;


泛型

引例ArrayList, 可以实现通用化(可以加入任何元素)

.NETFramework类库中包含集合类System.Collections.ArrayList,可用来存储任何引用或值类型。

ArrayList通用化是通过在类型与通用基类型object之间进行强制转换来实现:添加到ArrayList中的任何引用或值类型都将隐式的向上强制转换为object;如果想是值类型,则添加是需要进行装箱操作,检索是需要进行拆箱操作。

存在的弊端:装箱、拆箱降低性能; 缺少编译时类型检查,因为所有项均强制转换为object,在编译时无法防止客户端代码执行非法操作。

引例List<T> 

.NETFramework类库中包含泛型集合类 System.Collections.Generic.List<T>, 可以用来创建各种类型的列表。

与ArrayList相比,使用List<T>时,必须为每个实例指定具体的数据类型,这样不需要向上强制转换为object以及装箱拆箱操作,同时也使得编译器进行类型检查,保证通用性和健壮性。

泛型:

在C#中,可以定义泛型接口、泛型类、泛型方法、泛型事件和泛型委托。泛型定义是通过泛型参数(<T>)来进行定义的。

public   class Test <T>

{

private  T  data;

public  Setdata(T a){  data  =a ;}

}

Test<int>  test = new Test<int>();

public class  Cls<T1, T2>

{

private  T1  data1;

private T2  data2;

}

在泛型类的声明中需要声明泛型参数,然后在泛型类的成员声明中使用该泛型参数作为通用类型;而在创建泛型类的实例时,则需要与泛型参数对应的实际类型。

泛型参数的约束:

默认情况下,没有约束的类型参数称为未绑定的类型参数,创建未绑定的类型参数的泛型类的实例时,可以为泛型类的形参指定任何类型。如果需要限定该泛型类形参仅支持某些特定类型,则可以使用上下文关键字where定义泛型参数的约束。如果客户端代码尝试使用某个约束所不允许的类型来实例化,则会产生编译错误。

public  class Employee

{

/...../

}

public  class Test<T> where T: Employee

{

/....../

}

C#支持的泛型参数的约束类型:6种

T: struct    // 泛型参数必须是值类型,可以指定除null之外的任何值类型

T: class    //类型参数必须是引用类型,这一点适合于任何类、接口、委托或数组类型

T: new ()  //类型参数必须具有无参数的公共构造函数,当与其他约束一起使用时,new()约束必须最后指定

T:  基类名   //类型参数必须是指定的基类或派生自指定的基类

T: 接口名称  //类型参数必须是指定的接口或实现指定的接口。可以指定多个接口约束,约束接口也可以是泛型的。

T: U  //用作约束的类型参数成为裸类型约束。

public  class  Sample<T, U, V> where  T: V{}

泛型类: 泛型类一般用于封装非特定数据类型的操作。.NETFramework类库中包含若干泛型集合类,对于大多数需要集合类的方案,建议使用命名空间 System.Collections.Generic所提供的泛型集合类。

泛型类的继承设计:

(1)泛型类可以从具体的、封闭的构造或开放式构造基类继承

class  BaseNode{}

class BaseNodeGeneric<T>{]

class NodeConcrete<T> : BaseNode{}   //从具体类继承

class NodeClosed<T> : BaseNodeGeneric<int> {}  //从封闭式构造类继承

class NodeOpen<T> : BaseNodeGeneric<T> {}  //从开放式构造类继承

(2)非泛型类(具体类)可以从封闭式构造基类继承,但无法从开放式构造类或裸类型形参继承,因为运行时客户端代码无法提供实例化基类所需的类型实参。

class Node1 : BaseNodeGeneric <int> {}

class Node2 : BaseNodeGeneric<T> {} //错误

class Node3 : T{} //错误

(3)从开放式构造类型继承的泛型类,必须为任何未被继承类共享的基类类型参数提供类型变量

class  BaseNodeMultiple<T, U>{}

class Node4<T> : BaseNodeMultiple<T, int>  // 为未被继承的基类类型参数提供类型变量

(4)从开放式构造类型继承的泛型类,必须指定约束,这些约束是基类型约束的超集或暗示基类型约束

class NodeItem<T> where T : System.IComparable<T>, new() {}

class SpecialNodeItem<T> : NodeItem where T : System.IComparable<T> , new()  {}

泛型接口

在泛型类的设计中,通常将泛型类共同要实现的方法、委托或事件的签名封装为泛型接口,然后在实现这些泛型接口的泛型类中实现这些方法。

.NETFramework类库中定义了若干泛型接口,以用于System.Collections.Generic命名空间中的泛型集合类。

ICollection<T>   //定义操作泛型集合的方法

IComparable<T>                 //定义类型为比较两个对象而实现的方法

IDictionary<TKey,  TValue>       //表示键值对的泛型集合

IEnumerable<T> //公开枚举数,该枚举数支持在指定类型的集合上进行简单迭代

IEqualityComparer<T> //定义方法以支持对象的相等比较

IList<T> //表示可以按照索引单独访问的一组对象

例如,下限为0的一维数组自动实现 IList<T>, 故可以使用相同的代码循环访问数组,以及使用其他集合类型的泛型方法。

适用于类的继承规则同样适用于接口:

interface  IMonth<T> {}

interface IJanuary : IMonth<int> {}

interface IFebruary<T>  :IMonth<T> {}

interface IApril<T>  : IMonth<T, U> {}  //错误

只要类参数列表提供了接口必需的所有参数,泛型类便可以实现泛型接口后已关闭的构造接口。

interface  IBaseInterface<T, U> {}

class SampleClass1<T, U> : IBaseInterface<T, U>{}

class SampleClass2 <T> : IBaseInterface <T, string>

泛型方法

泛型方法是使用类型参数声明的方法,编译器能够根据传入的实参推断类型形参。

void  Func<T, U> ( T  para1,  U para2) where T : System.IComparable<T>

调用泛型方法时,可以省略指定 类型参数,编译器可以根据传入的实参进行判断。

如果泛型方法的泛型参数和类的泛型参数相同,则编译器会发出警告,如果需要使用其他类型参数(而不是实例化类时提供的类型参数),则应该为方法的类型参数提供不同于类的泛型参数不同的标识符。

class  GenericList<T>

{

public void  SampleMethod<U> (U a) { xxxx;}

}

通过泛型参数,同样可以指定泛型委托和泛型事件。

default关键字:

在泛型的定义中,若声明某泛型参数的一个变量(如T  t), 赋值时需要注意,引用类型应该赋值为null, 值类型应该为默认值。可以使用default(T)来统一赋值。

T  temp = default(T); //自动实现,值类型为默认值,引用类型为null。


原创粉丝点击