《C#图解教程》拾遗六-方法

目录

一、局部变量

1、对比实例字段和局部变量

2、var关键字

二、局部常量

1、特征

2、格式

三、控制流

跳转语句

四、局部函数

五、参数

1、形参

2、实参

位置参数

六、值参数

七、引用参数

八、引用类型作为值参数和引用参数

1、将引用类型作为值参数传递

2、将引用类型对象作为引用参数传递

九、输出参数

十、参数数组

1、方法调用

2、将数组作为实参

十一、参数类型总结

十二、ref局部变量和ref返回

1、ref局部变量

 2、ref返回

十三、方法重载

十四、命名参数

十五、可选参数

 十六、栈帧

 十七、递归


一、局部变量

1、对比实例字段和局部变量

实例字段与局部变量区别
实例字段局部变量
生存期实例被创建开始,到实例没有用时结束在块中被声明开始,到块完成执行结束
隐式初始化初始化为实例的默认值没有隐式初始化,若不被初始化,编译器报错
存储区域因为是类成员,不论是引用类型还是值类型,一律存储在堆内存

值类型存储在栈,引用类型存储在堆

2、var关键字

  1. 只能用于局部变量,不能用于字段声明;
  2. 只能在初始化变量时使用;
  3. 一旦编译器推断出类型,就固定且不可改变;
  4. 不像JavaScript中的var可以引用不同的类型,C#的var是从表达式右边推断出变量类型,因此var并不改变C#的强类型性质。

二、局部常量

1、特征

  1. 在声明时必须初始化;
  2. 声明后不能改变;

2、格式

c31d538d2f3b4d93a918ff5eb4a672c1.png

其中,const是关键字,Value初始化值是必须的。

初始化值是在编译时期决定的,可以是null引用,但不能是某对象的引用,因为对象的引用是在运行时决定的。

三、控制流

跳转语句

  • break   跳出当前循环
  • continue   到当前循环的顶部
  • goto   到一个命名的语句
  • return   返回到调用方法继续执行

四、局部函数

在一个方法内部嵌套的另一个方法就是局部函数。

与局部变量不同,局部函数可写在使用处的任何地方,也就是说,使用局部函数的代码可以先于局部函数的定义。

class LocalFunction
{
    public void MethodWithLocalFunction()
    {
        int result = MyLocalFunction(6);
        Console.WriteLine($"Results of local function call:{result}");

        int MyLocalFunction(int z1)     //声明局部函数
        {
            return z1 * 5;
        }
    }
}

五、参数

1、形参

形参是局部变量,声明在方法参数列表而不是方法体中。

  • 形参是变量,因此可以读写;
  • 与局部变量不同,形参方法体外面已定义,在方法体开始之前初始化,输出参数除外;
  • 参数列表中可以有任意数目的形参,彼此以逗号隔开;

2、实参

实参是初始化形参的表达式或变量。

  • 实参位于调用函数的参数列表中;
  • 实参必须与对应位置的形参类型保持一致,或者编译器能够推断出实参类型与对应位置形参类型一致;

位置参数

实参的数量必须与形参保持一致,且对应位置的类型也必须保持一致,这种形式的参数称为位置参数。

class LocationParameter
{
    public int Sum(int x,int y)
    {
        return x + y;
    }

    public float Avg(float f1,float f2)
    {
        return (f1 + f2) / 2;
    }

    public void ConsoleOut()
    {
        int someInt = 6;
        Console.WriteLine($"Newflash: Sum:5 + {someInt} = {Sum(5, 6)}");
        Console.WriteLine($"Newflash: Avg:(5 + {someInt})/2 = {Avg(5, 6)}");
    }
}

其中,调用Avg函数时,编译器已经将5和someInt隐式转换为float类型了。

六、值参数

值参数是把实参的值复制给形参。

方法被调用时执行如下操作:

  • 在栈中为形参分配空间;
  • 将实参的值复制给形参;

注意:值类型与值参数没有关系,值类型是指类型本身包含其值。

