C# 文档
简洁、以示例为主的 C# 参考文档。先读概念,再看代码,然后在 Coddy 学习旅程中动手练习。
开始课程入门
- C# 是什么C# 是 Microsoft 推出的一门静态类型通用编程语言,运行在 .NET 上。本文介绍它能用来做什么、和 .NET 是什么关系、每个版本新增了什么,以及一个 C# 程序是如何组织的。
- Hello World逐行讲解你的第一个 C# 程序:Main 方法,Console.WriteLine 和 Write,用 Console.ReadLine 读取输入,注释,用 dotnet CLI 创建和运行项目,以及顶级语句。
- C# 与 JavaC# 和 Java 看起来很像,解决的问题也相似。真正的区别在于属性、值类型、泛型、LINQ 与 Stream、异步、异常,以及围绕它们的生态系统。本文配合代码逐项对比。
- C# 与 C++C# 是带垃圾回收器的托管语言;C++ 编译为本机代码,让你手动控制内存。这个区别如何体现在安全性、性能、游戏引擎、构建方式以及学习难度上。
- 命名空间与 usingC# 命名空间如何组织类型,using 指令又如何导入它们:别名、using static、完全限定名、解决不明确的引用、全局 using 以及文件范围的命名空间。
基础
- 变量如何在 C# 中声明变量并赋值,什么时候适合用 var、什么时候不适合,作用域如何运作,const、readonly 和 static readonly 的区别,命名规范,以及“使用了未赋值的变量”错误。
- 数据类型C# 每种内置类型的大小、取值范围和字面量后缀,值类型与引用类型的区别,为什么金额要用 decimal,整数溢出与 checked,以及默认值。
- 字符串转 int如何在 C# 中用 int.Parse、int.TryParse 和 Convert.ToInt32 把字符串转换为 int,隐式转换和显式转换如何工作,为什么把 double 强制转换为 int 会截断,as 和 is 运算符,以及为什么 “1.5” 可能被解析成 15。
- 运算符C# 运算符及其确切行为:整数除法、负数取模、自增与复合赋值、比较、短路逻辑、位运算符、空值运算符,以及一张运算符优先级表。
- 字符串C# 中字符串的工作方式:不可变性,Length 与索引,用 ==、Equals 和 string.Compare 比较,转义字符,@ 逐字字符串与多行文本,原始字符串字面量,null 与空字符串检查,以及遍历字符。
- SubstringC# 中 String.Substring 的工作方式:起始索引和长度两个参数,ArgumentOutOfRangeException 以及如何避免它,根据 IndexOf 的结果截取前后的文本,取前 n 个或后 n 个字符,以及 C# 8 的范围运算符。
- 字符串插值C# 中 $"..." 字符串插值的工作方式:花括号里的表达式,F2、N0、C、D5、X 等格式说明符,用于表格的对齐,花括号转义,string.Format 与复合格式,逐字和原始插值字符串,以及按指定区域性格式化。
- 字符串方法C# 中每天都会用到的 String 方法:带多个分隔符和选项的 Split,Replace,Contains 与不区分大小写的查找,IndexOf 和 LastIndexOf,StartsWith 和 EndsWith,Trim,ToUpper 和 ToLower,PadLeft,Join,Concat,以及反转字符串。
- StringBuilder为什么在 C# 循环中反复拼接字符串很慢,以及 StringBuilder 如何解决这个问题:Append、AppendLine、AppendFormat、Insert、Remove、Replace、容量,以及什么时候直接拼接或用 string.Join 更合适。
- 随机数如何在 C# 中用 Random 类生成随机数:Next 与不包含的上限,指定范围内的随机 double,用种子得到可复现的结果,随机挑选元素,打乱列表,在循环里 new Random 的陷阱,以及密码学安全的随机数。
- DateTime在 C# 中处理日期和时间:创建 DateTime 值,Now、UtcNow 与 Today 的区别,加天数和月数,相减得到 TimeSpan,TotalHours 与 Hours 的区别,比较日期,DayOfWeek,用 ParseExact 和 TryParse 解析,DateTimeOffset 以及 DateOnly。
- DateTime 格式化如何在 C# 中把 DateTime 格式化为字符串:标准格式字符串(d、D、o、s、u、t)、yyyy-MM-dd HH:mm:ss 这样的自定义模式、完整的说明符表、区域性如何改变输出、用 "o" 实现 ISO 8601 往返,以及 mm 与 MM、hh 与 HH 的常见混淆。
流程控制
- if elseC# 中 if、else if 和 else 的工作方式:为什么条件必须是 bool,如何用 &&、|| 和 ! 组合判断,什么时候花括号很关键,用卫语句代替深层嵌套,以及 = 与 == 混用和多余分号导致的 bug。
- switch 语句C# switch 语句的工作方式:对 int、string、char 和 enum 值进行 switch,为什么每个 case 都需要 break(错误 CS0163),叠加 case 标签匹配多个值,goto case,default,字符串的大小写敏感性,以及带 when 的模式 case 标签。
- switch 表达式C# 8 的 switch 表达式用模式分支代替 case 和 break,把一个值转换成结果。语法,_ 弃元,属性、元组和关系模式,and/or/not,when 条件,穷尽性警告与 SwitchExpressionException,并附有可运行的 C# 7 等价写法。
- 三元运算符C# 的条件运算符 condition ? a : b 会从两个值中选出一个。它如何求值、为什么两个分支需要共同的类型(CS0173)以及修复它的强制转换、嵌套三元表达式、字符串插值中的括号,以及什么时候用 ?? 和 ?. 表达得更好。
- for 循环C# 的 for 循环如何工作:初始化、条件和迭代三部分,倒序计数与按步长递增,多个循环变量,按索引遍历数组,嵌套循环,for(;;),以及差一错误和边遍历边删除的 bug。
- foreach 循环C# 中 foreach 如何工作:遍历数组、列表、字符串和字典(KeyValuePair 与解构),获取索引,为什么循环变量是只读的,InvalidOperationException(“Collection was modified”)及其解决办法,以及一个类型要满足什么条件才能用于 foreach。
- while 循环C# 中 while 和 do-while 循环如何工作:条件在循环体之前还是之后检查,读取输入直到遇到哨兵值或输入结束,用 while (true) 配合 break,以及容易让人踩坑的无限循环错误。
- break 和 continueC# 中 break、continue 和 goto 的用法:提前结束循环、跳过一次迭代、switch 中的 break、跳出嵌套循环的三种方法(标志变量、goto、带 return 的方法),以及 while 循环里 continue 导致的死循环 bug。
方法与委托
- 方法如何在 C# 中声明和调用方法:返回类型与 void、参数、表达式主体方法、用元组返回多个值、重载、params 数组、静态方法与实例方法、Main 方法、递归和局部函数。
- 可选参数C# 中可选参数和命名参数的工作方式:默认值与编译时常量规则、参数顺序、按名称跳过参数、可选参数与重载的对比、调用方信息特性,以及默认值被编译进调用方所带来的版本陷阱。
- ref 与 outC# 中 ref、out 和 in 参数是如何工作的:传递变量本身而不是副本,用 out 返回额外的结果和 TryParse 模式,out var,只读的 in 参数,明确赋值规则,以及实参是引用类型时会有什么不同。
- Lambda 表达式C# 中 Lambda 表达式的工作方式:=> 语法的各种写法,把 Lambda 存进 Func 和 Action,把它传给方法和 LINQ,语句 Lambda,捕获变量的闭包,以及 for 循环里会出现、而 foreach 没有的变量捕获 bug。
- 委托C# 中的委托是什么、怎么用:声明委托类型,用方法、lambda 或匿名方法创建委托,调用委托,把委托用作回调,用 += 和 -= 实现多播委托,用 ?.Invoke 做空值检查,以及委托与 Func、Action 和事件的关系。
- Func 与 ActionC# 中 Func 和 Action 如何工作:Func<T, TResult> 用于有返回值的函数,Action<T> 用于没有返回值的函数,Predicate 和 Comparison,把函数作为参数传入和作为返回值返回,命令字典,异步 lambda,以及什么时候应该自己声明委托。
- 事件C# 中的事件是怎么工作的:用 EventHandler 和 EventHandler<T> 声明事件,自定义 EventArgs 类,用 += 和 -= 订阅和取消订阅,用 ?.Invoke 安全地触发事件,为什么事件比公共委托字段更好,以及忘记取消订阅导致的内存泄漏。
- 扩展方法C# 中的扩展方法是怎么工作的:静态类中第一个参数带 this 的静态方法,像实例方法一样调用它,扩展 string、枚举和 IEnumerable<T>,LINQ 如何用它们构建,为什么实例方法优先,null 接收者,以及它们需要的 using 指令。
集合与 LINQ
- 数组C# 数组是一块大小固定、元素类型相同的存储区域。本文讲解如何声明和初始化数组、读取 Length、用 Array 类排序和查找、安全地复制数组,以及如何使用二维数组和交错数组。
- ListList<T> 是 C# 中可以自动扩容的数组。学习如何创建列表,添加和插入元素,按值、索引或条件删除,用 Contains 和 Find 查找,按属性排序,以及避免在 foreach 中修改列表导致的错误。
- DictionaryDictionary<TKey, TValue> 把键映射到值,查找速度很快。学习如何添加和更新条目、用 TryGetValue 安全读取、遍历 KeyValuePair 条目、统计出现次数、让键忽略大小写,以及用 SortedDictionary 让键保持有序。
- HashSetHashSet<T> 保存不重复的元素,并能在常数时间内完成 Contains 判断。了解 Add 如何报告重复项,如何对列表去重,如何求集合的并集、交集和差集,以及如何让集合按值比较你自己的对象。
- Queue 队列Queue<T> 是先进先出的集合:元素按到达的顺序离开。学习 Enqueue、Dequeue 和 Peek,空队列异常与 TryDequeue,用队列实现广度优先搜索,以及什么时候该用 ConcurrentQueue 或 PriorityQueue。
- Stack 栈Stack<T> 是后进先出的集合:最近添加的元素最先出来。学习 Push、Pop 和 Peek,空栈异常与 TryPop,为什么栈的遍历顺序是反的,以及两个经典用法:撤销历史和括号匹配检查。
- 元组C# 元组无需声明类型就能把几个值组合成一个。学习值元组的语法、命名元素、从方法返回多个值、用 var (a, b) 解构、在字典中用元组作键,以及 ValueTuple 与旧的 System.Tuple 有何不同。
- LINQLINQ 给 C# 中的每个集合都加上了查询运算符。学习方法语法和查询语法,用 Where 过滤,用 Select 投影,用 OrderBy 和 ThenBy 排序,用 GroupBy 分组,First 与 FirstOrDefault 的陷阱,聚合运算,以及延迟执行如何改变查询的运行时机。
- IEnumerable 与 yieldIEnumerable<T> 是 foreach 和 LINQ 背后的接口,而 yield return 是实现它最简单的方式。了解 IEnumerable 与 ICollection、List 的关系,迭代器方法如何一步步执行,yield break,无限序列,以及惰性求值的陷阱。
类与类型
- 类和对象如何在 C# 中声明类、为它添加字段和方法、用 new 创建对象,以及类是引用类型意味着什么:共享的对象、null 与 NullReferenceException、ToString 和对象初始化器。
- 构造函数C# 构造函数的工作原理:隐式默认构造函数以及它何时消失、带参数和重载的构造函数、用 this(...) 和 base(...) 串联调用、各部分的执行顺序、静态构造函数和私有构造函数,以及 C# 12 的主构造函数。
- 属性C# 属性的工作方式:基于后备字段的 get 和 set 访问器、自动属性、private set 与只读属性、计算属性、setter 中的验证,以及 C# 9 和 C# 11 引入的 init 与 required 关键字。
- static 关键字C# 中 static 是什么意思:属于类型而不属于对象的成员,在实例之间共享的状态,静态方法和静态类,静态构造函数,const 与 static readonly,using static,以及 CS0120 错误。
- 访问修饰符C# 的六种访问修饰符(public、private、protected、internal、protected internal、private protected):每种允许什么访问,不写修饰符时的默认值,以及它们会引发的编译错误。
- 继承C# 中类继承的工作方式:用冒号派生,哪些成员会被继承、哪些不会,用 base 调用基类,virtual 与 override,用 new 隐藏成员(以及那个让所有人都意外的输出),sealed,多态,以及在继承层次中向上和向下转型。
- 抽象类C# 中的抽象类是什么:一种不能实例化的基类,可以声明派生类必须实现的抽象成员。内容包括抽象方法和抽象属性、构造函数、模板方法模式,以及什么时候应该改用接口。
- 接口C# 中接口的工作方式:如何声明接口,在类和结构体中实现它,把接口当作类型使用,同时实现多个接口,显式实现,你最常实现的框架接口(IComparable<T>、IEnumerable<T>、IDisposable),以及 C# 8 的默认接口方法。
- 结构体C# 中结构体的工作方式:值语义与赋值时复制,List<T> 修改报错,构造函数规则,相等性,装箱,readonly struct 与 record struct,以及 struct 与 class 的对比和何时该选用结构体。
- 枚举C# 中的枚举是怎么工作的:声明命名常量,底层整数值与类型转换,用 ToString 把枚举转成字符串、用 Parse 和 TryParse 把字符串转成枚举,列出所有值,[Flags] 与位运算符和 HasFlag,对枚举使用 switch,以及处理未定义的值。
- Record 记录C# 记录(C# 9 及以后)是什么:位置语法,编译器生成的成员,基于值的相等性,用 with 进行非破坏性复制,内置的 ToString,C# 10 起的 record struct,记录之间的继承,以及手写的等价类。
- 泛型C# 中泛型如何工作:用类型参数编写泛型类和泛型方法,类型推断,用 where 添加约束(class、struct、new()、基类、接口),default(T),按类型区分的静态成员,以及 IEnumerable<out T> 的协变。
- 可空类型关于 C# 中 null 的一切:可空值类型(int?、Nullable<T>)及其 HasValue、Value 和 GetValueOrDefault,空合并运算符 ?? 和 ??=,空条件运算符 ?.,空值检查,以及 C# 8 引入的可空引用类型(string?)。
- 模式匹配C# 中模式匹配的工作方式:is 运算符配合类型模式和常量模式,带 case 模式和 when 的 switch 语句,switch 表达式,属性、元组和位置模式,关系模式与逻辑模式(and、or、not),列表模式,以及每种模式是在哪个 C# 版本加入的。
异常
- try catchtry、catch 和 finally 是 C# 处理异常的方式。学习异常如何沿调用栈向上传递、如何按正确的顺序捕获特定的异常类型、用 when 过滤、在 finally 中清理、用 throw; 重新抛出而不丢失堆栈跟踪,以及哪些异常不该捕获。
- 抛出异常在 C# 中如何以及何时抛出异常。学习 throw 语句、各种内置异常类型分别对应哪类错误、配合 ?? 和 ?: 使用的 throw 表达式、ThrowIfNull 辅助方法,以及如何编写带有自定义数据和内部异常的自定义异常类。
- using 语句using 语句保证文件、流、连接以及其他 IDisposable 对象的 Dispose 一定会被调用,即使抛出了异常也一样。学习它会被编译成什么、对象按什么顺序释放、C# 8 的 using 声明,以及如何在你自己的类中实现 IDisposable。
异步与线程
- async awaitasync 和 await 让 C# 代码在等待慢操作时不阻塞线程。本文讲解异步方法如何执行、Task 与 Task<T>、用 Task.WhenAll 同时运行互不依赖的操作、异步代码中的异常,以及 async void 和 .Result 为什么会导致 bug 和死锁。
- TaskTask 是 .NET 中异步和并行工作的基本单位。学习用 Task.Run 处理 CPU 密集型工作、读取结果、用 WhenAll 和 WhenAny 组合多个任务、延续任务、用 CancellationToken 取消任务、Task 与 Thread 的区别,以及什么时候更适合用 Parallel.For。
- Locklock 语句让同一时刻只有一个线程运行一段代码。看竞态条件如何把计数器弄乱,用 lock 修复它,学会该锁哪个对象,用 Interlocked 处理简单计数器,避免由加锁顺序导致的死锁,并在块内需要 await 时改用 SemaphoreSlim。
- 定时器C# 有好几个定时器类,它们的行为各不相同。学习什么时候用 System.Threading.Timer、System.Timers.Timer、PeriodicTimer 或 UI 定时器,如何启动和停止它们,为什么回调在线程池上运行并可能重叠,为什么必须保留对定时器的引用,以及如何用 Stopwatch 测量耗时。
文件、JSON 等
- 读写文件如何在 C# 中写入文件和读取文件:用 File.WriteAllText、ReadAllText、AppendAllText、WriteAllLines 和 ReadAllLines 处理整个文件,用 StreamWriter 和 StreamReader 处理大文件,用 Path.Combine 拼接路径,以及目录、编码和文件操作必须处理的异常。
- JSON如何在 C# 中用 System.Text.Json 处理 JSON:JsonSerializer.Serialize 与 Deserialize,camelCase 与缩进输出,JsonPropertyName 和 JsonIgnore 等特性,把枚举写成字符串,用 JsonDocument 和 JsonNode 在没有类的情况下读取 JSON,错误处理,以及它与 Newtonsoft.Json 的对比。
- 正则表达式如何用 System.Text.RegularExpressions 在 C# 中使用正则表达式:IsMatch、Match 和 Matches,编号分组和命名分组,用替换模式和 lambda 进行 Replace,Split,RegexOptions,逐字字符串写模式,校验输入,超时,以及 .NET 7 的 GeneratedRegex 特性。
- 反射与特性C# 中的反射是如何工作的:通过 typeof 和 GetType 得到 Type 对象,读取和设置属性,按名称调用方法,创建实例;还有特性:Obsolete 等内置特性,声明你自己的特性,并在运行时读取它们。以及反射的代价。