第 18 节 · 第二部分 流程、方法与数据

结构体(Struct)

结构体把几样相关数据打包成一个值;赋值、传参都复制整份,类复制的只是引用。

时长 7:21授课计划第 7 周

学完这一节,你能

讲义

跟着视频的顺序写。每行代码都有注释,代码和运行结果都是实际运行得到的。

1结构体是什么

名字、年龄、成绩怎么打包成一个值?用结构体,关键字 struct。它是一种值类型,能把几样相关的数据装进同一个变量。

视频里拿图书举例:书名、作者、主题是文字,书号是整数,类型各不相同。做成结构体就像一只分了四格的资料匣,整只匣子是一个值,里面的四个格子叫它的字段。

2定义结构体

C# · 第 4-10 行
4struct Books// struct 关键字 + 结构体名 Books
5{// Books 结构体开始
6 public string title;// 字段:书名;public 表示外面的代码可以直接访问
7 public string author;// 字段:作者
8 public string subject;// 字段:主题
9 public int book_id;// 字段:书号,是整数
10};// Books 结构体结束

这样就有了一个新的数据类型,可以像 int 一样拿来声明变量。

3第一个例子:用点号填字段

Book1、Book2 的类型都是 Books,这里没有用 new,结构体可以这样直接声明,再用点号一格一格地填:

C# · 第 17-24 行
17Books Book1; /* 声明 Book1,类型为 Books */// 不用 new,直接声明结构体变量
18Books Book2; /* 声明 Book2,类型为 Books */// 两只匣子各装各的
19
20/* book 1 详述 */// 下面填 Book1 的四个字段
21Book1.title = "C Programming";// 变量名.字段名:填书名
22Book1.author = "Nuha Ali";// 填作者
23Book1.subject = "C Programming Tutorial";// 填主题
24Book1.book_id = 6495407;// 填书号
查看完整代码(47 行)
C#
1using System;// 引入 System 命名空间
2using System.Text;// 引入 System.Text(本例没用到)
3
4struct Books// 定义结构体 Books
5{// Books 结构体开始
6 public string title;// 字段:书名
7 public string author;// 字段:作者
8 public string subject;// 字段:主题
9 public int book_id;// 字段:书号
10};// Books 结构体结束
11
12public class testStructure// 定义 testStructure 类
13{// testStructure 类开始
14 public static void Main(string[] args)// 程序入口 Main 方法
15 {// Main 开始
16
17 Books Book1; /* 声明 Book1,类型为 Books */// 声明结构体变量 Book1
18 Books Book2; /* 声明 Book2,类型为 Books */// 声明结构体变量 Book2
19
20 /* book 1 详述 */// 下面填 Book1
21 Book1.title = "C Programming";// Book1 的书名
22 Book1.author = "Nuha Ali";// Book1 的作者
23 Book1.subject = "C Programming Tutorial";// Book1 的主题
24 Book1.book_id = 6495407;// Book1 的书号
25
26 /* book 2 详述 */// 下面填 Book2
27 Book2.title = "Telecom Billing";// Book2 的书名
28 Book2.author = "Zara Ali";// Book2 的作者
29 Book2.subject = "Telecom Billing Tutorial";// Book2 的主题
30 Book2.book_id = 6495700;// Book2 的书号
31
32 /* 打印 Book1 信息 */// 下面打印 Book1
33 Console.WriteLine( "Book 1 title : {0}", Book1.title);// {0} 处换成 Book1.title
34 Console.WriteLine("Book 1 author : {0}", Book1.author);// 打印作者
35 Console.WriteLine("Book 1 subject : {0}", Book1.subject);// 打印主题
36 Console.WriteLine("Book 1 book_id :{0}", Book1.book_id);// 冒号后面没写空格,输出也紧挨着
37
38 /* 打印 Book2 信息 */// 下面打印 Book2
39 Console.WriteLine("Book 2 title : {0}", Book2.title);// 打印书名
40 Console.WriteLine("Book 2 author : {0}", Book2.author);// 打印作者
41 Console.WriteLine("Book 2 subject : {0}", Book2.subject);// 打印主题
42 Console.WriteLine("Book 2 book_id : {0}", Book2.book_id);// 这行冒号后面有空格
43
44 Console.ReadKey();// 等待按键(现在的 VS 不写也会停住)
45
46 }// Main 结束
47}// testStructure 类结束
运行结果
Book 1 title : C Programming
Book 1 author : Nuha Ali
Book 1 subject : C Programming Tutorial
Book 1 book_id :6495407
Book 2 title : Telecom Billing
Book 2 author : Zara Ali
Book 2 subject : Telecom Billing Tutorial
Book 2 book_id : 6495700

