Day 6 - 数组与 List
建议用时:180-210 分钟
你将学会什么
- 为什么程序需要保存一组数据
- 数组是什么,
List是什么 - 数组和
List的区别 - 下标为什么从 0 开始
Length和Count分别是什么意思- 如何添加、读取、修改、删除
List里的数据 - 如何用
for和foreach遍历集合 - 如何统计总和、平均值、最高分和最低分
- 如何避免空集合和下标越界
本页只讲数组和 List。更复杂的集合放到第二周,这里先把“一组数据怎么保存和遍历”打牢。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
从 Day 5 走到 Day 6
前五天的变量大多只保存一个值:
int score = 80;
string name = "Tom";但“全班成绩”或“所有商品名称”不可能每一项都手写一个变量。Day 6 解决的就是这个问题:把多个同类型数据装进一个集合。
一个成绩 -> int score
多个成绩 -> List<int> scores今天的最低通过线
第一次学习只要求做到:
- 知道数组长度固定,
List可以继续添加。 - 知道第一个元素的下标是
0。 - 会用
Add添加数据,用Count查看数量。 - 会用
foreach把所有元素输出。
最高分、最低分、排序、插入和删除都可以第二遍再练。先把“保存多条数据并遍历”做通。
第一部分:先学原理和最小知识
这一部分先解决“这是什么、为什么要这样写”。每看到一个短例子就亲手敲一遍并运行,不要先背完整程序。
今天先抓住什么
前几天一个变量通常只保存一个值:
int score = 80;但真实程序经常要保存一组数据:
多个成绩
多个商品
多条账单
多个菜单选项这时就需要数组或 List。
List<int> 这一行拆开看
初学者看到下面一行通常会被符号吓到:
var scores = new List<int>();把它拆开:
| 部分 | 人话解释 |
|---|---|
scores | 变量名,表示“多条成绩” |
List | 一个长度可以变化的列表 |
<int> | 这个列表里只允许放整数 |
new | 创建一个新的列表 |
() | 调用创建列表所需的构造过程;现在先按固定写法使用 |
var | 让 C# 根据右边推断类型;这里推断成 List<int> |
它也可以写成完整类型:
List<int> scores = new List<int>();两种写法结果一样。第一次学习时可以先用完整写法,熟悉以后再用 var。
下面这行会失败:
// scores.Add("Tom");因为 scores 是 List<int>,只能保存 int,不能放字符串。
下标不是“第几个”的自然语言数字
假设列表里有三项:
值: 80 95 62
下标: 0 1 2所以:
- “第 1 个元素”写成
scores[0]。 - “第 2 个元素”写成
scores[1]。 - 最后一个元素的下标永远是
scores.Count - 1。
Count 表示数量,不表示最后一个下标。数量是 3 时,最后下标是 2。
名词先讲清楚
| 名词 | 直接解释 | 例子 |
|---|---|---|
| 集合 | 保存一组数据的结构 | 多个成绩 |
| 数组 | 长度固定的一组数据 | int[] scores = new int[3] |
List | 长度可以变化的一组数据 | List<int> |
| 元素 | 集合里的每一项 | 一个成绩、一个名字 |
| 下标 | 元素的位置,从 0 开始 | scores[0] |
Length | 数组长度 | array.Length |
Count | List 当前元素数量 | list.Count |
| 遍历 | 把集合里的元素一个个取出来 | foreach |
| 越界 | 访问了不存在的位置 | 有 3 项却取 [3] |
数组是什么
数组适合保存数量固定的数据。
int[] scores = new int[3];
scores[0] = 80;
scores[1] = 95;
scores[2] = 62;
Console.WriteLine(scores[0]);
Console.WriteLine(scores.Length);输出:
80
3数组一开始就确定长度。上面这个数组长度是 3,所以能用的下标是 0、1、2。
List 是什么
List 适合保存运行过程中不断增加或减少的数据。
var scores = new List<int>();
scores.Add(80);
scores.Add(95);
scores.Add(62);
Console.WriteLine(scores[0]);
Console.WriteLine(scores.Count);输出:
80
3Add 会把新元素追加到列表末尾。
数组和 List 怎么选
| 场景 | 推荐 |
|---|---|
| 数据数量一开始就固定 | 数组 |
| 数据会不断添加 | List |
| 数据会删除 | List |
| 只想练基础遍历 | 两个都可以 |
| 用户不断输入,直到回车结束 | List |
第一周练习里,如果不确定选哪个,优先用 List,因为它更适合输入数量不确定的程序。
下标为什么从 0 开始
集合里的第一个元素下标是 0,不是 1。
var names = new List<string> { "Tom", "Jerry", "Alice" };
Console.WriteLine(names[0]);
Console.WriteLine(names[1]);
Console.WriteLine(names[2]);输出:
Tom
Jerry
Alice如果写:
// Console.WriteLine(names[3]);会越界,因为第 3 个下标不存在。Count 是 3,但最后一个下标是 Count - 1,也就是 2。
Length 和 Count
数组用 Length:
int[] numbers = { 10, 20, 30 };
Console.WriteLine(numbers.Length);List 用 Count:
var numbers = new List<int> { 10, 20, 30 };
Console.WriteLine(numbers.Count);两个例子都输出:
3for 和 foreach 怎么选
只需要拿到每个元素,用 foreach:
var scores = new List<int> { 80, 95, 62 };
foreach (int score in scores)
{
Console.WriteLine(score);
}需要下标,用 for:
var scores = new List<int> { 80, 95, 62 };
for (int i = 0; i < scores.Count; i++)
{
Console.WriteLine($"第 {i + 1} 个成绩: {scores[i]}");
}注意 i < scores.Count,不要写成 i <= scores.Count。
List 常用操作
var names = new List<string>();
names.Add("Tom");
names.Add("Jerry");
names.Add("Alice");
names[1] = "Jack";
names.Remove("Tom");
Console.WriteLine(names.Contains("Alice"));
Console.WriteLine(names.Count);操作解释:
| 写法 | 作用 |
|---|---|
Add | 添加元素 |
names[1] = "Jack" | 修改下标 1 的元素 |
Remove | 删除匹配的元素 |
Contains | 判断是否包含 |
Count | 当前元素数量 |
更完整的常用操作表:
| 需求 | 写法 | 注意点 |
|---|---|---|
| 创建空列表 | new List<string>() | 一开始没有元素 |
| 创建时放数据 | new List<int> { 80, 95 } | 适合固定练习数据 |
| 添加到末尾 | list.Add(item) | 最常用新增 |
| 插入到指定位置 | list.Insert(0, item) | 下标不能越界 |
| 读取指定位置 | list[0] | 读取前确认 Count > 0 |
| 修改指定位置 | list[1] = value | 下标必须存在 |
| 删除指定值 | list.Remove(value) | 只删除匹配到的第一项 |
| 删除指定位置 | list.RemoveAt(0) | 下标必须存在 |
| 清空列表 | list.Clear() | 删除全部元素 |
| 判断是否包含 | list.Contains(value) | 适合简单值 |
| 查找位置 | list.IndexOf(value) | 找不到返回 -1 |
| 排序 | list.Sort() | 会改变原列表顺序 |
| 反转 | list.Reverse() | 会改变原列表顺序 |
| 转数组 | list.ToArray() | 得到固定长度数组 |
| 数量 | list.Count | 判断空列表和循环边界 |
完整例子:
var products = new List<string> { "Keyboard", "Mouse" };
products.Add("Monitor");
products.Insert(1, "Cable");
products[0] = "Keyboard Pro";
Console.WriteLine(products.Contains("Mouse"));
Console.WriteLine(products.IndexOf("Cable"));
products.Remove("Mouse");
products.Sort();
foreach (string product in products)
{
Console.WriteLine(product);
}输出:
True
1
Cable
Keyboard Pro
Monitor这里要注意:Sort() 会直接改变 products 自己的顺序,不是返回一个新列表。
空集合为什么要先判断
下面这段有风险:
var scores = new List<int>();
// int max = scores[0];因为列表里没有任何元素,scores[0] 不存在。
统计最高分和最低分前,要先判断:
var scores = new List<int>();
if (scores.Count == 0)
{
Console.WriteLine("没有数据");
return;
}今天写代码时按这个顺序
- 先判断要保存一条数据还是多条数据。
- 数量固定用数组,数量变化用
List。 - 添加数据前先校验输入。
- 用
foreach做普通遍历。 - 需要显示第几个时,用
for。 - 访问
[0]前先判断集合不是空的。 - 写循环时确认下标不会超过
Count - 1。
第二部分:把知识组合成完整例子
前面已经学过最小知识。现在把它们组合起来,先读懂执行顺序,再完整敲一遍。今天最终要写一个成绩统计程序:反复输入成绩,最后输出数量、总分、平均分、最高分、最低分。
这段完整程序看起来长,是因为它把前几天的知识组合起来了。第一次只找三条主线:
scores.Add(score) -> 保存一条成绩
foreach -> 逐条读取成绩
scores.Count -> 一共有多少条其余的输入校验、while、if 都是前几天学过的旧知识。不要把整段代码误认为全是新语法。
最终你要写出这个程序
var scores = new List<int>();
while (true)
{
Console.Write("输入成绩,直接回车结束: ");
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
{
break;
}
if (!int.TryParse(input, out int score))
{
Console.WriteLine("成绩必须是整数");
continue;
}
if (score < 0 || score > 100)
{
Console.WriteLine("成绩必须在 0 到 100 之间");
continue;
}
scores.Add(score);
}
if (scores.Count == 0)
{
Console.WriteLine("没有可统计的成绩");
return;
}
int sum = 0;
int max = scores[0];
int min = scores[0];
foreach (int score in scores)
{
sum += score;
if (score > max)
{
max = score;
}
if (score < min)
{
min = score;
}
}
double average = (double)sum / scores.Count;
Console.WriteLine($"数量: {scores.Count}");
Console.WriteLine($"总分: {sum}");
Console.WriteLine($"平均分: {average:F1}");
Console.WriteLine($"最高分: {max}");
Console.WriteLine($"最低分: {min}");示例输入:
输入成绩,直接回车结束: 80
输入成绩,直接回车结束: 95
输入成绩,直接回车结束: 62
输入成绩,直接回车结束: 40
输入成绩,直接回车结束:
数量: 4
总分: 277
平均分: 69.3
最高分: 95
最低分: 40这个程序里,List<int> 保存了所有成绩,foreach 负责一条一条统计。
第三部分:跟着敲代码
从这里开始动手。每个例子都可以直接放进 Program.cs 运行。
每个例子按三遍练:第一遍照敲;第二遍换一组集合数据;第三遍关掉答案,从创建集合开始完整重写。下标错误必须自己根据报错修一次。
动手前先做这 3 件事
- 打开控制台项目,确认
dotnet run能运行。 - 每做一个例子,就把
Program.cs旧内容替换掉。 - 每次只改一个数字或一个输入,看输出怎么变。
例子 1:数组保存固定数量成绩
int[] scores = new int[3];
scores[0] = 80;
scores[1] = 95;
scores[2] = 62;
for (int i = 0; i < scores.Length; i++)
{
Console.WriteLine($"第 {i + 1} 个成绩: {scores[i]}");
}预期输出:
第 1 个成绩: 80
第 2 个成绩: 95
第 3 个成绩: 62例子 2:用数组简写初始化
int[] scores = { 80, 95, 62 };
foreach (int score in scores)
{
Console.WriteLine(score);
}这种写法适合一开始就知道所有数据。
例子 3:List 添加多个输入
var names = new List<string>();
while (true)
{
Console.Write("姓名,直接回车结束: ");
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
{
break;
}
names.Add(input.Trim());
}
Console.WriteLine($"一共输入 {names.Count} 个姓名");
foreach (string name in names)
{
Console.WriteLine(name);
}输入几个名字,Count 就会变成几。
例子 4:修改和删除 List 元素
var names = new List<string> { "Tom", "Jerry", "Alice" };
names[1] = "Jack";
names.Remove("Tom");
foreach (string name in names)
{
Console.WriteLine(name);
}预期输出:
Jack
Alice例子 5:用 for 显示序号
var products = new List<string> { "Keyboard", "Mouse", "Cable" };
for (int i = 0; i < products.Count; i++)
{
Console.WriteLine($"{i + 1}. {products[i]}");
}预期输出:
1. Keyboard
2. Mouse
3. Cable显示给用户的序号可以从 1 开始,但访问 List 的下标仍然从 0 开始。
例子 6:统计总和和平均值
var scores = new List<int> { 80, 95, 62, 40 };
int sum = 0;
foreach (int score in scores)
{
sum += score;
}
double average = (double)sum / scores.Count;
Console.WriteLine($"数量: {scores.Count}");
Console.WriteLine($"总分: {sum}");
Console.WriteLine($"平均分: {average:F1}");预期输出:
数量: 4
总分: 277
平均分: 69.3例子 7:统计最高分和最低分
var scores = new List<int> { 80, 95, 62, 40 };
if (scores.Count == 0)
{
Console.WriteLine("没有成绩");
return;
}
int max = scores[0];
int min = scores[0];
foreach (int score in scores)
{
if (score > max)
{
max = score;
}
if (score < min)
{
min = score;
}
}
Console.WriteLine($"最高分: {max}");
Console.WriteLine($"最低分: {min}");先用第一个成绩当初始值,再遍历比较。
例子 8:完整输入成绩并统计
var scores = new List<int>();
while (true)
{
Console.Write("输入成绩,直接回车结束: ");
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
{
break;
}
if (!int.TryParse(input, out int score))
{
Console.WriteLine("成绩必须是整数");
continue;
}
if (score < 0 || score > 100)
{
Console.WriteLine("成绩必须在 0 到 100 之间");
continue;
}
scores.Add(score);
}
Console.WriteLine($"有效成绩数量: {scores.Count}");这个例子先只做输入和保存。统计放到作业完整答案里。
例子 9:商品名称列表增删改查
var products = new List<string>();
products.Add("Keyboard");
products.Add("Mouse");
products.Add("Monitor");
Console.WriteLine($"新增后数量: {products.Count}");
products[1] = "Wireless Mouse";
Console.WriteLine($"修改第 2 个商品: {products[1]}");
if (products.Contains("Keyboard"))
{
Console.WriteLine("列表里有 Keyboard");
}
int monitorIndex = products.IndexOf("Monitor");
Console.WriteLine($"Monitor 的下标: {monitorIndex}");
products.Remove("Keyboard");
products.Sort();
Console.WriteLine("最终商品列表:");
foreach (string product in products)
{
Console.WriteLine(product);
}预期输出:
新增后数量: 3
修改第 2 个商品: Wireless Mouse
列表里有 Keyboard
Monitor 的下标: 2
最终商品列表:
Monitor
Wireless Mouse常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
| 下标从 1 开始 | C# 集合下标从 0 开始 | 第一个元素是 items[0] |
访问 items[Count] | 最后一个下标是 Count - 1 | 先判断范围再访问 |
| 遍历时删除元素 | 集合长度变化容易出错 | 先收集要删的项,或倒序删除 |
| 空列表直接算平均值 | 除以 0 会出错 | Count > 0 时再计算 |
数组和 List 混用 | 数组长度固定,List 可增删 | 需要动态增删时用 List<T> |
每日小测
先独立回答,再看参考答案。
1. 选择数组还是 List
下面两个场景分别更适合什么?
A. 一周固定 7 天的温度
B. 用户不断添加的商品名称2. 预测下标
var names = new List<string> { "Tom", "Jerry", "Alice" };
Console.WriteLine(names[1]);输出什么?
3. 找越界错误
为什么下面代码会出错?
var scores = new List<int> { 80, 95, 62 };
Console.WriteLine(scores[scores.Count]);4. 读懂泛型符号
List<decimal> 表示这个列表允许保存什么类型的数据?能不能向里面添加 "20"?
5. 选择循环
只想输出每一个商品名称,不需要知道下标,优先用 for 还是 foreach?
参考答案
- A 数量固定,可以用数组;B 数量会变化,适合
List。 - 输出
Jerry,因为下标1是第二个元素。 Count是数量 3,但有效下标只有 0、1、2。最后一个元素应写scores[scores.Count - 1]。- 只能保存
decimal。"20"是字符串,不能直接添加;20m才是decimal。 - 优先用
foreach,因为只需要逐项读取,不需要下标。
如果第 2、3 题答错,先不要做最高分统计,重新画一遍“值和下标”的对应图。
上位机专项练习
上位机要保存通道、设备和连续采集值。数组适合固定通道,List 适合数量会变化的数据。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:数组保存固定通道名
string[] channels = { "温度", "压力", "流量" };
for (int index = 0; index < channels.Length; index++)
{
Console.WriteLine($"通道 {index + 1}: {channels[index]}");
}运行结果或界面效果:
通道 1: 温度
通道 2: 压力
通道 3: 流量改动任务: 增加“液位”通道。
专项例子 2:List 保存连续采集值
var readings = new List<double>();
readings.Add(20.1);
readings.Add(20.5);
readings.Add(21.0);
Console.WriteLine($"采集数量: {readings.Count}");运行结果或界面效果:
采集数量: 3改动任务: 删除第二个值,再查看 Count。
专项例子 3:统计平均值和最大值
var readings = new List<double> { 20.1, 25.8, 22.3 };
double average = readings.Average();
double maximum = readings.Max();
Console.WriteLine($"平均值: {average:F1}");
Console.WriteLine($"最大值: {maximum:F1}");运行结果或界面效果:
平均值: 22.7
最大值: 25.8改动任务: 增加 30.0,观察平均值和最大值。
第四部分:作业完整答案
这一部分给出当天作业的完整答案。先按第三部分做一遍,再用这里检查。
作业要求
写一个成绩统计程序,要求:
- 可以反复输入成绩。
- 直接回车结束输入。
- 非整数要提示并跳过。
- 小于 0 或大于 100 要提示并跳过。
- 如果没有有效成绩,输出“没有可统计的成绩”。
- 有成绩时,输出数量、总分、平均分、最高分、最低分。
作业拆解
按这个顺序写:
- 创建
List<int>保存成绩。 - 用
while (true)反复读取输入。 - 空输入时
break。 - 用
TryParse转换成绩。 - 用范围判断拦住错误成绩。
- 用
Add保存有效成绩。 - 统计前判断
Count == 0。 - 用
foreach统计总分、最高分、最低分。
完整代码
var scores = new List<int>();
while (true)
{
Console.Write("输入成绩,直接回车结束: ");
string? input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
{
break;
}
if (!int.TryParse(input, out int score))
{
Console.WriteLine("成绩必须是整数");
continue;
}
if (score < 0 || score > 100)
{
Console.WriteLine("成绩必须在 0 到 100 之间");
continue;
}
scores.Add(score);
}
if (scores.Count == 0)
{
Console.WriteLine("没有可统计的成绩");
return;
}
int sum = 0;
int max = scores[0];
int min = scores[0];
foreach (int score in scores)
{
sum += score;
if (score > max)
{
max = score;
}
if (score < min)
{
min = score;
}
}
double average = (double)sum / scores.Count;
Console.WriteLine("----- 成绩统计 -----");
Console.WriteLine($"数量: {scores.Count}");
Console.WriteLine($"总分: {sum}");
Console.WriteLine($"平均分: {average:F1}");
Console.WriteLine($"最高分: {max}");
Console.WriteLine($"最低分: {min}");正确运行结果
正常输入:
输入成绩,直接回车结束: 80
输入成绩,直接回车结束: 95
输入成绩,直接回车结束: 62
输入成绩,直接回车结束: 40
输入成绩,直接回车结束:
----- 成绩统计 -----
数量: 4
总分: 277
平均分: 69.3
最高分: 95
最低分: 40错误输入要分别测试:
输入 abc:提示 成绩必须是整数
输入 120:提示 成绩必须在 0 到 100 之间
直接回车且没有成绩:提示 没有可统计的成绩你要能说清楚这 7 件事
- 数组长度固定,
List长度可以变化。 - 数组用
Length,List用Count。 - 第一个元素下标是
0。 - 最后一个元素下标是
Count - 1。 Add会把元素追加到List末尾。foreach适合逐个读取元素。- 访问
[0]前要确认集合不是空的。