C#权威指南第9课:方法

我们在上个文章学习了语句,语句主要存在于方法中,而方法是类的函数成员。本章将详述方法的方方面面,学习过程中可能需要一些语句的知识,希望大家边学习边练习,在学习新知识的过程中,深化对已有知识的理解和掌握。OK,起锚,向下一站进发!

1.方法的构成

一个方法由签名和方法体构成。其中,方法的签名包括方法的名称以及参数信息(包括形参的修饰符、数目、类型以及泛型参数的数目)。特别需要注意的是,返回值类型、形参和类型参数的名称并不属于方法签名的一部分。下面看一个方法的实例,如图8-1所示。

这是一个执行加法运算的方法,返回两个数字相加后的和值,此方法的组成部分为:

1)方法的访问修饰符,此处为public。

2)返回值类型,此处为int型,如果没有返回值则返回类型设为void;

3)方法名称,对方法的调用要使用此名称,此处为Add,方法的命名建议使用Pascal规则。

4)方法的参数信息,包括参数的个数以及每个参数的类型,此处为两个输入参数,均是int类型,参数名称分别为a和b。

5)方法体则包含一系列的语句,这些语句被顺序执行。当然,语句中也可以使用分支和循环,对于有返回值的方法,方法体内可以使用return语句将结果返回,如没有,则可以使用return语句返回到方法调用的地方,如图8-2所示。

语句n执行了对方法F()的调用,那么此时程序进入F()方法内部开始运行,按照F()方法内部的语句先后顺序执行,直到遇到return语句或方法体结束。当遇到return语句之后,不管是否返回值,都要返回到方法的调用点,以继续执行语句(n+1)。

方法体是一个语句块,使用花括号为界,方法体可以由如下几个部分组成:

1)局部变量;

2)若干个语句,可以是顺序结构、分支结构或者循环结构;

3)调用其他方法。

2.局部变量和常量

相对于类的字段变量,定义在方法内的本地变量叫做局部变量,它在使用前必须初始化,而字段变量不需要,字段变量如果没有初始化,会被自动初始化为默认值。在前面我们学习了var关键字,编译器可以根据变量的初始值来推断变量的类型,要知道var关键字只能用于局部变量。

方法体本身是一个代码块,在它之中还可以包含更多的代码块,不管这些块是顺序的还是在更深的嵌套块中。局部变量,顾名思义,它的有效范围仅在定义它的块及其内部块有效,而出了这个范围则是无效的,这个有效范围称做变量的作用域。因此,下面这段代码在C++中有效,而在C#中却是无效的,这一点需要引起大家注意。如代码清单8-1所示。

上述代码编译器会产生如下错误信息:

不能在此范围内声明名为"m"的局部变量,因为这样会使"m"具有不同的含义,而它已在"父级或当前"范围中表示其他内容了。

这是因为定义在if嵌套块中的变量m和其父代码块中的m产生了冲突,可见定义于第1行的int型变量m,其作用域不但在所在的代码块,还"穿透"了其中的嵌套块,导致和定义于第4行的double型变量冲突。

下面,我们使用图8-3来说明变量的作用域。

图8-3显示了两个局部变量的作用域,并展示了它们在托管堆栈中的情况:

1)变量a声明在方法体中,在if语句的代码块之前,此刻对应状态

①;

2)变量b声明在if语句的代码块内部,它从声明之后开始存在,此刻

对应状态②;

3)if代码块结束,变量b从托管堆栈中弹出,此刻对应状态③。

上面我们讲述了局部变量,现在来说说作用域相同的局部常量,我们在前面文章已经讲了常量的概念,此处的局部常量也是常量的一种,只是其某些特性更加类似局部变量,如其作用域仅限于在声明它的块范围内,包括块中的嵌入块,其他则和一般常量无异,如:

1)必须在声明的时候就立即初始化,其值在初始化后将无法再进行

更改;

2)必须使用显式类型声明,不能使用类型推断关键字:var;

3)无法接受变量的赋值,哪怕该变量是static并且是readonly也不行,常量在初始化时只能使用另一个常量为它赋值,当然,直接赋予一个具体的值更好。