class MyValueParameterClass
{
    public int Val = 20;
}
public class ValueParameter
{
    static void MyMethod(MyValueParameterClass f1,int f2)
    {
        f1.Val = f1.Val + 5;
        f2 = f2 + 5;
        Console.WriteLine($"f1.Val={f1.Val}, f2={f2}");
    }

    static void Main()
    {
        MyValueParameterClass a1 = new MyValueParameterClass();
        int a2 = 10;

        MyMethod(a1, a2);
        Console.WriteLine($"a1.Val={a1.Val}, a2={a2}");
    }
}

执行结果: 

 上图展示了实参和形参在方法执行的不同阶段的值,表明如下3点:

  • 在方法调用前,用作实参的变量a2已经在栈里了;
  • 在方法开始时,系统在栈中为形参分配空间,并从实参复制值
    • 因为a1是引用类型的,所以引用被复制,结果实参和形参都引用堆中的同一个对象;
    • 因为a2是值类型的,所以值被复制,产生了一个独立的数据项;
  • 在方法的结尾,f2和对象f1的字段都被加上了5
    • 方法执行后,形参从栈中弹出;
    • a2,值类型,它的值不受方法行为的影响;
    • a1,引用类型,但它的值被方法的行为改变了;

七、引用参数

使用引用参数时,必须在方法的声明和调用中都使用ref修饰符。

实参必须是变量,在用作实参前必须被赋值。如果是引用类型变量,可以赋值为一个引用或null。

与值参数相比,引用参数有以下特点:

  • 不会在栈上为形参分配内存;
  • 形参的参数名将作为实参变量的别名,指向相同的内存位置。
class MyRefParameterClass
{
    public int Val = 20;
}

class RefParameter
{
    static void MyMethod(MyRefParameterClass f1,int f2)
    {
        f1.Val = f1.Val + 5;
        f2 = f2 + 5;
        Console.WriteLine($"f1.Val={f1.Val}, f2={f2}");
    }

    static void Main()
    {
        MyRefParameterClass a1 = new MyRefParameterClass();
        int a2 = 10;

        MyMethod(a1, a2);
        Console.WriteLine($"a1.Val={a1.Val}, a2={a2}");
    }
}

执行结果: 

 上图阐明了引用参数在方法执行的不同阶段实参和形参的值:

  • 在方法调用之前,将要被用作实参的变量a1和a2已经在栈里了;
  • 在方法的开始,形参名被设置为实参的别名,变量a1和f1引用相同的内存位置,a2和f2引用相同的内存位置;
  • 在方法的结束位置,f2和f1的对象的字段都被加上了5;
  • 方法执行之后,形参的名称已经失效,但是值类型a2的值和引用类型a1所指向的对象的值都被方法内的行为改变了;

八、引用类型作为值参数和引用参数

1、将引用类型作为值参数传递

如果在方法内创建一个新对象并赋值给形参,将切断形参与实参之间的关联,并且在方法调用结束后,新对象也将不复存在。

class MyRefAsValueParamClass
{
    public int Val = 20;
}
class RefAsValueParam
{
    static void MyMethod(MyRefAsValueParamClass f1)
    {
        f1.Val = 50;
        Console.WriteLine($"After member assignment: {f1.Val}");
        f1 = new MyRefAsValueParamClass();
        Console.WriteLine($"After new object creation: {f1.Val}");
    }

    static void Main()
    {
        MyRefAsValueParamClass a1 = new MyRefAsValueParamClass();
        Console.WriteLine($"Before method call: {a1.Val}");
        MyMethod(a1);
        Console.WriteLine($"After method call: {a1.Val}");
    }
}

 执行结果:

 上图阐明了以下几点:

  • 在方法开始时,实参和形参指向堆中相同的对象;
  • 在为对象的成员赋值之后,它们仍指向堆中相同的对象;
  • 当方法分配新的对象并赋值给形参时,(方法外部的)实参仍指向原始对象,而形参指向的是新对象;
  • 在方法调用之后,实参指向原始对象,形参和新对象都会消失;

2、将引用类型对象作为引用参数传递

如果在方法内创建一个新对象并赋值给形参,在方法结束后该对象依然存在,并且是实参所引用的值。

