简介:《C#经典编程200例》是一套面向C#入门与进阶学习者的编程实例合集,适合通过动手练习掌握从基础语法到项目开发的完整知识链。压缩包约41.25MB,文件总数与类型明细暂未提供,但不影响直接解压使用。目前已有289人学习下载。
内容覆盖变量、运算符、流程控制等基础语法,类与对象、接口、泛型等核心面向对象概念,并延伸至异常处理、文件操作、网络编程、LINQ查询、异步编程、事件与委托等实用技术,同时涉及Windows Forms、WPF、ASP.NET及单元测试等应用方向。每个实例围绕实际编程问题展开,配有清晰的问题描述与实现思路,能够帮助读者边练边学,逐步建立代码调试与排错能力。
对于希望系统提升C#实战水平、准备面试或完成课程作业的开发者,这套200例素材可作为随身练习册,反复演练以巩固知识并应对实际开发场景。
1. 这本C#经典编程200例,为什么老代码反而值得刷
如果你手头的工作是维护一套跑了七八年的老系统,翻开代码仓库那一刻,大概率会撞见这样的写法:DataTable 里逐行取数拼字符串、ArrayList 装着对象再强转、事件处理器里塞了三百行业务逻辑。这些代码和现在的 .NET 规范格格不入,但系统稳定运行至今,没人敢乱动。这时候你就明白,C#经典编程200例这类习题集练出来的不是优雅语法,而是读懂烂代码、改得动烂代码的底气。
这本书在初学阶段的价值被严重低估了。它不像现代教程那样讲依赖注入、async/await、LINQ 表达式,它用两百个短小精悍的例子把语言底子打牢:类型转换、循环边界、字符串操作、文件读写、集合增删。这些恰恰是刷题网站和框架教程都不愿意细讲的部分。对两类人最有用:一是刚学完语法、想用大量小练习巩固肌肉记忆的新手;二是需要接手遗留系统、每天面对老式 C# 代码的开发者。我的建议很直接:别管书龄多老,买一本来,用现代环境把每个例子亲手敲一遍,收获比看十遍教程都大。
2. 搭建重编译环境:让旧示例在 .NET 8 下复活
2.1 老代码为什么一跑就报错:框架差异的三个断面
那本两百例成书年代对应的技术栈,通常是 .NET Framework 2.0 到 3.5,C# 语言版本停留在 2.0 或 3.0。而你现在机器上大概率装的是 .NET 6 或 8,语言版本已经到 C# 12。这不是简单的“升级一下项目文件”就能解决的事,三个断面决定了你至少有一个下午要花在环境调试上。
第一个断面是 API 移除与过时。老书里常见的HttpWebRequest写法在 .NET Core 之后仍然能用,但被标记为过时;更麻烦的是System.Drawing相关的绘图例子,在非 Windows 平台上直接运行时,行为完全不同。第二个断面是控制台编码。老代码直接Console.WriteLine("中文")在旧框架下输出正常,在 .NET 8 的控制台里可能变成乱码,这和 .NET Core 默认 UTF-8 编码策略有关。第三个断面是项目结构。 .NET Framework 时代一个 .csproj 文件里写死所有编译选项,现在 SDK-style 项目用通配符自动包含文件,配置方式完全不同。
这意味着你不能天真地把书里的代码复制进新项目就指望跑通。我一般会先建一个统一的控制台项目,再逐个章节把例子拆成独立方法,用入口点转发调用。这样哪些例子能跑、哪些报错,一目了然。
2.2 最小化环境搭建:从 SDK 到第一个编译通过
先装 SDK,推荐装 LTS 版本,比如 .NET 8。装完验证命令,然后新建一个控制台项目作为两百例的统一宿主:
dotnet --version mkdir CSharp200Examples cd CSharp200Examples dotnet new console -n CSharp200 -f net8.0 cd CSharp200创建完成后,打开CSharp200.csproj,会看到当前采用 SDK-style 格式,默认包含了目录下所有 .cs 文件。这比老式项目手动逐个添加文件要省事得多。接着处理一个关键问题:让老代码里的中文和编码行为可预期。我习惯在最开始就加几行:
using System.Text; // 让 Windows 控制台正确显示中文 Console.OutputEncoding = Encoding.UTF8; Console.InputEncoding = Encoding.UTF8;放在Program.cs的入口最前面。注意Console.OutputEncoding和Console.InputEncoding两个都要设置,只设输出会导致从控制台读中文时出现奇怪字符。这个设置在 .NET 8 的 Windows 终端下表现稳定,在老式 cmd 窗口下也基本兼容。
接下来处理老代码中的中文读取问题。很多经典例子涉及读取本机文本文件并统计字符,老书里默认文本文件是 ANSI/GB2312 编码,而 .NET Core 默认只带 UTF-8 和 Unicode。解决方式是注册编码提供程序,我给你一个完整的可运行片段:
using System; using System.Text; using System.IO; class Program { static void Main() { // 注册代码页编码提供程序,GB2312、GBK 等老编码才可用 Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); string path = @"C:\temp\example.txt"; if (!File.Exists(path)) { File.WriteAllText(path, "你好,世界", Encoding.UTF8); } // 以 GB2312 读取老式文本文件 string content = File.ReadAllText(path, Encoding.GetEncoding("GB2312")); Console.WriteLine(content); } }这段代码里,Encoding.RegisterProvider必须在使用任何非 Unicode 编码前调用。Encoding.GetEncoding("GB2312")在注册了 Provider 之后才能解析到正确代码页。File.ReadAllText的第二个参数指定读取编码,缺省时 .NET 会尝试 BOM 检测,没有 BOM 则默认 UTF-8,这正是老文件乱码的根因。
2.3 批量编译策略:一个入口点调度两百个例子
把两百个例题都塞进一个解决方案里逐个建项目,会把自己累死,而且编译速度慢得让人想放弃。我常用的做法是:把书分成几个大块——语法基础、算法逻辑、字符串处理、文件操作、面向对象、界面相关。在宿主项目里按块建静态类,每个例子对应一个静态方法,入口点用switch或反射机制调用。
using System; class Program { static void Main(string[] args) { if (args.Length == 0) { Console.WriteLine("用法: dotnet run -- <章节id> <例题id>"); Console.WriteLine("示例: dotnet run -- chapter3 15"); return; } string chapter = args[0]; int exampleId = int.Parse(args[1]); switch (chapter) { case "chapter3": Chapter3.RunExample(exampleId); break; case "chapter4": Chapter4.RunExample(exampleId); break; default: Console.WriteLine($"未知章节: {chapter}"); break; } } }这种调度方式的好处是:编译一次,反复运行单个例题不必重启整个项目;出错的例题能单独定位,互不干扰。args[0]对应章节标识,args[1]对应例题编号,在Chapter3.RunExample内部再用嵌套 switch 分发。这里的参数设计完全是个人偏好,你也可以改成枚举或字典映射,但核心思路不变:一个宿主项目管所有题,而不是两百个项目各管各的。
3. 拆解两百例的结构:题型分布与现代视角下的取舍
3.1 经典题型地图:六类题目的训练侧重
老式习题集不像现代在线题库那样按难度标签分类,但页面上题目的排布是有内在逻辑的。按我自己的经验,两百例大致可以分成六个阵营。第一类是语法基础题,覆盖变量声明、类型转换、运算符优先级、分支与循环,这部分大概占四分之一,是整本书的地基。第二类是数组与集合操作,包括一维二维数组的遍历、排序、插入删除。第三类是字符串与正则处理,包括字符统计、子串查找、敏感词替换。第四类是文件与目录操作,读写文本、遍历目录、复制移动。第五类是面向对象三大特性及接口、委托和事件,这类题在两百例里开始变难,也最接近真实项目。第六类是界面与绘图,在老书里通常是最后几章,但现在参考价值明显下降。
这张题图决定了刷题顺序:不要从第一页读到最后一页,而是按专题横向打透。比如今天只做文件操作相关的十道题,把所有读写、路径拼接、异常处理的套路都过一遍,比一天刷三十道语法题有效得多。原因很简单,语法题的重复度太高,而文件操作这类涉及资源管理的题目,边界情况多,每一道都能暴露新的问题。
3.2 哪些题至今有价值,哪些可以跳过
我的观点很明确:算法和文件操作类题目完全不过时,面向对象设计题要带着批判眼光做,图形界面题直接跳过。这不代表界面题毫无意义,只是当年那些控件拖拽、事件绑定的操作在现在的主流开发里已经被替代,花时间重复没有回报。
真正让我觉得宝刀不老的是字符串处理题。书里那些“统计一段英文里每个字母出现的次数”“判断一个字符串是否是回文”“提取所有邮箱地址”的题,放到今天依然是面试常见的白板题原型。它们的共同点是:不依赖任何框架特性,纯粹考语言功底和逻辑思维,这恰恰是最难被替代的能力。
另一类被低估的是“内存中集合操作”的题目,比如“把两个有序数组合并成一个有序数组”。这类题当年要求手写循环比较大小,现在用 LINQ 一行搞定,但我建议你仍然老实地用循环写一遍。原因和性能无关,而是手写一遍你才能理解OrderBy背后做了什么。真实项目里遇到特殊排序规则时,不会 LINQ 照样能写,可依赖 LINQ 的人脱离了框架就手足无措。
3.3 题型分布的隐形逻辑:每章之间的依赖关系
读这本两百例时,一个容易被忽略的现象是:前一章的例题会用到后一章才会正式讲解的特性。比如数组排序的题里可能提前出现Array.Sort,虽然当时没讲过,但答案是查出来的。这不是作者编排失误,而是刻意的“预埋”。
我建议按这个链路刷:语法基础 → 字符串操作 → 数组与集合 → 文件与目录 → 面向对象 → 综合应用。中间任何一环没打通,后面遇到相关例题会卡壳。特别是字符串和数组,它们在文件操作题里是基础工具,如果这两个板块没练熟,做文件题时会同时面临语法和业务逻辑双重难题,排错时很难定位是哪个环节出了问题。
4. 三类高频题的现代改造:从两百例到能跑的代码
4.1 排序题:老式冒泡 vs 现代泛型扩展
排序题几乎在每个版本的两百例里都存在,典型描述是“输入 N 个整数,按从小到大排序”。老书的标准答案通常是这样:
int[] arr = { 5, 3, 8, 1, 9, 2 }; for (int i = 0; i < arr.Length - 1; i++) { for (int j = 0; j < arr.Length - 1 - i; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } foreach (int num in arr) { Console.Write(num + " "); }这段代码的逻辑本身没毛病,两层循环,外循环控制轮数,内循环做相邻比较和交换。arr.Length - 1 - i是内循环边界,因为每轮过后最大的数已经沉底,无需再比较。真正的问题在于,这个写法只能处理int[],一旦需要排序string[]或自定义对象数组,就必须重写一份。
我的改造建议是把它封装成泛型扩展方法,顺便加上比较器参数,适配现代代码风格:
public static class SortExtensions { public static void BubbleSort<T>(this T[] array, Comparison<T> comparison) { if (array == null) throw new ArgumentNullException(nameof(array)); if (comparison == null) throw new ArgumentNullException(nameof(comparison)); for (int i = 0; i < array.Length - 1; i++) { for (int j = 0; j < array.Length - 1 - i; j++) { if (comparison(array[j], array[j + 1]) > 0) { (array[j], array[j + 1]) = (array[j + 1], array[j]); } } } } }改造后调用方式变成了arr.BubbleSort((a, b) => a.CompareTo(b)),Comparison<T>委托让排序规则变得完全可控。这里的元组交换语法(a, b) = (b, a)是 C# 7.0 之后的特性,老写法是临时变量接三步交换。两种都行,但元组交换在代码审查时更易读。注意保留空参数校验,书里的老代码不具备这种习惯,但实际工程里这是基本素养。
4.2 文件遍历题:递归到迭代,避开访问拒绝的坑
文件操作题里有一道经典:给定目录路径,打印出下面所有子目录和文件名。老书里最常见的实现是递归调用,逻辑干净但遇到无权限访问的子目录时,整个程序直接崩溃。我当时刷到这道题时踩过一次,报错是UnauthorizedAccessException,当时一脸懵。
我的方案是递归加异常捕获,同时区分文件系统错误和业务错误:
using System; using System.IO; using System.Collections.Generic; public class DirectoryWalker { public static List<string> GetAllFiles(string root, string searchPattern = "*") { var result = new List<string>(); WalkDirectory(new DirectoryInfo(root), result, searchPattern); return result; } private static void WalkDirectory(DirectoryInfo dir, List<string> results, string pattern) { FileInfo[] files; try { files = dir.GetFiles(pattern); } catch (UnauthorizedAccessException) { Console.WriteLine($"跳过无权限目录: {dir.FullName}"); return; } catch (DirectoryNotFoundException) { Console.WriteLine($"目录不存在或已被删除: {dir.FullName}"); return; } foreach (var file in files) { results.Add(file.FullName); } DirectoryInfo[] subDirs; try { subDirs = dir.GetDirectories(); } catch (UnauthorizedAccessException) { Console.WriteLine($"跳过无权限子目录: {dir.FullName}"); return; } foreach (var subDir in subDirs) { WalkDirectory(subDir, results, pattern); } } }GetFiles和GetDirectories分别放在独立的try-catch里,这个细节很重要。因为一个目录可能文件可以读取但子目录没有访问权限,单独捕获能保证文件列表仍然可用,只是跳过无权限的子目录。searchPattern参数支持通配符,比如*.txt或report?.doc,用法上和其他 API 保持一致。这个例子跑出来的结果列表可以用于后续的批量改名、批量统计行数等操作,落地价值远比单纯的打印要大。
4.3 字符串频次统计:从 ArrayList 到 Dictionary 的进化
老书里统计词频的题,当年用了很多ArrayList存键值对再手工比较,代码冗长且容易出错。现代写法用Dictionary<TKey, TValue>一步到位,不仅代码量少一个数量级,性能也更好。
using System; using System.Collections.Generic; using System.Linq; public static class TextStatistics { public static Dictionary<char, int> CountCharacters(string input) { if (input == null) throw new ArgumentNullException(nameof(input)); var counter = new Dictionary<char, int>(); foreach (char c in input) { if (char.IsWhiteSpace(c)) continue; if (counter.ContainsKey(c)) { counter[c]++; } else { counter.Add(c, 1); } } return counter; } } // 使用示例 string text = "hello world hello csharp"; var stats = CountCharacters(text); foreach (var kvp in stats.OrderByDescending(p => p.Value)) { Console.WriteLine($"字符 '{kvp.Key}' 出现 {kvp.Value} 次"); }这里的counter[c]++依赖 Dictionary 索引器的特性:键存在时返回当前值并支持赋值,键不存在时直接索引会抛出异常,所以必须先ContainsKey判断。我见过不少新手试图用counter[c]++省掉判断,结果运行时异常,这就是对集合行为不熟的典型表现。char.IsWhiteSpace过滤掉空格、换行等空白字符,让统计结果更聚焦实际内容。最后的OrderByDescending(p => p.Value)来自 LINQ,不改变原字典,返回新的排序序列,这在展示场景下非常实用。
5. 刷两百例的常见翻车点与排查:五个真实踩坑记录
5.1 编译报错 CS0619
现象:老书里的Console.ReadLine()或某些System.Windows.Forms相关语句在新项目里编译时,编译器提示“已过时”并报错。
原因:.NET 8 中部分 API 被标记为[Obsolete],编译器默认把过时警告提升为错误,尤其是涉及安全或替代方案明确的 API。
解决:第一选择是查找替代 API,比如WebClient换成HttpClient。第二选择是实在需要保留老语法时,在文件顶部加#pragma warning disable CS0619,只作用于当前文件。我建议优先做迁移,pragma 是最后手段,因为压制警告会让后续代码审查失去一个信号。
5.2 控制台中文输出乱码
现象:Console.WriteLine("你好")输出变成浣犲ソ之类的乱码。
原因:.NET Core 进程默认输出编码是 UTF-8,而旧版 Windows 控制台默认代码页可能是 GBK。两者不一致导致输出字节被错误解读。
解决:在入口最前面设置Console.OutputEncoding = Encoding.UTF8;。如果你在 Windows Terminal 下运行,这行就足够了;如果仍跑在经典 cmd 里,可以改成Console.OutputEncoding = Encoding.GetEncoding("GB2312");,但前提是先Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)。
5.3 System.Drawing 绘图代码异常或界面空白
现象:书里用System.Drawing画的图形,在 Windows 上还能显示,在 Linux/macOS 上跑直接抛异常,或者画出来是空的。
原因:System.Drawing.Common从 .NET 6 开始仅支持 Windows,其他平台上报平台不支持异常。这和 GDI+ 的底层实现有关。
解决:如果只是练习绘图逻辑,把目标运行时限制为 Windows:在 csproj 里加<RuntimeIdentifier>win-x64</RuntimeIdentifier>。如果想跨平台,换用图像处理库如 SkiaSharp,但接口和思维模型完全不同。我的建议很简单,跳过所有界面绘图题,把时间留给算法和文件处理。
5.4 File.ReadAllText 读老文本文件出现多余字符
现象:读取一个 GB2312 编码的 txt 文件,中文正常但开头多了三个字符。
原因:这三个字符是 UTF-8 BOM 的字节序列被错误地按 GB2312 解码显示。文件实际是 UTF-8 带 BOM,而你用File.ReadAllText默认编码去读时发生了什么取决于 .NET 版本和检测逻辑。
解决:统一指定编码读取,不要依赖自动检测。先确认文件编码再决定参数,File.ReadAllText(path, Encoding.GetEncoding("GB2312"))。如果文件有 BOM,.NET 的自动检测其实能正确处理,乱码通常发生在无 BOM 的 UTF-8 文件被按其他编码读的情况。排查思路是先用十六进制查看器看文件头几个字节,再决定编码参数。
5.5 项目文件被自动通配符包含导致重复编译
现象:两百例代码文件全丢进项目目录后,编译报大量“类型已存在”错误。
原因:SDK-style 项目默认启用EnableDefaultCompileItems,会把项目目录下所有 .cs 文件自动加入编译。如果书自带示例代码和你的练习代码同名类,就冲突了。
解决:在 csproj 里显式关闭默认编译项,改为手工指定需要的文件。这样做的副作用是后续新增 .cs 文件都要手动编辑 csproj,管理成本上升。另一种做法是保持默认,但严格控制代码文件命名不重复。我个人习惯是保留默认,把老书自带代码放legacy子目录,并在 csproj 里排除该目录:
<ItemGroup> <Compile Remove="legacy/**" /> </ItemGroup>这个配置写在<Project>节点内即可,排除了整个legacy目录,避免脏文件干扰主项目编译。
6. 把两百例做成自己的工具箱:三个实测有用的改造方向
练习题和真实需求之间隔着一层翻译功夫。书里的例题是孤立的,而现实需求总是组合型的。我在刷到三分之一时开始尝试改造,把每个例题往“能复用的工具”方向拧。最成功的三个改造分别是批量文件改名器、日志文件统计器、通用排序扩展库。
批量文件改名器脱胎于“遍历目录文件并打印”那道题。我加了一个规则参数,比如把文件名里的[tmp]替换成日期,或者统一给所有文件加前缀。核心逻辑仍然是遍历 + 字符串替换,但包装成交互式命令行工具后,在日常工作中处理照片归类、下载文件整理时非常顺手。日志统计器来自字符频次统计题,我把输入从单个字符串换成日志文件,统计每个错误等级出现的次数,按频率排序输出。后来加了简单的过滤条件,比如只看某段时间的日志,几百行就搞定,够用。
通用排序扩展库就是前面 4.1 那段的泛型方法,我收集了七八种排序算法的实现,统一封装成扩展方法,全部无依赖。虽然工程上很少手写排序,但这段代码在面试讲解算法时很有说服力。做这些改造时有两点经验:一是不要贪多,一道题对应一个实际场景就够了,为改造而改造会浪费时间;二是一定要在自己真实遇到过的问题里选题材,否则做完就没有维护的动力。
我自己曾经犯过一个错:把书里所有例题都改成现代语法后,就觉得自己掌握了,结果在项目里遇到老代码仍然一筹莫展。原因是改写的过程中我是在“翻译”,不是在做“思考”。真正让我能力发生质变的转折点,是放弃查看参考答案,凭记忆重写关键逻辑,写不出来再回去翻书。这个过程非常痛苦,但每一道凭记忆写出来的题,都成了真正属于自己的肌肉记忆。
刷两百例的正确姿势不是“看完”,而是“复写”。第一遍对着书敲代码理解逻辑,第二遍合上书凭记忆写,第三遍尝试改造。三遍下来,一本老书的价值会被挖掘得透透的。环境问题、编码问题、API 过时问题,这些都是技术债,但解决它们本身也是重要的实战训练。希望帮到你。
本文还有配套的精品资源,点击获取