ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C#基础刷题坡道:从语法覆盖到完整控制台小项目

C#基础刷题坡道:从语法覆盖到完整控制台小项目

简介:C#基础练习题资源包聚焦C#语法与面向对象核心,适合刚入门的学习者通过刷题巩固基础,也适合初级开发者查漏补缺。内容覆盖变量与数据类型、条件与循环、函数重载、数组与集合操作、类与继承、接口与抽象类、异常处理、枚举与委托、文件读写、LINQ查询、异步编程和泛型,几乎涵盖C#入门阶段所有高频考点。压缩包内共1670个文件,除大量cs源码外,还包含exe可运行示例、aspx页面、项目工程文件(csproj/sln)以及配套图标、位图与数据库文件,整体约17.87MB,便于边看效果边对照源码理解运行逻辑。目前已有729人学习,练习题与微型示例项目组合适用于课后训练、期末复习或面试基础考核。通过动手完成这些练习,学习者能系统掌握从基本语法到委托事件、LINQ等进阶特性的典型写法,逐步具备独立编写和调试C#程序的能力。

1. 把 C# 基础练习题做成刷题坡道:从语法覆盖到能写小项目

很多 C# 新手卡在同一个地方:语法书翻完了,视频课也刷了,一落到“写一个完整的控制台程序”就不知道第一行代码该敲什么。这套基础练习题就是用来打穿这个瓶颈的——它把 C# 的核心语法点拆成一组小而明确的编程题,从变量、分支、循环一路推到集合、字符串和类的基础封装,每题都聚焦一个知识点,适合刚学完语法、想靠刷题建立手感的人。作为把整套题逐题跑过一遍的工程师,我可以先给结论:这套题的价值不在于单个题目的难度,而在于题目顺序形成的坡道。顺着刷,每个语法点都有机会至少亲手写一遍;跳着刷,容易在综合题上翻车。

2. 练习题集的结构与难度梯度:先看懂按语法点组织的刷题地图

拿到题集第一件事,别急着打开第一道题。建议先花十分钟看目录。这套练习的组织方式,简单说就是把同一个语法主题的多道题放在一起,形成一条从“确认概念”到“能做小项目”的坡道。直接跳着做的人,大概率会在综合题上卡住,然后怀疑自己不是写代码的料——其实只是顺序不对。

2.1 六个主题层的划分依据:为什么按语法点而不是按场景

常见做法是把 C# 基础练习按六个层次组织:变量与类型转换、分支、循环、集合与数组、字符串处理、类与对象基础。这套题集基本延续了这个分层,但每个主题下还加了“变式题”——同一个知识点换一个输入格式或输出要求,让你不得不重新组织逻辑,而不是背答案。

这么分层有三个实际好处。第一,调试技能是逐步建立的。循环主题的题不涉及类和对象,出错时只需要盯着循环条件和变量变化,问题面小,新手不至于一次面对三四个语法点同时出错。第二,语法点需要重复才能形成肌肉记忆。比如string.Split的参数、List<T>.RemoveAll的用法、int.TryParse的返回值,看十遍都不如亲手写一遍有效。第三,为综合题打底。题集后半部分的题往往会组合两到三个语法点,比如“用循环读入数据,存进List<T>,再用字符串方法格式化输出”,如果你前面每个点都单独练过,这题拆开就能做。

这套题里有一个很典型的递进例子:主题“循环”下的第一题是“输入 5 个整数,输出它们的总和”;变式题是“输入若干整数,直到用户输入 end,输出总和”;再往后一道是“输入若干整数,统计正数、负数、零的个数”。第一题只需要一个for循环加累加,第二题引入循环终止条件,第三题要用集合加分支组合。每道题只比上一道多一个知识点,这是我认为这套题集最值得抄的地方。

List<int> numbers = new List<int>(); while (true) { string line = Console.ReadLine(); if (line == "end") break; if (int.TryParse(line, out int value)) numbers.Add(value); } int positive = 0, negative = 0, zero = 0; foreach (int n in numbers) { if (n > 0) positive++; else if (n < 0) negative++; else zero++; } Console.WriteLine($"正数: {positive}, 负数: {negative}, 零: {zero}");

这段代码对应第三道变式题。while (true)配合break是控制台练习里很常用的“读入直到满足条件”写法;int.TryParse返回布尔值,能过滤掉非数字输入,避免int.Parse直接抛异常。foreach遍历List<int>比for按索引访问更安全,少写一个Count边界判断。练这题时,你可以试着把break条件改成“输入空行结束”,这个改动会立刻牵动TryParse的分支逻辑,是很好的进阶练习。

2.2 难度标记与刷题节奏:简单题值得全部手动敲一遍