class MyRefAsRefParamClass
{
    public int Val = 20;
}

class RefAsRefParam
{
    static void RefAsParameter(ref MyRefAsRefParamClass f1)
    {
        //设置对象成员
        f1.Val = 50;
        Console.WriteLine($"After member assignment: {f1.Val}");

        //创建新对象并赋值给形参
        f1 = new MyRefAsRefParamClass();
        Console.WriteLine($"After new object creation: {f1.Val}");
    }

    static void Main(string[] args)
    {
        MyRefAsRefParamClass a1 = new MyRefAsRefParamClass();
        Console.WriteLine($"Before method call: {a1.Val}");
        RefAsParameter(ref a1);
        Console.WriteLine($"After method call: {a1.Val}");
    }
}

执行结果:

 引用参数充当形参的别名,那么上述代码就容易理解了,上图阐明了以下几点:

  • 在方法调用时,形参和实参指向堆中相同的对象;
  • 对成员值的修改会同时影响到形参和实参;
  • 当方法创建新的对象并赋值给形参时,形参和实参的引用都指向该新对象;
  • 在方法结束后,实参指向在方法内创建的新对象;

九、输出参数

与引用参数类似,输出参数的形参充当实参的别名,输出参数有如下要求:

  • 必须在声明和调用中都使用修饰符,输出参数的修饰符是out;
  • 和引用参数类似,实参必须是变量,而不能是其它类型的表达式,因为方法需要内存位置来保存返回值;

 与引用参数不同,输出参数有以下要求:

  • 在方法内部,给输出参数赋值之后才能读取它,这意味着参数的初始值是无关的,而且没有必要在方法调用之前为实参赋值;
  • 在方法内部,在方法返回之前,代码中每条可能的路径都必须为所有输出参数赋值;
class MyOutParameterClass
{
    public int Val = 20;
}
class OutParameter
{
    static void MyMethod(out MyOutParameterClass f1,out int f2)
    {
        //在方法内,输出参数必须在赋值后读取
        //Console.WriteLine($"输出参数赋值前:{f1}  {f2}");
        f1 = new MyOutParameterClass();
        f1.Val = 25;
        f2 = 15;
        Console.WriteLine($"输出参数赋值后:{f1}  {f2}");
    }

    static void Main(string[] args)
    {
        MyOutParameterClass a1 = null;
        int a2;
        MyMethod(out a1, out a2);
        Console.WriteLine($"调用方法后:a1 = {a1}, a2 = {a2}");
    }
}

 上图阐述了在方法执行的不同阶段中实参和形参的值:

  • 在方法调用之前,将要被用作实参的变量a1和a2已经在栈里了;
  • 在方法的开始,形参的名称被设置为实参的别名,可认为a1和f1指向的是相同的内存位置,a2和f2指向的是相同的内存位置,a1和a2不在方法的作用域内,所以不能再MyMethod方法中访问;
  • 在方法内部,代码创建了一个MyOutParameterClass类型的对象并把它赋值给f1,然后赋一个值给f1的字段,也赋一个值给f2,对f1和f2的赋值都是必需的,因为它们是输出参数;
  • 方法执行之后,形参的名称已经失效,但是引用类型的a1和值类型的a2的值都被方法内的行为改变了;

十、参数数组

参数数组允许“特定”类型的零个或多个实参对应一个 特定的形参,其重点如下:

  • 在一个参数列表中只能有一个参数数组;
  • 如果有,它必须是列表中的最后一个;
  • 由参数数组表示的所有参数必须是同一类型;

声明一个参数数组时必须做的事如下:

  • 在数据类型前使用params修饰符;
  • 在数据类型后放置一组空的方括号;

关于数组,先了解以下几点:

  • 数组是一组有序的同一类型的数据项;
  • 数组使用一个数字索引进行访问;
  • 数组是一个引用类型,因此它的所有数据项都保存在堆中;

1、方法调用

有两种方式为参数数组提供实参:

  • 用逗号分隔的形参声明数据类型元素的列表,也被称作延申式,这种形式在调用中使用独立的实参;
  • 形参生命的数据类型元素的一维数组;

