类与对象
介绍C#面向对象编程的基本内容。
与使用C语言等结构化编程语言不一样,使用C#编程,所有的程序代码几乎都放在类中,不存在独立于类之外的函数。因此,类是面向对象编程的基本单元。
在绝大多数面向对象语言中,一个类都可以包含两种成员:字段(Field)与方法(Method)。字段与方法这两个概念是面向对象理论的术语,是通用于各种面向对象语言的。而在各种的具体面向对象语言(比如C#)中,可以简单的这样理解:
字段即变量,方法即函数。
类的字段一般代表类中被处理的数据,类的方法大多代表对这些数据的处理过程或用于实现某种特定的功能,方法中的代码往往需要访问字段保存的数据。
在C#中,定义若干个变量,写若干个函数,将这些代码按以下格式汇集起来,再起个有意义的名字,就完成了一个类的定义:
[public|private] class 类名
{
[public|private] 数据类型 变量名;
[public|private] 数据类型 函数名(参数列表)
{
}
}
在上述类的定义中,方括号代表这部分可选,而竖线则代表多选一。声明为public的变量和函数可以被外界直接访问,与此对应,private的变量与函数,则为类的私有成员,只能由类自己使用。
下面简要介绍组成类的基本成员。
2.1 类的字段
字段(Field)代表了类中的数据,在类的所有方法之外定义一个变量即定义了一个字段。在变量之前可以加上public、private和protected表示字段的访问权限。以下代码展示了在类Student中定义的两个公有字段Age和SN,外界可以通过类Student创建的对象来读取或设置这两个字段的值。
可以在定义字段的同时给予一个初始值,如下所示:
class Student
{
public int age=18; //年龄
public string SN="1220040110"; //学号
}
类的方法
(1)函数的概念
为了解决代码重复的问题,绝大多数程序设计语言都将完成某一公用功能的多个语句组合在一起,起一个名字用于代表这些语句的全体,这样的代码块被称为“函数(function)”。引入“函数”概念之后,程序中凡需要调用此公用功能的地方都可以只写出函数名,此名字就代表了函数中所包含的所有代码,这样一来,就不再需要在多个地方重复书写这些功能代码。
C#中一个函数的语法格式如下所示:
返回值类型 方法名(参数列表)
{
语句1;
语句2;
//……
return 表达式;
}
下面是一个典型的C#函数示例:
int Add(int x,int y)
{
return x+y;
}
函数需要向外界返回一个值,由return语句实现。
如果一个函数没有返回值或不关心其返回值,则将其返回值定义为void。
void f() //不返回任何值得函数
{
语句1;
语句2;
//……
return ; //在return后写一个空语句
}
(2)方法的定义与使用
放在一个类中的函数(通常附加一个存取权限修饰符如 public和private)称为“方法(method)”。
访问一个方法的最基本方式是通过类创建的对象。例如以下代码在类MathOpt中定义了一个Add()方法:
public class MathOpt
{
public int Add(int x,int,y)
{
return x+y;
}
}
则可以通过使用new关键字创建类MathOpt的对象来访问此Add()方法:
class Program
{
static void Main(string[] args)
{
//创建MathOpt类的对象
MathOpt obj=new MathOpt();
//通过对象调用类的方法,结果保存在局部变量中
int result=obj.Add(100,200);
//……
}
}
(3)方法重载
方法重载是面向对象语言(如C#)对结构化编程语言(如C)的一个重要扩充,请看以下代码:
class MathOpt
{
//整数相加
public int Add(int x,int y)
{
return x+y;
}
//浮点数相加
public double Add(double x,double y)
{
return x+y;
}
}
上述两个函数有以下独特之处:
(1)函数名相同,均为Add;
(2)参数类型不同,一个为int,另一个为double。
这两个同名的函数彼此构成了“重载(Overload)”关系。
重载函数的调用代码:
MathOpt mathobj=null; //定义MathOpt对象变量
mathobj=new MathOpt(); //创建对象
int IResult=mathobj.Add(100,200); //调用类的整数相加方法
double FResult=mathobj.Add(5.5,9.2); //调用类的浮点数相加方法
Console.WriteLine("100+200="+IResult); //输出结果
Console.WriteLine("5.5+9.2="+FResult); //输出结果
请注意两个方法调用语句。传给方法实参为浮点数时,将调用参数类型为double的Add(double ,double )方法,传给方法的实参为整数时,调用参数类型为int的Add(int ,int )方法。
函数重载是面向对象语言对比结构化语言特性的重要扩充,在面向对象的编程中应用极广。
两个构成重载关系的函数必须满足以下条件:
(1)函数名相同。
(2)参数类型不同,或参数个数不同。
需要注意的是,函数返回值类型的不同不是函数重载的判断条件。
比如,函数
public long Add(int x,int y){……}
就不与函数
public int Add(int x,int y){……}
构成重载关系,因为这两个函数的实参类型和数目都相同,仅函数返回值类型不同。
另外要注意C#是区分大小写的语言,因此,如果一个类中有以下两个函数:
public long add(int x,int y){……}
public int Add(int x,int y){……}
则可以通过编译并且程序可以运行,但这并不是说这两个函数构成了重载关系,事实上,C#认为这是两个完全不同的函数,彼此之间一点关系也没有!
类的静态成员
类中的函数,如果在声明时没有加“static”关键字,则称之为类的“实例方法(instance method)”。加了static关键字的方法称为“静态方法(static method)”。
类似地,加了static关键字的字段称为“静态字段(static field)”。
(1)访问类的静态成员的基本方法
.NET Framework提供了大量的静态方法供开发人员使用,最典型的是数学库函数,.NET Framework将常用的数学函数放到了类Math中,例如以下代码计算2的3次方:
double ret=Math.Pow(2,3);
对比前面介绍过的类实例方法的调用方式,可以发现静态方法在使用时不需要创建对象,而是按以下格式直接调用:
类名.静态方法名(参数列表)
类的实例方法可以直接访问类的公有静态字段,比如数字常数π就是Math类的公有静态字段,可以被用来计算圆周长:
//计算圆周长
double GetPerimeterOfCircle(Double radius)
{
return 2*Math.PI*radius;
}
(2)类静态成员的特性
类的静态成员有一个特殊的性质,先来看一个例子来说明这一点。
给类StaticMembers增加一个普通的实例方法increaseValue()和实例字段dynamicVar:
class StaticMembers
{
public static int staticVar=0; //静态字段
public int dynamicVar=0;
public void increaseValue()
{
staticVar++;
dynamicVar++;
}
}
在increaseValue()方法中,对类的静态字段staticVar和实例字段dynamicVar都进行了自增操作。
以下是测试代码:
static void Main(string[] args)
{
StaticMembers obj=null;
//创建100个对象
for(int i=0;i<=100;i++)
{
obj=new StaticMembers();
obj.increasseValue();
}
//查看静态字段与普通字段的值
Console.WriteLine("dynamicVar="+obj.dynamicVar);
Console.WriteLine("staticVar="+StaticMembers.staticVar);
//程序暂停,敲任意键继续
Console.ReadKey();
}
程序的运行结果:
dynamicVar=1
staticVar=100
因为类的静态成员拥有以下特性:
类的静态成员是供类的所有对象所共享的。
在本节示例中创建了100个对象,每个对象拥有1个dyanmicVar字段,一共有100个dyanmicVar字段,这些字段是独立的,“互不干涉内政”。而staticVar字段仅有一个,为所有对象所共享。因此,任何一个对象对staticVar字段的修改,都会被其他对象所感知。
(3)类实例成员与静态成员的访问规则
在面向对象的程序中,对类的实例和静态成员,有以下访问规则:
① 位于同一类中的实例方法可直接相互调用。
② 类的字段(包括实例字段和静态字段)可以被同一类中的所有实例方法直接访问。
③ 类中的静态方法只能直接访问类静态字段。
上述规则中,需要特别注意在静态方法中直接调用类的实例方法是非法的。例如以下代码将无法通过编译:
class StaticMembers
{
public static int staticVar=0; //静态字段
public static void staticFunc() //静态方法
{
increaseValue(); //静态方法中不能调用实例方法 错误
dynamicVar++; //静态方法中不能访问实例字段 错误
staticVar++; //静态方法可以访问静态字段 正确
}
public int dynamicVar=0;
public void increaseValue()
{
staticVar++;
dynamicVar++;
}
}
静态方法中只允许访问静态数据,那么,如何在静态方法中访问实例数据?
很简单,可以在静态方法中创建对象,通过对象来访问即可。将静态方法staticFunc()修改如下即可通过编译:
public static void staticFunc() //静态方法
{
//在静态方法中通过对象访问类的实例成员
StaticMembers obj=new StaticMembers();
obj.increaseValue(); //调用实例方法,OK!
obj.dynamicVar++; //访问实例字段,OK!
}
类的属性
属性是一种特殊的“字段”。
先来看一个用于表示学生信息的类Student:
class Student
{
public String Name; //姓名
public DateTime Birthday; //生日
public int Age; //年龄
}
Student类中使用公有字段来表达学生信息,这种方式无法保证数据的有效性。比如外界完全可以这样使用Student类:
Student stu=new Student();
stu.Name=""; //非法数据,名字怎能为空?
stu.Birthday=new DateTime(3000,1,3); //公元3000年出生,他来自未来世界?
stu.Age=-1; //年龄必须大于0!
在设计类时使用属性(Property)可以保证只有合法的数据可以传给对象。
以Name这个字段为例,它要求不能为空。
首先,定义一个私有的_Name字段;
private String _Name="姓名默认值";
接着,即可定义一个Name属性:
public String Name
{
get //读
{
return _Name;
}
set //写,使用隐含变量value
{
if(value.Length==0)
throw new Exception("名字不能为空");
_Name=value;
}
}
Name属性由两个特殊的读访问器(get)和写访问器(set)组成。
当读取Name属性时,读访问器被调用,仅简单的向外界返回私有字段_Name的值。
当设置Name属性时,写访问器被调用,先检查外界传入的值是不是空串,再将传入的值保存于私有字段中。
经过这样的设计,以下的代码在运行时会抛出一个异常提醒程序员出现了错误需要更正:
Student stu=new Student();
stu.Name=" "; //非法数据,名字怎能为空?
写访问器中有一个特殊的变量value必须特别注意,它代表了外界传入的值,例如以下代码向Name属性赋值:
Student stu=new Student();
stu.Name="张三";
“张三”这一字串值将被保存到value变量中,供写访问器使用。
由上述例子可知,编写属性的方法如下:
(1)设计一个私有的字段用于保存属性的数据;
(2)设计get读访问器和set写访问器存取私有字段数据。
C#中还允许定义只读属性和只写属性。只读属性只有get读访问器,而只写属性只有set写访问器。
深入理解类与对象
(1)类和对象的区别
① 对象是以类为模板创建出来的。类与对象之间是一对多的关系。
② 在C#中,使用new关键字创建对象。
③ 在程序中“活跃”的是对象而不是类。
在面向对象领域,对象有时又被称为是“类的实例”,“对象”与“类的实例”这两个概念是等同的。
(2)类的构造函数
当使用new关键字创建一个对象时,一个特殊的函数被自动调用,这就是类的构造函数(constructor)。
在C#中,类的构造函数与类名相同,没有返回值。
class A
{
//类A的构造函数
public A()
{
}
}
类的构造函数在以类为模板创建对象时被自动调用。构造函数一般用于初始化类的私有数据字段。
(3)引用类型与值类型
.NET将变量的类型分为“值类型”与“引用类型”两大类。诸如int和float之类的变量属于值类型,而“类”类型的变量则属于“引用类型”。
值类型变量与引用类型变量在使用上是有区别的。
值类型的变量一定义之后就马上可用。比如定义“int i;”之后,变量i即可使用。
引用类型的变量定义之后,还必须用new关键字创建对象后才可以使用,我们在前面已经多次这样使用过引用变量了。
在Visual Studio随机文档中,详细地列出了每种数据类型属于值类型还是引用类型:
类别 | 说明 | |
值类型 | 简单类型 | 有符号整型:sbyte,short,int,long |
无符号整型:byte,ushort,uint,ulong | ||
Unicode字符:char | ||
IEEE浮点型:float,double | ||
高精度小数:decimal | ||
布尔型:bool | ||
枚举类型 | enum E{...}形式的用户定义类型 | |
结构类型 | struct S{...}形式的用户定义类型 | |
引用类型 | 类类型 | 所有其他类型的最终基类:object |
Unicode字符串:string | ||
class C{...}形式的用户定义类型 | ||
接口类型 | interface I{...}形式的用户定义类型 | |
数组类型 | 一维和多维数组,例如int[]和int[,] | |
委托类型 | delegate T D(...)形式的用户定义类型 |
在上一篇博客中介绍了堆栈与托管堆
值类型变量与引用类型变量的内存分配模型也不一样。为了理解清楚这个问题,读者首先必须区分两种不同类型的内存区域:线程堆栈(Thread Stack)和托管堆(Managed Heap)。
每个正在运行的程序都对应着一个进程(process),在一个进程内部,可以有一个或多个线程(thread),每个线程都拥有一块“自留地”,称为“线程堆栈”,大小为1M,用于保存自身的一些数据,比如函数中定义的局部变量、函数调用时传送的参数值等,这部分内存区域的分配与回收不需要程序员干涉。
所有值类型的变量都是在线程堆栈中分配的。
另一块内存区域称为“堆(heap)”,在.NET这种托管环境下,堆由CLR进行管理,所以又称为“托管堆(managed heap)”。
用new关键字创建的类的对象时,分配给对象的内存单元就位于托管堆中。
在程序中我们可以随意地使用new关键字创建多个对象,因此,托管堆中的内存资源是可以动态申请并使用的,当然用完了必须归还。
打个比方更易理解:托管堆相当于一个旅馆,其中的房间相当于托管堆中所拥有的内存单元。当程序员用new方法创建对象时,相当于游客向旅馆预订房间,旅馆管理员会先看一下有没有合适的空房间,有的话,就可以将此房间提供给游客住宿。当游客旅途结束,要办理退房手续,房间又可以为其他旅客提供服务了。
从列表可以看到,引用类型共有四种:类类型、接口类型、数组类型和委托类型。
所有引用类型变量所引用的对象,其内存都是在托管堆中分配的。
严格地说,我们常说的“对象变量”其实是类类型的引用变量。但在实际中人们经常将引用类型的变量简称为“对象变量”,用它来指代所有四种类型的引用变量。在不致于引起混淆的情况下,本节也采用了这种惯例。
在了解了对象内存模型之后,对象变量之间的相互赋值的含义也就清楚了。请看以下代码:
01 class A
02 {
03 public int i;
04 }
05 class Program
06 {
07 static void Main(string[] args)
08 {
09 A a;
10 a=new A();
11 a.i=100;
12 A b=null;
13 b=a; //对象变量的相互赋值
14 Console.WriteLine("b.i="+b.i); //b.i=?
15 }
16 }
注意第12和13句。
程序的运行结果是:
b.i=100;
请读者思索一下:两个对象变量的相互赋值意味着什么?
事实上,两个对象变量的相互赋值意味着赋值后两个对象变量所占用的内存单元其内容是相同的。
讲得详细一些:
第10句创建对象以后,其首地址(假设为“1234 5678”)被放入到变量a自身的4个字节的内存单元中。
第12句又定义了一个对象变量b其值最初为null(即对应的4个字节内存单元中为“0000 0000”)。
第13句执行以后,a变量的值被复制到b的内存单元中,现在,b内存单元中的值也为“1234 5678”。
根据前面介绍的对象内存模型,我们知道现在变量a和b都指向同一个实例对象。
如果通过b.i修改字段i的值,a.i也会同步变化,因为a.i与b.i其实代表同一对象的同一字段。
由此得到一个重要结论:
对象变量的相互赋值不会导致对象自身被复制,其结果是两个对象变量指向同一对象。
另外,由于对象变量本身是一个局部变量,因此,对象变量本身是位于线程堆栈中的。
严格区分对象变量与对象变量所引用的对象,是面向对象编程的关键之一。
由于对象变量类似于一个对象指针,这就产生了“判断两个对象变量是否引用同一对象”的问题。
C#使用“==”运算符比对两个对象变量是否引用同一对象,“!=”比对两个对象变量是否引用不同的对象。参看以下的代码:
//a1与a2引用不同的对象
A a1=new A();
A a2=new A();
Console.WriteLine(a1==a2); //输出:false
a2=a1; //a1与a2引用相同的对象
Console.WriteLine(a1==a2); //输出:true
需要注意的是,如果“==”被用在值类型的变量之间,则对比的是变量的内容:
int i=0;
int j=100;
if(i==j)
{
Console.WriteLine("i与j的值相等");
}
理解值类型与引用类型的区别在面向对象编程中非常关键。