17.总结
17.1 知识点

学习的主要内容

强调


17.2 核心要点速览
枚举
- 定义:命名整型常量集合(默认从 0 起增,可手动赋值)
- 声明:
enum E_Name { A, B = 5, C }(仅限 namespace/class/struct) - 使用:
E_Name x = E_Name.B; if(x==E_Name.A)… switch(x){…} - 转换:
(int)x|x=(E_Name)i|x.ToString()|Enum.Parse(...) - 价值:替代魔法数,提升可读性。
数组
| 特性 | 一维数组 | 二维数组 | 交错数组 |
|---|---|---|---|
| 维度 | 1 | 2 | N×可变(数组的数组) |
| 声明 | T[] a = {…}new T[n] |
T[,] a = new T[r,c] |
T[][] a = new T[n][] |
| 长度 | a.Length |
a.GetLength(0)/a.GetLength(1) |
a.Length(行)a[i].Length(列) |
| 访问 | a[i] |
a[i,j] |
a[i][j] |
| 可变性 | 定长,扩缩需重建新数组 | 定行定列,扩缩需重建新数组 | 每行可单独扩缩 |
| 常用场景 | 线性数据、列表 | 矩阵运算、表格 | 行列不规则的数据集合 |
| 初始化示例 | int[] a = {1,2,3} |
int[,] m = {{1,2},{3,4}} |
int[][] j = { new[]{1,2}, new[]{3} } |
值和引用
使用和存储
| 特性 | 值类型 | 引用类型 |
|---|---|---|
| 存储位置 | 栈(Stack) | 堆(Heap) |
| 赋值行为 | 复制实际值 → 独立副本 | 复制引用 → 共享同一对象 |
| 修改影响 | 改变副本,不影响原变量 | 改变对象,所有引用都会看到 |
| 示例 | int a=10; int b=a; |
int[] a={1}; int[] b=a; |
| 典型类型 | 整数、浮点、struct等 |
数组、string、class等 |
| 特殊说明 | — | string 不可变,赋新值时会指向新对象 |
核心:
- 值类型 → 快、小、复制安全;
- 引用类型 → 灵活、共享同一数据;
- 选用:按数据大小与共享需求决定。
string
| 特性 | string |
|---|---|
| 类型 | 引用类型(特殊) |
| 赋值行为 | 复制引用,但重写赋值会创建新实例 |
| 可变性 | 不可变(Immutability) |
| 存储位置 | 堆(Heap) |
| 性能影响 | 频繁拼接会产生垃圾,建议使用 StringBuilder 优化 |
| 典型用法 | 文本处理、日志、格式化输出 |
核心:
- 虽为引用类型,却表现为”它变我不变”(不可变)。
- 每次赋新值都在堆上创建新对象。
- 大量字符串操作要用
StringBuilder或等效结构以减少内存开销。
函数
函数基础
定义:命名的代码块,用于封装、复用、抽象。
位置:只能写在
class或struct中,不能嵌套在方法里。语法:
static 返回类型 名称(参数列表) { … [return 值]; }非
void必须return。
ref 和 out
作用:让函数内修改影响调用者。
区别:
ref:参数必须先初始化,内部可赋可不赋。out:参数无需初始化,内部必须赋值。
调用:
FooRef(ref x); FooOut(out y);
变长参数 params
用途:允许 0…n 个同类型实参。
要点:只能有一个,写在参数列表末尾,类型为数组。
static int Sum(params int[] nums) { … }
可选参数(默认值)
用途:未传值时使用默认值。
要点:所有默认参数必须放在必选参数之后。
static void Speak(string msg = "…") { … }
函数重载
定义:同名方法通过参数数量、类型、顺序区分;与返回值无关。
示例:
Calc(int a,int b); Calc(float a,float b); Calc(int a,params int[] xs);
递归
定义:函数自己调用自己。
必须:
- 存在可变的终止条件。
- 终止条件能被满足。
示例:
static void Fun(int n) { if (n>10) return; Console.WriteLine(n); Fun(n+1); }
结构体
概念:自定义的值类型,字段 + 方法 的集合,用于表示一组相关数据(如学生、矩形、玩家等)。
声明位置:只能写在
namespace或class/struct外层,不能在方法内。语法:
struct Name { // 字段(变量) public 类型 字段1; … // 构造函数(可选) public Name(参数…) { this.字段=…; … } // 方法(无需 static) public void Func() { … } }访问修饰符:
public允许外部访问private(默认)仅内部可见
使用:
// 1. 声明 MyStruct s; // 2. 赋值字段 s.Field = …; // 3. 调用方法 s.Method(); // 或使用构造器 var s2 = new MyStruct(arg1, arg2, …);构造函数要点:
- 无返回值,名与 struct 同名
- 必须为所有字段赋初值
重要限制:
- 字段不能直接初始化:声明时不能赋值,只能在构造函数或外部赋值
- 方法无需 static:结构体内方法不加
static,直接通过实例调用
特点:
- 值类型 ⇒ 赋值/传参时复制整个实例
- 轻量级,适合存储小数据集合,无需 GC 开销。
排序算法
冒泡排序
原理:两两相邻比较,不停交换,每轮将极值”冒泡”到正确位置。
套路:两层循环,外层轮数,内层比较,满足条件交换。
代码:
bool isSort = false; for (int m = 0; m < arr.Length; m++) { isSort = false; for (int n = 0; n < arr.Length - 1 - m; n++) { if (arr[n] > arr[n + 1]) // 升序;降序改为 < { isSort = true; int temp = arr[n]; arr[n] = arr[n + 1]; arr[n + 1] = temp; } } if (!isSort) break; // 优化:已有序则提前退出 }优化:
- 每轮确定的极值不再参与比较(
arr.Length - 1 - m) - 用
bool标识判断是否已有序
- 每轮确定的极值不再参与比较(
选择排序
原理:每轮找出极值,放入目标位置。排序区逐渐增大,未排序区逐渐减小。
套路:两层循环,外层轮数,内层寻找极值索引,内层循环外交换。
代码:
for (int m = 0; m < arr.Length; m++) { int index = 0; // 记录极值索引 for (int n = 1; n < arr.Length - m; n++) { if (arr[index] < arr[n]) // 升序找最大;降序改为 > index = n; } if (index != arr.Length - 1 - m) // 避免无意义交换 { int temp = arr[index]; arr[index] = arr[arr.Length - 1 - m]; arr[arr.Length - 1 - m] = temp; } }
对比
| 特性 | 冒泡排序 | 选择排序 |
|---|---|---|
| 交换次数 | 多(频繁交换) | 少(每轮最多一次) |
| 稳定性 | 稳定 | 不稳定 |
| 实现难度 | 简单 | 简单 |
| 时间复杂度 | O(n²) | O(n²) |
| 适用场景 | 数据量小、基本有序时较优 | 数据量小、交换成本高时优 |
17.3 面试题精选
基础题
1. 值类型和引用类型的区别
题目
请简述 C# 中值类型和引用类型的区别,并举例说明它们在赋值时的行为差异。
深入解析
值类型和引用类型的核心区别在于存储位置和赋值行为。
存储位置
- 值类型:存储在栈(Stack),系统自动分配和回收,访问速度快
- 引用类型:存储在堆(Heap),需要手动申请,GC 负责回收
赋值行为
- 值类型:复制的是实际值,修改副本不影响原变量(它变我不变)
- 引用类型:复制的是引用(地址),两个变量指向同一对象(它变我也变)
int a = 10;
int b = a; // 值类型:b 得到 a 的副本
b = 20;
Console.WriteLine(a); // 输出 10,a 不受影响
int[] arr1 = { 1, 2, 3 };
int[] arr2 = arr1; // 引用类型:arr2 和 arr1 指向同一数组
arr2[0] = 99;
Console.WriteLine(arr1[0]); // 输出 99,arr1 也变了
典型类型
- 值类型:int、float、bool、char、struct、enum
- 引用类型:数组、string、class
答题示例
值类型存在栈上,引用类型存在堆上。
赋值的时候,值类型是复制实际值,改了副本不影响原变量;引用类型是复制地址,两个变量指向同一个对象,改一个另一个也跟着变。
比如 int 是值类型,数组是引用类型。
参考文章
- 7.值类型和引用类型-使用和存储上的区别
- 8.值类型和引用类型-特殊的引用类型string
2. ref 和 out 的区别
题目
ref 和 out 关键字有什么作用?它们之间有什么区别?
深入解析
ref 和 out 都用于让函数内部的修改影响外部变量,本质上都是传递引用而非值拷贝。
核心区别
| 关键字 | 调用前是否必须初始化 | 函数内是否必须赋值 |
|---|---|---|
| ref | 必须 | 可选 |
| out | 不需要 | 必须 |
使用场景
- ref:需要在函数内修改外部已初始化的变量
- out:函数需要返回多个值,调用者不需要预先初始化
static void TestRef(ref int x)
{
x = 100; // 可以改,也可以不改
}
static void TestOut(out int x)
{
x = 100; // 必须赋值,否则编译报错
}
// 调用
int a = 0; // ref 必须先初始化
TestRef(ref a);
int b; // out 不需要初始化
TestOut(out b);
答题示例
ref 和 out 都是让函数里改的值能传到外面去。
区别有两点:
- 第一,ref 传进去之前必须先初始化,out 不用
- 第二,out 在函数里必须赋值,ref 可改可不改
一般用 out 来返回多个值,用 ref 来修改已有的变量。
参考文章
- 10.函数-ref和out
进阶题
1. string 为什么是特殊的引用类型
题目
string 是引用类型,但为什么表现出”它变我不变”的特性?这在性能上有什么影响?
深入解析
string 虽然是引用类型,但它是不可变(Immutable)的。每次对 string 变量重新赋值或修改,都会在堆上创建一个全新的字符串对象。
原理
string str1 = "123";
string str2 = str1; // str2 指向 "123"
str2 = "321"; // str2 指向新对象 "321",str1 仍然是 "123"
C# 底层对 string 做了特殊处理,每次赋新值相当于 new 了一个新字符串,而不是修改原对象。
性能影响
- 频繁拼接字符串会产生大量临时对象,增加 GC 压力
- 解决方案:使用
StringBuilder进行大量字符串操作
// 不推荐:每次循环都创建新字符串对象
string s = "";
for (int i = 0; i < 1000; i++)
{
s += i; // 产生大量垃圾
}
// 推荐:StringBuilder
System.Text.StringBuilder sb = new System.Text.StringBuilder();
for (int i = 0; i < 1000; i++)
{
sb.Append(i);
}
string result = sb.ToString();
答题示例
string 是引用类型,但它是不可变的。
每次给它赋新值,底层其实是 new 了一个新字符串对象,原来的不变。所以看起来像值类型的”它变我不变”。
性能上的影响是,频繁拼接字符串会产生很多临时对象,增加 GC 压力,大量操作时建议用 StringBuilder。
参考文章
- 7.值类型和引用类型-使用和存储上的区别
- 8.值类型和引用类型-特殊的引用类型string
2. 结构体的特点和限制
题目
C# 中结构体(struct)有什么特点?声明时有哪些限制?
深入解析
结构体是自定义的值类型,是字段和方法的集合,适合表示一组相关的小数据。
核心特点
- 值类型:赋值和传参时复制整个实例
- 轻量级:存储在栈上,无 GC 开销
- 适合场景:小数据集合,如坐标点、颜色、矩形等
声明限制
- 字段不能直接初始化(声明时不能赋值)
- 构造函数必须为所有字段赋初值
- 方法不需要 static 关键字
struct Player
{
public string name; // 不能写 = "默认名"
public int hp;
// 构造函数必须初始化所有字段
public Player(string name, int hp)
{
this.name = name;
this.hp = hp;
}
// 方法不加 static
public void TakeDamage(int damage)
{
hp -= damage;
}
}
// 使用
Player p1 = new Player("勇者", 100);
Player p2 = p1; // 值类型:p2 是 p1 的副本
p2.hp = 50;
Console.WriteLine(p1.hp); // 输出 100,p1 不受影响
答题示例
结构体是值类型,赋值的时候是复制整个实例,不是复制引用。
声明的时候有几个限制:
- 字段不能直接赋初值
- 构造函数必须初始化所有字段
- 方法不需要加 static
适合存小数据,比如坐标、颜色这种,没有 GC 开销。
参考文章
- 14.复杂数据类型-结构体
深度题
1. 冒泡排序和选择排序的区别
题目
冒泡排序和选择排序的原理分别是什么?它们在交换次数和稳定性上有什么区别?
深入解析
两种排序时间复杂度都是 O(n²),但实现方式和特性不同。
冒泡排序
- 原理:两两相邻比较,不停交换,每轮把极值”冒泡”到正确位置
- 交换次数:多,频繁交换
- 稳定性:稳定(相等元素不交换,相对位置不变)
bool isSort = false;
for (int m = 0; m < arr.Length; m++)
{
isSort = false;
for (int n = 0; n < arr.Length - 1 - m; n++)
{
if (arr[n] > arr[n + 1]) // 升序
{
isSort = true;
int temp = arr[n];
arr[n] = arr[n + 1];
arr[n + 1] = temp;
}
}
if (!isSort) break; // 优化:已有序则提前退出
}
选择排序
- 原理:每轮找出极值索引,放到目标位置
- 交换次数:少,每轮最多一次
- 稳定性:不稳定(可能改变相等元素的相对位置)
for (int m = 0; m < arr.Length; m++)
{
int index = 0;
for (int n = 1; n < arr.Length - m; n++)
{
if (arr[index] < arr[n]) // 找最大值索引
index = n;
}
if (index != arr.Length - 1 - m) // 避免无意义交换
{
int temp = arr[index];
arr[index] = arr[arr.Length - 1 - m];
arr[arr.Length - 1 - m] = temp;
}
}
对比总结
| 特性 | 冒泡排序 | 选择排序 |
|---|---|---|
| 交换次数 | 多(频繁交换) | 少(每轮最多一次) |
| 稳定性 | 稳定 | 不稳定 |
| 适用场景 | 数据量小、基本有序时优 | 数据量小、交换成本高时优 |
答题示例
冒泡是两两比较,满足条件就交换,每轮把最大的冒到最后。
选择是每轮找最大值的索引,最后只交换一次。
冒泡交换次数多但是稳定,选择交换少但不稳定。
如果数据基本有序,冒泡有优化空间;如果交换成本高,选择更合适。
参考文章
- 15.排序-冒泡排序
- 16.排序-选择排序
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com