第 32 节 · 第四部分 进阶特性

属性(Property)

属性是包在字段外面的成员,外面读它走 get、写它走 set,数据还是存在字段里(计算属性连字段都没有)。

时长 9:30授课计划第 13 周

学完这一节,你能

讲义

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

1属性是什么

先分清两个词:第 30 节的特性英文是 Attribute,是贴在代码前面的标签;这一节的属性英文是 Property,是写在类或结构体里的一种成员,两个不是一回事。

类里的私有字段(也叫“域”)在类外面碰不到。外面想读写,就通过属性:它像字段外面的一层包装,get 负责读,set 负责写,数据还是存在字段里。视频里把它比成收发柜:格位是私有字段,投件口是 set,取件口是 get。真实程序里没有窗口,get 和 set 其实是编译器生成的两个方法。

2基本语法

C# · 第 4-10 行
4private string name;// 私有字段 name,类外面碰不到
5
6public string Name// 属性 Name:首字母大写,类型和字段一样
7{// Name 属性开始
8 get { return name; }// get:把字段的值交出去
9 set { name = value; }// set:value 是关键字,代表外面写进来的值
10}// Name 属性结束
C# · 第 16-18 行
16Person p = new Person();// 创建一个 Person 对象
17p.Name = "Zara";// 看着像给字段赋值,其实调用的是 set,value = "Zara"
18Console.WriteLine(p.Name);// 读 p.Name 调用的是 get,递出 Zara
查看完整代码(20 行)
C#
1using System;// 引入 System 命名空间
2public class Person// 定义 Person 类
3{// Person 类开始
4 private string name;// 私有字段 name
5
6 public string Name// 公共属性 Name
7 {// Name 属性开始
8 get { return name; }// get 访问器:返回 name
9 set { name = value; }// set 访问器:把 value 存进 name
10 }// Name 属性结束
11}// Person 类结束
12class Program// 定义 Program 类
13{// Program 类开始
14 static void Main()// 程序入口 Main 方法
15 {// Main 开始
16 Person p = new Person();// 创建 Person 对象 p
17 p.Name = "Zara";// 调用 set,把 "Zara" 存进 name
18 Console.WriteLine(p.Name);// 调用 get,输出 Zara
19 }// Main 结束
20}// Program 类结束
运行结果
Zara

在新版项目里,name 那一行有一条警告 CS8618,提醒它一开始没有值。给它一个初始值就没了,输出不变:

C# · 第 4 行
4private string name = "";// 初始值是空字符串 "",警告消失

3自动实现的属性

get 和 set 里不做额外的事,可以写得更短:大括号里只写 get; 和 set;。

C# · 第 4 行
4public string Name { get; set; }// 自动属性:编译器自动生成一个没有名字的私有字段来存值,只能通过属性读写

用法和刚才一样,同样输出 Zara。

4只读属性

省掉 set 就是只读属性。只读的自动属性只能在构造函数里或声明的时候赋值。

C# · 第 4-9 行
4public string Name { get; }// 只有 get:只读属性
5
6public Person(string name)// 构造函数,收一个 name
7{// 构造函数开始
8 Name = name;// 在构造函数里赋值:允许
9}// 构造函数结束

在 Main 里再写 p.Name = "Tom" 就编译不通过:

C# · 第 16 行
16p.Name = "Tom";// 给只读属性赋值
编译报错
Program.cs(16,9): error CS0200: 无法为属性或索引器“Person.Name”赋值 - 它是只读的

5只写属性

反过来,省掉 get 就是只写属性:能写进去,读不出来。它很少见,一般用在密码这种只许设置、不许读出的数据上。

C# · 第 8 行
8set { name = value; }// Name 只有 set,没有 get

硬要输出 p.Name,同样编译不通过:

C# · 第 17 行
17Console.WriteLine(p.Name);// 读一个没有 get 的属性
编译报错
Program.cs(17,27): error CS0154: 属性或索引器“Person.Name”不能用在此上下文中,因为它缺少 get 访问器

6在 set 里加检查

get 和 set 里还能加自己的逻辑,比如在 set 里先检查数据。