很多刷题者追求“只做难题”,在基础阶段这是个误解。这套题集里简单题占多数,中等题穿插其中,难题基本集中在类和对象主题的最后几道。我的建议是:简单题全部手动敲一遍,不要在脑子里过一遍就跳过。

原因有两个。第一,简单题的代码往往只有几行,但每一行都是高频写法。比如Console.ReadLine() ?? ""这个组合,新手往往不理解??是干嘛的——ReadLine在输入流结束时可能返回null,?? ""把它变成空字符串,防止后续调用.Length或.Split时抛NullReferenceException。这种细节只有在亲手写出来、再故意删掉?? ""看它崩溃一次,才能真正记住。第二,简单题容易让人忽略边界。

拆这套题时我发现,最容易翻车的恰好是“看起来简单”的题。比如“输入一个字符串,输出它的长度”,大多数人的第一版代码是这样的:

string input = Console.ReadLine(); Console.WriteLine(input.Length);

逻辑上没问题,但Console.ReadLine()在输入流结束时会返回null,此时input.Length直接抛异常。简单题省略了边界处理,并不代表边界不存在。所以我的习惯是:每做完一道简单题,都强制自己写一个“边界输入”试一遍——空行、超长字符串、中文、带空格的内容,看程序会不会崩。这套题集的好处是每题都自带“输入约定”,你可以对照约定设计测试输入,把边界问题提前暴露出来。

难度标记方面,这套题的常见做法是用一星到三星标在题号后面。一星题聚焦单一语法点,二星题开始组合两个点,三星题要求你独立设计一个小流程。我建议一星题每天做八到十道,二星题每天四到五道,三星题每天一到两道,重点是每道题都改一次输入或输出格式,把它变成自己的题。刷题节奏比刷题数量重要得多,这是我从这套题里得到的最大收获。

3. 从下载到跑通第一题:命令行环境准备与工程初始化

这套题集下载下来是一个或多个.csproj工程文件夹。很多新手折在第一步:不知道在哪里运行它。我的建议是,从一开始就使用命令行来编译和运行,不要依赖某个特定的集成开发环境。这样换机器、换编辑器都能跑,遇到报错还能直接看懂命令行里给的信息,排查起来更直接。

3.1 准备 .NET SDK 与工程结构:先用 dotnet --version 探底

在动手做题之前,先确保机器上有 .NET SDK。打开终端(Windows 下是 PowerShell 或 cmd,macOS/Linux 下是系统终端),执行:

dotnet --version

如果提示找不到命令,说明没装 .NET SDK。这时候需要先安装一个长期支持(LTS)版本的 SDK。我一般还会顺手执行dotnet --list-sdks,确认本机安装了几个版本。机器上同时存在多个版本的 SDK 时,dotnet run会默认选最新的一个,如果练习题项目里指定的目标框架版本更老,编译时会看到一长串兼容性提示,提前看一眼能省掉不少困惑。

这套题集常见的工程布局有两种。第一种是单工程多文件:一个.csproj对应一整套题,Program.cs里用switch按题号跳转到对应方法。第二种是每道题一个文件夹,每个文件夹里是一个独立的控制台工程,根目录放一个.sln解决方案文件把它们串起来。我在实际跑的时候更推荐第二种布局:

CSharpExercises/ ├── Exercise01_HelloWorld/ │ ├── Exercise01_HelloWorld.csproj │ └── Program.cs ├── Exercise02_OddEven/ │ ├── Exercise02_OddEven.csproj │ └── Program.cs ├── Exercise03_SumList/ │ ├── Exercise03_SumList.csproj │ └── Program.cs └── CSharpExercises.sln

这种多工程布局的好处是编译隔离。某一道题的代码写坏了,只影响它自己的工程,不会拖垮整套题。排查问题时,dotnet build会直接告诉你“在 Exercise03 里有个错误”,而不是在几千行的大文件里大海捞针。而且刷题时经常要跳着做,多工程可以自由切换,不用注释掉一大段不相关的代码。

3.2 用 dotnet run 跑通第一道题:从创建到输出

确认环境没问题后,先用最经典的方式创建一个控制台工程并跑起来:

dotnet new console -n Exercise01 cd Exercise01 dotnet run

这里-n参数指定工程名,dotnet new console会生成一个带.csproj和Program.cs的最小控制台工程,dotnet run会先编译再运行。如果工程文件不在当前目录,可以用dotnet run --project Exercise01.csproj指定路径,但一般情况下不需要。

跑通模板之后,替换成第一道练习题的实现。比如题集里常见的“读取一个名字并输出名字长度”:

string name = Console.ReadLine() ?? ""; Console.WriteLine($"你好,{name},你的名字有 {name.Length} 个字符。");

