第三章 C#面向对象初级编程 面向对象得程序设计越来越受到编程人员的喜爱。类和对象是面向对象程序设计中的重要概念。封装性、继承性和多态性是面向对象的特点,本章旨在全面说明C#编写面向对象程序设计的方法。
3.1 面向对象的基本概念 早期的程序设计方法多为面向过程的程序设计思想(POP,Procedure-Oriented Programming),在这种设计方法下,编程人员的主要任务是把一个处理分解成若干个过程,然后编写这些过程。每个过程都基于某些特定的算法。对于C语言来说就是编写一个个函数,每个函数的数据和程序代码是分离的,当修改某段程序时,所有与之有关的部分都需要作相应的调整。随着问题规模的增大,程序变得容易出错,而且越来越难以管理。这种面向过程的程序设计语言有C、Pascal、Basic等。 面向对象的程序设计(OOP,Object-Oriented Programming)则是一种基于结构分析的、以数据为中心的程序设计方法。它的主要思想是将数据及处理这些数据的操作都封装(Encapsulation)到一个称为类(Class)的数据结构中。使用这个类时,只需要定义一个类的变量即可,这个变量叫做对象(Object)。通过调用对象的数据成员完成对类的使用。这类编程思想较好地适应了现实世界中的问题,因而得以广泛应用。
3.1.1 对象和类 在我们的日常生活中,对象是指可辨识的一种实体。例如汽车、房子、书、文档和支票等。为了进一步说明,可以把对象看作是用于在程序中表示的一个实体。因此对象可以包括把有生命的“对象”——人、员工、客户,以及更抽象的“对象”——公司、数据库和国家等。 面向对象程序设计(OOP)通过使用真实世界的对象实例概念,改变了应用程序处理问题的模式。非面向对象的传统程序语言,必须针对特定问题开发应用程序,一旦所处理的问题脱离了原先考虑设计的范围,就得扩充应用程序得功能以面对新产生的问题;而在这种情形下,整个应用程序不是大幅改写就是必须重新开发。OOP程序设计理论的出现,解决了这样的缺陷,其中关键就在于对象的使用。 我们可以设计不同的对象,处理应用程序所要解决的各种问题,其中最大的好处在于应用程序可以轻易地针对新的问题进行扩充处理,只要修改原有的对象或是加入新定义的对象,完全不用重新改写原有的应用程序。
OOP得对象概念,使得开发应用程序得复杂度与难度,不会随着程序规模的扩大,变得难以处理与掌握。 类是一组具有相同数据结构和相同操作的对象的集合。类是一系列具有相同性质的对象的抽象,是对对象共同特征的描述。比如每一辆汽车是一个对象的话,所有的汽车可以作为一个模板,我们就定义汽车这个类。 在一个类中,每个对象都是类的实例,可以使用类中提供的方法。从类定义中产生对象,必须有建立实例的操作,C++和C#中的new操作符可用于建立一个类的实例,C#为我们提供的方法则更加安全。
3.1.2 继承 继承是使用己存在的定义作为基础建立新定义的技术。新类的定义可以是即存类所声明的数据和新类所增加的声明组合。新类复用即存的定义,而不要求修改即存类。即存类可以作为基类来引用,而新类可以作为派生类来引用。这种复用技术大大降低了软件的开发费用。 例如,汽车作为一个类己经存在,作为具有自身特征的卡车就可以从汽车类中继承。它同汽车一样,具有颜色、重量这些特征,可以行驶和鸣笛。它还具有一般汽车不一定具有的特征,比如可以载货等。
3.1.3 封装 一般来说,程序员都力求软件工程系统的高集成性。一个具有高集成性的软件系统包含着各种执行独立任务的成分,而每一个独立任务都是整个系统的重要组成部分。相反,如果一个软件系统的集成性差,那么系统所包含的各种成分由于没有很好的被定义而往往会容易发生冲突。 封装可以将对象相关的信息集中存放在一个独立的单元中,因此,用一个标识符就可以访问对象,还可以把整个对象作为一个变量参数传送给函数。
3.1.4 多态 多态性是指用一个名字定义不同的函数,这函数执行不同但又类似的操作,从而实现“一个接口,多种方法”。 多态性的实现与静态联编、动态联编有关。静态联编支持的多态性称为编译时的多态性,也称静态多态性,它是通过函数重载和运算符重载实现的。动态联编支持的多态性称为运行时的多态性,也称动态多态性,它是通过继承和虚函数实现的。
3.2 C#中的类与对象 3.2.1 在C#中定义类 类的声明格式如下: [类修饰符] class 类名 [:基类名] { 类的成员; } 类的修饰符可以是以下几种之一或者是它们的组合(在类的声明中同一修饰符不允许出现多次):
◆new——新建类,仅允许在嵌套类声明时使用,表明类中隐藏了由基类中继承而来的、与基类中同名的成员。 ◆public——公有类,表示不限制对该类的访问。 ◆protected一—保护类,表示只能从所在类和所在类派生的子类进行访问。 ◆internal——内部类,只有其所在类才能访问。 ◆private——私有类,只有对包.Net中的应用程序或库才能访问。 ◆abstract——抽象类,不允许建立类的实例。 ◆sealed——密封类,不允许被继承。 以上类修饰符可以两个或多个。
使用new关键字可以建立类的一个实例,比如下面的代码: class A { } class B void Fun A a; a=new A(); 在类B的方法Fun中创建了一个类A的实例。 我们使用如下代码表示类B从类A中继承: { } class B: A{ } 有关C#中的继承机制我们放在后面的章节中进行详细讨论,在这里要事先声明的一点是:C#中的类只支持单继承。
3.2.2 访问修饰符 类的成员有以下类型: ◆成员常量,代表与类相关的常量值。 ◆域,即类中的变量。 ◆成员方法,完成类中各种计算或功能的操作。 ◆属性,用于定义类中的值,并对它们提供读、写操作。 ◆事件,用于说明发生了什么事情。 ◆索引指示器,允许编程人员在访问数组时,通过索引指示器访问类的多个实例。 ◆操作符,定义类中特有的操作。 ◆构造函数,在类被实例化时首先执行的函数,主要是完成对象初始化操作。 ◆析构函数,在对象被销毁之前最后执行的函数,主要是完成对象结束时的收尾操作。
包含有可执行代码的成员被认为是类中的函数成员,这些函数成员有方法、属性、索引指示器、操作符、构造函数和析构函数。 在编写程序时,我们可以对类的成员使用不同的访问修饰符,从而定义它们的访问级别。 (1)公有成员 C#中的公有成员提供了类的外部界面,允许类的使用者从内部或外部直接进行访问。公有成员的修饰符为public,这是限制最少的一种访问方式。它的优先是使用灵活,缺点是外界可能会破坏对象成员值得合理性。 (2) 私有成员 C#中的私有成员仅限于类中的成员可以访问,从类的外部访问私有成员是不合法的。如果在声明中没有出现成员的访问修饰符,按照默认方式成员为私有的。私有成员的修饰符为private。 (3)保护成员 为了方便派生类的访问,又希望成员对于外界是隐藏的,这时可以使用protected修饰符,声明成员为保护成员。 (4)内部成员 使用internal修饰符的类的成员是一种特殊的成员。这种成员对于同一包中的应用程序或库是透明的,而在包.Net之外是禁止访问的。
使用下面的例子说明一下类的成员的访问修饰符的用法。 程序清单: class ClassA { public int a; private int b; protected int c; public void SetA() a=1;//正确,允许访问类自身公有成员 b=2;//正确,允许访问类自身私有成员 c=3;//正确,允许访问类自身保护成员 } class ClassB:A public void SetB()
ClassA BaseA=new ClassA(); BaseA.a=11;//正确,允许访问基类公有成员 BaseA.b=22;//错误,不允许访问基类私有成员 BaseA.c=33;//正确,允许访问基类保护成员 } class ClassC { public void AetB() BaseA.a=111;//正确,允许访问类的其他公有成员 BaseA.b=222;//错误,不允许访问类的其他私有成员 BaseA.c=333;//错误,不允许访问类的其他保护成员
(5) this保留字 保留字this仅限于在构造函数、类的方法和类的实例中使用,它有以下含义: ◆在类的构造函数中出现的this作为一个值类型,它表示对正在构造的对象本身的引用。 ◆在类的方法中出现的this作为一个值类型,它表示对调用该方法的对象的引用。 ◆在结构的构造函数中出现的this作为一个变量类型,它表示对正在构造的结构的引用。 ◆在结构的方法中出现的this作为一个变量类型,它表示对调用该方法的结构的引用。 除此以外,在其已地方使用this保留字都是不合法的。
案例:this保留字的使用 目标:学习保留字this的使用方法 步骤: 1、启动VS.NET,新建一个控制台应用程序,名称填写为“ThisTest”,位置设置为“c:\CSharpSamples\chp3。 2、在代码设计窗口中编辑Class1.cs。在其中的代码如下: using System; namespace thistest { class Class1 public int x; public void aaa()
x=5; Console.WriteLine("The value of x is: {0}",x); Console.WriteLine("The value of this. is: {0}", this.x); } static void Main(string[] args) { Class1 bb=new Class1(); bb.aaa();
5、按Ctrl + F5编译并运行该程序,效果如图3-1所示。图 3-1 程序运行结果 实际上,在C#内部,this被定义为一个常量。因此,使用this++,this--这样的语句都是不合法的。但是,this可以作为返回值来使用。 图 3-1 程序运行结果
3.2.3 实例化对象与构造函数 若将类中的某个成员声明为static该成员称为静态成员。类中的成员要么是静态,要么是非静态的。一般说来,静态成员是属于类所有的,非静态成员则属于类的实例化对象。
以下示例代码演示了如何声明静态和非静态成员。 程序清单: class Test { int x; static int y; void F() x=1;// 正确,等价于this.x=1 y=1;// 正确,等价于Test.y=1 } static void G() x=1;//错误,不能访问this.x y=1;//正确,等价于Test.y=1
static void Main(string[] args) { Test.t=new Test(); t.x=1;//正确 t.y=1;//错误,不能在类的实例中访问静态成员 Test.x=1;//错误,不能按类访问非静态成员 Test.y=1;//正确 }
类的非静态成员属于类的实例所有,每创建一个类的实例,都在内存中为非静态成员开辟了一块区域。而类的静态成员属于类所有,为这个类的所有实例所共享。无论这个类创建了多少个副本,一个静态成员在内存中只占有一块区域。 构造函数用于执行类的实例的初始化。每个类都有构造函数,即使我们没有声明它,编译器也会自动地为我们提供一个默认的构造函数。在访问一个类的时候,系统将最先执行构造函数中的语句。实际上,任何构造函数的执行都隐式地调用了系统提供默认的构造函数base()。
如果我们在类中声明了如下的构造函数, C(…){…} 它等价于: C(…):base(){…} 使用构造函数请注意以下几个问题: ◆一个类的构造函数通常与类名相同。 ◆构造函数不声明返回类型。 一般地,构造函数总是public类型的。如果是private类型的,表明类不能被实例化,这通常用于只含有静态成员的类。
下面的例子示范了构造函数的使用: class A { int x=0,y=0, count; public A() count=0; } public A(int vx,int vy) x=vx; y=vy;
构造函数的名字不能随便起,必须让编译器认得出才可以被自动执行。它的命名方法既简单又合理:让构造函数与类同名。除了名字外,构造函数的另一个特别之处是没有返回值类型,这与返回值类型为void的函数不同。如果它有返回值类型,那么编译器将不知所措。在你可以访问一个类的方法、属性或任何其它东西之前, 第一条执行的语句是包含有相应类的构造函数。甚至你自己不写一个构造函数,也会有一个缺省构造函数提供给你。
class TestClass { public TestClass(): base() {} // 由CLR提供 } 下面列举了几种类型的构造函数。 1)缺省构造函数 public TestClass(): base() {} 上面已介绍,它由系统(CLR)提供。
2)实例构造函数 实例构造函数是实现对类中实例进行初始化的方法成员。如: using System; class Point { public double x, y; public Point() { this.x = 0; this.y = 0; } public Point(double x, double y) { this.x = x; this.y = y; ……
class Test { static void Main() Point a = new Point(); Point b = new Point(3, 4); // 用构造函数初始化对象 …… }
声明了一个类Point,它提供了两个构造函数。它们是重载的。一个是没有参数的Point构造函数和一个是有两个double参数的Point构造函数。如果类中没有提供这些构造函数,那么会CLR会自动提供一个缺省构造函数的。但一旦类中提供了自定义的构造函数,如Point()和Point(double x, double y),则缺省构造函数将不会被提供,这一点要注意。
3)静态构造函数 静态构造函数是实现对一个类进行初始化的方法成员。它一般用于对静态数据的初始化。静态构造函数不能有参数,不能有修饰符而且不能被调用,当类被加载时,类的静态构造函数自动被调用。如: using System.Data; class Employee { private static DataSet ds; static Employee() ds = new DataSet(...); } …...
声明了一个有静态构造函数的类Employee。注意静态构造函数只能对静态数据成员进行初始化,而不能对非静态数据成员进行初始化。但是,非静态构造函数既可以对静态数据成员赋值,也可以对非静态数据成员进行初始化。 如果类仅包含静态成员,你可以创建一个private的构造函数:private TestClass() {…},但是private意味着从类的外面不可能访问该构造函数。所以,它不能被调用,且没有对象可以被该类定义实例化。
以上是几种类型构造函数的简单运用,下面将重点介绍一下在类的层次结构中(即继承结构中)基类和派生类的构造函数的使用方式。派生类对象的初始化由基类和派生类共同完成:基类的成员由基类的构造函数初始化,派生类的成员由派生类的构造函数初始化。 当创建派生类的对象时,系统将会调用基类的构造函数和派生类的构造函数,构造函数的执行次序是:先执行基类的构造函数,再执行派生类的构造函数。如果派生类又有对象成员,则,先执行基类的构造函数,再执行成员对象类的构造函数,最后执行派生类的构造函数。
至于执行基类的什么构造函数,缺省情况下是执行基类的无参构造函数,如果要执行基类的有参构造函数,则必须在派生类构造函数的成员初始化表中指出。如: class A { private int x; public A( ) { x = 0; } public A( int i ) { x = i; } } class B : A private int y; public B( ) { y = 0; } public B( int i ) { y = i; } public B( int i, int j ):A(i) { y = j; }
B b1 = new B(); //执行基类A的构造函数A(),再执行派生类的构造函数B() B b2 = new B(1); //执行基类A的构造函数A(),再执行派生类的构造函数B(int) B b3 = new B(0,1); //执行执行基类A的构造函数A(int),再执行派生类的构造函数B(int,int) 在这里构造函数的执行次序是一定要分析清楚的。另外,如果基类A中没有提供无参构造函数public A( ) { x = 0; },则在派生类的所有构造函数成员初始化表中必须指出基类A的有参构造函数A(i),如下所示:
class A { private int x; public A( int i ) { x = i; } } class B : A private int y; public B():A(i) { y = 0; } public B(int i):A(i) { y = i; } public B(int i, int j):A(i) { y = j; }
3.2.4 方法重载 方法是类中用于执行计算或其它行为的成员。我们看一下方法的声明格式: 方法修饰符 返回类型 方法名(方法参数列表) { 方法修饰符 返回类型 方法名(方法参数列表) { 方法实现部分; }
1、修饰符 方法的修饰符可以是:new、public、protected、internal、private、static、virtual、sealed、override、abstract和extern几种。 如果修饰符为static则表明这个方法只能访问类中的静态成员,没有修饰符static的方法可以访问类中任意成员。 如果修饰符为virtual,则称这个方法为虚方法,反之称为非虚方法。对于非虚方法,无论是被用此类定义的对象调用,还是被这个类的派生类定义的对象调用,方法的执行方式不变。对于虚方法,它的执行方式可以被派生类改变,这种改变是通过重载实现的。 如果修饰符为extern,则表示这个方法是外部方法。
2、返回值 方法的返回值的类型可以是合法的C#的数据类型。C#在方法的执行部分通过return语句得到返回值。
案例:求最大值、最小值。 目标:掌握方法的格式使用 步骤: 1、启动VS.NET,新建一个控制台应用程序,名称填写为“MaxandMinTest”,位置设置为“c:\CSharpSamples\chp3”。 2、在代码设计窗口中编辑Class1.cs。在其中的代码如下: using System; namespace MaxandMinTest { class Test { public static int max(int x ,int y) { if(x>y) return x; else return y; }
public static void WriteMin(int x,int y) { int temp=x; if(x>y) temp=y; Console.WriteLine("the min of {0} and {1} is:{2}。",x,y,temp); return; } public static void Main() Console.WriteLine("the max of 6 and 8 is: {0}。",max(6,8)); WriteMin(6,8);
3、按Ctrl + F5编译并运行该程序,效果如图3-2所示。图 3- 2 程序运行结果 图 3-2 程序运行结果
如果在return后不跟随任何值,方法返回值是void型的。 类的成员方法的重载也是类似的。类中两个以上的方法(包括隐藏的继承而来的方法),取的名字相同,只要使用的参数类型或者参数个数不同,编译器便知道在何种情况下应该调用哪个方法,这就叫做方法的重载。 方法重载得格式就是在一个类中两次或多次定义同名的方法,这些同名的方法也包括从基类中继承而来的方法。这些方法的名称相同,但每个方法的参数类型或个数不同,这样便于系统进行区分。
其实,我们非常熟悉的Console类之所以能够实现对字符串进行格式化的功能,就是因为已定义了多个重载的成员方法: public static void WriteLine(); public static void WriteLine(int); public static void WriteLine(float); public static void WriteLine(long); public static void WriteLine(uint); public static void WriteLine(char); public static void WriteLine(bool); public static void WriteLine(double); public static void WriteLine(char[]); public static void WriteLine(string); public static void WriteLine(Object); public static void WriteLine(ulong); public static void WriteLine(string, Object[]); public static void WriteLine(string, Object); public static void WriteLine(char[], int, int); public static void WriteLine(string, Object, Object); public static void WriteLine(string, Object, Object, Object);
案例:学生类中包含有学生姓名、性别、年龄、体重等信息。我们比较学生之间的年龄和体重。 目标:说明重载的使用基本方法 步骤: 1、启动VS.NET,新建一个控制台应用程序,名称填写为“StudentCompareTest”,位置设置为“c:\CSharpSamples\chp3”。 2、在代码设计窗口中编辑Class1.cs。在其中的代码如下: using System; namespace StudentCompareTest { class Student//定义学生类 { public string s_name; public int s_age; public float s_weight; public Student(string n,int a,float w)
s_name=n; s_age=a; s_weight=w; } public int max_age(int x, int y) { if(x>y) return x; else return y; public float max_weight(float x, float y)
class Test { public static void Main() { Student s1=new Student("Mike",21,70); Student s2=new Student("John",21,70); if(s1.max_age(s1.s_age,s2.s_age)==s1.s_age) Console.WriteLine("{0}'s age is bigger than {1}'s", s1.s_name,s2.s_name); else Console.WriteLine("{0}'s age is smaller than{1}'s", s1.s_name,s2.s_name); if(s1.max_weight(s1.s_weight,s2.s_weight)==s1.s_weight) Console.WriteLine("{0}'s weight is bigger than{1}'s", s1.s_name,s2.s_name); Console.WriteLine("{0}'s weight is smaller than{1}'s",s1.s_name,s2.s_name); }
5、按Ctrl + F5编译并运行该程序,效果如图3-4所示。 图 3-4 程序运行结果
3.2.5 销毁对象与析构函数 在类的实例超出范围时,我们希望确保它所占的存储能被收回。C#中提供了析构函数,用于专门释放被占用的系统资源。 析构函数的名字与类名相同,只是在前面加了一个符号“~”。析构函数不接受任何参数,也不返回任何值。如果你试图声明其已任何一个以符号“~”开头而不与类名相同的方法,和试图让析构函数返回一个值一样,编译器都会产生一个错误。 析构函数不能是继承而来的,也不能显式地调用。当某个类的实例被认为不再有效,符合析构的条件,析构函数就可能在某个时刻被执行。C++的程序员常常需要在析构函数中写上一系列delete语句来释放存储,而在C#中,我们不必再为此担心了。垃圾收集器会帮助我们完成这些易被遗忘的工作。 虽然C#(更确切的说是CLR)提供了一种新的内存管理机制---自动内存管理机制(Automatic memory management),资源的释放是可以通过“垃圾回收器” 自动完成的,一般不需要用户干预,但在有些特殊情况下还是需要用到析构函数的,如在C#中非托管资源的释放。
资源的释放一般是通过“垃圾回收器”自动完成的,但具体来说,仍有些需要注意的地方: 1、值类型和引用类型的引用其实是不需要什么“垃圾回收器”来释放内存的,因为当它们出了作用域后会自动释放所占内存,因为它们都保存在栈(Stack)中; 2、只有引用类型的引用所指向的对象实例才保存在堆(Heap)中,而堆因为是一个自由存储空间,所以它并没有像“栈”那样有生存期(“栈”的元素弹出后就代表生存期结束,也就代表释放了内存),并且要注意的是,“垃圾回收器”只对这块区域起作用。
然而,有些情况下,当需要释放非托管资源时,就必须通过写代码的方式来解决。通常是使用析构函数释放非托管资源,将用户自己编写的释放非托管资源的代码段放在析构函数中即可。需要注意的是,如果一个类中没有使用到非托管资源,那么一定不要定义析构函数,这是因为对象执行了析构函数,那么“垃圾回收器”在释放托管资源之前要先调用析构函数,然后第二次才真正释放托管资源,这样一来,两次删除动作的花销比一次大多的。下面使用一段代码来示析构函数是如何使用的: public class ResourceHolder { …… ~ResourceHolder() // 这里是清理非托管资源的用户代码段 }
3.2.6在方法调用中传递参数 C#中方法的参数有四种类型: ◆值参数,不含任何修饰符。 ◆引用型参数,以ref修饰符声明。 ◆输出参数,以out修饰符声明。 ◆数组型参数,以params修饰符声明。
3.2.6.1 值参数 当利用值向方法传递参数时,编译程序给实参的值做一份拷贝,并且将此拷贝传递给该方法。被调用的方法不会修改内存中实参的值,所以使用值参数时,可以保证实际值是安全的。在调用方法时,如果形式化参数的类型是值参数的话,调用的实参的表达式必须保证是正确的值表达式。
案例:两个数交换1 目标:掌握值参数进行交换 步骤: 1、启动VS.NET,新建一个控制台应用程序,名称填写为“SwapTest1”,位置设置为“c:\CSharpSamples\chp3”。 2、在代码设计窗口中编辑Class1.cs。在其中的代码如下: using System; namespace SwapTest1 { class Test { static void Swap(int x, int y) { int temp=x; x=y; y=temp; }
static void Main() { int i=1,j=2; Swap(i, j); Console.WriteLine("i={0}, j={1}",i,j); }
3、按Ctrl + F5编译并运行该程序,效果如图3-5所示。 图 3-5 程序运行结果
3.2.6.2 引用型参数 和值参不同的是,引用型参数并不开辟新的内存区域。当利用引用型参数向方法传递形参时,编译程序将把实际值在内存中的地址传递给方法。
案例:两个数交换2 目标:掌握引用型参数进行交换 步骤: 1、启动VS.NET,新建一个控制台应用程序,名称填写为“SwapTest2”,位置设置为“c:\CSharpSamples\chp3”。 2、在代码设计窗口中编辑Class1.cs。在其中的代码如下: using System; namespace SwapTest2 { 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("i={0}, j={1}",i,j);
3、按Ctrl + F5编译并运行该程序,效果如图3-6所示。 图 3-6 程序运行结果
Main函数中调用了Swap函数,x代表i,y代表j。这样,调用成功地实现了i和j的值交换。 在方法中使用引用型参数,会经常可能导致多个变量名指向同一处内存地址。见下例: class A { string s; void F(ref string a, ref string b) { s="One" a="Two" b="Three" } void G() { F(ref s, ref s); 在方法G对F的调用过程中,s的引用被同时传递给了a和b。此时s,a,b同时指向了同一块内存区域。
3.2.6.3 输出参数 与引用型参数类似,输出型参数也不开辟新的内存区域。与引用型参数的差别在于,调用方法前无需对变量进行初始化。输出型参数用于传递方法返回的数据。 out修饰符后应跟随与形参的类型相同的类型声明。在方法返回后,传递的变量被认为经过了初始化。
案例:输出参数 目标:掌握输出参数进行传送 1、启动VS.NET,新建一个控制台应用程序,名称填写为“OutTest”,位置设置为“c:\CSharpSamples\chp3”。 2、在代码设计窗口中编辑Class1.cs。在其中的代码如下: using System; namespace OutTest { class Test {static void SplitPath(string path, out string dir, out string name) { int i=path.Length; while (i>0)
char ch=path[i-1]; if (ch=='\\'||ch=='/'||ch==':')break; i--; } dir=path.Substring(0, i); name=path. Substring(i); static void Main() { string dir, name; SplitPath("c:\\Windows\\Systam\\hello.txt", out dir, out name); Console.WriteLine(dir); Console.WriteLine(name);
3、按Ctrl + F5编译并运行该程序,效果如图3-7所示。 图 3-7 程序运行结果 我们注意到,变量dir和name在传递给SplitPath之前并未初始化,在调用之后它们则有了明确的值。
3.2.6.4 数组型参数 如果形参表中包含了数组型参数,那么它必须在参数表中位于最后。另外,参数只允许是一维数组。比如,string[]和string[][]类型都可以作为数组型参数,而string[,]则不能。最后,数组型参数不能再有ref和out修饰符。
案例:数组型参数 目标:掌握数组型参数进行传送 步骤: 1、启动VS.NET,新建一个控制台应用程序,名称填写为“ArrayTest”,位置设置为“c:\CSharpSamples\chp3”。 2、在代码设计窗口中编辑Class1.cs。在其中的代码如下: using System; namespace ArrayTest { class Test static void F(params int[] args)
Console.WriteLine("Array contains {0} elements:",args.Length); foreach(int i in args) Console.Write(" {0}", i); Console.WriteLine(); } public static void Main() { int[] a={1, 2, 3}; F(a); F(10, 20, 30, 40); F();
3、按Ctrl + F5编译并运行该程序,效果如图3-8所示。 图 3-8 程序运行结果
在上例中,第一次调用F是简单地把数组a作为值参数传递;第二次调用把己给出数值的数组传递给了F;而在第一次调用中,F创建了含有0个元素的整型数组作为参数传递。后两次调用完整的写法应该是: F(new int[]{10, 20, 30, 40}); F(new int[]{});
3.2.7静态对象成员 C#的类定义中可以包含两种方法:静态的和非静态的。使用了static修饰符的方法为静态方法,反之则是非静态的。 静态方法是一种特殊的成员方法,它不属于类的某一个具体的实例。非静态方法可以访问类中的任何成员,而静态方法只能访问类中的静态成员。看这个例子:
class A { int; static int y; static int F() x=1;// 错误,不允许访问 y=2;//正确,允许访问 }
在这个类定义中,静态方法F()可以访问类中静态成员s,但不能访问非静态的成员x。这是因为,x作为非静态成员,在类的每个实例中都占有一个存储(或者说具有一个副本),而静态方法是类所共享的,它无法判断出当前的x属十哪个类的实例,所以不知道应该到内存的哪个地址去读取当前x的值。而y是非静态成员,所有类的实例都公用一个副本,静态方法F使用已就不存在什么问题。
那么,是不是静态方法就无法识别类的实例了呢?在C#中,我们可以灵活地采用传递参数的办法。例如: 程序清单: using System class Window { public string m_caption;//窗口的标题 public bool IsActive;//窗口是台被激活 public handle m_handle;//窗口的句柄 public static int m_total; //当前打开的窗口数目 public handle Window() { m_total++;//窗口总数加1 //……创建窗口的一些执行代码 return m_handle;//窗口的返回值们为句柄 } ~Window() {
m_total--//窗口总数减1 //……撤消窗口的一些执行代码 } public static string GetWindowCaption(Window w) { return w.m_caption; //……窗口的其它成员 分析一下上面例子中的代码。每个窗口都有窗口标题m_caption、窗口句柄m_handle、窗口是否激活IsActive三个非静态的数据成员(窗口句柄是Windows操作系统中保存窗口相关信急的一种数据结构,我们在这个例子中简化了对句柄的使用)。系统中总共打开的窗口数目m_total作为一个静态成员。每个窗口调用构造函数创建,这时m_total的值加1。窗口关闭或因为其它行为撤消时,通过析构函数m_total的值减1。 我们要注意窗口类的静态方法GetWindowCaption(Window w)。这里已通过参数w将对象传递给方法执行,这样已就可以通过具体的类的实例指明调用的对象,这时它可以访问具体实例中的成员,无论是静态成员还是非静态成员。
3.2.8用属性封装数据 属性是对现实世界中实体特征的抽象,它提供了对类或对象性质的访问。比如,一个用户的姓名、一个文件的大小、一个窗口的标题,都可以作为属性。类的属性所描述的是状态信息,在类的某个实例中属性的值表不该对象的状态值。 C#中的属性更充分地体现了对象的封装性:不直接操作类的数据内容,而是通过访问器进行访问。它借助于get和set对属性的值进行读写,这在C++中是需要程序员手工完成的一项工作。
3.2.8.1 声明 属性采用如下方式进行声明: [属性修饰符] 属性的类型 属性名称 {访问声明} [属性修饰符] 属性的类型 属性名称 {访问声明} 属性修饰符有new、public、protected、internal、private、static、virtual、override和abstract。 若属性声明中含有static修饰符,这个属性就被称作静态属性。当没有static修饰符时,这个属性被称为实例属性(非静态属性)。一个静态属性与指定的实例无关,并且在静态属性的访问器中使用this是错误的。在一个静态属性中包括virtual、abstract或override修饰符也是错误的。一个实例属性与一个类中给定的实例相关,并且这个实例可以被属性访问器中的this访问。当属性在形式为E.M的成员访问中被引用时,如果M是一个静态属性,E必须表示一个类,而如果M是一个实例属性,E必须表示一个实例。
若实例属性声明中包括virtual修饰符,这个属性就被称为虚属性。当没有virtual修饰符时,这个属性就被称为非虚属性。非虚属性的执行是不变的,不管属性是否在所声明的类的实例或派生类的实例中被访问,执行都是相同的。相反,虚属性的执行可以被派生类改变。改变继承的虚方法的执行过程称为属性重载。 若实例属性声明中包括override修饰符,这个属性就被称为覆盖属性。覆盖属性用相同的声明覆盖一个继承虚属性。然而虚属性声明引入新属性,覆盖属性通过提供这个属性访问器或访问器的新执行来对存在的继承的虚属性进行特殊化。 若实例属性声明中包括abstract修饰符,这个属性就被称为抽象属性。抽象属性隐含地也是一个虚属性。抽象属性声明引入新虚属性,但是没有提供属性访问器或访问器的执行。作为替代,非抽象派生类需要为访问器或覆盖的属性的访问器提供它们自己的执行。因为一个抽象属性的访问器不提供实际执行,它的访问器主体就完全由分号组成。
例如: class A { public abstrat int P {get;set;} }
3.2.8.2 属性的访问 在属性的访问声明中,对属性的值的读操作用get关键字标出,对属性的值的写操作用set关键字标出。 案例:对属性的操作 目标:掌握属性访问的基本方法 步骤: 1、启动VS.NET,新建一个控制台应用程序,名称填写为“FileTest”,位置设置为“c:\CSharpSamples\chp3”。 2、在代码设计窗口中编辑Class1.cs。在其中的代码如下: using System; namespace FileTest { public class File { private string s_filename; public string Filename { get { return s_filename;
} set { if(s_filename!=value) { s_filename=value; public class Test { public static void Main() File f=new File(); f.Filename="myfile.txt"; string s=f.Filename;
3、完成了对属性的操作。 在属性的访问声明中: ◆只有set访问器,表明属性的值只能进行设置而不能读出。 ◆只有get访问器,表明属性的值是只读的,不能改写。 ◆同时具有set访问器和get访问器,表明属性的值的读写都是允许的。 除了使用了abstract修饰符的抽象属性,每个访问器的执行体中只有分号“;”,其它所有属性的get访问器都通过return来读取属性的值,set访问器都通过value来设置属性的值。 举个例子,旅馆对住宿人员进行登记,要记录的信息有:客人姓名、性别、所住的房间号、己住宿的人数。这里,客人的姓名和性别一经确定就不能再更改了,用户可以要求改变房间,住宿的人数当然也是不断变化的。我们在类的构造函数中对客人的姓名和性别进行初始化,在四个属性中,客人的姓名和性别是只读的,故只具有get访问器;房间号和住宿人数允许改变,同时具有set访问器和get访问器。
程序清单: using System; namespace CustomerTest { public class Customer1 { public enum sex { man, woman, }; private string s_name; public string Name { get { return s_name; } private sex m_sex; public sex Sex
get { return m_sex; } private string s_no; public string No { get return s_no; set { if (s_no!=value) { s_no=value; private int i_day;
public int Day { get { return i_day; } set { if (i_day!=value) { i_day=value; public void Customer(string name,sex sex,string no,int day) { s_name=name; m_sex=sex; s_no=no; i_day=day;
实训三 1、定义一个类,并完成对该类构造函数和析构函数的创建,体会这两个函数对类的影响。 2、在一个类中对方法进行重载,体会重载函数之间参数不同的意义。 3、在方法调用中传递参数的方法有哪些。
本章小结 这一章我们介绍了C#中的类的基本概念,展示了类的各种组成。 C#中的类是对数据结构的封装与抽象,是C#最重要的组成部分。我们利用类定义各种新的数据类型,其中即包含了数据的内容,又包含对数据内容的操作。封装之后,类可以控制外界对已的成员的访问。 类的静态成员属于该类,非静态成员则属于这个类的某个实例。 在一个类的实例—对象的生命周期中,最先执行的代码就是类的构造函数。构造函数是用来初始化对象的特殊类型的函数。 不带参数的构造函数对类的实例的初始化是固定的。我们也可以使用带参数的构造函数,通过向已传递参数来对类的不同实例进行不同的初始化。构造函数同样可以使用默认参数。 当这个类的实例超出作用域时,或者由于其它理由被破坏时,析构函数将释放分配给该实例的任何存储区。
练习与提高三 1、类和对象的关系是什么?用具体的例子说明二者的联系和区别? 2、如何实现对类的重用?在类的设计中,主要的成员是那些? 3、类的静态成员和非静态成员有哪些区别? 4、构造函数和析构函数分别有什么意义?