3.方法的调用

可以通过使用方法名称,再结合方法签名规定的参数个数及类型,对方法进行调用。如代码清单8-2所示:

代码清单8-2 方法的调用

cs 复制代码
using System;

namespace ProgrammingCSharp4
{
    public class MethodSample
    {
        public static void Main()
        {
            MethodSample ms = new MethodSample();
            int i2 = 10;
            int result = ms.Add(20, i2);
            Console.WriteLine("20+i2={0}", result);
        }

        public int Add(int a, int b)
        {
            return a + b;
        }
    }
}

第15行:声明了Add(int a,int b)方法,它接收两个int型参数,关于参数,我们稍后进行说明;

第9行:实例化MethodSample类;

第10行:声明int型变量i2,并将它的值初始化为10;

第11行:通过MethodSample类的实例ms调用Add方法,并将一个实参和一个形参传入(关于实参和形参稍后介绍),并声明一个int型变量result来存放Add方法的返回值,结合第6章学到的关于表达式的知识,这里的Add方法即是一个表达式;

第12行:将Add(20,i2)的运算结果输出到控制台:

20+i2=30

下面分析一下这段代码的调用顺序,如图8-4:

下面我们对图8-4进行说明:

1)①→②:顺序执行;

2)③:调用Add方法,直到方法执行完成;

3)④:Add方法执行完成后,返回调用点,并把返回值(如果有的话)赋予result变量;

4)⑤:继续按顺序执行。

4.返回值

代码清单8-2中的Add方法在执行完加法运算后,并没有简单结束,而是将运算结果返回,最终结果赋给了调用处的result变量(第11行),这里的运算结果就是返回值。在C#中,和C、C++一样,使用return语句将返回值返回。这里展开讲述return语句在方法中的具体用法。

我们知道,并非所有方法都有返回值,像下面的方法就没有返回值:

cs 复制代码
public void Print(string str)
{
    Console.WriteLine(str);
}

这只是一个简单的方法,将传入的字符串参数打印到控制台,它并没有返回值。而上面例子中的Add方法,由于我们需要两个数相加的和值结果,因此它需要将结果返回给我们。那么,关于方法的返回值,我们总结有如下两点:

1)如果方法有返回值,那么必须在方法的签名部分指明返回值的类型,并在方法体中使用return语句返回正确类型的值作为返回值,如图8-5所示;

2)如果方法没有返回值,那么必须在方法的签名部分使用void关键字进行说明,方法体中就不必使用return语句返回任何值了,但仍然可以使用return语句提前将方法返回,如图8-6所示。

注意 return;语句仅可以使用在没有返回值的方法中,有返回值必须使用return(返回值)。

5.参数

在前面的例子中,我们已经不止一次地接触到方法的参数,如Print(string str)方法中的str、Add(int a,int b)方法中的a和b等。参数起的是一个占位符的作用,它表示在实际执行方法时传入的值在方法体的代码中参与运算。

方法的参数有形参和实参两种,什么是形参?什么是实参?它们之前有何区别?且待我慢慢为大家道来。

5.1形参

形参,从名称上来看,它是一个形式上的参数,既然是形式上的,它就不是一个实际的值,而是一个"替身",它代替实际传入的值,在方法体代码中代表了值本身参与运算。形参定义于参数中,它不同于方法体内局部变量,但因为它也是一个变量,在它的作用域内同样不允许存在另一个同名的局部变量,不管它们的类型是否相同,都是不允许的。

光说不练假把式,下面我们来看一个实际的形参例子:

cs 复制代码
public void SayHelloTo(string name)
{
    Console.WriteLine("Hello, {0}!", name);
}

这里的name就是一个形参,它也是一个变量,在整个方法体中都不允许存在第2个名为name的局部变量了。我们说了,形参是一个占位符,当我们传入一个Tom,这段程序的输出结果即为:

Hello,Tom!

同理,传入Jack,输出为:

Hello,Jack!

因此,形参具有如下特点:

1)形参是一个变量,它具有变量的全部特点,比如可以存取值;

2)方法的形参可以有多个,形参之间使用逗号隔开,即使类型相同