这里?? ""是处理 null 的惯用写法,name.Length拿到的是 UTF-16 码元数量。中文字符在这个计算里算一个,但 emoji 或生僻扩展字符可能算两个。这个细节在基础阶段不需要深究,但如果你输入的字符串里混入了 emoji,输出长度和预期不一致时,不至于一头雾水。

如果用的是 Visual Studio Code 而不是终端,装一下 C# 扩展,打开工程文件夹后按 F5 就能调试。命令行跑题和调试器跑题的区别在于:命令行适合快速验证输出,调试器适合在变量值不对时单步看执行过程。我一般先用命令行跑通,再用调试器打断点观察循环变量的变化。这道题练完后,建议把Console.ReadLine()换成固定字符串试一次,再把?? ""删掉试一次,体会两种写法的差异,这比多做十道题更能建立边界意识。

4. 典型题拆解:循环、集合与字符串处理的四个实战场景

这一章从题集里挑几类最有代表性的题,把从读题到写完代码的完整路径走一遍。重点不是“答案是什么”,而是“看到题目后怎么一步步把逻辑捋出来”,以及每段代码背后的参数选择。

4.1 用 List 循环读入并完成分组统计

题目:读取若干整数成绩,直到空行结束,输出及格人数和不及格人数。这是循环主题里比较综合的一道题,因为它同时用到了while、List<T>和foreach。

List<int> scores = new List<int>(); while (true) { string? input = Console.ReadLine(); if (string.IsNullOrEmpty(input)) break; if (int.TryParse(input, out int score)) scores.Add(score); } int pass = 0; foreach (int s in scores) { if (s >= 60) pass++; } Console.WriteLine($"及格人数: {pass}, 不及格人数: {scores.Count - pass}");

逻辑分三步:循环收集数据、遍历统计、输出结果。string? input中的?表示这个变量可能为 null,配合string.IsNullOrEmpty能同时处理“用户直接按回车”和“输入流结束返回 null”两种情况。TryParse比Parse安全之处在于它不会抛异常,解析失败时返回 false,并把out int score赋值为 0,但因为TryParse返回 false 时不会执行Add,所以 0 不会被错误地当成有效成绩存进列表。

这题有个很常见的变式:把“空行结束”改成“输入 -1 结束”。改法是把break条件换成if (score == -1) break;,但要注意-1本身不能被当成成绩存进去,所以TryParse和break的判断顺序要调整。先解析、再判断是不是 -1、最后再决定是否Add。这一个顺序问题,就是这道题真正的考点。

4.2 字符串拆分与过滤:一个参数避免空字符串陷阱

题目:输入“A,B,,C”这样的逗号分隔字符串,逐个输出其中的非空姓名。这题考察string.Split的重载参数。

string input = Console.ReadLine() ?? ""; string[] parts = input.Split(',', StringSplitOptions.RemoveEmptyEntries); foreach (string part in parts) { Console.WriteLine(part.Trim()); }

Split(',')会拆出四个片段,其中一个是空字符串;加上StringSplitOptions.RemoveEmptyEntries之后,空字符串会被自动过滤,输出就变成 A、B、C 三个。Trim()用来去掉姓名前后的空格,因为实际输入可能是A, B, C这种带空格的格式,不 Trim 的话输出会带着一个前导空格,字符串比较时会踩坑。

这个参数在中文场景里尤其重要。输入里如果出现全角逗号,Split(',')拆不动,输出会是完整一整行。遇到这种看起来“没生效”的情况,先检查输入里的逗号是英文逗号,还是中文逗号,。基础练习题不会故意为难你,但实际写代码时这是必踩的坑。

4.3 把业务规则封装到类里:属性、构造函数与方法的拆分

面向对象主题的题目开始要求你用类组织代码。最常见的例子是定义一个学生类,包含姓名和成绩,再算平均分。

class Student { public string Name { get; set; } public int Score { get; set; } public Student(string name, int score) { Name = name; Score = score; } }
List<Student> students = new List<Student>(); while (true) { string line = Console.ReadLine() ?? ""; if (line == "end") break; string[] parts = line.Split(','); if (parts.Length == 2 && int.TryParse(parts[1], out int score)) { students.Add(new Student(parts[0].Trim(), score)); } } double average = students.Count == 0 ? 0 : students.Average(s => s.Score); Console.WriteLine($"平均分: {average:F1}");

这里Average(s => s.Score)是 LINQ 的用法,=>是一个 lambda 表达式,含义是“取每个学生的 Score 属性”。{average:F1}中的F1是格式说明符,表示保留一位小数。这题的意义在于:数据从“散落的变量”变成“对象的属性”,后续不管是排序还是筛选,操作对象都比操作多个平行数组清晰得多。练习题做到这一步,基本就摸到工程代码的门槛了。

