方法又称成员函数(Member Function)集中体现了类或对象的行为方法同样分为静态方法和实例方法静态方法只可以操作静态域而实例方法既可以操作实例域也可以操作静态域虽然这不被推荐但在某些特殊的情况下会显得很有用方法也有如域一样的种存取修饰符publicprotectedinternalprotected internalprivate它们的意义如前所述
方法参数
方法的参数是个值得特别注意的地方方法的参数传递有四种类型传值(by value)传址(by reference)输出参数(by output)数组参数(by array)传值参数无需额外的修饰符传址参数需要修饰符ref输出参数需要修饰符out数组参数需要修饰符params传值参数在方法调用过程中如果改变了参数的值那么传入方法的参数在方法调用完成以后并不因此而改变而是保留原来传入时的值传址参数恰恰相反如果方法调用过程改变了参数的值那么传入方法的参数在调用完成以后也随之改变实际上从名称上我们可以清楚地看出两者的含义传值参数传递的是调用参数的一份拷贝而传址参数传递的是调用参数的内存地址该参数在方法内外指向的是同一个存储位置看下面的例子及其输出
using System;class Test{static void Swap(ref int x ref int y) {int temp = x;x = y;y = temp;}static void Swap(int xint y) {int temp = x;x = y;y = temp;}static void Main() {int i = j = ;Swap(ref i ref j);ConsoleWriteLine(i = {} j = {} i j);Swap(ij);ConsoleWriteLine(i = {} j = {} i j);}}
程序经编译后执行输出
i = j =
i = j =
我们可以清楚地看到两个交换函数Swap()由于参数的差别传值与传址而得到不同的调用结果注意传址参数的方法调用无论在声明时还是调用时都要加上ref修饰符
笼统地说传值不会改变参数的值在有些情况下是错误的我们看下面一个例子
using System;class Element{public int Number=;}class Test{static void Change(Element s){sNumber=;}static void Main() {Element e=new Element();ConsoleWriteLine(eNumber);Change(e);ConsoleWriteLine(eNumber);}}
程序经编译后执行输出
我们看到即使传值方式仍然改变了类型为Element类的对象t但严格意义上讲我们是改变了对象t的域而非对象t本身我们再看下面的例子
using System;class Element{public int Number=;}class Test{static void Change(Element s){Element r=new Element();rNumber=;s=r;}static void Main() {Element e=new Element();ConsoleWriteLine(eNumber);Change(e);ConsoleWriteLine(eNumber);}}
程序经编译后执行输出
传值方式根本没有改变类型为Element类的对象t!实际上如果我们能够理解类这一C#中的引用类型(reference type)的特性我们便能看出上面两个例子差别!在传值过程中引用类型本身不会改变(t不会改变)但引用类型内含的域却会改变(tNumber改变了)!C#语言的引用类型有object类型(包括系统内建的class类型和用户自建的class类型继承自object类型)string类型interface类型array类型delegate类型它们在传值调用中都有上面两个例子展示的特性
在传值和传址情况下C#强制要求参数在传入之前由用户明确初始化否则编译器报错!但我们如果有一个并不依赖于参数初值的函数我们只是需要函数返回时得到它的值是该怎么办呢?往往在我们的函数返回值不至一个时我们特别需要这种技巧答案是用out修饰的输出参数但需要记住输出参数与通常的函数返回值有一定的区别函数返回值往往存在堆栈里在返回时弹出而输出参数需要用户预先制定存储位置也就是用户需要提前声明变量当然也可以初始化看下面的例子
using System;class Test{static void ResoluteName(string fullnameout string firstnameout string lastname) {string[] strArray=fullnameSplit(new char[]{ });firstname=strArray[];lastname=strArray[];}public static void Main() {string MyName=Cornfield Lee;string MyFirstNameMyLastName;ResoluteName(MyNameout MyFirstNameout MyLastName);ConsoleWriteLine(My first name: {} My last name: {} MyFirstName MyLastName);}}
程序经编译后执行输出
My first name: Cornfield My last name: Lee
在函数体内所有输出参数必须被赋值否则编译器报错!out修饰符同样应该应用在函数声明和调用两个地方除了充当返回值这一特殊的功能外out修饰符ref修饰符有很相似的地方传址我们可以看出C#完全摈弃了传统C/C++语言赋予程序员莫大的自由度毕竟C#是用来开发高效的下一代网络平台安全性包括系统安全(系统结构的设计)和工程安全(避免程序员经常犯的错误)是它设计时的重要考虑当然我们看到C#并没有因为安全性而丧失多少语言的性能这正是C#的卓越之处Sharp之处!
数组参数也是我们经常用到的一个地方传递大量的数组集合参数我们先看下面的例子
using System;class Test{static int Sum(params int[] args){int s=;foreach(int n in args){s+=n;}return s;}static void Main() {int[] var=new int[]{};ConsoleWriteLine(The Sum:+Sum(var));ConsoleWriteLine(The Sum:+Sum());}}
程序经编译后执行输出
The Sum:
The Sum:
可以看出数组参数可以是数组如var也可以是能够隐式转化为数组的参数如这为我们的程序提供了很高的扩展性
同名方法参数的不同会导致方法出现多态现象这又叫重载(overloading)方法需要指出的是编译器是在编译时便绑定了方法和方法调用只能通过参数的不同来重载方法其他的不同(如返回值)不能为编译器提供有效的重载信息
方法继承
第一等的面向对象机制为C#的方法引入了virtualoverridesealedabstract四种修饰符来提供不同的继承需求类的虚方法是可以在该类的继承自类中改变其实现的方法当然这种改变仅限于方法体的改变而非方法头(方法声明)的改变被子类改变的虚方法必须在方法头加上override来表示当一个虚方法被调用时该类的实例亦即对象的运行时类型(runtime type)来决定哪个方法体被调用我们看下面的例子
using System;class Parent{public void F() { ConsoleWriteLine(ParentF); }public virtual void G() { ConsoleWriteLine(ParentG); }}class Child: Parent{new public void F() { ConsoleWriteLine(ChildF); }public override void G() { ConsoleWriteLine(ChildG); }}class Test{static void Main() {Child b = new Child();Parent a = b;aF();bF();aG();bG();}}
程序经编译后执行输出
ParentF
ChildF
ChildG
ChildG
我们可以看到class Child中F()方法的声明采取了重写(new)的办法来屏蔽class Parent中的非虚方法F()的声明而G()方法就采用了覆盖(override)的办法来提供方法的多态机制需要注意的是重写(new)方法和覆盖(override)方法的不同从本质上讲重写方法是编译时绑定而覆盖方法是运行时绑定值得指出的是虚方法不可以是静态方法也就是说不可以用static和virtual同时修饰一个方法这由它的运行时类型辨析机制所决定override必须和virtual配合使用当然也不能和static同时使用
那么我们如果在一个类的继承体系中不想再使一个虚方法被覆盖我们该怎样做呢?答案是sealed override (密封覆盖)我们将sealed和override同时修饰一个虚方法便可以达到这种目的sealed override public void F()注意这里一定是sealed和override同时使用也一定是密封覆盖一个虚方法或者一个被覆盖(而不是密封覆盖)了的虚方法密封一个非虚方法是没有意义的也是错误的看下面的例子
//sealedcs// csc /t:library sealedcsusing System;class Parent{public virtual void F() {ConsoleWriteLine(ParentF);}public virtual void G() {ConsoleWriteLine(ParentG);}}class Child: Parent{sealed override public void F() {ConsoleWriteLine(ChildF);} override public void G() {ConsoleWriteLine(ChildG);} }class Grandson: Child{override public void G() {ConsoleWriteLine(GrandsonG);} }
抽象(abstract)方法在逻辑上类似于虚方法只是不能像虚方法那样被调用而只是一个接口的声明而非实现抽象方法没有类似于{…}这样的方法实现也不允许这样做抽象方法同样不能是静态的含有抽象方法的类一定是抽象类也一定要加abstract类修饰符但抽象类并不一定要含有抽象方法继承含有抽象方法的抽象类的子类必须覆盖并实现(直接使用override)该方法或者组合使用abstract override使之继续抽象或者不提供任何覆盖和实现后两者的行为是一样的看下面的例子
//abstractcs// csc /t:library abstractcsusing System;abstract class Parent{public abstract void F();public abstract void G();}abstract class Child: Parent{public abstract override void F();}abstract class Grandson: Child{public override void F(){ConsoleWriteLine(GrandsonF);}public override void G(){ConsoleWriteLine(GrandsonG);}}
抽象方法可以抽象一个继承来的虚方法我们看下面的例子
//abstractcs// csc /t:library abstractcsusing System;class Parent{public virtual void Method(){ConsoleWriteLine(ParentMethod);}}abstract class Child: Parent{public abstract override void Method();}abstract class Grandson: Child{public override void Method(){ConsoleWriteLine(GrandsonMethod);}}
归根结底我们抓住了运行时绑定和编译时绑定的基本机理我们便能看透方法呈现出的种种overloadvirtualoverridesealedabstract等形态我们才能运用好方法这一利器!
外部方法
C#引入了extern修饰符来表示外部方法外部方法是用C#以外的语言实现的方法如Win API函数如前所是外部方法不能是抽象方法我们看下面的一个例子
using System;using SystemRuntimeInteropServices;class MyClass{[DllImport(userdll)]static extern int MessageBoxA(int hWnd string msgstring caption int type);public static void Main() {MessageBoxA( Hello World! This is called from a C# app! );}}
程序经编译后执行输出
screenwidth)thisstylewidth=screenwidth; border=>
这里我们调用了Win API函数int MessageBoxA(int hWnd string msgstring caption int type)