的多个参数也不可合并声明。

5.2实参

实参是相对形参而言的。我们知道,形参是实际值的"替身",或者说"占位符",那么这个替身所代表的实际值即为实参。这里的实际值可

以为具体的值,也可以是一个变量,如果不好理解的话,我们来看如图8-7所示的示例。

如图8-7所示,调用Add方法,传入两个参数,第一个20是一个实际的值,第二个i2为变量,它已经在调用前声明并且做了初始化,然后开始对Add方法的调用。此时,形参a代表实际值20,形参b代表实际值i2(也就是10),那么返回值即为30。

因此,实参是:

1)用于初始化形参的实际值或表达式;

2)实参位于要调用的方法参数列表中。

5.3

C#4.0的新增特性之一就是命名和可选参数,这是一个十分独特的特性。在4.0版本之前,要调用一个方法,必须传入方法签名规定的参数个数,并且每个参数的类型也要一致,这在某些情况下是十分繁琐的,如果某个参数可以不传,又会怎样?怎样指定某个参数的默认值?怎样不按照方法签名规定的参数顺序传值?这就是我们要研究的命名和可选参数,它能满足你的这些愿望!还等什么?我们开始吧!

1)可选参数

可选参数,顾名思义,它不是必需的,英文叫做optional。我们知道,位于方法签名中的参数为形参,它扮演着"占位符"的角色,如果不

为它指定值,可能会导致运行出错或不可知的结果(这里不考虑声明了

形参,而在方法体却没有使用的情况),我们可以为这样的参数指定一

个默认值,如代码清单8-3所示。

代码清单8-3 可选参数

cs 复制代码
int result = ms.Add(20);
int result = ms.Add(20, 10);
public int Add(int a, int b = 1)
{
    return a + b; // 运算符两侧各留一个空格
}

如果没有可选参数,我们必须通过方法重载来达到相同目的,如代码清单8-4所示。

代码清单8-4 使用方法重载替代可选参数

cs 复制代码
int result = ms.Add(20);
int result2 = ms.Add(20, 10); 

public int Add(int a, int b)
{
    return a + b;
}

public int Add(int a)
{
    return a + 1;
}

关于方法重载,我们会在9节进行介绍,想要提前了解的读者可以自行参阅。

对比代码清单8-3和代码清单8-4,实现相同的目的,显然前者更简洁、更方便!当然,你也可以传入两个实参,此时以你传入的实参为准,而不使用默认值。

在图8-8中,在传入参数不同的情况下,形参所代表的值和返回值情况见表8-1。

可选参数虽然好用,但使用它是要遵守一定规则:

1)可选参数不能为参数列表第一个参数,它必须位于所有必选参数

之后;

2)可选参数必须指定一个默认值;

3)可选参数的默认值必须是一个常量表达式,不能为变量;

4)所有可选参数以后的参数都必须是可选参数。

提示:不仅方法的参数可以设定为可选,在后面要讲到的知识点中,类的构造函数、索引器、委托都可以指定某些参数为必选或者可选。并且,可以使用OptionalAttribute标签指定可选的参数。

2)命名参数

可选参数解决的是参数默认值的问题,而命名参数解决的是参数顺序的问题。命名参数将我们从记忆每个方法数目繁多的参数列表中解放了出来,每个参数都有一个名称(推荐使用有意义的名称),下一步就可以使用参数名来对参数进行赋值(实参),而不必在意参数的实际顺序如何。是不是听起来激动人心?下面使用一个计算矩形面积的小例子进行说明。先来看看不使用命名参数该如何,如代码清单8-5所示。

代码清单8-5 不使用命名参数的情况

cs 复制代码
using System;

public class MethodSample
{
    public int CalculateRectangleArea(int length, int width)
    {
        if (length < width)
        {
            return length * width;
        }
        return 0;
    }
}

public class Program
{
    public static void Main()
    {
        MethodSample ms = new MethodSample();
        int l = 10;
        int w = 20;
        int result = ms.calculateRectangleArea(l, w);
        Console.WriteLine("The Rectangle's area is:{0}m2", result);
    }
}

在上述代码中,CalculateRectangleArea方法要求传入两个参数:

1)矩形的长度(length)

2)矩形的宽度(width)

并且,这两个参数是有顺序的,前者为长度,后者为宽度。并且此方法的逻辑要求如果长度小于宽度,才计算面积,否则返回0,这里的

逻辑毫无意义,仅起到约束参数顺序的作用。

我们为这个方法传入了两个实参:l和w,其值分别为10和20,满足逻辑要求,因此输出此矩形面积值:

html 复制代码
The Rectangle's area is:200 m2

下面,我们使用命名参数对其进行改写,如代码清单8-6:代码清单8-6 使用命名参数的情况

cs 复制代码
using System;

public class MethodSample
{
    public int CalculateRectangleArea(int length, int width)
    {
        if (length < width)
        {
            return length * width;
        }
        return 0;
    }
}

public class Program
{
    public static void Main()
    {
        MethodSample ms = new MethodSample();
        int l = 10;
        int w = 20;
        int result = ms.CalculateRectangleArea(width: w, length: l);
        Console.WriteLine("The Rectangle's area is:{0}m2", result);
    }
}

在上述代码中,我们传入的实参顺序为先宽度,后长度,这在不使用命名参数的情况下由于第一个参数被认为是长度,后者被认为是宽

度,因此(20<10)表达式不成立,应该输出0,但这里因为使用命名参数,虽然顺序不对,但因为实参信息中包含了参数名信息,因此运算的结果仍然是正确的,length形参被赋值10,width形参被赋值20,满足方法逻辑,运行结果同代码清单8-5。

3)重载决策机制

可选和命名参数导致了重载决策机制,先看下面这种情况,编译器会做何种选择呢?如代码清单8-7所示。

代码清单8-7 重载决策机制

html 复制代码
M(string s,int i=1);
M(object o);
M(int i,string s="Hello");
M(int i);
M(5);

当遇到这种情况的时候,编译器会根据以下规则进行判断,我们先说规则,再来分析上面这段代码。规则如下:

-1)如果所有参数要么都可选,要么有一个和参数类型兼容的参数(根据名称或位置);

-2)如果有多种选择,重载决策机制优先选择为参数指定了具体值的重载,而忽略那些没有为可选参数提供值的情况;

-3)如果有两种选择待选,则优先选择没有省略可选参数的重载。

下面,依据上述规则来分析在代码清单8-7的情况下,M(5);执行的是哪个重载。

首先,M(string,int)被淘汰,因为实参5不能被隐式转换为string类型;

其次,M(int,string)符合条件,是待选之一,因为它的第二个参数是可选的;

显然,M(object)和M(int)也都是待选的重载;

但是,M(int,string)和M(int)都要优于M(object),因为5到int的转换要好于5到object的转换;

最后,M(int)要优于M(int,string),因为没有可选参数被省略。

因此,M(5);最后调用的是M(int)方法。

6.四种类型的参数

截至目前,我们学习的参数都是一种类型,也是默认参数类型,叫做值参数。下面将学习另外几种参数,它们的行为和值参数稍有不同。

6.1按值传递参数

值参数是通过将实参的值复制到形参,来实现将值传递到方法,也就是我们通常说的按值传递。方法被调用时,CLR做如下操作:

1)在托管堆栈中为形参分配空间;

2)将实参的值复制到形参。

值参数中,实参也可以是任何计算结果满足类型要求的表达式,不一定是变量。关于这一点,举例如下,如代码清单8-8所示。

代码清单8-8 使用表达式做实参

cs 复制代码
using System;

public class MethodSample
{
    public int Add(int a, int b)
    {
        return a + b;
    }
}

public class Program
{
    public static void Main()
    {
        MethodSample ms = new MethodSample();
        int l = 10;
        int w = 20;
        int result = ms.Add(l * 2, (w + 10) / 10);
        Console.WriteLine("Add(l*2,(w+10)/10)={0}", result);
    }
}

上述代码中,(l*2)和((w+10)/10)充当了实参的角色,这两个表达式的计算结果均为int型,它们满足Add方法对签名类型的要求,因此是可以的。