注意,调用时不需要使用params修饰符,参数数组修饰符的使用与其他参数类型的模式不同,

  • 其他参数类型是一致的
    • 值参数的声明和调用都不带修饰符;
    • 引用参数和输出参数在声明和调用时都需要带修饰符;
  • params修饰符的用法总结
    • 在声明中需要修饰符
    • 在调用中不允许有修饰符

对于延申式调用

static void MyMethod(params int[] intVals)
{
}

static void Main()
{
    MyMethod();//0个实参
    MyMethod(1);//1个实参
    MyMethod(1, 2);//2个实参
    MyMethod(1, 2, 3);//3个实参
    MyMethod(1, 2, 3, 4);//4个实参
}

在使用一个为参数数组使用独立实参的调用时,编译器做下面几件事:

  • 接受实参列表,用它们在队中创建并初始化一个数组;
  • 把数组的引用保存到栈中的形参里;
  • 如果在对应形参数组的位置没有实参,编译器会创建一个有零个元素的数组来使用;
class MyArrayParameterClass
{
    public void MyMethod(params int[] intVals)
    {
        if (intVals != null && intVals.Length != 0)
        {
            for (int i = 0; i < intVals.Length; i++)
            {
                intVals[i] = intVals[i] * 10;
                Console.WriteLine($"{intVals[i]}");
            }
        }
    }
}
class ArrayParameter
{
    static void Main()
    {
        int first = 5, second = 6, third = 7;

        MyArrayParameterClass a1 = new MyArrayParameterClass();
        a1.MyMethod(first, second, third);
        Console.WriteLine($"first = {first}, second = {second}, third = {third}");
    }
}

 上图阐明了在方法执行的不同阶段实参和形参的值:

  • 方法调用之前,3个实参已经在栈里;
  • 在方法的开始,3个实参被用于初始化堆中的数组,并且数组的引用被赋值给形参intVals;
  • 在方法内部,代码首先检查以确认数组引用不是null,然后处理数组,把每个元素乘以10并保存回去;
  • 方法执行之后,形参intVals失效;

关于参数数组,需要记住的一点是,当数组在堆中被创建时,实参的值被复制到数组中:

  • 如果数组参数是值参数,那么值被复制,实参在方法内部不受影响;
  • 如果数组参数时引用类型,那么引用被复制,是餐饮用的对象在方法内部会受到影响;

2、将数组作为实参

在方法调用之前,创建并组装一个数组,把单一的数组变量作为实参传递,此时,编译器使用你创建的数组,而不是重新创建一个。

int[] intArr = new int[] { 5, 6, 7 };
MyArrayParameterClass a2 = new MyArrayParameterClass();
a2.MyMethod(intArr);
foreach (int arr in intArr)
    Console.WriteLine($"{arr}");

十一、参数类型总结

参数类型语法使用总结
参数类型修饰符是否在声明时使用是否在调用时使用执行
将实参的值复制给形参
引用ref形参是实参的别名
输出out仅包含一个返回的值,形参是实参的别名
数组params允许传递可变数目的实参到方法

十二、ref局部变量和ref返回

使用ref关键字定义一个引用参数,可以传递一个对象的引用给方法调用,这样在调用上下文中,对对象的任何改动在方法返回后依然可见。

ref返回功能则相反,它允许你将一个引用发送到方法外,然后在调用上下文内使用这个引用。

一个相关的功能是ref局部变量,它允许一个变量是另一个变量的别名。

个人理解:引用参数是允许传递参数的引用给方法,并在方法内找到这个引用去内存操作引用所指向的值;ref返回则是允许将方法域内某变量的引用传递到方法外,在方法域外获取到这个引用去操作引用所指向的值;ref局部变量则是实现ref返回的桥梁,给变量起别名,使变量在方法域外可以指向方法域内的引用。

1、ref局部变量

关于ref局部变量功能的重要事项:

  • 你可以使用这个功能创建一个变量的别名,即使引用的对象是值类型;
  • 对任意一个变量的赋值都会反映到另一个变量上,因为它们引用的是相同的对象,即使是值类型;

