1. 关于集合的几句话
到现在为止我们一直在用数组——这是存储同一类型多个元素最简单的方式。但C#里其实有很多更方便更强大的集合,能帮你解决各种问题。
集合就是能存一组其他对象的对象。和数组不一样,集合通常能动态改变大小,提供了很多方便操作数据的方法,还针对不同场景做了优化。
主要集合类型(简单了解下):
List<T> —— 动态数组,可以自动扩容或缩小:
List<string> names = new List<string>();
names.Add("亚历克斯");
names.Add("玛丽亚");
Console.WriteLine(names[0]); // 亚历克斯
Console.WriteLine(names.Count); // 2
Dictionary<TKey, TValue> —— “键-值”对的集合,每个key对应一个value:
Dictionary<string, int> ages = new Dictionary<string, int>();
ages["亚历克斯"] = 25;
ages["玛丽亚"] = 30;
Console.WriteLine(ages["亚历克斯"]); // 25
注意:上面例子里我们用[]方括号访问集合元素——和数组一模一样!这就是索引器的功劳,今天我们就来聊聊它。
这里只是初步认识集合——后面几节课会详细讲它们的功能、区别和用法。现在你只要知道,集合能用方括号访问元素,这不是魔法,而是语言自带的特殊机制。
2. 索引器
普通C#对象一般通过属性和方法来操作。但如果你的对象本身就是个小集合怎么办?比如:
- 你写了个Week类,想通过数字拿到星期几的名字:week[0] → "星期一"。
- 或者写了个Library类,想通过编号拿到书名:library[3] → "朝向斯万"。
是不是很方便?每次都写library.GetBookByIndex(3)太麻烦了——当然想像数组一样直接访问!
这时候就轮到索引器登场了。
索引器就是类里的特殊成员,让你能用方括号语法像数组一样操作对象: obj[0]、 obj["key"]等等。索引器外观看起来像属性,但没有名字,参数写在方括号里。就像你写.Name,但这里是[i]。
3. 带索引器的简单集合
来写个小类,存你最喜欢的颜色。我们用字符串数组存。如果没有索引器,就得写个GetColor(int i)方法。有了索引器就简单多了:
using System;
public class FavoriteColors
{
// 用来存颜色的私有字段
private string[] colors = new string[5];
// 索引器:
public string this[int index]
{
get
{
// 检查数组边界(封装体现!)
if (index < 0 || index >= colors.Length)
throw new IndexOutOfRangeException("颜色索引不对!");
return colors[index];
}
set
{
if (index < 0 || index >= colors.Length)
throw new IndexOutOfRangeException("颜色索引不对!");
colors[index] = value ?? throw new ArgumentNullException(nameof(value));
}
}
}
class Program
{
static void Main()
{
FavoriteColors favorites = new FavoriteColors();
favorites[0] = "绿色";
favorites[1] = "蓝色";
favorites[2] = "红色";
favorites[10] = "紫色"; // 会抛异常!
Console.WriteLine(favorites[1]); // 蓝色
}
}
这里发生了什么?
- 我们创建了个私有数组,外部不能直接乱改。
- 索引器写成public string this[int index],this表示是当前对象的索引器。
- 在get和set里做了边界检查,不让越界/写入null。
- 最后就能像数组一样用favorites[0]了。
4. 索引器语法:细节
语法和属性很像,但名字(比如Age)换成了this加参数:
// 索引器签名(通用模板)
[修饰符] 返回类型 this[索引类型 索引名]
{
get { ... }
set { ... }
}
例子:经典写法
public class MyCollection
{
private int[] data = new int[10];
// 可读可写的索引器
public int this[int index]
{
get { return data[index]; }
set { data[index] = value; }
}
}
索引器不一定只能用int
重点:索引器参数不一定只能是int,你可以用任何类型(只要key有意义):
public string this[string colorName]
{
get { /* ... */ }
set { /* ... */ }
}
比如电话本类,按名字查最合理:
public class PhoneBook
{
private Dictionary<string, int> entries = new Dictionary<string, int>();
public int this[string name]
{
get
{
if (entries.ContainsKey(name))
return entries[name];
return null;
}
set
{
entries[name] = value;
}
}
}
关于集合和Dictionary<string, string>怎么用,以后还会详细讲 :P
5. 实战例子:文本里的单词计数器
继续完善我们的应用。假设现在有个类,能统计文本里每个单词出现了多少次。最好让用户能直接用方括号查单词出现次数:
using System.Collections.Generic;
public class WordCounter
{
private Dictionary<string, int> counter = new Dictionary<string, int>();
// 按字符串(单词)索引
public int this[string word]
{
get
{
if (counter.ContainsKey(word))
return counter[word];
return 0; // 没有这个单词就返回0
}
set
{
counter[word] = value;
}
}
// 从字符串统计单词
public void AddWords(string text)
{
foreach (var word in text.Split(' ', System.StringSplitOptions.RemoveEmptyEntries))
{
if (counter.ContainsKey(word))
counter[word]++;
else
counter[word] = 1;
}
}
}
// 在Main里:
var wc = new WordCounter();
wc.AddWords("妈妈 洗 框 洗 妈妈 爸爸");
Console.WriteLine($"'妈妈' 出现了 {wc["妈妈"]} 次");
Console.WriteLine($"'框' 出现了 {wc["框"]} 次");
Console.WriteLine($"'猫' 出现了 {wc["猫"]} 次"); // 0
实际有什么用? 这种写法常用来实现自定义集合、内存库、映射(map和index)甚至DSL(C#里的小型专用语言)。
6. 限制和细节
索引器很强大,但也有一些规则和坑(哪有没坑的~)。
索引器没有名字
和属性不一样,索引器没有名字,只有this[参数类型]这种签名。你要是写public int MyIndexer[int i],编译器会懵逼。只能用this。
不能有静态索引器
索引器只能属于类的实例,不能是static成员。你不能写static int this[int i]——因为this永远指的是某个对象。
可以重载参数类型/数量
你可以在一个类里写多个索引器,只要参数类型或数量不一样。例如:
public string this[int i] { get { ... } set { ... } }
public string this[string key] { get { ... } set { ... } }
这样没问题,编译器不会搞混——如果参数重复就会报错。
必须有get或set
索引器必须有get或set。只读就去掉set,只写就去掉get。一般都写俩。
7. 实用价值和为什么要学
- 索引器在数据集合里用得超多。很多.NET类都带索引器,比如List<T>、Dictionary<TKey,TValue>。你写list[2]其实就是用的索引器!
- 索引器能隐藏内部实现(封装!),但给用户一个熟悉又方便的接口。用你类的人不用管你怎么存数据,直接用[index]就行。
- 你的代码会变得简洁直观——面试官(还有你未来的同事)都很喜欢。
属性 vs 索引器:对比
| 属性 | 索引器 | |
|---|---|---|
| 名字 | 有(比如Name) | 没有(用this[参数]代替) |
| 访问方式 | 按名字 | 按索引或key |
| 静态 | 可以是static | 只能实例用 |
| 一个类里能有多少 | 随便几个 | 可以有多个,但参数签名要不一样 |
| 用途 | 存/取数据 | 小型集合、关联数据 |
8. 常见错误和建议
错误1: 想写静态索引器。
不行——this[...]只能用在对象上。
错误2: 忘了检查索引。
如果在get或set里不检查边界,程序可能直接崩掉。
错误3: 参数类型搞混了。
如果写了两个参数一样的索引器,编译器会报错。
错误4: 忘了写get或set。
想读写都能用,两个都得写。
建议: 如果你的类只是包了个数组——直接把数组访问转给索引器就行。这样又快又直观。
9. 为什么要学这个
索引器让对象接口变得简单、直观又好用。它能隐藏内部实现,但给开发者一个超级方便的数据访问方式。
内置集合就是这么做的:string、List<T>、Dictionary<TKey, TValue>、Span<T>等等。你写array[2]或text[0],其实就是在用索引器。
最重要的是——你现在也能写出自己的类,像这些内置集合一样灵活简洁。也就是说——你离写出专业、易读的代码又近了一步!
GO TO FULL VERSION