1属性是什么
先分清两个词:第 30 节的特性英文是 Attribute,是贴在代码前面的标签;这一节的属性英文是 Property,是写在类或结构体里的一种成员,两个不是一回事。
类里的私有字段(也叫“域”)在类外面碰不到。外面想读写,就通过属性:它像字段外面的一层包装,get 负责读,set 负责写,数据还是存在字段里。视频里把它比成收发柜:格位是私有字段,投件口是 set,取件口是 get。真实程序里没有窗口,get 和 set 其实是编译器生成的两个方法。
2基本语法
| 4 | private string name; | // 私有字段 name,类外面碰不到 |
| 5 | | |
| 6 | public string Name | // 属性 Name:首字母大写,类型和字段一样 |
| 7 | { | // Name 属性开始 |
| 8 | get { return name; } | // get:把字段的值交出去 |
| 9 | set { name = value; } | // set:value 是关键字,代表外面写进来的值 |
| 10 | } | // Name 属性结束 |
| 16 | Person p = new Person(); | // 创建一个 Person 对象 |
| 17 | p.Name = "Zara"; | // 看着像给字段赋值,其实调用的是 set,value = "Zara" |
| 18 | Console.WriteLine(p.Name); | // 读 p.Name 调用的是 get,递出 Zara |
查看完整代码(20 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | public 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 类结束 |
| 12 | class 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,提醒它一开始没有值。给它一个初始值就没了,输出不变:
| 4 | private string name = ""; | // 初始值是空字符串 "",警告消失 |
3自动实现的属性
get 和 set 里不做额外的事,可以写得更短:大括号里只写 get; 和 set;。
| 4 | public string Name { get; set; } | // 自动属性:编译器自动生成一个没有名字的私有字段来存值,只能通过属性读写 |
用法和刚才一样,同样输出 Zara。
4只读属性
省掉 set 就是只读属性。只读的自动属性只能在构造函数里或声明的时候赋值。
| 4 | public string Name { get; } | // 只有 get:只读属性 |
| 5 | | |
| 6 | public Person(string name) | // 构造函数,收一个 name |
| 7 | { | // 构造函数开始 |
| 8 | Name = name; | // 在构造函数里赋值:允许 |
| 9 | } | // 构造函数结束 |
在 Main 里再写 p. 就编译不通过:
| 16 | p.Name = "Tom"; | // 给只读属性赋值 |
Program.cs(16,9): error CS0200: 无法为属性或索引器“Person.Name”赋值 - 它是只读的
5只写属性
反过来,省掉 get 就是只写属性:能写进去,读不出来。它很少见,一般用在密码这种只许设置、不许读出的数据上。
| 8 | set { name = value; } | // Name 只有 set,没有 get |
硬要输出 p.,同样编译不通过:
| 17 | Console.WriteLine(p.Name); | // 读一个没有 get 的属性 |
Program.cs(17,27): error CS0154: 属性或索引器“Person.Name”不能用在此上下文中,因为它缺少 get 访问器
6在 set 里加检查
get 和 set 里还能加自己的逻辑,比如在 set 里先检查数据。
| 9 | set | // 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 并输出,再写入两个空格:
| 24 | try | // try:试着执行下面的代码 |
| 25 | { | // try 开始 |
| 26 | p.Name = " "; | // 两个空格没通过检查,抛出异常 |
| 27 | } | // try 结束 |
| 28 | catch (ArgumentException e) | // 接住参数异常 |
| 29 | { | // catch 开始 |
| 30 | Console.WriteLine(e.Message); | // 输出异常的消息 |
| 31 | } | // catch 结束 |
| 32 | Console.WriteLine(p.Name); | // name 没被改,仍然输出 Zara |
查看完整代码(34 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | public 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 类结束 |
| 17 | class 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计算属性
属性还可以不依赖字段,值是算出来的,这叫计算属性。
| 4 | public int Width { get; set; } | // 自动属性 Width(宽) |
| 5 | public int Height { get; set; } | // 自动属性 Height(高) |
| 6 | | |
| 7 | public int Area | // 计算属性 Area(面积) |
| 8 | { | // Area 属性开始 |
| 9 | get { return Width * Height; } | // 只有 get:每次读都现算 Width × Height,背后没有字段 |
| 10 | } | // Area 属性结束 |
| 17 | r.Width = 4; | // 宽设为 4 |
| 18 | r.Height = 5; | // 高设为 5 |
| 19 | Console.WriteLine(r.Area); | // 读 Area:4 × 5 = 20 |
| 20 | r.Width = 10; | // 把宽改成 10 |
| 21 | Console.WriteLine(r.Area); | // 再读 Area:按现在的宽高重新算,10 × 5 = 50 |
20 50
Area 没有 set,给它赋值同样编译不通过:
| 17 | r.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 声明,编译报错:
| 1 | using System; | // 引入 System 命名空间 |
| 2 | { | // 大括号前面少了 namespace 声明 |
Program.cs(2,2): error CS1513: 应输入 } Program.cs(71,1): error CS1022: 应输入类型、命名空间定义或文件尾
补上一行 namespace 就能编译了:
| 2 | namespace runoob | // 补上的命名空间声明 |
| 62 | s.Code = "001"; | // 通过属性写入编号(调用 Code 的 set) |
| 63 | s.Name = "Zara"; | // 写入名字 |
| 64 | s.Age = 9; | // 写入年龄 9 |
| 65 | Console.WriteLine("Student Info: {0}", s); | // 输出时自动调用重写的 ToString,年龄是 9 |
| 66 | // 增加年龄 | |
| 67 | s.Age += 1; | // 先调 get 读出 9,加 1 后调 set 写回 10 |
| 68 | Console.WriteLine("Student Info: {0}", s); | // 再输出一次,年龄是 10 |
查看完整代码(72 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | namespace 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. 要先读、后写。
10抽象属性
抽象类里可以声明抽象属性,只写 get 和 set,不写实现。
| 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 类结束 |
| 13 | public string Code { get; set; } = "N.A"; | // Student 自己新加的属性 Code,初始值 "N.A" |
| 14 | public override string Name { get; set; } = "N.A"; | // 用 override 把 Name 实现成自动属性,等号后面是初始值 |
| 15 | public override int Age { get; set; } = 0; | // Age 同样用 override 实现,初始值 0 |
查看完整代码(44 行)
| 1 | using System; | // 引入 System 命名空间 |
| 2 | namespace 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 各报一次错:
| 13 | public string Code { get; set; } = "N.A"; | // 有 Code |
| 14 | public 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 来实现。
下一节:想像数组那样用方括号加下标访问对象里的数据,该怎么写?