创建别名的语法需要使用关键字ref两次,一次是在别名声明的类型的前面,另一次是在赋值运算符的右边,“被别名”的变量的前面。

class RefLocalVariable
{
    static void Main()
    {
        int x = 2;
        ref int y = ref x;//y是x的别名,当x改变时,y也会改变,反之亦然
        Console.WriteLine($"x = {x}, y = {y}");

        x = 5;
        Console.WriteLine($"x = {x}, y = {y}");

        y = 6;
        Console.WriteLine($"x = {x}, y = {y}");
    }
}

 2、ref返回

ref返回功能提供了一种使方法返回变量引用而不是变量值的方法,这里需要的额外语法也是使用了ref关键字两次:

  • 一次是在方法的返回类型声明之前;
  • 另一次是在return关键字之后,被返回对象的变量名之前;
class MyRefReturnClass
{
    private int Score = 5;

    public ref int GetScore()
    {
        return ref Score;
    }

    public void ShowScore()
    {
        Console.WriteLine($"Value inside class object: {Score}");
    }
}
class RefReturn
{
    static void Main()
    {
        MyRefReturnClass myRefReturnClass = new MyRefReturnClass();
        myRefReturnClass.ShowScore();

        ref int outSideScore = ref myRefReturnClass.GetScore();

        outSideScore = 10;
        myRefReturnClass.ShowScore();
    }
    //在调用域外修改outSideScore的值,myRefReturnClass的Score的值也会随之改变
}

 另外一个可能有用的例子是Math库中Max方法的变形,提供两个数字类型的变量,Math.Max能够返回两个值中较大的那个。但是,假设你想返回的是包含较大值的变量的引用,而不是实际的值,为此,你可以使用ref返回功能。

class RefInMathMax
{
    static ref int Max(ref int m1, ref int m2)
    {
        if (m1 > m2)
            return ref m1;  //返回引用而不是值
        else
            return ref m2;  //返回引用而不是值
    }

    static void Main()
    {
        int v1 = 10;
        int v2 = 20;

        ref int max = ref Max(ref v1, ref v2);
        Console.WriteLine($"max = {max}");
        Console.WriteLine($"v1 = {v1}, v2 = {v2}");
        max++;
        Console.WriteLine($"max = {max}, v1 = {v1}, v2 = {v2}");
    }
}

 ref返回功能有如下限制:

  • 不能将返回类型是void的方法声明为ref返回方法;
  • ref return表达式不能返回如下内容:
    • 空值
    • 常量
    • 枚举成员
    • 类或者结构体的属性
    • 指向只读位置的指针
  • ref return表达式只能指向原先就在调用域内的位置或者字段,所以,它不能指向方法的局部变量;
  • ref局部变量只能被赋值一次,也就是说,一旦初始化,它就不能指向不同的存储位置了;
  • 即使将一个方法声明为ref返回方法,如果在调用该方法时省略了ref关键字,则返回的将是值,而不是指向值的内存位置的指针;
  • 如果将ref局部变量作为常规的实际参数传递给其他方法,则该方法仅获取该变量的一个副本,尽管ref局部变量包含指向存储位置的指针,但是当以这种方式使用时,他会传递值而不是引用;

十三、方法重载

一个类中可以有多个同名方法,这叫作方法重载,使用相同名称的每个方法必须有一个和其他方法不同的签名。

  • 方法的签名由下列信息组成,它们在方法声明的方法头中:
    • 方法的名称;
    • 参数的数目;
    • 参数的数据类型和顺序;
    • 参数修饰符;
  • 返回类型不是签名的一部分,而我们往往误认为它是签名的一部分;
  • 形参的名称也不是签名的一部分;
class MethodOverLoading
{
    #region 以下五个方法是AddValues的合法重载方法
    long AddValues(int a,out int b)
    {
        b = 10;
        return a + b;
    }
    long AddValues(int a,int b)
    {
        return a + b;
    }
    long AddValues(int a,int b,int c)
    {
        return a + b - c;
    }
    long AddValues(long a,long b)
    {
        return a + b;
    }
    float AddValues(float a,float b)
    {
        return a + b;
    }
    #endregion