下面,通过一个例子(见代码清单8-9),分别观察形参、实参在托管堆和托管栈中的分配和使用情况,以加深对值参数的理解。

代码清单8-9 按值传递参数示例

cs 复制代码
using System;

public class MethodSample
{
    public static void Main()
    {
        MethodSample ms = new MethodSample();
        Rectangle rectangle = new Rectangle();
        int myValue = 10;

        ms.CalculateArea(rectangle, myValue); //调用方法,这两个是实参
    }

    public void CalculateArea(Rectangle rect, int value) //这两个是形参
    {
        rect.Length += 10; //长度加10
        rect.Width += 15; //宽度加15
        rect.Area = rect.Length * rect.Width; //计算矩形面积
        value++; //第2个参数自增1
    }
}

//矩形类
public class Rectangle
{
    public int Length = 10; //初始化长度为10
    public int Width = 15; //初始化宽度为15
    public int Area;
}

我们把代码清单8-9的代码用图表示如下,揭示托管堆栈和托管堆中的资源分配情况,如图8-9所示。

下面对图8-9中的四个状态做个说明(见表8-2):

6.2按引用传递参数------关键字:ref

和前面的"按值传递"相对应的是按引用传递。顾名思义,这里传的不再是值,而是引用,通过图8-9我们看到,实参myValue的值在方法执行以后并没有发生改变,原因就是在方法内部操作的不是myValue变量本身,而是它的"克隆"------value变量。那么,当我们想要myValue在方法执行之后也做了相应的改变,该如何去做呢?那就用到了本节要讲的内容:引用参数,也就是说,传递的是参数的引用而不是参数值。打个比方:你想要风筝,而我不是给你买一个跟我一模一样的风筝,我交给你的是风筝线,至于你对这只风筝如何处置,就看你的了,你所做的每一个动作都直接作用于风筝本身,而不是作用于和它一模一样的副本。要实现通过引用传递参数也不难,只需要在形参和实参前面都加上ref关键字即可,我们举个例子来看,如代码清单8-10:代码清单8-10 按引用传递参数

cs 复制代码
using System;

public class MethodSample
{
    public static void Main()
    {
        MethodSample ms = new MethodSample();
        int x = 5;
        string s = "猜猜我会被改变吗?";
        
        ms.DoSomething(ref x, ref s); // 实参前加ref关键字
        Console.WriteLine("x={0},s={1}", x, s);
    }

    public void DoSomething(ref int someValue1, ref string someValue2) // 形参前加ref关键字
    {
        someValue1 = 10;
        someValue2 = "我确实被改变了";
    }
}

如代码清单8-10所示,首先在方法声明的时候,在形参前加上ref关键字,如第15行所示。其次在调用方法时,在实参前加ref关键字,如第9行所示。这时传递的就是x和s变量的引用,而不是其值,上述代码的运行结果为:

html 复制代码
x=10,s=我确实被改变了

那么,我们将上述按值传递参数的代码(见代码清单8-9)改为按引用传递(见代码清单8-11),来观察它们的异同。

代码清单8-11 按引用传递参数示例

cs 复制代码
using System;

public class MethodSample
{
    public static void Main()
    {
        MethodSample ms = new MethodSample();
        Rectangle rectangle = new Rectangle();
        int myValue = 10;
        
        ms.CalculateArea(ref rectangle, ref myValue);
    }

    public void CalculateArea(ref Rectangle rect, ref int value)
    {
        rect.Length += 10;
        rect.Width += 15;
        rect.Area = rect.Length * rect.Width;
        value++;
    }
}

public class Rectangle
{
    public int Length = 10;
    public int Width = 15;
    public int Area;
}

我们仍然把上述代码使用图8-10表示如下,然后再结合图示给大家做解释。

下面我们对上述代码和图示做如下解释,见表8-3:

由此我们可以得出以下结论:

1)按引用传递的参数,系统不再为形参在托管栈中分配新的内存;

2)此时,形参名其实已经成为实参名的一个别名,它们成对地指向

相同的内存位置。

6.3输出参数------关键字:out

