C#学习笔记——基础知识点(叁)

目录

目录

一、命名法

  • 驼峰命名法:myName(变量)

  • 帕斯卡命名法:MyName (函数、类、属性等)

二、值类型和引用类型

1、分类

  • 引用类型: string,数组,类

  • 值类型: 其他类型,结构体,枚举

2、区别

  • 值类型在相互赋值时把内容拷贝给对方,它变我不变

  • 引用类型相互赋值时是让两者指向同一个值,它变我也变

3、原因

值类型存储在栈空间 ——系统分配,自动回收,小而快

引用类型存储在堆空间 ——手动申请和释放,大而慢,new就是在堆中开辟新空间

4、特殊引用类型string

  • string的它变我不变:具备值类型的特点

  • 原因:c#进行了特殊处理,比如将str1赋值给str2,此时str1和str2指向堆中同一个区域,当str2重新赋值时,会为str2的值在堆中开辟一个新空间,此时str1和str2指向的就不是同一个区域。

  • 缺点:频繁重新赋值会产生内存垃圾,优化替代方案下面的stringBuilder章节中

补充知识点在进阶知识点的最后一部分

三、ref和out

1、用ref和out的原因

可以解决函数内部改变外部传入的内容的问题

2、使用

返回类型 函数名(ref 参数类型 参数名1,......)
    
函数名(ref 参数)
返回类型 函数名(ref 参数类型 参数名1,......)
    
函数名(out 参数)

3、ref和out的区别

  • ref传入的变量必须初始化,out不用

  • out传入的变量必须在内部赋值,ref不用

4、对比表

对比维度refout
调用前是否初始化必须不必
方法内是否必须赋值必须
数据流方向传入+传出仅传出
语义侧重点修改已有值生成新值

四、面向对象三大特性

1、封装(Encapsulation)

  • 核心思想:将对象的状态(属性)和行为(方法)包装在一起,对外隐藏内部实现细节,仅暴露必要的交互接口。
  • 意义
    • 保护数据安全,避免外部直接修改对象内部状态。
    • 降低代码耦合度,修改内部实现时不影响外部调用。
    • 提升代码可维护性,统一的接口让使用更清晰。

2、继承(Inheritance)

  • 核心思想:让一个类(子类)可以继承另一个类(父类)的属性和方法,实现代码复用与拓展。
  • 意义
    • 复用父类的代码,减少重复编写。
    • 建立类的层级关系,让代码结构更清晰。
    • 为多态提供基础。

3、多态(Polymorphism)

  • 核心思想:同一个行为在不同对象上可以有不同的表现形式,分为编译时多态运行时多态

  • 分类

    • 编译时多态(静态多态):通过方法重载、运算符重载实现,在编译阶段就确定调用哪个方法。

    • 运行时多态(动态多态):通过继承 + 虚方法重写实现,在运行阶段根据对象的实际类型确定调用哪个方法。

  • 意义

    • 提升代码的灵活性和可扩展性,新增子类时无需修改原有调用逻辑。
    • 符合开闭原则,拓展新功能时不影响旧代码。

4、三大特性关系总结

  • 封装是基础:隐藏内部细节,保障数据安全,为继承和多态提供稳定的基础。

  • 继承是手段:复用代码,建立类的层级关系,是实现多态的前提。

  • 多态是目的:让代码更灵活、可扩展,是面向对象设计的核心价值体现。

五、万物之父与装箱拆箱

1、万物之父

  • 关键字:object

  • 概念:object是所有类型的基类,可以使用里氏替换原则,用object装载所有类型的对象。

  • 使用:例子如下

// string
object obj = "123456";
string str = obj as string;

// 值类型
object obj2 = 1f;
float f1 = (float)o2;

2、装箱拆箱

(1)概念

  • 装箱:用object存值类型,把值类型用引用类型存储,栈内存会迁移到堆内存中
  • 拆箱:将object转换为值类型,把引用类型存储的值取出,堆内存会迁移到栈内存

(2)优缺点

  • 优点:不确定类型时方便参数存储和传递
  • 缺点:内存迁移增加性能消耗