    #region 以下五个方法是AddValues的合法重载方法
    long AddValues(long e,long f)
    {
        return e + f;
    }
    int AddValues(long a,long b)
    {
        return a + b;
    }
    #endregion 
}

十四、命名参数

此前我们看到的都是位置参数,即方法调用时,实参与形参的位置都是一一对应的。

命名参数则不同,只要显式指定参数的名字,就可以以任意顺序在方法调用中列出实参,细节如下:

  • 方法的声明没有什么不一样,形参已经有名字了;
  • 不过在调用方法的时候,形参的名字后面跟着冒号和实际的参数值或表达式;

可以同时使用位置参数和命名参数,但所有位置参数必须先列出,如下代码:

class NamedParameter
{
    public int Calc(int a,int b,int c)
    {
        return (a + b) * c;
    }

    static void Main()
    {
        NamedParameter np = new NamedParameter();

        int r0 = np.Calc(4, 3, 2);                  //位置参数
        int r1 = np.Calc(4, b: 3, c: 2);            //位置参数和命名参数
        int r2 = np.Calc(4, c: 2, b: 3);            //交换了顺序
        int r3 = np.Calc(c: 2, b: 3, a: 4);         //所有都是命名参数
        int r4 = np.Calc(c: 2, b: 1 + 2, a: 2 + 2); //命名参数表达式

        Console.WriteLine($"{r0},{r1},{r2},{r3},{r4}");
    }
}

执行结果: 

 命名参数对于自描述的程序来说很有用,我们可以在方法调用的时候显示哪个值赋给哪个形参,如下代码:

class CylinderVolume
{
    double GetCylinderVolume(double radius,double height)
    {
        return 3.1415926 * radius * radius * height;
    }

    static void Main(string[] args)
    {
        CylinderVolume cv = new CylinderVolume();
        double volume;

        volume = cv.GetCylinderVolume(3.0, 4.0);
        Console.WriteLine($"volume = {volume}");
        volume = cv.GetCylinderVolume(height: 4.0, radius: 3.0);
        Console.WriteLine($"volume = {volume}");
    }
}

执行结果如下:

十五、可选参数

所谓可选参数就是可以在调用方法的时候包含这个参数,也可以省略它。

为表明某个参数是可选的,你需要在方法声明中为该参数提供默认值,指定默认值的语法和初始化局部变量的语法一样,如下代码所示:

class OptionalParameterOne
{
    public int Calc(int a,int b = 3)    //b为可选参数
    {
        return a + b;
    }
    static void Main()
    {
        OptionalParameterOne opo = new OptionalParameterOne();

        int r0 = opo.Calc(5, 6); //使用显式值
        int r1 = opo.Calc(5);    //为b使用默认值

        Console.WriteLine($"r0 = {r0}, r1 = {r1}");
    }
}

 执行结果:

 在上述代码中:

  • 形参b的默认值设置成3;
  • 因此,若在调用方法的时候只有一个参数,方法会使用3作为第二个参数的初始值;

对于可选参数的声明,我们需要知道如下几个重要事项:

  • 不是所有的参数类型都可以作为可选参数:
    • 只要值类型的默认值在编译的时候可以确定,就可以使用值类型作为可选参数;
    • 只有在默认值是null的时候,引用类型才可以用作可选参数;

  •  所有必填参数必须在可选参数声明之前声明,如果有params参数,必须在所以有可选参数之后声明;

 虽然可以在方法调用的时候省略相应的实参,从而为可选参数使用默认值,但是,不能随意省略可选参数的组合,因为在很多情况下这么做会导致使用哪些可选参数变得不明确,规则如下:

  • 必须从可选参数列表的最后开始省略,一直到列表开头;
  • 即,可以省略最后一个可选参数,或是最后n个可选参数,但是不能随意选择省略任意的可选参数,省略必须从最后开始;
class OptionalParameterTwo
{
    public int Calc(int a=2,int b=3,int c = 4)
    {
        return (a + b) * c;
    }