输出参数和引用参数有一定程度的类似,输出参数用于将值从方法内传递到方法外。要使用输出参数只需要将引用参数的ref关键字替换为out关键字即可,但有一点必须注意的是,只有变量才有资格作为输出参数,文本值和表达式都不可以,这点要谨记。好了,我们来看一个例子,里面有引用参数和输出参数,大家可以做个对比,然后再看看它们的运行结果有何异同。如代码清单8-12所示。

代码清单8-12 输出参数

cs 复制代码
using System;

public class MethodSample
{
    public static void Main()
    {
        MethodSample ms = new MethodSample();
        int x = 5;
        string s; // out参数在使用前可以不初始化
        
        ms.DoSomething(ref x, out s);
        Console.WriteLine("x={0},s={1}", x, s);
    }

    public void DoSomething(ref int someValue1, out string someValue2)
    {
        // Console.WriteLine(someValue1);
        // Console.WriteLine(someValue2); // out参数在赋值前不能使用
        someValue1 = 10;
        someValue2 = "我确实被改变了";
    }
}

是不是形式很相似?再看运行结果:

x=10,s=我确实被改变了

咦?这和代码清单8-10的运行结果完全一样!是不是就可以认为输出参数和引用参数除了关键字不同以外,其实就是一个东西?且慢!请

读者将代码清单8-12自行录入电脑,分别将第15、16行注释去掉,运行两次,看看两次结果是否相同。亲自动手做过实验的读者就能发现,去掉第15行的注释能正常编译运行,而去掉第16行的注释则编译都无法通过,编译器发出如下错误信息:

使用了未赋值的out参数"someValue2"

这说明两个问题:

1)编译器允许在方法中的任意位置、任意时刻读取引用参数的值;

2)编译器禁止在为输出参数赋值前读取它。

这意味着输出参数的初始值基本上是没意义的,因为它在使用前要被赋为新的值。因此,想通过输出参数将值传入方法的路是行不通的。

这次,我们将代码清单8-11使用输出参数改写,来看看会是怎样的结果,它和前面按引用传递有何异同。如代码清单8-13所示。

代码清单8-13 使用输出参数示例

cs 复制代码
using System;

public class MethodSample
{
    public static void Main()
    {
        MethodSample ms = new MethodSample();
        Rectangle rectangle;
        int myValue;
        
        ms.CalculateArea(out rectangle, out myValue);
    }

    public void CalculateArea(out Rectangle rect, out int value)
    {
        Rectangle tmpRect = new Rectangle();
        tmpRect.Length += 10;
        tmpRect.Width += 15;
        tmpRect.Area = tmpRect.Length * tmpRect.Width;
        
        rect = tmpRect;
        value = 15;
    }
}

public class Rectangle
{
    public int Length = 10;
    public int Width = 15;
    public int Area;
}

大家仔细观察代码清单8-13和代码清单8-11,有什么不同?结果我们将稍后揭晓,按照惯例,现在我们使用图8-11来揭示代码背后的事实。

现在揭晓答案,见表8-4。

6.4参数数组------关键字:params

这里引入一个崭新的参数类型:参数数组,它的关键字是params。知识的积累往往从类比开始。这里的参数数组有点类似于前面讲的可选参数,但也有很大的不同。相比之下,参数数组更加灵活,具体先来看一个示例,如代码清单8-14所示:代码清单8-14 参数数组

cs 复制代码
using System;

public class MethodSample
{
    public static void Main()
    {
        MethodSample ms = new MethodSample();
        ms.DoSomething("a"); // 参数数组本身可选,这里没传一个值
        ms.DoSomething("b", 1); // 传一个值,类型要和定义一致
        ms.DoSomething("c", 1, 2); // 传两个值
        
        int[] array = { 1, 2, 3, 4 };
        ms.DoSomething("d", array); // 还可以传一个数组
    }

    // 参数声明的划线部分为参数数组声明方式
    public void DoSomething(string str, params int[] values)
    {
        if (null != values && values.Length > 0)
        {
            for (int i = 0; i < values.Length; i++)
            {
                Console.WriteLine("{0},{1}", str, values[i]);
            }
        }
        else
        {
            Console.WriteLine(str);
        }
    }
}

