C#学习笔记-入门篇

本人是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 关键代码

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 时会产生额外开销。
相关推荐
Restart-AHTCM1 小时前
LangChain学习之提示词模板 Prompts(2/8)
学习·langchain
Xin_ye100861 小时前
C# 零基础到精通教程 - 第九章:面向对象编程(高级)——接口、委托与事件
开发语言·c#
步步为营DotNet1 小时前
深入.NET 11:C# 14 在边缘计算数据处理的优化与实践
c#·.net·边缘计算
weixin_428005301 小时前
C#调用 AI学习从0开始-第1阶段(基础与工具)-第6天流式输出
开发语言·学习·c#·流式输出stream
xiaoshuaishuai81 小时前
C# Anthropic连接超时原因及方案
开发语言·网络·tcp/ip·c#
清平乐的技术专栏1 小时前
【Flink学习】(七)Flink 四大窗口机制,实时时间段统计
大数据·学习·flink
清平乐的技术专栏1 小时前
【Flink学习】(九)Flink 容错机制 Checkpoint 与 Savepoint
大数据·学习·flink
加号31 小时前
【C#】 实现 CRC16 校验:原理、算法与工程实践
算法·c#
HEADKON1 小时前
阿西米尼常见副作用血小板减少及高血压的临床特征与管理
c#