购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.3 方法

在面向过程的语言如C语言中,数据和对数据的操作通常分为两部分;在C++语言中,大多数数据成为类的数据成员,而大多数对数据的操作放在了类的成员方法中。C#实现了完全意义上的面向对象,任何事物都必须封装在类中或者作为类的实例成员,没有全局常数、全局变量,也没有全局方法。

我们在上一节中学习了类的基本概念,并掌握了如何对类进行实例化,这一节将利用类的方法为类添加功能。

2.3.1 方法的声明

方法是类中用于执行计算或其他行为的成员,我们看一下方法的声明格式:

其中,方法头method-header的格式为

传递给方法的参数在方法的形式化参数表formal-parameter-list中声明,我们随后将进行详细论述。

在方法的声明中至少应包括方法名称、修饰符和参数类型,返回值和参数名则不是必需的。

注意方法名member-name不应与同一个类中的其他方法同名,也不能与类中的其他成员名称相同。

方法的修饰符method-modifier可以是new、public、protected、internal、private、static、virtual、sealed、override、abstract、extern。

对于使用了abstract和extern修饰符的方法,方法的执行体method-body只有一个简单的分号,其他所有的方法执行体中应包含调用该方法所要执行的语句返回值。

方法返回值的类型可以是合法的C#的数据类型,C#在方法的执行部分通过return语句得到返回值,可见程序清单2-12。

程序清单 2-12:

程序的输出是

如果在return后不跟随任何值,方法返回值是void型的。

2.3.2 方法中的参数

C#中方法的参数有四种类型:

1)值参数:不含任何修饰符。

2)引用型参数:以ref修饰符声明。

3)输出参数:以out修饰符声明。

4)数组型参数:以params修饰符声明。

2.3.2.1 值参数

当利用值向方法传递参数时,编译程序给实参的值做一份拷贝并且将此拷贝传递给该方法。被调用的方法不会修改内存中实参的值,所以使用值参数时可以保证实际值是安全的,在调用方法时,如果形式化参数的类型是值参数,调用的实参的表达式必须保证是正确的值表达式。在下面的例子中程序员并没有实现他希望交换值的目的。

程序清单 2-13:

编译上述代码程序将输出:

2.3.2.2 引用型参数

和值参数不同的是,引用型参数并不开辟新的内存区域。当利用引用型参数向方法传递形参时,编译程序将把实际值在内存中的地址传递给方法。在方法中引用型参数通常已经初始化,再看下面的例子:

程序清单 2-14:

编译上述代码程序将输出:

Main函数中调用了Swap函数,x代表i,y代表j,这样调用成功地实现了i和j的值交换。在方法中使用引用型参数可能会导致多个变量名指向同一处内存地址,见示例:

在方法G对F的调用过程中,s的引用被同时传递给a和b,此时s、a、b指向同一块内存区域。

2.3.2.3 输出参数

与引用型参数类似,输出型参数也不开辟新的内存区域,它与引用型参数的差别在于调用方法前无须对变量进行初始化。输出型参数用于传递方法返回的数据。out修饰符后应跟随与形参的类型相同的类型,声明在方法返回后传递的变量被认为经过了初始化。

程序清单 2-15:

可以预计程序的输出将会是

我们注意到变量dir和name在传递给SplitPath之前并未初始化,在调用之后它们则有了明确的值。

2.3.2.4 数组型参数

如果形参表中包含了数组型参数,那么它必须在参数表中位于最后。另外,参数只允许是一维数组,比如string[]和string[][]类型都可以作为数组型参数,而string[,]则不能。最后,数组型参数不能再有ref和out修饰符。

程序清单 2-16:

程序输出

在上例中第一次调用F是简单地把数组a作为值参数传递,第二次调用把已给出数值的数组传递给了F,而在第三次调用中F创建了含有0个元素的整型数组作为参数传递。后两次调用完整的写法应该是

2.3.3 静态和非静态的方法

C#的类定义中可以包含两种方法:静态的和非静态的。使用了static修饰符的方法为静态方法,反之则是非静态的。静态方法是一种特殊的成员方法,它不属于类的某一个具体的实例。非静态方法可以访问类中的任何成员,而静态方法只能访问类中的静态成员。看这个例子:

在这个类定义中,静态方法F()可以访问类中静态成员y,但不能访问非静态的成员x,这是因为x作为非静态成员,在类的每个实例中都占有一个地址或者说具有一个副本。而静态方法是类所共享的,它无法判断出当前的x属于哪个类的实例,所以不知道应该到内存的哪个地址去读取当前x的值,而y是非静态成员,所有类的实例都公用一个副本,静态方法F使用它就不存在问题。

那么是不是静态方法就无法识别类的实例了呢?在C#中可以灵活地采用传递参数的办法。之前提到一个Window窗口程序的例子,这里再对这个例子进行一些改变:

程序清单 2-17:

分析一下上面例子中的代码,每个窗口都有窗口标题m_caption、窗口句柄m_handle,以及窗口激活状态IsActive。三个非静态的数据成员,窗口句柄是Windows操作系统中保存窗口相关信息的一种数据结构,我们在这个例子中简化了对句柄的使用。

系统中打开的窗口总数为m_total。作为一个静态成员,每个窗口调用构造函数创建时m_total的值加1,窗口关闭或因为其他行为撤销时析构函数m_total的值减1,窗口类的静态方法GetWindowCaption(Window w)通过参数w将对象传递给方法执行,这样它就可以通过具体的类的实例指明调用的对象,这时它可以访问具体实例中的成员,无论是静态成员还是非静态成员。

2.3.4 方法的重载

在前面的例子中,我们实际上已经看到了构造函数的重载。

程序清单 2-18:

类的成员方法的重载也是类似的,类中两个以上的方法可以取相同的名字,只要使用的参数类型或者参数个数不同,编译器便知道在何种情况下,应该调用哪个方法,这就叫作方法的重载。

其实我们非常熟悉的Console类之所以能够实现对字符串进行格式化的功能,就是因为它定义了多个重载的成员方法:

由于C#支持重载,可以给两个比较大小的方法取同一个名字max,程序员在调用方法时,只需在其中带入实参,编译器就会根据实参类型来决定到底调用哪个重载方法。代码可以改写为

程序清单 2-19:

2.3.5 操作符重载

2.3.5.1 问题的提出

在面向对象的程序设计中,自己定义一个类就等于创建了一个新类型。类的实例和变量一样可以作为参数传递,也可以作为返回类型。

我们知道对于两个整型变量使用算术操作符可以简便地进行算术运算:

再比如我们希望将属于不同类的两个实例的数据内容相加:

使用a.x+b.x这种写法不够简洁,也不够直观,更为严重的问题是如果类的成员在声明时使用的不是public修饰符,这种访问就是非法的。

我们知道,在C#中所有数据要么属于某个类,要么属于某个类的实例,充分体现了面向对象的思想,因此为了表达上的方便,人们希望可以重新给已定义的操作符赋予新的含义,在特定的类的实例上进行新的解释,这就需要通过操作符重载来解决。

2.3.5.2 使用成员方法重载操作符

C#中操作符重载总是在类中进行声明,并且通过调用类的成员方法来实现操作符重载。声明的格式为

C#中下列操作符都是可以重载的:

但也有一些操作符是不允许进行重载的,如:

1.一元操作符重载

顾名思义,一元操作符重载时,操作符只作用于一个对象,此时参数表为空,当前对象作为操作符的单操作数。

下面举一个角色类游戏中经常遇到的例子:扮演的角色具有内力、体力、经验值、剩余体力、剩余内力五个属性,每当经验值达到一定程度时,角色便会升级。我们使用重载操作符++来实现:

程序清单 2-20:

2.二元操作符重载

大多数情况下,我们使用二元操作符重载。这时参数表中有一个参数。当前对象作为该操作符的左操作数,参数作为操作符的右操作数。

下面给出二元操作符重载的一个简单例子,即笛卡儿坐标相加。

程序清单 2-21:

试着编译运行该程序,看结果是否与预期的一致。 9xY2aHVVBv8y2JtmOuhGZ5izeEgEh+MBi/G+xHK/HIL5JznlqioipUlnKyUtUJm1

点击中间区域
呼出菜单
上一章
目录
下一章
×