简介:面向C#初学者的基础练习题集,适合高校学生与自学者系统巩固语法知识。练习覆盖基本语法与数据类型、控制结构、函数与方法、数组与集合、类与对象、接口与抽象类、异常处理、枚举与委托、文件流、LINQ查询、异步编程及泛型等主题,由浅入深形成完整训练体系。压缩包共1670个文件,核心代码为224个cs源码文件,配套涵盖可运行的exe、程序集dll、调试符号pdb,以及vs工程文件(csproj、sln)、Web项目文件(aspx、asmx)和界面素材(jpg、bmp、ico)等,便于直接编译运行与对照查看,整体仅17.87MB。已有729人学习下载。通过逐题练习与调试,学习者能深入理解C#核心机制,掌握从桌面、Web到通用业务逻辑的实际编码能力。
1. 为什么这套 C# 基础练习题值得你从第一题敲到最后一题
在我接触过的初级开发者和转行候选人里,“C# 语法看过一遍,但一动手就卡壳”几乎是一种常态。这套 C# 基础练习题不是知识点讲义,它是一份把变量、分支、循环、面向对象、字符串和文件操作拆成具体小题的练习册,每题都带参考答案和思路讲解,覆盖范围正好落在“刚学完语法”到“能应付笔试”之间那段最容易被忽视的上手期。它更适合当成验收清单来用:每一个题目都要求你在控制台里亲手跑出结果,再对照参考答案纠正自己的写法,而不是当作阅读材料翻一遍就放下。
2. 从语法到逻辑:变量、分支、循环题里最该磨的三类高频题型
2.1 类型、转换与溢出:int、double、decimal 三兄弟的边界题
基础练习题里,数值类型几乎是必考区。最容易让人翻车的不是复杂语法,而是对类型选型和转换规则不熟:int 相除得到整数、double 累加丢精度、decimal 该出场时没人想起它。这套练习里专门有一组类型题,下面逐个拆开看。
先看最典型的整数除法陷阱题:
int a = 7; int b = 2; double result = a / b; // 预期 3.5,实际输出 3.0 Console.WriteLine(result);运行结果为什么是 3.0?因为 a 和 b 都是 int,a / b 先按整数除法执行,得到 3,然后才隐式转换成 double 赋值给 result。整数除法直接截断小数位,这一点和“四舍五入”没有任何关系。要得到 3.5,必须在除法发生前把至少一个操作数转成浮点类型:
double result = (double)a / b; // 3.5,强转后运算按浮点规则执行逻辑说明:关键是把 a 或 b 的其中一个先转换成 double,整个除法表达式就会提升为浮点运算。建议你把这段代码分别改成 (double)b、两个都转,试完三种写法再回来看结果,印象会比只看答案深得多。
另一道精度题同样经典:
double sum = 0.0; for (int i = 0; i < 10; i++) { sum += 0.1; } Console.WriteLine(sum); // 0.9999999999999999语法没错,输出却和直觉不符。原因在于 double 用二进制近似表示十进制小数,0.1 在二进制里是无限循环小数,十次累加后误差累积到了可见位数。参考答案给的解决方案是:涉及金额、计数等要求精确的场景改用 decimal。
decimal sum = 0.0m; for (int i = 0; i < 10; i++) { sum += 0.1m; } Console.WriteLine(sum); // 1.0参数说明:decimal 字面量必须带 m 后缀,写成 0.1 会被编译器当成 double,直接赋给 decimal 变量会报错。这个细节在笔试和实际编码里都容易出现,练习里专门安排一道题让这两个类型对比出现,就是为了让你记住“什么时候选谁”。
2.2 分支与循环的组合拳:质数、水仙花、九九乘法表
分支和循环单独出题时难度不高,但组合起来就考验执行顺序、边界条件和循环优化。这套练习里出现频率最高的三道题是:判断质数、打印水仙花数、输出九九乘法表。
质数判断的参考实现:
static bool IsPrime(int n) { if (n < 2) return false; if (n == 2) return true; if (n % 2 == 0) return false; for (int i = 3; i * i <= n; i += 2) { if (n % i == 0) return false; } return true; }逻辑说明:先把小于 2、等于 2、偶数这三种特殊情况处理掉,再从 3 开始只遍历奇数,i * i <= n 表示检查到根号 n 就可以停止。功能上写成 for (int i = 2; i < n; i++) 也能得到正确结果,但在输入范围扩大到十万、百万时,两者的循环次数差距就很明显。这道题的价值不只是判断逻辑本身,它还在提醒你:先排除特殊值,再减小循环范围,是很多数学题优化思路的起点。
水仙花数题考查的是“拆位”能力:
for (int n = 100; n <= 999; n++) { int units = n % 10; // 个位 int tens = (n / 10) % 10; // 十位 int hundreds = n / 100; // 百位 if (units * units * units + tens * tens * tens + hundreds * hundreds * hundreds == n) { Console.WriteLine(n); } }拆位思路值得单独背:n % 10 取个位,(n / 10) % 10 取十位,整除 100 取百位。这套写法在回文数、日期计算、进制转换题里会反复出现,本质是把整数按位权拆解。也有参考实现用 ToString 取字符,也能跑通,但对理解位权帮助不大,不建议作为首选写法。
九九乘法表则是格式控制题:
for (int i = 1; i <= 9; i++) { for (int j = 1; j <= i; j++) { Console.Write($"{j} x {i} = {i * j,-4}"); } Console.WriteLine(); }参数说明:{i * j,-4} 里的 -4 表示左对齐占 4 格,去掉负号则是右对齐。练习题里还有两个常见变种:输入 n 输出前 n 行,以及右上角对齐版本。前者只需把外层循环条件改成 i <= n,后者需要调整内层循环起点。两个变种分别对应“参数化输入”和“循环边界调整”两个基础点,建议都跟着敲一遍。
2.3 数组与索引边界:越界的代价和排查方式
数组题在基础练习里占比不低,常见题型有翻转、求最值、指定位置插入。插入题是方向感最容易乱的一道:
int[] arr = new int[6] { 1, 3, 5, 7, 9, 0 }; int insertVal = 6; int insertIndex = 2; for (int i = arr.Length - 1; i > insertIndex; i--) { arr[i] = arr[i - 1]; } arr[insertIndex] = insertVal; // 结果: 1, 3, 6, 5, 7, 9数组长度固定,所以这道题先预留最后一个元素 0 作为空位。插入过程必须从尾部开始,把前一个位置的值搬到当前位置,一路搬到 insertIndex + 1 停止,最后写入新值。如果从前往后搬,后面的值会被覆盖,结果就变成 1, 6, 3, 5, 7, 9,完全错误。我在实际带人时发现这个方向错误率相当高,一个有用的排查技巧是:循环里临时打印数组中间状态。
for (int i = arr.Length - 1; i > insertIndex; i--) { arr[i] = arr[i - 1]; Console.WriteLine(string.Join(",", arr)); }代码说明:string.Join 把数组转成可读字符串输出,比逐个元素打印快得多。调试完再删掉这行。使用 List 的场景可以直接用 Insert 方法解决,但数组版的价值在于让你理解内存移动本身,这部分练习不要跳过。
数组越界题也可以归到这一组。比如数组左移一位,循环写成 arr[i - 1] = arr[i] 时,i 为 0 就会访问 arr[-1] 抛 IndexOutOfRangeException。排查思路是:先把 0 和 Length - 1 两个边界值代入代码手算一遍,再决定循环起点和步长,而不是直接改索引碰运气。这套题练完之后,你会发现很多笔试里的“手写数组”题目都有相似套路。
3. 面向对象练习题:类、继承、接口不是背出来的,是做出来的
3.1 类与属性:用“银行账户”的设计题检验封装基本功
面向对象练习题如果只是让定义“一个学生类有两个属性”,练完和没练差别不大。这套练习里银行账户题设计得比较到位:要求实现存款、取款、查询余额,并且取款不能透支。
public class BankAccount { private decimal _balance; public decimal Balance => _balance; public void Deposit(decimal amount) { if (amount <= 0) throw new ArgumentException("存款金额必须大于 0"); _balance += amount; } public bool Withdraw(decimal amount) { if (amount <= 0) throw new ArgumentException("取款金额必须大于 0"); if (amount > _balance) return false; _balance -= amount; return true; } }逻辑说明:Balance 用只读属性暴露,外部只能读不能直接改,修改余额必须通过 Deposit 和 Withdraw,这是封装最直观的体现。两个方法都先做参数校验,Deposit 对非法参数直接抛异常,Withdraw 对余额不足返回 false,让调用方根据自己的 UI 层决定提示文案。
参数说明:金额字段用 decimal 而不是 double,这里呼应了第二章的精度题。笔试如果追问“为什么用 decimal”,需要答出“金额计算要求精确十进制,double 有二进制舍入误差”。这类理由放进代码练习里反复出现,比单纯背概念记得牢。
这道题还留了一个值得细想的点:为什么 Deposit 抛异常,而 Withdraw 返回 false?“存款金额不合法”属于调用方传参错误,应当快速失败;而“余额不足”是业务状态判断,适合返回布尔值让上层决定如何展示。理解这个区分,是面向对象练习走向真实业务的一个重要台阶。
3.2 继承与虚方法:几何图形与工资计算的两套范式
几何图形的面积计算题是继承和多态最典型的载体:
public abstract class Shape { public abstract double Area(); } public class Circle : Shape { public double Radius { get; set; } public override double Area() => Math.PI * Radius * Radius; } public class Rectangle : Shape { public double Width { get; set; } public double Height { get; set; } public override double Area() => Width * Height; }抽象基类 Shape 的 Area 只有签名没有实现,子类通过 override 各自实现。调用方可以统一处理所有形状:
List<Shape> shapes = new List<Shape> { new Circle { Radius = 3 }, new Rectangle { Width = 4, Height = 5 } }; foreach (var shape in shapes) { Console.WriteLine(shape.Area()); }这段代码的价值在于:遍历时不需要判断 shape 到底是 Circle 还是 Rectangle,直接调用虚方法就能得到正确面积。如果不用多态,代码里会到处是 if (shape is Circle) 这类类型判断,扩展新形状时还容易漏改。做一次两种写法的对比,比反复背“多态优势”有效得多。
虚方法和抽象方法有个容易混淆的点:抽象方法必须在派生类里 override,否则编译不过;virtual 虚方法则提供默认实现,子类可以 override 也可以继续沿用。工资计算题正好适合用虚方法:
public class Employee { public virtual decimal GetSalary() => 5000m; } public class Developer : Employee { public decimal Bonus { get; set; } public override decimal GetSalary() => base.GetSalary() + Bonus; }代码说明:base.GetSalary() 调用父类版本,是一种“在父类结果上扩展”的写法,比在子类里复制父类那段工资逻辑要整洁。练习题里输出 Developer 工资 = 基础工资 + 奖金,这个例子做完,对 override 和 base 关键字的用法就比较清楚了。
3.3 接口与实际场景:从支付流程看依赖抽象
接口练习题在这套资源里是另一条线。参考题是定义一个支付接口,成功返回 true,失败返回 false:
public interface IPayment { bool Pay(decimal amount); }写两个实现:
public class AliPay : IPayment { public string Account { get; set; } public bool Pay(decimal amount) { Console.WriteLine($"账户 {Account} 支付 {amount} 元"); // 这里模拟支付渠道调用,成功返回 true return true; } } public class WeChatPay : IPayment { public bool Pay(decimal amount) { Console.WriteLine($"微信支付 {amount} 元"); return true; } }接口的价值在调用函数上体现得最清楚:
static void ProcessPayment(IPayment payment, decimal amount) { if (payment.Pay(amount)) { Console.WriteLine("支付成功"); } }ProcessPayment 只依赖接口,不关心实际实现是哪个渠道。以后新增银行卡支付、余额支付,只需再写一个类实现 IPayment,ProcessPayment 一行都不用改。这就是“面向接口编程”——它把一个原本容易写成一堆 if 分支的场景,简化成了插入新类即可扩展的结构。
接口和抽象类的选择也是笔试高频:接口强调“能做什么”,一个类可以实现多个接口;抽象类强调“是什么”,单继承,适合存放公共字段和方法。图形题更适合抽象类,因为圆和矩形确实是形状的子类;支付题则更适合接口,因为支付渠道之间没有继承关系,只是约定 Pay 行为。练完这组题再回头做选型,就不会觉得抽象类和接口的区别那么玄学了。
4. 字符串、集合与文件读写:把练习题做成本地工具的三条主线
4.1 字符串处理:拼接习惯与词频统计
字符串类练习在基础题中占比很高,但很多人把它们当成背方法列表的题。真正值得练的实际是三个点:StringBuilder 的使用、字符统计的结构、不可变性带来的编码习惯。
先看一个最经典的坏味道:循环内用 += 拼接字符串。
string result = ""; for (int i = 0; i < 10000; i++) { result += "a"; }这段代码在小数据量下看不出问题,但字符串是不可变类型,每次 += 都会在托管堆里创建一个新的字符串对象,旧的等着垃圾回收。1 万次循环就是 1 万个中间对象,循环次数上到十万、百万,卡顿就肉眼可见了。参考答案改用 StringBuilder:
var sb = new StringBuilder(); for (int i = 0; i < 10000; i++) { sb.Append("a"); } string result = sb.ToString();逻辑说明:StringBuilder 内部维护一个可变缓冲区,Append 直接写入缓冲区,不会产生中间字符串对象。参数说明:它默认容量是 16,超长自动扩容;如果一开始就知道最终长度,可以直接 new StringBuilder(20000) 指定容量,减少扩容次数。这个习惯我在工作里一直坚持,循环内拼接一律用 StringBuilder,等卡顿了再回头改就晚了。
字符统计题用来考查字典结构的使用很合适:
var dict = new Dictionary<char, int>(); foreach (char ch in sentence) { if (char.IsWhiteSpace(ch)) continue; if (dict.ContainsKey(ch)) dict[ch]++; else dict[ch] = 1; } var sorted = dict.OrderByDescending(k => k.Value); foreach (var entry in sorted) { Console.WriteLine($"{entry.Key}: {entry.Value}"); }代码说明:用 Dictionary<char, int> 维护“字符 → 出现次数”的映射,键存在则计数加一,否则初始化为 1。注意 char.IsWhiteSpace 过滤空白字符,它覆盖空格、制表符、换行符,比直接判断 ch != ' ' 更全面。输出时用 OrderByDescending 按次数降序排列,这些在文本处理类小工具里都是常用操作。
4.2 集合与 LINQ:去重、排序、查找的三种常用姿势
集合练习不能停留在 List 的 Add 和 Remove。这道题要求把整数数组去重并按降序输出:
var numbers = new List<int> { 3, 1, 4, 1, 5, 9, 2, 6, 5 }; var uniqueDesc = numbers .Distinct() .OrderByDescending(x => x) .ToList(); foreach (var n in uniqueDesc) { Console.WriteLine(n); }Distinct 去重,OrderByDescending 降序,ToList 把结果物化为 List。我给新人的建议是:LINQ 和普通循环各写一遍。普通循环帮你理解去重和排序的本质,LINQ 写法帮你适应团队代码风格。两者结果一致,正好也是一种“不改变功能的重构”练习。
再看查找场景:
int[] nums = { 1, 4, 7, 9, 3, 8 }; var matched = nums.Where(n => n >= 3 && n <= 7);参数说明:Where 返回的是延迟执行的 IEnumerable ,只有真正遍历时才开始计算。很多人踩的坑是:在循环外拿着这个查询结果用,等到集合被修改后再遍历,数据可能和你第一次观察时不一样。把这一点记住,调试 LINQ 相关问题时能省不少时间。
4.3 文件读写与异常处理:成绩单导入导出小工具
文件读写是从基础语法过渡到真实项目最常见的一道门槛。练习题通常以“读取成绩文件,计算统计信息,写入结果文件”为场景:
var lines = File.ReadAllLines("scores.txt", Encoding.UTF8); var scores = new List<int>(); foreach (var line in lines) { if (string.IsNullOrWhiteSpace(line)) continue; var parts = line.Split(','); if (parts.Length == 2 && int.TryParse(parts[1], out int score)) { scores.Add(score); } }逻辑说明:先跳过空行,再按逗号拆出姓名和分数,用 int.TryParse 解析第二列。这里用 TryParse 而不是 Parse,是因为解析外部输入时不能假设数据一定合法——文件里偶尔会有缺失或脏数据,TryParse 失败顶多跳过这一行,不会让整个程序崩溃。这是我在真实项目里积累出来的习惯:凡是从文件、网络或用户输入来的数据,都默认它可能不合法。
统计后写入:
using (var writer = new StreamWriter("result.txt", false, Encoding.UTF8)) { writer.WriteLine($"人数:{scores.Count}"); writer.WriteLine($"平均分:{scores.Average():F2}"); writer.WriteLine($"最高分:{scores.Max()}"); }参数说明:StreamWriter 的第二个参数 false 表示覆盖旧文件,改成 true 就是追加。Encoding.UTF8 保证中文不乱码。using 关键字保证离开代码块时自动释放文件句柄,避免后面再打开文件时报“文件正被占用”。
这一章做完之后,建议把固定文件名改成从控制台参数读取:
static void Main(string[] args) { string input = args.Length > 0 ? args[0] : "scores.txt"; // 其余逻辑不变 }这样一来,练习就从一个固定输入的小程序,变成了可以处理任意文件的小工具,完成度和真实感完全不一样。
5. 常见问题与避坑:C# 基础练习题里人人踩过的五个坑
5.1 整数相除变成“截断”而不是“四舍五入”
现象:计算平均分时写 scoreSum / scoreCount,结果总是丢小数。原因:两个 int 相除的结果还是 int,小数部分被直接向零截断,不是四舍五入。解决:把其中一个操作数转换为 double,比如 (double)scoreSum / scoreCount,或者直接把求和变量和计数变量定义为 double。如果要求保留两位小数,输出时用 $"{avg:F2}"。还有更稳妥的办法:一开始就用 decimal 承载分数和人数,避免中途忘记转类型。
5.2 用 == 比较 double,结果时对时错
现象:循环累加 10 次 0.1 后,判断 sum == 1.0 返回 false。原因:double 是二进制近似表示,0.1 无法被精确表示,累加十次后实际值略小于 1.0。解决:浮点相等比较要写成 Math.Abs(sum - 1.0) < 1e-9,用误差范围代替精确相等。涉及金额、数量的练习直接改用 decimal,就从根源上绕开了这个问题。这种细节平时不在练习里踩一次,到了项目里排查精度问题时往往要花半天。
5.3 循环里拼接字符串,数据量一大就明显卡顿
现象:循环一万次用 += 拼接字符串,控制台程序明显卡住,内存占用也升高。原因:字符串不可变,每次 += 都会创建新的字符串对象,旧对象等待垃圾回收,分配和回收的开销随着迭代次数放大。解决:循环内拼接改用 StringBuilder;如果是对数组元素拼接,用 string.Join。这个坏味道我在面试代码里经常见到,不少人用 += 拼 SQL、拼 HTML 日志,数据量一大就出问题。练习题里早一点改掉这个习惯,后面工作会轻松很多。
5.4 foreach 里删除集合元素,运行中段直接抛异常
现象:写了一个把 List 中偶数全部删除的练习,foreach 里调用 RemoveAt,程序在第二次迭代时抛出 InvalidOperationException。原因:foreach 依赖迭代器,集合在迭代过程中被修改,迭代器检测到集合版本变化后立即拒绝继续访问。解决:改用 for 从后往前移除,或者先筛选出要删除的元素再单独遍历删除。
var toRemove = items.Where(x => x.IsExpired).ToList(); foreach (var item in toRemove) { items.Remove(item); }代码说明:先用 Where 筛选出目标元素并 ToList 复制一份,再遍历新列表删除原集合里的元素,完全不触碰迭代器,这是日常开发里很稳妥的写法。
5.5 把文件路径写死成本地路径,换台机器就报错
现象:练习里写 File.ReadAllLines("D:\temp\scores.txt") 在本地一切正常,拷贝给别人或换台电脑就抛 FileNotFoundException。原因:路径被写死成绝对路径,目标机器上没有这个目录。解决:练习项目使用相对路径,把数据文件放进程序运行目录;跨平台拼接路径用 Path.Combine;真实项目里路径来自配置文件、环境变量或命令行参数,不应该散落在代码各处。做完这道题后用控制台参数传入路径,就迈出了从练习到工具的第一步。
补充一个和这五条并列重要的排查习惯:控制台练习题出问题时,先手动代入边界值看逻辑,再想语法问题。比如数组题先把 i = 0 和 i = Length - 1 代入循环,字符串题先把空字符串和 null 代入方法。大部分基础题的错误都是边界条件没处理好,而不是语法不会写。
6. 把练习题的参考答案做二次重构:三个值得养成的收尾习惯
参考答案和你的实现通常存在差异,这很正常,关键在于你是否读得懂、改得动。我建议拿到这套练习后按这个顺序走:先自己独立写一遍,再对照参考答案改一遍,最后把每个题目做一次“不改变功能的重构”。重构不是炫技,而是把练习代码的结构往真实项目方向调整。
第一个习惯是先抽方法,让 Main 足够短。把每个练习题的核心逻辑封装成独立函数,参数化边界值。质数题写成 bool IsPrime(int n),乘法表题写成 void PrintTable(int rows),成绩统计题写成 void ProcessScoreFile(string path)。这样一来,每个函数独立可测,Main 里只剩调用和输出,可读性会明显提升。
第二个习惯是消除魔法数字。练习里经常出现 4、100、400、1000 这种常量,直接散落在条件表达式里,别人看不懂来龙去脉。把它们提取成 const 或者有意义的命名常量,代码就多了一层解释。这一步在资深代码审查里是被反复提的,早一点养成,写出来的练习代码就不像练习,更像能维护的工程代码。
第三个习惯是补输入校验。练习题往往默认输入合法,但真实用户会给你负数、空串、超长文本,甚至比你预想更怪的数据。每道题多加一行判断,整个程序的健壮性都不一样。我一般会在写完功能后,把所有 Console.ReadLine 和文件读取的地方过一遍,确认各种诡异输入都不会让程序直接崩溃。
另外我有个坚持了很久的习惯:做完题之后跑一遍边界输入。质数题输入 1、2、3、4、9、97,成绩统计题输入空文件、只有表头、带非法行的文件。这些边界情况能暴露绝大多数逻辑漏洞。从那以后我每次做完练习题,都强制走一遍边界输入再对答案,这个习惯让我在面试手撕代码时很少因为边界条件扣分,也让这些练习从“能跑的代码”变成了真正能迁移到工作里的基本功。希望帮到你。
本文还有配套的精品资源,点击获取