5. 避坑与排查:这套练习题最常见的五个翻车现场

刷这套题的过程中,新手翻车的点非常集中。下面五条是按出现频率排的,每一条都按“现象 → 原因 → 解决”来写,方便你对照排查。

5.1 dotnet run 报“找不到项目文件”

现象:在练习题根目录执行dotnet run,终端输出 MSB1003,提示找不到项目文件。 原因:根目录下没有.csproj文件。练习题是多个子工程,根目录只有.sln,而dotnet run默认寻找当前目录下的项目文件。 解决:先cd到某个子工程目录再运行,或者用指定路径的方式:

dotnet run --project Exercise01/Exercise01.csproj

5.2 控制台中文输出乱码

现象:程序里Console.WriteLine("你好"),运行时屏幕上显示方块或问号。 原因:Windows 终端默认代码页通常是 GBK,而 .NET 默认以 UTF-8 输出,编码不一致导致显示错乱。 解决:在程序入口处强制设置输出编码:

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

或者在终端执行chcp 65001把代码页切到 UTF-8。两种方式选一种就行,我推荐第一种,因为它在代码里留了记录,换机器也不会忘。

5.3 Console.ReadLine 与 int.Parse 联手崩溃

现象:输入空行后直接回车,程序抛FormatException;输入“12a”这种带字母的数字也崩溃。 原因:int.Parse要求输入字符串必须整体是合法的整数格式,空字符串和混合格式都无法解析。 解决:用int.TryParse代替int.Parse。前者的返回值是布尔值,解析成功时为 true,失败时为 false,配合out int value可以直接拿到解析结果,不会抛异常。

if (int.TryParse(Console.ReadLine(), out int value)) { // 只有解析成功才会走到这里 }

5.4 使用 List 时报“找不到类型或命名空间”

现象:代码里写了List<int> list = new List<int>();,编译时报错,说找不到List类型。 原因:旧版 .NET 模板需要手动引入using System.Collections.Generic;才能使用泛型集合。新版模板默认启用了隐式 using,所以不需要手写。 解决:先看报错的工程是基于哪个 SDK 创建的。如果代码文件顶部没有using System.Collections.Generic;,补上这一行再编译。如果补上之后还报错,检查是不是引用了错误的程序集或目标框架版本。

5.5 循环变量与外部变量同名

现象:在循环里写了for (int i = 0; i < 10; i++),循环外又声明了一个int i = 0;,编译报错 CS0136。 原因:局部变量的作用域覆盖了外部变量名,C# 不允许在嵌套作用域里重复声明同名变量。 解决:把外部变量改名,比如total或index。这种问题在练习题里看起来低级,但实际项目里经常发生在重构代码时——把一段逻辑从方法里抽出来,不小心和原变量重名。遇到 CS0136 别慌,改个名就好。

6. 把题集变成自测题:用批处理脚本批量验证与自动比对

刷完整套题之后,最大的遗憾是没有一个“一键验证”的机制。手动跑一遍所有题目,再逐个看输出对不对,十分消耗耐心。我后来做了一套批处理脚本,把整个题集变成可重复运行的自测题集。

思路很简单:每道题配两个文本文件,input.txt存放标准输入,expected.txt存放期望输出。脚本对每个子工程执行“编译并运行”,把input.txt的内容喂给程序,运行结果写入out.txt,再和expected.txt做逐行比对。

$projects = Get-ChildItem -Directory -Filter "Exercise*" foreach ($proj in $projects) { $inputFile = Join-Path $proj.FullName "input.txt" $expectedFile = Join-Path $proj.FullName "expected.txt" if (-not (Test-Path $inputFile)) { continue } dotnet run --project $proj.FullName < $inputFile > out.txt $diff = Compare-Object (Get-Content out.txt) (Get-Content $expectedFile) if ($diff) { Write-Host "FAIL: $($proj.Name)" } else { Write-Host "PASS: $($proj.Name)" } }

Get-ChildItem -Directory -Filter "Exercise*"会抓取所有以 Exercise 开头的子目录;< $inputFile是 PowerShell 的输入重定向,把文件内容当作程序的标准输入;Compare-Object按行比较两个文件,返回非空就说明输出不一致。脚本跑完,屏幕上直接列出失败的项目名,然后只需要去修那几道题。

这套脚本最大的价值是让“刷题”变成“验证”。从那以后,我拿到任何一套练习题,都会先拆出输入输出,再跑一遍批量验证脚本,确认环境能编能跑、输出和参考答案一致,再往深里刷。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进