方法参数
1.引用参数
引用与值参数不同,引用参数并不创建新的存储单元,它与方法调用中的实在参数变量 同处一个存储单元。因此,在方法内对形参的修改就是对外部实参变量的修改。 【例3.11】将【例3.9】程序中Sort方法的值参数传递方式改成引用参数传递,这样在方 法Sort中对参数x、y、z按从小到大的排序影响了调用它的实参a、b、c。 using System; 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();
}
}
使用ref参数的注意点:
(1) ref关键字仅对跟在它后面的参数有效,而不能应用于整个参数表。例如Sort方法中x, y, z都要加ref修饰。
(2) 在调用方法时,也用ref修饰实参变量,因为是引用参数,所以要求实参与形参的数据类型必须完全匹配,而且实参必须是变量,不能是常量或表达式。
(3) 在方法外,ref参数必须在调用之前明确赋值,在方法内,ref参数被视为初始值已赋过。
输出参数
在参数前加out修饰符的被称为输出参数,它与ref参数很相似,只有一点除外,就是它只
能用于从方法中传出值,而不能从方法调用处接受实参数据。在方法内out参数被认为是
未赋过值的,所以在方法结束之前应该对out参数赋值。
在下面程序4-7中,求一个数组元素中的最大值、最小值以及平均值。
希望得到三个返回值,显然用方法的返回值不能解决,而且这三个值必须通过计算得
到,初始值没有意义,所以解决方案可以定义三个out参数。
using System;
class Myclass
{
public void MaxMinArray (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;
}
}
class Test
{
static void Main()
{
Myclass m = new Myclass ( );
int [ ] score = { 87,89,56,90,100,75,64,45, 80, 84} ;
int smax, smin;
double savg;
m.MaxMinArray(score, out smax, out smin, out savg);
Console.Write ("Max={0}, Min={1}, Avg={2} " , smax, smin, savg );
Console.Read ();
}
}
参数数组
一般而言,调用方法时其实参必须与该方法声明的形参在类型和数量上相匹配,但有时候我们更希望灵活一些,能够给方法传递任意个数的参数,比如在三个数中找最大、最小和在5个数中找最大、最小甚或任意多个数中找最大、最小能使用同一个方法。C#提供了传递可变长度的参数表的机制,即使用params关键字来指定一个参数可变长的参数表。
【例3.14】下面程序演示了Myclass类中的方法MaxMin有一个参数数组类型的参数,那么在调用这个方法是所具有的灵活性。
using System;
class Myclass
{
public void MaxMin (out int max, out int min, params int [ ] a )
{
if (a.Length==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
{
static void Main()
{
Myclass m = new Myclass ( );
int [] score = { 87,89,56,90,100,75,64,45, 80, 84} ;
int smax, smin;
m.MaxMin (out smax, out smin); // 可变参数的个数可以是零个
Console.WriteLine ("Max={0}, Min={1} " , smax, smin );
m.MaxMin (out smax, out smin, 45, 76, 89, 90 ); // 在四个数之间找最大、最小
Console.WriteLine ("Max={0}, Min={1} " , smax, smin );
m.MaxMin (out smax, out smin, score); // 可变参数也可接受数组对象
Console.WriteLine ("Max={0}, Min={1} " , smax, smin );
Console.Read ();
}
}
从上面例程中可以看出设立可变参数非常方便也很实用。但在使用时要注意以下几点:
(1) 一个方法中只能声明一个params参数,如果还要其他常规参数,则params参数应放在参数表的最后。
(2) 用params修饰符声明的参数是一个一维数组类型,例如,可以是int [ ], string [ ], double [ ], 或int [ ] [ ], string [ ]等,但不能是int [ , ], string [ , ]等。
(3) 由于params参数其实是一个数组,所以在调用时可以为参数数组指定零个或多个参数,其中每个参数的类型都应与参数数组的元素类型相同或能隐式地转换。
(4) 当调用具有params参数的方法时,可以作为一个元素列表(如:m.MaxMin (smax, smin, 45, 76, 89, 90 );)或作为一个数组(如:m.MaxMin (out smax, out smin, score);)传递给params参数。
(5) 无论采用哪种方式来调用方法,params参数都是作为一个数组被处理。所以在方法内可以使用数组的长度属性来确定在每次调用中所传递参数的个数。
(6) params参数在内部会进行数据的复制,不可能将params修饰符与ref和out修饰符组合起来用。所以在这个方法中即使对参数数组的元素进行了修改,在这个方法之外的数值也不会发生变化。
方法的重载与覆盖
一个方法的名字和形式参数的个数、修饰符以及类型共同构成了这个方法的签名,同一个类中不能有相同签名的方法。如果一个类中有两个或两个以上的方法的名字相同,而它们的形参个数或形参类型有所不同则是允许的,它们属于不同的方法签名。实际上这些同名方法都在某些方面具有惟一性,编译器能够正确区别它们。不过需要注意的是方法的返回类型不是方法签名的组成部分,也就是说,仅仅是返回类型不同的同名方法,编译器是不能识别的。下面通过一个例子来介绍方法重载。
下面程序定义了一个基类Shape,含有字段域width和height分别表示形状的宽
和高,并定义了一个area方法求形状的面积。它的派生类Triangle和Trapezia都用关键字
new修饰了area方法。
using System;
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 ( ) // 派生类方法与基类方法同名,编译时会有警告信息
{
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
{
static void Main()
{
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 ();
}
}
从例中可以看出,使用关键字new修饰方法,可以在一个继承的结构中隐藏有相同签名的方法。但是正如程序中演示的基类对象A被引用到派生类对象B时,它访问的仍是基类的方法。更多的时候,我们期望根据当前所引用的对象来判断调用哪一个方法,这个判断过程是在运行时进行的。
在Shape类中方法area用virtual修饰,而派生类Triangle和
Trapezia用关键字override修饰area方法,这样就可以在程序运行时决定调用哪个类的
area方法。
using System;
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 ( ) // 基类中用virtual修饰符声明一个虚方法
{ 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
{
static void Main()
{
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 ();
}
}
从例中可以看到,由于area方法在基类被定义为虚方法又在派生类中被覆盖,所以当基类的对象引用A被引用到派生类对象时,调用的就是派生类覆盖的area方法。
一个重载方法可以通过base引用被重载的基方法。例如:
class Triangle : Shape // 三角形
{
public override double area ( ) // 派生类中用override修饰符覆盖基类虚方法
double s;
s = base.area( ); // 调用基类的虚方法
return s/2 ;
}
在类的层次结构中,只有使用override修饰符,派生类中的方法才可以覆盖(重载)基类的虚方法,否则就是隐藏基类的方法。
具体使用过程应注意以下几点:
(1) 不能将虚方法声明为静态的,因为多态性是针对对象的,不是针对类的。
不能将虚方法声明为私有的,因为私有方法不能被派生类覆盖。
(3) 覆盖方法必须与它相关的虚方法匹配,也就是说,它们的方法签名(方法名称、参数个数、参数类型)、返回类型以及访问属性等都应该完全一致。
(4) 一个覆盖方法覆盖的必须是虚方法,但它本身又是一个隐式的虚方法,所以它的派生类还可以覆盖这个方法。不过尽管如此还是不能将一个覆盖方法显式地声明为虚方法。