如代码清单8-14所示,从第17行可以看出,参数数组的定义以params关键字开始,指明数组类型(这里是int型),然后是参数名称;然而,第8、9、10、13行对带有参数数组的方法的调用却显示出和前面的引用参数、输出参数很大的不同:调用方法的实参前没有带相应的关键字。这里我们对比一下前面学到的各种参数类型,它们的声明和调用方式见表8-5。

7.栈帧

栈帧也叫做过程活动记录,是编译器用来实现方法调用的一种数据结构。栈帧包含如下信息:

1)方法参数;

2)方法中的局部变量;

3)方法执行完后的返回地址;

4)获取文件中包含所执行代码的行号和列号;

5)获取包含所执行代码的文件名;

6)......

代码清单8-15 给出了栈帧示例:

代码清单8-15 栈帧示例

cs 复制代码
using System;

public class MethodSample
{
    public static void Main()
    {
        MethodSample ms = new MethodSample();
        ms.DoSomething1();
    }

    public void DoSomething1()
    {
        Console.WriteLine("DoSomething1()被调用.");
        DoSomething2();
    }

    public void DoSomething2()
    {
        Console.WriteLine("DoSomething2()被调用.");
    }
}

上述代码,有三个方法,分别是Main()、DoSomething1()、DoSomething2(),它们之间的调用关系如代码清单8-15所示,那么栈帧在托管栈中是什么情况呢?见图8-12。

图8-12表示了在方法调用时托管栈中的栈帧的情况,具体如下:

1)Main方法开始执行,此刻Main方法的栈帧入栈;

2)进入Main方法,开始调用DoSomething1方法,DoSomething1栈帧入栈;

3)进入DoSomething1方法,开始调用DoSomething2方法,DoSomething2栈帧入栈;

4)DoSomething2方法执行完毕,栈帧出栈;

5)DoSomething1方法执行完毕,栈帧出栈。

8.递归

简单地说,当方法直接或者间接调用自己时,则发生了递归。

递归是一种算法,指的是方法在调用过程中直接或间接调用自身而产生的重入现象。它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。用递归思想写出的程序往往十分简洁易懂。

一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。

注意:

1)递归就是在方法里调用自身;

2)在使用递归策略时,必须有一个明确的递归结束条件,称为递归出口,否则将无限进行下去(死锁)。

我们已经知道了什么是递归,接下来,我们使用一个实例来说明有趣的递归算法。计算阶乘是递归程序设计的一个经典示例。计算某个数的阶乘就是用那个数去乘以包括1在内的所有比它小的数。例如,factorial(5)等价于5*4*3*2*1,而factorial(3)等价于3*2*1。

阶乘的一个有趣特性是,某个数的阶乘等于起始数(startingnumber)乘以比它小1的数的阶乘。例如,factorial(5)与5*factorial(4)相同。你很可能会这样编写阶乘函数,见代码清单8-16:

代码清单8-16 阶乘计算

cs 复制代码
public int factorial(int n)
{ 
     return n*factorial(n-1);
}

能否看出这个程序有什么问题?对了,它缺少递归结束条件,这段程序将永远无休止地执行下去。因此,我们需要设置一个结束条件,如代码清单8-17所示:

代码清单8-17 改进后的阶乘计算

cs 复制代码
public int Factorial(int n)
{
    // 递归结束条件
    if (n == 1)
    {
        return 1;
    }
    else
    {
        return n * Factorial(n - 1);
    }
}

当然,任何事物都不是完美无瑕的,递归也有缺点:

1)运行效率较低;

2)在递归调用的过程当中,系统会为每一次调用生成新的栈帧,并

使用栈来存储,因此递归次数过多容易造成栈溢出。

9.方法的重载

首先说一下什么是方法重载。在C#这样的面向对象的高级语言中,允许我们在类中定义多个名称相同、签名(方法名、参数类型及个数、参数顺序及修饰符)不同的方法。需要注意的是,方法的返回值不属于签名的一部分。也就是说,决定方法是否构成重载有以下几个条件:

1)在同一个类中;

2)方法名相同;

3)方法签名不同。

下面,我们看一个方法重载的示例,见代码清单8-18:

