☰
C#编程入门主线:从环境项目结构到类型系统与异步避坑
2026/10/28 13:42:49 网站建设 项目流程

简介:《C# 玩家指南》第三版PDF面向零基础编程新手及希望系统补齐C#知识体系的自学者,围绕C#语言与Visual Studio 2017的使用展开,帮助读者从编写第一个程序逐步过渡到面向对象与高级主题。全书按入门、基础、面向对象、高级主题、工具与实战五部分组织,涵盖变量、类型系统、基本数学、用户输入、决策与循环、数组、枚举、方法、值类型与引用类型,并深入类与对象、继承、多态、泛型,以及文件、异常、线程等进阶内容。压缩包内为1个PDF文件,大小约9.27MB,便于在电脑或平板端离线阅读与检索。目前已有131人学习,适合作为课程配套参考或自学路线中的长期案头书。读者可借助清晰的分阶段目录建立完整知识框架,在Visual Studio实操中理解语法与.NET平台开发要点,并通过高级主题章节衔接后续项目实践,提升代码组织与调试效率。

1. C# 编程入门真正的门槛不在语法:先看清这份资源的主线

装完 Visual Studio,打开第一份 C# 代码,很多人会愣一下:解决方案资源管理器里一堆文件,Program.cs里只有几行,跑起来一个黑窗口一闪而过。C# 编程入门的难点往往就在这里:没人告诉你这些文件之间怎么串起来,语法学到哪一步才能写点有用的东西。这份《C# 玩家指南(第三版)》的目录给出的主线很清晰:从安装 Visual Studio、Hello World 起步,依次进入变量与类型系统、控制流、数组与方法,再抬到面向对象编程、泛型、委托事件、异步,最后落在 .NET 平台、调试技巧和项目文件组织上。五十来个章节覆盖的是一个新手从零到能独立改代码、能看懂别人项目的主干路径。它适合完全没碰过 C# 的人,也适合写过其他语言、想把 C# 的强类型体系和 Visual Studio 这套工具链一次性弄明白的开发者。书里对应的是 Visual Studio 2017 那一代,版本号旧了,但类型系统、面向对象、异步这些核心内容不会过时,跟着当下的新版 IDE 走完全没问题。

提示:这是一份付费数字读物,仅通过作者本人的官方分发页面流出,正文里也写明了禁止通过邮件、网盘或任何站点二次传播。拿到手之后自己存好就行,别往群里丢。

2. 装环境、看懂项目结构:第一次编译到底在编译什么

很多人把 Visual Studio 当成一个“写代码的记事本”,装完就开始抄例子,结果第一步就卡在“新建项目”那一屏:控制台应用、类库、WPF、ASP.NET,一大堆模板不知道该点哪个。其实这几十个模板最终都归到两三种产物上,选错了后面全是麻烦。这一章把安装、项目结构、编译这条链路拆开,走通一遍之后,你再看任何一个 C# 项目都不会发怵。

2.1 工作负载别全勾,先装三样够用

Visual Studio 的安装器用的是“工作负载”模型,每个负载是一大包 SDK、模板和工具。新手最常见的错误是怕少装,把能勾的全勾上,结果几十个 G 下去,磁盘告急,而真正用到的可能不到十分之一。常见做法是按学习阶段分两批装:第一批只装能跑通控制台程序的最小集,第二批等真的要做界面或者接口再加。

工作负载装了什么什么时候需要
.NET 桌面开发.NET SDK、控制台/WinForms/WPF 模板、调试器学语法阶段必装
ASP.NET 和 Web 开发Web API、MVC 模板、IIS Express要写接口或网页时再加
数据存储和处理SQL Server 工具、LINQ to SQL 相关组件要连数据库时再加
单个组件里的 .NET SDK编译器、运行时、dotnet CLI只要跑控制台,勾这一项也够

