☰
C#经典编程200例实战指南:四层学习法与避坑总结
2026/10/16 14:11:31 网站建设 项目流程

简介:C#经典编程200例(吉林明日出版社)是一本面向C#初学者与中级开发者的实例型学习资源,围绕实际开发中常用的语言特性展开,示例由浅入深,便于逐步进阶。资源内容覆盖基本语法、类与对象、接口、泛型、异常处理、文件操作、网络编程、LINQ、异步编程、事件与委托,以及Windows Forms/WPF桌面界面开发、ASP.NET Web应用和单元测试等核心主题,通过200个可运行的经典实例,帮助读者在练习中掌握C#的工程化用法,并能够迁移到桌面应用、Web开发或网络通信等真实场景。每一例都聚焦一个具体问题,配有完整的代码实现与思路讲解;资源按功能模块组织实例,既有简单的语法验证,也有贴近真实项目的小型程序,能让学习者边看边练,逐步增强编码手感与排错能力。该资源以RAR压缩包形式提供,包体大小41.25MB,下载后可直接解压使用。目前已有289人学习或下载,适合希望快速上手并需要系统刷题式练习的开发者,也可作为日常编码的参考手册。

1. 拿到《C#经典编程200例》前,先想清楚一个问题

打开这本《C#经典编程200例(吉林明日出版社)》的人,多半是照着目录从例1一路敲到例200。敲完你会得到一个熟悉的错觉:C#好像会了,可真让自己写个小工具时,大脑一片空白。我见过太多这样的翻车现场。这本书真正的价值不在那200个例子本身,而在例子背后藏着的一条成长路径:从单个语法点,到面向对象的思维方式,再到把数据、界面、文件串成完整程序的系统能力。把它当练习册抄,你只能收获手熟;把它当地图走,才能换来解决问题的本事。这篇文章就聊怎么走,哪些例子值得精做,哪些跑一遍就够,以及新环境下跑老代码最容易踩的五个坑。

2. 把200例拆成四层知识块:哪些该精读,哪些该跳过

2.1 为什么要把例程集按依赖关系重新分层

这类例程书通常按知识点铺排,前面是变量、分支、循环,中间是类和对象,后面是文件、数据库、多线程。直接从第1例顺序敲到第200例,最明显的问题是:敲到第80例时,前面50例的细节已经忘得差不多,而你自己并不知道哪些例子之间是有依赖关系的。

我一般建议把例程按依赖关系拆成四层,像下面这样:

层级覆盖主题典型例子类型建议投入
第一层:语法热身变量、分支、循环、数组、字符串水仙花数、九九乘法表、字符串反转一刷跑通即可,不要恋战
第二层:面向对象类、继承、接口、委托、事件图书类设计、简易计算器精做,改造成自己的类
第三层:界面交互控件、菜单、对话框、事件模型登录窗体、文本编辑器选做,重点是事件机制
第四层:数据与服务文件、数据库、网络、多线程学生信息管理、文件批量处理最值得反复做

这样拆的依据很简单:每一层都在为下一层提供零件。语法热身提供的是表达式和流程控制能力;面向对象提供的是组织代码的结构能力;界面交互让你理解事件驱动;数据与服务才是把这些东西串成一个真实系统的黏合剂。顺序敲的弊端在于,前面两层占了将近一半的例子,等你真正接触到文件读写和数据库时,精力已经耗得差不多了。

2.2 判断一个例子值不值得精做的三条标准

对所有例子一视同仁是另一种浪费。我判断一个例子要不要精做,只看三件事:

第一,它是否涉及了流程控制之外的东西——文件、网络、控件、数据库,只要沾上就算高价值。第二,能不能在半小时内把它改造成一个自己用得上或者面试能讲的小工具。第三,例子用的 API 在新环境里是否已经被替代,如果被替代了,正好拿来练迁移。

举个例子,书里大概率有个「数组排序」的入门例程,原始版本一般是:

int[] nums = { 5, 1, 4, 2, 3 }; Array.Sort(nums); foreach (int n in nums) { Console.Write(n + " "); }

这个例子只涉及数组和循环,按标准属于第一层的低价值例子,跑通就够了。但如果你把它改造成「从文件读取数字、排序后写回另一个文件」,它就同时覆盖了文件读写、异常容忍、集合转换三个新知识点:

// 改造:读文件里的数字,排序后写回另一个文件 var lines = File.ReadAllLines("input.txt"); // 按行读取全部文本 var list = new List<int>(); // 泛型集合,替代老书里的 ArrayList foreach (var line in lines) { // Trim 去掉首尾空白,TryParse 解析失败返回 false,自动跳过坏行 if (int.TryParse(line.Trim(), out int v)) { list.Add(v); } } list.Sort(); // 原地升序排序 File.WriteAllLines("output.txt", list.ConvertAll(x => x.ToString())); // 把 int 列表转成 string 列表再写文件

这段代码里TryParse的out int v是 C# 的内联变量声明,解析成功才把值赋给v;ConvertAll接收一个 lambda 表达式,把List<int>逐元素转成List<string>,这样WriteAllLines才能直接接收。改造之后,这个例子的价值立刻从第一层跳到第四层。判断标准反过来说就是:一个例子如果只能让你「看懂」,不能让你「改着玩」,那它就不值得投入第二遍。

2.3 按「先跑通、再改坏、后重写」的顺序刷例程

很多初学者刷例程的方式是照着敲,敲完运行,看到结果一致就翻下一页。这种刷法的问题在于,你看到的永远是正确的路径,而从没看过异常路径长什么样。我习惯用三步走:

第一步,跑通。照着代码敲一遍,保证编译和运行都正常,确认自己理解每个语句的作用。第二步,改坏。故意改一个地方,比如把循环边界改错,让程序抛异常,观察异常信息和堆栈。第三步,重写。合上书,凭记忆和思路重新实现一遍,不要求逐行一致,只要求功能等价。

改坏这一步很多人不敢做,其实它才是学得最快的一步。看下面这段:

int[] arr = { 10, 20, 30, 40 }; for (int i = 0; i <= arr.Length; i++) // 故意写成 <=,越界一次 { Console.WriteLine(arr[i]); }

运行后会抛出IndexOutOfRangeException,提示索引超出了数组界限。这时你去看调用堆栈,会发现异常发生在arr[i]这一行,并且能看到具体是哪个索引、数组长度是多少。亲眼看一次越界,比背十遍「数组下标从 0 开始」都记得牢。

这种刷法的额外收益是:你会顺便建立一套对异常信息的直觉。以后自己写代码出错,第一反应是看堆栈定位,而不是重新读一遍代码瞎猜。A同学当年就是靠这个笨办法,把异常处理从「看着头疼」变成了「日常工具」,面试时被问到一个数组越界的场景题,他直接说出了异常类型和定位思路。

3. 从零跑通第一组例子:环境搭建与三种项目类型的标准操作

3.1 环境怎么选:老书与新 SDK 之间怎么落地

这是所有照着老书练 C# 的人第一个要面对的现实问题:书里的代码大概率基于 .NET Framework 时代,用 Visual Studio 创建项目,默认跑在 Windows 上。而你现在装新 SDK,创建的项目默认是 .NET 8 或更高版本,两者在项目结构上有差异,但核心语法和大部分类库仍然兼容。

我建议两条路线:如果你主力机是 Windows,直接装 Visual Studio 社区版,安装时勾选「.NET 桌面开发」工作负载,这样既能建 .NET Framework 项目,也能建新 .NET 项目,WinForms 例子跑起来最省事。如果你主力机是 Linux 或 macOS,就用 .NET SDK 加 VS Code,控制台和数据库类例子完全能跑,窗体类例子只能在 Windows 上运行。装完后先验证环境:

dotnet --version # 查看 SDK 版本 dotnet new console -n Hello200 # 创建一个控制台项目 cd Hello200 dotnet run # 编译并运行

dotnet new console创建的项目自带一个Program.cs,里面是Console.WriteLine("Hello, World!");。dotnet run会自动还原 NuGet 依赖、编译并启动程序。这个流程跑通,说明环境没问题。

注意:WinForms 项目无法在 Linux 或 macOS 上用 .NET SDK 直接运行。你可以在非 Windows 系统上编写代码,但要调试窗体界面,仍然需要一台 Windows 机器。

3.2 控制台例子:最小可运行模板

控制台项目是刷例程性价比最高的载体,绝大部分语法、面向对象、文件和网络例子都能在控制台里跑。我建议先拿一个综合了数组、集合和 Linq 的例子练手,因为它能同时覆盖老书里多个单独例子的内容。下面这段是「合并两个数组并去重排序」:

using System; using System.Collections.Generic; using System.Linq; class Program { static void Main() { int[] a = { 3, 1, 4, 1, 5 }; int[] b = { 2, 6, 5, 3 }; // Concat:把两个序列首尾相接;Distinct:去重;OrderBy:升序排序 var merged = a.Concat(b).Distinct().OrderBy(x => x); Console.WriteLine(string.Join(" ", merged)); } }

输出结果是1 2 3 4 5 6。Concat不会修改原数组,而是返回一个新的可枚举序列;Distinct依赖元素的Equals方法判断重复;OrderBy是稳定的升序排列。这三个方法都是延迟执行的,真正遍历发生在string.Join里,这一点值得留意:Linq 的查询不会在定义时执行,而是在你开始枚举时才执行。

如果你想把这段代码改成老书里没有 Linq 时的写法,对比一下:

// 不用 Linq 的等价实现,兼容老编译环境 List<int> result = new List<int>(); foreach (int n in a) if (!result.Contains(n)) result.Add(n); foreach (int n in b) if (!result.Contains(n)) result.Add(n); result.Sort(); Console.WriteLine(string.Join(" ", result));

老写法用List.Contains判断重复,时间复杂度是 O(n²),但逻辑更直白。新手建议两种都写一遍,能明显感觉到 Linq 的声明式风格和命令式风格之间的差异。控制台例子里这类对比做多了,后面看别人代码时就不会被不同风格吓到。

3.3 窗体例子:把控制台逻辑搬进界面

书里的窗体例子占了相当比例,按钮、文本框、列表控件来回用。这类例子的核心不是控件本身,而是事件驱动模型:用户点击按钮时,系统触发一个事件,你写的事件处理方法被调用。创建一个 WinForms 项目:

dotnet new winforms -n Form200 cd Form200 dotnet run

模板会生成一个空白窗体。把下面代码写进Form1.cs,实现一个「输入一串数字,点击排序,结果列在列表里」的小工具:

using System; using System.Linq; using System.Windows.Forms; public class MainForm : Form { private TextBox inputBox; private Button runButton; private ListBox outputBox; public MainForm() { Text = "排序工具"; inputBox = new TextBox { Left = 20, Top = 20, Width = 260 }; runButton = new Button { Left = 290, Top = 18, Text = "排序" }; outputBox = new ListBox { Left = 20, Top = 60, Width = 360, Height = 240 }; runButton.Click += RunButton_Click; // 订阅按钮的点击事件 Controls.Add(inputBox); Controls.Add(runButton); Controls.Add(outputBox); } private void RunButton_Click(object sender, EventArgs e) { // 按空格拆分文本,TryParse 过滤掉非数字项 var nums = inputBox.Text.Split(' ') .Where(s => int.TryParse(s, out _)) .Select(int.Parse) .OrderBy(x => x) .ToArray(); outputBox.Items.Clear(); foreach (var n in nums) { outputBox.Items.Add(n); } } [STAThread] static void Main() { Application.Run(new MainForm()); } }

new TextBox { Left = 20, Top = 20, Width = 260 }是对象初始化器语法,等价于先new再逐个赋值。runButton.Click += RunButton_Click是订阅事件的标准姿势,Click事件要求方法签名是(object sender, EventArgs e),这是所有控件事件的统一模板。[STAThread]是 Windows 窗体程序必需的线程模型标记,缺失可能导致部分控件行为异常。

提示:老书里的按钮事件名叫button1_Click,因为控件拖到窗体上默认叫button1。新版里事件方法名可以随意取,只要签名正确。别被名字吓住,重点看签名。

3.4 数据例子:从「SQL Server 为主」切换到本地免配置数据库

老例程书里的数据库例子大多依赖 SQL Server,连接字符串写的是Server=localhost;Database=xxx;Uid=sa;Pwd=xxx。问题在于你本机大概率没装 SQL Server,装了服务也未必启动,第一个连接就会卡住。我的做法是:学习阶段把数据库层换成 SQLite,语法和 SQL 都不变,只是把连接对象从SqlConnection换成SqliteConnection。先装包:

dotnet add package Microsoft.Data.Sqlite

然后用下面这段建库、建表、插入、查询一次跑通:

using System; using Microsoft.Data.Sqlite; // Data Source 指定数据库文件,文件不存在时自动创建 using var conn = new SqliteConnection("Data Source=books.db"); conn.Open(); // 建表,IF NOT EXISTS 保证重复运行不报错 string createSql = @" CREATE TABLE IF NOT EXISTS books ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, price REAL ); "; using var createCmd = conn.CreateCommand(); createCmd.CommandText = createSql; createCmd.ExecuteNonQuery(); // 插入一条记录,参数化查询避免拼字符串 using var insertCmd = conn.CreateCommand(); insertCmd.CommandText = "INSERT INTO books (title, price) VALUES ($title, $price);"; insertCmd.Parameters.AddWithValue("$title", "C# 入门"); insertCmd.Parameters.AddWithValue("$price", 59.0); insertCmd.ExecuteNonQuery(); // 查询全部记录,Read 方法逐行读取结果集 using var queryCmd = conn.CreateCommand(); queryCmd.CommandText = "SELECT id, title, price FROM books;"; using var reader = queryCmd.ExecuteReader(); while (reader.Read()) { Console.WriteLine($"{reader.GetInt32(0)} {reader.GetString(1)} {reader.GetDouble(2)}"); }

Data Source=books.db里的books.db是相对路径,程序启动时在当前目录创建文件,如果写绝对路径则按绝对路径生效。ExecuteNonQuery用于执行不返回结果集的语句,ExecuteReader返回一个数据读取器,Read()每调用一次前进一行,返回false时表示结果集遍历完毕。参数名用$title、$price是 SQLite 的参数前缀约定,AddWithValue会把 .NET 值映射成 SQLite 能识别的类型。

方案连接字符串示例适用场景
老书 SQL ServerServer=localhost;Database=Books;Uid=sa;Pwd=123456公司项目,装有 SQL Server
LocalDBServer=(localdb)\MSSQLLocalDB;Database=Books;Integrated Security=trueWindows 本机练习,零配置
SQLiteData Source=books.db跨平台学习,文件即数据库

我建议你学习阶段直接选 SQLite,等理解表、查询、参数化这些概念后,再切换到真实 SQL Server 时只需要改连接对象和连接字符串,SQL 本身几乎不用动。

4. C# 200例实战避坑:老代码、命名空间和编译器报错的 5 个翻车现场

刷这类例程书,最大的成本不在例子的难度,而在环境差异带来的连环报错。下面这 5 个问题是我按出现频率排的,每一条都见过不止一个人卡住。

4.1 报错 CS0246:找不到类型或命名空间 System.Windows.Forms

现象:把书里的窗体代码复制到控制台项目里,编译报错CS0246 找不到类型或命名空间 System.Windows.Forms。

原因:书里的窗体例子需要 WinForms 类库支持,而控制台项目默认没有引用 Windows Forms,Form、Button、TextBox这些类型都不在编译范围内。

解决:不要再往控制台项目里硬塞窗体代码。窗体例子单独创建 WinForms 项目,即在项目目录执行dotnet new winforms。如果你在用 Visual Studio,新建项目时选「Windows 窗体应用」模板,包含的引用和启动配置都是对的。做完这一步,System.Windows.Forms的报错就消失了。

4.2 ArrayList 和 Hashtable 被 IDE 画波浪线:该换泛型了

现象:老书里大量使用ArrayList、Hashtable,你原样敲进去,代码能运行,但 IDE 会在这些类型下面画波浪线,给出建议。

原因:这些是非泛型集合,元素类型是object,存进去和取出来都要做类型转换,运行时才可能暴露类型错误。新 .NET 环境推荐使用List<T>和Dictionary<TKey, TValue>。

解决:遇到老例子里的非泛型集合,主动改写成泛型版本:

// 老写法:ArrayList 存整数也要装箱,取值要强转 ArrayList list = new ArrayList(); list.Add(10); list.Add("text"); // 编译能过,运行时可能爆雷 int x = (int)list[0]; // 每次取值都要强转 // 新写法:List<int> 只接受 int,错误在编译期暴露 List<int> nums = new List<int> { 10, 20 }; nums.Add(30); // nums.Add("text"); // 这行编译直接报错 int y = nums[0]; // 不需要强转

改写时注意:ArrayList对应的泛型版本是List<T>,Hashtable对应Dictionary<TKey, TValue>,Queue和Stack也有对应的泛型版本。这个迁移本身就是很好的练习,等于把书里的老代码升级成现代 C#。

4.3 数据库例子连不上服务器:本机压根没有 SQL Server

现象:照着书里的连接字符串写,conn.Open()抛异常,提示无法连接到服务器或网络相关错误。

原因:书里默认你装了 SQL Server,并且服务已启动。很多新电脑上根本没装这个服务,自然连不上。

解决:区分场景处理。如果你只是学习,把连接对象换成SqliteConnection,用Data Source=books.db本地文件库,完全不用装服务。如果你必须用 SQL Server 语法,Windows 上可以先启用 Visual Studio 自带的 LocalDB,连接字符串改成:

string connStr = @"Server=(localdb)\MSSQLLocalDB;Database=Books;Integrated Security=true;";

(localdb)\MSSQLLocalDB是 LocalDB 实例的固定名称,Integrated Security=true表示用当前 Windows 账户登录,不需要用户名密码。

注意:LocalDB 只在 Windows 上可用,且要求本机装有 Visual Studio 或 SQL Server Express 相关组件。其它平台直接用 SQLite 最省事。

4.4 Read 和 ReadLine 混用:输入被「吃掉」一半

现象:程序先Console.Read()读一个字符,后面再Console.ReadLine()读一行,结果ReadLine返回空字符串,好像输入被吃掉了。

原因:Read()只读取一个字符,回车换行的剩余部分留在输入缓冲区里,紧接着的ReadLine()会把残留的换行符读走,直接返回空行。

解决:统一使用Console.ReadLine()读取整行,然后再解析想要的字符:

Console.WriteLine("请输入一个字符:"); string input = Console.ReadLine() ?? ""; // 防止用户在交互窗口直接回车 int code = string.IsNullOrEmpty(input) ? 0 : input[0]; Console.WriteLine($"ASCII 码:{code}");

这样不管用户输入多少个字符,都能完整拿到。如果坚持用Read(),要在它后面补一个Console.ReadLine()把缓冲区的换行消费掉,但这容易忘,不好维护。

4.5 控制台中文变成乱码:文件编码和代码页打架

现象:书里的字符串是中文,比如Console.WriteLine("欢迎使用本系统");,运行时输出一串乱码。

原因:老书代码可能以 GB2312 或 GBK 编码保存,而新版 .NET 控制台默认输出 UTF-8,编码不一致就会出现乱码。这和 Dreamweaver 时代的 Web 页面乱码是同一个道理,当年大家称之为玄学,其实就是编码映射不一致。

解决:在静态Main方法开头强制指定输出编码:

Console.OutputEncoding = System.Text.Encoding.UTF8;

如果是 Windows 上的 cmd,还可以先执行chcp 65001切到 UTF-8 代码页。另外,现代编辑器默认 UTF-8 保存文件,只要代码文件本身是 UTF-8,再设置输出编码,乱码就能解决。如果文件已经是旧编码,用任何编辑器另存为 UTF-8 再重新编译即可。

5. 把三五个小例子拼成一个工具箱:检验你是否真读懂的唯一方法

刷完 200 例之后,检验水平的唯一标准不是记住多少 API,而是能不能把互不相干的例子组合成一个真实工具。我推荐一个具体练法:从不同章节里挑三个例子,文件读取、数组排序、字符串处理,拼成一个「文本清理器」。这个工具读入一个文本文件,去掉空行和注释行,把连续空白压缩成单个空格,再给每行标上行号,写到新文件。

using System; using System.IO; using System.Text.RegularExpressions; using System.Collections.Generic; class TextCleaner { static void Main(string[] args) { if (args.Length < 1) { Console.WriteLine("用法:TextCleaner <文件路径>"); return; } string path = args[0]; string[] lines = File.ReadAllLines(path); // 读取全部行 var cleaned = new List<string>(); for (int i = 0; i < lines.Length; i++) { string line = lines[i].Trim(); // 去掉首尾空白 if (line.Length == 0) continue; // 跳过空行 if (line.StartsWith("#")) continue; // 跳过注释行 line = Regex.Replace(line, @"\s+", " ");// 合并连续空白为单个空格 cleaned.Add($"{i + 1}: {line}"); // 加行号 } File.WriteAllLines(path + ".clean", cleaned); Console.WriteLine($"处理完成,保留 {cleaned.Count} 行"); } }

验证方法:准备一个几百行的测试文件,故意混入空行、以#开头的注释行、夹着多个制表符和空格的行,运行后检查输出的.clean文件。行号是否正确、空行是否真的没了、连续空白是否被合并,每一条都能用肉眼核对。再用一个只有几行的小文件验证边界,比如第一行就是空行、最后一行没有换行符。

提示:验证时先用小样本,别直接拿生产数据试。等逻辑确定没问题,再放到真实文件上跑。

我见过太多人把 200 例从头到尾敲完,结果连这样一个几十行的工具都组装不起来,原因就是每个例子都是孤立的零件,从没想过让它们配合。我现在拿到这类例程书,第一件事不是翻目录,而是先找三五个互不相关的例子,想清楚怎么让它们合作。这个习惯帮我避开了不少白费功夫的刷题式学习。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询