推荐好书:《CLR via C#》 《C# IN DEPTH》(中文名 《深入理解C#》)

学习完课程后,一定要把这两本书好好读读,完成从初级到高级的转变。

方法的由来

  • 方法(method)的前身是 C/C++ 语言的函数(function)
    • 方法是面向对象范畴的概念,在非面向对象语言中仍然称为函数
    • 使用 C/C++ 语言做对比
  • 方法永远都是类(或结构体)的成员
    • C# 语言中函数不可能独立于类(或结构体)之外
    • 只有作为类(或结构体)的成员时才被称为方法
    • C++ 中函数可以独立于类之外,称为“全局函数”
  • 方法是类(或结构体)最基本的成员之一
    • 最基本的成员只有两个 —— 字段与方法(成员变量与成员方法),本质还是数据 + 算法
    • 方法表示类(或结构体)“能做什么事情”
  • 为什么需要方法和函数
    • 目的 1:隐藏复杂的逻辑
    • 目的 2:复用(reuse,重用)
    • 示例:计算圆面积、圆柱体积、圆锥体积

VS2017 装上 SDK,就可以直接写 C 和 C++。

C++ 函数示例:

  1. #include <iostream>
  2. double Add(double a, double b)
  3. {
  4. return a + b;
  5. }
  6. int main()
  7. {
  8. double x = 3.0;
  9. double y = 5.0;
  10. double result = Add(x, y);
  11. std::cout << x << "+" << y << "=" << result;
  12. return 0;
  13. }

当一个函数以类的成员的身份出现时,它就被称为方法。方法有一个别名叫“成员函数”。

C++ 里面完成了函数向方法的过渡。

C++ 添加类

008,009 方法的定义、调用与调试 - 图1

Student.h:

  1. #pragma once
  2. class Student
  3. {
  4. public:
  5. Student();
  6. ~Student();
  7. void SayHello();
  8. };

Student.cpp:

  1. #include "Student.h"
  2. #include <iostream>
  3. Student::Student()
  4. {
  5. }
  6. Student::~Student()
  7. {
  8. }
  9. void Student::SayHello()
  10. {
  11. std::cout << "Hello! I'm a student!";
  12. }

Source.cpp:

  1. #include <iostream>
  2. #include "Student.h"
  3. int main()
  4. {
  5. Student *pStu = new Student();
  6. pStu->SayHello();
  7. return 0;
  8. }

方法永远都是类(或结构体)的成员

C# namespace 里不能有方法。
008,009 方法的定义、调用与调试 - 图2

方法的声明与调用

008,009 方法的定义、调用与调试 - 图3

  • C# 声明/定义不分家
  • C++ 声明放在头文件,定义放在代码文件里面

C# 语言定义文档 —— 方法声明

008,009 方法的定义、调用与调试 - 图4

构造器(一种特殊的方法)

  • 构造器(constructor)是类型的成员之一
  • 狭义的构造器指的是“实例构造器”(instance constructor)
  • 如何调用构造器
  • 声明构造器
  • 构造器的内存原理

构造函数译为构造器,成员函数译为方法,它们本质都还是函数。

默认构造器

  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. }
  10. }
  11. class Student
  12. {
  13. public int ID;
  14. public string Name;
  15. }
  16. }

默认构造器将int型的 ID 初始化为 0:

  1. 0
  2. 请按任意键继续. . .

无参数默认构造器

  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. ID = 1;
  17. Name = "No name";
  18. }
  19. public int ID;
  20. public string Name;
  21. }
  22. }

输出:

  1. 1
  2. No name
  3. 请按任意键继续. . .

带参数构造器

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

  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. }
  11. }
  12. class Student
  13. {
  14. public Student(int initId,string initName)
  15. {
  16. ID = init Id;
  17. Name = initName;
  18. }
  19. public int ID;
  20. public string Name;
  21. }
  22. }

输出:

  1. 2
  2. Mr.Okay
  3. 请按任意键继续. . .

Code Snippet
ctor + TAB * 2:快速生成构造器代码片段 008,009 方法的定义、调用与调试 - 图5

构造器内存原理

默认构造器图示

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

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

008,009 方法的定义、调用与调试 - 图6

带参数构造器图示

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

008,009 方法的定义、调用与调试 - 图7

方法的重载(Overload)

调用重载方法的示例

Console.WriteLine() 有多达 19 个重载。
008,009 方法的定义、调用与调试 - 图8

声明带有重载的方法

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

“类型形参”是用在泛型里面的。

  1. public int Add(int a, int b)
  2. {
  3. return a + b;
  4. }
  5. public int Add<T>(int a, int b)
  6. {
  7. T t;//...
  8. return a + b;
  9. }

参数种类不同示例:

  1. public int Add(int a, int b)
  2. {
  3. return a + b;
  4. }
  5. public int Add(ref int a, int b)
  6. {
  7. return a + b;
  8. }
  9. public int Add(int a,out int b)
  10. {
  11. b = 100;
  12. return a + b;
  13. }

如何对方法进行 debug

  • 设置断点(breakpoint)
  • 观察方法调用时的 call stack
  • Step-in、Step-over、Step-out
  • 观察局部变量的值与变化

call stack

通过 call stack 可以直观的追溯方法调用链。
008,009 方法的定义、调用与调试 - 图9

Step-out

Step-out(Shift + F11)用于跳出当前方法并返回到调用它的方法。

这在跨文件方法调用、大项目里面很有用,你想知道到底谁调用了这个方法,直接一个 Shift + F11 就跳过去了。

钉变量

008,009 方法的定义、调用与调试 - 图10

方法的调用与栈

  • 方法调用时栈内存的分配
    • 对 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。
008,009 方法的定义、调用与调试 - 图11

2.Main 方法中调用 GetConeVolume 时

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

3.进入 GetConeVolume 后

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

4.GetConeVolume 调用 GetCylinderVolume 时

将两个参数压入栈中。
008,009 方法的定义、调用与调试 - 图14

5.进入 GetCylinderVolume 后

局部变量 a 入栈。
008,009 方法的定义、调用与调试 - 图15

6.GetCylinderVolume 调用 GetCircleArea 时

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

7.进入 GetCircleArea 后

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

8.GetCircleArea 返回后

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

9.GetCylinderVolume 返回后

008,009 方法的定义、调用与调试 - 图19

10.GetConeVolume 返回后

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

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

Main 方法的 stack frame 也被清空。
008,009 方法的定义、调用与调试 - 图21