如果只是跟着这本书敲例子,勾“.NET 桌面开发”一个就够,它自带控制台项目模板和完整的调试器。装完第一次启动会问开发设置,选“常规”或者“C#”都行,差别只在默认快捷键布局。Mac 和 Linux 上没有完整版 Visual Studio,通常用 Visual Studio Code 加 .NET SDK,命令行dotnet new console建项目、dotnet run跑程序,书里的语法部分完全通用,只是调试和项目属性面板长得不一样。

装完之后先别急着写代码,在命令行验证一下环境:

dotnet --version # 看 SDK 装没装上,输出版本号即可 dotnet --list-sdks # 机器上存在的所有 SDK 版本

这两条命令能输出内容,说明 .NET SDK 这一层是通的,后面 IDE 里出问题大概率是模板或项目配置的事,排查范围一下就缩小了。我一般还会顺手dotnet new console -o Demo建一个空项目跑一次,确认编译链路没有环境变量之类的暗坑。

2.2 项目、解决方案、程序集,三个词别混着用

目录里最容易被忽略的是这三个概念的层级关系。新手看到“解决方案资源管理器”里一堆节点,改错了文件、删错了目录很常见,根因就是没分清谁包含谁。

MySolution/ # 解决方案目录,对应一个 .sln 文件 ├── MySolution.sln # 解决方案:只是一个容器,引用若干项目 └── HelloWorld/ # 项目目录,对应一个 .csproj 文件 ├── HelloWorld.csproj # 项目:一次编译的单元,记录引用了哪些包 ├── Program.cs # 源码,编译后进 bin 目录下的 dll ├── bin/ # 编译产物:HelloWorld.dll / .exe └── obj/ # 中间文件:缓存、还原信息,别手动改

解决方案(.sln)本质是一个文本清单,记录“这个仓库里有哪几个项目、用什么配置编译”,它自己不含任何代码。项目(.csproj)才是编译单元,一个项目编译出一个程序集。程序集(assembly)就是bin目录下那个.dll或.exe,是运行时的最小加载单位。

搞清楚这层之后,几个高频困惑自然就解了:为什么bin和obj要加进.gitignore(都是可重新生成的中间产物);为什么 A 项目引用 B 项目之后能直接用 B 里的类(编译时会把 B 的程序集一起带上);为什么删掉bin再编译一次经常能解决莫名其妙的报错(清掉了脏缓存)。一个解决方案里放多个项目是很常见的组织方式,比如一个类库放业务逻辑、一个控制台项目当入口,两者靠项目引用串起来,这也解释了书里第 46 章为什么专门讲“依赖与多项目”。

2.3 从新建到运行:最小闭环里每一行在干什么

新建一个控制台项目,模板会给你一份能直接跑起来的代码。别看它短,入口方法、命名空间、输出三条主线全在里面。

using System; // 引入命名空间,Console 类在这里 namespace HelloWorld // 命名空间:给类型分组,避免重名 { class Program // 类是所有代码的容器,哪怕只有一个 Main { static void Main(string[] args) { // Main 是程序入口,签名固定这几种变体 Console.WriteLine("Hello World"); Console.ReadKey(); // 阻塞等待按键,防止窗口一闪而过 } } }

Main是运行时找的入口点,签名可以是void也可以是int(返回退出码),可以带string[] args接命令行参数,也可以不带。static表示它属于类本身而不是某个对象实例,所以在创建任何对象之前就能被调用。Console.ReadKey()这一行在真实项目里没人写,但练习阶段很实用:它让控制台停在那里,你能看清输出结果。

编译运行有两个快捷键,行为完全不同。Ctrl+F5是“开始执行不调试”,程序跑完窗口会停住等你按键;F5是“启动调试”,会挂上调试器,抛异常时能断在出错行,但正常结束后窗口直接关。调试阶段一律用F5,只为了看一眼输出用Ctrl+F5。编译本身对应Ctrl+Shift+B,它只生成不运行,看“输出”窗口底部那句“生成成功”或者错误列表里的红叉,比盯着黑窗口猜快得多。