C# · 第 9-14 行
9set// set 访问器
10{// set 开始
11 if (string.IsNullOrWhiteSpace(value))// IsNullOrWhiteSpace:判断字符串是不是空的,或者全是空格
12 throw new ArgumentException("Name cannot be empty.");// 是空白就抛出 ArgumentException(参数不合法的异常)
13 name = value;// 检查通过,才把 value 存进字段
14}// set 结束

Main 里先写入 Zara 并输出,再写入两个空格:

C# · 第 24-32 行
24try// try:试着执行下面的代码
25{// try 开始
26 p.Name = " ";// 两个空格没通过检查,抛出异常
27}// try 结束
28catch (ArgumentException e)// 接住参数异常
29{// catch 开始
30 Console.WriteLine(e.Message);// 输出异常的消息
31}// catch 结束
32Console.WriteLine(p.Name);// name 没被改,仍然输出 Zara
查看完整代码(34 行)
C#
1using System;// 引入 System 命名空间
2public class Person// 定义 Person 类
3{// Person 类开始
4 private string name;// 私有字段 name
5
6 public string Name// 公共属性 Name
7 {// Name 属性开始
8 get { return name; }// get:返回 name
9 set// set 访问器
10 {// set 开始
11 if (string.IsNullOrWhiteSpace(value))// 如果 value 是空的或全是空格
12 throw new ArgumentException("Name cannot be empty.");// 就抛出 ArgumentException,带一句提示
13 name = value;// 否则把 value 存进 name
14 }// set 结束
15 }// Name 属性结束
16}// Person 类结束
17class Program// 定义 Program 类
18{// Program 类开始
19 static void Main()// 程序入口 Main 方法
20 {// Main 开始
21 Person p = new Person();// 创建 Person 对象 p
22 p.Name = "Zara";// 写入 "Zara":通过检查
23 Console.WriteLine(p.Name);// 输出 Zara
24 try// try:可能出错的代码放这里
25 {// try 开始
26 p.Name = " ";// 写入两个空格:检查不通过,抛异常
27 }// try 结束
28 catch (ArgumentException e)// 接住 ArgumentException
29 {// catch 开始
30 Console.WriteLine(e.Message);// 输出异常消息 Name cannot be empty.
31 }// catch 结束
32 Console.WriteLine(p.Name);// name 没被改,输出 Zara
33 }// Main 结束
34}// Program 类结束
运行结果
Zara
Name cannot be empty.
Zara

这就是属性比公开字段好的地方:坏数据进不来。

7计算属性

属性还可以不依赖字段,值是算出来的,这叫计算属性。

C# · 第 4-10 行
4public int Width { get; set; }// 自动属性 Width(宽)
5public int Height { get; set; }// 自动属性 Height(高)
6
7public int Area// 计算属性 Area(面积)
8{// Area 属性开始
9 get { return Width * Height; }// 只有 get:每次读都现算 Width × Height,背后没有字段
10}// Area 属性结束
C# · 第 17-21 行
17r.Width = 4;// 宽设为 4
18r.Height = 5;// 高设为 5
19Console.WriteLine(r.Area);// 读 Area:4 × 5 = 20
20r.Width = 10;// 把宽改成 10
21Console.WriteLine(r.Area);// 再读 Area:按现在的宽高重新算,10 × 5 = 50
运行结果
20
50

Area 没有 set,给它赋值同样编译不通过:

C# · 第 17 行
17r.Area = 30;// 给只有 get 的 Area 赋值
编译报错
Program.cs(17,9): error CS0200: 无法为属性或索引器“Rectangle.Area”赋值 - 它是只读的

8访问器

get 和 set 就叫属性的访问器,里面写的是能执行的语句:get 负责读取或计算,set 负责写入。两个都有是可读可写;只有 get 是只读,赋值报 CS0200;只有 set 是只写,读取报 CS0154。下面例子里的 Code、Name、Age 都是两个都有的写法,各自包装一个小写的字段(完整代码见折叠块)。

9完整的例子

教程的实例第一行 using 后面直接跟了大括号,少了 namespace 声明,编译报错:

C# · 第 1-2 行
1using System;// 引入 System 命名空间
2{// 大括号前面少了 namespace 声明
编译报错
Program.cs(2,2): error CS1513: 应输入 }
Program.cs(71,1): error CS1022: 应输入类型、命名空间定义或文件尾

补上一行 namespace 就能编译了:

C# · 第 2 行
2namespace runoob// 补上的命名空间声明
C# · 第 62-68 行
62s.Code = "001";// 通过属性写入编号(调用 Code 的 set)
63s.Name = "Zara";// 写入名字
64s.Age = 9;// 写入年龄 9
65Console.WriteLine("Student Info: {0}", s);// 输出时自动调用重写的 ToString,年龄是 9
66// 增加年龄
67s.Age += 1;// 先调 get 读出 9,加 1 后调 set 写回 10
68Console.WriteLine("Student Info: {0}", s);// 再输出一次,年龄是 10
查看完整代码(72 行)
C#
1using System;// 引入 System 命名空间
2namespace runoob// 补上的命名空间 runoob
3{// 命名空间 runoob 开始
4 class Student// 定义 Student 类
5 {// Student 类开始
6
7 private string code = "N.A";// 私有字段 code,初值 "N.A"
8 private string name = "not known";// 私有字段 name,初值 "not known"
9 private int age = 0;// 私有字段 age,初值 0
10
11 // 声明类型为 string 的 Code 属性
12 public string Code// Code 属性
13 {// Code 属性开始
14 get// get 访问器
15 {// get 开始
16 return code;// 返回 code
17 }// get 结束
18 set// set 访问器
19 {// set 开始
20 code = value;// 存进 code
21 }// set 结束
22 }// Code 属性结束
23
24 // 声明类型为 string 的 Name 属性
25 public string Name// Name 属性
26 {// Name 属性开始
27 get// get 访问器
28 {// get 开始
29 return name;// 返回 name
30 }// get 结束
31 set// set 访问器
32 {// set 开始
33 name = value;// 存进 name
34 }// set 结束
35 }// Name 属性结束
36
37 // 声明类型为 int 的 Age 属性
38 public int Age// Age 属性
39 {// Age 属性开始
40 get// get 访问器
41 {// get 开始
42 return age;// 返回 age
43 }// get 结束
44 set// set 访问器
45 {// set 开始
46 age = value;// 存进 age
47 }// set 结束
48 }// Age 属性结束
49 public override string ToString()// 重写 ToString(每个对象都有的方法,输出对象时会调用它)
50 {// ToString 开始
51 return "Code = " + Code +", Name = " + Name + ", Age = " + Age;// 把三个属性拼成一行字
52 }// ToString 结束
53 }// Student 类结束
54 class ExampleDemo// 定义 ExampleDemo 类
55 {// ExampleDemo 类开始
56 public static void Main()// 程序入口 Main 方法
57 {// Main 开始
58 // 创建一个新的 Student 对象
59 Student s = new Student();// 创建 Student 对象 s
60
61 // 设置 student 的 code、name 和 age
62 s.Code = "001";// 写入编号 "001"
63 s.Name = "Zara";// 写入名字 "Zara"
64 s.Age = 9;// 写入年龄 9
65 Console.WriteLine("Student Info: {0}", s);// 输出:Age = 9
66 // 增加年龄
67 s.Age += 1;// 先 get 读出 9,再 set 写回 10
68 Console.WriteLine("Student Info: {0}", s);// 输出:Age = 10
69 Console.ReadKey();// 等待按键(现在的 VS 不写也会停住)
70 }// Main 结束
71 }// ExampleDemo 类结束
72}// 命名空间 runoob 结束
运行结果
Student Info: Code = 001, Name = Zara, Age = 9
Student Info: Code = 001, Name = Zara, Age = 10

年龄从 9 变成 10:s.Age += 1 要先读、后写。

10抽象属性

抽象类里可以声明抽象属性,只写 get 和 set,不写实现。