3、万物之父中的方法

  • Equals: 判断两个对象是否相等,按照左侧对象Equals方法的规则进行比较
  • ReferenceEquals: 判断两个对象是否是相同的引用,值对象始终是false
  • GetType: 反射中的重要方法,返回对象运行时的类型Type
  • MemberwiseClone: 获取对象的浅拷贝
  • GetHashCode: 获取对象的哈希码(一种通过算法算出的表示对象的唯一编码,不同对象哈希码可能一样,具体由哈希算法决定)
  • ToString: 返回当前对象代表的字符串

六、垃圾回收机制

1、概念

垃圾回收(Garbage Collector,GC): 垃圾回收的过程是遍历堆上动态分配的所有对象,通过识别他们是否被引用来确定对象是否是垃圾。所谓垃圾就是没有被任何变量、对象引用的内容,而垃圾就需要被回收释放。

垃圾回收有很多算法,比如:

  • 引用计数(Reference Counting)

  • 标记清除(Mark Sweep)

  • 标记整理(Mark Compact)

  • 复制集合(Copy Collection)

C#的GC使用的是标记清除算法分代回收算法

注意:

  • GC只负责堆内存的垃圾回收
  • 引用类型都是在堆中的,所以它的分配释放都通过垃圾回收机制管理
  • 栈上的内存有系统自动管理,值类型在栈中分配内存,有自己的生命周期,因此无需对他们进行管理,会自动分配和释放

2、原理

C#的GC采用分代回收策略,将堆划分为不同的代,通常有3代,如0代内存、1代内存、2代内存

代的概念(Generations): 代是垃圾回收机制使用的一种算法(分代算法),新分配的对象通常放在第0代,每次分配都可能会进行垃圾回收以释放内存(如0代内存满时)。

在一次内存回收的过程开始时,垃圾回收器会进行以下两步:

  • 标记对象(标记清除算法),标记后的为可达对象,未被标记的就是不可达对象,即垃圾。
  • 搬迁对象,压缩堆(内存压缩):释放未标记对象的内存,搬迁可达对象,修改引用地址。

3、标记清除算法

主要分为2个阶段:

  • 标记阶段
    • 根对象:GC会从一组根对象开始,根对象指始终可达的对象(全局变量、静态变量、当前执行方法的局部变量等)
    • 标记过程:GC会遍历所有根对象,递归的标记所有从根对象可达的对象,表明他们仍被使用,不能回收
  • 清除阶段
    • 清除过程:标记阶段完成后,GC会遍历整个托管堆,将所有未被标记的对象视为垃圾,并回收他们所占的内存空间

托管堆:

不同于堆,它由CLR(Common Language Runtime,公共语言运行库)管理,当堆中内存满时,会自动清理堆中的垃圾

4、分代回收算法

C#的GC采用分代回收策略,将堆划分为不同的代,通常有3代,如0代内存、1代内存、2代内存

代的划分原则:

  • 第0代:新创建的对象通常分配到第0代。这一代的对象生命周期短,大部分创建后很快就不再使用
  • 第1代:经过一次0代垃圾回收后仍然存活的对象会晋升到第1代。这一代的对象生命周期相对较长
  • 第2代:经过多次1代垃圾回收后仍然存活的对象会晋升到第2代。这一代的包含一些长期存活的对象,如静态对象等

分代回收的优势:

  • 提高回收效率: 大部分新创建的对象很快变成垃圾,频繁对第0代进行垃圾回收可以快速回收大量内存空间,减少垃圾回收的时间开销
  • 减少内存碎片: 分代回收将生命周期不同的对象分开处理,减少内存碎片的产生

5、内存压缩

某些情况下,GC可能会执行内存压缩操作,将活动对象移动到堆的连续区域,消除内部碎片,提高内存利用率和分配性能

6、触发垃圾回收的条件

  • 内存不足
  • 特定代达到阈值:某一代的对象数量达到一定阈值,就会触发该代的垃圾回收
  • 手动调用GC.Collect方法

七、string和StringBuilder

1、string

  • 特点:string变量只要修改就会分配新的空间,string本质是char数组

  • 常用方法:

1.字符串指定位置获取,字符串长度
string str;
str[0]
str.Length

2.字符串拼接
string.Format("{0}",str);

3.正向查找字符位置(没找到默认返回-1)
str = "唐老狮";
str.indexOf("唐");

4.反向查找字符位置
str.LastIndexOf();

5.移除指定位置后的字符(返回新字符串,不改变原字符串)
str = str.Remove(4); // 把索引4后的字符全移除
str = str.Remove(开始位置,字符个数);

6.替换指定字符串
str = str.Replace(老字符串,新字符串);

7.大小写转换
str = str.ToUpper();
str = str.ToLower();

8.字符串截取(截取从指定位置开始之后的字符串)
str = str.Substring(2);
str = str.Substring(开始位置,指定个数);
    
9.字符串切割
str = "1,2,3,5,6,7,8";
string[] strs = str.Split(',');

2、StringBuilder

  • 概念:C#提供的用于处理字符串的公共类,修改字符串而不创建新的对象,适用于频繁修改和拼接的字符串,提升性能
  • 使用:
using System.Text
StringBuilder str = new StringBuilder("123456");
  • 常用方法:
1、容量
str.Capacity;

2、字符串长度
str.Length;

3、增加
str.Append(增加内容);
str.AppendFormat("{0}",100);

4、插入
str.Insert(位置,内容);

5、删除
str.Remove(位置,个数);

6、清空
str.Clear();

7、查找
str[0];

8、修改
str[0] = 'A'; // string是不能这样改的

9、替换
str.Repalce(原字符串,新字符串);

3、string和stringBuilder区别

  • string相对StringBuilder更容易产生垃圾,每次修改拼接都会产生垃圾
  • string相对StringBuilder更加灵活,提供了更多的方法供使用

4、如何优化内存

  • 少new对象、少产生垃圾
  • 合理使用static
  • 合理使用string和StringBuilder

八、结构体和类的区别

1、区别概述

结构体和类最大区别是在存储空间上的,结构体是值类型,类是引用类型。结构体存储在栈上,类存储在堆上。

结构体和类在使用上很相似,并且结构体具备面向对象的封装特性,但不具备继承和多态特性。因为不具备继承特性,因此结构体无法使用protected修饰符。但结构体可以实现接口,因为接口是行为的抽象