第四行冒号后面紧挨着数字,因为这行的格式字符串里冒号后面本来就没写空格。

4结构体的特点

结构体里除了字段,还能放方法、属性、索引器、运算符方法和事件(后面几种后面会讲),适合表示坐标、范围、日期、时间这类小而简单的数据。

构造函数

结构体可以写构造函数,创建时给字段赋初值。从 C# 10 开始,结构体也能自己写无参数的构造函数(旧版本不行):

C# · 第 9-13 行
9public MyStruct()// 无参构造函数:名字和结构体相同,没有返回类型
10{// MyStruct 构造函数开始
11 X = 10;// 给 X 赋初值 10
12 Y = 20;// 给 Y 赋初值 20
13}// MyStruct 构造函数结束
C# · 第 20-21 行
20MyStruct a = new MyStruct();// new 时调用上面写的无参构造函数
21Console.WriteLine($"{a.X}, {a.Y}");// $"…" 里的 {a.X} 换成变量值:输出 10, 20

不能写的东西

结构体不能继承别的结构体:

C# · 第 9 行
9struct MyDerivedStruct : MyStruct// 想让 MyDerivedStruct 继承 MyStruct
编译报错
Program.cs(9,26): error CS0527: 接口列表中的类型“MyStruct”不是接口

反过来,它也不能给别的结构体或类当基类,但可以实现一个或多个接口(后面的章节讲)。成员也不能标 protected、virtual,析构函数也不行:

C# · 第 5-7 行
5protected int X;// 结构体成员标 protected
6public virtual void Show() { }// 结构体成员标 virtual
7~MyStruct() { }// 析构函数:~ 加结构体名
编译报错
Program.cs(5,19): error CS0666: “MyStruct.X”: 结构中已声明新的保护成员
Program.cs(7,6): error CS0575: 只有类类型才能包含析构函数
Program.cs(6,25): error CS0106: 修饰符“virtual”对该项无效

不用 new:字段先赋值才能读

用 new 创建时会调用对应的构造函数;不用 new 也行,但所有字段都赋好值以后,结构体才能拿来用:

C# · 第 13-15 行
13MyStruct a;// 不用 new 声明
14a.X = 1;// 只给 X 赋了值
15Console.WriteLine(a.Y);// Y 还没赋值就去读
编译报错
Program.cs(15,27): error CS0170: 使用了可能未赋值的字段“Y”

放在哪里

结构体变量通常放在栈上(方法运行时临时放变量的地方),创建和销毁都很快;但它要是某个类的字段,就跟着那个对象一起放在堆上(存放 new 出来的对象的地方)。放在哪里由运行时决定,结构体不一定在栈上。

只读结构体

结构体默认可变。在 struct 前加 readonly 就成了只读结构体,字段也要标 readonly,之后再改就不让了:

