本人是java后端出生,但是公司是csharp技术栈,所以开始学习csharp的相关知识,如果你也是java出生的话思维应该和我差不多,所以希望这篇笔记能够对有相似需求的朋友有所帮助
笔记大纲是参照b站的一个视频,不过我没有去仔细看,如果你喜欢看视频学习的话也可以去看该视频进行csharp的相关学习
面向Java程序员的C#基础课程——入门篇https://www.bilibili.com/video/BV1ZsxmeHEW8?spm_id_from=333.788.videopod.sections&vd_source=668b15b6e26adc9a2edc438f4b6926b1
1. 基础类型、字面量与常量
1.1 核心概念
int / long:整数,区别主要是范围(32 位 vs 64 位)。
float / double / decimal:小数类型。
- double:通用默认选择。
- decimal:金额优先。
const:编译期常量,必须声明时赋值,后续不可改。
1.2 常用写法
- L:long 字面量后缀。
- F:float 字面量后缀。
- M:decimal 字面量后缀。
- 小数字面量默认是 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 是值类型(struct),string 是引用类型(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); // ok=false, value=0 // Convert:null 转 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. 枚举类(Enum)
7.1 核心概念
- 普通枚举:表示一组固定、互斥的状态(如订单状态)。
- Flags 枚举:表示可组合能力/权限(可同时拥有多个值)。
- 枚举本质有底层整数值,可以与 int 转换。
7.2 常用写法
- 显式赋值:Pending = 1, Paid = 2 ...,避免隐式值漂移。
- 字符串转枚举:优先 Enum.TryParse,避免异常。
- 枚举遍历:Enum.GetValues<TEnum>()。
- 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.GetValues<Priority>() 或 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# 12)
- int[] 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].Length
9.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)后再传递。
- Func<T1, T2, TResult>:前面是参数类型,最后是返回值类型。
- Action<T1, T2, ...>:只有参数类型,无返回值(void)。
- lambda(=>)是创建匿名函数的简写形式,常用于给委托赋行为。
10.5 函数可配置的含义
- 同一段流程代码不变,通过替换委托变量中的函数行为,实现不同结果。
static int Calc(int a, int b, Func<int, 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, Action<string> onCompleted) { // ... 省略下载流程 onCompleted($"下载完成: {url}"); } // 策略:把算法作为参数传入 static int Calc(int a, int b, Func<int, 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 Func<int, int, int> max = (x, y) => x > y ? x : y; Action<string> 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 和 out
11.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); // 80,ref 修改了调用方变量 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); // 4,ref 改到了外部变量 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 时会产生额外开销。