1类是什么
数据和操作数据的方法,怎么装进一个能造出很多对象的模板?用 class 关键字写一个类。
定义类就像画一张蓝图:写清楚照它造出来的东西有哪些数据、能做哪些操作,但蓝图本身不装数据。照蓝图造出来的才是对象,也叫这个类的实例;一张蓝图能造很多对象,每个对象有自己的一份数据。写在类里面的变量和方法,统称类的成员。
2类的定义
一般形式:开头写 class 和类名,主体放在一对大括号里。里面先写成员变量(访问修饰符、数据类型、变量名),再写成员方法(访问修饰符、返回类型、方法名和参数列表,后面是方法体)。返回类型就是方法算完交回的值是什么类型。
访问修饰符(教程叫访问标识符)决定谁能访问这个成员。不写就用默认值:类本身默认 internal(同一个项目里都能用),成员默认 private(只有类自己能用)。访问对象的成员用点号,把对象名和成员名连起来。
3第一个例子:Box
| 4 | class Box | // 定义 Box 类 |
| 5 | { | // Box 类开始 |
| 6 | public double length; // 长度 | // public:外面能用点号访问 |
| 7 | public double breadth; // 宽度 | // 成员变量,类型 double |
| 8 | public double height; // 高度 | // 成员变量,类型 double |
| 9 | } | // Box 类结束 |
new 造出两个对象,变量里放的是对象的引用,就像一张取货牌:
| 14 | Box Box1 = new Box(); // 声明 Box1,类型为 Box | // new 造出第一个对象,Box1 拿到它的引用 |
| 15 | Box Box2 = new Box(); // 声明 Box2,类型为 Box | // new 造出第二个对象 |
| 16 | double volume = 0.0; // 体积 | // 存放体积的变量 |
再用点号给三条边赋值:Box1 的高、长、宽是 5、6、7,Box2 是 10、12、13,两个对象各有一份,互不影响。最后乘出体积:
| 28 | // Box1 的体积 | |
| 29 | volume = Box1.height * Box1.length * Box1.breadth; | // 5 × 6 × 7 = 210 |
查看完整代码(38 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | namespace BoxApplication | // 定义命名空间 BoxApplication |
| 3 | { | // 命名空间开始 |
| 4 | class Box | // 定义 Box 类 |
| 5 | { | // Box 类开始 |
| 6 | public double length; // 长度 | // 成员变量:长 |
| 7 | public double breadth; // 宽度 | // 成员变量:宽 |
| 8 | public double height; // 高度 | // 成员变量:高 |
| 9 | } | // Box 类结束 |
| 10 | class Boxtester | // 定义 Boxtester 类,放 Main |
| 11 | { | // Boxtester 类开始 |
| 12 | static void Main(string[] args) | // 程序入口 Main 方法 |
| 13 | { | // Main 开始 |
| 14 | Box Box1 = new Box(); // 声明 Box1,类型为 Box | // 造出第一个对象 |
| 15 | Box Box2 = new Box(); // 声明 Box2,类型为 Box | // 造出第二个对象 |
| 16 | double volume = 0.0; // 体积 | // 存放体积 |
| 17 | | |
| 18 | // Box1 详述 | |
| 19 | Box1.height = 5.0; | // Box1 的高 |
| 20 | Box1.length = 6.0; | // Box1 的长 |
| 21 | Box1.breadth = 7.0; | // Box1 的宽 |
| 22 | | |
| 23 | // Box2 详述 | |
| 24 | Box2.height = 10.0; | // Box2 的高 |
| 25 | Box2.length = 12.0; | // Box2 的长 |
| 26 | Box2.breadth = 13.0; | // Box2 的宽 |
| 27 | | |
| 28 | // Box1 的体积 | |
| 29 | volume = Box1.height * Box1.length * Box1.breadth; | // 5 × 6 × 7 = 210 |
| 30 | Console.WriteLine("Box1 的体积: {0}", volume); | // 输出 210 |
| 31 | | |
| 32 | // Box2 的体积 | |
| 33 | volume = Box2.height * Box2.length * Box2.breadth; | // 10 × 12 × 13 = 1560 |
| 34 | Console.WriteLine("Box2 的体积: {0}", volume); | // 输出 1560 |
| 35 | Console.ReadKey(); | // 等待按键(现在的 VS 不写也会停住) |
| 36 | } | // Main 结束 |
| 37 | } | // Boxtester 类结束 |
| 38 | } | // 命名空间结束 |
Box1 的体积: 210 Box2 的体积: 1560
把 length 前面的 public 去掉,成员默认是 private,外面一用点号访问就编译报错:
| 6 | double length; // 没写访问修饰符 | // 默认是 private |
Program.cs(13,18): error CS0122: “Box.length”不可访问,因为它具有一定的保护级别
4成员函数和封装
成员函数就是写在类里的方法,能访问这个对象的所有成员。C# 里方法写在一处就完整了,不用像 C++ 那样另写“原型”。
第二个例子把三条边改成 private,外面不能直接碰,只能调用 public 的方法,这就是封装:
| 6 | private double length; // 长度 | // private:只有 Box 自己的方法能用 |
| 7 | private double breadth; // 宽度 | // 外面摸不到 |
| 8 | private double height; // 高度 | // 外面摸不到 |
| 9 | public void setLength( double len ) | // public 方法:设置长度 |
| 10 | { | // setLength 开始 |
| 11 | length = len; | // 在类内部可以直接改 length |
| 12 | } | // setLength 结束 |
| 23 | public double getVolume() | // 返回类型 double:算完交回体积 |
| 24 | { | // getVolume 开始 |
| 25 | return length * breadth * height; | // 在类内部把三条边乘起来 |
| 26 | } | // getVolume 结束 |
| 37 | Box1.setLength(6.0); | // 调用方法设长 = 6 |
| 38 | Box1.setBreadth(7.0); | // 设宽 = 7 |
| 39 | Box1.setHeight(5.0); | // 设高 = 5,getVolume() 得 210 |
查看完整代码(57 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | namespace BoxApplication | // 定义命名空间 BoxApplication |
| 3 | { | // 命名空间开始 |
| 4 | class Box | // 定义 Box 类 |
| 5 | { | // Box 类开始 |
| 6 | private double length; // 长度 | // 私有成员变量:长 |
| 7 | private double breadth; // 宽度 | // 私有成员变量:宽 |
| 8 | private double height; // 高度 | // 私有成员变量:高 |
| 9 | public void setLength( double len ) | // 设置长度 |
| 10 | { | // setLength 开始 |
| 11 | length = len; | // 存进 length |
| 12 | } | // setLength 结束 |
| 13 | | |
| 14 | public void setBreadth( double bre ) | // 设置宽度 |
| 15 | { | // setBreadth 开始 |
| 16 | breadth = bre; | // 存进 breadth |
| 17 | } | // setBreadth 结束 |
| 18 | | |
| 19 | public void setHeight( double hei ) | // 设置高度 |
| 20 | { | // setHeight 开始 |
| 21 | height = hei; | // 存进 height |
| 22 | } | // setHeight 结束 |
| 23 | public double getVolume() | // 计算体积 |
| 24 | { | // getVolume 开始 |
| 25 | return length * breadth * height; | // 返回长 × 宽 × 高 |
| 26 | } | // getVolume 结束 |
| 27 | } | // Box 类结束 |
| 28 | class Boxtester | // 定义 Boxtester 类 |
| 29 | { | // Boxtester 类开始 |
| 30 | static void Main(string[] args) | // 程序入口 Main 方法 |
| 31 | { | // Main 开始 |
| 32 | Box Box1 = new Box(); // 声明 Box1,类型为 Box | // 造出第一个对象 |
| 33 | Box Box2 = new Box(); // 声明 Box2,类型为 Box | // 造出第二个对象 |
| 34 | double volume; // 体积 | // 存放体积 |
| 35 | | |
| 36 | // Box1 详述 | |
| 37 | Box1.setLength(6.0); | // Box1 长 = 6 |
| 38 | Box1.setBreadth(7.0); | // Box1 宽 = 7 |
| 39 | Box1.setHeight(5.0); | // Box1 高 = 5 |
| 40 | | |
| 41 | // Box2 详述 | |
| 42 | Box2.setLength(12.0); | // Box2 长 = 12 |
| 43 | Box2.setBreadth(13.0); | // Box2 宽 = 13 |
| 44 | Box2.setHeight(10.0); | // Box2 高 = 10 |
| 45 | | |
| 46 | // Box1 的体积 | |
| 47 | volume = Box1.getVolume(); | // 6 × 7 × 5 = 210 |
| 48 | Console.WriteLine("Box1 的体积: {0}" ,volume); | // 输出 210 |
| 49 | | |
| 50 | // Box2 的体积 | |
| 51 | volume = Box2.getVolume(); | // 12 × 13 × 10 = 1560 |
| 52 | Console.WriteLine("Box2 的体积: {0}", volume); | // 输出 1560 |
| 53 | | |
| 54 | Console.ReadKey(); | // 等待按键(现在的 VS 不写也会停住) |
| 55 | } | // Main 结束 |
| 56 | } | // Boxtester 类结束 |
| 57 | } | // 命名空间结束 |
输出和第一个例子一样,还是 210 和 1560。成员变量可以看成对象的特征;C# 另有专门的“属性”语法(后面会讲)。
5构造函数
构造函数是类里一个特殊的方法,每次用 new 创建对象都会自动执行。它的名字和类名完全一样,不写返回类型,连 void 也不写。
| 7 | public Line() | // 构造函数:名字就是类名 Line,没有返回类型 |
| 8 | { | // Line 构造函数开始 |
| 9 | Console.WriteLine("对象已创建"); | // 只打印一句 |
| 10 | } | // Line 构造函数结束 |
| 23 | Line line = new Line(); | // new 的那一刻构造函数就跑了:先打印“对象已创建” |
| 24 | // 设置线条长度 | |
| 25 | line.setLength(6.0); | // 长度设成 6 |
| 26 | Console.WriteLine("线条的长度: {0}", line.getLength()); | // 再打印“线条的长度: 6” |
上面没有参数,叫默认的构造函数。构造函数也可以带参数,叫参数化构造函数,创建对象的同时就给初始值:
| 7 | public Line(double len) // 参数化构造函数 | // 带一个参数 len |
| 8 | { | // Line 构造函数开始 |
| 9 | Console.WriteLine("对象已创建,length = {0}", len); | // 先打印 |
| 10 | length = len; | // 再把 len 存进 length |
| 11 | } | // Line 构造函数结束 |
| 24 | Line line = new Line(10.0); | // 传进 10:打印“对象已创建,length = 10” |
| 25 | Console.WriteLine("线条的长度: {0}", line.getLength()); | // 长度 10 |
| 26 | // 设置线条长度 | |
| 27 | line.setLength(6.0); | // 改成 6 |
| 28 | Console.WriteLine("线条的长度: {0}", line.getLength()); | // 长度 6 |
对象已创建,length = 10 线条的长度: 10 线条的长度: 6
注意:自己写了带参数的构造函数,编译器就不再补那个无参的,这时写 new Line() 会报错:
| 14 | Line line = new Line(); | // 只有 Line(double len),却不传参数 |
Program.cs(14,26): error CS7036: 未提供与“Line.Line(double)”的所需参数“len”对应的参数
6析构函数
析构函数的名字是类名前加波浪号 ~,没有返回值,也不带参数。在 Line 里加上它,看“对象已删除”会不会打印:
| 11 | ~Line() //析构函数 | // ~ 加类名:析构函数 |
| 12 | { | // ~Line 开始 |
| 13 | Console.WriteLine("对象已删除"); | // 析构时打印 |
| 14 | } | // ~Line 结束 |
对象已创建 线条的长度: 6
在现在的 .NET 上真跑只有两行(教程的说法已过时)。析构函数什么时候跑由垃圾回收器(.NET 里自动清理没人用的对象的部分)决定,不是变量一离开范围就跑;而且 .NET 5 以后程序退出时也不调用它。先让对象没人再用,再请垃圾回收器来一趟,才打印出来:
| 16 | [MethodImpl(MethodImplOptions.NoInlining)] | // 让 Make 保持为独立的方法,不被合并进 Main |
| 17 | static void Make() | // Make:只在里面建一个对象 |
| 18 | { | // Make 开始 |
| 19 | Line line = new Line(); | // 打印“对象已创建” |
| 20 | } | // Make 结束 |
| 24 | Make(); // 离开 Make,line 不再被用到 | // 对象没人用了 |
| 25 | GC.Collect(); // 请垃圾回收器来一趟 | // 发现对象没人用,把它的析构函数排进待执行队列 |
| 26 | GC.WaitForPendingFinalizers(); // 等析构函数跑完 | // 等队列里的析构函数执行完:打印“对象已删除” |
| 27 | Console.WriteLine("程序结束"); | // 最后打印“程序结束” |
对象已创建 对象已删除 程序结束
官方文档现在把它叫作终结器。一个类只能有一个,不能继承,也不能重载。内存由垃圾回收器管;直接管理文件这类外部资源时,主要用 Dispose(一个专门用来释放资源的方法)主动释放(用完马上关掉);析构函数只是忘了调用 Dispose 时的兜底。
7静态成员
用 static 声明的成员,不管类造了多少个对象,都只有一份。
| 6 | public static int num; | // 静态变量 num:整个类共用一份,默认 0 |
| 7 | public void count() | // 实例方法 count:每调用一次给共用的 num 加 1 |
| 8 | { | // count 开始 |
| 9 | num++; | // num 加 1 |
| 10 | } | // count 结束 |
| 20 | StaticVar s1 = new StaticVar(); | // 对象 s1 |
| 21 | StaticVar s2 = new StaticVar(); | // 对象 s2 |
| 22 | s1.count(); | // num = 1 |
| 23 | s1.count(); | // num = 2 |
| 24 | s1.count(); | // num = 3 |
| 25 | s2.count(); | // 加在同一个 num 上:num = 4 |
| 26 | s2.count(); | // num = 5 |
| 27 | s2.count(); | // num = 6 |
| 28 | Console.WriteLine("s1 的变量 num: {0}", s1.getNum()); | // 输出 6 |
| 29 | Console.WriteLine("s2 的变量 num: {0}", s2.getNum()); | // 也是 6 |
静态变量的值随时能改,就像这里的 num,它不是常量,真要常量用 const。C# 一般在声明静态变量的地方直接写初始值,不像 C++ 那样在类外部初始化。
方法也可以声明成 static,叫静态函数,它在对象创建之前就存在,用类名直接调用,不需要对象:
| 11 | public static int getNum() | // 静态函数 getNum |
| 12 | { | // getNum 开始 |
| 13 | return num; | // 返回静态变量 num |
| 14 | } | // getNum 结束 |
对象 s 调用三次 count 以后,用类名调用:
| 24 | Console.WriteLine("变量 num: {0}", StaticVar.getNum()); | // 类名.方法名 调用,不需要对象:输出 3 |
静态函数没有自己的对象,不能直接用实例成员(每个对象各有一份的成员):
| 7 | public int id; | // 实例字段 id:每个对象各有一份 |
| 8 | public static int getNum() | // 静态函数 |
| 9 | { | // getNum 开始 |
| 10 | return id; | // 没有对象,不知道是哪个对象的 id |
Program.cs(10,20): error CS0120: 对象引用对于非静态的字段、方法或属性“StaticVar.id”是必需的
8小结
- 类是一张蓝图,用
new造出对象,用点号访问成员;成员默认private。 - 构造函数在创建时执行;写了带参构造函数,编译器就不再自动补无参的。
- 析构函数(终结器)什么时候跑由垃圾回收器决定。
- 静态成员整个类只有一份,静态函数用类名调用。
下一节:已经有了一个类型,想在它的基础上加点功能,不用从头再写,该怎么办?
