简介:面向C#初学者的“C#100例”是一份以100个编程实例串联基础语法、面向对象、集合、异常处理、LINQ、异步编程、多线程、反射、Windows Forms/WPF及网络编程等核心模块的教程资源,旨在帮助零基础读者从基本语法逐步过渡到实际项目开发,建立扎实的编程思维。资源包共1717个文件,压缩后约9.65MB,主要包括224个cs源码、82个sln解决方案、167个exe示例程序、97个resx资源文件以及135个ico图标和163张jpg图片;sln可直接打开对应工程,exe可运行观察效果,cs代码便于逐行对照调试,整体按示例编号组织,检索方便。目前已有490人学习/下载。每个示例覆盖一个独立知识点,并配有完整的工程结构与界面资源;读者可先运行exe感受程序行为,再阅读源代码理解实现原理,最后修改代码验证自己的猜想。从控制台应用到桌面UI、从同步到异步、从单机到网络通信,覆盖不同层次的学习路径;相比纯理论教程,这种“运行—阅读—修改”的闭环方式能显著提升学习效率,适合C#自学入门、课堂教学配套练习或考前集中刷题使用。
1. C#入门为什么绕不开100例:从语法到能写工具的最近路径
很多想学C#的朋友一开始满网找教程,市面上的C#教程要么厚得像字典,要么讲半天环境搭建还没敲到第一行代码。我自己的经验是,C#上手最快的方式不是从头到尾读语法,而是跟着一套覆盖面全的实例集,一边敲一边理解。这个C#100例资源就是这样的东西——100个可直接运行的例子,从控制台语法、WinForms界面、文件操作到数据库访问,几乎把初学者前三个月会碰到的知识点全占了。适合完全零基础的人按顺序练,也适合写过一点但没做过完整项目的人快速补短板,更合适把例子改造成自己小工具的上班族。
2. 先看资源结构:100个例子的分类逻辑与学习路线
2.1 三类划分:语法、界面、进阶应用
拿到这套资源的第一步,不是打开第一个例子,而是翻目录。100个例子如果按编号一个个刷,很容易在某个无聊的语法例子上耗尽耐心。我习惯先把它们分成三类:
| 分类 | 数量占比 | 典型内容 | 建议用时 |
|---|---|---|---|
| 基础语法 | 约30% | 变量、运算符、循环、数组、方法 | 1-2周 |
| WinForms界面 | 约40% | 窗体控件、事件、菜单、对话框 | 2-3周 |
| 进阶应用 | 约30% | 文件、正则、数据库、多线程、GDI+ | 3-4周 |
这个划分不需要太精确,重要的是理解三类例子各自的定位。基础语法类是积木,单个例子小,跑起来快,重点是把"怎么写"变成条件反射。资源里前十几个例子基本是控制台输出和变量运算,看着简单,但手速和语感就是靠这些重复练出来的。
WinForms类是模型房,每一个例子都是一间能住的房间,开始涉及控件属性、事件委托和界面刷新这些真实开发里的基本概念。到这一阶段,代码量会突然增加,一个窗体初始化就是十几行,不用怕,设计器会自动生成,关键看事件方法里写的业务逻辑。
进阶应用类是装修队,文件读写和数据库访问是后面所有小工具的地基。这一块有少数例子开始涉及多线程和网络,相对难啃,但不用每个都吃透,先把文件、数据库、正则这三个高频场景学会,剩下的等真用到再回头看。
三类之间不是严格隔离的。比如"读取文本文件"这个例子,既可以用控制台程序演示,也可以做成一个带"打开文件"对话框的窗体程序。我一般建议看例子标题时多看一眼它用的项目类型,同一个功能在控制台和窗体里实现,需要额外处理的边界完全不同。控制台版本只需要搞清楚流的方向,窗体版本还要考虑用户选文件、进度显示、界面释放这些额外因素。
2.2 学习顺序:控制台先行,别被界面骗
很多初学者一上来挑WinForms的例子,因为看着有窗口、有按钮,感觉自己在做软件。结果控件属性、事件委托、界面布局三个概念同时涌过来,很容易在第一个窗体的设计器里迷路,代码逻辑还没跑通,先被一堆可视化操作的细节淹没了。最常见的翻车方式是照着视频把控件拖好、属性设好,代码部分反而没理解,最后关掉视频自己写就卡住。
我的排序建议是严格走"控制台 → WinForms → 数据库+文件"三步。控制台阶段把基本语法过完,重点关注三类高频问题:值类型和引用类型的区别、字符串拼接与格式化、循环里break和continue的作用。这三个问题在100个例子里会反复出现,在控制台阶段理解透了,后面无论做窗体还是做服务都很顺手。
WinForms阶段的核心不是记住每个控件的属性和方法,而是抓住"事件驱动"这个模型。界面上放一个按钮,双击它就能生成一个事件处理方法,这个方法的签名是固定的(object sender, EventArgs e),理解了这个签名,大部分控件事件都能照着写。再往后看定时器、列表选择变化、鼠标键盘事件,其实都是同一个套路,只是触发时机不同。
数据库和文件操作放在最后,是因为它们依赖前面的界面知识。用DataGridView绑定数据时,需要先知道数据表长什么样;做登录窗口时,需要先会写文本框的事件。把顺序反过来,每一步都要回头补前面的概念,学习曲线会陡很多,而且容易产生"控件事件还没搞懂,怎么又来一堆数据库对象"的挫败感。
时间分配上,我建议每天做三到五个例子,每个例子做完不要急着看下一个,先在原代码基础上改一个需求。比如例子是把一行文本输出到窗体,改成在多个文本框里按格式拆分输出;比如例子是读取文本文件并显示行数,改成读取文本文件并按行倒序显示。语法没变,但思考量上去了,这是从"看懂"到"会用"的分水岭。
2.3 环境准备:项目类型和框架版本怎么选
这套资源里的例子大多基于较老的开发环境编写,默认项目模板是".NET Framework的Windows窗体应用"。现在用新版IDE新建项目时,对话框里会同时出现"Windows窗体应用"和"Windows窗体应用(.NET Framework)"两个模板,前者用的是.NET Core/.NET 5+,后者才是老例子能直接跑起来的那个。
我的建议是:练习阶段一律选".NET Framework"模板,目标框架选4.x。这样有两个明显的好处:第一,老例子里的命名空间和控件引用方式不用改,编译通过率高;第二,这个资源对应的答疑和资料大多基于老框架,排查报错时更容易匹配上。如果机器上只有新版环境,也可以把老代码迁移过去,但部分例子中System.Drawing、System.Data的引用方式会有细微差别,新手容易卡在"命名空间不存在"这类报错上。
注意:判断例子的目标框架,最直接的方法是看项目文件里有没有packages.config,或者打开项目的属性页看"目标框架"下拉框。老例子里常见的TargetFrameworkVersion值,在新环境里不一定默认支持。
老例子解压出来一般是一个个单独的文件夹,每个文件夹对应一个完整项目,里面是.csproj文件、Properties目录和Program.cs。直接用新版IDE打开.csproj,通常会提示做一次提升转换,点确定即可。但有些例子里引用了绝对路径的DLL,提升后会编译不过,这时要手动加引用。如果对项目文件不熟,建议先挑一个最简单的例子走通整个打开流程,再批量处理其他例子。
环境这一步看着跟写代码无关,却是"照着敲却编译不过"的头号原因。项目类型选错,后面每一步都在跟IDE较劲,而不是在学C#。选对了模板,再打开第一个例子,双击运行,能看到一个窗体弹出来,这个正反馈比什么激励都管用。
3. 核心例子逐行拆解:从窗体到数据库的四个实战原型
3.1 第一个窗体程序:按钮、事件与MessageBox
资源里第一个WinForms例子通常非常简单:窗体上放一个按钮,点击弹出"Hello World"消息框。这个例子虽然短,却能把"事件驱动"这个概念讲透,也为后面所有带界面的例子打底。
namespace Example001 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } // 按钮点击事件:把一句话显示到Label控件上 private void BtnShow_Click(object sender, EventArgs e) { lblResult.Text = "你好,C#100例!"; MessageBox.Show("点击事件已触发", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); } } }代码里的逻辑分三段。构造函数负责把设计器里生成的控件初始化代码跑起来,这段不用手写,但要知道它存在。事件处理方法BtnShow_Click是按钮被点击时自动调用的回调,方法名左边的private表示这个方法只在类内部使用,右边的void表示没有返回值。方法内部两行:第一行给Label赋值,第二行弹出一个标准消息框。
这里需要理解的关键点是sender和e这两个参数。sender代表触发事件的控件本身,类型是object,使用时需要先转换成Button才能访问按钮的其他属性;e携带事件相关数据,对于点击事件来说通常是空的。当同一个窗体上有多个按钮共用一个事件方法时,可以通过((Button)sender).Name来区分是哪个按钮被点了,这是老例子里非常常见的判分方式。
3.2 文件读写:StreamReader与编码问题
文件操作是100例里最实用的一类,几乎每个小工具都离不开。资源里的例子通常会同时演示写入和读取两个方向,顺便把using语句、编码、路径三个知识点串在一起。
// 写入文件:StreamWriter,追加模式 string logPath = Path.Combine(Application.StartupPath, "log.txt"); using (StreamWriter writer = new StreamWriter(logPath, true, Encoding.UTF8)) { writer.WriteLine($"{DateTime.Now:yyyy-MM-dd HH:mm:ss} 登录成功"); } // 读取文件:按行读取到List<string> List<string> lines = new List<string>(); using (StreamReader reader = new StreamReader(logPath, Encoding.UTF8)) { string line; while ((line = reader.ReadLine()) != null) { lines.Add(line); } }用using语句包住StreamWriter和StreamReader是这套例子里反复出现的标准姿势,核心目的是确保文件句柄在代码块结束时自动释放。不释放文件句柄的后果是文件被占用,下次写入时抛"文件正由另一进程使用"的异常,这个错在Windows上特别常见。
构造函数里的三个参数分别是文件路径、是否追加写入、编码格式。追加模式传true时,内容是接在文件末尾;传false时会覆盖原文件。编码参数值得单独说,老例子喜欢用系统默认编码,但在简体中文系统上,处理带中文的旧文件时用Encoding.GetEncoding("GB2312")读更稳,自己写新文件时统一用UTF8,避免在不同机器上出现乱码。
文件路径这里埋了一个知识点:Application.StartupPath拿的是程序exe所在的目录,用Path.Combine拼接路径比手工加斜杠安全,因为不用考虑路径分隔符在不同系统上的写法差异。如果文件在项目目录而不是输出目录,运行时路径会不同,这个边界经常让新手困惑。
3.3 字符串与正则:从文本里提取数据的通用套路
第40个左右的例子开始接触正则表达式,这是很多人第一次觉得C#难的地方。资源里的典型场景是从一段混合文本中提取电话号码,看起来简单,但写出来的pattern对新手很有冲击力。
string rawText = "联系人:张三,电话:13800138000,备用:010-12345678"; string pattern = @"1[3-9]\d{9}|\d{3,4}-\d{7,8}"; foreach (Match match in Regex.Matches(rawText, pattern)) { Console.WriteLine($"找到号码:{match.Value}"); } Console.WriteLine($"共找到 {Regex.Matches(rawText, pattern).Count} 个号码");正则表达式的学习最忌背符号,建议把pattern当成一个"迷你语言"来读。1[3-9]\d{9}表示第一位是1,第二位是3到9,后面跟着9个数字,这是手机号;\d{3,4}-\d{7,8}表示区号加座机号,其中\d{3,4}表示三到四位数字,{7,8}同理。中间的|是或者关系,两个分支可以匹配不同形态的号码。C#里用@前缀声明字符串,反斜杠不用转义,写\d就是\d,这比在普通字符串里写\d省心很多。
如果只想取第一个匹配,用Regex.Match;如果想替换,用Regex.Replace。这三个方法在参数上高度一致,第一个参数都是原始文本,第二个都是pattern,掌握一个就基本掌握了三个。实际开发里正则用得最多的是数据清洗场景,比如从日志文件、报表文本里把时间、IP、错误级别提取出来,再落到数据表里。常见踩坑点是正则引擎默认的贪婪匹配,遇到"提取字符串中两个引号之间的内容"这类需求时,容易把中间的所有内容都吞掉,遇到这种情况先查一下贪婪和懒惰匹配的区别。
3.4 ADO.NET数据绑定:DataGridView与数据库表
数据库相关例子的里程碑是"把一张表显示到DataGridView里"。这个例子统一了后面所有数据管理类小工具的写法,也是初学者第一次接触到分层的数据访问对象。
string connStr = "Data Source=localhost;Initial Catalog=DemoDB;User ID=sa;Password=123456"; string sql = "SELECT Id, UserName, CreateTime FROM Users"; using (SqlConnection conn = new SqlConnection(connStr)) { SqlDataAdapter adapter = new SqlDataAdapter(sql, conn); DataTable dt = new DataTable(); adapter.Fill(dt); dataGridView1.DataSource = dt; }这段代码的核心路径是:SqlConnection负责建立连接,SqlDataAdapter把SQL查询结果填充到内存里的DataTable,最后把DataTable直接赋给DataGridView的DataSource属性。三个对象各管一段,连接、查询、展示彻底分开,这是老例子为了教学故意拆开讲的。新手最常犯的错是把SqlConnection、SqlCommand、SqlDataReader三个对象混着用,其实展示场景下DataAdapter已经封装了查询和填充细节,代码量更少也不容易遗漏释放操作。
连接字符串可以看成一套键值对,分号分隔。Data Source是数据库服务器地址,localhost表示本机;Initial Catalog是库名;User ID和Password是登录凭据。如果是本机且用Windows身份验证,可以把后两段改成Integrated Security=True,避免把密码硬编码在代码里。connect超时这个值默认15秒,网络不稳时可以加Connect Timeout=30。
注意:SqlConnection和SqlDataAdapter都实现了IDisposable,用using包住它们是最稳妥的资源释放方式。老例子里有手工Close的写法,一旦中间抛出异常,Close可能不会执行到,这个问题在实际项目里会造成连接泄漏。
4. 经典算法与逻辑陷阱:从排序、递归到LINQ
4.1 冒泡排序的C#实现:从数组到泛型List
100例里排序是基本盘。冒泡排序在教科书里是最直观的排序算法,但在C#里实现时有一个容易被忽略的细节:数组的Length属性到List的Count属性,遍历边界写法不一样。
List<int> numbers = new List<int> { 5, 3, 8, 1, 9, 2 }; for (int i = 0; i < numbers.Count - 1; i++) { bool swapped = false; for (int j = 0; j < numbers.Count - 1 - i; j++) { if (numbers[j] > numbers[j + 1]) { // C# 7.0 元组交换,老框架请换成三行temp写法 (numbers[j], numbers[j + 1]) = (numbers[j + 1], numbers[j]); swapped = true; } } if (!swapped) break; // 某一轮没发生交换,说明已有序 }注意内层循环的边界:j < numbers.Count - 1 - i。每完成一轮,最大的数已经沉到末尾,下一轮不需要再比较它。这个边界是冒泡算法最容易写错的地方,写成了Count会越界,写成了Count-1会有一次多余比较。外层循环i < Count - 1是因为只剩一个元素时不需要再排。如果目标框架比较老,元组交换语法不支持,要换成三行temp交换。
swapped是一个优化标记。如果某一轮遍历里一次交换都没发生,说明整个序列已经有序,提前退出循环节省时间。对已经有序的输入,这个优化能把时间复杂度从O(n²)降到O(n)。这套例子里排序只是载体,真正要学的是"比较和交换"这个抽象。把if里的大于号改成小于号,就得到降序排列;把比较的规则提取成委托参数,就能对自定义类型排序,后面很多参数里都用到这个套路。
4.2 递归与迭代:斐波那契数列两种写法
递归例子安排在排序后面,主要是让学习者体会同一逻辑的两种表达方式。斐波那契数列是经典递归教材,但直接照抄会踩性能坑:
// 递归写法:简洁但性能差 static int FibRecursive(int n) { if (n <= 1) return n; return FibRecursive(n - 1) + FibRecursive(n - 2); } // 迭代写法:用循环代替递归 static int FibIterative(int n) { if (n <= 1) return n; int prev = 0, current = 1; for (int i = 2; i <= n; i++) { int temp = current; current = prev + current; prev = temp; } return current; }递归版本在n=30时已经能感觉到卡顿,n=45以上就基本跑不动了,原因是同一个子问题被重复计算了数万次,比如FibRecursive(40)会递归调用大概上亿次。迭代版本用两个中间变量滚动前两个数,每轮循环只做一次加法,时间复杂度从指数级降到线性。如果硬要保留递归的写法,可以用一张字典做记忆化,把算过的子问题存下来,但这已经是优化技巧而不是入门内容。
学习这个对比的目的不是让你拒绝递归,而是理解递归的适用边界。树形遍历、目录扫描这类天然有递归结构的问题适合递归,因为代码和问题形状一致;纯数值计算、顺序遍历的场景优先用迭代。很多开发者在面试时把递归当成高级技巧,其实生产代码里递归用得很少,反倒是迭代和循环更常见。例子集把这两个版本并列放在一起,就是为了让你在最短时间内建立这个直觉。
4.3 LINQ初体验:查询语法与方法语法的区别
100例里后期会出现LINQ,这是C#相对其他主流语言比较突出的语言特性。理解LINQ需要同时看两种写法,因为网上资料两种风格一半一半:
// 方法语法:链式调用 var adults = students .Where(s => s.Age >= 18) .OrderByDescending(s => s.Score) .Select(s => new { s.Name, s.Score }); // 查询语法:类SQL var adults2 = from s in students where s.Age >= 18 orderby s.Score descending select new { s.Name, s.Score };方法语法和查询语法在编译后生成的是同一套中间代码,选择哪种纯粹是风格问题。查询语法对从SQL转过来的人更友好,尤其是where和orderby的位置和SQL很像;方法语法在需要动态拼接条件时更灵活,比如在循环里逐个添加Where条件。lambda表达式s => s.Age >= 18是方法语法的核心,左边是参数,右边是返回布尔值的表达式,编译器会根据上下文自动推断类型。
遇到"在集合中筛选"的需求,先想到LINQ而不是写for循环加if,这是C#开发者和别的语言开发者一个显著的习惯差异。数据量只有几十条时两者性能没有差别,但代码长度差一半,可读性也差很多。LINQ还有一个好处是延迟执行:上面的查询在用foreach遍历之前不会真正访问数据源。新手可能在这里遇到"明明过滤了为什么结果没变"的疑惑,多半是因为执行时机没搞懂。
5. 避坑:照着例子敲代码最常见的五个翻车点
100个例子本身就是一套完整的踩坑集合。以下五个问题是我在带着别人练这套资源时见到频率最高的,基本覆盖了"照着敲却跑不通"的八成场景。
5.1 现象:运行报错“未将对象引用设置到对象的实例”
新手照着窗体例子敲完,一运行就在某一行抛NullReferenceException,英文界面显示"Object reference not set to an instance of an object"。最常见的原因是设计器里的控件名和代码里引用的名字不一致。比如窗体上拖了一个button1,代码里写的是btnShow.Click,事件绑定对不上,或者控件在InitializeComponent之前被访问。
解决方法是先看错误信息定位到具体行号,然后检查代码里引用的每一个控件名是否和设计器属性窗口里的Name一致。我习惯在InitializeComponent()之后的第一行代码打断点,用快速监视查看this.Controls里的控件集合,确认哪些控件已经创建了。还有一类情况是对象确实没赋值,比如数据库查询返回了空结果,却直接访问了行集合的第一行,这种要养成先判断null再取值的习惯。
5.2 现象:窗体按钮点了没反应,界面卡死
执行一个耗时的文件操作或数据库查询时,窗体会假死,鼠标变成旋转圈,点哪里都没反应。原因是代码跑在UI线程上,阻塞了界面的消息循环。资源里老例子的处理方式往往很简单,但实际用起来就会发现大文件操作时界面完全没法看。
这背后的原理是Windows消息循环机制:UI线程既要处理用户的点击,又要执行你的代码,两者共用一条线程。常见解决思路是把耗时操作放到后台线程。老例子里用BackgroundWorker组件,新代码可以用Task.Run。涉及跨线程更新控件时要小心,直接用赋值的方式更新文本会抛"线程间操作无效"的异常,需要把更新动作切回UI线程,比如用Control.Invoke。网上有人建议设置控件的CheckForIllegalCrossThreadCalls属性来屏蔽这个报错,我一般不推荐,那是掩盖问题而不是解决阻塞。
5.3 现象:数据库连接超时或打不开
照着数据库例子改连接字符串,本地连接没问题,换一台机器就报"在与SQL Server建立连接时出现与网络相关的或特定于实例的错误"。原因通常是连接字符串里的服务器地址和实例名写死了,换到别人的机器上,服务名、端口、登录方式全都不一样。
解决方法是把连接字符串放到配置文件里,而不是写在代码里。在App.config中添加一个connectionStrings节点,程序运行时通过ConfigurationManager.ConnectionStrings来读取。这样切换环境时只需要改配置文件,不用重新编译。另外一个参数是连接超时时间,SqlConnection默认15秒,如果网络环境差,可以设置Connect Timeout=30。还要注意连接字符串的键值对用分号分隔,如果密码本身带分号或不支持的字符,需要用引号包住,这种边界情况在真实项目里遇到过不止一次。
5.4 现象:用相对路径读写文件时找不到文件
WinForms程序用相对路径"data.txt"去读文件,运行时提示找不到文件,但文件明明就在项目目录里。原因是工作目录不一定是程序集所在目录。在集成环境里按F5运行和直接双击exe运行时,工作目录完全不同,相对路径会基于不同的起点去解析。
解决方法是不要依赖相对路径,用Application.StartupPath或AppDomain.CurrentDomain.BaseDirectory来拼完整路径。另一个注意点是,老例子的路径分隔符用的是"\"双反斜杠转义,在新代码里最好用Path.Combine替代,这样在Windows和跨平台场景下都不会出错。文件不存在时StreamReader会抛FileNotFoundException,先试用File.Exists判断或者用try-catch包住,这是例子代码里经常省略但生产代码里必须有的部分。
5.5 现象:编译报错“命名空间不存在”或“类型不存在”
最常见的原因是目标框架版本不对。老例子里的某些命名空间,比如System.Web.Extensions、System.Data.SqlClient,在新框架版本里不是默认引用的。解决方法是在项目里检查引用列表,右键项目选择添加引用,在程序集列表里找到对应的项目,勾选后重新编译。如果框架版本跨度太大,有些老API已经被移除,就需要找替代方案。
给一个通用排查顺序:先看项目目标框架是多少,再看出错代码所在文件顶部有没有对应的using声明,然后检查项目是否引用了对应的程序集。这三步按顺序走一遍,八成的编译错误都能定位。还有一个看起来很奇怪但真实存在的坑:解决方案里有两个同名类,一个在WinForms项目里,一个在类库里,编译器选了错误的那一个,导致类型不匹配。这种问题看错误信息里提示的命名空间就能发现。
6. 把100例变成本地作品集:三个进阶玩法
6.1 组合改造:三个例子拼成一个日志查看器
单个例子无论多标准,都算不上作品。我的做法是挑三个看似无关的例子做组合实验。比如"文件读写"+"正则提取"+"DataGridView",拼成一个日志查看器:窗体左边放一个读取按钮,中间是DataGridView表格,正则负责从每行日志里提取时间、级别、消息三个字段。组合的价值在于强迫自己定义数据在模块间的流转方式,正则提取出来的集合怎么塞进DataTable,DataTable又怎么绑定到控件上,这些衔接细节在单个例子里是学不到的。
6.2 异常处理和日志落盘:让代码从能跑到抗造
例子集的代码普遍缺少异常处理,直接把输入框里的值拿去解析,用户一旦输入不可转换的内容,整个程序就会崩溃。进阶版本我统一加一个try-catch结构,catch块里调用自己写的LogHelper.WriteError方法,把异常信息和当前时间追加写入一个error.log文本文件。这个LogHelper本身又是对"文件读写"例子的复用。至此,例子集里的零散知识开始形成一个闭合循环,写日志用的是文件例子,解析输入用的是正则例子,展示结果用的是控件例子,每个环节其实都是前面练过的。
6.3 把算法抽到独立类库:用单元测试做回归保险
挑排序、二分查找这类逻辑纯函数的例子,把它们从窗体代码中抠出来,放进一个单独的类库项目,再建一个单元测试项目引入它。写三个测试方法分别覆盖正常值、边界值(空数组、单个元素)、重复元素。运行测试通过后,这些方法就可以放心复用了。从那以后我每次拿到新的实例集,都会强制走一遍"先分类→按顺序刷→最后挑几个例子组合改造并加测试"的流程,希望这个习惯也能帮到你。
本文还有配套的精品资源,点击获取