方法的由来

方法(method)的前身是C/C++语言的函数(function)

方法是面向对象范畴的概念,在非面向对象语言中仍然称为函数

永远都是类(或构造体)的成员

C#语言中函数不可能独立于类(或结构体)之外
只有作为类(构造体)的成员才能被称为方法
C++中是可以的,称为“全局函数”
C# namespace中不能有方法

是类(或结构体)最基本的成员之一

最基本的成员只有两个——字段与方法(成员变量与成员方法),本质还是数据+算法
方法表示类(或结构体)“能做什么事情”

为何需要方法和函数

目的1:隐藏复杂的逻辑;
目的2:复用(reuse、重用)

方法的声明(或称为定义)与调用

  1. using System.Text;
  2. using System.Theading,Tasks;
  3. namespace CSharpMethodExample
  4. {
  5. class Program
  6. {
  7. static void Main(string[] args)
  8. {
  9. Calculator c = new Calculator();
  10. Concle.writeLine(GetCircleVolume(50,90));
  11. }
  12. public static double GetCircleArea(double r)
  13. {
  14. return Math.PI*r*r;
  15. }
  16. public static double GetCircleVolume(double r,double h)
  17. {
  18. return GetCircleArea(r)*h;
  19. }
  20. public static double GetConeVolume(double r,double h)
  21. {
  22. return GetCircleVolume(r,h)/3;
  23. }
  24. }
  25. }

构造器(一种特殊的方法)(又称为构造函数)

构造器(constructor)是类型的成员之一
狭义的构造器指的是“实例构造器”(instance constructor)

如何调用构造器 声明构造器

  1. /默认构造器
  2. namespace ConstructorExample
  3. {
  4. class Program
  5. {
  6. static void Main(string[] args)
  7. {
  8. Student stu = new Student();//创建实例,调用构造器
  9. Console.WriteLine(stu.ID);//输出为0,原因是初始化字段值
  10. }
  11. }
  12. class Student
  13. {
  14. public int ID;
  15. public string Name;
  16. }
  17. }
  1. namespace ConstructorExample
  2. {
  3. class Program
  4. {
  5. static void Main(string[] args)
  6. {
  7. Student stu = new Student();
  8. Console.WriteLine(stu.ID);
  9. Console.WriteLine(stu.Name);
  10. }
  11. }
  12. class Student
  13. {
  14. public Student()
  15. {
  16. this.ID = 1;
  17. this.Name = "No name";
  18. }
  19. /自定义构造器数值,不带参数
  20. public int ID;
  21. public string Name;
  22. }
  23. }
  1. namespace ConstructorExample
  2. {
  3. class Program
  4. {
  5. static void Main(string[] args)
  6. {
  7. Student stu = new Student(2, "Mr.Okay");
  8. Console.WriteLine(stu.ID);
  9. Console.WriteLine(stu.Name);
  10. Student stu = new Student();//此构造器不报错的原因不是为默认构造器,而是自己书写了一个无参数构造器。
  11. }
  12. }
  13. class Student
  14. {
  15. public Student(int initId,string initName)
  16. {
  17. this.ID = init Id;
  18. this.Name = initName;
  19. }
  20. public Student() //自定义无参数构造器
  21. {
  22. this.name=1;
  23. this.Name= "no name";
  24. }
  25. public int ID;
  26. public string Name;
  27. }
  28. }

一旦有了带参数的构造器,默认构造器就不存在了。若还想调用无参数构造器,必需自己写。

构造器的内存原理

默认构造器图示

图中左侧代指栈内存,右侧代指堆内存。
注意栈内存分配是从 高地址 往 低地址 分配,直到分配到栈顶。

  1. public int ID;// int 结构体 占4个字节
  2. public string Name;// string 引用类型 占4个字节 存储的是实例的地址

0809方法的定义、调用与调试 - 图1

带参数构造器图示

  1. Student stu = new Student(1, "Mr.Okay");

0809方法的定义、调用与调试 - 图2

方法的重载(Overload)

调用重载方法示例

  1. namespace OverloadExample
  2. {
  3. class Program
  4. {
  5. static void Main(string[] args)
  6. {
  7. Console.WriteLine("");
  8. }
  9. }
  10. }

:::info 此时书写到Console.WriteLine上时,会出现“1 of 19 void Console.WriteLine()”;

:::

声明带有重载的方法

方法签名(method signature)由方法的名称、类型形参的个数和它的每一个形参(按从左到右的顺序)的类型种类(值、引用或输出)组成。方法签名不包含返回类型

实例构造

  1. public int Add(int a, int b)
  2. {
  3. return a + b;
  4. }
  5. public int Add(int a, int b,int c) //编译成功,修改形参数量
  6. {
  7. return a + b + c
  8. }
  9. public int Add(double a,double b) //编译成功,修改参数类型
  10. {
  11. return a + b;
  12. }
  13. public double Add(int a, int b) //编译失败,修改返回值类型
  14. {
  15. return a + b;
  16. }
  17. public double Add(int e, int q) //编译失败,修改参数名字
  18. {
  19. return a + b;
  20. }
  21. public int Add<T>(int a, int b) //编译失败,类型形参
  22. {
  23. T t;//...
  24. return a + b;
  25. }
  26. public int Add(ref/out int a, int b) //编译成功,参数种类
  27. {
  28. return a + b;
  29. }