2、细节区别

  • 结构体是值类型,类是引用类型
  • 结构体存在栈中,类存在堆中(具体点就是:结构体局部变量在栈、作为类字段时在堆;类实例数据在堆、引用本身可在栈/堆)
  • 结构体成员无法使用protected修饰符
  • 结构体成员变量声明无法指定初始值(静态字段除外),类可以
  • 结构体不能声明无参构造函数,类可以(C#10 及以上结构体可以声明无参构造函数,此时会覆盖默认的)
  • 结构体声明有参构造函数后,无参构造不会被顶掉;类声明有参构造后,默认无参构造会被顶掉
  • 结构体不能声明析构函数,类可以
  • 结构体不能继承,类可以
  • 结构体需要在构造函数中初始化所有成员变量,类随意
  • 结构体本身不能用static修饰(没有静态结构体),类可以
  • 结构体不能在内部声明和自己一样的结构体变量,类可以

3、如何选择

  • 想要继承和多态,选择类,如玩家、怪物等等
  • 对象是数据集合时,优先考虑结构体,如位置、坐标、颜色等等
  • 从值类型和引用类型的区别去考虑,比如经常被赋值、传递的对象,并且修改副本不想影响原对象,就用结构体,如坐标、向量、旋转等等

九、抽象类和接口的区别

1、共同点

  • 都可以被继承
  • 都不能直接实例化
  • 都可以包含方法声明
  • 子类必须实现未实现的方法
  • 遵循里氏替换原则

2、区别

  • 抽象类可以有构造函数;接口不能
  • 抽象类只能单继承;接口可以被继承多个
  • 抽象类可以有成员变量;接口不能
  • 抽象类可以声明成员方法、虚方法、抽象方法、静态方法;接口只能声明没有实现的抽象方法
  • 抽象类方法可以使用访问修饰符;接口建议不写,默认public

3、如何选择

  • 表示对象的用抽象类,表示行为拓展的用接口
  • 不同对象拥有相同行为,往往可以用接口实现

十、面向对象的七大原则

总体目标: 高内聚低耦合,使程序模块的可重用性、移植性增强

1、单一职责原则(Single Responsibility Principle, SRP)

  • 核心思想:一个类只应该负责一项功能,专注于单一职责。
  • 意义:减少类被修改的频率,避免功能间的关联耦合,防止修改一个功能时影响其他功能。
  • 举例:程序、策划、美术类应各司其职,只处理自身职责范围内的内容。

2、开闭原则(Open-Closed Principle, OCP)

  • 核心思想:对拓展开放,对修改关闭。
  • 意义:新增功能时优先通过拓展(如新增子类、实现新接口)完成,而非修改原有代码,以此保障系统稳定性。
  • 举例:通过继承扩展功能,添加新的子类并重写父类方法来实现新需求。

3、里氏替换原则(Liskov Substitution Principle, LSP)

  • 核心思想:任何父类出现的地方,子类都可以替换,且不会破坏原有系统的逻辑。
  • 意义:保障继承体系的正确性,让父类容器可以安全地装载子类对象。
  • 举例:父类的容器可以直接装入子类对象,因为子类包含了父类的所有内容。

4、依赖倒转原则(Dependence Inversion Principle, DIP)

  • 核心思想:依赖于抽象,不要依赖于具体实现。
  • 意义:通过抽象(接口 / 抽象类)解耦模块间的依赖,降低耦合度,提升系统的可扩展性。
  • 举例:依赖 “开枪接口” 这个抽象,而非具体的手枪、步枪类,间接实现不同枪械的开火功能。

5、迪米特法则(Law of Demeter, LoP)

  • 核心思想:一个对象应当对其他对象尽可能少的了解,即 “不要和陌生人说话”。
  • 意义:降低对象间的耦合度,减少不必要的直接关联。
  • 举例:一个类的成员应尽量少与其他类直接建立关系,仅与 “朋友”(直接关联的对象)交互。

6、接口隔离原则(Interface Segregation Principle, ISP)

  • 核心思想:不应该强迫别人依赖他们不需要使用的方法,一个接口只提供单一对外功能。
  • 意义:避免臃肿的大接口,让使用者可以按需选择实现对应的接口。
  • 举例:将移动行为拆分为飞行接口走路接口跑步接口等,而非封装到一个大接口中。

7、合成复用原则(Composite Reuse Principle, CRP)

  • 核心思想:尽量使用对象组合,而不是继承来达到复用的目的。
  • 意义:继承是强耦合关系,组合是低耦合关系,优先组合可提升系统灵活性。
  • 举例:脸是眼镜、鼻子、嘴巴的组合,角色与装备也是组合关系,而非继承关系。

十一、控制台相关

1、输入输出

Console.WriteLine()
Console.Write()
Console.ReadLine()
Console.ReadKey(true) // 若括号中为true,则不会把输入的内容显示在控制台上
Console.ReadKey(true).Key // 返回枚举类型

2、清空

Console.Clear()

3、设置控制台大小

Console.SetWindowSize(宽,高)

4、缓冲区大小

Console.SetBufferSize(宽,高)

5、设置光标的位置

Tip:

  • 左上角为0,0 右侧x轴正方向,下方y轴正方向

  • 边界问题

  • 横纵距离单位不同 1y = 2x (视觉上)

Console.SetCursorPosition(x,y)

6、设置文字颜色

Console.ForegroundColor = ConsoleColor.Red

7、设置背景颜色

Console.BackgroundColor = ConsoleColor.White;
Console.Clear();

8、光标显隐

Console.CursorVisible = true;

9、关闭控制台

Environment.Exit(0);

10、随机数

Random ran = new Random();
ran.Next(5,100) // 5~99的随机数

11、检测按键按下

Console.KeyAvailable  // 键盘按下键时变为true,否则为false