<三> 方法
方法(在C语言中叫函数,或在类的内部调用方法时也可叫调用函数)在类中是最重要的函数成员。定义格式如下:
1、方法的定义
[方法修饰符] 返回类型 方法成员名 ([形式参数列表])
{
语句块;
}
◆ 方法修饰符
修饰符 |
作用说明 |
new |
在一个继承结构中,用于隐藏基类同名的方法 |
public |
表示该方法可以在任何地方被访问 |
protected |
表示该方法可以在它的类体或派生类体中被访问,但不能在类体外访问 |
private |
表示该方法只能达个类体内被访问 |
internal |
表示该方法可以被同处于一个工程的文件访问 |
static |
表示该方法属于类型本身,而不属于某特定对象 |
virtual |
虚方法,表示该方法可在派生类中被覆盖/重写,来更改该方法的实现(与override成对出现) |
override |
表示该方法是覆盖/重写基类带有virtual修饰符的方法(与virtual成对出现) |
abstract |
抽象方法,表示该方法仅仅定义了方法名以及执行方式,但没有给出具体实现代码,所以包含了这种修饰符的方法必须要在派生类中被重写,来实现该方法(重写该抽象方法时要加上override修饰符) |
sealed |
表示这是一个密封方法,不能再被重载,它必须同时包含override修饰符,以防止它的派生类进一步重写该方法(注意:密封方法必须是虚方法的重写) |
extern |
表示该方法从外部实现 |
方法修饰符中public、protected、private、internal、protected internal属于访问修饰符,用于表示访问的级别,默认情况下,方法的访问级别为public。访问修饰符也可以和其他的方法修饰符有效地组合在一起,但某些修饰符是互相排斥的。如下表:
◆ 修饰符的无效组合
修饰符 |
不能与下列选项一起使用 |
static |
virtual、abstract和override |
virtual |
static、abstract和override |
override |
new、static和virtual |
abstract |
virtual和static |
new |
override |
extern |
abstract |
◆ 返回类型:方法可以返回值也可以不返回值,如果返回值,则需要说明返回值的类型,它可以是任何一种C#的数据类型,在方法体内通过return语句将数据交给调用者。如果方法不返回值,则它的返回类型可标为void,当然默认情况下,默认为void。
◆ 方法名:每个方法都有一个名称,一般可以按标识符的命名规则随意给定方法名,不过要记住Main()是为开始执行程序的方法预留的,另外不要使用C#的关键字作为方法。为了使方法容易理解和记忆,建议方法的命名尽可能地同所要进行的操作联系起来,就是我们通常说的顾名思义。
◆ 形参列表:由零个或多个用逗号分隔的形式参数组成的,形式参数可用属性、参数修饰符,类型等描述。当形参表为空时,外面的圆括号也不能省略。
2、方法的参数传递
参数的功效就是能使信息在方法中传入或传出,当声明一个方法时,包含的参数说明是形式参数(形参)。当调用一个方法时,给出的对应实际参数量是实在参数(实参),传入或传出就是在实参与形参之间发生的,在C#中实参与形参有四种传递方式。
2.1 值参数:
形参得到的仅仅是实参的值,即使在方法中形参的值发生改变,而实参的值也不会改变。
在方法声明时不加修饰的形参就是值参数,它表明实参与形参之间按值传递。当这个方法被调用时,编译器为值参数分配存储单元,然后将对应的实参的值拷贝到形参中。实参可以是变量、常量、表达式,但要求其值的类型必需与形参声明的类型相同或者能够被隐式地转化为这种类型。这种传递方式的好处是在方法中对形参的修改不影响外部的实参,也就是说数据只能传入而不能从方法传出,所以值参数有时也被称为入参数。
下面的例子演示了当方法Sort传递的是值参数时,对形参的修改不影响其实参。
using System;
using System.Collections;
namespace 笔记
{
class Myclass
{
public void Sort(int x, int y, int z)
{
int tmp; //tmp是方法Sort的局部变量
//将x,y,z从小到大排序
if (x > y) { tmp = x; x = y; y = tmp; }
if (x > z) { tmp = x; x = z; z = tmp; }
if (y > z) { tmp = y; y = z; z = tmp; }
}
}
class Test
{
static void Main()
{
Myclass m = new Myclass();
int a, b, c;
a = 30; b = 20; c = 10;
m.Sort(a, b, c);
Console.WriteLine("a={0},b={1},c={2}", a, b, c);
Console.Read();
}
}
}
程序运行结果:a=30,b=20,c=10
a,b,c变量的值并没有发生改变,因为它们都是按值传给形参x,y,z的,形参x,y,z的变化并不能影响外部的a,b,c 。
2.2 引用参数
形参与实参的内存地址相同,如果在方法中形参的值发生改变,则实参的值也会被改变。
如果调用一个方法,期望能够对传递给它的实际变量进行操作,如上例中Sort方法对x,y,z的排序希望对调用这个方法的实际变量a,b,c产生作用,如前面所见用C#默认的按值传递是不可能实现的。所以C#用了ref修饰符来解决此类问题,它告诉编译器,实参形参的传递方式是引用(传址)。
引用与值参数不同,引用参数并不创建新的存储单元,它与方法调用中的实在参数变量同处一个存储单元。因此,在方法内对形参的修改就是对外部实参变量的修改。
将上例程序中Sort方法的值参数传递方式改成引用参数传递方式(传址),这样就在方法Sort中对形参x,y,z按从小到大的排序就影响到了实参a,b,c。
using System;
using System.Collections;
namespace 笔记
{
class Myclass
{
public void Sort(ref int x, ref int y, ref int z)
{
int tmp; //tmp是方法Sort的局部变量
//将x,y,z从小到大排序
if (x > y) { tmp = x; x = y; y = tmp; }
if (x > z) { tmp = x; x = z; z = tmp; }
if (y > z) { tmp = y; y = z; z = tmp; }
}
}
class Test
{
static void Main()
{
Myclass m = new Myclass();
int a, b, c;
a = 30; b = 20; c = 10;
m.Sort(ref a, ref b, ref c);
Console.WriteLine("a={0},b={1},c={2}", a, b, c);
Console.Read();
}
}
}
程序运行结果:a=10,b=20,c=30
使用ref参数应注意:
◆ ref关键字仅对跟在它后面的参数有效,而不能应用于整个参数表。
◆ 在调用方法时,也用ref修饰参数变量,因为是引用参数,所以要求实参与形参的数据类型必须完全匹配,而且实参必须是变量,不能是常量或表达式。
◆ 在方法外,ref参数必须在调用之前明确赋值,在方法内,ref参数被视为初始值已赋过。
2.3 输出参数
在参数前加out修饰符的被称为输出参数,它与ref参数很相似,只有一点除外,就是它只能用于方法中传出值,而不能从方法调用外接受实参数据。在方法内out参数被认为是未赋值的,所以在方法结果之前应该对out参数赋值。
在下面的程序中,求一个数组元素中最大值、最小值及平均值。
我们希望得到三个返回值,显然用方法的返回值不能解决,而且这三个值必须通过计算得到,初始值没有意义,所以解决方案可以定义三个out参数。
using System;
using System.Collections;
namespace 笔记
{
class Myclass
{
public void MaxMinArrar(int[] a, out int max, out int min, out double avg)
{
int sum;
sum = max = min = a[0];
for (int i = 1; i < a.Length; i++)
{
if (a[i] > max) max = a[i];
if (a[i] < min) min = a[i];
sum += a[i];
}
avg = sum / a.Length;
}
}
public class Test
{
public static void MMain()
{
Myclass m= new Myclass();
int[] score ={ 87, 89, 56, 90, 100, 75, 64, 45, 80, 84 };
int smax, smin;
double savg;
m.MaxMinArrar(score, out smax,out smin, out savg);
Console.Write("Max={0},Min={1},Avg={2}", smax, smin, savg);
Console.Read();
}
}
}
程序运行结果:Max=100,Min=45,Avg=77
2.4 数组参数的传送
C#.NET允许把数组作为实参传送到方法中。如下例:
using System;
using System.Collections;
namespace 笔记
{
class Myclass
{
public void SortArray(int[] a)
{
int i, j, pos, tmp;
for (i = 0; i < a.Length - 1; i++)
{
for (pos = j = i; j < a.Length; j++)
{
if (a[pos] > a[j]) pos = j;
}
if (pos != i)
{
tmp = a[i];
a[i] = a[pos];
a[pos] = tmp;
}
}
}
}
class Test
{
static void Main()
{
Myclass m = new Myclass();
int[] score ={ 87, 89, 56, 90, 100, 75, 64, 45, 80, 84 };
m.SortArray(score);
for (int i = 0; i < score.Length; i++)
{
Console.Write("score[{0}]={1},", i, score[i]);
if (i == 4) Console.WriteLine();
}
Console.Read();
}
}
}
程序运行结果:score[0]=45,score[1]=56,score[2]=64,score[3]=75,score[4]=80,
score[5]=84,score[6]=87,score[7]=89,score[8]=90,score[9]=100,
当用数组作为方法的参数时,使用的是“引用”(传址)传递方式,而不是传值方式,即不是把score数组中各元素的值一一传送给方法的a数组,而是把score数组的起始地址传给方法,使a数组也具有与score数组相同的起始地址。
2.5 可变参数
一般而言,调用方法时其实参必须与该方法声明的形参在类型和数量上相匹配,但有时候我们希望更灵活一些,能够给方法传递任意个数的参数,比如在三个数中找最大数、最小数和/任意多个数中找最大数、最小数能使用同一个方法。C#提供了传递可变长度的参数表的机制,即使用params关键字来指定一个参数可变长的参数表。
下面程序演示了MyClass类中的方法MaxMin有一个参数数组类型的参数及在调用这个方法时所具有的灵活性
using System;
using System.Collections;
namespace 笔记
{
class Myclass
{
public void MaxMin(out int max, out int min, params int[] a)
{
if (a.Length == 0) //如果可变参数为0个,可以取一个约定值产生异常
{
max = min = -1;
return;
}
max = min = a[0];
for (int i = 1; i < a.Length; i++)
{
if (a[i] > max) max = a[i];
if (a[i] < min) min = a[i];
}
}
}
class Test
{
public static void MMain()
{
Myclass NewMyClass = new Myclass();
int[] score ={ 87, 89, 90, 100, 75, 45, 80, 84 };
int smax, smin;
NewMyClass.MaxMin(out smax, out smin); //可变参数的个数可以是0个
Console.WriteLine("Max={0},Min={1}", smax, smin);
NewMyClass.MaxMin(out smax, out smin, 45, 76, 89, 90); //在四个数之间找最大数,最小数
Console.WriteLine("Max={0},Min={1}", smax, smin);
NewMyClass.MaxMin(out smax, out smin, score); //可变参数也可接受数组对象
Console.WriteLine("Max={0},Min={1}", smax, smin);
Console.Read();
}
}
}
程序运行结果:Max=-1,Min=-1
Max=90,Min=45
Max=100,Min=45
在上面程序中可以看出设立可变参数非常方便也很实用。但在使用时要注意以下几点:
◆ 一个方法中只能有一个params参数,如果还要其他常规参数,则params参数应该放在参数表的最后。
◆ 用params修饰符声明的参数是一个一维数组类型,例如,可以是int[]、string[]、double[]或int[][]等,但不能是int[ , ]、string[ , ]等。
◆ 由于params参数其实是一个数组,所以在调用时可以为参数指定0个或多个参数,其中每个参数的类型都应与参数数组的元素相同或能隐式地转换。
◆ 当调用具有params参数的方法时,可以作为一个元素列表(如:MyArr.MaxMin(smax,smin,45,76,89,90);)或作为一个数组(如:MyArr(out smax,out smin,score);)传递给params参数。
◆ 无论采用哪种方式来调用方法,params参数都是用为一个数组被处理。所以在方法内可以使用数组的长度属性来确定在每次调用中所传递参数的个数。
◆ params参数在内部会进行数据的复制,不可能将params修饰符与ref和out修饰符组合起来用。所以在这个方法中即使对参数数组的元素进行了修改,达个方法之外的数值也不会发生变化。
3、静态方法与实例方法
类的数据成员可以分静态字段和实例字段。静态字段是和类相关联的,不依赖特定对象的存在,实例字段是和对象相关联的,访问实例字段依赖实例的存在。因此,根据静态字段和实例字段的特性,构造函数可分为静态构造函数和实例构造函数,方法也可分为静态方法和实例方法。
通常若一个方法中含是有static修饰符,则表明这个方法是静态方法,同时说明它只对这个类中的静态成员操作,不可以直接访问实例字段。
右一个方法声明中不包含static修饰符,则该方法是一个实例方法。一个实例方法的执行与特定对象关联,所以需要量一个对象存在。实例方法可以直接访问静态字段和实例字段。
3.1 调用静态方法
(1)在静态方法类的内部调用:直接书写静态方法名即可。
(2)在其它类调用:一种是先实例化静态方法的类,通过(实例化对象名.静态方法名)方式调用;另一种是通过(静态方法类名.静态方法名)方式调用。
4、方法重载
像C++和Java一样,C#允许在类中声明多于两个以上的同名方法,这称为重载。重载方法参数的个数与参数类型必须不同(返回值类型不同不算),或者两者至少必须有一个不同(,以使得编译器能根据调用时所提供的参数,知道应调用哪一个方法。
下面例程定义的MyClass类中含有四个名为Max的方法,但它们参数个数不同或者参数类型不同,在Main中调用该方法时编译器会根据参数的个数和类型确定调用哪一个Max方法。
class MyClass
{
public int Max(int x, int y)
{
return x >= y ? x : y;
}
public double Max(double x, double y)
{
return x >= y ? x : y;
}
public int Max(int x, int y, int z)
{
return Max(Max(x, y), z);
}
public double Max(double x, double y, double z)
{
return Max(Max(x, y), z);
}
}
从上例可以看出,方法Max是求若干参数最大值,类MyClass在有四个同名的方法Max,它们或参数个数不一样,或参数类型不一样。在调用Max时,编译器会根据调用时给出的实参个数及类型调用相应的方法,这就是编译时实现的多态。多态是面向对象编程语言的特性之一,重载是金矿的形式之一,在C#中,最学用的重载就是方法重载。
5、方法覆盖/重写(虚拟方法)与隐藏
前面介绍了方法重载,当派生类中的方法与基类中的方法相同的名称但参数列表不同时,发生方法重载。如果派生类中的新方法与基类中的方法不但名称相同而且参数列表也一样(参数个数与参数类型相同),则派生类中的新方法将覆盖(重写)基类中同名的方法。
有三种覆盖方式:
(1)new关键字修饰:一种是用new关键字修饰派生类中与基类同名的方法,那么在派生类中就隐藏了基类方法成员。
例:下面程序定义了一个基类Shape,含有字段域width和height分别表示形状的宽度和高,并定义了一个area方法求形状的面积。它的派生类Triangle和Trapezia都用关键字new修饰了are方法。
using System;
using System.Collections;
namespace 笔记
{
class Shape
{
protected double width;
protected double height;
public Shape()
{ width = height = 0; }
public Shape(double x)
{ width = height = x; }
public Shape(double w, double h)
{
width = w;
height = h;
}
public double area()
{ return width * height; }
}
class Triangle : Shape //三角形
{
public Triangle(double x, double y)
: base(x, y)
{
}
new public double area() //派生类方法与基类方法同名,没有new时,编译时会有警告信息
{
return width * height / 2;
}
}
class Trapezia : Shape //梯形
{
double width2;
public Trapezia(double w1, double w2, double h)
: base(w1, h)
{ width2 = w2; }
new public double area() //加new隐藏基类的area方法
{
return (width + width2) * height / 2;
}
}
class Test
{
public static void MMain()
{
Shape A = new Shape(2, 4);
Triangle B = new Triangle(1, 2);
Trapezia C = new Trapezia(2, 3, 4);
Console.WriteLine("A.area={0}", A.area()); //调用Shape的area方法
Console.WriteLine("B.area={0}", B.area()); //调用Triangle的area方法
Console.WriteLine("C.area={0}", C.area()); //调用Trapezia的area方法
A = B; //在C#中,基类的引用也能够引用派生类对象
Console.WriteLine("A.area={0}", A.area()); //调用Shape的area方法
A = C;
Console.WriteLine("A.area={0}", A.area()); //调用Shape的area方法
Console.Read();
}
}
}
程序运行结果:
A.area=8
B.area=1
C.area=10
A.area=2
A.area=8
从例子中可以看出,使用关键字new修饰方法,可以在一个继承的结构中隐藏有相同签名的方法。但是正如程序中的基类对象A被引用到派生类对象B时,它访问的仍是基类的方法。更多的时候,我们期望根据当前所引用的对象来判断调用哪一个方法,这个判断过程是在运行时进行的。
(2)使用virtual与override关键:另一种更为灵活和有效的手段是:首先将基类的方法用关键字virtual修饰为虚拟方法,再在派生类中用关键字override修饰与基类中虚拟方法具有相同名称的方法,表明是对基类的虚拟方法重载。后者的优势在于它可以在程序运行时再决定调用哪一个方法,这就是所谓运行时的多态,或者称为动态绑定。
例:将上例改写,在Shape类中方法area用virtual修饰,而派生类Triangle和Trapezia用关键字override修饰area方法,这样就可以在程序运行时决定调用哪个类的area方法。
using System;
using System.Collections;
namespace 笔记
{
class Shape
{
protected double width;
protected double height;
public Shape()
{ width = height = 0; }
public Shape(double x)
{ width = height = x; }
public Shape(double w, double h)
{
width = w;
height = h;
}
public virtual double area()
{ return width * height; }
}
class Triangle : Shape //三角形
{
public Triangle(double x, double y)
: base(x, y) //调用基中的的构造函数
{
}
public override double area() //派生类中用override修饰符覆盖基类虚方法
{
return width * height / 2;
}
}
class Trapezia : Shape //梯形
{
double width2;
public Trapezia(double w1, double w2, double h)
: base(w1, h)
{ width2 = w2; }
public override double area() //派生类中用override修饰覆盖基类虚方法
{
return (width + width2) * height / 2;
}
}
class Test
{
public static void MMain()
{
Shape A = new Shape(2, 4);
Triangle B = new Triangle(1, 2);
Trapezia C = new Trapezia(2, 3, 4);
Console.WriteLine("A.area={0}", A.area()); //调用Shape的area方法
Console.WriteLine("B.area={0}", B.area()); //调用Triangle的area方法
Console.WriteLine("C.area={0}", C.area()); //调用Trapezia的area方法
A = B;
Console.WriteLine("A.area={0}", A.area()); //调用Triangle的area方法
A = C;
Console.WriteLine("A.area={0}", A.area()); //调用Trapezia的area方法
Console.Read();
}
}
}
程序运行结果:
A.area=8
B.area=1
C.area=10
A.area=1
A.area=10
从例中可以看到,由于area方法在基类被定义为虚方法又在派生类中被覆盖,所以当基类的对象引用A被引用到派生类对象时,调用的就是派生类覆盖的area方法。
(3)使用base关键字扩展式覆盖:我们可以通过扩展基类中方法的基本功能来覆盖该方法。为此,要用到base关键字。该关键字的作用类似于引用一个类当前实例的基类的对象变量,通常用于访问在派生类中被覆盖或隐藏的基类成员。例如,可以用:base([参数列表])从派生类的构造函数中显式地调用基类的构造函数。通俗地说,用base关键字可以提供基类中的所有方法。
例如:一个重载方法可以通过base引用被重载的基类的方法
class Triangle : Shape //三角形
{
public Triangle(double x, double y)
: base(x, y)
{
}
public override double area() //派生类中用override修饰符覆盖基类虚方法
{
double s;
s = base.area(); //调用基类的虚方法
return s / 2;
}
}
在类的层次结构中,只有使用override修饰符,派生类中的方法才可以覆盖(重写)基类的虚方法,否则就是隐藏基类的方法。
具体使用过程液注意以下几点:
(1)不能将虚方法声明为静态的,因数多态性是针对对象的。不是针对类的。
(2)不能将虚方法声明为私有的,因为私有方法不能被派生类覆盖。
(3)覆盖方法必须与它相关的虚方法匹配,也就是说,它们的方法签名(方法名称、参数个数、参数类型)、返回类型以及访问属性等都应该完全一致。
(4)一个覆盖方法覆盖的必须是虚方法,但它本身又是一个隐式的虚方法,所以它的派生类还可以覆盖这个方法。不过尽管如此还是不能将一个覆盖方式显式地声明为虚方法。