3. 类型系统与运算:为什么1/2等于 0

C# 是强类型语言,每个变量在声明那一刻就定死了类型,后面所有运算规则都由它决定。这一点和脚本语言差别巨大,也是新手最容易“代码看着没错,结果就是不对”的地方。这一章按“变量里存什么 → 数值类型的边界 → 字符串怎么拼 → 控制流怎么写 → 方法怎么传参”的顺序推一遍,每一节都给能直接抄进项目的写法。

3.1 变量里到底存了什么:值类型和引用类型

同样一句赋值b = a,值和引用的行为完全相反,这是后面无数 bug 的源头。值类型(int、double、bool、char、struct、enum)的变量里直接存数据本身,赋值就是复制一份;引用类型(class、string、数组、委托)的变量里存的是一个地址,赋值只复制地址,两个变量指向同一块内存。

int a = 10; int b = a; b = 20; Console.WriteLine(a); // 10,值类型复制的是内容,互不影响 int[] arr1 = { 1, 2, 3 }; int[] arr2 = arr1; arr2[0] = 99; Console.WriteLine(arr1[0]); // 99,引用类型复制的是地址,指向同一个数组

跑一遍这段,10和99会牢牢刻进脑子里。实际写代码时的推论是:把数组或对象传给方法,方法里改了内容,外面也会跟着变,因为传的是引用;而传int、传struct,方法里怎么改都不影响外面。第 16 章把这一节单列出来,就是因为后面讲类、讲继承、讲泛型全建立在这个前提上。

3.2 八种整数类型和它们的边界

C# 的整数不是只有int,按位数分了好几种,选错了在小数据量下看不出来,数据一多就出溢出问题。

类型位数取值范围典型用途
sbyte8-128 ~ 127少见,和字节流打交道时
byte80 ~ 255图片像素、二进制协议
short16-32768 ~ 32767少见
ushort160 ~ 65535少见
int32约 -21.5 亿 ~ 21.5 亿默认整数,绝大多数场景
uint320 ~ 约 42.9 亿位运算、协议字段
long64约 ±9.2×10^18时间戳、大数计数
ulong640 ~ 约 1.8×10^19少见

写代码时的默认选择就是int,不够大再升long。int的上限是 2147483647,int.MaxValue + 1不会报错,会静默回绕成-2147483648,这个现象叫溢出。如果那段逻辑不能容忍回绕,用checked包起来,溢出时直接抛OverflowException,让问题在编译期附近就暴露,而不是在数据库里留下一条负数金额。

int max = int.MaxValue; // 2147483647 int wrap = max + 1; // -2147483648,静默回绕 // checked { int boom = max + 1; } // 抛 OverflowException,适合金额类计算

3.3 整数除法、浮点精度与 decimal 的取舍

1 / 2在 C# 里等于0,因为两边都是int,编译器先做整数除法截断,再把结果赋给左边的double。这个顺序是写反过来的常见错误:你以为先提升了类型,实际是先截断。

int total = 7; int count = 2; double wrong = total / count; // 3,不是 3.5 double right = (double)total / count; // 3.5,先转类型再除 double right2 = total / (double)count; // 3.5,转任意一边都行 decimal money = 19.9m + 0.1m; // 20.0,精确 double approx = 0.1 + 0.2; // 0.30000000000000004

参数说明:(double)是显式转换(cast),优先级高于除法,所以会把total先变成7.0,之后的除法走浮点规则。m后缀表示decimal字面量,不加后缀的小数字面量默认是double。

选型的经验规则很稳定:涉及金额、单价、税率一律用decimal,它用十进制表示,不会有0.1 + 0.2 != 0.3这种问题;科学计算、图形、性能敏感的场景用double,它底层是二进制浮点,速度快但精度是近似的;float只在显存、文件格式、图形库里见得多,一般业务代码用不上。金额算错一次,后面查账的时间够你把这条规则背一百遍。

