二类对象方法和作用域讲解.docx
《二类对象方法和作用域讲解.docx》由会员分享,可在线阅读,更多相关《二类对象方法和作用域讲解.docx(88页珍藏版)》请在冰豆网上搜索。
二类对象方法和作用域讲解
类、对象、方法和作用域
一、什么是类和对象
二、构造与析构
三、什么是方法
四、声明方法
五、调用方法
六、编写方法
七、运用作用域
八、多态与new关键字
九、常用函数和方法
十、对象封装和类
十一、集合对象
十二、类的私有成员
十三、装箱与拆箱
十四、C#类的继承机制
一、什么是类和对象
组件编程不是对传统面向对象的抛弃,相反组件编程正是面向对象编程的深化和发展。
类作为面向对象的灵魂在C#语言里有着相当广泛深入的应用,很多非常“Sharp”的组件特性甚至都是直接由类包装而成。
对类的深度掌握自然是我们“SharpXP”重要的一环。
类
C#的类是一种对包括数据成员,函数成员和嵌套类型进行封装的数据结构。
其中数据成员可以是常量,域。
函数成员可以是方法,属性,索引器,事件,操作符,实例构建器,静态构建器,析构器。
我们将在“第五讲构造器与析构器”和“第六讲域方法属性与索引器”对这些成员及其特性作详细的剖析。
除了某些导入的外部方法,类及其成员在C#中的声明和实现通常要放在一起。
C#用多种修饰符来表达类的不同性质。
根据其保护级C#的类有五种不同的限制修饰符:
public可以被任意存取;
protected只可以被本类和其继承子类存取;
internal只可以被本组合体(Assembly)内所有的类存取,组合体是C#语言中类被组合后的逻辑单位和物理单位,其编译后的文件扩展名往往是“.DLL”或“.EXE”。
protectedinternal唯一的一种组合限制修饰符,它只可以被本组合体内所有的类和这些类的继承子类所存取。
private只可以被本类所存取。
如果不是嵌套的类,命名空间或编译单元内的类只有public和internal两种修饰。
new修饰符只能用于嵌套的类,表示对继承父类同名类型的隐藏。
abstract用来修饰抽象类,表示该类只能作为父类被用于继承,而不能进行对象实例化。
抽象类可以包含抽象的成员,但这并非必须。
abstract不能和new同时用。
下面是抽象类用法的伪码:
abstractclassA
{
publicabstractvoidF();
}
abstractclassB:
A
{
publicvoidG(){}
}
classC:
B
{
publicoverridevoidF()
{
//方法F的实现
}
}
抽象类A内含一个抽象方法F(),它不能被实例化。
类B继承自类A,其内包含了一个实例方法G(),但并没有实现抽象方法F(),所以仍然必须声明为抽象类。
类C继承自类B,实现类抽象方法F(),于是可以进行对象实例化。
sealed用来修饰类为密封类,阻止该类被继承。
同时对一个类作abstract和sealed的修饰是没有意义的,也是被禁止的。
对象与this关键字
类与对象的区分对我们把握OO编程至关重要。
我们说类是对其成员的一种封装,但类的封装设计仅仅是我们编程的第一步,对类进行对象实例化,并在其数据成员上实施操作才是我们完成现实任务的根本。
实例化对象采用MyClassmyObject=newMyClass()语法,这里的new语义将调用相应的构建器。
C#所有的对象都将创建在托管堆上。
实例化后的类型我们称之为对象,其核心特征便是拥有了一份自己特有的数据成员拷贝。
这些为特有的对象所持有的数据成员我们称之为实例成员。
相反那些不为特有的对象所持有的数据成员我们称之为静态成员,在类中用static修饰符声明。
仅对静态数据成员实施操作的称为静态函数成员。
C#中静态数据成员和函数成员只能通过类名引用获取,看下面的代码:
usingSystem;
classA
{
publicintcount;
publicvoidF()
{
Console.WriteLine(this.count);
}
publicstaticstringname;
publicstaticvoidG()
{
Console.WriteLine(name);
}
}
classTest
{
publicstaticvoidMain()
{
Aa1=newA();
Aa2=newA();
a1.F();
a1.count=1;
a2.F();
a2.count=2;
A.name="CCW";
A.G();
}
}
我们声明了两个A对象a1,a2。
对于实例成员count和F(),我们只能通过a1,a2引用。
对于静态成员name和G()我们只能通过类型A来引用,而不可以这样a1.name,或a1.G()。
在上面的程序中,我们看到在实例方法F()中我们才用this来引用变量count。
这里的this是什么意思呢?
this关键字引用当前对象实例的成员。
在实例方法体内我们也可以省略this,直接引用count,实际上两者的语义相同。
理所当然的,静态成员函数没有this指针。
this关键字一般用于从构造函数、实例方法和实例访问器中访问成员。
在构造函数中this用于限定被相同的名称隐藏的成员,例如:
classEmployee
{
publicEmployee(stringname,stringalias)
{
this.name=name;
this.alias=alias;
}
}
将对象作为参数传递到其他方法时也要用this表达,例如:
CalcTax(this);
声明索引器时this更是不可或缺,例如:
publicintthis[intparam]
{
get
{
returnarray[param];
}
set
{
array[param]=value;
}
}
System.Object类
C#中所有的类都直接或间接继承自System.Object类,这使得C#中的类得以单根继承。
如果我们没有明确指定继承类,编译器缺省认为该类继承自System.Object类。
System.Object类也可用小写的object关键字表示,两者完全等同。
自然C#中所有的类都继承了System.Object类的公共接口,剖析它们对我们理解并掌握C#中类的行为非常重要。
下面是仅用接口形式表示的System.Object类:
namespaceSystem
{
publicclassObject
{
publicstaticboolEquals(objectobjA,objectobjB){}
publicstaticboolReferenceEquals(objectobjA,objectobjB){}
publicObject(){}
publicvirtualboolEquals(objectobj){}
publicvirtualintGetHashCode(){}
publicTypeGetType(){}
publicvirtualstringToString(){}
protectedvirtualvoidFinalize(){}
protectedobjectMemberwiseClone(){}
}
我们先看object的两个静态方法Equals(objectobjA,objectobjB),ReferenceEquals(objectobjA,objectobjB)和一个实例方法Equals(objectobj)。
在我们阐述这两个方法之前我们首先要清楚面向对象编程两个重要的相等概念:
值相等和引用相等。
值相等的意思是它们的数据成员按内存位分别相等。
引用相等则是指它们指向同一个内存地址,或者说它们的对象句柄相等。
引用相等必然推出值相等。
对于值类型关系等号“==”判断两者是否值相等(结构类型和枚举类型没有定义关系等号“==”,我们必须自己定义)。
对于引用类型关系等号“==”判断两者是否引用相等。
值类型在C#里通常没有引用相等的表示,只有在非托管编程中采用取地址符“&”来间接判断二者的地址是否相等。
静态方法Equals(objectobjA,objectobjB)首先检查两个对象objA和objB是否都为null,如果是则返回true,否则进行objA.Equals(objB)调用并返回其值。
问题归结到实例方法Equals(objectobj)。
该方法缺省的实现其实就是{returnthis==obj;}也就是判断两个对象是否引用相等。
但我们注意到该方法是一个虚方法,C#推荐我们重写此方法来判断两个对象是否值相等。
实际上Microsoft.NET框架类库内提供的许多类型都重写了该方法,如:
System.String(string),System.Int32(int)等,但也有些类型并没有重写该方法如:
System.Array等,我们在使用时一定要注意。
对于引用类型,如果没有重写实例方法Equals(objectobj),我们对它的调用相当于this==obj,即引用相等判断。
所有的值类型(隐含继承自System.ValueType类)都重写了实例方法Equals(objectobj)来判断是否值相等。
注意对于对象x,x.Equals(null)返回false,这里x显然不能为null(否则不能完成Equals()调用,系统抛出空引用错误)。
从这里我们也可看出设计静态方法Equals(objectobjA,objectobjB)的原因了--如果两个对象objA和objB都可能为null,我们便只能用object.Equals(objectobjA,objectobjB)来判断它们是否值相等了--当然如果我们没有改写实例方法Equals(objectobj),我们得到的仍是引用相等的结果。
我们可以实现接口IComparable(有关接口我们将在“第七讲接口继承与多态”里阐述)来强制改写实例方法Equals(objectobj)。
对于值类型,实例方法Equals(objectobj)应该和关系等号“==”的返回值一致,也就是说如果我们重写了实例方法Equals(objectobj),我们也应该重载或定义关系等号“==”操作符,反之亦然。
虽然值类型(继承自System.ValueType类)都重写了实例方法Equals(objectobj),但C#推荐我们重写自己的值类型的实例方法Equals(objectobj),因为系统的System.ValueType类重写的很低效。
对于引用类型我们应该重写实例方法Equals(objectobj)来表达值相等,一般不应该重载关系等号“==”操作符,因为它的缺省语义是判断引用相等。
静态方法ReferenceEquals(objectobjA,objectobjB)判断两个对象是否引用相等。
如果两个对象为引用类型,那么它的语义和没有重载的关系等号“==”操作符相同。
如果两个对象为值类型,那么它的返回值一定是false。
实例方法GetHashCode()为相应的类型提供哈希(hash)码值,应用于哈希算法或哈希表中。
需要注意的是如果我们重写了某类型的实例方法Equals(objectobj),我们也应该重写实例方法GetHashCode()--这理所应当,两个对象的值相等,它们的哈希码也应该相等。
下面的代码是对前面几个方法的一个很好的示例:
usingSystem;
structA
{
publicintcount;
}
classB
{
publicintnumber;
}
classC
{
publicintinteger=0;
publicoverrideboolEquals(objectobj)
{
Cc=objasC;
if(c!
=null)
returnthis.integer==c.integer;
else
returnfalse;
}
publicoverrideintGetHashCode()
{
return2^integer;
}
}
classTest
{
publicstaticvoidMain()
{
Aa1,a2;
a1.count=10;
a2=a1;
//Console.Write(a1==a2);没有定义“==”操作符
Console.Write(a1.Equals(a2));//True
Console.WriteLine(object.ReferenceEquals(a1,a2));//False
Bb1=newB();
Bb2=newB();
b1.number=10;
b2.number=10;
Console.Write(b1==b2);//False
Console.Write(b1.Equals(b2));//False
Console.WriteLine(object.ReferenceEquals(b1,b2));//False
b2=b1;
Console.Write(b1==b2);//True
Console.Write(b1.Equals(b2));//True
Console.WriteLine(object.ReferenceEquals(b1,b2));//True
Cc1=newC();
Cc2=newC();
c1.integer=10;
c2.integer=10;
Console.Write(c1==c2);//False
Console.Write(c1.Equals(c2));//True
Console.WriteLine(object.ReferenceEquals(c1,c2));//False
c2=c1;
Console.Write(c1==c2);//True
Console.Write(c1.Equals(c2));//True
Console.WriteLine(object.ReferenceEquals(c1,c2));//True
}
}
如我们所期望,编译程序并运行我们会得到以下输出:
TrueFalse
FalseFalseFalse
TrueTrueTrue
FalseTrueFalse
TrueTrueTrue
实例方法GetType()与typeof的语义相同,它们都通过查询对象的元数据来确定对象的运行时类型,我们在“第十讲特征与映射”对此作详细的阐述。
实例方法ToString()返回对象的字符串表达形式。
如果我们没有重写该方法,系统一般将类型名作为字符串返回。
受保护的Finalize()方法在C#中有特殊的语义,我们将在“第五讲构造器与析构器”里详细阐述。
受保护的MemberwiseClone()方法返回目前对象的一个“影子拷贝”,该方法不能被子类重写。
“影子拷贝”仅仅是对象的一份按位拷贝,其含义是对对象内的值类型变量进行赋值拷贝,对其内的引用类型变量进行句柄拷贝,也就是拷贝后的引用变量将持有对同一块内存的引用。
相对于“影子拷贝”的是深度拷贝,它对引用类型的变量进行的是值复制,而非句柄复制。
例如X是一个含有对象A,B引用的对象,而对象A又含有对象M的引用。
Y是X的一个“影子拷贝”。
那么Y将拥有同样的A,B的引用。
但对于X的一个“深度拷贝”Z来说,它将拥有对象C和D的引用,以及一个间接的对象N的引用,其中C是A的一份拷贝,D是B的一份拷贝,N是M的一份拷贝。
深度拷贝在C#里通过实现ICloneable接口(提供Clone()方法)来完成。
对对象和System.Object的把握为类的学习作了一个很好的铺垫,但这仅仅是我们锐利之行的一小步,关乎对象成员初始化,内存引用的释放,继承与多态,异常处理等等诸多“Sharp”特技堪为浩瀚,让我们继续期待下面的专题!
二、构造与析构
构造器
构造器负责类中成员变量(域)的初始化。
C#的类有两种构造器:
实例构造器和静态构造器。
实例构造器负责初始化类中的实例变量,它只有在用户用new关键字为对象分配内存时才被调用。
而且作为引用类型的类,其实例化后的对象必然是分配在托管堆(ManagedHeap)上。
这里的托管的意思是指该内存受.NET的CLR运行时管理。
和C++不同的是,C#中的对象不可以分配在栈中,用户只声明对象是不会产生构造器调用的。
实例构造器分为缺省构造器和非缺省构造器。
缺省构造器是在一个类没有声明任何构造器的情况下,编译器强制为该类添加的一个无参数的构造器,该构造器仅仅调用父类的无参数构造器。
缺省构造器实际上是C#编译器为保证每一个类都有至少一个构造器而采取的附加规则。
注意这里的三个要点:
子类没有声明任何构造器;
编译器为子类加的缺省构造器一定为无参数的构造器;
父类一定要存在一个无参数的构造器。
看下面例子的输出:
usingSystem;
publicclassMyClass1
{
publicMyClass1()
{
Console.WriteLine(“MyClass1
ParameterlessContructor!
”);
}
publicMyClass1(stringparam1)
{
Console.WriteLine(“MyClass1
ConstructorParameters:
”+param1);
}
}
publicclassMyClass2:
MyClass1
{
}
publicclassTest
{
publicstaticvoidMain()
{
MyClass2myobject1=newMyClass2();
}
}
编译程序并运行可以得到下面的输出:
MyClass1ParameterlessContructor!
读者可以去掉MyClass1的无参构造器publicMyClass1()看看编译结果。
构造器在继承时需要特别的注意,为了保证父类成员变量的正确初始化,子类的任何构造器默认的都必须调用父类的某一构造器,具体调用哪个构造器要看构造器的初始化参数列表。
如果没有初始化参数列表,那么子类的该构造器就调用父类的无参数构造器;如果有初始化参数列表,那么子类的该构造器就调用父类对应的参数构造器。
看下面例子的输出:
usingSystem;
publicclassMyClass1
{
publicMyClass1()
{
Console.WriteLine("MyClass1ParameterlessContructor!
");
}
publicMyClass1(stringparam1)
{
Console.WriteLine("MyClass1
ConstructorParameters:
"+param1);
}
}
publicclassMyClass2:
MyClass1
{
publicMyClass2(stringparam1):
base(param1)
{
Console.WriteLine("MyClass2
ConstructorParameters:
"+param1);
}
}
publicclassTest
{
publicstaticvoidMain()
{
MyClass2myobject1=newMyClass2("Hello");
}
}
编译程序并运行可以得到下面的输出:
MyClass1ConstructorParameters:
Hello
MyClass2ConstructorParameters:
Hello
C#支持变量的声明初始化。
类内的成员变量声明初始化被编译器转换成赋值语句强加在类的每一个构造器的内部。
那么初始化语句与调用父类构造器的语句的顺序是什么呢?
看下面例子的输出:
usingSystem;
publicclassMyClass1
{
publicMyClass1()
{
Print();
}
publicvirtualvoidPrint(){}
}
publicclassMyClass2:
MyClass1
{
intx=1;
inty;
publicMyClass2()
{
y=-1;
Print();
}
publicoverridevoidPrint()
{
Console.WriteLine("x={0},y={1}",x,y);
}
}
publicclassTest
{
staticvoidMain()
{
MyClass2MyObject1=newMyClass2();
}
}
编译程序并运行可以得到下面的输出:
x=1,y=0
x=1,y=-1
容易看到初始化语句在父类构造器调用之前,最后执行的才是本构造器内的语句。
也就是说