C# · 第 3-6 行
3readonly struct Point// readonly struct:只读结构体
4{// Point 结构体开始
5 public readonly int X;// 字段也要标 readonly
6 public readonly int Y;// 只能在构造函数里赋值
C# · 第 20 行
20p.X = 5;// 创建以后再改字段
编译报错
Program.cs(20,9): error CS0191: 无法分配到只读字段(除非在定义了该字段的类型的构造函数或 init-only 资源库中,或者在变量初始值设定项中)

5类和结构体

类适合表示复杂的对象和行为,支持继承和多态;结构体适合表示轻量的数据。最根本的区别是:结构体是值类型,类是引用类型。

赋值

C# · 第 56-61 行
56MyStruct structInstance1 = new MyStruct(1, 2);// new 结构体:X=1,Y=2
57MyStruct structInstance2 = structInstance1; // 复制整个结构// 整只匣子复制一份,两边各有一只
58
59// 类是引用类型,分配在堆上
60MyClass classInstance1 = new MyClass(3, 4);// new 类的对象:X=3,Y=4
61MyClass classInstance2 = classInstance1; // 复制引用,指向同一个对象// 只复制一张“取货牌”,两张牌指向同一个对象
C# · 第 64-69 行
64structInstance1.X = 5;// 只改第一只匣子
65Console.WriteLine($"Struct: {structInstance1.X}, {structInstance2.X}");// 输出 5 和 1
66
67// 修改类实例会影响其他实例
68classInstance1.X = 6;// 通过第一张牌改对象
69Console.WriteLine($"Class: {classInstance1.X}, {classInstance2.X}");// 两张牌看的是同一个对象:输出 6 和 6
查看完整代码(71 行)
C#
1using System;// 引入 System 命名空间
2
3// 结构声明
4struct MyStruct// 定义结构体 MyStruct
5{// MyStruct 结构体开始
6 public int X;// 字段 X
7 public int Y;// 字段 Y
8
9 // 结构不能有无参数的构造函数
10 // public MyStruct()
11 // {
12 // }
13
14 // 有参数的构造函数
15 public MyStruct(int x, int y)// MyStruct 的有参构造函数
16 {// MyStruct 构造函数开始
17 X = x;// 参数 x 存进字段 X
18 Y = y;// 参数 y 存进字段 Y
19 }// MyStruct 构造函数结束
20
21 // 结构不能继承
22 // struct MyDerivedStruct : MyBaseStruct
23 // {
24 // }
25}// MyStruct 结构体结束
26
27// 类声明
28class MyClass// 定义类 MyClass
29{// MyClass 类开始
30 public int X;// 字段 X
31 public int Y;// 字段 Y
32
33 // 类可以有无参数的构造函数
34 public MyClass()// 空的无参构造函数(一个都不写时系统也会自动补一个)
35 {// MyClass 无参构造函数开始
36 }// MyClass 无参构造函数结束
37
38 // 有参数的构造函数
39 public MyClass(int x, int y)// MyClass 的有参构造函数
40 {// MyClass 有参构造函数开始
41 X = x;// 参数 x 存进字段 X
42 Y = y;// 参数 y 存进字段 Y
43 }// MyClass 有参构造函数结束
44
45 // 类支持继承
46 // class MyDerivedClass : MyBaseClass
47 // {
48 // }
49}// MyClass 类结束
50
51class Program// 定义 Program 类
52{// Program 类开始
53 static void Main()// 程序入口 Main 方法
54 {// Main 开始
55 // 结构是值类型,分配在栈上
56 MyStruct structInstance1 = new MyStruct(1, 2);// 结构体:X=1,Y=2
57 MyStruct structInstance2 = structInstance1; // 复制整个结构// 两边各有一只匣子
58
59 // 类是引用类型,分配在堆上
60 MyClass classInstance1 = new MyClass(3, 4);// 类的对象:X=3,Y=4
61 MyClass classInstance2 = classInstance1; // 复制引用,指向同一个对象// 两张牌指向同一个对象
62
63 // 修改结构实例不影响其他实例
64 structInstance1.X = 5;// 只改第一只匣子
65 Console.WriteLine($"Struct: {structInstance1.X}, {structInstance2.X}");// 输出 Struct: 5, 1
66
67 // 修改类实例会影响其他实例
68 classInstance1.X = 6;// 改的是两张牌共同指向的对象
69 Console.WriteLine($"Class: {classInstance1.X}, {classInstance2.X}");// 输出 Class: 6, 6
70 }// Main 结束
71}// Program 类结束

真实内存里没有匣子和牌,“牌”其实是一个内存地址,也就是引用。

传给方法

C# · 第 15-19 行
15static void Change(MyStruct s, MyClass c)// Change 收一个结构体、一个类的对象
16{// Change 开始
17 s.X = 99;// 改的是结构体的副本
18 c.X = 99;// 牌的复制品也指向原来那个对象
19}// Change 结束
C# · 第 23-27 行
23MyStruct s = new MyStruct();// 结构体 s,X 默认是 0
24MyClass c = new MyClass();// 类的对象 c,X 默认是 0
25Change(s, c);// 传进去:s 复制整份,c 复制一张牌
26Console.WriteLine($"Struct: {s.X}");// 原来的 s 没变:输出 0
27Console.WriteLine($"Class: {c.X}");// 对象被改了:输出 99

类的对象传进方法时,传进去的是牌的复制品(引用的副本),只不过两张牌指向同一个对象。

能不能为空

类的变量可以是 null(空值),表示还没指向对象;结构体直接赋 null 会报错:

C# · 第 13 行
13MyStruct a = null;// 给结构体变量赋 null
编译报错
Program.cs(13,22): error CS0037: 无法将 null 转换为“MyStruct”,因为后者是不可为 null 的值类型

真要表示“没有值”,就在类型后面加问号(第 15 章的可空类型):

C# · 第 13-14 行
13MyStruct? a = null;// MyStruct? 是可以为空的结构体
14Console.WriteLine(a.HasValue);// HasValue 表示有没有值:输出 False

区别一览

  • 继承:结构体不能;类可以,还支持多态。
  • 无参构造函数:类一直可以写;结构体从 C# 10 起也可以。
  • 赋值和传参:结构体复制整份,类复制引用。
  • 可以为空:类的变量可以;结构体要加问号。
  • 性能:结构体通常更轻,类的对象要多一些内存管理的开销。

6重写图书例子

四个字段改成 private,外面的代码不能直接碰;再加 setValues 一次填好四个字段,display 负责打印四行(见完整代码):

C# · 第 10-16 行
10public void setValues(string t, string a, string s, int id)// 方法 setValues:一次收四个值
11{// setValues 开始
12 title = t;// 填书名
13 author = a;// 填作者
14 subject = s;// 填主题
15 book_id =id;// 填书号
16}// setValues 结束
查看完整代码(52 行)
C#
1using System;// 引入 System 命名空间
2using System.Text;// 引入 System.Text(本例没用到)
3
4struct Books// 定义结构体 Books
5{// Books 结构体开始
6 private string title;// private:只有结构体自己的方法能访问
7 private string author;// 字段:作者
8 private string subject;// 字段:主题
9 private int book_id;// 字段:书号
10 public void setValues(string t, string a, string s, int id)// 方法 setValues:一次填好四个字段
11 {// setValues 开始
12 title = t;// 填书名
13 author = a;// 填作者
14 subject = s;// 填主题
15 book_id =id;// 填书号
16 }// setValues 结束
17 public void display()// 方法 display:打印四个字段
18 {// display 开始
19 Console.WriteLine("Title : {0}", title);// 打印书名
20 Console.WriteLine("Author : {0}", author);// 打印作者
21 Console.WriteLine("Subject : {0}", subject);// 打印主题
22 Console.WriteLine("Book_id :{0}", book_id);// 打印书号
23 }// display 结束
24
25};// Books 结构体结束
26
27public class testStructure// 定义 testStructure 类
28{// testStructure 类开始
29 public static void Main(string[] args)// 程序入口 Main 方法
30 {// Main 开始
31
32 Books Book1 = new Books(); /* 声明 Book1,类型为 Books */// 用 new 创建第一本书
33 Books Book2 = new Books(); /* 声明 Book2,类型为 Books */// 用 new 创建第二本书
34
35 /* book 1 详述 */// 下面填 Book1
36 Book1.setValues("C Programming",// 调用 setValues,参数分两行写
37 "Nuha Ali", "C Programming Tutorial",6495407);// 作者、主题、书号
38
39 /* book 2 详述 */// 下面填 Book2
40 Book2.setValues("Telecom Billing",// 调用 setValues
41 "Zara Ali", "Telecom Billing Tutorial", 6495700);// 作者、主题、书号
42
43 /* 打印 Book1 信息 */// 下面打印 Book1
44 Book1.display();// 打印第一本书的四行
45
46 /* 打印 Book2 信息 */// 下面打印 Book2
47 Book2.display();// 打印第二本书的四行
48
49 Console.ReadKey();// 等待按键(现在的 VS 不写也会停住)
50
51 }// Main 结束
52}// testStructure 类结束

输出还是那八行,只是前面少了 Book 1、Book 2 的编号,比如第一行是 Title : C Programming。

7小结

  • 结构体用 struct 定义,把几样数据打包成一个值,用点号访问字段。
  • 赋值、传参都复制整份;类复制的是引用。
  • 结构体不能继承、不能直接赋 null;C# 10 起可以写无参构造函数。

下一节:一周七天只有几个固定取值,用 0、1、2 表示容易搞混,怎么给它们起好名字?

检查题

5 道,选完马上告诉你对不对。

  1. 1MyStruct a = new MyStruct(1, 2); MyStruct b = a; a.X = 5; 之后 b.X 是?

  2. 2同样的写法换成类 MyClass,b.X 是?

  3. 3方法里把结构体参数的 X 改成 99,回到 Main,原变量的 X?

  4. 4MyStruct a = null; 会怎样?

  5. 5现在的 C#(10 及以后)里,结构体能不能自己写无参构造函数?