3.4 字符串:转义、逐字、插值与比较

字符串是引用类型,但行为有一点特殊:它是不可变的,任何“修改”其实都新建了一个对象。循环里用+拼几百次字符串,就会新建几百个对象,这是性能问题里最经典的一条。

string name = "C#"; string path = "C:\\temp\\log.txt"; // 反斜杠要转义 string verbatim = @"C:\temp\log.txt"; // @ 开头的逐字字符串,反斜杠不转义 string msg = $"欢迎,{name},当前 {DateTime.Now:HH:mm}"; // 插值,$ 开头 bool same = name == "C#"; // true bool ignoreCase = string.Equals(name, "c#", StringComparison.OrdinalIgnoreCase); // true

对比一下:==在string上被重载成了“比较内容”而不是“比较引用”,所以直接写==是对的;但如果要忽略大小写、或者做区域性相关的比较,必须显式用string.Equals传StringComparison参数。默认的==是区分大小写的,判断用户输入、比对配置项时容易翻车。

拼接大量文本时换成StringBuilder,它在内部维护一个可变缓冲区,Append不新建对象,最后ToString()出结果。判断字符串是不是空的,用string.IsNullOrEmpty和string.IsNullOrWhiteSpace,比== ""更稳,后者碰上空引用直接炸。

3.5 if、switch、循环与数组:把语法写成完整逻辑

控制流本身没什么难的,坑都在细节上。switch在 C# 里不允许“隐式贯穿”:写完一个case必须break、return或goto,不能像 C 那样自然地掉进下一个分支。想多个值走同一段逻辑,就把case叠起来写。

