简介:这是一套面向C#初学者与入门级开发者的系统性基础练习题资源,聚焦语法巩固、编程思维训练与核心机制理解,覆盖从变量声明到异步编程的12大知识模块,特别适合自学备考、课程实训或转岗练手。资源包共1670个文件,以224个.cs源码文件为核心,辅以164个可执行exe用于即时验证、94个.csproj与82个.sln工程文件保障项目结构完整性,另含大量.ico、.jpg等资源文件及.config、.aspx等Web相关配置与页面文件,整体压缩包仅17.87MB,轻量易下载。已有730人学习下载,说明其内容组织清晰、实践导向明确。学习者可直接运行示例、调试代码逻辑、对比标准实现,快速掌握类设计、集合操作、异常处理、LINQ查询等关键能力,并通过多类型项目文件(含WinForm、WebForms等)理解C#在不同应用场景下的工程落地方式。
1. C#基础练习题:不是刷题清单,而是你写第一行可运行代码前该踩的12个语法坑
刚学完Console.WriteLine("Hello World"),一做练习就卡在“找不到符号”“类型不匹配”“索引超出范围”——这不是你手慢,是C#基础练习题里埋着大量隐性语法契约:比如int.Parse()和Convert.ToInt32()对空字符串的反应完全不同;for循环里用i++和++i在某些嵌套场景下会悄悄改变逻辑;string不可变性导致Replace()不修改原字符串却常被误当作“就地替换”。这份练习题包不是题海战术,而是按真实开发断点频次排序的28道题,覆盖变量声明、数组越界、引用/值类型混淆、异常捕获粒度、using语句生命周期等新手最易翻车的环节。适合两类人:一是刚敲完第一个控制台程序、想验证自己是否真懂var推导规则的初学者;二是带新人的导师,用其中第7题(List<T>扩容机制+foreach修改报错)现场演示“为什么编译器不让改正在遍历的集合”。题干全部附带最小可复现代码片段,答案不只给结果,更标出VS调试器里该停在哪一行、看哪个局部变量窗口——毕竟,调试能力才是C#入门真正的分水岭。
2. 练习题结构解析:为什么这28道题必须按「声明→运算→流程→异常→资源」顺序做
2.1 题型设计逻辑:从编译器报错源头反向构建认知链
C#编译器报错信息有强误导性。比如CS0103: 名称"xxx"在当前上下文中不存在,新手常以为是拼写错误,实际90%是作用域问题:变量在if块内声明却在块外访问,或for循环变量在循环结束后仍被引用。本练习题将28题严格按编译错误→运行时异常→逻辑错误三级递进:
- 前6题聚焦编译期拦截:
var声明后立即赋值、const与readonly修饰符混用、enum底层类型隐式转换; - 中间12题触发运行时异常:
NullReferenceException(未初始化对象)、IndexOutOfRangeException(数组长度计算偏差)、FormatException(字符串转数字时忽略空格); - 后10题暴露逻辑黑匣子:
decimal精度丢失导致财务计算偏差、DateTime.Now在跨时区服务中引发时间戳错乱、==比较字符串时未考虑文化差异。
提示:不要跳题!第3题(
const string s = null;编译失败)和第4题(readonly string s = null;编译通过)必须连续做,这是理解C#编译期常量推导的关键锚点。
2.2 文件组织与执行入口:每个.cs文件都是独立可运行单元
资源包解压后为标准C#项目结构:
CSharpExercises/ ├── Solutions/ # 参考答案(含详细注释) │ ├── Q01_Variables.cs │ └── ... ├── Tests/ # 单元测试(xUnit框架) │ ├── Q01_Tests.cs │ └── ... └── Program.cs # 主入口:调用所有练习题的验证函数关键设计:每个练习题对应一个独立.cs文件,且文件内含完整Main方法。例如Q05_ArrayBounds.cs:
// Q05_ArrayBounds.cs using System; class Q05_ArrayBounds { static void Main() { int[] arr = { 1, 2, 3 }; Console.WriteLine(arr[3]); // 故意越界 } }这样设计的目的,是让你能单文件编译运行:
csc Q05_ArrayBounds.cs && dotnet run --no-build避免因项目引用混乱掩盖真实错误。所有答案文件(Solutions/目录)均采用相同结构,但Main方法内调用Validate()函数验证结果,而非直接输出——这是为后续接入CI流水线预留的接口。
2.3 答案验证机制:为什么不用Console.WriteLine而用断言
参考答案中不出现Console.WriteLine("正确")这类弱验证。以第12题(字符串分割)为例:
// Solutions/Q12_StringSplit.cs public static bool Validate() { string input = "a,b,c"; string[] parts = input.Split(','); // 注意:此处无Trim() // 强验证:检查数组长度、元素内容、是否包含空格 if (parts.Length != 3) return false; if (parts[0] != "a") return false; if (parts[1] != "b") return false; if (parts[2] != "c") return false; // 关键:验证Trim行为(常见坑) string inputWithSpace = " a , b , c "; string[] spacedParts = inputWithSpace.Split(','); // 此处必须手动Trim每个元素,否则spacedParts[0] = " a " for (int i = 0; i < spacedParts.Length; i++) spacedParts[i] = spacedParts[i].Trim(); return spacedParts[0] == "a" && spacedParts[1] == "b" && spacedParts[2] == "c"; }这种验证方式强制你关注数据状态而非输出表象。当Validate()返回false时,VS调试器可直接在return false行设断点,观察parts数组每个元素的实际值——这才是生产环境排查问题的常态。
3. 核心语法避坑指南:12个新手必踩的C#基础陷阱及现场修复
3.1 现象:var声明后未赋值,编译报错CS0818
现象:var number;编译失败,提示“隐式类型变量必须初始化”。
原因:var不是动态类型,而是编译器根据右侧表达式推导静态类型。无初始化则无法推导,本质是语法糖限制。
解决:必须初始化,且类型明确:
var number = 42; // 推导为int var name = "Alice"; // 推导为string // 错误写法: // var number; // 编译失败 // var list = new List<>(); // 编译失败(泛型类型参数缺失)血泪经验:团队代码规范要求
var仅用于类型名过长时(如Dictionary<string, List<int>>),简单类型如int、string必须显式声明——避免阅读时反复跳转查类型。
3.2 现象:foreach循环中修改集合,抛出InvalidOperationException
现象:
List<int> numbers = new List<int> { 1, 2, 3 }; foreach (int n in numbers) { if (n == 2) numbers.Remove(n); // 运行时报错 }原因:foreach底层调用IEnumerator,其MoveNext()方法检测到集合结构变更(Remove改变Count)即抛异常,属.NET安全机制。
解决:
- 方案1(推荐):用
for倒序遍历,避免索引失效:
for (int i = numbers.Count - 1; i >= 0; i--) { if (numbers[i] == 2) numbers.RemoveAt(i); }- 方案2:收集待删项,循环后批量删除:
var toRemove = numbers.Where(x => x == 2).ToList(); foreach (var item in toRemove) numbers.Remove(item);3.3 现象:string.Replace()不修改原字符串,误以为“没生效”
现象:
string text = "hello world"; text.Replace("world", "C#"); Console.WriteLine(text); // 输出"hello world",非"hello C#"原因:string是不可变引用类型,Replace()返回新字符串,原字符串对象地址不变。
解决:必须重新赋值:
text = text.Replace("world", "C#"); // 关键:赋值回text玄学时刻:在VS调试器中,
text变量值显示为"hello world",但内存窗口能看到新字符串对象已创建——这是不可变性的物理证据。
3.4 现象:int.Parse()解析空字符串,抛出FormatException
现象:int.Parse("")报错,而Convert.ToInt32("")也报错,但新手常混淆二者区别。
原因:int.Parse()严格要求输入为有效数字格式,空字符串、null、含空格均失败;Convert.ToInt32()对null返回0,但对空字符串仍失败。
解决:生产环境必须用int.TryParse():
string input = ""; if (int.TryParse(input, out int result)) { Console.WriteLine($"解析成功: {result}"); } else { Console.WriteLine("输入无效,使用默认值0"); result = 0; }参数说明:TryParse第二个参数out int result是输出参数,仅当解析成功时才赋值,避免未初始化变量风险。
3.5 现象:DateTime.Now在服务器部署后时间错乱
现象:本地开发时DateTime.Now输出正确时间,部署到Linux服务器后相差8小时。
原因:DateTime.Now返回本地时区时间,Windows服务器默认东八区,Linux容器常为UTC时区。
解决:统一使用DateTime.UtcNow存储,显示时再转换:
// 存储(数据库字段用datetime2) DateTime utcNow = DateTime.UtcNow; // 显示给用户(假设用户在北京) DateTime beijingTime = utcNow.AddHours(8); // 更健壮:用TimeZoneInfo TimeZoneInfo beijingZone = TimeZoneInfo.FindSystemTimeZoneById("China Standard Time"); DateTime beijingTime2 = TimeZoneInfo.ConvertTimeFromUtc(utcNow, beijingZone);注意:
DateTimeKind枚举值(Unspecified/Local/Utc)影响序列化行为,存库前务必调用ToUniversalTime()。
4. 从练习题到真实项目:如何把Q17的LINQ查询迁移到ASP.NET Core API
4.1 Q17题目还原:用LINQ筛选学生列表并分组统计
原题代码(Q17_LinqQuery.cs):
var students = new List<Student> { new Student { Name = "Alice", Grade = "A", Score = 95 }, new Student { Name = "Bob", Grade = "B", Score = 87 }, new Student { Name = "Charlie", Grade = "A", Score = 92 } }; // 要求:按Grade分组,统计每组平均分、最高分、学生数 var result = students .GroupBy(s => s.Grade) .Select(g => new { Grade = g.Key, AvgScore = g.Average(s => s.Score), MaxScore = g.Max(s => s.Score), Count = g.Count() }) .ToList();此代码在控制台运行无问题,但迁移到Web API需解决三个现实约束:
- 延迟执行问题:
IQueryable在数据库端执行,IEnumerable在内存执行,选错导致全表加载; - DTO分离:API响应不能直接返回实体类,需映射到轻量DTO;
- 空值安全:数据库字段可能为NULL,
Average()对空集合抛异常。
4.2 ASP.NET Core API迁移步骤
步骤1:定义DTO与数据上下文
// Models/StudentDto.cs public class StudentDto { public string Grade { get; set; } public double AvgScore { get; set; } public int MaxScore { get; set; } public int Count { get; set; } } // Data/ApplicationDbContext.cs public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<Student> Students { get; set; } }步骤2:Controller中编写安全查询
// Controllers/StudentsController.cs [ApiController] [Route("api/[controller]")] public class StudentsController : ControllerBase { private readonly ApplicationDbContext _context; public StudentsController(ApplicationDbContext context) => _context = context; [HttpGet("grade-stats")] public async Task<ActionResult<IEnumerable<StudentDto>>> GetGradeStats() { // 关键:使用IQueryable避免内存加载 var query = _context.Students.AsQueryable(); // 处理空集合:先判断是否存在数据 if (!await query.AnyAsync()) return Ok(new List<StudentDto>()); // 安全聚合:Avg()对空集合返回null,需处理 var result = await query .GroupBy(s => s.Grade) .Select(g => new StudentDto { Grade = g.Key, AvgScore = g.Average(s => (double?)s.Score) ?? 0, // null合并操作符 MaxScore = g.Max(s => s.Score), Count = g.Count() }) .ToListAsync(); return Ok(result); } }参数说明:
AsQueryable()确保查询在SQL Server执行,生成类似SELECT AVG([Score]) FROM [Students] GROUP BY [Grade];(double?)s.Score将int转为可空double?,使Average()返回double?,?? 0提供默认值;ToListAsync()异步执行,避免阻塞线程。
4.3 单元测试验证迁移正确性
在Tests/StudentsControllerTests.cs中添加:
[Fact] public async Task GetGradeStats_ReturnsCorrectStats() { // Arrange var options = new DbContextOptionsBuilder<ApplicationDbContext>() .UseInMemoryDatabase(databaseName: "TestDb") .Options; using var context = new ApplicationDbContext(options); context.Students.AddRange( new Student { Name = "Alice", Grade = "A", Score = 95 }, new Student { Name = "Bob", Grade = "B", Score = 87 }, new Student { Name = "Charlie", Grade = "A", Score = 92 } ); await context.SaveChangesAsync(); var controller = new StudentsController(context); // Act var result = await controller.GetGradeStats() as OkObjectResult; // Assert var stats = result?.Value as IEnumerable<StudentDto>; Assert.NotNull(stats); Assert.Equal(2, stats.Count()); // A组和B组 var gradeA = stats.First(s => s.Grade == "A"); Assert.Equal(93.5, gradeA.AvgScore, 1); // 95和92的平均值 }此测试验证了数据库层聚合与空值处理双重逻辑,比原练习题的内存操作更贴近真实场景。
5. 调试技巧实战:用VS诊断Q23的“引用类型意外修改”问题
5.1 Q23题目核心:理解ref、out与对象引用传递的区别
原题代码(Q23_ReferenceTypes.cs):
class Program { static void Main() { List<int> list1 = new List<int> { 1, 2, 3 }; List<int> list2 = list1; // 引用复制 ModifyList(list1); Console.WriteLine(string.Join(",", list2)); // 输出"1,2,3,4",非预期! } static void ModifyList(List<int> input) { input.Add(4); // 修改原对象 input = new List<int> { 5, 6 }; // 重赋值,不影响list1 } }新手困惑点:list2为何被修改?input = new List...为何没影响list1?这涉及C#引用类型传递的本质:参数传递的是对象引用的副本,而非引用本身。
5.2 VS调试四步定位法
步骤1:在ModifyList入口设断点,观察input地址
- 运行到
static void ModifyList(List<int> input)第一行; - 在“局部变量”窗口查看
input,右键选择“转到内存”,记录地址(如0x000001F2A8D4E320); - 对比
list1地址,确认二者相同——证明是同一对象。
步骤2:执行input.Add(4)后,观察list1和list2同步变化
- 单步执行
input.Add(4); - 切换到
Main方法栈帧,在“自动”窗口观察list1和list2的Count均为4,Items[3]为4; - 结论:
Add()修改堆上对象,所有引用该地址的变量都可见。
步骤3:执行input = new List<int> {5,6}后,对比地址变化
- 执行赋值后,
input地址变为新值(如0x000001F2A8D4F1A0),而list1和list2地址不变; - 关键洞察:
=操作符改变的是input这个局部变量存储的地址值,不影响其他变量持有的旧地址。
步骤4:用“内存窗口”验证堆对象状态
- 在
input.Add(4)后暂停,打开“调试”→“窗口”→“内存”→“内存1”; - 输入
list1地址(如0x000001F2A8D4E320),查看内存布局:0x000001F2A8D4E320: 00 00 00 00 04 00 00 00 ... // Count=4 0x000001F2A8D4E328: 01 00 00 00 02 00 00 00 ... // Items[0]=1, Items[1]=2 - 再执行
input = new...后,原地址内容不变,新地址有独立内存块。
5.3 修复方案:何时用ref强制修改引用本身
若需在方法内让list1指向新对象,必须用ref:
static void ModifyListRef(ref List<int> input) { input.Add(4); // 仍修改原对象 input = new List<int> { 5, 6 }; // 此时list1将指向新对象 } // 调用时需加ref ModifyListRef(ref list1);此时list1地址在调用后改变,list2仍指向原地址——这才是“引用的引用”。
从那以后我每次遇到对象状态异常,都强制走一遍VS内存窗口验证:先记地址,再改值,最后比对。地址不变而值变,是对象修改;地址变了而值没变,是引用重定向。希望帮到你。
本文还有配套的精品资源,点击获取