本人是java后端出生,但是公司是csharp技术栈,所以开始学习csharp的相关知识,如果你也是java出生的话思维应该和我差不多,所以希望这篇笔记能够对有相似需求的朋友有所帮助
笔记大纲是参照b站的一个视频,不过我没有去仔细看,如果你喜欢看视频学习的话也可以去看该视频进行csharp的相关学习
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 关键代码
cs
// 常见基础类型
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)。
cs
// 别名与完整类型名是等价的
int a = 10;
System.Int32 b = 20;
string name1 = "Alice";
System.String name2 = "Bob";
2. 常见运算符
2.1 核心概念
- 算术:+ - * / %
- 比较:== != > >= < <=
- 逻辑:&& || !
- 条件:?:
- 空合并:??
- 自增自减:++ --
2.2 关键代码
cs
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 关键代码
cs
// 隐式转换
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 或接口变量)。
- 装箱后是新对象;再转回值类型时是拆箱。
cs
int n = 123;
object obj = n; // 自动装箱:值类型 -> 引用对象
int n2 = (int)obj; // 拆箱:引用对象 -> 值类型
4. 字符串
4.1 核心概念
- 字符串是不可变对象。
- 常用操作:Trim、Contains、StartsWith、EndsWith、Substring、Replace、Split。
- 空判断:IsNullOrEmpty、IsNullOrWhiteSpace。
- @ 逐字字符串:反斜杠不转义。
4.2 关键代码
cs
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 关键代码
cs
// 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 关键代码
cs
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 关键代码
cs
// 普通枚举:固定状态
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 组合使用。
cs
[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 关键代码
cs
// 一维数组
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[][]。
cs
// 矩形二维数组: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]); // 50
8.6 补充:数组初始化新语法(C# 12)
- int[] ages = [35, 20, 22, 18]; 是 C# 12 的集合表达式写法。
- 对数组来说,它等价于 int[] ages = new[] { 35, 20, 22, 18 };。
cs
// 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 关键代码
cs
// 交错数组:每行长度可不同
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。
cs
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 集合表达式,可用于初始化交错数组。
cs
// 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 函数可配置的含义
- 同一段流程代码不变,通过替换委托变量中的函数行为,实现不同结果。
cs
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 通过委托接收筛选/映射规则。
cs
// 回调:下载完成后执行回调逻辑
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 关键代码
cs
// 顶级语句入口(省略 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 关键代码
cs
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 可理解为"把调用方变量本体交给方法",因此可替换外部变量指向。
cs
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 参数。
cs
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 个参数,结果 0
12. 结构体
12.1 核心概念
- struct 是值类型,赋值和传参默认走值语义(复制)。
- 结构体适合小而简单的数据对象(坐标、尺寸、颜色、日期片段)。
- 与 class 不同:class 是引用类型,变量保存对象引用。
12.2 常用写法
- 定义结构体:struct Point { ... }
- 带参构造:public Point(int x, int y) { ... }
- 不可变结构体:readonly struct Size { ... }
- 需要修改调用方结构体时使用 ref。
12.3 关键代码
cs
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 时会产生额外开销。