int score = 85; if (score >= 90) { Console.WriteLine("优秀"); } else if (score >= 60) { Console.WriteLine("及格"); } else { Console.WriteLine("不及格"); } int level = 2; switch (level) { case 1: Console.WriteLine("初级"); break; // 必须显式跳出,否则编译不过 case 2: case 3: Console.WriteLine("中级"); // 多个 case 共用一段逻辑 break; default: Console.WriteLine("未知"); break; } int[] scores = new int[5]; // 长度 5,元素默认 0 scores[0] = 90; int[,] grid = new int[3, 4]; // 二维数组,3 行 4 列 foreach (int s in scores) // 只读遍历,不能在里面改集合结构 { Console.WriteLine(s); }

说明几点:数组声明时长度就固定了,new int[5]之后没法再扩,要动态长度得用List<T>。foreach的迭代变量是只读的,循环体里给它赋值编译就报错;而且遍历过程中增删元素会抛InvalidOperationException,这跟下一章的坑直接相关。二维数组用grid[行, 列]访问,和“数组的数组”int[][]是两种结构,内存布局不同,日常业务代码里前者更常见。

3.6 方法参数:ref、out 与返回多个值的写法

C# 默认按值传参,int传进去方法里怎么改外面都不变。需要让方法回写结果,有两种修饰符:ref要求调用前必须先初始化,out只要求方法内部必须赋值,适合“尝试解析”这类模式。

static void AddOne(ref int x) { x = x + 1; // 直接改调用方那个变量 } static bool TryDivide(int a, int b, out int result) { if (b == 0) { result = 0; // out 参数在返回前必须被赋值 return false; } result = a / b; return true; } // 调用 int n = 1; AddOne(ref n); // n 变成 2 if (TryDivide(10, 2, out int q)) // C# 7 起可以直接在调用处声明 Console.WriteLine(q); // 5

TryDivide这种“返回 bool + out 出结果”的模式在 .NET 基础库里到处都是,int.TryParse、Dictionary.TryGetValue都是这个形状。它比“解析失败就抛异常”更轻,因为异常有性能成本,用作流程控制不合适。要返回多个值又不想写 out,可以直接返回元组(bool ok, int value),现代写法更清爽。

4. 面向对象与泛型:类、继承、接口怎么选才不返工

语法阶段过了之后,下一个坎是设计:什么时候该建类,什么时候用继承,什么时候该抽接口。书里第三部分从“创建自己的类”一路讲到“创建泛型类型”,中间还插了一个井字棋的小项目,这个安排很实在——面向对象不是背概念,是把一堆散落的变量和方法归到合适的对象名下。

4.1 类与属性:封装不是只写 get 和 set

字段(field)是类内部的数据,属性(property)是对外暴露的访问口。新手常见的写法是字段全public,简单但等于放弃了封装:任何地方都能把它改成非法值。

public class Player { private int _hp; // 私有字段,外部看不到 public string Name { get; set; } // 自动属性,编译器生成隐藏字段 public int Hp { get => _hp; set => _hp = value < 0 ? 0 : value; // setter 里做范围校验 } public Player(string name) // 构造函数:创建对象时初始化 { Name = name; Hp = 100; } }

value是 setter 里隐含的入参名,不是变量声明。把校验放在 setter 里,是保证“任何路径改这个值都会被约束”的最省事办法,比在每个调用点判断一次可靠得多。构造函数的职责是把对象带到一个可用状态,尽量不要在里面做耗时 IO 或者调外部接口,否则单元测试会很难写。

如果某个属性只在构造时赋值、之后不再变,用public string Id { get; }加构造函数赋值,把不可变性做在类型层面,能省掉一整类“谁把它改了”的排查。

4.2 继承、虚方法、抽象类与接口

对象之间“是什么”的关系用继承,“能做什么”的关系用接口。这条规则听起来抽象,落到代码里其实很清楚:Circle是一种Shape,所以继承;Circle可以被缩放,所以实现IResizable。

public abstract class Shape // 抽象类:不能直接 new,只能被继承 { public abstract double Area(); // 抽象方法:子类必须实现 public virtual string Describe() // 虚方法:子类可以重写,也可以不重写 => $"面积 {Area():F2}"; } public class Circle : Shape { public double R { get; set; } public Circle(double r) => R = r; public override double Area() => Math.PI * R * R; } public interface IResizable { void Scale(double factor); // 接口只声明签名,不带实现 }

关键区别记住三条:abstract方法没有方法体,子类不实现就编译报错;virtual方法有默认实现,子类用override选择性覆盖;接口只规定签名,一个类可以实现多个接口,但只能继承一个基类。实际项目里,接口用得比继承频繁得多——依赖注入、单元测试打桩、解耦模块,都要靠接口。

设计时还有一个反直觉的结论:继承层级越深越脆弱,因为基类一改,所有子类都可能受影响。宁可用组合(把一个对象当字段持有)也不要为了复用两行代码去拉一条继承链。

4.3 泛型与约束:为什么List<T>比ArrayList好

ArrayList存的是object,什么都能塞,取出来要强制转换,转错了运行时炸。泛型把类型参数化,编译期就能检查,还省掉了装箱拆箱的开销。

public class Repository<T> where T : class, new() { private readonly List<T> _items = new List<T>(); public void Add(T item) => _items.Add(item); public T Create() => new T(); // new() 约束保证 T 有无参构造函数 public int Count => _items.Count; } // 用的时候类型是确定的,取出来不需要转换 var repo = new Repository<Player>(); repo.Add(new Player("张三"));

where后面的约束决定了T能用哪些操作。常用几种:where T : class限定引用类型,where T : struct限定值类型,where T : new()要求有无参构造函数,where T : IComparable<T>要求实现某接口。约束不是装饰,是让编译器知道你会在方法体里对T做什么,写new T()就必须有new()约束,否则编译不过。

泛型方法也能单独声明:static T Max<T>(T a, T b) where T : IComparable<T>,这样int、DateTime、string都能直接复用一套比较逻辑,不用为每种类型写一遍重载。

4.4 struct 还是 class:一个影响性能与正确性的选择

struct是值类型,赋值复制整个内容,适合小而简单的数据,比如坐标、金额、范围。class是引用类型,赋值复制地址,适合有身份、有行为、可能被多处共享的对象。

判断标准有两个:数据量小(经验上不超过 16 字节左右)、语义上是“值”而不是“实体”,就考虑struct;需要继承、需要引用语义、需要频繁在方法之间传递同一个实例,就用class。用错方向的代价不一样:把一个几百字节的大对象做成struct,每次赋值都复制一遍,性能会莫名其妙地掉;把本该是值语义的东西做成class,两处代码共用同一个对象,改一处影响另一处,排查起来非常难受。

5. 避坑与排查:五条我踩过的记录

这一章全部是“现象 → 原因 → 解决”的实录,都是跟着书敲例子、然后自己改需求时会撞上的。新手最耗时间的不是不会写,是写完了不知道错在哪,下面这几条覆盖了大部分场景。

5.1 改了代码,跑出来还是旧结果

现象:把Console.WriteLine里的文字改了,Ctrl+F5一跑,输出的还是上一次的内容,确认保存过了。

原因:三种情况最常见。一是解决方案里有多个项目,粗体的启动项目不是你以为的那个,跑的是另一个项目;二是Debug和Release配置切换了,改的是 Debug 下的代码却跑的 Release 产物;三是增量编译没识别到改动,或者 IDE 卡住了。

解决:先看解决方案资源管理器里哪个项目名是粗体,右键“设为启动项目”校正;再看工具栏的配置下拉框是 Debug 还是 Release;最后Ctrl+Shift+B重新生成,盯着“输出”窗口看有没有“生成成功”。还不行就去bin/Debug/下看 dll 的修改时间,时间不对说明根本没编译进去,直接删掉bin和obj再生成一次,能解决九成的玄学问题。

5.2 单价、平均分算出来全是 0

现象:算平均分total / count,两个变量都是int,结果是整数,小数部分直接没了;或者算单价时明明除法没错,赋值给double之后还是整数。

原因:C# 的除法运算符看的是两边操作数的类型,两边都是int就走整数除法,先截断再赋值,左边的double完全不影响计算过程。

解决:先转类型再算,(double)total / count或者total * 1.0 / count;涉及金额直接用decimal,别用double。养成的习惯是:只要出现除法,先问一句两边的类型是什么,再决定要不要转型。

5.3NullReferenceException找不出是哪一行

现象:程序跑到某处直接抛System.NullReferenceException,提示“未将对象引用设置到对象的实例”,但代码里看不出哪里有空。

原因:引用类型变量声明了但没new,或者方法在某些分支下返回了null,或者从数组、字典里取出来的元素本身就是null。编译期不检查这个,只有运行时才炸。

解决:在异常弹窗里点“查看详细信息”,堆栈最上面那一行就是出事的位置,鼠标悬停看涉及的变量是不是null。防御性写法用?.和??:

string s = null; // Console.WriteLine(s.Length); // 直接抛 NullReferenceException Console.WriteLine(s?.Length ?? 0); // 输出 0,不抛

如果项目允许,把“可空引用类型”打开(.csproj里加<Nullable>enable</Nullable>),编译器会把可能为null的地方标黄,把运行时错误提前到编译期。

5.4 方法里改了 struct 的字段,出来还是旧值

现象:把一个struct传进方法,方法里改了它的字段,回到调用处一看,纹丝不动。

原因:struct是值类型,默认按值传参,方法拿到的是副本,改的是副本。这个行为和class完全相反,所以从类切到结构体时特别容易忘。

struct Point { public int X; } static void Move(Point p) { p.X = 100; } // 改的是副本,外面不变 static void MoveRef(ref Point p) { p.X = 100; } // 改的是原变量 var pt = new Point { X = 1 }; Move(pt); // pt.X 还是 1 MoveRef(ref pt); // pt.X 变成 100

解决:需要回写就用ref传参;或者让方法返回一个新的struct,调用方接住;再或者重新评估一下,这个类型是不是本来就应该做成class。

5.5async方法忘了await,结果永远拿不到

现象:调用一个异步方法,直接取.Result或者不等待就往下走,拿到的数据不对,界面还会卡住;严重的时候整个程序死锁。

原因:async方法返回的是Task<T>,是“将来会有结果”的凭据,不是结果本身。不await就直接用,操作的是那个Task对象;用.Result同步阻塞去等,在 UI 线程或者有同步上下文的环境里容易死锁。

static async Task<string> LoadAsync() { await Task.Delay(100); return "数据"; } // 错误:同步阻塞,可能死锁 // var data = LoadAsync().Result; // 正确:一路 async/await 到底 static async Task Main() { string data = await LoadAsync(); Console.WriteLine(data); }

解决:把调用链一路async上去,从Main到最底层都用await,不要在中途.Result或.Wait()。只有最外层是控制台程序时,用async Task Main作入口,运行时会把异步执行到底。

6. 从能跑到能改:用一个小项目把基础串一遍

书里的语法点单独看都懂,合起来用就露怯,原因是缺少一个“把所有知识点连起来”的载体。我的做法一直是:学完基础就写一个控制台版记账本,只做三件事——追加一笔、读回所有记录、汇总金额。它同时用到文件读写、异常处理、集合、LINQ 和字符串处理,写完基本就把主干串起来了。

using System; using System.Collections.Generic; using System.IO; using System.Linq; class Program { static void Main() { const string file = "records.csv"; var lines = new List<string>(); try { if (File.Exists(file)) lines.AddRange(File.ReadAllLines(file)); } catch (IOException ex) // 只捕获预期的异常,别 catch 所有 { Console.WriteLine($"读取失败:{ex.Message}"); return; } lines.Add($"{DateTime.Now:yyyy-MM-dd},{99.5m}"); // 追加一笔 File.WriteAllLines(file, lines); var total = lines .Select(l => l.Split(',')) .Where(p => p.Length == 2) // 过滤掉脏行 .Select(p => decimal.Parse(p[1])) .Sum(); Console.WriteLine($"共 {lines.Count} 笔,合计 {total:F2}"); } }

这段的每一行都能对应到书里的一个章节:List<string>是泛型集合,File.ReadAllLines是文件读写,try/catch/IOException是异常处理,=>是 Lambda,Select/Where/Sum是查询表达式,decimal.Parse是类型转换。跑通之后,试着改几个参数验证自己是不是真懂了:把金额改成double看会不会出现尾数误差,把catch换成catch (Exception)看会不会把本该暴露的 bug 吞掉,把Split换成固定分隔符再跑一次看脏数据怎么处理。

改动点该观察什么对应的知识点
金额类型换成double累加后是否出现0.30000000000000004浮点精度与decimal选型
catch范围放大文件路径写错时还能不能看到原因异常粒度
用foreach遍历时删元素是否抛InvalidOperationException集合迭代限制
拼字符串换成StringBuilder改写一千行时耗时变化字符串不可变性

我自己的习惯是每学完一批语法点,就回头把这个小项目重写一遍,不加功能,只换写法:第一遍全用for,第二遍换foreach,第三遍抽成方法和类,第四遍加异常处理和泛型仓储。写不动的地方,就是没真正掌握的地方,比刷十道选择题管用。

从那以后我每次带新人,都强制让对方先把环境、项目结构、类型系统这三块走一遍,再动手写业务代码,跳过这一步的人后面总要在某个深夜对着一个NullReferenceException反复猜。希望帮到你。

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

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

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

立即咨询