C#高级编程学习笔记(-)C#基础 对象和类型 继承
一、.NET体系结构
中间语言(Intermediate Language,IL)
通用类型系统(Common Type System,CTS)
公共语言规范(Common Language Specification,CLS)
公共语言运行库(Common Language Runtime,CLR)
即时编译(Just-In-Time,JIT)
.NET Framework的核心是其运行库的执行环境(即CLR)。在CLR的控制下运行的代码称为托管代码(managed code)
托管代码的优点:平台无关性、提高性能、语言的互操作性
二、C#基础
1、变量
字段和局部变量的作用域冲突
在某些情况下,可以区分名称相同、作用域相同的两个标识符。此时编译器允许声明第二个变量。原因是C#在变量之间有一个基本的区分,它把声明为类型级的变量看作是字段,而把方法中声明的变量看作局部变量。考虑下的例子:
using System;
namespace Wrox.ProCSharp.Basics
{
class Scope Test2
{
static int j=20;
public static void Main()
{
int j=30;
Console.WriteLine(j);
return;
}
}
}
运行代码,显示数字30
2、常量
常量和静态只读字段区别
常量是必须在声明时初始化,常量的值必须能在编译时用于计算,不能用从一个变量中提取的值来实例化常量。常量总是静态的。
当表达式引用常量时,该常量的值在编译时获取,但是当表达式引用只读字段时,要等到运行时才获取该字段的值。
3、值类型和引用类型
值类型存储在堆栈上,引用类型存储在托管堆上。
4、预定义类型(C#有15个)
值类型(12个):sbyte,short,int,long,byte,ushort,uint,ulong,float,double,decimal(不是基本类型,使用时会有性能损失),bool,char
引用类型(3个):
object,string(与引用类型不同的是修改字符串,就会创建一个全新的string对象,实际上是运算符重载的结果)
5、流控制
if语句,switch语句,循环(for,while,do..while,foreach),跳转语句(goto,break,continue,return)
6、枚举
7、数组
8、命名空间
using指令:创建命名空间的别名或导入其他命名空间中定义的类型。
using语句:定义一个范围,在此范围末尾将处理对象。
9、Main()方法
多个Main()方法可以使用/main选项,如:csc MainExample.cs /main:Wrox.ProCSharp.Basic.MathExample
三、对象和类型
1、类和结构
区别:
(1)类是引用类型,结构是值类型
这是最根本的区别。所以类是在堆里被分配内存,而结构是在栈里被分配内存。C#中,堆和栈是两种分配内存的方式,所有的值类型,比如int这样的.net基本类型,以及结构,枚举是值类型,他们会直接在函数堆栈里面被分配内存,这样,在函数执行完毕,内存就被自动被释放出来。而引用类型,比如类,则会被放在堆里面,然后在栈里存一个地址的引用,在函数执行完毕的时候,内存引用会被立刻释放,而堆里的内存,则会在没有被其他引用的情况下被垃圾回收器回收。
除此之外,值类型和引用类型还会带来new运算符的工作方式的变化,new在结构中,并不回去分配堆中的地址(因为结构根本就是被存放在栈里的,而栈里的地址已经在声明的时候被分配好了。),而是只调用相应的构造函数,初始化字段。下面是类和结构的具体对比:
Class1 a //声明这里分配栈里的地址,只是一个内存的引用的地址
= new Class1();//这里会是分配堆里面的内存,并且调用相应的构造函数
Struct1 b //声明这里分配栈里的地址,整个Struct都在这里被分配好了
= new Struct1 b();//这里只调用相应的构造函数
更直接的影响是来自性能。两种分配方式的性能各有利弊。在声明,初始化的时候,结构的性能要高于类,而在拷贝或者传值的时候,由于要对结构进行一个拷贝,所以效率不如类来的高,不过在传值的时候,可以使用ref进行引用传值。
(2)结构不允许继承,不过有一个例外,所有的结构都是继承自Object的,也可以对ToString等方法重写。继承链是:System.Object -> System.ValueType -> YourStruct
(3) 编译器会给结构指定一个默认的无参数构造函数
如题,所以我们不允许给结构定义一个无参数的构造函数,更严格的是,在结构里面,你甚至不能通过初始化变量(如public int a = 10;)来绕过无参数构造函数。系统的无参数构造函数会把所以的成员初始化为0.
(4)使用结构,可以制定字段如何在内存中布局。
2、类成员
数据成员:字段、常量和事件。
函数成员:方法、属性、构造函数、终结器(finalizer)、运算符以及索引器
类的成员或者是静态成员(static member),或者是实例成员(instance member)。静态成员属于类,实例成员属于对象(类的实例)。
下表提供了类所能包含的各种成员的描述。
成 员 |
描 述 |
常数 |
与类关联的常量值 |
字段 |
类的变量 |
方法 |
能够被类执行的计算和行为 |
属性 |
使对象能够读取和写入类的命名属性 |
索引器 |
使对象能够用与数组相同的方式进行索引 |
事件 |
能够被类产生的通知 |
运算符 |
类支持的转换和表达式运算符 |
构造函数 |
初始化类的实例或者类本身 |
析构函数 |
在永久销毁类的实例之前执行的行为 |
类型 |
被类声明的嵌套类型 |
类的每个成员都有关联的可访问性,它控制能够访问该成员的程序文本区域。有5种可能的可访问性形式。下表概述了类的可访问性的意义。
可访问性 |
意 义 |
public |
访问不受限制 |
protected |
访问仅限于包含类或从包含类派生的类型 |
internal |
访问仅限于当前程序集 |
protected internal |
访问仅限于从包含类派生的当前程序集或类型 |
private |
访问仅限于包含类 |
字段
字段是与对象或类相关联的变量。
当一个字段声明中含有static修饰符时,由该声明引入的字段为静态字段(static field)。它只标识了一个存储位置。不管创建了多少个类实例,静态字段都只会有一个副本。
当一个字段声明中不含有static修饰符时,由该声明引入的字段为实例字段(instance field)。类的每个实例都包含了该类的所有实例字段的一个单独副本。
在下面的示例中,Color类的每个实例都有r,g,b实例字段的不同副本,但是Black,White,Red,Green和Blue等静态字段只有一个副本:
public class Color
{
public static readonly Color Black = new Color(0, 0, 0);
public static readonly Color White = new Color(255, 255, 255);
public static readonly Color Red = new Color(255, 0, 0);
public static readonly Color Green = new Color(0, 255, 0);
public static readonly Color Blue = new Color(0, 0, 255);
private byte r, g, b;
public Color(byte r, byte g, byte b) {
this.r = r;
this.g = g;
this.b = b;
}
}
如前面的示例所示,通过readonly修饰符声明只读字段。给readonly字段的赋值只能作为声明的组成部分出现,或者在同一类中的实例构造函数或静态构造函数中出现。
方法
方法(method)是一种用于实现可以由对象或类执行的计算或操作的成员。静态方法(static method)只能通过类来访问。实例方法(instance method)则要通过类的实例访问。
方法有一个参数(parameter)列表(可能为空),表示传递给方法的值或者引用;方法还有返回类型(return type),用于指定由该方法计算和返回的值的类型。如果方法不返回一个值,则它的返回类型为void。
在声明方法的类中,该方法的签名必须是惟一的。方法的签名由它的名称、参数的数目、每个参数的修饰符和类型组成。返回类型不是方法签名的组成部分。
参数
参数用于将值或者引用变量传递给方法。当方法被调用时,方法的参数译注5从指定的自变量(argument)译注6得到它们实际的值。C#有4种参数:值参数、引用参数、输出参数和参数数组。
值参数(value parameter)用于输入参数的传递。值参数相当于一个局部变量,它的初始值是从为该参数所传递的自变量获得的。对值参数的修改不会影响所传递的自变量。
引用参数(reference parameter)用于输入和输出参数的传递。用于引用参数的自变量必须是一个变量,并且在方法执行期间,引用参数和作为自变量的变量所表示的是同一个存储位置。引用参数用ref修饰符声明。下面的示例展示了ref参数的使用:
using System;
class Test
{
static void Swap(ref int x, ref int y) {
int temp = x;
x = y;
y = temp;
}
static void Main() {
int i = 1, j = 2;
Swap(ref i, ref j);
Console.WriteLine("{0} {1}", i, j); //输出 "2 1"
}
}
输出参数(output parameter)用于输出参数的传递。输出参数类似于引用参数,不同之处在于调用方提供的自变量初始值无关紧要。输出参数用out修饰符声明。下面的示例展示了out参数的使用:
using System;
class Test {
static void Divide(int x, int y, out int result, out int remainder) {
result = x / y;
remainder = x % y;
}
static void Main() {
int res, rem;
Divide(10, 3, out res, out rem);
Console.WriteLine("{0} {1}", res, rem); //输出 "3 1"
}
}
参数数组(parameter array)允许将可变长度的自变量列表传递给方法。参数数组用params修饰符声明。只有方法的最后一个参数能够被声明为参数数组,而且它必须是一维数组类型。System.Console类的Write和WriteLine方法是参数数组应用的很好的例子。它们的声明形式如下:
public class Console
{
public static void Write(string fmt, params object[] args) {...}
public static void WriteLine(string fmt, params object[] args) {...}
...
}
在方法中使用参数数组时,参数数组表现得就像常规的数组类型参数一样。然而,带数组参数的方法调用中,既可以传递参数数组类型的单个自变量,也可以传递参数数组的元素类型的若干自变量。对于后者的情形,数组实例将自动被创建,并且通过给定的自变量初始化。示例:
Console.WriteLine("x={0} y={1} z={2}", x, y, z);
等价于下面的语句:
object[] args = new object[3];
args[0] = x;
args[1] = y;
args[2] = z;
Console.WriteLine("x={0} y={1} z={2}", args);
方法体和局部变量
方法体指定方法调用时所要执行的语句。
方法体能够声明特定于该方法调用的变量。这样的变量被称为局部变量(local variable)。局部变量声明指定类型名、变量名,可能还有初始值。下面的示例声明了一个局部变量i,其初始值为0;另一个局部变量j没有初始值。
using System;
class Squares
{
static void Main() {
int i = 0;
int j;
while(i < 10){
j = i * i;
Console.WriteLine("{0} x {0} = {1}", i, j);
i = i + 1;
}
}
}
C#要求局部变量在其值被获得之前明确赋值(definitely)。例如,假设前面的变量i的声明没有包含初始值,那么,在接下来对i的使用将导致编译器报告错误,原因就是i在程序中没有明确赋值。
方法能够使用return语句将控制返回给它的调用方。如果方法是void的,则return语句不能指定表达式;如果方法是非void的,则return语句必须包含表达式,用于计算返回值。
静态方法和实例方法
若一个方法声明中含有static修饰符,则称该方法为静态方法(static method)。静态方法不对特定实例进行操作,只能访问静态成员。
若一个方法声明中没有static修饰符,则称该方法为实例方法(instance method)。实例方法对特定实例进行操作,既能够访问静态成员,也能够访问实例成员。在调用实例方法的实例上,可以用 this来访问该实例,而在静态方法中引用this是错误的。
下面的Entity类具有静态和实例两种成员:
class Entity
{
static int nextSerialNo;
int serialNo;
public Entity() {
serialNo = nextSerialNo++;
}
public int GetSerialNo() {
return serialNo;
}
public static int GetNextSerialNo() {
return nextSerialNo;
}
public static void SetNextSerialNo(int value) {
nextSerialNo = value;
}
}
每一个Entity实例包含一个序列号(并且假定这里省略了一些其他信息)。Entity构造函数(类似于实例方法)用下一个有效的序列号初始化新的实例。因为构造函数是一个实例成员,所以,它既可以访问serialNo实例字段,也可以访问nextSerialNo静态字段。
GetNextSerialNo和SetNextSerialNo静态方法能够访问nextSerialNo静态字段,但是如果访问serialNo实例字段就会产生错误。
下面的示例展示了Entity类的使用:
using System;
class Test
{
static void Main() {
Entity.SetNextSerialNo(1000);
Entity e1 = new Entity();
Entity e2 = new Entity();
Console.WriteLine(e1.GetSerialNo()); //输出 "1000"
Console.WriteLine(e2.GetSerialNo()); //输出 "1001"
Console.WriteLine(Entity.GetNextSerialNo()); //输出 "1002"
}
}
注意,SetNextSerialNo和GetNextSerialNo静态方法通过类调用,而GetSerialNo实例成员则通过类的实例调用。
虚拟方法、重写方法和抽象方法
若一个实例方法的声明中含有virtual修饰符,则称该方法为虚拟方法(virtual method)。若其中没有virtual修饰符,则称该方法为非虚拟方法(nonvirtual method)。
在一个虚拟方法调用中,该调用所涉及的实例的运行时类型(runtime type)确定了要被调用的究竟是该方法的哪一个实现。在非虚拟方法调用中,实例的编译时类型(compile-time type)是决定性因素。
虚拟方法可以由派生类重写(override)译注7实现。当一个实例方法声明中含有override修饰符时,该方法将重写所继承的相同签名的虚拟方法。虚拟方法声明用于引入新方法,而重写方法声明则用于使现有的继承虚拟方法专用化(通过提供该方法的新实现)。
抽象(abstract)方法是没有实现的虚拟方法。抽象方法的声明是通过abstract修饰符实现的,并且只允许在抽象类中使用抽象方法声明。非抽象类的派生类需要重写抽象方法。
下面的示例声明了一个抽象类Expression,它表示一个表达式树的节点;它有三个派生类Constant,VariableReference,Operation,它们实现了常数、变量引用和算术运算的表达式树节点。
using System;
using System.Collections;
public abstract class Expression
{
public abstract double Evaluate(Hashtable vars);
}
public class Constant: Expression
{
double value;
public Constant(double value) {
this.value = value;
}
public override double Evaluate(Hashtable vars) {
return value;
}
}
public class VariableReference: Expression
{
string name;
public VariableReference(string name) {
this.name = name;
}
public override double Evaluate(Hashtable vars) {
object value = vars[name];
if (value == null) {
throw new Exception("Unknown variable: " + name);
}
return Convert.ToDouble(value);
}
}
public class Operation: Expression
{
Expression left;
char op;
Expression right;
public Operation(Expression left, char op, Expression right) {
this.left = left;
this.op = op;
this.right = right;
}
public override double Evaluate(Hashtable vars) {
double x = left.Evaluate(vars);
double y = right.Evaluate(vars);
switch(op) {
case '+' : return x + y;
case '-' : return x - y;
case '*' : return x * y;
case '/' : return x / y;
}
throw new Exception("Unknown operator");
}
}
前面的4个类用于模型化算术表达式。例如,使用这些类的实例,表达式x+3能够被表示为如下的形式:
Expression e = new Operation(
new VariableReference("x"),
'+',
new Constant(3));
Expression实例的Evaluate方法将被调用,以计算表达式的值,从而产生一个double值。该方法取得一个包含变量名(输入的键)和值(输入的值)的Hashtable作为其自变量。Evaluate方法是虚拟的抽象方法,意味着派生类必须重写它并提供实际的实现。
Evaluate方法的Constant的实现只是返回保存的常数。VariableReference的实现在Hashtable中查找变量名,并且返回相应的值。Operation的实现则首先计算左操作数和右操作数的值(通过递归调用Evaluate方法),然后执行给定的算术运算。
下面的程序使用Expression类,对于不同的x和y的值,计算表达式x*(y+2)。
using System;
using System.Collections;
class Test
{
static void Main() {
Expression e = new Operation(
new VariableReference("x"),
'*',
new Operation(
new VariableReference("y"),
'+',
new Constant(2)
)
);
Hashtable vars = new Hashtable();
Vars["x"] = 3;
Vars["y"] = 5;
Console.WriteLine(e.Evaluate(vars)); //输出 "21"
Vars["x"] = 1.5;
Vars["y"] = 9;
Console.WriteLine(e.Evaluate(vars)); //输出 "16.5"
}
}
方法重载
方法重载(Method overloading)允许在同一个类中采用同一个名称声明多个方法,条件是它们的签名是惟一的。当编译一个重载方法的调用时,编译器采用重载决策(overload resolution)确定应调用的方法。重载决策找到最佳匹配自变量的方法,或者在没有找到最佳匹配的方法时报告错误信息。下面的示例展示了重载决策工作机制。在Main方法中每一个调用的注释说明了实际被调用的方法。
class Test
{
static void F() {
Console.WriteLine("F()");
}
static void F(object x) {
Console.WriteLine("F(object)");
}
static void F(int x) {
Console.WriteLine("F(int)");
}
static void F(double x) {
Console.WriteLine("F(double)");
}
static void F(double x, dpuble y) {
Console.WriteLine("F(double, double)");
}
static void Main(){
F(); //调用F()
F(1); //调用F(int)
F(1.0); //调用F(double)
F("abc"); //调用F(object)
F((double)1); //调用F(double)
F((object)1); //调用F(object)
F(1, 1); //调用F(double, double)
}
}
如上例所示,总是通过自变量到参数类型的显式的类型转换,来选择特定方法。
构造函数
C#既支持实例构造函数,也支持静态构造函数。实例构造函数(instance constructor)是实现初始化类实例所需操作的成员。静态构造函数(static constructor)是一种在类首次加载时用于实现初始化类本身所需操作的成员。
构造函数的声明如同方法一样,不过,它没有返回类型,它的名字与包含它的类名一样。若构造函数的声明中包含static修饰符,则它声明了一个静态构造函数,否则声明实例构造函数。
实例构造函数能够被重载。例如,List声明了两个实例构造函数,一个不带参数,一个带有一个int参数。使用new运算符可以调用实例参数。下面的语句使用各个List类的构造函数创建了两个List实例。
List list1 = new List();
List list2 = new List(10);
实例构造函数不同于其他方法,它是不能被继承的。并且,一个类除了自己声明的实例构造函数外,不可能有其他的实例构造函数。如果一个类没有声明任何实例构造函数,则会自动地为它提供一个默认的空的实例构造函数。
属性
属性(property)是字段的自然扩展,两者都是具有关联类型的命名成员,而且访问字段和属性的语法是相同的。然而,属性与字段不同,不表示存储位置。相反,属性有访问器(accessor),这些访问器指定在它们的值被读取或写入时需执行的语句。
属性的声明类似于字段,不同之处在于属性的声明以定界符{}之间的get访问器和/或set访问器结束,而不是分号。同时包含get访问器和set访问器的属性称为读写属性(read-write property)。只具有get访问器的属性称为只读属性(read-only property)。只具有set访问器的属性称为只写属性(write-only property)。
get访问器相当于一个具有属性类型返回值的无参数方法。除了作为赋值的目标外,当在表达式中引用属性时,会调用该属性的get访问器以计算该属性的值。
set访问器相当于一个具有单个名为value的参数和无返回类型的方法。当一个属性作为赋值的目标,或者作为++或--运算符的操作数被引用时,就会调用set访问器,所传递的自变量将提供新值。
List类声明了两个属性Count和Capacity,依次是只读和只写的。下面是使用这些属性的示例:
List names = new List();
names.Capacity = 100; //调用set访问器
int i = names.Count; //调用get访问器
int j = names.Capacity; //调用get访问器
与字段和方法类似,对于实例属性和静态属性,C#两者都支持。静态属性是声明中具有static修饰符,而实例属性则没有。
属性的访问器可以是虚拟的。当属性声明中包含virtual,abstract,override修饰符时,它们将运用到属性访问器。
索引器
索引器是这样一个成员:它使对象能够用与数组相同的方式进行索引。索引器的声明与属性很相似,不同之处在于成员的名字是this,后面的参数列表是在定界符([])之间。参数在索引器的访问器中是可用的。与属性类似,索引器可以是读写、只读、只写的,并且索引器的访问器也可以是虚拟的。
List类声明了单个读写索引器,接受一个int型的参数。通过索引器就可能用int值索引List实例。例如:
List names = new List();
names.Add("Liz");
names.Add("Martha");
names.Add("Beth");
for (int i = 0; i < names.Count; i++) {
string s = (string) names[i];
names[i] = s.ToUpper();
}
索引器能够被重载,意味着可以声明多个索引器,只要它们的参数个数或类型不同。
事件
事件是使对象或类能够提供通知的成员。事件的声明与字段的类似,不同之处在于事件声明包含一个event关键字,并且事件声明的类型必须是委托类型。
在包含事件声明的类中,事件可以像委托类型的字段一样使用(这样的事件不能是 abstract,而且不能声明访问器)。该字段保存了一个委托的引用,表示事件处理程序已经被添加到事件上。如果尚未添加任何事件处理程序,则该字段为null。
List类声明了名为Changed的单个事件成员,Changed事件表明有一个新项添加到事件处理程序列表,它由OnChanged虚拟方法引发,它首先检查事件是否为null(意思是没有事件处理程序)。引发事件的通知正好等价于调用事件所表示的委托——因此,不需要特殊的语言构件引发事件。
客户通过事件处理程序(event handler)响应事件。使用“+=”运算符添加或者使用“-=”移除事件处理程序。下面的示例添加一个事件处理程序到List类的Changed事件:
using System;
class Test
{
static int changeCount;
static void ListChanged(object sender, EventArgs e) {
changCount++;
}
static void Main() {
List names = new List();
names.Changed += new EventHandler(ListChanged);
names.Add("Liz");
names.Add("Martha");
names.Add("Beth");
Console.WriteLine(changeCount); //输出 "3"
}
}
对于要求控制事件的底层存储的更高级场景译注8,事件的声明可以显式地提供add和remove访问器,它们在某种程度上类似于属性的set访问器。
运算符
运算符(operator)是一种函数成员,用来定义可应用于类实例的特定表达式运算符的含义。有三种运算符能够被定义:一元运算符、二元运算符和转换运算符。所有的运算符必须声明为public和static。
List类声明了两个运算符,运算符 “==”和运算符 “!=”,并且向表达式赋予新的含义,而这些表达式将这些运算符应用到List实例上。特别指出,这些运算符定义了两个List对象的相等比较,即使用它们的Equals方法进行比较。下面的示例使用“==”运算符比较两个List实例。
using System;
class Test
{
static void Main() {
List a = new List();
a.Add(1);
a.Add(2);
List b = new List();
b.Add(1);
b.Add(2);
Console.WriteLine(a == b); //输出 "True"
b.Add(3);
Console.WriteLine(a == b); //输出 "False"
}
}
第一个Console.WriteLine输出True,原因是两个List集合对象包含个数和值都相同的对象。假如List没有定义运算符 “==”,那么第一个Console.WriteLine将输出False,因为a和b引用不同的List实例。
析构函数
析构函数(destructor)是用于实现析构类实例所需操作的成员。析构函数不能带参数,不能具有可访问性修饰符,也不能被显式地调用。垃圾回收期间会自动调用所涉及实例的析构函数。
垃圾回收器在决定何时回收对象和运行析构函数方面采取宽松的策略。特别指出,析构函数的调用时机是不确定的,并且析构函数可能运行在任何线程上。由于这些或者其他原因,只有没有其他可行的解决方案,类才实现析构函数。
四、继承
1、继承的类型
(1)实现继承和接口继承
实现继承:表示一个类型派生于一个基类型,拥有该基类型的所有成员字段和函数。
接口继承:表示一个类型只继承了函数的签名,没有继承任何实现代码。在需要指定该类型具有某些可用的特性时,最好使用这种类型的继承。
(2)多重继承
(3)结构和类
结构总派生于System.ValueType,它们还可以派生于任意多个接口。
类总是派生于用户选择的另一个类,它们可以派生于任意多个接口。
2、实现继承
虚方法:把基类函数声明为Virtual。在C#中,函数默认情况下不是虚拟的,但(除了构造函数以外)可以显示地声明为Virtual。派生类中重写函数时,使用override关键破败显式声明。成员字段和静态函数都不能声明为Virtual,因为这个概念只对类中的实例函数成员有意义。
隐藏方法:如果签名相同的方法在基类和派生类中都进行了声明,但该方法没有声明为virtual和override,派生类方法就会隐藏基类方法。大多数情况下都是重写基类方法。但是C#中应使用new关键字声明我们隐藏一个基类的方法。
调用函数和基类版本:base.<MethodName>()
抽象类和抽象函数
抽象类不能被实例化,而抽象函数没有执行代码,必须在非抽象的派生类中重写。显然,抽象函数也是虚拟的(但也不需要提供virtual关键字,实际上,如果提供了该关键字,就会产生一个语法错误)。如果类包含抽象函数,该类将也是抽象的,也必须声明为抽象的。
密封类和密封方法
C#允许把类和方法声明为Sealed,对于类,这表示不能继承该类;对于方法来说,这表示不能生写该方法。
派生类的构造函数
当两个类之间有继承关系时,第一次构造子类的实例时,是按照如下顺序进行的:
1.子类的静态成员初始化语句
2.子类的静态构造函数
3.子类的非静态成员初始化语句
4.父类的静态成员初始化语句
5.父类的静态构造函数
6.父类的非静态成员初始化语句
7.父类的构造函数
8.子类的构造函数
看以下例子
using System;
public class A
{
public A()
{
Console.WriteLine("A");
}
}
public class B
{
public B()
{
Console.WriteLine("B");
}
}
public class C : A
{
B newb = new B();
}
class MainClass
{
public static void Main()
{
C newc = new C();
Console.ReadLine();
}
}
输出结果:
B
A
如果在C中也创建一个构造函数输出“C”,即添加:
public C()
{
Console.WriteLine("C");
}
则整个程序运行的结果为:
B
A
C
事实上,出现上述顺序的原因是,在执行完子类的静态构造函数后,程序会接着执行子类的构造函数,但是,此时,程序会先自动访问该构造函数继承的父类的构造函数(如果没有显式继承,那么就默认继承父类的默认构造函数,如果父类没有默认构造函数,那么会出现编译错误),而且,由于是第一次访问父类,因此会先运行静态成员初始化的代码和静态构造函数。虽然是小地方,不过,如果不注意的话,在特殊情况下会造成不必要的逻辑错误
3、修饰符
可见性修饰符
修饰符 应用于 说明
public 所有的类型或成员 任何代码均可以访问该方法
protected 类型和内嵌类型的所有成员 只有派生的类型能访问该方法
internal 类型和内嵌类型的所有成员 只能在包含它的程序集中访问该方法
private 所有的类型或成员 只能在它所属的类型中访问该方法
protected internal 类型和内嵌类型的所有成员 只能在包含它的程序集和派生类型的代码中访问该方法
注意,类型字义可以是公共或私有的,这取决于是否希望在包含类型的程序集外部访问它。不能把类型定义为protected、internal和protected internal,因为这些修饰符对于包含在命名空间中的类型来说是没有意义的。因此这些修饰符只能应用于成员。但是可以用这些修饰符定义嵌套的类型(即包含在其他类型中的类型),因为在这种情况下,类型也具有成员的状态。
其他修饰符
修饰符 应用于 说明
new 函数成员 成员用相同的签名隐藏继承的成员
static 所以的成员 成员不在类的具体实例上执行
virtual 仅类和函数成员 成员可以由派生类重写
abstract 仅函数成员 虚拟成员定义了成员的签名,但没有提供实现代码
override 仅函数成员 成员重写了继承的虚拟或抽象成员
sealed 类 成员重写了继承的虚拟成员,但继承该类的任何类都不能重写该成员。该修饰符必须与override一起使用
extern 仅静态[DllImport]方法 成员在外部用另一种语言实现
在这些修饰符中,internal和protected internal是C#和.NET Framework新增的。internal与public类似,但访问仅限于同一个程序集中的其他代码,换言之,在同一个程序中同时编译的代码。protected internal合并了protected和internal,但这是一种OR合并,而不是AND合并。protected internal成员在同一个程序集的任何代码中都可见,在派生类中也可见,甚至在其他程序集中也可见。
4、接口
interface声明。不能实例化接口,它只能包含其成员签名,接口不能有构造函数或字段。接口定义也不允许包含运算符重载(因为包含运算符重载会引起一些与其他.NET语言不兼容的问题,例如VB.NET不支持运算符重载)。在接口定义中还不允许声明成员上的修饰符。接口成员总是公共的,不能声明为虚拟或静态。如果需要,就应由执行的类来声明,因此最好通过执行的类来声明访问修饰符。