函数签名由它的每一个形参(按从左到右的顺序)的类型和种类(值、引用或输出)组成
重载决策(到底调用哪一个重载):用于在给定了参数列表和一组候选函数成员的情况下,选择一个最佳的函数成员来实施调用

如何对方法进行debug

  1. using System;
  2. namespace CSharpMethodExample
  3. {
  4. class Program
  5. {
  6. static void Main(string[] args)
  7. {
  8. double result = Calculator.GetConeVolume(100, 90);
  9. }
  10. }
  11. class Calculator
  12. {
  13. public static double GetCircleArea(double r)
  14. {
  15. return Math.PI * r * r;
  16. }
  17. public static double GetCylinderVolume(double r,double h)
  18. {
  19. double a = GetCircleArea(r);
  20. return a * h;
  21. }
  22. public static double GetConeVolume(double r,double h)
  23. {
  24. double cv = GetCylinderVolume(r, h);
  25. return cv / 3;
  26. }
  27. }
  28. }

设置断点(breakpoint)

断点:程序运行到设置断点位停止运行
操作:点击需要中断位置的,左侧行数空白部分,会出现红色标点。此时断点设置成功。

观察方法调用时的 call stack(调用栈)

通过 call stack 可以直观的追溯方法调用链。
套娃越多,call stack的层数越多。
0809方法的定义、调用与调试 - 图3

Step-in、Step-over、Step-out

step-in:逐步执行程序(F11)
step-over:用于跳出当前方法并返回到调用它的方法(F10)
step-out:跳转到调用本方法的方法。
现在称为:逐句、逐过程、跳出

观察局部变量的值与变化

Local:会出现程序调用逐步、逐过程等中间运行的值的名称、数值、类型等信息。

方法的调用与栈

方法调用时栈内存的分配

对 stack frame 的分析

stack frame

一个方法被调用时,它在栈内存中的布局。

C# 中调用方法时的变量归 Caller(主调函数) 管,不归 Callee(被调用者) 管。
压变量入栈,C# 是从左至右的顺序。

图示是为了重点解释方法、变量、参数的压栈,实际情况下还要压入返回地址等。
返回值一般存在 CPU 的寄存器里面,特殊情况寄存器存不下该返回值时,会到栈上开辟空间。

stack overflow 就是栈无限向上延伸(分配变量、参数、栈针等),最后溢出了。

  1. using System;
  2. namespace CSharpMethodExample
  3. {
  4. class Program
  5. {
  6. static void Main(string[] args)
  7. {
  8. double result = Calculator.GetConeVolume(100, 90);
  9. }
  10. }
  11. class Calculator
  12. {
  13. public static double GetCircleArea(double r)
  14. {
  15. return Math.PI * r * r;
  16. }
  17. public static double GetCylinderVolume(double r,double h)
  18. {
  19. double a = GetCircleArea(r);
  20. return a * h;
  21. }
  22. public static double GetConeVolume(double r,double h)
  23. {
  24. double cv = GetCylinderVolume(r, h);
  25. return cv / 3;
  26. }
  27. }
  28. }

分步讲解

1.进入 Main 方法,调用 GetConeVolume 方法前

在栈上开辟了 Main 方法的 stack frame。
0809方法的定义、调用与调试 - 图4

2.Main 方法中调用 GetConeVolume 时

将两个参数压入栈中。因为 C# 中调用时的参数归 Caller 管,此处即归 Main 管。
0809方法的定义、调用与调试 - 图5

3.进入 GetConeVolume 后

局部变量是需要入栈的,GetConeVolume 方法中的 cv 入栈。
r,h 也是局部变量,但已经作为参数被 Main 方法压入栈了,所以它只需要压 cv 即可。
0809方法的定义、调用与调试 - 图6

4.GetConeVolume 调用 GetCylinderVolume 时

将两个参数压入栈中。
0809方法的定义、调用与调试 - 图7

5.进入 GetCylinderVolume 后

局部变量 a 入栈。
0809方法的定义、调用与调试 - 图8

6.GetCylinderVolume 调用 GetCircleArea 时

GetCircleArea 只有一个参数,将其压入栈即可。
0809方法的定义、调用与调试 - 图9

7.进入 GetCircleArea 后

GetCircleArea 中没有局部变量,但它在栈上也占内存,它有自己的栈针。
0809方法的定义、调用与调试 - 图10

8.GetCircleArea 返回后

返回值存在 CPU 的寄存器(register)里面。
call stack 少了一层。
函数返回后,它所占有的 stack frame 就清空了。
0809方法的定义、调用与调试 - 图11

9.GetCylinderVolume 返回后

0809方法的定义、调用与调试 - 图12

10.GetConeVolume 返回后

GetConeVolume 的 stack frame被清空。
Main 方法中调用 GetConeVolume 时压入栈中的两个参数也出栈了。
0809方法的定义、调用与调试 - 图13

11.Main 返回后(程序结束)

Main 方法的 stack frame 也被清空。
0809方法的定义、调用与调试 - 图14