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

索引器(Indexer)

索引器让对象像数组一样用下标读写,写法是 this[参数] 加 get / set;它没有名字,可以按参数类型重载。

时长 5:55授课计划第 13 周

学完这一节,你能

讲义

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

1索引器是什么

索引器(Indexer)让一个对象像数组一样,用方括号加下标来访问。教程说,给一个类定义了索引器,它就像一个虚拟数组。

比如 names[0] = "Zara"; 里的 names 不是数组,而是 IndexedNames 类的对象,方括号写在对象后面。

视频里把 IndexedNames 比成一个名册柜:十个抽屉是私有数组 namelist,下标窗口就是索引器。真实的索引器没有柜子,它只是一对 get 和 set 方法。

2语法

C# · 第 13-14 行
13public string this[int index]// string 是元素类型;名字的位置写 this,方括号里是参数
14{// 索引器开始:里面放 get 和 set

开头的元素类型就是取出来的值是什么类型;get 按 index 返回一个值,set 按 index 设置一个值。这个写法和上一节的属性很像,也是 get 和 set。不同的是:属性有自己的名字(比如 Name),读写一个特定的数据成员;索引器没有名字,用 this 关键字,this 指的就是这个对象本身,它把对象里的数据分成一小块一小块,用下标挑出其中一块来读写。

3一维索引器

C# · 第 6-11 行
6private string[] namelist = new string[size];// 私有字符串数组 namelist,长度是 size
7static public int size = 10;// size = 10;static 表示属于类本身,不属于某个对象
8public IndexedNames()// 构造函数
9{// 构造函数开始
10 for (int i = 0; i < size; i++)// 循环十次
11 namelist[i] = "N. A.";// 每一格都填上 "N. A.",意思是还没有名字

get 先检查下标在不在 0 到 9 之间,在就取出那一格,不在就返回空字符串,不会因为越界出错:

C# · 第 19-26 行
19if( index >= 0 && index <= size-1 )// 下标在 0 到 9 之间吗?
20{// if 开始
21 tmp = namelist[index];// 在范围内:取出那一格的名字
22}// if 结束
23else// 否则(越界)
24{// else 开始
25 tmp = "";// tmp 设为空字符串
26}// else 结束

set 也先检查范围,下标越界时什么也不做:

C# · 第 32-35 行
32if( index >= 0 && index <= size-1 )// 同样先检查范围
33{// if 开始
34 namelist[index] = value;// value 是等号右边赋进来的值,存进那一格
35}// if 结束

Main 里先给 0 到 6 号格存进七个名字,再循环打印十格:

C# · 第 41-43 行
41IndexedNames names = new IndexedNames();// 创建对象:10 格全是 N. A.
42names[0] = "Zara";// 方括号在等号左边,调用 set:0 号格换成 Zara
43names[1] = "Riz";// 1 号格存 Riz(后面 2 到 6 号依次存名字)
C# · 第 49-52 行
49for ( int i = 0; i < IndexedNames.size; i++ )// 循环十次,i 从 0 到 9
50{// for 开始
51 Console.WriteLine(names[i]);// 读 names[i],调用的是 get
52}// for 结束
查看完整代码(56 行)
C#
1using System;// 引入 System 命名空间
2namespace IndexerApplication// 定义命名空间 IndexerApplication
3{// 命名空间开始
4 class IndexedNames// 定义 IndexedNames 类
5 {// IndexedNames 类开始
6 private string[] namelist = new string[size];// 私有数组 namelist,长度 size
7 static public int size = 10;// 静态字段 size = 10
8 public IndexedNames()// 构造函数
9 {// 构造函数开始
10 for (int i = 0; i < size; i++)// 循环十次
11 namelist[i] = "N. A.";// 每格填 "N. A."(for 后面只有一句,可以不加大括号)
12 }// 构造函数结束
13 public string this[int index]// 索引器:参数 int index,返回 string
14 {// 索引器开始
15 get// get 访问器
16 {// get 开始
17 string tmp;// 临时变量 tmp
18
19 if( index >= 0 && index <= size-1 )// 下标在 0 到 9 之间?
20 {// if 开始
21 tmp = namelist[index];// 取出那一格
22 }// if 结束
23 else// 否则
24 {// else 开始
25 tmp = "";// tmp 设为空字符串
26 }// else 结束
27
28 return ( tmp );// 返回 tmp
29 }// get 结束
30 set// set 访问器
31 {// set 开始
32 if( index >= 0 && index <= size-1 )// 下标在范围内?
33 {// if 开始
34 namelist[index] = value;// 存进那一格
35 }// if 结束
36 }// set 结束
37 }// 索引器结束
38
39 static void Main(string[] args)// 程序入口 Main 方法
40 {// Main 开始
41 IndexedNames names = new IndexedNames();// 创建对象 names
42 names[0] = "Zara";// 调用 set:0 号格存 Zara
43 names[1] = "Riz";// 1 号格存 Riz
44 names[2] = "Nuha";// 2 号格存 Nuha
45 names[3] = "Asif";// 3 号格存 Asif
46 names[4] = "Davinder";// 4 号格存 Davinder
47 names[5] = "Sunil";// 5 号格存 Sunil
48 names[6] = "Rubic";// 6 号格存 Rubic
49 for ( int i = 0; i < IndexedNames.size; i++ )// 循环 0 到 9
50 {// for 开始
51 Console.WriteLine(names[i]);// 调用 get,打印第 i 格
52 }// for 结束
53 Console.ReadKey();// 等待按键(现在的 VS 不写也会停住)
54 }// Main 结束
55 }// IndexedNames 类结束
56}// 命名空间结束
运行结果
Zara
Riz
Nuha
Asif
Davinder
Sunil
Rubic
N. A.
N. A.
N. A.

最后三行是 N. A.:7 到 9 号格子没人赋值,还是构造函数填进去的。

4它不是真数组

把循环条件改成 names.Length,编译报错:IndexedNames 里没有 Length。所以教程的循环写的是 IndexedNames.size。

C# · 第 49 行
49for ( int i = 0; i < names.Length; i++ )// 对象没有数组的 Length
编译报错
Program.cs(49,37): error CS1061: “IndexedNames”未包含“Length”的定义,并且找不到可接受第一个“IndexedNames”类型参数的可访问扩展方法“Length”(是否缺少 using 指令或程序集引用?)

5重载索引器

索引器可以重载。第二个实例又加了一个 this[string name],返回 int,而且只有 get:

C# · 第 44-53 行
44int index = 0;// 从 0 号格开始
45while(index < size)// 还没比对完十格就继续
46{// while 开始
47 if (namelist[index] == name)// 这一格的名字和要找的一样吗?
48 {// if 开始
49 return index;// 一样就返回这一格的下标
50 }// if 结束
51 index++;// 下标加 1,比对下一格
52}// while 结束
53return index;// 找遍了都没有:此时 index 是 10
C# · 第 73-74 行
73// 使用带有 string 参数的第二个索引器
74Console.WriteLine(names["Nuha"]);// 方括号里是字符串,用第二个索引器:Nuha 在 2 号格

两个索引器靠参数类型区分:方括号里放整数用第一个,放字符串用第二个。方括号里的参数不一定是整数。运行后前十行和第一个实例一样,接着输出 2(完整输出见下一小节的前 11 行)。

6查不到和越界

C# · 第 75-76 行
75Console.WriteLine(names["Bob"]);// 查一个没有的名字:返回 10,不是有效下标
76Console.WriteLine("[" + names[12] + "]");// 读越界的下标 12:get 走到 else,返回空字符串
查看完整代码(80 行)
C#
1using System;// 引入 System 命名空间
2namespace IndexerApplication// 定义命名空间 IndexerApplication
3{// 命名空间开始
4 class IndexedNames// 定义 IndexedNames 类
5 {// IndexedNames 类开始
6 private string[] namelist = new string[size];// 私有数组 namelist
7 static public int size = 10;// 静态字段 size = 10
8 public IndexedNames()// 构造函数
9 {// 构造函数开始
10 for (int i = 0; i < size; i++)// 循环十次
11 {// for 开始
12 namelist[i] = "N. A.";// 每格填 "N. A."
13 }// for 结束
14 }// 构造函数结束
15 public string this[int index]// 第一个索引器:参数 int
16 {// 索引器开始
17 get// get 访问器
18 {// get 开始
19 string tmp;// 临时变量 tmp
20
21 if( index >= 0 && index <= size-1 )// 下标在范围内?
22 {// if 开始
23 tmp = namelist[index];// 取出那一格
24 }// if 结束
25 else// 否则
26 {// else 开始
27 tmp = "";// tmp 设为空字符串
28 }// else 结束
29
30 return ( tmp );// 返回 tmp
31 }// get 结束
32 set// set 访问器
33 {// set 开始
34 if( index >= 0 && index <= size-1 )// 下标在范围内?
35 {// if 开始
36 namelist[index] = value;// 存进那一格
37 }// if 结束
38 }// set 结束
39 }// 第一个索引器结束
40 public int this[string name]// 第二个索引器:参数 string,返回下标
41 {// 索引器开始
42 get// 只有 get
43 {// get 开始
44 int index = 0;// 从 0 号格开始
45 while(index < size)// 比对十格
46 {// while 开始
47 if (namelist[index] == name)// 名字一样?
48 {// if 开始
49 return index;// 返回这一格的下标
50 }// if 结束
51 index++;// 比对下一格
52 }// while 结束
53 return index;// 没找到:返回 10
54 }// get 结束
55
56 }// 第二个索引器结束
57
58 static void Main(string[] args)// 程序入口 Main 方法
59 {// Main 开始
60 IndexedNames names = new IndexedNames();// 创建对象 names
61 names[0] = "Zara";// 0 号格存 Zara
62 names[1] = "Riz";// 1 号格存 Riz
63 names[2] = "Nuha";// 2 号格存 Nuha
64 names[3] = "Asif";// 3 号格存 Asif
65 names[4] = "Davinder";// 4 号格存 Davinder
66 names[5] = "Sunil";// 5 号格存 Sunil
67 names[6] = "Rubic";// 6 号格存 Rubic
68 // 使用带有 int 参数的第一个索引器
69 for (int i = 0; i < IndexedNames.size; i++)// 循环 0 到 9
70 {// for 开始
71 Console.WriteLine(names[i]);// 打印第 i 格
72 }// for 结束
73 // 使用带有 string 参数的第二个索引器
74 Console.WriteLine(names["Nuha"]);// 查 Nuha:输出 2
75 Console.WriteLine(names["Bob"]);// 查 Bob:输出 10
76 Console.WriteLine("[" + names[12] + "]");// 读 12 号格:输出 []
77 Console.ReadKey();// 等待按键(现在的 VS 不写也会停住)
78 }// Main 结束
79 }// IndexedNames 类结束
80}// 命名空间结束
运行结果
Zara
Riz
Nuha
Asif
Davinder
Sunil
Rubic
N. A.
N. A.
N. A.
2
10
[]

这个查名的索引器没有 set,给它赋值,编译报错:它是只读的。

C# · 第 74 行
74names["Nuha"] = 5;// 给只有 get 的 this[string] 赋值
编译报错
Program.cs(74,10): error CS0200: 无法为属性或索引器“IndexedNames.this[string]”赋值 - 它是只读的

7多个参数

索引器可以带多个参数,每个参数的类型也可以不同。教程没有给例子,我们自己写了一个座位表:排号 'B' - 'A' 得 1、座号 3 - 1 得 2,换算成数组的行和列(都从 0 数)。

C# · 第 6-10 行
6private string[,] seats = new string[3, 4];// 二维数组 seats:3 排,每排 4 个座位([,] 表示二维)
7public string this[char row, int seat]// 两个参数:char 排号、int 座号
8{// 索引器开始
9 get { return seats[row - 'A', seat - 1]; }// get:换算成行和列,取出名字
10 set { seats[row - 'A', seat - 1] = value; }// set:按同样的换算存进名字
C# · 第 16-17 行
16chart['B', 3] = "Riz";// B 排 3 号存进 Riz
17Console.WriteLine(chart['B', 3]);// 读出来:输出 Riz
查看完整代码(20 行)
C#
1using System;// 引入 System 命名空间
2namespace IndexerApplication// 定义命名空间 IndexerApplication
3{// 命名空间开始
4 class SeatChart// 定义 SeatChart 类(座位表)
5 {// SeatChart 类开始
6 private string[,] seats = new string[3, 4];// 二维数组 seats:3 排,每排 4 个座位([,] 表示二维)
7 public string this[char row, int seat]// 索引器有两个参数:char 类型的排号,int 类型的座号
8 {// 索引器开始
9 get { return seats[row - 'A', seat - 1]; }// get:'B' - 'A' = 1、3 - 1 = 2,取出名字
10 set { seats[row - 'A', seat - 1] = value; }// set:按同样的换算存进名字
11 }// 索引器结束
12
13 static void Main(string[] args)// 程序入口 Main 方法
14 {// Main 开始
15 SeatChart chart = new SeatChart();// 创建座位表
16 chart['B', 3] = "Riz";// B 排 3 号存进 Riz
17 Console.WriteLine(chart['B', 3]);// 按同样的排号和座号读出来
18 }// Main 结束
19 }// SeatChart 类结束
20}// 命名空间结束
运行结果
Riz

8小结

  • 索引器让对象像数组一样用下标访问,写法是 this 加方括号参数。
  • 它和属性一样有 get 和 set,但没有名字,读写的是对象里的一小块。
  • 索引器可以重载,靠参数类型区分;只有 get 的只能读,不能赋值。

下一节:能不能把一个方法也装进变量里,像数据一样传来传去?

检查题

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

  1. 1names[0] = "Zara"; 执行的是?

  2. 2教程实例 1 输出的最后三行?

  3. 3实例 2 的 Console.WriteLine(names["Nuha"]); 输出?

  4. 4给只有 get 的 this[string] 写 names["Nuha"] = 5;?

  5. 5循环条件写 i < names.Length?