
一句话概括数组是编译器为你划出的一排连号储物柜——快、稳、但一旦建成就不能扩建ListT则是会自动扩建的智能储物间。搞懂它们的取舍你才算真正会存数据。一、先从一个尴尬场景说起你要统计班里 5 名学生的成绩。新手最直观的写法是intscore188;intscore292;intscore375;intscore460;intscore595;求平均分(score1 score2 score3 score4 score5) / 5。如果班里有 50 人呢500 人呢难道要写 500 个变量这就是数组存在的意义用一个名字管理一整排同类型的数据。二、数组Array连号储物柜2.1 它是什么数组是长度固定、元素类型相同、在内存中连续存放的数据结构。想象一排编号从 0 开始的储物柜索引: [0] [1] [2] [3] [4] 值: 88 92 75 60 95每个柜子只能放同一种类型的东西柜子总数创建时就定死了通过**编号索引**直接开柜速度极快O(1)2.2 四种创建方式// ① 指定长度元素为默认值int 默认 0int[]anewint[5];// ② 声明 初始化int[]bnewint[]{88,92,75,60,95};// ③ 简写最常用int[]scores{88,92,75,60,95};// ④ C# 12 集合表达式int[]c[88,92,75,60,95];2.2 读取、修改与遍历int[]scores{88,92,75,60,95};Console.WriteLine(scores[0]);// 88索引从 0 开始scores[3]65;// 修改Console.WriteLine(scores.Length);// 5// 遍历只读用 foreach需要索引用 forforeach(intsinscores)Console.Write(s );for(inti0;iscores.Length;i)Console.WriteLine($第{i1}位同学{scores[i]}分);2.3 现代 C# 的两个糖^与..int[]scores{88,92,75,60,95};Console.WriteLine(scores[^1]);// 95倒数第 1 个Console.WriteLine(scores[^2]);// 60倒数第 2 个int[]middlescores[1..4];// 取索引 1 到 3含头不含尾Console.WriteLine(string.Join(,,middle));// 92,75,60提示对数组使用..切片会复制出一个新数组而不是原数组的视图。2.4Array类自带的实用工具int[]scores{88,92,75,60,95};Array.Sort(scores);// 升序60,75,88,92,95Array.Reverse(scores);// 反转95,92,88,75,60intidxArray.IndexOf(scores,88);// 查找返回索引找不到返回 -1boolhasArray.Exists(scores,ss60);// 是否存在不及格int[]copy(int[])scores.Clone();// 浅拷贝Array.Clear(scores,0,scores.Length);// 全部清零2.5 多维数组与交错数组// 矩形数组3 行 4 列每行一样长int[,]gridnewint[3,4];grid[1,2]99;Console.WriteLine(grid.GetLength(0));// 3行数Console.WriteLine(grid.GetLength(1));// 4列数// 交错数组数组的数组每行长度可以不同int[][]jaggednewint[3][];jagged[0]newint[]{1};jagged[1]newint[]{2,3};jagged[2]newint[]{4,5,6};Console.WriteLine(jagged[2][1]);// 5类型语法特点矩形数组int[,]每行等长像 Excel 表格交错数组int[][]每行可不等长更灵活通常性能更好三、实战演示班级成绩分析器我们用纯数组完成一个小项目输入一批成绩输出最高分、最低分、平均分、排名和分数段分布。usingSystem;classProgram{staticvoidMain(){string[]names{小明,小红,小刚,小美,小强,小丽};int[]scores{88,92,75,59,95,67};// ---------- 1. 基础统计 ----------intmaxscores[0],minscores[0],sum0;intmaxIdx0,minIdx0;for(inti0;iscores.Length;i){sumscores[i];if(scores[i]max){maxscores[i];maxIdxi;}if(scores[i]min){minscores[i];minIdxi;}}doubleavg(double)sum/scores.Length;Console.WriteLine( 成绩概览 );Console.WriteLine($最高分{names[maxIdx]}{max}分);Console.WriteLine($最低分{names[minIdx]}{min}分);Console.WriteLine($平均分{avg:F1}分);// ---------- 2. 分数段统计用数组做计数器 ----------// 索引含义0不及格 160-79 280-89 390以上int[]bandsnewint[4];foreach(intsinscores){if(s60)bands[0];elseif(s80)bands[1];elseif(s90)bands[2];elsebands[3];}string[]bandNames{不及格,60-79 ,80-89 ,90以上};Console.WriteLine(\n 分数段分布 );for(inti0;ibands.Length;i)Console.WriteLine(${bandNames[i]}|{newstring(■,bands[i])}{bands[i]}人);// ---------- 3. 排名成绩与姓名同步排序 ----------// Array.Sort(keys, items)按 keys 排序同时移动 itemsint[]sortedScores(int[])scores.Clone();string[]sortedNames(string[])names.Clone();Array.Sort(sortedScores,sortedNames);// 升序Array.Reverse(sortedScores);Array.Reverse(sortedNames);// 变成降序Console.WriteLine(\n 排行榜 );for(inti0;isortedScores.Length;i)Console.WriteLine($第{i1}名{sortedNames[i]}{sortedScores[i]}分);}}运行结果 成绩概览 最高分小强 95 分 最低分小美 59 分 平均分79.3 分 分数段分布 不及格 | ■ 1 人 60-79 | ■■ 2 人 80-89 | ■ 1 人 90以上 | ■■ 2 人 排行榜 第 1 名小强 95 分 第 2 名小红 92 分 第 3 名小明 88 分 第 4 名小刚 75 分 第 5 名小丽 67 分 第 6 名小美 59 分这个例子里有个很典型的技巧用数组下标本身来表达含义bands的索引对应分数段这是数组最优雅的用法之一。四、数组的天花板为什么我们需要 List回头看上面的代码你有没有嗅到几个不对劲的地方名字和成绩是两个平行数组靠下标相同来对应。一旦排序时漏掉同步数据就错位了。长度写死。如果新转来一位同学你得新建更大的数组、把旧数据一个个搬过去。想删除某位同学数组没有Remove只能手动挪位置。这些痛点一个是数据建模问题应该用类把姓名和成绩绑在一起一个是容量问题应该用能自动伸缩的集合。答案就是List类。五、ListT会自动扩建的智能储物间5.1 它是什么ListT是System.Collections.Generic命名空间下的泛型动态列表。T是你要存放的类型可以是int、string更常见的是你自己定义的类底层依然是数组但由框架帮你管理扩容支持增、删、查、排序等一整套方法5.2 它是怎么自动扩建的varlistnewListint();Console.WriteLine(list.Capacity);// 0list.Add(1);Console.WriteLine(list.Capacity);// 4for(inti0;i4;i)list.Add(i);Console.WriteLine(list.Capacity);// 8装不下时容量翻倍原理当元素个数超出容量ListT会新建一个容量翻倍的内部数组把旧数据复制过去。性能小技巧如果你事先大概知道会放多少元素直接指定容量可以避免反复扩容var list new Listint(10000);5.3 重头戏List类真实项目中我们几乎不会存一堆孤立的int而是存对象。先把姓名与成绩封装成一个类publicclassStudent{publicintId{get;}publicstringName{get;}publicintScore{get;set;}publicStudent(intid,stringname,intscore){Idid;Namename;Scorescore;}publicoverridestringToString()$[{Id}]{Name}{Score}分;}然后一个ListStudent就替代了之前的两个平行数组usingSystem;usingSystem.Collections.Generic;usingSystem.Linq;classProgram{staticvoidMain(){// 1. 创建并初始化varstudentsnewListStudent{newStudent(1,小明,88),newStudent(2,小红,92),newStudent(3,小刚,75),newStudent(4,小美,59),};// 2. 新同学转入直接 Add无需操心容量students.Add(newStudent(5,小强,95));// 3. 查找Student?targetstudents.Find(ss.Name小红);Console.WriteLine($找到{target});// 4. 修改List 里存的是对象引用改对象就是改列表里的数据if(target!null)target.Score5;// 5. 删除批量删除不及格的同学intremovedstudents.RemoveAll(ss.Score60);Console.WriteLine($移除了{removed}位不及格同学);// 6. 排序按成绩降序students.Sort((a,b)b.Score.CompareTo(a.Score));Console.WriteLine(\n 排行榜 );for(inti0;istudents.Count;i)// 注意List 用 Count数组用 LengthConsole.WriteLine($第{i1}名{students[i]});// 7. 配合 LINQ统计变得极其简单Console.WriteLine($\n平均分{students.Average(ss.Score):F1});Console.WriteLine($最高分{students.Max(ss.Score)});Console.WriteLine(90 分以上string.Join(、,students.Where(ss.Score90).Select(ss.Name)));}}运行结果找到[2] 小红92 分 移除了 1 位不及格同学 排行榜 第 1 名 [2] 小红97 分 第 2 名 [5] 小强95 分 第 3 名 [1] 小明88 分 第 4 名 [3] 小刚75 分 平均分88.8 最高分97 90 分以上小红、小强对比一下之前的数组版本没有平行数组没有手动搬运没有下标错位排序、查找、删除都是一行代码的事。5.4ListT常用方法速查方法 / 属性作用示例Add(item)末尾添加list.Add(stu)AddRange(items)批量添加list.AddRange(others)Insert(i, item)在指定位置插入list.Insert(0, stu)Remove(item)删除第一个匹配项list.Remove(stu)RemoveAt(i)按索引删除list.RemoveAt(2)RemoveAll(pred)按条件批量删除list.RemoveAll(s s.Score 60)Find/FindAll查找单个 / 全部list.FindAll(s s.Score 90)Contains(item)是否包含list.Contains(stu)Sort(comparison)原地排序list.Sort((a, b) ...)Count当前元素个数list.CountClear()清空list.Clear()ToArray()转回数组var arr list.ToArray();六、数组 vsListT到底该选谁对比维度数组T[]ListT长度固定创建后不可变动态自动扩容长度属性LengthCount增删元素不支持需手动搬运内置Add/Remove等多维支持原生支持[,]需嵌套ListListT性能极致无额外开销略有开销扩容、边界封装典型场景数据量固定、追求性能、底层 API数据量不确定、需要频繁增删选择口诀数量固定不变如一周 7 天、棋盘 8×8、RGB 三通道→数组数量会变、需要增删查改 →ListT拿不准先用ListT性能瓶颈出现时再优化七、高手都踩过的 5 个坑坑 1IndexOutOfRangeException越界int[]arrnewint[5];arr[5]1;// ❌ 有效索引是 0~4遍历时用i arr.Length而不是i arr.Length。ListT越界则抛出ArgumentOutOfRangeException。坑 2数组赋值是共享不是复制int[]a{1,2,3};int[]ba;// b 和 a 指向同一块内存b[0]99;Console.WriteLine(a[0]);// 99 int[]c(int[])a.Clone();// ✅ 真正复制坑 3foreach遍历时修改List会直接抛异常foreach(varsinstudents){if(s.Score60)students.Remove(s);// ❌ InvalidOperationException集合已修改}// ✅ 正确做法students.RemoveAll(ss.Score60);坑 4List类的拷贝只是浅拷贝varcopynewListStudent(students);// 新列表但里面装的仍是同一批对象copy[0].Score0;// students[0] 的分数也变成 0新列表只复制了引用对象本身是共享的。需要独立数据必须逐个克隆对象。坑 5忘记判断Find的返回值可能为nullvarstustudents.Find(ss.Name不存在的人);Console.WriteLine(stu.Score);// ❌ NullReferenceExceptionif(stu!null){/* ✅ 先判空 */}八、写在最后数组与ListT的关系就像自行车与汽车数组简单、轻快、没有多余零件适合固定路线ListT功能齐全、自动挡适合复杂多变的日常通勤。而用类封装数据再用List类管理对象几乎是每一个 C# 业务系统的基本骨架——订单列表、用户列表、商品列表无一例外。掌握这套组合拳你就跨过了会写代码到会建模的第一道门槛。 课后练习用二维数组int[,]实现一个 3×3 井字棋的棋盘打印。给Student增加ClassName属性用ListStudent配合 LINQ 的GroupBy统计每个班的平均分。写一个方法实现深拷贝一个ListStudent并验证修改副本不会影响原列表。觉得有收获欢迎收藏并转发给那个还在用 5 个变量存 5 个成绩的朋友