代码清单8-18 方法重载

cs 复制代码
using System;

public class MethodSample
{
    public static void Main()
    {
        MethodSample ms = new MethodSample();
        ms.DoSomething(5);
        ms.DoSomething(5, "10");
        ms.DoSomething("10", 5);
    }

    public int DoSomething(int n)
    {
        Console.WriteLine("方法1");
        return 1;
    }

    public void DoSomething(int n, string m)
    {
        Console.WriteLine("方法2");
    }

    public void DoSomething(string m, int n)
    {
        Console.WriteLine("方法3");
    }
}

如代码清单8-18所示,第11行和第16行就是两个DoSomething的重载方法,它们的参数个数不同,名称相同,这构成了重载的条件。

特别注意第2和第3个重载,它们的参数个数和类型均相同,但参数顺序不同,这同样构成了重载。当有多个重载方法可用的时候,VisualStudio的IntelliSense功能会进行提示,如图8-13所示:

10.静态方法

方法主要可以分为静态方法和实例方法[1]。如何区分静态方法和实例方法呢?很简单,使用了static修饰符的方法为静态方法,反之则是非静态方法。

静态方法有几个特点:

1)它不属于特定对象的方法,它属于某一个类的具体实例;

2)它只可以访问静态成员变量,而不可以直接访问实例变量;

3)它不用创建类的对象即可访问(要求满足相应访问级别的情况下);

4)静态方法中不能使用this关键字引用类的当前实例。

接下来看一个实例,如代码清单8-19所示:

代码清单8-19 静态方法

cs 复制代码
using System;

namespace ProgrammingCSharp41
{
    public class MethodSample
    {
        // 静态字段
        public static string str1;
        // 实例字段
        public string str2;

        public static void Main()
        {
            MethodSample ms = new MethodSample();

            // 错误,通过类实例不能调用静态方法
            // ms.DoSomething1();

            // 正确,必须通过类调用静态方法
            MethodSample.DoSomething1();

            // 正确,通过类实例调用实例方法
            ms.DoSomething2();
        }

        /// <summary>
        /// 静态方法
        /// </summary>
        /// <returns></returns>
        public static int DoSomething1() // static关键字
        {
            str1 = "只能访问静态成员str1";
            // this.str2 = "错误,无法编译通过"; // 静态方法中不能使用 this 关键字

            Console.WriteLine("静态方法:");
            Console.WriteLine(str1);

            return 1;
        }

        /// <summary>
        /// 实例方法
        /// </summary>
        public void DoSomething2()
        {
            str1 = "可以访问静态成员str1";   // 实例方法可以访问静态成员
            str2 = "也可以访问非静态成员str2"; // 实例方法可以访问实例成员

            Console.WriteLine("实例方法:");
            Console.WriteLine(str1);
            Console.WriteLine(str2);
        }
    }
}

说明,如表8-6所示。

相关推荐
张人玉2 小时前
C# 串口通讯中 SerialPort 类的关键参数和使用方法
开发语言·c#·串口通讯
时光追逐者7 小时前
一款基于 .NET WinForm 开源、轻量且功能强大的节点编辑器,采用纯 GDI+ 绘制无任何依赖库仅仅100+Kb
c#·.net·winform
sali-tec7 小时前
C# 基于halcon的视觉工作流-章58-输出点云图
开发语言·人工智能·算法·计算机视觉·c#
白雪公主的后妈7 小时前
Auto CAD二次开发——文字样式
c#·cad二次开发·文字样式
智者知已应修善业7 小时前
【c# 想一句话把 List<List<string>>的元素合并成List<string>】2023-2-9
经验分享·笔记·算法·c#·list
FuckPatience8 小时前
C# 接口隔离的一个案例
c#
E_ICEBLUE9 小时前
快速合并 Excel 工作表和文件:Java 实现
java·microsoft·excel
津津有味道9 小时前
Ntag 424 DNA写入URI网址配置开启动态UID计数器镜像C#源码
c#·uri·ndef·424dna·动态uid·计数器镜像
万199913 小时前
asp.net core webapi------3.AutoMapper的使用
c#·.netcore