C# · 第 4-8 行
4public abstract class Person// 抽象类 Person
5{// Person 类开始
6 public abstract string Name { get; set; }// 抽象属性 Name:只声明,不实现
7 public abstract int Age { get; set; }// 抽象属性 Age
8}// Person 类结束
C# · 第 13-15 行
13public string Code { get; set; } = "N.A";// Student 自己新加的属性 Code,初始值 "N.A"
14public override string Name { get; set; } = "N.A";// 用 override 把 Name 实现成自动属性,等号后面是初始值
15public override int Age { get; set; } = 0;// Age 同样用 override 实现,初始值 0
查看完整代码(44 行)
C#
1using System;// 引入 System 命名空间
2namespace Runoob// 定义命名空间 Runoob
3{// 命名空间 Runoob 开始
4 public abstract class Person// 抽象类 Person
5 {// Person 类开始
6 public abstract string Name { get; set; }// 抽象属性 Name
7 public abstract int Age { get; set; }// 抽象属性 Age
8 }// Person 类结束
9
10 class Student : Person// Student 继承 Person
11 {// Student 类开始
12 // 声明自动实现的属性
13 public string Code { get; set; } = "N.A";// 新加的自动属性 Code
14 public override string Name { get; set; } = "N.A";// 实现抽象属性 Name
15 public override int Age { get; set; } = 0;// 实现抽象属性 Age
16
17 public override string ToString()// 重写 ToString
18 {// ToString 开始
19 return $"Code = {Code}, Name = {Name}, Age = {Age}";// $"..." 是插值字符串,{Code} 处填 Code 的值
20 }// ToString 结束
21 }// Student 类结束
22
23 class ExampleDemo// 定义 ExampleDemo 类
24 {// ExampleDemo 类开始
25 public static void Main()// 程序入口 Main 方法
26 {// Main 开始
27 // 创建一个新的 Student 对象
28 Student s = new Student// 创建 Student,用对象初始化器赋值
29 {// 初始化器开始
30 Code = "001",// Code 设为 "001"
31 Name = "Zara",// Name 设为 "Zara"
32 Age = 9// Age 设为 9
33 };// 初始化器结束
34
35 Console.WriteLine("Student Info:- {0}", s);// 输出:Age = 9
36
37 // 增加年龄
38 s.Age += 1;// 先 get 再 set,年龄变成 10
39 Console.WriteLine("Student Info:- {0}", s);// 输出:Age = 10
40
41 Console.ReadKey();// 等待按键(现在的 VS 不写也会停住)
42 }// Main 结束
43 }// ExampleDemo 类结束
44}// 命名空间 Runoob 结束
运行结果
Student Info:- Code = 001, Name = Zara, Age = 9
Student Info:- Code = 001, Name = Zara, Age = 10

Main 里用 new Student { ... } 后面的大括号直接给三个属性赋值,这叫对象初始化器,每一项都调用 set。结果和上一个例子一样,只是冒号后面多一个短横(教程的结果漏了它)。

派生类必须实现全部抽象属性(除非它自己也是抽象类)。Student 漏写 Name,get 和 set 各报一次错:

C# · 第 13-14 行
13public string Code { get; set; } = "N.A";// 有 Code
14public override int Age { get; set; } = 0;// 有 Age,但漏写了 Name
编译报错
Program.cs(10,11): error CS0534: “Student”不实现继承的抽象成员“Person.Name.get”
Program.cs(10,11): error CS0534: “Student”不实现继承的抽象成员“Person.Name.set”

11小结

  • 属性包在字段外面:get 负责读,set 负责写,value 是写进来的值。
  • 自动属性让编译器生成字段;省掉 set 是只读,省掉 get 是只写。
  • set 里可以检查数据;计算属性不存值,每次读都现算。
  • 抽象属性只声明不实现,派生类用 override 来实现。

下一节:想像数组那样用方括号加下标访问对象里的数据,该怎么写?

检查题

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

  1. 1p.Name = "Zara";(Name 是带 get、set 的属性)执行时发生什么?

  2. 2教程自定义逻辑例子,先写入 Zara,再写入两个空格并接住异常,最后输出 Name,最后一行是?

  3. 3Rectangle 先设 Width=4、Height=5,输出 Area 后把 Width 改成 10,再输出 Area?

  4. 4只有 get 的属性,在 Main 里给它赋值?

  5. 5s.Age += 1; 访问器的调用顺序?