1结构体是什么
名字、年龄、成绩怎么打包成一个值?用结构体,关键字 struct。它是一种值类型,能把几样相关的数据装进同一个变量。
视频里拿图书举例:书名、作者、主题是文字,书号是整数,类型各不相同。做成结构体就像一只分了四格的资料匣,整只匣子是一个值,里面的四个格子叫它的字段。
2定义结构体
| 4 | struct 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,结构体可以这样直接声明,再用点号一格一格地填:
| 17 | Books Book1; /* 声明 Book1,类型为 Books */ | // 不用 new,直接声明结构体变量 |
| 18 | Books Book2; /* 声明 Book2,类型为 Books */ | // 两只匣子各装各的 |
| 19 | | |
| 20 | /* book 1 详述 */ | // 下面填 Book1 的四个字段 |
| 21 | Book1.title = "C Programming"; | // 变量名.字段名:填书名 |
| 22 | Book1.author = "Nuha Ali"; | // 填作者 |
| 23 | Book1.subject = "C Programming Tutorial"; | // 填主题 |
| 24 | Book1.book_id = 6495407; | // 填书号 |
查看完整代码(47 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | using System.Text; | // 引入 System.Text(本例没用到) |
| 3 | | |
| 4 | struct Books | // 定义结构体 Books |
| 5 | { | // Books 结构体开始 |
| 6 | public string title; | // 字段:书名 |
| 7 | public string author; | // 字段:作者 |
| 8 | public string subject; | // 字段:主题 |
| 9 | public int book_id; | // 字段:书号 |
| 10 | }; | // Books 结构体结束 |
| 11 | | |
| 12 | public 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 开始,结构体也能自己写无参数的构造函数(旧版本不行):
| 9 | public MyStruct() | // 无参构造函数:名字和结构体相同,没有返回类型 |
| 10 | { | // MyStruct 构造函数开始 |
| 11 | X = 10; | // 给 X 赋初值 10 |
| 12 | Y = 20; | // 给 Y 赋初值 20 |
| 13 | } | // MyStruct 构造函数结束 |
| 20 | MyStruct a = new MyStruct(); | // new 时调用上面写的无参构造函数 |
| 21 | Console.WriteLine($"{a.X}, {a.Y}"); | // $"…" 里的 {a.X} 换成变量值:输出 10, 20 |
不能写的东西
结构体不能继承别的结构体:
| 9 | struct MyDerivedStruct : MyStruct | // 想让 MyDerivedStruct 继承 MyStruct |
Program.cs(9,26): error CS0527: 接口列表中的类型“MyStruct”不是接口
反过来,它也不能给别的结构体或类当基类,但可以实现一个或多个接口(后面的章节讲)。成员也不能标 protected、virtual,析构函数也不行:
| 5 | protected int X; | // 结构体成员标 protected |
| 6 | public 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 也行,但所有字段都赋好值以后,结构体才能拿来用:
| 13 | MyStruct a; | // 不用 new 声明 |
| 14 | a.X = 1; | // 只给 X 赋了值 |
| 15 | Console.WriteLine(a.Y); | // Y 还没赋值就去读 |
Program.cs(15,27): error CS0170: 使用了可能未赋值的字段“Y”
放在哪里
结构体变量通常放在栈上(方法运行时临时放变量的地方),创建和销毁都很快;但它要是某个类的字段,就跟着那个对象一起放在堆上(存放 new 出来的对象的地方)。放在哪里由运行时决定,结构体不一定在栈上。
只读结构体
结构体默认可变。在 struct 前加 readonly 就成了只读结构体,字段也要标 readonly,之后再改就不让了:
| 3 | readonly struct Point | // readonly struct:只读结构体 |
| 4 | { | // Point 结构体开始 |
| 5 | public readonly int X; | // 字段也要标 readonly |
| 6 | public readonly int Y; | // 只能在构造函数里赋值 |
| 20 | p.X = 5; | // 创建以后再改字段 |
Program.cs(20,9): error CS0191: 无法分配到只读字段(除非在定义了该字段的类型的构造函数或 init-only 资源库中,或者在变量初始值设定项中)
5类和结构体
类适合表示复杂的对象和行为,支持继承和多态;结构体适合表示轻量的数据。最根本的区别是:结构体是值类型,类是引用类型。
赋值
| 56 | MyStruct structInstance1 = new MyStruct(1, 2); | // new 结构体:X=1,Y=2 |
| 57 | MyStruct structInstance2 = structInstance1; // 复制整个结构 | // 整只匣子复制一份,两边各有一只 |
| 58 | | |
| 59 | // 类是引用类型,分配在堆上 | |
| 60 | MyClass classInstance1 = new MyClass(3, 4); | // new 类的对象:X=3,Y=4 |
| 61 | MyClass classInstance2 = classInstance1; // 复制引用,指向同一个对象 | // 只复制一张“取货牌”,两张牌指向同一个对象 |
| 64 | structInstance1.X = 5; | // 只改第一只匣子 |
| 65 | Console.WriteLine($"Struct: {structInstance1.X}, {structInstance2.X}"); | // 输出 5 和 1 |
| 66 | | |
| 67 | // 修改类实例会影响其他实例 | |
| 68 | classInstance1.X = 6; | // 通过第一张牌改对象 |
| 69 | Console.WriteLine($"Class: {classInstance1.X}, {classInstance2.X}"); | // 两张牌看的是同一个对象:输出 6 和 6 |
查看完整代码(71 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | | |
| 3 | // 结构声明 | |
| 4 | struct 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 | // 类声明 | |
| 28 | class 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 | | |
| 51 | class 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 类结束 |
真实内存里没有匣子和牌,“牌”其实是一个内存地址,也就是引用。
传给方法
| 15 | static void Change(MyStruct s, MyClass c) | // Change 收一个结构体、一个类的对象 |
| 16 | { | // Change 开始 |
| 17 | s.X = 99; | // 改的是结构体的副本 |
| 18 | c.X = 99; | // 牌的复制品也指向原来那个对象 |
| 19 | } | // Change 结束 |
| 23 | MyStruct s = new MyStruct(); | // 结构体 s,X 默认是 0 |
| 24 | MyClass c = new MyClass(); | // 类的对象 c,X 默认是 0 |
| 25 | Change(s, c); | // 传进去:s 复制整份,c 复制一张牌 |
| 26 | Console.WriteLine($"Struct: {s.X}"); | // 原来的 s 没变:输出 0 |
| 27 | Console.WriteLine($"Class: {c.X}"); | // 对象被改了:输出 99 |
类的对象传进方法时,传进去的是牌的复制品(引用的副本),只不过两张牌指向同一个对象。
能不能为空
类的变量可以是 null(空值),表示还没指向对象;结构体直接赋 null 会报错:
| 13 | MyStruct a = null; | // 给结构体变量赋 null |
Program.cs(13,22): error CS0037: 无法将 null 转换为“MyStruct”,因为后者是不可为 null 的值类型
真要表示“没有值”,就在类型后面加问号(第 15 章的可空类型):
| 13 | MyStruct? a = null; | // MyStruct? 是可以为空的结构体 |
| 14 | Console.WriteLine(a.HasValue); | // HasValue 表示有没有值:输出 False |
区别一览
- 继承:结构体不能;类可以,还支持多态。
- 无参构造函数:类一直可以写;结构体从 C# 10 起也可以。
- 赋值和传参:结构体复制整份,类复制引用。
- 可以为空:类的变量可以;结构体要加问号。
- 性能:结构体通常更轻,类的对象要多一些内存管理的开销。
6重写图书例子
四个字段改成 private,外面的代码不能直接碰;再加 setValues 一次填好四个字段,display 负责打印四行(见完整代码):
| 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 结束 |
查看完整代码(52 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | using System.Text; | // 引入 System.Text(本例没用到) |
| 3 | | |
| 4 | struct 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 | | |
| 27 | public 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 表示容易搞混,怎么给它们起好名字?
