
简介一份面向C#初学者的软件代码练习包围绕基本语法与数据类型、条件与循环、函数与方法、数组与集合、类与对象、接口与抽象类、异常处理、枚举与委托、文件与流、LINQ、异步编程和泛型等核心知识点组织适合正在系统入门或准备查漏补缺的学习者通过动手编码巩固基础。压缩包共1670个文件约17.87MB内含224个cs源码、94个csproj工程文件、82个sln解决方案以及pdb、exe、resx、jpg、ico等编译输出和配套资源多数练习可按解决方案直接打开、编译与调试。已有731人学习说明其作为练习素材具有一定参考价值。读者可借助分层练习逐项验证变量声明、循环控制、值传递与引用传递、集合增删改查、接口实现、异常捕获、LINQ查询及async/await异步方法等常见考点也能从完整工程结构中熟悉C#项目的组织方式、资源引用和调试流程对于需要从语法练习过渡到小型项目实践的读者这份题库提供了较密集的动手样本与排错线索。1. 从一份 C# 基础练习题说起它究竟练的是什么手感很多人学 C# 卡住的地方不是看不懂语法而是打开编辑器之后不知道下一行该写什么。手上这份 C# 基础练习题就是针对这个尴尬来的它把变量与类型、分支循环、数组与字符串、类与接口、集合与 LINQ 拆成一批能直接运行的控制台小题每题一个入口改完就能看到输出。它适合两类人一是刚过完语法教程、急需手感的新手二是要带新人、想找一套现成练手素材的老开发。整套东西不碰框架也不碰界面纯控制台加 .NET SDK跑起来的门槛很低。但也正因为简单不少人做完前五题就停了真正值钱的那部分——怎么把这些小题组织成一个能反复练、能自动判对的工程——反而被跳过去了。2. 工程骨架把零散小题组织成一个能编译的仓库练习题最容易写成“一堆随手丢的 Program.cs”。单题能跑一多就乱想回头复习第三题得翻半天文件夹想确认自己没把前面的题改坏只能一个个手动跑。所以第一步不是写代码是先把骨架搭对让dotnet build一次编译全部题目让每道题的入口互不干扰。这一步花十分钟后面能省掉大量返工。2.1 用 dotnet CLI 搭出题目仓库我一般不用 IDE 的图形化新建项目直接用命令行因为命令可复制、可写进脚本下次重建题库时改两行就行。核心思路是“一个解决方案 每题一个控制台项目”这样每道题都有自己的Main互不抢占入口点。# 建总目录后面所有命令都在这个目录里执行 mkdir CSharpBasics cd CSharpBasics # 创建解决方案文件用来统一管理所有题目项目 dotnet new sln -n CSharpBasics # 第 1 题变量、类型与类型转换 dotnet new console -n Ex01_Types -o src/Ex01_Types dotnet sln add src/Ex01_Types/Ex01_Types.csproj # 第 2 题分支与循环 dotnet new console -n Ex02_Loops -o src/Ex02_Loops dotnet sln add src/Ex02_Loops/Ex02_Loops.csproj # 第 3 题数组与字符串 dotnet new console -n Ex03_ArrayString -o src/Ex03_ArrayString dotnet sln add src/Ex03_ArrayString/Ex03_ArrayString.csproj # 一次性编译整个解决方案确认骨架没问题 dotnet builddotnet new console生成的是最小可运行模板只有项目名.csproj和Program.cs两个文件。-n是指项目名会同时决定程序集名和默认命名空间-o是输出目录我习惯统一放到src/下根目录只留解决方案文件。dotnet sln add把项目挂进解决方案之后dotnet build不带参数就会编译全部能立刻发现“改了第 2 题却把第 3 题改崩了”这种连锁问题。单题运行用dotnet run --project src/Ex02_Loops--project后面跟的是项目目录而不是解决方案这点别搞混否则会报“找不到可运行的项目”。2.2 用 Directory.Build.props 统一目标框架如果每个 csproj 各写一套配置练到第十题你就会发现有的项目目标框架是旧版、有的没开可空引用行为不一致排查起来很不方便。.NET 的 MSBuild 支持在目录层级放一个Directory.Build.props子目录里的项目会自动继承这是我做练习仓库时必加的一层。Project PropertyGroup !-- 所有题目统一目标框架避免每题语法能力不一致 -- TargetFrameworknet8.0/TargetFramework !-- 隐式 using省掉每題都写 using System; -- ImplicitUsingsenable/ImplicitUsings !-- 开启可空引用类型强制练习时正视 null -- Nullableenable/Nullable LangVersionlatest/LangVersion !-- 练习题不需要发布产物关掉能少一堆 bin/obj 噪音 -- GenerateDocumentationFilefalse/GenerateDocumentationFile /PropertyGroup /Project这几个参数值得逐个说清楚。TargetFramework决定能用哪些 API写死成一个长期支持版本能避免“网上抄的代码在我这编译不过”。ImplicitUsings打开后System、System.Collections.Generic、System.Linq这些常用命名空间自动引入练习题代码能短很多。Nullable是最有争议的一个打开后写string s Console.ReadLine();会直接给警告新手第一反应是烦但这恰恰是 C# 里最该早点撞上的墙第 5 章会专门讲怎么处理。提示如果本机只装了某一个版本的 SDK把net8.0改成dotnet --list-sdks里实际存在的版本别硬抄。2.3 题目分类与目录清单题目如果只是按“第 1 题、第 2 题”平铺复习时完全没有线索。我习惯按语法主线切成七个台阶每道题挂在一个台阶下目录名直接体现考察点找题时看目录就够。台阶目录示例主要考察点建议题量类型与转换Ex01_Types值类型、引用类型、TryParse、格式化输出46分支与循环Ex02_Loopsif/switch、for/while、嵌套循环58数组与字符串Ex03_ArrayString遍历、排序、string常用方法58类与对象Ex04_Oop字段、属性、构造、封装46继承与接口Ex05_Interface抽象类、接口、多态35集合与泛型Ex06_CollectionListT、DictionaryK,V、泛型方法46LINQ 与异常Ex07_Linq查询表达式、链式调用、try/catch46对应的目录结构大致是这样后面加题只要往src/里塞新项目再dotnet sln add就行CSharpBasics/ ├── CSharpBasics.sln ├── Directory.Build.props # 全局编译配置所有题目继承 └── src/ ├── Ex01_Types/ │ ├── Ex01_Types.csproj │ └── Program.cs ├── Ex02_Loops/ │ ├── Ex02_Loops.csproj │ └── Program.cs └── ...这套结构的好处是“可增量”。今天只做类型转换就只dotnet run --project src/Ex01_Types哪天想整体回归一遍dotnet build全量编译能通过的说明语法层面没退化。至于每道题内部的代码怎么写就是下一章的事了。3. 核心题型拆解五类题目背后的 C# 语法主线练习题的价值不在于题目本身多难而在于每道题是不是真的逼你用上了某个语法点。这一章按难度递进拆四类典型题每类给一段能直接跑的代码并说明它到底在练什么、参数怎么改、改错会看到什么现象。看的时候建议对着自己的Program.cs一起改别只读。3.1 输入输出与类型转换最容易被跳过的一关第一类题通常是“输入半径算圆面积”这种看起来简单到没必要写但它是新手翻车最集中的地方。原因在于控制台输入的原始形态永远是字符串而Console.ReadLine()在开启可空引用后返回的是string?直接double.Parse一旦遇到空输入或者用户手抖打了字母整个程序就崩了。// 计算圆面积演示输入校验 数值格式化 Console.Write(请输入半径); string? raw Console.ReadLine(); // TryParse 返回 bool转换成功才把结果写进 out 参数 // 这里同时校验了“能转成 double”和“必须大于 0”两个条件 if (!double.TryParse(raw, out double radius) || radius 0) { Console.WriteLine(输入不合法请输入一个大于 0 的数字。); return; } double area Math.PI * radius * radius; // F2 表示保留两位小数会做四舍五入 Console.WriteLine($半径 {radius:F2} 的圆面积是 {area:F2});double.TryParse的第一个参数是待转换字符串第二个是out参数转换失败时它会被赋成0所以判断必须用返回值而不是看radius是不是 0——这是很多人第一次踩的坑。Math.PI是常量不用自己写 3.14。格式串里的F2是标准数值格式符F表示定点2是小数位数想改成三位就写F3想去掉千分位就用N2换成N。这段代码还有个隐藏考点return写在顶层语句里表示结束程序。如果你把它放进一个方法里return只会退出方法后面的代码还会继续跑逻辑就错了。新手常犯的错是把校验写在方法里却忘了返回一个“失败标记”。3.2 循环与数组把排序和图形打印写扎实第二类题是循环。很多人觉得循环没什么可练的但嵌套循环的边界控制是后面所有算法的地基。我建议至少手写一遍冒泡排序和一次图形打印不要复制粘贴。// 冒泡排序演示数组遍历 嵌套循环 提前退出优化 static int[] BubbleSort(int[] source) { // 复制一份避免修改调用方传进来的数组 int[] data (int[])source.Clone(); int n data.Length; for (int i 0; i n - 1; i) { bool swapped false; // 本轮是否发生交换 // 每轮结束最大的元素已经沉到末尾所以内层可以少比一次 for (int j 0; j n - 1 - i; j) { if (data[j] data[j 1]) { (data[j], data[j 1]) (data[j 1], data[j]); // 元组交换 swapped true; } } if (!swapped) break; // 已经有序提前收工 } return data; } int[] input { 5, 2, 9, 1, 5, 6 }; Console.WriteLine(string.Join(, , BubbleSort(input))); Console.WriteLine($原数组未被修改{string.Join(, , input)});为什么要有n - 1 - i这个减法因为每一轮外层循环结束后末尾的i 1个元素已经排好序了再比就是浪费。把这个减法写错成n - 1程序还能跑结果也对只是多做了无用的比较属于“对但不够好”调试时根本发现不了。swapped标记是提前退出优化最好的情况数组本来有序复杂度从 O(n²) 降到 O(n)。(data[j], data[j 1]) (data[j 1], data[j])是 C# 的元组解构赋值等价于借助临时变量交换但少写三行。如果目标框架版本太老不支持就老老实实写临时变量别硬用。图形打印题比如输出等腰三角形、九九乘法表练的是“循环变量和输出位置的关系”。这类题没有标准答案但有个自检方法把行数从 5 改成 3 和 7看图形是不是仍然对称。如果不改代码就变形说明边界写死了。3.3 面向对象类、属性、继承与接口的最小闭环到了这一层题目开始从“算一个值”变成“描述一个东西”。典型的题是“定义图形基类派生圆和矩形统一计算面积”。这类题看着像八股实际上是把封装、继承、多态三件事串起来的唯一低成本方式。// 抽象基类约束所有图形都必须能算面积和周长 public abstract class Shape(string name) { public string Name { get; } name; public abstract double Area(); public abstract double Perimeter(); public override string ToString() ${Name}: 面积{Area():F2}, 周长{Perimeter():F2}; } public sealed class Circle(double radius) : Shape(圆) { // 主构造函数参数直接参与属性初始化属性只读避免外部乱改 public double Radius { get; } radius 0 ? radius : throw new ArgumentOutOfRangeException(nameof(radius)); public override double Area() Math.PI * Radius * Radius; public override double Perimeter() 2 * Math.PI * Radius; } public sealed class Rectangle(double width, double height) : Shape(矩形) { public double Width { get; } width 0 ? width : throw new ArgumentOutOfRangeException(nameof(width)); public double Height { get; } height 0 ? height : throw new ArgumentOutOfRangeException(nameof(height)); public override double Area() Width * Height; public override double Perimeter() 2 * (Width Height); } // 多态调用方只认 Shape不关心具体是哪种图形 ListShape shapes [new Circle(2), new Rectangle(3, 4)]; foreach (Shape s in shapes) Console.WriteLine(s);public abstract class Shape(string name)用的是主构造函数语法参数name可以直接用在属性初始化里省掉一个显式构造函数。sealed表示这个类不允许再被继承练习题阶段建议加上能避免“乱继承导致行为跑偏”的迷惑现象。属性初始化里那句radius 0 ? radius : throw new ...是三目运算符加抛异常的组合好处是把“不合法状态根本创建不出来”这条规则焊死在类型里而不是靠调用方自觉。nameof(radius)会在编译期变成字符串radius好处是以后重命名参数时异常信息会自动跟着改不会写死一个过期字符串。多态那三行是重点ListShape里放的是不同子类循环时调用s.Area()会自动走到各自的实现。如果这里写成Listobject再手动判断类型说明多态没理解到位题目就白做了。3.4 集合与 LINQ从 List 到链式查询最后一类题是集合操作。典型题是“给一批学生成绩按班级分组求平均分再按平均分倒序输出”。这题用传统for循环能写二十行用 LINQ 五行就完了但只写 LINQ 不写循环版本你不知道它到底替你做了什么。public record Student(string Name, string ClassName, int Score); ListStudent students [ new(A同学, 一班, 88), new(B同学, 一班, 72), new(C同学, 二班, 95), new(D同学, 二班, 61), new(E同学, 二班, 79), ]; // 按班级分组 → 算平均分 → 过滤掉平均分低于 70 的班 → 按平均分倒序 var report students .GroupBy(s s.ClassName) // 分组键是班级名 .Select(g new { Class g.Key, Count g.Count(), Average g.Average(s s.Score) // 组内对 Score 求平均 }) .Where(x x.Average 70) // 过滤 .OrderByDescending(x x.Average); // 排序 foreach (var item in report) Console.WriteLine(${item.Class}{item.Count} 人平均 {item.Average:F1} 分);GroupBy返回的是IGroupingTKey, TElement每个分组既能当集合遍历g.Count()、g.Average(...)又能通过g.Key拿到分组键这是它和Dictionary最大的区别。Select里用了匿名类型new { ... }适合只在当前方法里临时组装结果如果这个结果要跨方法传递就该定义一个record否则类型没法写在签名上。Where和OrderByDescending的顺序会对结果产生影响虽然这里逻辑上先过滤再排序和反过来结果一样但数据量大时先过滤能少排很多元素。这类“顺序无所谓但要养成习惯”的细节就是练习题和真实项目之间的差距所在。4. 自证与调试给每道题配一套能跑的校验练习题最大的问题是没人给你判对错。控制台输出一行“面积 12.57”你怎么知道是自己算对了还是碰巧如果每道题都靠肉眼对比练到第十题就会开始偷懒随手跑一遍看着像对的就过了。所以我在练习仓库里固定加一层极简断言让每道题都能自证。4.1 手写一个不到三十行的断言器不用引入第三方测试框架练习阶段一个静态类足够好处是零依赖、打开就能用也不会被一堆测试配置劝退。public static class Check { private static int _pass; private static int _fail; // 值比较用 Equals 而不是 能兼容装箱后的值类型和字符串 public static void That(string caseName, object? actual, object? expected) { if (Equals(actual, expected)) { _pass; Console.WriteLine($[通过] {caseName}); } else { _fail; Console.WriteLine($[失败] {caseName} 期望{expected} 实际{actual}); } } // 浮点比较单独处理直接 Equals 会因为精度问题永远不相等 public static void Near(string caseName, double actual, double expected, double tolerance 1e-6) That(caseName, Math.Abs(actual - expected) tolerance, true); public static void Summary() { Console.WriteLine($\n合计 {_pass _fail} 项通过 {_pass}失败 {_fail}); if (_fail 0) Environment.Exit(1); // 有失败就让进程返回非零方便脚本判断 } }Equals(actual, expected)是静态方法会自动处理 null比actual expected安全——后者在两边都是引用类型且没重载运算符时比的是引用地址字符串虽然重载过但其他类型不一定。Near专门给浮点用tolerance默认1e-6如果题目要求保留两位小数可以放宽到0.005。最后那个Environment.Exit(1)是给自动化留的口子有失败用例时进程退出码非零外面用 shell 或 CI 一判就知道过没过。4.2 把题目改造成“输入 → 计算 → 返回”三段式断言要能用前提是逻辑不能和Console.ReadLine搅在一起。做法很简单把纯计算抽成一个返回值的静态方法Main只负责读输入、调方法、打印结果。// 纯函数只依赖参数不碰控制台方便断言 static double CalcCircleArea(double radius) { if (radius 0) throw new ArgumentOutOfRangeException(nameof(radius), 半径必须大于 0); return Math.PI * radius * radius; } // Main 里只做 IO逻辑一行调用 Console.Write(请输入半径); if (double.TryParse(Console.ReadLine(), out double r)) { try { Console.WriteLine($面积 {CalcCircleArea(r):F2}); } catch (ArgumentOutOfRangeException ex) { Console.WriteLine(ex.Message); } } // 校验部分集中在文件末尾改题时只动上面不动这里 Check.Near(半径 2 的面积, CalcCircleArea(2), 12.566370, 1e-3); Check.Near(半径 0.5 的面积, CalcCircleArea(0.5), 0.785398, 1e-3); Check.That(半径 0 应抛异常, CatchType(() CalcCircleArea(0)), typeof(ArgumentOutOfRangeException)); Check.Summary();抽成纯函数之后同一份逻辑既能在Main里跑给人看又能被断言调用改一次两边都受益。CatchType是个十行的小工具捕获异常并返回类型static Type? CatchType(Action action) { try { action(); return null; } // 没抛异常返回 null catch (Exception ex) { return ex.GetType(); } // 抛了就返回实际类型 }参数Action表示“无参无返回值的委托”所以调用时要包成() CalcCircleArea(0)的 lambda。返回Type?是因为“没抛异常”这个分支没有类型可返回用可空类型表达最自然这也正好呼应了第 2 章开启的Nullable。4.3 边界用例与异常路径怎么定写完断言之后最容易被忽略的是“该测哪些值”。我的习惯是每题至少四类正常值一个、边界值一个0、空字符串、数组首尾、非法值一个负数、字母、null、规模极值一个空数组、单元素数组。这四类覆盖下来题目里的if分支基本都能走到。跑的时候用dotnet run --project src/Ex01_Types看输出里有没有[失败]。如果有先看期望值和实际值差多少差一点点多半是精度差一整倍多半是公式写错差个符号多半是边界条件取反了。这套流程走顺之后调试就从“盯着屏幕猜”变成“看断言报告定位”。5. 常见问题排查练习时最容易翻车的五个点下面这几条是我带人做练习时反复见到的基本每个新手都会中一两条。写法统一按“现象 → 原因 → 解决”来遇到时直接对号入座。5.1 语言与类型层面的坑坑一一开Nullable满屏黄色警告代码像错了一样。现象是刚在Directory.Build.props里打开Nullableenable/NullableProgram.cs里就出现CS8600、CS8602、CS8618一串警告程序还能跑但看着心里发毛很多人第一反应是把开关关掉。原因是可空引用类型把“这个变量可能是 null”从运行时隐患提前到了编译期提示。Console.ReadLine()确实可能返回 null比如输入流被重定向且没有内容string? raw接住它是对的但接着raw.Length就会报CS8602因为编译器无法保证raw非空。CS8618则是“非空属性在构造函数结束时还没赋值”。解决办法不是关开关而是把警告当提示用能判空的就判空if (raw is null) return;确实非空的就用!断言并写注释说明为什么安全属性用主构造函数或required修饰符保证初始化。练习题阶段建议全部老老实实判空别用!图省事这个习惯带到真实项目里会救命。坑二整数相除结果永远是整数平均分算出来少一截。现象是求平均分int avg total / count;输入 3 个 90 分期望 90 实际也是 90看着没问题但输入 3 个 88、89、90期望 89 实际 89还是看不出问题直到输入 2 个 1 和 2期望 1.5 实际 1。原因是 C# 里两个int相除结果类型还是int小数部分直接截断不是四舍五入。这是语言规定不是 bug所以不会有任何警告。解决办法是把其中一个操作数转成浮点double avg (double)total / count;或者total / (double)count。注意(double)(total / count)是错的因为括号里先算完除法再转换那时小数已经丢了。这类题我强烈建议在断言里放一组除不尽的用例不然永远发现不了。坑三浮点数用比较两个“相等”的值判成不等。现象是0.1 0.2 0.3返回false用Check.That断言直接失败但打印出来看两个值明明都是0.3。原因是double用二进制表示十进制小数0.1和0.2都无法精确表示相加后是一个极接近但不等于0.3的值。这跟语言无关是 IEEE 754 的固有特性。解决办法是所有浮点比较都改成“差值绝对值小于容差”也就是第 4 章那个Check.Near。容差取多少看题目要求算面积这种可以取1e-6要求保留两位小数的可以取0.005。涉及金额的场景干脆别用double改用decimal它不是二进制浮点能精确表示十进制小数代价是运算慢一点。5.2 工程与运行层面的坑坑四dotnet run报“找到多个入口点”。现象是命令行一跑就报CS0017: 程序定义了多个入口点编译直接失败一个题都跑不了。原因是你把两个Program.cs放在了同一个项目里或者给某道题新建了项目却没在正确的目录下执行dotnet run——顶层语句会自动生成Main两个文件就两个Main编译器不知道该用哪个。解决办法是确认“一题一项目”跑的时候带上--project指定到具体目录dotnet run --project src/Ex02_Loops。如果确实需要在同一项目里放多个练习入口那就把它们写成带名字的静态方法只在Program.cs里保留一个Main负责分发别再新增顶层语句文件。坑五控制台输出中文变成乱码。现象是打印“请输入半径”时终端里显示的是一串问号或者方块。原因是控制台编码和源文件编码不一致。源文件默认是 UTF-8而部分终端默认代码页不是 UTF-8输出时就会错位。解决办法有两个层次一是在Program.cs开头加一行Console.OutputEncoding System.Text.Encoding.UTF8;让输出走 UTF-8二是检查终端本身的编码设置把它调到 UTF-8。第一种更省事我一般在模板里就写死新题复制过去自动生效。顺带说一句读入中文时同理必要时把Console.InputEncoding也设上。6. 二次开发把题目集改成自己的题库与自动判题器练习题做通关之后多数人就停在那了。其实这套东西稍微改一改能变成一个自己长期用的刷题系统甚至给团队新人当入职训练。我自己的改法是三步把题目元数据化、把运行入口统一、把校验结果汇总成报告。第一步是给每道题加统一接口。现在每题各写各的Main想批量跑只能靠 shell 循环。改成定义一个接口每道题的逻辑实现它再写一个调度器用反射把所有实现找出来跑// 所有题目统一实现这个接口调度器只认它 public interface IExercise { string Id { get; } // 题号如 Ex03_02 string Title { get; } // 题名报告里显示 bool Run(); // 返回 true 表示本题全部用例通过 } // 以冒泡排序题为例 public sealed class Ex03_02_BubbleSort : IExercise { public string Id Ex03_02; public string Title 冒泡排序并返回新数组; public bool Run() { // 这里复用第 4 章的 Check但把 Summary 换成返回值 Check.That(普通数组, string.Join(,, BubbleSort([5, 2, 9])), 2,5,9); Check.That(已有序数组, string.Join(,, BubbleSort([1, 2, 3])), 1,2,3); Check.That(单元素数组, string.Join(,, BubbleSort([7])), 7); return Check.Failed 0; } private static int[] BubbleSort(int[] source) { /* 同第 3 章实现 */ return source; } }第二步是写调度器扫描当前程序集里所有IExercise实现并依次执行。Activator.CreateInstance要求类型有无参构造函数所以题目类别写带参构造。用typeof(IExercise).IsAssignableFrom(t)判断而不是t typeof(IExercise)前者能匹配到所有实现类包括间接实现。var exercises typeof(IExercise).Assembly.GetTypes() .Where(t typeof(IExercise).IsAssignableFrom(t) !t.IsAbstract !t.IsInterface) .Select(t (IExercise)Activator.CreateInstance(t)!) .OrderBy(e e.Id); int failed 0; foreach (var ex in exercises) { bool ok ex.Run(); Console.WriteLine(${(ok ? PASS : FAIL)} {ex.Id} {ex.Title}); if (!ok) failed; } Console.WriteLine($\n共 {exercises.Count()} 题失败 {failed} 题); Environment.Exit(failed 0 ? 0 : 1);第三步是把它接进日常流程。我会在仓库根目录放一个run-all.sh内容就一行dotnet run --project src/Runner然后每次改完题目先跑它。退出码非零就说明有题被改坏了这时候再去看具体是哪道题失败比逐个项目手点快得多。想再进一步可以把Check里的通过数、失败数输出成 JSON长期记录每道题的通过率哪题反复错就重点补那一块。改造过程中最容易翻车的地方是题目类没有无参构造函数反射时会抛MissingMethodException报错信息还不直接指向原因。血泪经验是题目类只放字段、属性和方法需要注入的东西一律通过方法参数传别在构造函数里做初始化。另外一个后悔药是给调度器加上 try/catch某道题抛异常时记一笔继续跑下一道否则一道题崩掉整个报告都出不来。从那以后我每次把练习仓库交给别人跑之前都会先在一个干净目录里dotnet build加run-all.sh走一遍确认没有隐式依赖本机环境的东西再发出去。这套习惯看起来啰嗦但省下来的解释成本远比这几分钟多。希望帮到你。本文还有配套的精品资源点击获取