C#学习笔记-入门篇
本人是java后端出生,但是公司是csharp技术栈,所以开始学习csharp的相关知识,如果你也是java出生的话思维应该和我差不多,所以希望这篇笔记能够对有相似需求的朋友有所帮助笔记大纲是参照b站的一个视频,不过我没有去仔细看,如果你喜欢看视频学习的话也可以去看该视频进行csharp的相关学习面向Java程序员的C#基础课程——入门篇https://www.bilibili.com/video/BV1ZsxmeHEW8?spm_id_from333.788.videopod.sectionsvd_source668b15b6e26adc9a2edc438f4b6926b11. 基础类型、字面量与常量1.1 核心概念int / long整数区别主要是范围32 位 vs 64 位。float / double / decimal小数类型。double通用默认选择。decimal金额优先。const编译期常量必须声明时赋值后续不可改。1.2 常用写法Llong 字面量后缀。Ffloat 字面量后缀。Mdecimal 字面量后缀。小数字面量默认是 double。数字分隔符 _ 只影响可读性不影响值。1.3 关键代码// 常见基础类型 int age 28; long population 1_412_000_000L; // L 表示 long float temperature 36.5F; // F 表示 float double pi 3.14; // 小数默认 double decimal salary 12345.67M; // M 表示 decimal char grade A; string userName Alice; bool isOnline true; // 常量声明后不可修改 const string appName CSharp Study;1.4 易错点double 转 int 需要强转会丢失小数。char 用单引号string 用双引号。1.5 类型别名与本质C# 关键字类型大多是 .NET 类型别名int System.Int32、long System.Int64、string System.String。代码风格上通常优先写关键字int、string可读性更统一。int/long/bool 是值类型structstring 是引用类型class。// 别名与完整类型名是等价的 int a 10; System.Int32 b 20; string name1 Alice; System.String name2 Bob;2. 常见运算符2.1 核心概念算术 - * / %比较 ! 逻辑 || !条件?:空合并??自增自减 --2.2 关键代码int a 20; int b 6; // 算术运算 int add a b; int div a / b; // 整数除法结果为 3 int mod a % b; // 逻辑判断 bool pass a 10 b 10; // 前置/后置自增 int n 5; Console.WriteLine(n); // 先输出 5再自增 Console.WriteLine(n); // 先自增再输出 // 空合并左边为 null 时使用右边 string? nickName null; string displayName nickName ?? 匿名用户;2.3 易错点int / int 是整数除法。C# 不支持 20 x 25要写成 x 20 x 25。x/--x 行为与 Java 一样后置先用后变前置先变后用。3. 类型转换、装箱与拆箱3.1 核心概念隐式转换安全范围自动转换如 int - long。显式转换需强转可能丢失信息。Parse失败抛异常。TryParse失败不抛异常返回 false。Convert.ToInt32(null)返回 0。装箱值类型 - object拆箱object - 值类型。3.2 关键代码// 隐式转换 int x 100; long y x; // 显式转换 int z (int)19.99; // 结果 19 // Parse失败会异常 int p int.Parse(123); // TryParse失败返回 false不抛异常 bool ok int.TryParse(10A, out int value); // okfalse, value0 // Convertnull 转 int 返回 0 string? emptyValue null; int converted Convert.ToInt32(emptyValue); // 装箱与拆箱 object boxed 300; // 装箱 int unboxed (int)boxed; // 拆箱3.3 易错点int.TryParse 不能只写一个参数必须有 out。拆箱类型必须精确匹配否则 InvalidCastException。3.4 补充什么时候会自动装箱值类型在“需要按对象使用”时会自动装箱如赋给 object 或接口变量。装箱后是新对象再转回值类型时是拆箱。int n 123; object obj n; // 自动装箱值类型 - 引用对象 int n2 (int)obj; // 拆箱引用对象 - 值类型4. 字符串4.1 核心概念字符串是不可变对象。常用操作Trim、Contains、StartsWith、EndsWith、Substring、Replace、Split。空判断IsNullOrEmpty、IsNullOrWhiteSpace。 逐字字符串反斜杠不转义。4.2 关键代码string text Hello CSharp ; // 去掉首尾空格 string trimmed text.Trim(); // 查询 bool hasSharp text.Contains(Sharp); bool starts text.StartsWith( He); // 开头匹配包含空格 bool ends text.EndsWith( ); // 结尾匹配包含空格 // 截取与替换 string code ORD-2026-0001; string year code.Substring(4, 4); string replaced code.Replace(-, /); // 分割 string[] tags dotnet,csharp,backend.Split(,); // 空值判断 bool e1 string.IsNullOrEmpty(); bool e2 string.IsNullOrWhiteSpace( ); // 路径字符串两种写法 string path1 D:\\zzb_workspace\\project; string path2 D:\zzb_workspace\project;4.3 易错点StartsWith / EndsWith 是字面匹配空格也算字符。逐字字符串里写 需要写成 。5. 条件语句、模式匹配与循环语句5.1 条件语句if / else if / else按条件顺序判断。switch 表达式输入值映射输出结果。5.2 模式匹配is判断类型并声明变量。switch when先类型匹配再附加条件过滤。5.3 循环语句for已知次数。while先判断再执行。do-while至少执行一次。foreach遍历集合。continue跳过本次break结束循环。5.4 关键代码// if / else if / else if (score 90) { Console.WriteLine(A); } else if (score 80) { Console.WriteLine(B); } else { Console.WriteLine(C/D); } // switch 表达式 string season month switch { 12 or 1 or 2 冬, 3 or 4 or 5 春, _ 未知 // 兜底分支 }; // is 模式匹配 object value Hello; if (value is string s s.Length 3) { Console.WriteLine(s); } // switch when 模式匹配 object data 42; string result data switch { int n when n 0 正整数, // int 且 0 int 整数(非正), // 其他 int string text when text.Length 0 空字符串, string 非空字符串, null null, _ 其他类型 }; // 循环 for (int i 1; i 10; i) { } while (count 0) { count--; } do { number; } while (number 0); foreach (var name in names) { Console.WriteLine(name); }5.5 易错点switch / switch 表达式 按顺序匹配命中第一条就结束。do-while 会先执行一次再判断条件。var 需要右侧是可推导类型的完整表达式如 new[] { ... }。6. 异常6.1 核心概念try放可能抛异常的代码。catch捕获并处理异常。finally无论是否异常通常都会执行常用于资源清理。throw主动抛出异常。throw;在 catch 中重新抛出原异常保留原始堆栈。catch (...) when (...)异常过滤满足条件才进入该 catch。自定义异常用于表达业务错误通常包含错误码等业务字段。6.2 关键代码try { // 可能抛异常的代码 int x 10; int y 0; int result x / y; // DivideByZeroException } catch (DivideByZeroException ex) { // 捕获特定异常 Console.WriteLine(ex.GetType().Name); } catch (Exception ex) { // 兜底异常分支 Console.WriteLine(ex.GetType().Name); } finally { // 无论是否异常都会执行 Console.WriteLine(释放资源); } // 主动抛异常 if (age 18) { throw new ArgumentOutOfRangeException(nameof(age), 年龄必须 18); } // 异常过滤先匹配异常类型再判断 when 条件 try { throw new InvalidOperationException(状态非法); } catch (InvalidOperationException ex) when (ex.Message.Contains(状态)) { Console.WriteLine(命中过滤条件); } // 重新抛出原异常保留原始堆栈 try { int.Parse(abc); } catch { throw; } // 自定义异常携带业务错误码 internal class BusinessException : Exception { public string ErrorCode { get; } public BusinessException(string errorCode, string message) : base(message) { ErrorCode errorCode; } } // 使用自定义异常 if (amount 0) { throw new BusinessException(ORDER_AMOUNT_INVALID, 订单金额必须大于 0); }6.3 易错点throw; 和 throw ex; 不同前者保留原始堆栈后者会重置堆栈起点。不要用异常做普通流程控制例如把 TryParse 场景硬写成 Parse catch。先写具体异常 catch再写 catch (Exception) 兜底。自定义异常建议继承 Exception并提供必要构造函数与业务字段如 ErrorCode。7. 枚举类Enum7.1 核心概念普通枚举表示一组固定、互斥的状态如订单状态。Flags 枚举表示可组合能力/权限可同时拥有多个值。枚举本质有底层整数值可以与 int 转换。7.2 常用写法显式赋值Pending 1, Paid 2 ...避免隐式值漂移。字符串转枚举优先 Enum.TryParse避免异常。枚举遍历Enum.GetValuesTEnum()。Flags 权限组合|增加、 ~移除、HasFlag判断。7.3 关键代码// 普通枚举固定状态 internal enum OrderStatus { Pending 1, Paid 2, Shipped 3, Completed 4, Cancelled 5 } // Flags 枚举可组合权限 [Flags] internal enum UserPermission { None 0, Read 1, Write 2, Delete 4, Admin 8 } OrderStatus status OrderStatus.Paid; int numeric (int)status; // 枚举转数字 // switch 匹配枚举 string desc status switch { OrderStatus.Pending 待支付, OrderStatus.Paid 已支付, _ 其他状态 }; // 字符串转枚举推荐 TryParse bool ok Enum.TryParse(Shipped, out OrderStatus parsedStatus); // Flags 组合与判断 UserPermission permission UserPermission.Read | UserPermission.Write; bool canRead permission.HasFlag(UserPermission.Read); permission | UserPermission.Delete; // 增加权限 permission ~UserPermission.Write; // 移除权限7.4 易错点Flags 枚举值建议使用 2 的幂1、2、4、8...否则组合会冲突。Enum.Parse 失败会抛异常输入不可靠时用 Enum.TryParse。var x { ... } 不是完整表达式用 new[] { ... } 或显式类型。Enum.GetValues(Priority) 这种写法会报错用 Enum.GetValuesPriority() 或 Enum.GetValues(typeof(Priority))。~Delete 只是“按位取反”的掩码不等于“全部权限减 Delete”应与 All 组合使用。[Flags] internal enum UserPermission { None 0, Read 1, Write 2, Delete 4, Admin 8, All Read | Write | Delete | Admin } // 推荐在已定义权限范围内移除 Delete UserPermission permission UserPermission.All ~UserPermission.Delete;8. 一维数组和二维数组8.1 核心概念一维数组同类型元素的线性集合索引从 0 开始。二维数组表格结构索引写法是 [行, 列]。数组长度固定创建后长度不可变。8.2 常用写法一维数组声明int[] nums new int[3];一维数组初始化int[] nums { 10, 20, 30 };二维数组声明int[,] matrix new int[2,3];行列获取GetLength(0) 取行数GetLength(1) 取列数。8.3 关键代码// 一维数组 int[] scores { 85, 92, 78 }; Console.WriteLine(scores[0]); // 访问第 1 个元素索引 0 scores[2] 88; // 修改元素 for (int i 0; i scores.Length; i) { Console.WriteLine($scores[{i}] {scores[i]}); } foreach (int score in scores) { Console.WriteLine(score); // 直接遍历元素 } Array.Sort(scores); Console.WriteLine(string.Join(, , scores)); // 二维数组2 行 3 列 int[,] matrix { { 1, 2, 3 }, { 4, 5, 6 } }; Console.WriteLine(matrix[1, 2]); // 第 2 行第 3 列 6 Console.WriteLine(matrix.GetLength(0)); // 行数 2 Console.WriteLine(matrix.GetLength(1)); // 列数 3 for (int row 0; row matrix.GetLength(0); row) { for (int col 0; col matrix.GetLength(1); col) { Console.Write(${matrix[row, col]} ); } Console.WriteLine(); }8.4 易错点数组下标从 0 开始arr[arr.Length] 会越界。for 循环边界要写 Length不要写 Length。二维数组用 [row, col]不是 [row][col]。int[,]矩形二维数组和 int[][]交错数组不是同一种类型。8.5 int[,] 与 int[][] 对比int[,]矩形二维数组行列规则所有行列长度固定。int[][]交错数组数组的数组每一行是独立的一维数组长度可不同。选择建议数据天然是规则表格如 3x4 成绩表用 int[,]。每行长度不一致如每个班人数不同用 int[][]。// 矩形二维数组2 行 3 列固定 int[,] table { { 1, 2, 3 }, { 4, 5, 6 } }; Console.WriteLine(table[1, 2]); // 6 // 交错数组每行长度可不同 int[][] jagged { new[] { 10, 20 }, new[] { 30, 40, 50 }, new[] { 60 } }; Console.WriteLine(jagged[1][2]); // 508.6 补充数组初始化新语法C# 12int[] ages [35, 20, 22, 18]; 是 C# 12 的集合表达式写法。对数组来说它等价于 int[] ages new[] { 35, 20, 22, 18 };。// C# 12 集合表达式 int[] ages1 [35, 20, 22, 18]; // 传统等价写法 int[] ages2 new[] { 35, 20, 22, 18 };9. 交错数组9.1 核心概念交错数组写法是 T[][]本质是“数组里的每个元素仍是一个一维数组”。每一行长度可以不同适合不规则数据。访问语法是 arr[row][col]。9.2 常用写法声明并初始化int[][] data { new[] {1,2}, new[] {3,4,5} };先声明行数再逐行赋值int[][] data new int[3][];行长度获取data[row].Length9.3 关键代码// 交错数组每行长度可不同 int[][] scoresByClass { new[] { 90, 85, 88 }, new[] { 76, 92 }, new[] { 100, 98, 95, 93 } }; Console.WriteLine(scoresByClass[2][1]); // 第3行第2列 98 // 双层循环遍历 for (int row 0; row scoresByClass.Length; row) { for (int col 0; col scoresByClass[row].Length; col) { Console.Write(${scoresByClass[row][col]} ); } Console.WriteLine(); } // 先建行再给每行分配不同长度 int[][] data new int[3][]; data[0] new[] { 1, 2 }; data[1] new[] { 3, 4, 5 }; data[2] new[] { 6 }; // 与二维数组对比 int[,] rectangle { { 1, 2, 3 }, { 4, 5, 6 } };9.4 易错点交错数组访问写法是 arr[i][j]不是 arr[i, j]。new int[3][] 只创建“行容器”每一行默认是 null使用前必须初始化。行长度不一致时内层循环边界必须用 arr[row].Length。int[][]交错数组和 int[,]矩形二维数组是不同类型不能直接互换。9.5 练习补充与写法优化外层长度 teams.Length 表示“组数”内层长度 teams[i].Length 表示“该组人数”两者不要混用。交错数组遍历时推荐把外层下标打印出来调试更直观。访问元素前可做空值判断避免某一行未初始化导致 NullReferenceException。for (int groupIndex 0; groupIndex teams.Length; groupIndex) { // 某一行可能还没初始化先判空更安全 if (teams[groupIndex] is null) { Console.WriteLine($第 {groupIndex} 组未初始化); continue; } Console.WriteLine($第 {groupIndex} 组人数 {teams[groupIndex].Length}); for (int memberIndex 0; memberIndex teams[groupIndex].Length; memberIndex) { Console.WriteLine($teams[{groupIndex}][{memberIndex}] {teams[groupIndex][memberIndex]}); } }9.6 交错数组与 C# 12 集合表达式[] 写法是 C# 12 集合表达式可用于初始化交错数组。与传统 new[] { ... } 写法等价选团队统一风格即可。// C# 12 写法 int[][] a [ [1, 2], [3, 4, 5], [6] ]; // 传统写法 int[][] b { new[] { 1, 2 }, new[] { 3, 4, 5 }, new[] { 6 } };10. 顶级语句和函数10.1 核心概念顶级语句Top-level statements是省略显式 Program.Main 的入口写法。编译器会自动生成入口方法并执行文件中的顶级代码。args 在顶级语句中可直接使用本质仍对应入口参数。10.2 与非顶层写法对应关系顶级语句省略 class Program 与 static void Main(string[] args) 样板。非顶层写法显式声明 Program.Main结构更清晰适合教学和较大项目。两者本质都是 C# 程序入口能力等价。10.3 函数组织方式局部函数定义在当前顶级流程里作用域局限在当前流程。static 局部函数不能捕获外层变量只依赖参数和静态成员。类型静态方法定义在 class 中通过 类名.方法名 调用。10.4 委托、Func、Action、lambda委托可理解为“函数类型”可把函数赋值给变量再调用。普通函数本身不能像普通值那样直接传递通常需要先绑定到委托变量方法组或 lambda后再传递。FuncT1, T2, TResult前面是参数类型最后是返回值类型。ActionT1, T2, ...只有参数类型无返回值void。lambda是创建匿名函数的简写形式常用于给委托赋行为。10.5 函数可配置的含义同一段流程代码不变通过替换委托变量中的函数行为实现不同结果。static int Calc(int a, int b, Funcint, int, int op) { return op(a, b); } int r1 Calc(10, 3, (x, y) x y); // 加法 int r2 Calc(10, 3, (x, y) x - y); // 减法 int r3 Calc(10, 3, (x, y) x * y); // 乘法10.6 委托的典型应用场景回调通知将“处理完成后要做什么”作为参数传入。策略切换同一流程中按需切换算法加减乘除、不同计费规则。事件处理按钮点击、消息到达等场景本质是委托回调。集合处理Where、Select、OrderBy 等 LINQ API 通过委托接收筛选/映射规则。// 回调下载完成后执行回调逻辑 static void Download(string url, Actionstring onCompleted) { // ... 省略下载流程 onCompleted($下载完成: {url}); } // 策略把算法作为参数传入 static int Calc(int a, int b, Funcint, int, int op) { return op(a, b); } Download(https://example.com/a.zip, msg Console.WriteLine(msg)); int total Calc(10, 3, (x, y) x * y);10.7 关键代码// 顶级语句入口省略 Program/Main Console.WriteLine($args.Length {args.Length}); int a 12; int b 5; Console.WriteLine(Add(a, b)); Console.WriteLine(Square(a)); // 委托 lambda Funcint, int, int max (x, y) x y ? x : y; Actionstring log msg Console.WriteLine($[LOG] {msg}); Console.WriteLine(max(a, b)); log(lambda 调用完成); // 局部函数可访问当前流程变量 int Square(int value) { return value * value; } // static 局部函数不能捕获外层变量 static int Add(int left, int right) { return left right; } // 类型静态方法 internal static class MathHelper { public static int Multiply(int left, int right) left * right; }10.8 易错点顶级语句项目中通常只保留一个入口文件避免入口冲突。局部函数与类型方法概念不同局部函数无 public/private/internal 修饰。static 局部函数不能访问外层变量误访问会编译报错。项目中已有非顶层 Main 时再加入顶级语句可能产生“多个入口点”错误。11. 常见参数传递、ref 和 out11.1 核心概念C# 默认按值传递参数。值类型按值传递方法内改参数副本不影响调用方变量。引用类型按值传递可改对象内容但重新 new 只改到局部副本引用。ref按引用传递调用方变量必须先初始化方法内可读可写。out按引用传递调用方可不初始化方法内必须赋值后返回。11.2 常用写法void Increase(ref int x)bool TryParseAge(string input, out int age)int Sum(params int[] nums)可变参数11.3 关键代码int n 10; ChangeValue(n); Console.WriteLine(n); // 10值类型按值传递不变 Student stu new Student { Name Alice }; ChangeStudentName(stu); Console.WriteLine(stu.Name); // Bob对象内容被修改 ReassignStudent(stu); Console.WriteLine(stu.Name); // 仍是 Bob引用副本被替换不影响外部 int score 60; AddBonus(ref score, 20); Console.WriteLine(score); // 80ref 修改了调用方变量 bool ok TryParseAge(18, out int age); Console.WriteLine($ok{ok}, age{age}); // 值类型按值传递 static void ChangeValue(int x) x 999; // 引用类型按值传递改对象内容会生效 static void ChangeStudentName(Student s) s.Name Bob; // 引用类型按值传递替换引用仅影响局部副本 static void ReassignStudent(Student s) s new Student { Name NewGuy }; // ref可读可写调用方变量 static void AddBonus(ref int value, int bonus) value bonus; // out方法内必须赋值 static bool TryParseAge(string input, out int age) int.TryParse(input, out age);11.4 ref 与 out 对比调用前ref 变量必须已赋值out 可以不赋值。方法内ref 可先读后写out 在读取前必须先赋值。场景ref 常用于“修改原变量”out 常用于“返回多个结果/Try 模式”。11.5 易错点ref/out 必须在“方法声明”和“调用处”同时写缺一不可。不要把“引用类型按值传递”误解为“对象引用本身可自动被替换”。out 参数若存在未赋值返回路径会编译报错。ref 和 out 不是重载区分依据签名冲突风险需要注意。11.6 易混点澄清可用“值/地址”做直觉类比但 C# 的 ref/out 是语言级安全语义不是直接操作裸指针。引用类型默认传参是“引用的副本”改对象内容会影响外部同一对象。重新 new只改变方法内副本引用的指向不影响外部变量指向。ref 可理解为“把调用方变量本体交给方法”因此可替换外部变量指向。Person p new Person { Name A }; ChangeName(p); // 改内容外部可见 Reassign(p); // 改副本引用外部不可见 Replace(ref p); // 改变量本体外部可见 static void ChangeName(Person x) x.Name B; static void Reassign(Person x) x new Person { Name C }; static void Replace(ref Person x) x new Person { Name D };11.7 params 补充params 用于“不确定数量、同类型参数”。调用方式支持“逗号展开”或“直接传数组”。params 参数必须放在参数列表最后且一个方法只能有一个 params 参数。static int Sum(params int[] nums) { int s 0; foreach (int n in nums) s n; return s; } int a Sum(1, 2, 3); // 逗号展开 int b Sum(new[] { 4, 5, 6 }); // 直接传数组 int c Sum(); // 允许 0 个参数结果 012. 结构体12.1 核心概念struct 是值类型赋值和传参默认走值语义复制。结构体适合小而简单的数据对象坐标、尺寸、颜色、日期片段。与 class 不同class 是引用类型变量保存对象引用。12.2 常用写法定义结构体struct Point { ... }带参构造public Point(int x, int y) { ... }不可变结构体readonly struct Size { ... }需要修改调用方结构体时使用 ref。12.3 关键代码Point p1 new Point(3, 5); Point p2 p1; // 值拷贝 p2.X 100; Console.WriteLine(p1.X); // 3原对象不受影响 Console.WriteLine(p2.X); // 100 MoveByValue(p1, 1, 1); Console.WriteLine(p1.X); // 3按值传参未改外部 MoveByRef(ref p1, 1, 1); Console.WriteLine(p1.X); // 4ref 改到了外部变量 readonly struct Size { public int Width { get; } public int Height { get; } public Size(int width, int height) { Width width; Height height; } } static void MoveByValue(Point p, int dx, int dy) { p.X dx; p.Y dy; } static void MoveByRef(ref Point p, int dx, int dy) { p.X dx; p.Y dy; }12.4 struct 与 class 快速对比存储语义struct 值语义class 引用语义。赋值行为struct 复制数据class 复制引用。适用场景struct轻量、短生命周期、不可变小对象。class较大对象、共享状态、继承多态场景。12.5 易错点结构体赋值是复制不是共享同一实例。结构体较大时频繁复制可能有性能开销。readonly struct 内不应设计可变状态。结构体装箱到 object 时会产生额外开销。