    static void Main()
    {
        OptionalParameterTwo opt = new OptionalParameterTwo();

        int r0 = opt.Calc(5, 6, 7);     //使用所有的显式值
        int r1 = opt.Calc(5, 6);        //为c使用默认值
        int r2 = opt.Calc(5);           //为b和c使用默认值
        int r3 = opt.Calc();            //使用所有的默认值

        Console.WriteLine($"r0 = {r0}, r1 = {r1}, r2 = {r2}, r3 = {r3}");
    }
}

执行结果如下:

 如果需要随意省略可选参数列表中的可选参数,而不是从列表的最后开始,那么必须使用可选参数的名字来消除赋值的歧义。

在这种情况下,需要结合利用命名参数和可选参数特性。

class OptionalParameterThree
{
    double GetCylinderVolume(double radius=3.0,double height = 4.0)
    {
        return 3.1415926 * radius * radius * height;
    }
    static void Main()
    {
        OptionalParameterThree opt = new OptionalParameterThree();
        double volume;

        volume = opt.GetCylinderVolume(3.0, 4.0);       //位置参数
        Console.WriteLine($"Volume = " + volume);

        volume = opt.GetCylinderVolume(radius: 2.0);    //使用height默认值
        Console.WriteLine($"Volume = " + volume);

        volume = opt.GetCylinderVolume(height: 2.0);    //使用radius默认值
        Console.WriteLine($"Volume = " + volume);

        volume = opt.GetCylinderVolume();               //使用两个默认值
        Console.WriteLine($"Volume = " + volume);
    }
}

执行结果:

 十六、栈帧

在调用方法的时候,内存从栈的顶部开始分配,保存和方法关联的一些数据项,这块内存叫做方法的栈帧。

  • 栈帧包含的内存保存如下内容:
    • 返回地址,也就是在方法退出的时候继续执行的位置;
    • 分配内存的参数,也就是方法的值参数,还可能是参数数组(如果有的话);
    • 和方法调用相关的其他管理数据项;
  • 在方法调用时,整个 栈帧都会压入栈;
  • 在方法退出的时候,整个栈帧都会从栈上弹出,弹出栈桢有的时候也叫作栈展开;
class StackFrame
{
    static void MethodA(int par1,int par2)
    {
        Console.WriteLine($"Enter MethodA: {par1}, {par2}");
        MethodB(11, 18);    //调用MethodB
        Console.WriteLine("Exit MethodA");
    }

    static void MethodB(int par1,int par2)
    {
        Console.WriteLine($"Enter MethodB: {par1}, {par2}");
        Console.WriteLine("Exit MethodB");
    }

    static void Main()
    {
        Console.WriteLine($"Enter Main");
        MethodA(15, 30);    //调用MethodA
        Console.WriteLine("Exit Main");
    }
}

执行结果:

 下图阐述了调用方法时栈帧压入栈的过程和方法结束后栈帧展开的过程:

 十七、递归

所谓递归,就是方法调用自身。

如下代码,计算阶乘:

class RecursionOne
{
    public int Factorial(int inValue)
    {
        if (inValue <= 1)
            return inValue;
        else
            return inValue * Factorial(inValue - 1);    //再一次调用Factrial
    }
    static void Main()
    {
        RecursionOne ro = new RecursionOne();
        int value = ro.Factorial(3);

        Console.WriteLine($"Factorial of 3: {value}");
    }
}

 执行结果:

 调用方法自身(递归)的机制和调用其他方法其实是完全一样的,都是为每一次方法调用把新的栈帧压入栈顶。

如下代码中,Count方法使用比输入参数小1的值调用自身,然后打印输入的参数。

随着递归越来越深,栈也越来越大。

class RecursionTwo
{
    public void Count(int inVal)
    {
        if (inVal == 0)
            return;
        Count(inVal - 1);   //再一次调用自身

        Console.WriteLine($"{inVal}");
    }
    static void Main()
    {
        RecursionTwo rt = new RecursionTwo();
        rt.Count(3);
    }
}

执行结果:

 注意,如果输入值3,那么Count方法就有4个不同的独立栈帧,每一个都有其自己的输出参数值inVal。下图阐述了整个调用过程:

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值