C# 学习总结:从泛型与集合到IO、多线程
摘要:本文是 C# 进阶学习笔记,梳理了这几天学习的泛型、集合、异常处理、IO 流与多线程五大知识模块。文章从「类型复用与安全」「数据组织与存储」「错误处理」「数据持久化」「并发执行」五个方向出发,结合老师布置的作业案例和实际踩过的坑,帮助初学者建立知识之间的联系,而非简单罗列 API。全文包含大量可运行的代码示例和易错点提醒,适合正在学习 C# 基础和进阶语法的同学参考。
一、前言:这几天在学什么
C# 学习内容从基础语法转向了更接近实际开发的知识。简单说,这几天围绕几个核心问题展开:如何让一段代码适用于不同类型(泛型),如何组织和存储数据(集合),程序出错时怎么办(异常处理),如何把数据保存到磁盘(IO),以及如何让程序同时做多件事(多线程)。下面按这个逻辑逐个整理,每个部分都会结合老师布置的作业案例,把知识点落到实处。
二、泛型:写完一次,适用多种类型
2.1 为什么需要泛型
写 C# 代码时经常遇到这样的场景:一个方法或类只因为参数类型不同,就要写好几份几乎一样的代码。用object虽然能暂时解决问题,但会带来两个麻烦:一是需要频繁装箱拆箱,影响性能;二是类型不安全,运行时才报错。泛型的核心作用就是让「类型」变成可以传递的参数,编译时就能确定类型,既保证类型安全,又省去重复代码。
举个例子:如果用ArrayList(非泛型集合)存数据,存进去都是object,取出来必须强制转换。一旦类型不匹配,编译器不会报错,运行时直接抛InvalidCastException——这类 bug 排查起来很费时间。泛型集合List<T>在编译阶段就锁定了类型,根本不会出现这种问题。
2.2 泛型类与泛型方法
泛型类在定义时使用类型参数T,实例化时再指定具体类型。泛型方法同理,可以在方法级别定义独立的类型参数。除此之外还有泛型接口和泛型委托,思想一致:把类型的选择权推迟到使用时。下面是一个简单的泛型容器示例:
// 定义一个简单的泛型容器 class MyBox<T> { public T Value { get; set; } public void Show() { Console.WriteLine($"值:{Value}"); } } // 使用 MyBox<int> intBox = new MyBox<int> { Value = 100 }; intBox.Show(); // 输出:值:100 MyBox<string> strBox = new MyBox<string> { Value = "Hello" }; strBox.Show(); // 输出:值:Hello同一个MyBox<T>定义,传int就存整数,传string就存字符串,不需要为每种类型重复写类。T就是一个占位符,具体类型在实例化时填入。
2.3 泛型约束
有时类型参数不能是任意类型,比如要求T必须是引用类型,或必须有无参构造函数。这时用泛型约束:
where T : class——约束为引用类型where T : struct——约束为值类型where T : new()——要求有无参构造函数where T : 基类名——约束为指定基类或其子类where T : 接口名——约束为实现了指定接口的类型
合理使用约束能让泛型代码更安全、更可控。比如一个泛型方法需要在内部创建实例,就必须加new()约束,否则编译不通过。
三、异常处理:程序出错了怎么应对
3.1 异常的本质
程序运行时难免遇到意料之外的情况:访问空对象(NullReferenceException)、数组越界(IndexOutOfRangeException)、除数为零(DivideByZeroException)、参数格式不对(FormatException)。这些问题发生时,.NET 会抛出一个异常对象(Exception),如果代码里不做任何处理,程序直接崩溃。异常处理的目的不是让错误不发生,而是让代码在出错时有一个体面的反应。
3.2 try-catch-finally 的执行逻辑
基本结构是try-catch-finally:try块包裹可能出错的代码;catch块捕获并处理特定类型的异常;finally块中的代码无论是否抛出异常都会执行,适合放释放文件句柄、关闭数据库连接等清理逻辑。多重catch时要注意顺序:具体异常写前面,Exception放最后,否则编译器会报错。下面是一个安全除法的例子:
try { Console.Write("输入被除数:"); int a = int.Parse(Console.ReadLine()); Console.Write("输入除数:"); int b = int.Parse(Console.ReadLine()); Console.WriteLine($"结果:{a / b}"); } catch (DivideByZeroException) { Console.WriteLine("除数不能为零。"); } catch (FormatException) { Console.WriteLine("请输入有效的整数。"); } catch (Exception ex) { Console.WriteLine($"发生未知错误:{ex.Message}"); } finally { Console.WriteLine("计算结束。"); }finally块里的「计算结束」无论成功与否都会打印,这一点在文件操作中特别有用——确保文件句柄在出错时也能被释放。
3.3 主动抛出异常
除了被动捕获,也可以用throw主动抛出异常。比如在方法参数校验不通过时抛出ArgumentException,让调用方明确感知到问题所在。作业里常见的场景是:参数为 null 或超出合法范围时,主动throw new ArgumentException("参数不合法"),而不是让程序带着错误数据继续跑。
四、集合:各种数据容器的选择
4.1 数组与 ArrayList 的对比
数组长度固定,定义后不能动态增删元素,适合事先知道数据量的场景。用Length获取长度。下面是一个典型作业示例:输入 5 个数字,计算总和和平均值:
int[] nums = new int[5]; int sum = 0; for (int i = 0; i < nums.Length; i++) { Console.Write($"请输入第{i + 1}个数字:"); nums[i] = int.Parse(Console.ReadLine()); sum += nums[i]; } Console.WriteLine($"总和:{sum},平均值:{sum / 5.0:F1}");ArrayList可以动态扩容,容量随元素增减自动调整。但它本质上保存的是object,可以存放不同类型,取出来时需要显式类型转换,否则编译通过但运行时报错。下面用ArrayList重写上面的求和逻辑,并加入字符串操作和查找删除:
// 用 ArrayList 保存数字并计算总和 ArrayList list = new ArrayList(); int sum = 0; for (int i = 0; i < 5; i++) { Console.Write($"请输入第{i + 1}个数字:"); int num = int.Parse(Console.ReadLine()); list.Add(num); sum += num; } Console.WriteLine($"总和:{sum},平均值:{sum / 5.0:F1}"); // 用 ArrayList 保存字符串,按索引读取并循环遍历 ArrayList names = new ArrayList() { "张三", "李四", "王五" }; for (int i = 0; i < names.Count; i++) { // 取出元素时需要类型转换 string name = (string)names[i]; Console.WriteLine($"第{i}个姓名:{name}"); } // 根据姓名查找索引,找到后删除 string target = "李四"; int index = names.IndexOf(target); // 查找对象在集合中的索引 if (index != -1) { names.RemoveAt(index); // 按索引删除 Console.WriteLine($"{target} 已从集合中删除。"); } else { Console.WriteLine($"未找到 {target}"); }这段代码涉及ArrayList的多个常用操作:Add添加、Insert插入到指定位置、Remove删除对象、RemoveAt按索引删除、Clear清空、Count获取元素个数、IndexOf查找索引。实际开发中,新项目应优先使用泛型集合List<T>,原因有两个:一是类型安全,不需要手动类型转换;二是避免了值类型的装箱拆箱开销。
4.2 Queue<T> 与 Stack<T>
Queue<T>(队列)是先进先出(FIFO),像排队:Enqueue入队,Dequeue出队。Stack<T>(栈)是后进先出(LIFO),像一摞盘子:Push入栈,Pop出栈。Peek只查看顶部/队首元素但不移除。队列适合任务排队、消息处理;栈适合撤销(Ctrl+Z)、浏览器的前进后退、括号匹配等场景。常用操作速查:
| 操作 | Queue<T>(FIFO) | Stack<T>(LIFO) | 说明 |
|---|---|---|---|
| 添加元素 | Enqueue(item) | Push(item) | 将元素放入集合 |
| 取出并移除 | Dequeue() | Pop() | 取出后元素不再存在 |
| 只查看不移除 | Peek() | Peek() | 查看但不改变集合 |
| 元素个数 | Count | Count | 获取集合中的元素数量 |
4.3 Dictionary<TKey, TValue> 与 SortedList
Dictionary<TKey, TValue>是键值对结构,通过唯一 Key 快速查找 Value,查找速度接近 O(1)。SortedList<TKey, TValue>在键值对基础上保持按键自动排序,适合需要有序访问的场景,但插入性能略低于 Dictionary。
常用方法速查:Add(key, value)新增键值对(Key 重复会抛异常);dict[key] = value索引器可用于新增或修改;ContainsKey(key)判断键是否存在;TryGetValue(key, out value)安全读取;Remove(key)删除键值对;foreach (var kv in dict)遍历KeyValuePair<TKey, TValue>。
下面用一个统计单词次数的示例串起字典的基本操作:
string text = "apple banana apple orange banana apple"; string[] words = text.Split(' '); Dictionary<string, int> dict = new Dictionary<string, int>(); foreach (string w in words) { if (dict.ContainsKey(w)) dict[w]++; // 键已存在,次数加一 else dict[w] = 1; // 键不存在,新增条目 } foreach (var kv in dict) Console.WriteLine($"{kv.Key} 出现 {kv.Value} 次");这里ContainsKey先判断再操作,避免了 Key 重复时Add抛异常的问题。
4.4 作业案例:字典的几个典型场景
老师在作业中设计了几个非常实用的字典应用场景,这里挑典型来说:
场景一:网站收藏夹。网站名称作为 Key,URL 作为 Value,查找后可以用Process.Start(url)打开网页:
Dictionary<string, string> favorites = new Dictionary<string, string>(); favorites["百度"] = "https://www.baidu.com"; favorites["GitHub"] = "https://github.com"; // 安全查找并打开 if (favorites.TryGetValue("GitHub", out string url)) { Console.WriteLine($"即将打开:{url}"); // System.Diagnostics.Process.Start(url); // 实际打开网页 } else { Console.WriteLine("收藏夹中没有该网站。"); }场景二:城市与邮编。城市已存在时修改邮编,否则新增城市。这种「存在则更新、不存在则新增」的逻辑用索引器最方便:
Dictionary<string, string> cities = new Dictionary<string, string>(); string city = "北京"; string zipCode = "100000"; // 索引器:存在则覆盖,不存在则新增 cities[city] = zipCode; Console.WriteLine($"{city} 的邮编已更新为 {zipCode}");场景三:学生管理。用Dictionary<string, Student>保存学号和学生对象。学生类内部做属性校验,比如姓名长度限制、分数范围 0-100,外部通过学号快速查找学生并计算平均分:
class Student { private string name; private int score; public string Name { get { return name; } set { if (string.IsNullOrWhiteSpace(value) || value.Length > 10) throw new ArgumentException("姓名长度应在1-10之间。"); name = value; } } public int Score { get { return score; } set { if (value < 0 || value > 100) throw new ArgumentException("分数应在0-100之间。"); score = value; } } } // 使用 Dictionary<string, Student> students = new Dictionary<string, Student>(); students["S001"] = new Student { Name = "张三", Score = 85 }; students["S002"] = new Student { Name = "李四", Score = 92 }; double avg = students.Values.Average(s => s.Score); Console.WriteLine($"平均分:{avg:F1}");场景四:手机号状态管理。用Dictionary<string, bool>表示号码是否可用(true 可用,false 已占用)。选择成功后更新状态为 false,这种场景下字典比 List 更直观:
Dictionary<string, bool> phones = new Dictionary<string, bool>() { {"13800000001", true}, {"13800000002", true}, {"13800000003", false} // 已占用 }; string selected = "13800000001"; if (phones.ContainsKey(selected) && phones[selected]) { phones[selected] = false; Console.WriteLine($"{selected} 选择成功。"); } else { Console.WriteLine("号码不可用。"); }场景五:模拟数据表。用List<Dictionary<string, object>>模拟一张汽车数据表,每行是一个 Dictionary,列名是 Key,值用 object 存储不同类型:
List<Dictionary<string, object>> cars = new List<Dictionary<string, object>>(); cars.Add(new Dictionary<string, object>() { {"品牌", "比亚迪"}, {"型号", "汉EV"}, {"价格", 219800} }); cars.Add(new Dictionary<string, object>() { {"品牌", "特斯拉"}, {"型号", "Model 3"}, {"价格", 259900} }); // 遍历输出 foreach (var car in cars) Console.WriteLine($"{car["品牌"]} {car["型号"]},价格:{car["价格"]}元");4.5 易错点小结
集合这边我在作业中踩过的坑,整理如下:
- 索引从 0 开始,「第四个元素」对应索引 3,不是 4。
Remove删除对象,RemoveAt删除索引,两者不要混淆。如果ArrayList存的是整数,Remove(3)是删除值为 3 的元素,不是删除索引为 3 的元素。- 数组用
Length,集合用Count,不要交叉使用。 - ArrayList 取出元素后需要类型转换,泛型集合
List<T>不需要。 - Dictionary 的 Key 不能重复,重复使用
Add会抛异常。先ContainsKey判断或直接使用索引器修改。 - 判断存在后再取值,用
TryGetValue比直接索引器更安全,避免 Key 不存在时抛KeyNotFoundException。 Dictionary拼写不能写成Directory,后者是文件夹操作类,完全是两码事。- 使用
Dictionary<string, Student>时,要注意对象内部属性的封装和校验,不要让外部直接修改内部数据绕过校验逻辑。
五、文件与 IO:把数据存到磁盘上
5.1 路径、文件、目录:三个概念的区分
操作文件前先分清三个概念:路径(Path)描述文件或目录的位置,是一个字符串,Path类只处理路径字符串本身,不会直接创建物理文件;文件(File)是存储数据的实体;目录(Directory)是组织文件的容器。
常用类速查:
Path类:GetFileName(获取文件名)、GetExtension(获取扩展名)、GetDirectoryName(获取目录名)、GetFullPath(获取完整路径)、Combine(安全拼接路径,比手动拼反斜杠更可靠)。File类:静态方法,适合一次性操作,如Create、Copy、Move、Delete、ReadAllText、WriteAllText、Exists。Directory类:静态方法,适合文件夹的CreateDirectory、Delete、GetFiles、GetDirectories。FileInfo/DirectoryInfo:面向实例的版本,适合需要多次操作同一文件或目录的场景,可以获取文件大小等属性。
有一个典型的作业题是:根据输入路径复制文件到 D 盘,文件不存在时输出提示:
Console.Write("请输入源文件路径:"); string sourcePath = Console.ReadLine(); string destPath = Path.Combine(@"D:\", Path.GetFileName(sourcePath)); if (File.Exists(sourcePath)) { File.Copy(sourcePath, destPath, overwrite: true); Console.WriteLine($"文件已复制到 {destPath}"); } else { Console.WriteLine("源文件不存在,请检查路径。"); }这里用了两个重要的习惯:复制前先File.Exists判断;用Path.Combine拼接路径而不是手动拼反斜杠,避免漏斜杠或重复斜杠的问题。
5.2 字节流与字符流:从底层到封装
Stream 是一段连续的数据流,带有一个当前位置指针。从底层到高层,IO 流有几个层次:
FileStream:按字节读写,适合处理所有文件类型(图片、视频、文本都可以),需要配合FileMode(打开方式)、FileAccess(访问权限)、FileShare(共享方式)使用。BufferedStream:给流加一层缓冲区,减少直接磁盘读写次数,提升性能。MemoryStream:操作内存中的流,数据存在内存而非磁盘。StreamReader/StreamWriter:在字节流基础上封装了字符编码处理,适合读写文本文件。结合using语句可以自动释放流资源。
下面是一个用FileStream读取文件并逐行输出的例子:
string path = @"D:\data\scores.txt"; if (File.Exists(path)) { using (FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read)) using (StreamReader sr = new StreamReader(fs, Encoding.UTF8)) { string line; while ((line = sr.ReadLine()) != null) Console.WriteLine(line); } } else { Console.WriteLine("文件不存在。"); }这里using块保证流在读写完成后被自动Dispose,Encoding.UTF8则确保中文等字符不会乱码。
5.3 作业案例:目录遍历与文件复制
作业中涉及不少目录操作,这里汇总两个典型场景:
遍历文件夹的子目录和文件:
string dirPath = @"D:\test"; if (Directory.Exists(dirPath)) { // 遍历子目录 foreach (string dir in Directory.GetDirectories(dirPath)) Console.WriteLine($"目录:{dir}"); // 遍历文件 foreach (string file in Directory.GetFiles(dirPath)) Console.WriteLine($"文件:{Path.GetFileName(file)}"); // 只遍历 txt 文件 foreach (string txtFile in Directory.GetFiles(dirPath, "*.txt")) Console.WriteLine($"TXT文件:{Path.GetFileName(txtFile)}"); }将文件夹中的文件复制到另一个目录,只复制目标目录中不存在的文件:
string sourceDir = @"D:\source"; string targetDir = @"D:\backup"; // 目标目录不存在则先创建 if (!Directory.Exists(targetDir)) Directory.CreateDirectory(targetDir); foreach (string file in Directory.GetFiles(sourceDir)) { string destFile = Path.Combine(targetDir, Path.GetFileName(file)); if (!File.Exists(destFile)) // 目标不存在才复制 { File.Copy(file, destFile); Console.WriteLine($"已复制:{Path.GetFileName(file)}"); } }这里的关键习惯:创建目录前判断Directory.Exists,复制前判断目标文件是否已存在,目标目录不存在时File.Copy会失败,必须先创建目录。
5.4 FileStream 循环读写的正确姿势
用FileStream.Read读取文件时,有一个重要细节:一次不一定读完整个文件,必须用循环读取,直到返回 0 表示读完。写入时也要用实际读取长度count,不能盲目写整个 buffer,否则可能把无效数据写入文件:
string sourcePath = @"D:\data\source.jpg"; string destPath = @"D:\data\copy.jpg"; using (FileStream fsRead = new FileStream(sourcePath, FileMode.Open, FileAccess.Read)) using (FileStream fsWrite = new FileStream(destPath, FileMode.Create, FileAccess.Write)) { byte[] buffer = new byte[4096]; int count; // 循环读取,直到返回 0 while ((count = fsRead.Read(buffer, 0, buffer.Length)) > 0) { fsWrite.Write(buffer, 0, count); // 使用实际读取长度 count,不是 buffer.Length } } Console.WriteLine("文件复制完成。");5.5 易错点小结
- 复制前先判断
File.Exists,创建目录前先判断Directory.Exists。 Path.Combine比手动拼反斜杠更安全,自动处理路径分隔符。FileStream.Read要循环读取,一次不一定读完整个文件,直到返回 0 才结束。- 写入时使用实际读取长度
count,不要写整个 buffer,否则尾部可能混入无效数据。 - 文件复制目标目录不存在时
File.Copy会失败,必须先创建目录。 - 处理中文文本时读取和写入编码必须一致,优先使用 UTF-8。
- 用
using或try-finally确保流被正确释放。
六、多线程:让程序同时做多件事
6.1 进程与线程
进程是资源分配的单位,线程是执行单位。一个进程至少包含一个线程(主线程),也可以包含多个工作线程。多线程让程序在「同一时间」处理多个任务,比如 UI 界面在响应用户点击的同时,后台还能下载文件。实际开发中,多线程适合独立计算、IO 等待、模拟多个用户访问等场景。
6.2 Thread 的基本操作与作业案例
使用System.Threading.Thread类创建线程,调用Start()才会真正启动线程,直接调用方法只是普通同步调用。Sleep让当前线程暂停指定毫秒数,Join等待指定线程执行完毕,IsAlive判断线程是否仍在运行。带参数的线程可以使用ParameterizedThreadStart委托。线程优先级(ThreadPriority)只影响调度概率,不保证绝对执行顺序。
先看一个简单示例,感受多线程的执行节奏:
Thread t = new Thread(() => { for (int i = 1; i <= 3; i++) { Console.WriteLine($"线程工作中:第{i}次"); Thread.Sleep(500); } }); t.Start(); t.Join(); // 等待线程结束,确保主线程在此阻塞 Console.WriteLine("线程工作完成。");没有Join()的话,主线程可能在子线程结束前就执行完毕,输出顺序会变得不可控。作业中典型的多线程场景有这几个:
场景一:两个线程分别计算 1 到 100 的和——每个线程独立计算,不涉及共享数据,线程安全不是问题:
int sum1 = 0, sum2 = 0; Thread t1 = new Thread(() => { for (int i = 1; i <= 50; i++) sum1 += i; }); Thread t2 = new Thread(() => { for (int i = 51; i <= 100; i++) sum2 += i; }); t1.Start(); t2.Start(); t1.Join(); t2.Join(); // 等两个线程都结束 Console.WriteLine($"总和:{sum1 + sum2}");场景二:将数组拆分,两个线程分别排序,最后合并。这个题目综合了排序算法和多线程协作:
int[] arr = { 9, 3, 7, 1, 5, 8, 2, 6, 4, 0 }; int mid = arr.Length / 2; int[] left = new int[mid]; int[] right = new int[arr.Length - mid]; Array.Copy(arr, 0, left, 0, mid); Array.Copy(arr, mid, right, 0, arr.Length - mid); Thread t1 = new Thread(() => Array.Sort(left)); Thread t2 = new Thread(() => Array.Sort(right)); t1.Start(); t2.Start(); t1.Join(); t2.Join(); // 必须等两个排序线程都结束再合并 // 双指针合并两个有序数组 int[] result = new int[arr.Length]; int i = 0, j = 0, k = 0; while (i < left.Length && j < right.Length) result[k++] = left[i] < right[j] ? left[i++] : right[j++]; while (i < left.Length) result[k++] = left[i++]; while (j < right.Length) result[k++] = right[j++]; Console.WriteLine($"排序结果:{string.Join(", ", result)}");这里有一个易错点:合并前必须等两个排序线程结束(Join),否则合并的是未排序的数组。
6.3 共享数据与 lock
多个线程同时读写同一个变量时可能出现竞态问题:一个线程改到一半,另一个线程也来读或改,最终结果不可预测。解决办法是用lock保护临界区,保证同一时刻只有一个线程进入被锁住的代码块。lock的参数通常是一个专用的private static readonly object对象,不要用this或字符串字面量。另外:线程开得越多不一定越快,线程创建、上下文切换和锁竞争都会产生开销,当任务本身是计算密集型时,盲目增加线程反而可能拖慢程序。还要注意:Random共享实例在多线程中可能产生竞争,实际项目应考虑使用线程安全的随机数方案。
七、综合案例:控制台版文件处理与数据统计
下面用一个完整的综合案例把本周的知识串联起来。这个案例模拟一个实际场景:从成绩文件中读取学生数据,用面向对象的方式组织数据,进行统计计算,并演示多线程并行处理:
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading; class Student { public string Name { get; set; } public int Score { get; set; } } class Program { static void Main() { string filePath = @"D:\data\scores.txt"; List<Student> students = new List<Student>(); Dictionary<string, int> gradeCount = new Dictionary<string, int>(); // 1. 用 IO 读取文件,用异常处理应对文件缺失和格式错误 try { using (StreamReader sr = new StreamReader(filePath, Encoding.UTF8)) { string line; while ((line = sr.ReadLine()) != null) { string[] parts = line.Split(','); if (parts.Length != 2) continue; string name = parts[0].Trim(); // TryParse 比 Parse 更健壮,格式错误不会抛异常 if (!int.TryParse(parts[1].Trim(), out int score)) continue; students.Add(new Student { Name = name, Score = score }); } } } catch (FileNotFoundException) { Console.WriteLine("未找到成绩文件,请检查路径。"); return; } finally { // finally 块确保资源相关的提示总是输出 Console.WriteLine("文件读取阶段结束。"); } if (students.Count == 0) { Console.WriteLine("没有读取到任何有效数据。"); return; } // 2. 用泛型方法计算统计值 double avgScore = CalcAverage(students); Student topStudent = FindMax(students); Console.WriteLine($"平均分:{avgScore:F1},最高分:{topStudent.Name} - {topStudent.Score}"); // 3. 用 Dictionary 统计各等级人数 foreach (var s in students) { string grade = s.Score >= 90 ? "优秀" : s.Score >= 80 ? "良好" : s.Score >= 60 ? "及格" : "不及格"; if (!gradeCount.ContainsKey(grade)) gradeCount[grade] = 0; gradeCount[grade]++; } Console.WriteLine("--- 等级分布 ---"); foreach (var kv in gradeCount) Console.WriteLine($"{kv.Key}:{kv.Value} 人"); // 4. 数据量大时,用多线程并行处理(演示双线程合并思路) int mid = students.Count / 2; double avgLeft = 0, avgRight = 0; Thread t1 = new Thread(() => avgLeft = CalcAverage(students.GetRange(0, mid))); Thread t2 = new Thread(() => avgRight = CalcAverage(students.GetRange(mid, students.Count - mid))); t1.Start(); t2.Start(); t1.Join(); t2.Join(); Console.WriteLine($"双线程分别计算平均分——前半:{avgLeft:F1},后半:{avgRight:F1}"); } // 泛型方法:计算平均分 static double CalcAverage(List<Student> list) { if (list.Count == 0) return 0; double sum = 0; foreach (var s in list) sum += s.Score; return sum / list.Count; } // 泛型方法:查找最高分学生 static Student FindMax(List<Student> list) { Student max = list[0]; foreach (var s in list) if (s.Score > max.Score) max = s; return max; } }知识串联说明:
- 泛型——
List<Student>存储学生列表,泛型方法CalcAverage和FindMax复用统计逻辑; - 集合——
Dictionary<string, int>统计等级人数,List<Student>配合GetRange拆分数据; - 异常处理——
try-catch-finally处理文件缺失和格式错误,TryParse替代Parse增强健壮性; - IO——
StreamReader+using+Encoding.UTF8按行读取文件; - 多线程——双线程并行计算两部分平均分,
Join等待结束后输出结果。
八、总结:本周学习的四条主线
经过这些内容的学习和作业的练习,如果只记住一堆 API 名称其实意义不大。真正值得记住的是四条主线:复用——泛型让代码写一次适用多种类型;稳定——异常处理让程序遇到错误时不会直接崩溃;存储——集合负责组织内存中的数据,IO 负责把数据持久化到磁盘;并发——多线程让多个任务同时推进。后续写项目时遇到问题,不妨回到这四条线上去找对应的知识点。
参考知识点
- C# 泛型编程(类型参数与约束)
- 集合类对比:Array / ArrayList / List<T> / Dictionary / Queue / Stack
- 异常处理模型:try-catch-finally 与 throw
- 文件与流操作:File / Directory / FileStream / StreamReader / StreamWriter
- 多线程基础:Thread / Sleep / Join / lock