1、委托和事件(C#)
委托(Delegate)是什么?
委托 = 能存储「方法」的「变量」
委托的作用
-
把方法当参数传递
-
实现解耦(调用者不用知道具体是谁执行)
-
是事件、回调、Lambda、匿名函数的基础
事件(Event)是什么?
事件 = 加了「安全限制」的特殊委托
事件本质就是委托,但更安全、更适合做消息通知。
| 特性 | 普通委托 | 事件(Event) |
|---|---|---|
| 外部能否赋值 | 可以直接覆盖 | 不允许外部赋值 |
| 外部能否触发 | 可以随便调用 | 只有类内部能触发 |
| 使用场景 | 通用方法容器 | 消息通知、按钮点击、状态变化 |
| 安全性 | 低 | 高 |
事件 = 被保护起来的委托,防止被乱用。
极简代码示例
// 1. 先定义委托(事件依赖委托)
delegate void DoorBellHandler();
// 2. 定义一个类,里面包含「门铃事件」
class Room
{
// 定义事件(关键字 event)
public event DoorBellHandler DoorBell;
// 只有类内部能触发事件
public void Ring()
{
DoorBell?.Invoke(); // 触发:有人按门铃
}
}
// 3. 使用事件
class Program
{
static void Main()
{
Room room = new Room();
// 订阅事件:听到门铃就执行 OpenDoor
room.DoorBell += OpenDoor;
// 触发事件
room.Ring();
}
static void OpenDoor()
{
Console.WriteLine("开门!");
}
}
输出:
开门!
事件的特点
-
只能用
+=/-=订阅 / 取消,不能用 = 覆盖 -
外部不能直接触发事件,保证安全
-
专门用于模块间通信:按钮点击、消息到达、数据更新
一句话终极总结
-
委托:装方法的盒子,能存、能调用、能传递。
-
事件 :受安全保护的委托 ,专门用来做通知 / 消息 / 回调。
2、叙述private、protected、public、internal修饰符
一句话速记
权限从严到松:private < protected < internal < public,控制类 / 成员能被谁访问。
逐个详解
-
private(私有) :最严格,只有当前类内部能访问。
用法:字段、私有工具方法默认都用它。
-
protected(受保护) :当前类 + 子类(不管在哪个程序集) 可以访问。
用法:要留给继承类重写 / 调用的成员。
-
internal(内部) :整个当前项目 (程序集) 都能访问,跨项目不行。
用法:项目内部共用、不想对外暴露的类。
-
public(公开):完全无限制,任何地方、跨项目都能访问。
用法:对外暴露的接口、公共工具类。
快速对比表
| 修饰符 | 本类 | 子类 | 同项目 | 跨项目 |
|---|---|---|---|---|
| private | ✅ | ❌ | ❌ | ❌ |
| protected | ✅ | ✅ | ❌ | ❌ |
| internal | ✅ | ✅ | ✅ | ❌ |
| public | ✅ | ✅ | ✅ | ✅ |
补充小规则
-
类内部成员默认权限是 private;
-
顶级 class 默认是 internal;
-
protected internal是组合权限:同项目 或 子类 都能访问。
3、CTS、CLS、CLR分别怎么解释
CLR 是运行容器,CTS 是类型统一规则,CLS 是跨语言兼容子集,三者是.NET 底层核心架构。
1. CLR (Common Language Runtime 公共语言运行时)
-
通俗说:.NET 的虚拟机,程序跑起来的「发动机」。
-
作用:管理内存、垃圾回收 GC、线程调度、安全校验、JIT 编译 IL 转机器码。
-
位置:所有 C#/VB.NET代码最终都交给 CLR 执行。
2. CTS (Common Type System 通用类型系统)
-
通俗说:.NET 所有语言统一的「类型标准字典」。
-
作用:规定所有数据类型(int、string、类、继承)怎么定义、怎么继承、怎么兼容。
-
意义:C# 的 int 和 VB 的 Integer 在 CTS 里是同一个类型,底层互通。
3. CLS (Common Language Specification 公共语言规范)
-
通俗说:跨语言开发的「最低兼容公约」。
-
作用:是 CTS 的子集,限制语法和命名,保证 C# 写的类能被 VB/F# 无缝调用。
-
场景:做类库给其他.NET 语言调用时,要遵守 CLS 规范。
极简对比记忆
-
CLR:管运行(程序活着靠它)
-
CTS:管类型(所有语言类型统一)
-
CLS:管兼容(跨语言互相调用)
4、什么是装箱和拆箱?
装箱:值类型 转 引用类型 ;拆箱:引用类型转回值类型,会损耗性能。
1. 基础概念
C# 分两种类型:
-
值类型:int、double、struct,存在栈内存
-
引用类型:object、string、类,存在堆内存
装箱(Boxing)
隐式自动发生:把栈上的值类型,包装进堆里的 object 对象
int a = 10;
object obj = a; // 自动装箱
过程:a 在栈 → 复制数据到堆新建 object → obj 指向堆地址
拆箱(Unboxing)
显式强制转换:把堆里 object,拆回栈里的值类型
int b = (int)obj; // 强制拆箱
过程:检查 obj 类型匹配 → 复制堆数据回栈变量
2. 关键特点
-
装箱自动、拆箱必须强转
-
频繁转换会产生 GC 压力、拖慢性能
-
泛型 List<T>能避免装箱拆箱,这也是优先用泛型的原因
3. 快速记忆口诀
值变引用是装箱,引用变值是拆箱;
装箱自动拆箱强,频繁使用耗性能。
5、string str = null 和string str = "";区别?
null 是空引用(没指向任何内存);"" 是空字符串(有内存对象,里面没字符)。
1. 底层本质
- string str = null;
-
栈里变量没指向堆任何地址
-
不存在字符串对象
-
调用
.Length、.Trim()直接报空指针异常
- string str = "";
-
在堆里已有空字符串常量对象
-
内存已分配,是合法实例
-
可以正常调用所有字符串方法,不会报错
2. 实操对比代码
string a = null;
string b = "";
//Console.WriteLine(a == null); // True
Console.WriteLine(b == null); // False
Console.WriteLine(b.Length); // 0 正常运行
a.ToString(); // 直接抛 NullReferenceException
3. 开发实用判断
日常判空推荐写法(一步兼顾两者):
// 同时判断 null 和空字符串
if(string.IsNullOrEmpty(str))
{
// 为空逻辑
}
// 连空白空格也一起判
if(string.IsNullOrWhiteSpace(str))
4. 快速记忆
-
null = 空空如也,对象都没有
-
"" = 空盒子,盒子存在但里面没东西
6、.NET(dotnet)中,类(class)和结构体(struct)的区别?
class 是引用类型 存堆、支持继承;struct 是值类型存栈、不能继承、轻量小巧。
1. 内存存储
-
class 类 :引用类型,实例存在堆 Heap,栈只存地址
-
struct 结构体 :值类型,直接存在栈 Stack(或随宿主内嵌)
2. 继承特性
-
class:支持单类继承、多接口继承,可抽象、可虚方法重写
-
struct:不能继承类,只能实现接口;默认隐式密封
3. 默认构造
-
class:可自定义无参构造
-
struct:编译器强制自带无参构造,不能手动写无参构造(新版.NET 放宽但仍有约束)
4. 赋值行为
-
class 赋值:拷贝引用地址,多个变量指向同一个对象
-
struct 赋值:完整拷贝整个数据,互不影响
5. 初始化与 null
-
class:变量可赋值
null,代表无实例 -
struct:值类型不能为 null(可加?变成可空 struct)
6. 使用场景
-
class:业务复杂、需要继承、对象较大、需要多地方共享引用
-
struct:数据简单小巧(坐标、颜色、配置参数)、高频创建追求性能
快速对照表
| 特性 | class 类 | struct 结构体 |
|---|---|---|
| 类型 | 引用类型 | 值类型 |
| 内存 | 堆 | 栈 / 内嵌 |
| 继承 | 支持类继承 | 不可继承类 |
| 赋值 | 引用拷贝 | 值完整拷贝 |
| null | 可以 null | 默认不可 null |
| 适用 | 复杂业务对象 | 轻量小型数据 |
7、枚举的作用是什么?
枚举就是把固定常量换成易懂名字,替代魔法数字,代码更干净、不易错。
1. 基础理解
不用枚举时:
// 看不懂 1/2/3 代表啥,还容易写错
int status = 1;
用枚举:
enum OrderStatus
{
WaitPay = 1,
Shipping = 2,
Finished = 3
}
OrderStatus status = OrderStatus.WaitPay;
一眼就懂语义。
2. 核心作用
-
语义清晰:用名字代替数字,可读性拉满
-
限制取值:只能选枚举里定义的值,避免乱传非法数字
-
统一规范:全项目共用一套固定状态,不会各写各的
-
便于维护:改一处枚举,全局生效
-
配合强类型:编译器校验,减少 Bug
3. 常用场景
订单状态、性别、权限级别、设备模式、开关类型这类固定有限选项,全都适合用枚举。
4. 小补充
默认底层是int,可以改类型;还能转数字、字符串互相解析。
8、内存泄露
内存泄露:不用的对象还被强引用挂着,GC 回收不掉,内存越用越多、程序越来越卡。
1. 通俗理解
你租房子:搬走了但没退钥匙 → 房子一直占着不让别人用
代码里:对象已经没用了,还有地方偷偷引用它 → GC 清不掉,内存堆积。
2. C# 常见泄露原因
-
事件没解绑
订阅事件
+=后,退出不用了没
-=,对象被死死持有。
-
静态集合一直累加
static List不停 Add,从不 Clear,对象永远被引用。
-
长生命周期持有短生命周期对象
全局类引用窗口 / 临时控件,窗口销毁也释放不了。
-
非托管资源没释放
文件流、数据库连接、串口、Bitmap 没
Dispose()、没 using。
-
定时器 / 后台线程没停止
线程死循环、Timer 不关,牢牢绑定对象。
3. 怎么避免
-
事件成对:
+=订阅,关闭时必-=取消 -
非托管资源用 using 自动释放
-
静态容器定期清理、不要无限加数据
-
窗口 / 页面销毁时:停线程、关定时器、清空引用
-
用弱引用
WeakReference存非必要对象
4. 快速判断现象
程序运行越久内存越高,重启就恢复 → 大概率内存泄漏。
事件导致内存泄漏 完整版对照代码
1. 泄漏版(错误写法)
核心问题:订阅事件后从不解绑,宿主销毁还被持有,GC 回收不掉
// 发布者(生命周期很长)
public class LongLifePublisher
{
public event Action UpdateEvent;
public void Trigger() => UpdateEvent?.Invoke();
}
// 订阅者(短生命周期,用完要销毁)
public class ShortLifeSubscriber
{
public void DoSomething()
{
Console.WriteLine("执行业务逻辑");
}
}
// 调用测试
class Program
{
static void Main()
{
var publisher = new LongLifePublisher();
// 局部订阅者,本该用完就回收
var sub = new ShortLifeSubscriber();
publisher.UpdateEvent += sub.DoSomething;
// sub 离开作用域,手动置空
sub = null;
// GC强制回收
GC.Collect();
GC.WaitForPendingFinalizers();
// 结果:sub 根本没被回收!事件还握着它的引用 → 内存泄漏
}
}
2. 修复版(正确写法)
用完必须 -= 解绑事件
class Program
{
static void Main()
{
var publisher = new LongLifePublisher();
var sub = new ShortLifeSubscriber();
// 订阅
publisher.UpdateEvent += sub.DoSomething;
// 关键:不用了立刻解绑
publisher.UpdateEvent -= sub.DoSomething;
sub = null;
GC.Collect();
GC.WaitForPendingFinalizers();
// 现在:订阅者成功被GC回收,无泄漏
}
}
3. 工业上位机常用补充(你工控开发必用)
窗口关闭时固定套路:
protected override void OnClosed(EventArgs e)
{
// 1. 解绑所有外部事件
plc.DataReceive -= Plc_DataReceive;
// 2. 释放串口/相机/流资源
serialPort?.Close();
camera?.Dispose();
// 3. 停定时器、后台线程
timer?.Stop();
base.OnClosed(e);
}
4. 一句话记死
长对象绑短对象事件,不解绑必泄漏;退出页面全清理,内存稳得住。
静态集合导致内存泄漏 + 修复代码
1. 泄漏原理
static 静态集合全局常驻内存,不断 Add 对象、从不清理;
临时对象用完还被集合强引用,GC 永远回收不掉,内存一路暴涨。
2. 泄漏错误代码
// 全局静态列表:程序整个生命周期都存在
public static class GlobalContainer
{
// 静态集合常驻内存
public static List<UserInfo> UserList = new List<UserInfo>();
}
// 临时业务类
public class UserInfo
{
public string Name { get; set; }
}
class Program
{
static void Main()
{
for (int i = 0; i < 1000; i++)
{
// 创建大量临时对象
var user = new UserInfo();
// 加入静态集合
GlobalContainer.UserList.Add(user);
}
// 循环结束,所有user局部变量失效
// 但还卡在静态List里 → GC无法回收 → 内存泄漏
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
问题关键点
-
static List生命周期 = 整个程序 -
不断 Add 不 Remove/Clear
-
临时对象被静态集合永久持有,泄漏常驻
3. 修复正确写法
方案 1:用完主动清空 / 移除
//业务结束后,不再使用立刻清理
GlobalContainer.UserList.Clear();
方案 2:按需复用,不要无限新增
//复用旧对象,而非一直new添加
if(GlobalContainer.UserList.Count > 0)
{
var user = GlobalContainer.UserList[0];
}
方案 3:弱引用存储(超大对象专用)
//弱引用:GC想回收就能回收,不卡死内存
public static List<WeakReference<UserInfo>> WeakUserList
= new List<WeakReference<UserInfo>>();
//添加方式
WeakUserList.Add(new WeakReference<UserInfo>(new UserInfo()));
工控上位机额外重点(你高频踩坑)
-
设备连接、相机图像缓存不要塞静态 List 无限堆积;
-
日志缓存、报警列表一定要定时清理 / 限制最大条数;
-
页面关闭时,清空绑定的静态数据源。
9、什么是SDK
SDK = 给开发者用的「工具包」,让你不用写底层代码,直接调用现成功能。
1. 通俗理解
你要做一个扫码支付的功能:
-
自己从头写:加密、网络、安全、对接银行 → 累死
-
用支付宝 SDK:直接调用
Pay()一个方法就搞定
SDK = 别人帮你把复杂底层封好,你拿来直接用。
2. 官方解释
SDK(Software Development Kit)
软件开发工具包:
-
包含:库文件(dll)+ 示例代码 + 文档 + 工具
-
作用:快速实现某类功能(相机、PLC、串口、人脸识别、支付)
-
你在 C# 里添加的NuGet 包、PLC 驱动、相机 SDK 都是 SDK
3. 你在工控 / 上位机里最常见的 SDK
-
PLC SDK(欧姆龙、三菱、西门子、Modbus)
-
相机 SDK(海康威视、大恒、Basler)
-
串口 / 网口通讯 SDK
-
硬件传感器 SDK
这些都是厂商给你封装好的,你直接调用方法就能读写数据。
4. SDK 和 API 的简单区别
-
SDK:工具箱(一堆工具 + 方法 + 文档)
-
API :工具箱里的某个具体接口 / 方法
例子:
-
海康 SDK = 整个相机工具包
-
Play()、Capture()= 里面的 API 接口
5. 面试一句话背下来
SDK 是软件开发工具包,包含库、文档、示例,帮助开发者快速调用硬件或第三方功能,不用从零开发底层。
总结
-
SDK = 现成功能工具箱
-
拿来直接用,不用造轮子
-
工控里:PLC、相机、硬件驱动 几乎都是 SDK
10、什么是面向过程编程
面向过程:按步骤写代码,流程至上,跟着做事顺序一路往下执行。
1. 通俗理解
像写菜谱:
第一步洗菜 → 第二步切菜 → 第三步下锅 → 第四步出锅
从头到尾一条线,按顺序执行,拆成一个个函数 / 步骤。
2. 核心特点
-
关注怎么做流程,不是关注事物本身
-
代码由 全局变量 + 一堆函数 组成
-
数据和逻辑分开存放,不封装
-
结构线性,适合简单小程序
3. C# 极简例子(面向过程风格)
csharp
运行
// 全局数据
static string name;
static int age;
// 函数:赋值
static void SetInfo(string n, int a)
{
name = n;
age = a;
}
// 函数:打印
static void ShowInfo()
{
Console.WriteLine(name + " " + age);
}
// 按流程一步步调用
static void Main()
{
SetInfo("张三", 20);
ShowInfo();
}
4. 和面向对象对比
-
面向过程:先流程后步骤,数据散着放
-
面向对象:先封装成类,再调用对象行为
5. 记忆口诀
面向过程流水线,一步一步往前连;
数据逻辑各一边,简单小项目最方便。
11、什么是面向对象编程
面向对象编程 (OOP):把现实里的事物封装成类,用封装、继承、多态三大特性管理代码,复用性和扩展性更强。
1. 通俗理解
不按步骤写流程,而是先造模型:
比如做学生管理:
先建「学生类」(包含姓名、年龄 + 学习行为),再 new 出一个个学生对象直接用,不用反复写重复逻辑。
2. 三大核心特性(必背)
-
封装:把数据和方法藏在类里,对外只暴露必要接口,保护内部数据安全。
-
继承:子类复用父类代码,不用重复写相同逻辑。
-
多态:同一个方法,不同子类能实现不一样的行为,灵活扩展。
3. 极简代码对比
// 定义类:封装属性和行为
public class Student
{
// 封装
public string Name { get; set; }
public void Study()
{
Console.WriteLine("正在学习");
}
}
// 使用对象
Student s = new Student();
s.Name = "小李";
s.Study();
4. 和面向过程一眼分清
-
面向过程:关注怎么做步骤,流水线执行
-
面向对象:关注是谁、有什么能力,先建模再调用
5. 快速口诀
封装藏细节,继承省代码,多态更灵活;万物皆对象,项目好维护。
12、八大基本类型
C# 8 大基本值类型:整数 4 种 + 浮点 2 种 + 字符 1 种 + 布尔 1 种
1. 完整列表 + 占用字节
-
byte :1 字节,0~255(无符号)
-
short:2 字节,短整型
-
int:4 字节,常用整型(默认)
-
long:8 字节,长整型
-
float:4 字节,单精度小数(后缀 f)
-
double:8 字节,双精度小数(默认小数)
-
char:2 字节,单个字符
-
bool:1 字节,true/false 布尔
2. 补充重点
-
string 不是基本类型,是引用类型
-
decimal 财务专用,也不属于 8 大基础类型
-
默认整数写数字是
int;小数默认是double
3. 极简背诵口诀
字节 byte 长短 long,int 日常最常用;
float 单精 double 双,char 字符 bool 判空。
13、什么是值类型?什么是引用类型?
值类型存栈,拷贝就是复制本身;引用类型存堆,拷贝只复制地址。
一、值类型
-
存储位置:栈 (Stack)
-
赋值特点:互相独立,改一个不影响另一个
-
包含:基础数值 (int/double)、bool、char、struct、枚举
-
例子:
int a = 10;
int b = a;
b = 20;
// a还是10,互不干扰
二、引用类型
-
存储位置:数据在堆 (Heap),栈里只存地址引用
-
赋值特点:多个变量指向同一个堆对象,一改全改
-
包含:class、string、数组、委托
-
例子:
Person p1 = new Person();
p1.Name = "小明";
Person p2 = p1;
p2.Name = "小红";
// p1.Name 也变成小红,因为指向同一个对象
三、快速对比记忆
| 对比 | 值类型 | 引用类型 |
|---|---|---|
| 内存 | 栈 | 堆存数据,栈存引用 |
| 赋值 | 完全复制,互不影响 | 复制地址,共用对象 |
| 默认值 | 不能为 null | 可以赋值 null |
| 代表 | int、struct、枚举 | class、string、数组 |
补充彩蛋
string 看着像值类型(改值不互相影响),本质是引用类型,只是有字符串不可变特性。
14、什么是封装
封装:把数据和方法包进类里,隐藏内部细节,只暴露安全入口给外面用。
1. 通俗理解
像充电宝:
外壳包住内部电池、电路(隐藏细节)
只留充电口 / 按键给你用(暴露简单接口)
你不用管里面结构,安全又好用。
2. C# 代码体现
-
字段用
private藏起来 -
属性 / 方法
public对外开放
public class Person
{
// 隐藏内部数据
private int _age;
// 公开安全访问入口
public int Age
{
get => _age;
set
{
// 加校验,防止非法数据
if(value > 0 && value < 150)
_age = value;
}
}
}
3. 三大好处
-
安全:外部不能随便乱改内部字段,能加校验
-
简洁:内部逻辑藏起来,调用简单
-
易维护:内部改代码,外部不用动
速记口诀
数据私有藏内部,公开方法做门户;保护隐私防乱改,封装安全好维护。
15、什么是继承
继承就是子类直接复用父类代码,不用重复写,还能扩展新功能。
通俗理解
父亲会:开车、赚钱
儿子继承父亲:天生就会开车、赚钱(直接复用)
儿子还能额外学:编程(自己扩展新能力)
C# 代码示例
// 父类
public class Person
{
public string Name { get; set; }
public void Sleep()
{
Console.WriteLine("睡觉");
}
}
// 子类 Student 继承 Person
public class Student : Person
{
// 自己新增的方法
public void Study()
{
Console.WriteLine("学习");
}
}
调用:
Student s = new Student();
s.Name = "小明";
s.Sleep(); // 直接继承父类,不用重写
s.Study(); // 自己独有
继承核心特点
-
减少重复代码,复用父类成员
-
单一继承:C# 类只能继承一个父类
-
可以用
override重写父类方法改逻辑
口诀
父类通用写一套,子类继承直接套;
原有功能不用改,新增扩展更灵巧。
16、什么是多态
父类引用指向子类对象,同一个方法,不同子类执行不同逻辑,这就是多态。
通俗理解
同样是 "叫声":
猫叫喵喵、狗叫汪汪;
你统一喊【动物发声】,但猫和狗表现不一样 ------ 这就是多态。
C# 核心写法(背熟)
-
父类方法加
virtual -
子类重写加
override -
用父类变量装子类实例
代码示例
//父类
public class Animal
{
public virtual void Cry()
{
Console.WriteLine("动物叫");
}
}
//子类1
public class Cat : Animal
{
public override void Cry()
{
Console.WriteLine("喵喵");
}
}
//子类2
public class Dog : Animal
{
public override void Cry()
{
Console.WriteLine("汪汪");
}
}
调用:
Animal a1 = new Cat();
Animal a2 = new Dog();
a1.Cry(); //喵喵
a2.Cry(); //汪汪
三大好处
-
统一父类写法,不用写一堆 if 判断
-
新增子类不用改旧代码,扩展性强
-
项目结构更干净
速记口诀
虚方法父类打底,子类重写改逻辑;
父类引用装子类,运行自动看真身。
17、什么是重载?什么是重写?
重载:同一个类里,方法名一样、参数不一样;
重写:继承关系里,子类覆盖父类的虚方法。
一、重载(Overload)
-
位置:同一个类内部
-
规则:
-
方法名完全相同
-
参数个数 / 类型 / 顺序不同
-
和返回值无关、不需要关键字
- 代码示例
csharp
运行
public void Calc(int a) { }
public void Calc(int a, int b) { } // 参数不同:重载
public void Calc(double a) { } // 类型不同:重载
- 特点:编译时就确定调用哪个
二、重写(Override)
-
位置:父子继承关系
-
规则:
-
父类:
virtual虚方法 -
子类:用
override完全覆盖 -
方法名、参数、返回值必须一模一样
- 代码示例
csharp
运行
public class Father
{
public virtual void Show(){}
}
public class Son : Father
{
public override void Show(){} // 重写
}
- 特点:运行时看真实对象类型(多态核心)
三、一眼区分对照表
表格
| 对比 | 重载 Overload | 重写 Override |
|---|---|---|
| 关系 | 同类平级 | 父子继承 |
| 关键字 | 不用关键字 | virtual + override |
| 参数 | 必须不一样 | 必须完全一样 |
| 多态 | 编译多态 | 运行多态 |
口诀背
重载同类多名参;
重写继承盖父栏。
18、基本类型传值
C# 里基本值类型传参默认是值传递:传的是拷贝,方法里改参数,外面原值完全不变。
1. 简单例子
void Change(int num)
{
num = 999; // 改的是副本
}
int a = 10;
Change(a);
Console.WriteLine(a); // 输出还是 10
2. 原理
-
int、double、bool、struct 都是值类型
-
传参时:复制一份副本进方法
-
方法内部只改副本,外面原变量不受影响
3. 想改外面原值怎么办?
加 ref / out 变成引用传递:
void ChangeRef(ref int num)
{
num = 999;
}
int a = 10;
ChangeRef(ref a);
Console.WriteLine(a); // 输出 999
口诀记住
基本类型默认传值,修改互不影响;
加 ref 才改原变量。
19、什么是引用类型传值
引用类型传值:传的是地址的副本,地址不变能改对象内容,但不能换外面的引用本身。
1. 通俗拆解
-
引用类型(class 实例)栈里存地址,堆里存真实对象;
-
传参时:把地址复制一份传给方法(传值);
-
两个变量地址一样,指向同一个堆对象。
2. 代码演示(能改对象内部)
public class Person
{
public string Name { get; set; }
}
void Test(Person p)
{
p.Name = "李四"; // 修改堆里原对象内容
}
//调用
Person per = new Person(){Name="张三"};
Test(per);
Console.WriteLine(per.Name); // 输出:李四 ✅ 变了
3. 关键对比误区
方法里new 新对象,外面不会变:
void TestNew(Person p)
{
p = new Person(){Name="新的人"}; // 只改副本地址
}
Person per = new Person(){Name="张三"};
TestNew(per);
Console.WriteLine(per.Name); // 还是张三 ❌没变
4. 核心口诀
引用传值传地址副本:
改对象字段全局生效;
改引用指向外面不动。
补充
想直接换掉外面引用,要加 ref:
void TestRef(ref Person p)
{
p = new Person();
}
20、什么是抽象类?什么是接口?
抽象类:半完整模板,有成员有实现,只能单继承;
接口:纯行为规范,全是定义无字段,能多实现。
一、抽象类 abstract class
- 特点
-
用
abstract修饰,不能 new 实例 -
可以有:属性、字段、已实现方法 + 抽象无体方法
-
子类必须重写全部抽象方法
-
C# 只能继承一个抽象类
-
有构造函数
- 极简示例
abstract class Animal
{
// 已有实现
public void Sleep() => Console.WriteLine("睡觉");
// 抽象方法:必须子类重写
public abstract void Cry();
}
class Cat : Animal
{
public override void Cry() => Console.WriteLine("喵喵");
}
二、接口 interface
- 特点
-
用
interface定义,全是行为契约 -
不能有字段、不能有构造函数
-
默认成员都是 public
-
一个类可以同时实现多个接口
-
新版 C# 可加默认方法实现,但依然无状态
- 极简示例
interface IRun
{
void Run();
}
class Dog : IRun
{
public void Run() => Console.WriteLine("狗狗奔跑");
}
三、核心对比(速背)
| 区别 | 抽象类 | 接口 |
|---|---|---|
| 内容 | 有字段、有实现、有抽象 | 只有方法 / 属性定义(无字段) |
| 继承数量 | 只能继承 1 个 | 能实现多个 |
| 构造函数 | 有 | 没有 |
| 定位 | 父子「血缘模板」 | 能力「行为规范」 |
四、口诀
抽象类存状态,一单继承带实现;
接口定行为,多实现纯规范。
21、如何在同一个类中实现具有相同方法名的接口
同名接口方法冲突时,用显式接口实现,只能通过接口调用,隐藏冲突方法。
1. 场景:两个接口同名方法
// 接口1
interface IA
{
void Show();
}
// 接口2
interface IB
{
void Show();
}
同一个类同时实现,两个Show重名冲突。
2. 普通写法会报错,显式实现解决
public class Test : IA, IB
{
// 显式实现 IA 的Show
void IA.Show()
{
Console.WriteLine("IA 的 Show");
}
// 显式实现 IB 的Show
void IB.Show()
{
Console.WriteLine("IB 的 Show");
}
}
3. 调用方式(重点)
显式实现的方法不是 public,不能直接用类对象点出来:
Test t = new Test();
// t.Show(); // 报错,访问不到
// 必须转成接口类型调用
IA a = t;
a.Show(); // IA 的 Show
IB b = t;
b.Show(); // IB 的 Show
4. 混合写法(一个公有、一个显式)
如果想留一个通用公开:
public class Test : IA, IB
{
// 公有统一实现
public void Show()
{
Console.WriteLine("公共Show");
}
// 单独显式覆盖另一个
void IB.Show()
{
Console.WriteLine("IB专属Show");
}
}
速记口诀
接口同名有冲突,显式写明接口名;
实例不能直接调,转成接口再调用。
22、什么构造函数?它有那些使用方法?那些不同的类型?
构造函数:创建对象new时自动执行的特殊方法,用来初始化赋值,名字和类名一模一样。
一、核心特点
-
方法名 = 类名
-
没有返回值
-
new 类名()自动调用 -
作用:初始化字段、属性
二、三大类型 + 用法(超好记)
1. 无参构造(默认构造)
没参数,不写编译器会自动给一个
public class Person
{
// 无参构造
public Person()
{
Console.WriteLine("无参构造执行");
}
}
//调用:Person p = new Person();
2. 有参构造
创建对象直接给成员赋值,不用挨个 set
public class Person
{
public string Name;
//有参构造
public Person(string name)
{
Name = name;
}
}
//调用:Person p = new Person("张三");
3. 静态构造函数 static
只执行一次,程序第一次用到类自动触发,初始化静态成员
public class Person
{
public static string Type;
//静态构造
static Person()
{
Type = "人类";
}
}
三、补充:构造函数重载 & 构造链
-
重载:一个类多个构造(参数不同)
-
this 调用本类其他构造
public Person() : this("默认名")
{
}
public Person(string name)
{
Name = name;
}
四、快速背诵总结
-
类型:无参、有参、静态三种
-
用途:实例初始化、静态数据初始化
-
规则:同名无返回,new 自动跑;静态只跑一次
23、try、catch、finally作用?
try 放可能报错代码,catch 捕获异常处理,finally 无论成败一定执行。
1. 各自作用
-
try:包裹容易崩溃、出异常的风险代码
-
catch:抓到异常,防止程序闪退,做报错提示 / 补救
-
finally :不管报错与否,必定执行,专门用来释放资源
2. 最简代码示例
try
{
int a = 1 / 0; // 一定会报错
}
catch (Exception ex)
{
// 捕获错误,程序不崩
Console.WriteLine("出错了:" + ex.Message);
}
finally
{
// 必执行:关连接、关串口、释放设备
Console.WriteLine("我一定会执行");
}
3. 工控 / 上位机重点用法
释放资源全写在 finally:
关闭串口、相机 Dispose、数据库连接、停止定时器。
4. 记忆口诀
try 冒险防崩溃,
catch 抓错救程序,
finally 收尾必执行,
资源释放放这里。
24、什么是泛型
泛型就是模板占位符,先不定死类型,用的时候再指定;一套代码适配所有类型,安全又不用重复写。
1. 通俗理解
不用泛型:
写一个 int 容器、再写一个 string 容器、再写对象容器 → 重复抄代码
用泛型:
只写 1 个模板<T>,int/string/ 类随便套用,全程强类型不拆箱。
2. 核心优点
-
代码复用:一套搞定所有数据类型
-
类型安全:编译报错,不会存错类型
-
无装箱拆箱:性能更高
3. 最简单代码示例
泛型类
// T 是类型占位符
public class Box<T>
{
public T Value { get; set; }
}
// 使用时再指定类型
Box<int> b1 = new Box<int>();
Box<string> b2 = new Box<string>();
泛型方法
public void Show<T>(T msg)
{
Console.WriteLine(msg);
}
Show(100);
Show("Hello");
4. 你天天在用的泛型
List<int> Dictionary<string,object>
这些全是泛型。
5. 记忆口诀
尖括号放 T,类型后期替;
复用又安全,性能还给力。
25、类和对象
类是模板,对象是模板造出来的实例;类抽象定义,对象真实存在。
1. 通俗理解
-
类 = 图纸(汽车设计图,定义有轮子、能跑)
-
对象
= 真车(按图纸造出来的一辆辆实体车)
一张图纸,可以造出无数辆车。
2. 代码演示
csharp
运行
// 1.类:模板
public class Person
{
public string Name;
public void Speak(){}
}
// 2.对象:new 出来实例
Person p1 = new Person();
p1.Name = "张三";
Person p2 = new Person();
p2.Name = "李四";
3. 核心区别
-
类:抽象、定义、不占实例内存,不能直接用
-
对象:具体、实例、堆内存分配,可以调用属性方法
-
关系:一个类可以 new 出无数个对象
4. 速记口诀
类是模型画蓝图,对象实例堆里住;
一类能生千万象,各自数据互不输。
26、关键字static
static 静态:属于类本身,不属于对象;全局唯一,不用 new 就能直接用。
1. 三大用法
-
静态字段 / 属性
全类共享一份内存,所有对象共用
public class Person
{
public static int Count = 0;
}
//调用:类名.成员
Person.Count = 10;
-
静态方法
不用 new 对象,直接类名调用,不能访问非静态成员
public static void Show(){}
Person.Show();
-
静态构造函数
类第一次被使用时,只执行一次,初始化静态数据
static Person()
{
Count = 1;
}
2. 核心区别
-
非静态:属于对象,new 一个就有一份
-
静态:属于类,全局只有一份,常驻内存
3. 内存 & 踩坑(你工控开发重点)
-
static 变量程序运行全程不释放,容易内存泄漏
-
多线程下静态共享数据,要加锁防冲突
4. 口诀
静态属于类,全局只一份;
不用 new 实例,常驻要谨慎。