home commit with new plugins

This commit is contained in:
2026-06-21 09:51:55 +08:00
parent ebb9e385c5
commit 3d067fe75c
85 changed files with 10794 additions and 1443 deletions
@@ -0,0 +1,224 @@
# 万物之父和装箱与拆箱
# object
概念:
`object``System.Object` 的别名
在 C# 的统一类型系统里,可以把它理解成==万物之父==
因为==所有类型最终都能当成 `object` 来看待==
通常用法:
1. 作为所有类型的公共父类型来理解 C# 类型系统
2. 在不知道具体类型时,临时用 `object` 接收数据
3. 理解为什么值类型可以被当成“对象”使用
核心理解:
1. 引用类型赋值给 `object` 时,通常只是==把引用当成 `object` 看待==
2. 值类型赋值给 `object` 时,会发生==装箱==
3. 所以“能放进 `object`”不代表底层过程都一样
案例:
```csharp
string str = "hello";
object o1 = str; // 引用类型赋值给 object,不是装箱
int num = 10;
object o2 = num; // 值类型赋值给 object,发生装箱
```
----
# 为什么说 object 是万物之父
可以先这样记:
- 类最终都继承自 `object`
- 数组、委托等引用类型也都可以当成 `object`
- 值类型虽然不是普通类,但最终也能通过装箱当成 `object`
所以从使用角度看:
- ==所有类型最终都能统一到 `object` 这一层==
常见成员:
1. `ToString()`
把对象转成字符串
2. `Equals(object obj)`
比较两个对象是否相等
3. `GetType()`
获取对象运行时类型
4. `GetHashCode()`
获取哈希码
5. `ReferenceEquals(a, b)`
判断两个引用是不是指向同一个对象
例子:
```csharp
int num = 123;
object obj = num;
Console.WriteLine(obj.ToString());
Console.WriteLine(obj.GetType());
Console.WriteLine(obj.Equals(123));
```
补充:
- `ToString()``Equals()``GetType()``GetHashCode()` 是最常见的基础成员
- `ReferenceEquals``object` 提供的静态方法
----
# 装箱(Boxing
概念:
装箱就是==把值类型转换成 `object` 或它实现的接口类型==
运行时会把这个值==复制一份==,包装成一个对象放到托管堆上
通常用法:
1. 把值类型当成对象处理
2. 把值类型传给接收 `object` 的 API
3. 把值类型放进某些非泛型容器里
语法:
```csharp
int num = 10;
object obj = num; // 隐式装箱
object obj2 = (object)num; // 显式写法,但一般没必要
```
案例:
```csharp
int a = 123;
object obj = a; // 装箱
a = 456;
Console.WriteLine(a); // 456
Console.WriteLine(obj); // 123
```
理解:
装箱时放进 `object` 的,是当时那个值的一份副本
所以后面 `a` 变了,不会影响已经装箱进去的 `obj`
注意:
1. ==装箱是值类型 -> `object` 或接口类型==
2. 装箱会产生额外对象分配,通常比普通赋值更耗性能
3. 引用类型赋值给 `object` 不叫装箱
----
# 拆箱(Unboxing
概念:
拆箱就是==把 `object` 里的值类型取出来,还原成原来的值类型==
拆箱是==显式转换==
语法:
```csharp
object obj = 123; // 装箱
int num = (int)obj; // 拆箱
```
案例:
```csharp
object obj = 100;
int num = (int)obj;
Console.WriteLine(num);
```
核心理解:
拆箱做的事不只是“强转一下”
它会先检查 `obj` 里面装着的是不是目标值类型
检查通过后,再把值复制出来
常见错误:
```csharp
object obj = 10;
short s = (short)obj; // 报错
```
原因:
`obj` 里装箱进去的真实类型是 `int`
拆箱时要求==类型先对上==
所以应该先拆成 `int`,再按需要继续转换
正确写法:
```csharp
object obj = 10;
short s = (short)(int)obj;
```
再补一个坑:
```csharp
object obj = null;
int num = (int)obj; // 运行时报错
```
注意:
1. ==拆箱一定是显式的==
2. ==拆箱时目标类型必须和装箱时的值类型匹配==
3.`null` 拆箱会报错
4. 类型不匹配时会抛出转换异常
----
# object 和装箱拆箱的关系
最核心的一句话:
==值类型之所以也能被当成“对象”来处理,就是因为有装箱和拆箱==
对比理解:
```csharp
int num = 10;
object o1 = num; // 装箱
string str = "hello";
object o2 = str; // 不是装箱,只是引用转换
```
所以要分清:
1. 引用类型 -> `object`
通常只是“把它当父类看”
2. 值类型 -> `object`
通常会真的发生装箱
----
# 性能和使用建议
为什么要注意装箱拆箱:
1. 装箱会分配新对象
2. 拆箱要做类型检查和复制
3. 大量发生时,会增加 GC 压力和性能开销
常见容易发生装箱的场景:
1. 使用非泛型集合,比如 `ArrayList`
2. 方法参数写成 `object`
3. 值类型转成接口类型时
示意:
```csharp
ArrayList list = new ArrayList();
list.Add(1); // 装箱
list.Add(2); // 装箱
int num = (int)list[0]; // 拆箱
```
更推荐:
```csharp
List<int> list = new List<int>();
list.Add(1);
list.Add(2);
```
这样通常可以避免很多不必要的装箱拆箱。
----
# 一句话记忆
1. ==`object` 是万物之父,所有类型最终都能统一到它这一层==
2. ==值类型进 `object` 叫装箱,从 `object` 还原值类型叫拆箱==
3. ==引用类型给 `object` 不等于装箱==
----
引用:
1. Microsoft Learn: [Built-in reference types - The object type](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/reference-types)
2. Microsoft Learn: [Boxing and Unboxing (C# Programming Guide)](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/boxing-and-unboxing)
3. Microsoft Learn: [Types - C# language specification](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/types)
4. Microsoft Learn: [Object.GetType Method](https://learn.microsoft.com/en-us/dotnet/api/system.object.gettype)
5. Microsoft Learn: [System.Object.ToString method](https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-object-tostring)
6. Microsoft Learn: [System.Object.Equals method](https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-object-equals)
#核心
#object
#装箱拆箱
+284
View File
@@ -0,0 +1,284 @@
# 命名空间
概念:
命名空间使用 `namespace` 关键字定义
作用是:==组织代码,避免类型名字冲突==
简单理解:
命名空间就像文件夹
同名文件放在不同文件夹里不会冲突
同名类放在不同命名空间里也不会冲突
通常用法:
1. 按功能或模块组织代码
2. 避免不同类库之间出现同名类型冲突
3. 让大型项目结构更清楚
4. 配合 `using` 简化类型书写
----
# 语法
传统写法:
```csharp
namespace
{
class
{
}
}
```
案例:
```csharp
namespace Game.Character
{
class Player
{
}
}
```
文件范围命名空间写法:
```csharp
namespace Game.Character;
class Player
{
}
```
这种写法表示:
当前文件中的类型都属于 `Game.Character` 命名空间
初学阶段先掌握传统大括号写法即可。
----
# using 的作用
如果类型在某个命名空间中,使用前可以写完整名称。
```csharp
System.Console.WriteLine("Hello");
```
也可以先写 `using`
```csharp
using System;
Console.WriteLine("Hello");
```
理解:
`using System;` 表示引入 `System` 命名空间
这样就不用每次都写 `System.Console`
----
# 避免同名冲突
```csharp
namespace Game
{
class Player
{
}
}
namespace Tool
{
class Player
{
}
}
```
这两个 `Player` 不冲突,因为它们在不同命名空间中。
使用完整名称:
```csharp
Game.Player p1 = new Game.Player();
Tool.Player p2 = new Tool.Player();
```
如果只写:
```csharp
Player p = new Player();
```
编译器可能不知道你要用哪个 `Player`
----
# 命名空间可以分开写
同一个命名空间可以分布在多个文件中。
文件1
```csharp
namespace Game
{
class Player
{
}
}
```
文件2
```csharp
namespace Game
{
class Enemy
{
}
}
```
理解:
这两个类型都属于 `Game` 命名空间
命名空间不是文件夹本身,而是代码里的逻辑组织方式
----
# 命名空间可以嵌套
```csharp
namespace Game
{
namespace Character
{
class Player
{
}
}
}
```
更常见的写法:
```csharp
namespace Game.Character
{
class Player
{
}
}
```
两种写法都表示:
`Player` 属于 `Game.Character`
----
# 命名空间和访问修饰符
命名空间只是组织代码,不代表访问权限。
```csharp
namespace Game
{
public class Player
{
}
}
```
`public``private``internal` 这些才是控制访问权限的关键。
注意:
1. 顶级类一般不能直接写 `private`
2. 顶级类不写访问修饰符时,默认通常是 `internal`
3. 命名空间不负责决定谁能访问谁
----
# using 别名
如果两个命名空间中有同名类,可以用别名简化。
```csharp
using GamePlayer = Game.Player;
using ToolPlayer = Tool.Player;
GamePlayer p1 = new GamePlayer();
ToolPlayer p2 = new ToolPlayer();
```
这个写法不常用,但遇到同名类型冲突时很有用。
----
# 常见错误
错误1:以为文件夹名就是命名空间
实际:
文件夹名和命名空间名可以一致,但不是同一个东西
真正决定命名空间的是代码里的 `namespace`
----
错误2:忘记写 `using`
```csharp
Console.WriteLine("Hello");
```
如果没有引入 `System`,可能找不到 `Console`
解决:
```csharp
using System;
```
或者:
```csharp
System.Console.WriteLine("Hello");
```
----
错误3:同名类型不知道用哪个
```csharp
using Game;
using Tool;
Player p = new Player();
```
如果 `Game``Tool` 里都有 `Player`,这里可能会冲突。
解决:
```csharp
Game.Player p = new Game.Player();
```
----
# 什么时候需要命名空间
小练习:
可以不太关注命名空间,直接写类也能运行
项目变大后:
1. 不同模块类越来越多
2. 第三方库越来越多
3. 同名类型冲突概率增加
4. 需要清晰分层和组织
这时就应该认真使用命名空间。
----
# 注意
1. `namespace` 用来组织代码和避免命名冲突
2. `using` 用来引入命名空间,简化类型名称
3. 不同命名空间里可以有同名类型
4. 使用完整类型名可以解决同名冲突
5. 命名空间可以跨多个文件
6. 命名空间可以嵌套,常用点号表示层级
7. 命名空间不是访问权限控制工具
8. 文件夹结构和命名空间通常保持一致,但语法上不是一回事
----
# 一句话记忆
==namespace 负责给类型分组,using 负责把分组引进来方便使用。==
再压缩一点:
==命名空间像文件夹,用来防止重名。==
----
# 面试/复习时怎么说
可以这样答:
“命名空间是用 `namespace` 定义的代码组织方式,主要作用是给类型分组并避免命名冲突。
使用类型时可以写完整命名空间,也可以通过 `using` 引入命名空间来简化书写。
命名空间可以跨多个文件,也可以嵌套,但它本身不控制访问权限。”
----
引用:
1. Microsoft Learn: [namespace (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/namespace)
2. Microsoft Learn: [using directive (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-directive)
#命名空间
#核心
+424
View File
@@ -0,0 +1,424 @@
# 多态 vob
概念:
多态是面向对象三大特性之一
指的是:==同一个父类引用,指向不同子类对象时,调用同一个方法会表现出不同结果==
简单理解:
调用方只知道“这是一个动物”,但真正执行时,会根据对象实际是狗、猫还是鸟,执行各自的叫声
`vob` 可以这样记:
- `v``virtual`,父类声明虚方法
- `o``override`,子类重写父类虚方法
- `b``base`,子类中调用父类原本的成员
通常用法:
1. 父类提供统一入口
2. 子类根据自己的特点重写行为
3. 调用方只面向父类或接口写代码,不关心具体子类
4. 减少大量 `if/else` 类型判断
----
# 语法
父类:
```csharp
class
{
public virtual void ()
{
}
}
```
子类:
```csharp
class :
{
public override void ()
{
}
}
```
调用:
```csharp
= new ();
.();
```
----
# 基础案例
```csharp
class Animal
{
public virtual void Speak()
{
Console.WriteLine("动物发出声音");
}
}
class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("狗:汪汪");
}
}
class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("猫:喵喵");
}
}
Animal a1 = new Dog();
Animal a2 = new Cat();
a1.Speak(); // 狗:汪汪
a2.Speak(); // 猫:喵喵
```
理解:
- `a1``a2` 的变量类型都是 `Animal`
- 但是它们实际指向的对象分别是 `Dog``Cat`
- 调用 `Speak()` 时,会执行实际对象对应的重写版本
这就是多态。
----
# 为什么要用多态
不用多态时,代码可能会这样:
```csharp
void MakeSpeak(Animal animal)
{
if (animal is Dog)
{
Console.WriteLine("狗:汪汪");
}
else if (animal is Cat)
{
Console.WriteLine("猫:喵喵");
}
}
```
问题:
1. 每增加一个新动物,就要改 `MakeSpeak`
2. 判断会越来越多
3. 行为分散在外部函数里,子类自己的特点没有放回子类中
使用多态后:
```csharp
void MakeSpeak(Animal animal)
{
animal.Speak();
}
```
以后新增子类:
```csharp
class Bird : Animal
{
public override void Speak()
{
Console.WriteLine("鸟:叽叽");
}
}
```
调用方不用改:
```csharp
MakeSpeak(new Bird());
```
这就是多态最重要的价值:
==把变化交给子类,把稳定入口留给父类==
----
# virtual
作用:
`virtual` 用在父类成员上,表示这个成员允许被子类重写
```csharp
class Character
{
public virtual void Attack()
{
Console.WriteLine("角色攻击");
}
}
```
注意:
1. 没有写 `virtual` 的普通方法,子类不能用 `override` 重写
2. `virtual` 常用于方法、属性、索引器、事件
3. 构造函数不能是 `virtual`
4. 静态方法不能被 `override`
----
# override
作用:
`override` 用在子类成员上,表示重写父类的虚成员
```csharp
class Warrior : Character
{
public override void Attack()
{
Console.WriteLine("战士挥剑攻击");
}
}
```
重写要求:
1. 父类成员必须是 `virtual``abstract` 或已经是 `override`
2. 子类方法名、参数列表、返回类型要匹配
3. 访问修饰符通常要保持一致,例如父类是 `public`,子类也要是 `public`
4. `override` 之后,这个方法仍然可以继续被更下一级子类重写,除非加 `sealed`
----
# base
作用:
`base` 用在子类内部,表示访问父类成员
常见用途:
1. 调用父类构造函数
2. 在重写方法中,先执行父类原本逻辑,再补充子类逻辑
```csharp
class Character
{
public virtual void Attack()
{
Console.WriteLine("消耗体力");
}
}
class Warrior : Character
{
public override void Attack()
{
base.Attack();
Console.WriteLine("战士挥剑攻击");
}
}
Warrior w = new Warrior();
w.Attack();
```
输出:
```text
消耗体力
战士挥剑攻击
```
理解:
`base.Attack()` 不是创建父类对象,而是在子类方法里调用父类原本的 `Attack`
----
# 多态调用看实际对象
多态最关键的一句:
==编译时看变量类型,运行时看实际对象==
```csharp
Animal animal = new Dog();
animal.Speak();
```
这里:
1. 编译时,`animal` 的类型是 `Animal`
2. 所以只能直接调用 `Animal` 中声明过的成员
3. 运行时,`animal` 实际指向的是 `Dog`
4. 所以 `Speak()` 执行的是 `Dog` 重写后的版本
示例:
```csharp
class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("狗叫");
}
public void Bite()
{
Console.WriteLine("狗咬人");
}
}
Animal animal = new Dog();
animal.Speak(); // 可以,多态调用
// animal.Bite(); // 错误,Animal 类型里没有 Bite
```
如果确实要调用 `Dog` 独有方法,需要类型转换:
```csharp
Dog dog = animal as Dog;
if (dog != null)
{
dog.Bite();
}
```
但如果代码经常需要这样转,通常要回头检查抽象设计是不是不够好。
----
# override 和 new 的区别
`override` 是真正的多态重写。
```csharp
class Father
{
public virtual void Say()
{
Console.WriteLine("Father");
}
}
class Son : Father
{
public override void Say()
{
Console.WriteLine("Son");
}
}
Father f = new Son();
f.Say(); // Son
```
`new` 是隐藏父类成员,不是真正的多态。
```csharp
class Father
{
public void Say()
{
Console.WriteLine("Father");
}
}
class Son : Father
{
public new void Say()
{
Console.WriteLine("Son");
}
}
Father f = new Son();
f.Say(); // Father
```
区别:
1. `override`:看实际对象,所以会调用 `Son.Say()`
2. `new`:看变量类型,所以 `Father f` 调用的是 `Father.Say()`
3. 想要多态效果,用 `virtual + override`
----
# 常见错误
错误1:父类没写 `virtual`,子类却写 `override`
```csharp
class Animal
{
public void Speak()
{
}
}
class Dog : Animal
{
public override void Speak()
{
}
}
```
错误原因:
父类的 `Speak` 不是虚方法,不能被重写
----
错误2:以为子类独有成员也能通过父类变量直接调用
```csharp
Animal animal = new Dog();
// animal.Bite(); // 错误
```
原因:
父类变量只能看到父类声明过的成员
----
错误3:用 `new` 代替 `override`
```csharp
class Dog : Animal
{
public new void Speak()
{
Console.WriteLine("狗叫");
}
}
```
这样只是隐藏,不是真正的多态。
----
# 和里氏替换原则的关系
多态能成立的前提是:
==子类真的能当父类来用==
比如:
```csharp
void MakeSpeak(Animal animal)
{
animal.Speak();
}
```
这个函数相信:所有 `Animal` 都能 `Speak()`
如果某个子类重写后直接抛异常,或者完全破坏父类原本承诺的行为,就会影响[[里氏替换原则]]。
所以可以这样联系:
1. [[继承]]提供父子类型关系
2. 多态让同一个调用产生不同表现
3. [[里氏替换原则]]约束子类不要乱改父类承诺
----
# 注意
1. 多态主要依赖 `virtual + override`
2. 父类引用可以指向子类对象,这是多态的基础
3. 调用重写方法时,看的是对象的实际类型
4. 父类变量只能访问父类中声明过的成员
5. `base` 是在子类中访问父类成员,不是创建父类对象
6. `new` 是隐藏成员,不等于 `override`
7. 如果父类方法不希望再被重写,可以在子类重写时写 `sealed override`
----
# 一句话记忆
==父类定规则,子类改表现,调用时看实际对象。==
再压缩一点:
==virtual 允许改,override 真正改,base 调父类。==
----
# 面试/复习时怎么说
可以这样答:
“多态是指同一个父类引用指向不同子类对象时,调用同一个虚方法会执行不同子类的重写实现。
在 C# 中通常通过 `virtual``override` 实现,父类用 `virtual` 声明可重写方法,子类用 `override` 提供自己的实现。
如果子类重写时还想保留父类原本逻辑,可以用 `base` 调用父类方法。多态的好处是调用方可以面向父类编程,新增子类时尽量少改原有代码。”
----
引用:
1. Microsoft Learn: [Polymorphism](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/polymorphism)
2. Microsoft Learn: [virtual (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/virtual)
3. Microsoft Learn: [override (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/override)
4. Microsoft Learn: [The base keyword](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/base)
5. Microsoft Learn: [Knowing When to Use Override and New Keywords](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/knowing-when-to-use-override-and-new-keywords)
#多态
#核心
+296
View File
@@ -0,0 +1,296 @@
# 密封函数
概念:
密封函数指的是使用 `sealed` 修饰的==重写成员==
在 C# 里常见写法是 `sealed override`
准确一点说:
C# 里通常叫“密封方法”或“密封重写成员”
有些教程会说“密封函数”,初学阶段可以理解成:==不允许下一级子类继续重写的方法==
通常用法:
1. 父类允许子类重写某个方法
2. 某个子类重写后,认为这个实现已经定死
3. 允许这个子类继续被继承
4. 但不允许更下一级子类再改这个方法
----
# 语法
```csharp
class :
{
public sealed override void ()
{
}
}
```
注意:
`sealed` 修饰方法时,必须和 `override` 一起使用
----
# 基础案例
```csharp
class Animal
{
public virtual void Speak()
{
Console.WriteLine("动物叫");
}
}
class Dog : Animal
{
public sealed override void Speak()
{
Console.WriteLine("狗叫");
}
}
class Husky : Dog
{
// 这里不能再 override Speak()
}
```
理解:
1. `Animal.Speak()` 是虚方法,允许被重写
2. `Dog.Speak()` 重写了父类方法
3. `Dog.Speak()` 又被 `sealed` 密封
4. `Husky` 可以继承 `Dog`
5.`Husky` 不能继续重写 `Speak()`
----
# sealed class 和 sealed override 的区别
密封类:
```csharp
sealed class Dog : Animal
{
}
```
含义:
整个 `Dog` 类不能再被继承
密封函数:
```csharp
class Dog : Animal
{
public sealed override void Speak()
{
Console.WriteLine("狗叫");
}
}
```
含义:
`Dog` 类仍然可以被继承
但是 `Speak()` 不能再被下一级子类重写
对比:
1. `sealed class`:封住整个类
2. `sealed override`:只封住某个已经重写的成员
这个点可以和[[密封类]]一起记。
----
# 为什么密封函数必须是 override
错误写法:
```csharp
class Dog
{
public sealed void Speak()
{
}
}
```
原因:
普通方法本来就不能被 `override`
没有必要再用 `sealed` 封住
正确理解:
`sealed` 修饰方法时,是在说:
“这个方法原本处在可重写链条上,但从我这里开始不准继续重写”
所以它必须修饰已经重写的成员,也就是 `sealed override`
----
# 多级继承案例
```csharp
class A
{
public virtual void Test()
{
Console.WriteLine("A");
}
}
class B : A
{
public override void Test()
{
Console.WriteLine("B");
}
}
class C : B
{
public sealed override void Test()
{
Console.WriteLine("C");
}
}
class D : C
{
// public override void Test()
// {
// Console.WriteLine("D");
// }
}
```
调用:
```csharp
A a = new C();
a.Test(); // C
```
理解:
1. `A.Test()` 开启虚方法
2. `B.Test()` 继续重写
3. `C.Test()` 重写并密封
4. `D` 仍然可以继承 `C`
5.`D` 不能再重写 `Test()`
----
# 密封属性
`sealed override` 不只可以用于方法,也可以用于属性。
```csharp
class Character
{
public virtual int MaxHp
{
get { return 100; }
}
}
class Boss : Character
{
public sealed override int MaxHp
{
get { return 1000; }
}
}
```
后续子类不能再重写 `MaxHp`
----
# 和多态的关系
密封函数不会取消已有多态效果。
```csharp
Animal animal = new Dog();
animal.Speak(); // 狗叫
```
`Dog.Speak()` 仍然会通过多态被调用。
`sealed override` 只是限制:
后续子类不能继续改这个方法
也就是说:
1. 对上层父类来说,它仍然是重写后的多态方法
2. 对下层子类来说,它已经不能再被重写
----
# 常见错误
错误1:普通方法前写 `sealed`
```csharp
class A
{
public sealed void Test()
{
}
}
```
`sealed` 不能这样修饰普通方法。
----
错误2:只写 `sealed` 不写 `override`
```csharp
class B : A
{
public sealed void Test()
{
}
}
```
密封重写成员必须写成 `sealed override`
----
错误3:父类方法不是虚方法
```csharp
class A
{
public void Test()
{
}
}
class B : A
{
public sealed override void Test()
{
}
}
```
父类方法不是 `virtual` / `abstract` / `override`,子类不能 `override`,也就不能 `sealed override`
----
# 什么时候用密封函数
适合用的情况:
1. 某个重写逻辑非常关键,不希望后续子类改掉
2. 父类设计允许多态,但某一层子类希望终止这个方法的重写链
3. 框架或基础类库中需要保证关键行为稳定
初学阶段:
先知道这个语法和作用即可
自己写练习代码时用得不多
----
# 注意
1. 密封函数常见写法是 `sealed override`
2. 它只能用于已经重写的成员
3. 它阻止下一级子类继续重写这个成员
4. 它不会阻止当前类被继承
5. 如果想阻止整个类被继承,用 `sealed class`
6. 方法、属性、索引器、事件等重写成员都可以理解这个规则
----
# 一句话记忆
==sealed override 表示这个重写到我这里为止,后面的子类不能再改。==
----
# 面试/复习时怎么说
可以这样答:
“密封函数通常指 `sealed override` 修饰的重写方法。
它表示当前类重写了父类的虚方法,但不允许更下一级子类继续重写。
它和密封类不同,密封类是不允许整个类被继承,而密封函数只是禁止某个重写成员继续被重写。”
----
引用:
1. Microsoft Learn: [sealed (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/sealed)
2. Microsoft Learn: [override (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/override)
#密封函数
#密封方法
#核心
+199
View File
@@ -0,0 +1,199 @@
# 密封类
概念:
使用 `sealed` 关键字修饰的类,叫做==密封类==
作用是:==让这个类不能再被其他类继承==
简单理解:
这个类已经是“最终版本”了,别人只能使用它,不能在它的基础上继续派生子类
通常用法:
1. 不希望某个类被继续继承,避免子类乱改行为
2. 类的设计已经完整,不打算给别人扩展
3. 保护程序规范性和安全性
4. 表示某个类型在业务上就是终点,不应该再有更细的子类
语法:
```csharp
sealed class
{
}
```
案例:
```csharp
class Enemy
{
public string Name;
public virtual void Attack()
{
Console.WriteLine("敌人攻击");
}
}
sealed class Boss : Enemy
{
public override void Attack()
{
Console.WriteLine("Boss发动攻击");
}
}
```
上面代码中:
- `Boss` 可以继承 `Enemy`
- 但是 `Boss``sealed` 修饰后,别的类就不能再继承 `Boss`
错误示意:
```csharp
class FinalBoss : Boss
{
}
```
这样会报错,因为 `Boss` 是密封类,不能作为父类继续被继承
----
# 密封类还能不能正常使用
可以。
`sealed` 只是不允许别人继承它,==不影响创建对象和调用成员==。
```csharp
sealed class PlayerConfig
{
public int MaxHp = 100;
public void Print()
{
Console.WriteLine(MaxHp);
}
}
PlayerConfig config = new PlayerConfig();
config.Print();
```
----
# 密封类和继承
密封类不是说“自己不能继承别人”,而是说“别人不能继承自己”。
```csharp
class Animal
{
}
sealed class Dog : Animal
{
}
```
这个是可以的:
- `Dog` 继承了 `Animal`
- `Dog` 自己被密封
- 所以以后不能再写 `class Husky : Dog`
----
# 和普通类的区别
普通类:
```csharp
class A
{
}
class B : A
{
}
```
密封类:
```csharp
sealed class A
{
}
// class B : A // 错误,A 不能被继承
```
一句话:
普通类可以当父类,密封类不能当父类
----
# 密封类和抽象类不能同时用
错误写法:
```csharp
sealed abstract class A
{
}
```
原因:
- `abstract` 抽象类的意义是:必须被子类继承后补全实现
- `sealed` 密封类的意义是:不允许被继承
这两个要求互相矛盾,所以不能一起用
----
# 顺带理解:sealed 也能修饰重写成员
`sealed` 不只可以修饰类,还可以修饰==已经 override 的方法或属性==。
作用是:
允许这个类继续被继承,但是不允许子类继续重写某个成员
```csharp
class Animal
{
public virtual void Speak()
{
Console.WriteLine("动物叫");
}
}
class Dog : Animal
{
public sealed override void Speak()
{
Console.WriteLine("狗叫");
}
}
class Husky : Dog
{
// 这里不能再 override Speak
}
```
这里要区分:
1. `sealed class Dog`:整个 `Dog` 类不能被继承
2. `sealed override void Speak()``Dog` 类还能被继承,但 `Speak` 不能继续被重写
这个内容后面可以和[[多态vob|多态]]、[[密封函数]]一起理解。
----
# 注意
1. `sealed` 修饰类时,表示这个类==不能被继承==
2. 密封类自己仍然可以继承普通类,也可以实现接口
3. 密封类可以正常创建对象、调用方法、访问属性
4. 密封类不能和 `abstract` 同时使用
5. 如果只是想禁止某个方法继续被重写,可以用 `sealed override`,不一定要把整个类密封
6. 初学阶段不要滥用 `sealed`,只有确定“不应该再被继承”时再用
----
# 一句话记忆
==sealed class 表示这个类到此为止,后面不能再派生子类。==
----
引用:
1. Microsoft Learn: [sealed (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/sealed)
2. Microsoft Learn: [Inheritance - derive types to create more specialized behavior](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/inheritance)
#密封类
#核心
@@ -1,55 +1,69 @@
# 成员变量和访问修饰符
概念:
1. 声明在类中
2. 用来描述对象特征
3. 任意变量类型
4. 是否赋值根据需求决定
1. 成员变量也常叫==字段(field)==
2. 它声明在类或结构体中,用来描述对象状态
3. 字段类型可以是任意合法类型
4. 字段如果没手动赋值,会先使用该类型的默认值
通常用法:
- 保存对象的基础数据
- 配合访问修饰符控制谁能访问
- 再配合[[成员属性]]对外暴露更安全的访问方式
语法:
1. 声明:
```Csharp
enum E_SexType
{
male,
female
}
class Human
{
string name;
int age;
E_Sex sex;
Human[] friend; //==可以类里声明自己类的同类型 但是不能实例化!==
}
```csharp
class Human
{
public string name;
private int age;
protected bool isAlive;
internal Human[] friends;
}
```
2. 访问修饰符
public——公共的 类内部和外部都可以进行访问
private——私有的 只有内部才能访问 ==子类不行==
protected——保护的 只有自己内部和子类才可以访问
3. 成员默认值
可以看[[值类型和引用类型]]
懒得看可以cw(default(变量类型))
访问修饰符:
1. `public`
类内部和外部都能访问
2. `private`
只有当前类型内部能访问
3. `protected`
当前类型和子类能访问
4. `internal`
同一个程序集内能访问
案例:
```Csharp
class Human
{
string name;
int age;
E_Sex sex;
Human[] friend; //==可以类里声明自己类的同类型 但是不能实例化!==
}
Human h1 = new Human();
h1.age = 18;
cw(h1.age);
```csharp
class Human
{
public string name;
private int age;
public void SetAge(int value)
{
age = value;
}
public void PrintInfo()
{
Console.WriteLine($"{name} - {age}");
}
}
Human h1 = new Human();
h1.name = "小明";
// h1.age = 18; // 报错,外部不能直接访问 private 字段
h1.SetAge(18);
h1.PrintInfo();
```
注意:
1. ==类中的字段如果不写访问修饰符,默认是 `private`==
2. 字段可以声明成当前类型本身,比如 `Human[] friends`
3. 默认值示例:
- `int``0`
- `bool``false`
- 引用类型是 `null`
4. 实际开发中,很多字段会写成 `private`,再通过[[成员属性]]控制访问
#封装
#核心
+61 -63
View File
@@ -1,90 +1,88 @@
# 成员属性
概念:
==和成员变量不完全是一个概念!!!!==
1. 用于保护成员变量
2. 为成员属性的获取和复制添加逻辑处理
3. 解决3P局限性
自动属性
- 属性(Property)不是字段本身,而是==对字段的访问入口==
- 它看起来像变量,用起来也像变量,但本质上是特殊方法
- 属性常用于:读取、赋值、校验、限制访问
通常用法:
配合private加密或规范成员属性的修改获取等
让其==只能获取不能修改== 或 ==只能修改不能获取==
1. 保护字段,不让外部随便乱改
2. 在赋值或读取时加入逻辑
3. 做“只能读”“只能类内改”“初始化后不可改”等控制
语法:
属性声明:
```Csharp
访
{
get{}
set{}
}
```
案例:
```Csharp
class Person()
完整语法:
```csharp
class Person
{
private int age;
private string name;
private int money;
private bool sex;
public string Name [[|]]
public string Name
{
get====
get
{
//这里写处理获取内容的逻辑
return name;
}
set
{
//这里写处理修改/添加内容的逻辑,不写就没有
==//关键字value 用于表示外部传入的值 只在set中有==
name = value;
}
}
}
```
案例:
```csharp
class Person
{
private int age;
public int Age
{
get
{
return age;
}
set
{
if (value < 0)
{
age = 0;
return;
}
age = value;
}
}
}
Person p = new Person();
~~p.name = xxx~~ private所以没法用.name
p.Name = xxx
p.Age = -5;
Console.WriteLine(p.Age); // 0
```
↑写了一个public的属性大家都可以访问,用该属性Name进行修改 return直接返回原始的nameset时使用.Name将xxx作为传入参数放到set的value关键字中
说明:
- `get` 负责取值
- `set` 负责赋值
- `set` 里的 `value` 表示外部传入的新值
案例2
```Csharp
public int Money()
{
get
{
return money;
}
set
{
if(value<0) value = 0;
cw();
money = value;
}
}
自动属性
```csharp
public string Name { get; set; }
public float Height { get; private set; }
public string Id { get; init; }
```
↑最简单的防止money为负数的逻辑
补充:
- 自动属性会由编译器生成隐藏的 backing field
- `private set` 表示外部能读、类内部能改
- `init` 表示只能在创建对象时赋值
注意:
==get必须有返回值==
成员属性不加访问修饰符,会使用成员变量声明时的访问权限
==get和set也可以加访问修饰符,但不能大于成员属性前访问修饰符的权限==
可以只有一个set或只有一个get(只能得或只能改),但是通常没人这么干
自动属性
相当于声明了一个成员变量,但是好处是可以添加访问修饰符
案例:可以得,但是不能改得一个成员↓
```Csharp
public float Height
{
get;
private set;
}
```
1. ==`get` 中如果要返回值,就必须 `return`==
2. 属性本身的访问级别看属性前面的修饰符
3. `get` / `set` 可以单独再加更严格的访问修饰符,但不能比属性本身更开放
4. 只有 `get` 没有 `set` 时是只读属性
5. 只有 `set` 没有 `get` 时是只写属性,但实际开发里很少这样写
6. 属性不是普通变量,所以通常不能当 `ref` / `out` 参数传入
#封装
#核心
@@ -0,0 +1,540 @@
概念:
使用 `abstract` 关键字修饰的类,叫做==抽象类==
使用 `abstract` 关键字修饰的方法,叫做==抽象方法==
简单理解:
抽象类像一份“不完整的父类模板”
它可以规定子类必须有什么能力,但具体怎么做,交给子类自己实现
补充:
有些教程会把“抽象方法”叫作“抽象函数”,初学阶段可以先理解成同一个意思
通常用法:
1. 多个类有共同身份,但父类本身不应该被直接创建对象
2. 父类能提供一部分公共代码
3. 父类还想强制子类必须实现某些方法
4. 配合[[继承]]和[[多态vob|多态]],让调用方通过父类统一调用子类行为
----
# 语法
抽象类:
```csharp
abstract class
{
}
```
抽象方法:
```csharp
abstract class
{
public abstract void ();
}
```
子类实现抽象方法:
```csharp
class :
{
public override void ()
{
}
}
```
注意:
抽象方法没有方法体,结尾直接写分号 `;`
----
# 基础案例
```csharp
abstract class Animal
{
public string Name;
public Animal(string name)
{
Name = name;
}
public void Eat()
{
Console.WriteLine(Name + "正在吃东西");
}
public abstract void Speak();
}
class Dog : Animal
{
public Dog(string name) : base(name)
{
}
public override void Speak()
{
Console.WriteLine(Name + ":汪汪");
}
}
class Cat : Animal
{
public Cat(string name) : base(name)
{
}
public override void Speak()
{
Console.WriteLine(Name + ":喵喵");
}
}
Animal a1 = new Dog("旺财");
Animal a2 = new Cat("小花");
a1.Eat();
a1.Speak();
a2.Eat();
a2.Speak();
```
理解:
1. `Animal` 是抽象类,不能直接 `new Animal()`
2. `Eat()` 是普通方法,子类可以直接继承使用
3. `Speak()` 是抽象方法,父类只规定“动物要会叫”
4. `Dog``Cat` 必须用 `override` 实现自己的 `Speak()`
5. `Animal a1 = new Dog()` 可以配合多态使用
----
# 抽象类不能直接实例化
错误写法:
```csharp
abstract class Animal
{
public abstract void Speak();
}
Animal animal = new Animal();
```
原因:
抽象类是不完整的类型,里面可能有没实现的抽象成员
所以不能直接创建对象
正确用法:
```csharp
Animal animal = new Dog("旺财");
animal.Speak();
```
----
# 抽象方法必须在子类中实现
如果子类不是抽象类,就必须实现父类中所有抽象方法。
```csharp
abstract class Shape
{
public abstract double GetArea();
}
class Circle : Shape
{
public double Radius;
public Circle(double radius)
{
Radius = radius;
}
public override double GetArea()
{
return 3.14 * Radius * Radius;
}
}
```
错误写法:
```csharp
class Circle : Shape
{
}
```
原因:
`Circle` 是普通类,但没有实现 `Shape` 要求的 `GetArea()`
----
# 子类也可以继续抽象
如果子类暂时不想实现所有抽象方法,那么这个子类也必须声明成 `abstract`
```csharp
abstract class Shape
{
public abstract double GetArea();
public abstract double GetLength();
}
abstract class RoundShape : Shape
{
public override double GetLength()
{
return 0;
}
// GetArea 暂时不实现,所以 RoundShape 也必须是抽象类
}
```
理解:
普通类必须把抽象成员补完整
抽象类可以继续把没补完的内容留给下一级子类
----
# 抽象类里可以有什么
抽象类不只是能写抽象方法,也可以写普通成员。
```csharp
abstract class Character
{
public string Name;
public int Hp;
public Character(string name, int hp)
{
Name = name;
Hp = hp;
}
public void TakeDamage(int damage)
{
Hp -= damage;
Console.WriteLine(Name + "受到了" + damage + "点伤害");
}
public virtual void Move()
{
Console.WriteLine(Name + "正在移动");
}
public abstract void Attack();
}
```
抽象类中可以有:
1. 成员变量
2. 成员属性
3. 构造函数
4. 普通方法
5. 虚方法
6. 抽象方法
7. 抽象属性、抽象索引器、抽象事件
一句话:
抽象类可以有“已经写好的公共功能”,也可以有“要求子类必须补上的功能”
----
# 抽象类可以有构造函数
抽象类不能直接 `new`,但它仍然可以有构造函数。
原因:
创建子类对象时,会先构造父类那一部分
```csharp
abstract class Enemy
{
public string Name;
public Enemy(string name)
{
Name = name;
Console.WriteLine("Enemy构造");
}
public abstract void Attack();
}
class Boss : Enemy
{
public Boss(string name) : base(name)
{
Console.WriteLine("Boss构造");
}
public override void Attack()
{
Console.WriteLine(Name + "发动攻击");
}
}
Boss boss = new Boss("魔王");
```
执行顺序还是:
1. 先执行父类 `Enemy` 构造函数
2. 再执行子类 `Boss` 构造函数
----
# abstract、virtual、override 的关系
`abstract` 方法:
```csharp
public abstract void Attack();
```
特点:
1. 没有方法体
2. 子类必须实现
3. 本身就表示允许被重写,所以子类用 `override`
`virtual` 方法:
```csharp
public virtual void Move()
{
Console.WriteLine("默认移动");
}
```
特点:
1. 有方法体
2. 子类可以选择重写,也可以不重写
3. 子类重写时也用 `override`
对比:
1. `abstract`:父类不给默认实现,子类必须改
2. `virtual`:父类给默认实现,子类可以改
3. `override`:子类真正改写父类的抽象方法或虚方法
----
# 抽象方法和普通方法的区别
普通方法:
```csharp
public void Eat()
{
Console.WriteLine("吃东西");
}
```
抽象方法:
```csharp
public abstract void Speak();
```
区别:
1. 普通方法有方法体,抽象方法没有方法体
2. 普通方法可以直接继承使用,抽象方法必须由子类实现
3. 普通方法可以写在普通类或抽象类里,抽象方法只能写在抽象类里
4. 抽象方法天然就是给多态准备的
----
# 抽象属性
抽象的不一定只有方法,也可以是属性。
```csharp
abstract class Weapon
{
public abstract int Damage { get; }
}
class Sword : Weapon
{
public override int Damage
{
get
{
return 10;
}
}
}
```
简写:
```csharp
class Bow : Weapon
{
public override int Damage => 8;
}
```
理解:
父类规定“武器必须有伤害值”
不同武器自己决定伤害值是多少
----
# 抽象类和密封类不能同时用
错误写法:
```csharp
sealed abstract class Animal
{
}
```
原因:
1. `abstract` 表示这个类必须被继承后才能补完整
2. `sealed` 表示这个类不能被继承
3. 两个要求互相矛盾
这个点可以和[[密封类]]一起记。
----
# 常见错误
错误1:抽象方法写了方法体
```csharp
abstract class Animal
{
public abstract void Speak()
{
Console.WriteLine("动物叫");
}
}
```
抽象方法不能有方法体。
----
错误2:普通类里写抽象方法
```csharp
class Animal
{
public abstract void Speak();
}
```
抽象方法只能写在抽象类中。
----
错误3:子类没实现抽象方法
```csharp
abstract class Animal
{
public abstract void Speak();
}
class Dog : Animal
{
}
```
`Dog` 如果是普通类,就必须 `override Speak()`
----
错误4:抽象方法上再写 `virtual`
```csharp
abstract class Animal
{
public abstract virtual void Speak();
}
```
抽象方法本身就表示必须被重写,不需要再写 `virtual`
----
错误5:直接 new 抽象类
```csharp
Animal animal = new Animal();
```
抽象类不能直接实例化。
----
# 什么时候用抽象类
适合用抽象类的情况:
1. 多个子类确实属于同一种父类
2. 父类有公共字段、属性、构造函数或普通方法可以复用
3. 父类还需要强制子类实现某些行为
4. 你希望调用方通过父类统一调用子类对象
例如:
- `Animal`:动物都能吃,但不同动物叫声不同
- `Shape`:图形都有面积,但不同图形面积算法不同
- `Character`:角色都有血量,但不同角色攻击方式不同
不适合用抽象类的情况:
1. 只是想规定一种能力,例如“能飞”“能攻击”
2. 多个类之间没有明确的父子关系
3. 一个类需要同时拥有多种能力
这些情况后面通常更适合用[[接口]]。
----
# 和多态的关系
抽象类经常和多态一起使用。
```csharp
abstract class Skill
{
public abstract void Release();
}
class FireSkill : Skill
{
public override void Release()
{
Console.WriteLine("释放火球");
}
}
class IceSkill : Skill
{
public override void Release()
{
Console.WriteLine("释放冰锥");
}
}
void UseSkill(Skill skill)
{
skill.Release();
}
UseSkill(new FireSkill());
UseSkill(new IceSkill());
```
理解:
1. `Skill` 规定所有技能都必须能 `Release()`
2. `FireSkill``IceSkill` 各自实现释放逻辑
3. `UseSkill` 只依赖抽象父类,不依赖具体技能类
这就是:
==抽象类定规则,多态负责运行时执行不同实现==
----
# 注意
1. 抽象类不能直接创建对象
2. 抽象方法没有方法体,结尾用分号
3. 有抽象方法的类必须是抽象类
4. 普通子类继承抽象类后,必须实现所有抽象成员
5. 抽象类可以有普通方法、字段、属性和构造函数
6. 抽象方法不能写 `private`,因为子类需要能重写它
7. 抽象方法不能同时写 `virtual`,因为它本身就是虚的
8. 抽象类不能和 `sealed` 同时使用
----
# 一句话记忆
==抽象类不能直接 new,抽象方法只定规则不写实现,子类必须 override。==
再压缩一点:
==父类抽象定规矩,子类重写补细节。==
----
# 面试/复习时怎么说
可以这样答:
“抽象类是用 `abstract` 修饰的类,不能直接实例化,主要用来作为父类。
它可以包含普通成员,也可以包含抽象成员。抽象方法没有方法体,只声明方法签名,普通子类继承后必须用 `override` 实现。
抽象类适合用来提取多个子类的共同代码,同时强制子类实现不同的行为,常和继承、多态一起使用。”
----
引用:
1. Microsoft Learn: [abstract (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/abstract)
2. Microsoft Learn: [Abstract and Sealed Classes and Class Members](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/abstract-and-sealed-classes-and-class-members)
3. Microsoft Learn: [Inheritance - derive types to create more specialized behavior](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/inheritance)
#抽象类
#抽象方法
#核心
@@ -0,0 +1,350 @@
# 抽象类和接口的区别
概念:
抽象类和接口都可以用来做抽象,都能配合多态使用
但它们表达的语义不一样
简单理解:
抽象类更像“是什么”
接口更像“能做什么”
例如:
1. `Animal` 抽象类:表示狗、猫、鸟都是动物
2. `IFlyable` 接口:表示某个对象具备飞行能力
----
# 先看抽象类
抽象类:
```csharp
abstract class Animal
{
public string Name;
public Animal(string name)
{
Name = name;
}
public void Eat()
{
Console.WriteLine(Name + "吃东西");
}
public abstract void Speak();
}
```
子类:
```csharp
class Dog : Animal
{
public Dog(string name) : base(name)
{
}
public override void Speak()
{
Console.WriteLine(Name + "汪汪");
}
}
```
抽象类适合:
1. 有明确父子关系
2. 要复用公共字段、构造函数、普通方法
3. 还要强制子类实现某些行为
一句话:
抽象类适合表达“这类东西的共同基础”
----
# 再看接口
接口:
```csharp
interface IFlyable
{
void Fly();
}
```
实现类:
```csharp
class Bird : IFlyable
{
public void Fly()
{
Console.WriteLine("鸟飞");
}
}
class Plane : IFlyable
{
public void Fly()
{
Console.WriteLine("飞机飞");
}
}
```
接口适合:
1. 表达一种能力
2. 多个无关类型都能实现同一种行为
3. 一个类需要同时拥有多种能力
4. 调用方只关心能力,不关心具体身份
一句话:
接口适合表达“谁能做这件事”
----
# 核心区别
| 对比点 | 抽象类 | 接口 |
|---|---|---|
| 关键词 | `abstract class` | `interface` |
| 主要表达 | 是什么 | 能做什么 |
| 继承数量 | 一个类只能继承一个类 | 一个类可以实现多个接口 |
| 成员实现 | 可以有普通实现,也可以有抽象成员 | 主要规定契约,要求实现类提供成员 |
| 字段 | 可以有字段 | 通常不放实例字段 |
| 构造函数 | 可以有构造函数 | 不能像类一样写实例构造函数 |
| 适合场景 | 提取共同父类和公共代码 | 表达能力、规则、插件式扩展 |
| 多态使用 | 可以用抽象类引用指向子类 | 可以用接口引用指向实现类 |
----
# 单继承和多实现
C# 中:
一个类只能继承一个父类
但可以实现多个接口
```csharp
abstract class Character
{
public string Name;
}
interface IMovable
{
void Move();
}
interface IAttackable
{
void Attack();
}
class Player : Character, IMovable, IAttackable
{
public void Move()
{
Console.WriteLine("移动");
}
public void Attack()
{
Console.WriteLine("攻击");
}
}
```
理解:
1. `Player` 是一种 `Character`
2. `Player` 同时具备移动能力和攻击能力
3. 父类写一个,接口可以写多个
----
# 什么时候用抽象类
适合用抽象类:
```csharp
abstract class Enemy
{
public int Hp;
public void TakeDamage(int damage)
{
Hp -= damage;
}
public abstract void Attack();
}
```
因为:
1. 小怪、Boss、远程敌人都可以算敌人
2. 它们都有血量、受伤逻辑
3. 它们攻击方式不同
这种情况抽象类很合适:
有共同身份,也有公共代码
----
# 什么时候用接口
适合用接口:
```csharp
interface IDamageable
{
void TakeDamage(int damage);
}
```
实现者可能是:
```csharp
class Player : IDamageable
{
public void TakeDamage(int damage)
{
}
}
class Enemy : IDamageable
{
public void TakeDamage(int damage)
{
}
}
class Box : IDamageable
{
public void TakeDamage(int damage)
{
}
}
```
玩家、敌人、箱子不是同一种父类关系,但它们都可以“受伤/受击”。
这种情况接口更合适:
没有共同身份,但有共同能力
----
# 可以一起用
抽象类和接口不是二选一,经常会一起使用。
```csharp
abstract class Character
{
public string Name;
public Character(string name)
{
Name = name;
}
public abstract void Update();
}
interface IAttackable
{
void Attack();
}
class Warrior : Character, IAttackable
{
public Warrior(string name) : base(name)
{
}
public override void Update()
{
Console.WriteLine(Name + "更新状态");
}
public void Attack()
{
Console.WriteLine(Name + "挥剑攻击");
}
}
```
理解:
1. `Warrior` 是一种 `Character`
2. `Warrior` 具备 `IAttackable` 能力
3. 抽象类负责共同身份和基础逻辑
4. 接口负责额外能力
----
# 和多态的关系
抽象类多态:
```csharp
Animal animal = new Dog();
animal.Speak();
```
接口多态:
```csharp
IFlyable flyable = new Bird();
flyable.Fly();
```
它们都能做到:
用统一类型接收不同具体对象
运行时执行不同实现
区别在于:
1. 抽象类变量强调“它属于某个父类体系”
2. 接口变量强调“它拥有某个能力”
----
# 常见判断方法
问自己几个问题:
1. 这些类是不是同一种东西?
- 是:优先考虑抽象类
- 不是:优先考虑接口
2. 是否需要复用字段、构造函数、普通方法?
- 是:抽象类更合适
3. 一个类是否需要同时拥有多种能力?
- 是:接口更合适
4. 调用方真正关心的是身份还是能力?
- 关心身份:抽象类
- 关心能力:接口
----
# 常见误区
误区1:接口就是更高级的抽象类
不是。
接口和抽象类不是上下级关系,而是用途不同
----
误区2:有公共方法就一定用抽象类
不一定。
如果这些类型没有真正的父子关系,硬抽象成父类可能违反[[里氏替换原则]]
----
误区3:接口只能有方法
接口也可以有属性、索引器、事件等成员。
初学阶段先重点掌握方法和属性即可。
----
# 注意
1. 抽象类和接口都不能直接创建对象
2. 抽象类用 `abstract class`,接口用 `interface`
3. 抽象类适合表达共同身份和公共代码
4. 接口适合表达能力和契约
5. 类只能继承一个父类,但可以实现多个接口
6. 抽象类和接口都可以配合多态
7. 设计时先看语义,不要只看语法
----
# 一句话记忆
==抽象类管“是谁的一种”,接口管“能做什么”。==
再压缩一点:
==同类用抽象类,能力用接口。==
----
# 面试/复习时怎么说
可以这样答:
“抽象类和接口都能表达抽象,也都可以配合多态使用。
抽象类更强调父子关系和公共代码复用,一个类只能继承一个抽象类;接口更强调能力或契约,一个类可以实现多个接口。
如果多个类本质上是同一种东西,并且有公共字段、构造函数或普通方法,可以用抽象类;如果只是多个类型都有同一种能力,通常用接口更合适。”
----
引用:
1. Microsoft Learn: [abstract (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/abstract)
2. Microsoft Learn: [interface (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/interface)
3. Microsoft Learn: [Interfaces](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/interfaces)
#抽象类
#接口
#核心
+476
View File
@@ -0,0 +1,476 @@
# 接口
概念:
接口使用 `interface` 关键字定义
它主要用来规定一组==能力 / 规则 / 契约==
简单理解:
接口不关心“你是谁”
只关心“你能不能做这件事”
例如:
`IFlyable` 表示“会飞”
不管你是鸟、飞机还是飞行道具,只要实现了这个接口,就代表你拥有飞行能力
通常用法:
1. 规定某些类必须实现哪些成员
2. 表达一种能力,例如会飞、会攻击、可保存、可移动
3. 让不同类型用同一套调用方式
4. 弥补 C# 类只能单继承的问题,因为一个类可以实现多个接口
5. 配合[[多态vob|多态]],让代码面向能力编程
----
# 语法
定义接口:
```csharp
interface
{
void ();
}
```
实现接口:
```csharp
class :
{
public void ()
{
}
}
```
命名习惯:
接口名通常以大写字母 `I` 开头
例如:`IFlyable``IAttackable``ISaveable`
----
# 基础案例
```csharp
interface IFlyable
{
void Fly();
}
class Bird : IFlyable
{
public void Fly()
{
Console.WriteLine("鸟在飞");
}
}
class Plane : IFlyable
{
public void Fly()
{
Console.WriteLine("飞机在飞");
}
}
void LetItFly(IFlyable flyable)
{
flyable.Fly();
}
LetItFly(new Bird());
LetItFly(new Plane());
```
理解:
1. `IFlyable` 规定实现者必须有 `Fly()` 方法
2. `Bird``Plane` 类型不同,但都实现了 `IFlyable`
3. `LetItFly` 不关心传进来的是鸟还是飞机,只关心它会不会飞
这就是接口常见用法:
==面向能力编程,而不是面向具体类型编程==
----
# 接口中的成员
常见接口成员:
```csharp
interface IMovable
{
void Move();
int Speed { get; set; }
}
```
实现:
```csharp
class Player : IMovable
{
public int Speed { get; set; }
public void Move()
{
Console.WriteLine("玩家移动");
}
}
```
接口中常见成员包括:
1. 方法
2. 属性
3. 索引器
4. 事件
初学阶段重点先掌握:
接口方法 + 接口属性
----
# 接口成员默认是 public
接口里这样写:
```csharp
interface IAttackable
{
void Attack();
}
```
实现时必须写 `public`
```csharp
class Enemy : IAttackable
{
public void Attack()
{
Console.WriteLine("敌人攻击");
}
}
```
错误写法:
```csharp
class Enemy : IAttackable
{
void Attack()
{
}
}
```
原因:
接口成员代表对外公开的契约
类实现接口时,成员访问级别不能比接口更低
----
# 一个类可以实现多个接口
C# 的类只能继承一个父类,但可以实现多个接口。
```csharp
interface IMovable
{
void Move();
}
interface IAttackable
{
void Attack();
}
class Player : IMovable, IAttackable
{
public void Move()
{
Console.WriteLine("玩家移动");
}
public void Attack()
{
Console.WriteLine("玩家攻击");
}
}
```
理解:
`Player` 同时拥有“可移动”和“可攻击”两种能力
----
# 继承类和实现接口同时存在
写法:
```csharp
class : , 1, 2
{
}
```
注意顺序:
==先写父类,再写接口==
案例:
```csharp
class Character
{
public string Name;
}
interface IAttackable
{
void Attack();
}
class Warrior : Character, IAttackable
{
public void Attack()
{
Console.WriteLine(Name + "挥剑攻击");
}
}
```
错误示意:
```csharp
// class Warrior : IAttackable, Character
// {
// }
```
如果既要继承类,又要实现接口,父类必须写在最前面。
----
# 接口也可以继承接口
接口之间可以继承,而且可以继承多个接口。
```csharp
interface IMovable
{
void Move();
}
interface IAttackable
{
void Attack();
}
interface IPlayerAction : IMovable, IAttackable
{
void UseItem();
}
```
实现 `IPlayerAction` 的类,必须实现:
1. `Move()`
2. `Attack()`
3. `UseItem()`
```csharp
class Player : IPlayerAction
{
public void Move()
{
}
public void Attack()
{
}
public void UseItem()
{
}
}
```
----
# 接口引用和多态
接口也可以当变量类型使用。
```csharp
IAttackable attacker = new Player();
attacker.Attack();
```
也可以作为方法参数:
```csharp
void DoAttack(IAttackable attacker)
{
attacker.Attack();
}
```
好处:
调用方只依赖 `IAttackable`
任何实现了这个接口的对象都能传进来
```csharp
DoAttack(new Player());
DoAttack(new Enemy());
```
这也是多态的一种表现。
----
# 接口和里氏替换原则
接口常用来拆分能力,避免不合理继承。
比如:
```csharp
abstract class Bird
{
public string Name;
}
interface IFlyable
{
void Fly();
}
class Sparrow : Bird, IFlyable
{
public void Fly()
{
Console.WriteLine("麻雀飞");
}
}
class Penguin : Bird
{
public void Swim()
{
Console.WriteLine("企鹅游泳");
}
}
```
这样:
1. `Bird` 只表达“鸟”
2. `IFlyable` 只表达“会飞”
3. 不会飞的企鹅不用硬实现 `Fly()`
这个例子可以和[[里氏替换原则]]一起理解。
----
# 显式实现接口
当两个接口有同名方法,或者你不希望方法直接暴露在类对象上,可以显式实现接口。
```csharp
interface IWalkable
{
void Move();
}
interface IFlyable
{
void Move();
}
class Bird : IWalkable, IFlyable
{
void IWalkable.Move()
{
Console.WriteLine("鸟在走");
}
void IFlyable.Move()
{
Console.WriteLine("鸟在飞");
}
}
Bird bird = new Bird();
IWalkable walkable = bird;
walkable.Move();
IFlyable flyable = bird;
flyable.Move();
```
注意:
显式实现接口时,不写 `public`
调用时通常要先把对象当成对应接口类型
初学阶段先知道有这个写法即可,最常用的还是普通实现接口。
----
# 常见错误
错误1:实现接口时忘记 `public`
```csharp
class Player : IAttackable
{
void Attack()
{
}
}
```
接口成员默认是公开契约,实现时通常要写 `public`
----
错误2:没有实现接口所有成员
```csharp
interface IMovable
{
void Move();
int Speed { get; set; }
}
class Player : IMovable
{
public void Move()
{
}
}
```
`Speed` 没实现,所以会报错。
----
错误3:把接口当成类直接创建对象
```csharp
IFlyable flyable = new IFlyable();
```
接口只是规则,不能直接 `new`
正确写法:
```csharp
IFlyable flyable = new Bird();
```
----
# 什么时候用接口
适合用接口的情况:
1. 你要表达的是“能力”,不是“父子身份”
2. 多个无关类型都有同一种行为
3. 一个类型需要拥有多种能力
4. 想让代码依赖规则,而不是依赖具体类
例如:
- `IFlyable`:会飞
- `IAttackable`:能攻击
- `ISaveable`:可保存
- `IDamageable`:可受伤
----
# 注意
1. 接口用 `interface` 定义
2. 接口名通常以 `I` 开头
3. 接口不能直接创建对象
4. 类和结构体都可以实现接口
5. 一个类可以实现多个接口
6. 接口成员实现时通常要写 `public`
7. 接口更适合表达“能做什么”,抽象类更适合表达“是什么”
8. 接口常和多态、里氏替换原则一起理解
----
# 一句话记忆
==接口是能力契约,谁实现它,谁就必须按规则提供这些能力。==
再压缩一点:
==接口管能不能做,不管你是谁。==
----
# 面试/复习时怎么说
可以这样答:
“接口是用 `interface` 定义的一组契约,主要用来规定实现类必须提供哪些成员。
类可以实现多个接口,所以接口常用来表达能力,比如可移动、可攻击、可保存。
调用方可以面向接口编程,只依赖能力规则,不依赖具体类,这样更利于扩展和多态。”
----
引用:
1. Microsoft Learn: [interface (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/interface)
2. Microsoft Learn: [Interfaces](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/interfaces)
#接口
#核心
@@ -142,4 +142,8 @@ step4:D放入
![[Pasted image 20260412210946.png]]
step5:如果0代1代后续又满了,那么这套算法会同时做用在两代内存中,以此类推
类似多级缓存cache,L1最快但常换L3最慢
#基础
<<<<<<< ours
#基础
=======
#基础
>>>>>>> theirs
@@ -1,245 +1,180 @@
# 构造函数
# 构造函数
概念:
实例化时,用于初始化的函数
不写默认有一个无参构造函数
==可以重载==
- 构造函数用于在对象创建时做初始化
- 构造函数名必须和类名相同
- 构造函数没有返回值类型
- ==可以重载==
通常用法:
自定义初始化规则
- 给字段或属性设置初始值
- 保证对象一创建出来就处于可用状态
语法:
访问修饰符 类名(){}
```csharp
访 ()
{
}
```
案例:
```Csharp
class Person()
```csharp
class Person
{
public string name;
public int age;
public string Name;
public int Age;
public Person()
{
name = "默认值";
age = ;
Name = "默认值";
Age = 0;
}
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
```
this关键字
this指当前类声明的成员变量
```Csharp
class Person()
`this` 调用其他构造函数
```csharp
class Person
{
public string Name;
public int Age;
public Person() : this("默认值", 0)
{
public string name;
public int age;
public Person()
{
name = "默认值";
age = ;
}
public Person(int age,string name) //重构
{
this.name = name;
//后面的name是传递的参数,this.name是最上方public int出来的
this.age = name;
}
}
public Person(string name, int age)
{
Name = name;
Age = age;
}
}
```
特殊写法:
通过this重用构造函数代码,即先调用无参构造函数,然后在调用这个特殊写法的构造函数
语法:访问修饰符 构造函数名(参数列表):this(【可选】参数1,参数2.....)
例子:
```Csharp
public Person(int age, string name)thisage
{
balabala
}
```
过程:先调用无参Person构造函数,然后寻找只有构造单个参数age的构造函数进行初始化
注意:
==函数名和类名必须相同==
==没有特殊用途,一般都是public==
==如果写了有参构造函数且自己没有保留无参构造函数,那么类在实例化时只能使用有参构造函数且无法使用无参构造函数==
1. ==如果类里完全没写任何实例构造函数,编译器才会自动补一个无参构造函数==
2. ==只要你自己写了任意实例构造函数,默认无参构造函数就不会再自动生成==
3. 构造函数通常写 `public`,但也可以根据设计写成 `private` / `protected`
4. 子类构造函数可以通过 `base(...)` 调用父类构造函数
----
# 析构函数
概念:
当引用类型堆内存被回收时,会调用该函数
对于需要手动管理内存的语言(C++),需要在析构函数中做一些内存回收处理
C#有自动垃圾回收机制GC
Unity中基本不用管
通常用法:
- C# 里的析构函数更准确地说是==终结器(finalizer==
- 当对象即将被 GC 回收时,运行时可能会调用它
- 它主要用于“兜底清理非托管资源”
语法:
```Csharp
~
```csharp
class Person
{
}
```
案例:
```Csharp
~Person
{
~Person()
{
// 清理代码
}
}
```
注意:
1. 析构函数只能写在==类==里,结构体不能写
2. 不能手动直接调用析构函数
3. 析构函数执行时机==不确定==
4. 大多数业务代码里==不推荐为了练习而滥写析构函数==
5. 现代 .NET 中,更常见的做法是优先使用 `IDisposable``using``SafeHandle`
----
# 垃圾回收
垃圾回收(GC
通过遍历堆Heap上动态分配的所有对象
通过识别他们是否被引用来确定哪些对象时垃圾,哪些对象仍然需要引用
垃圾:没有变量或对象引用的内容
如果是垃圾就要被回收释放
垃圾回收(GC
- GC 是 .NET 的自动内存管理机制
- 它负责识别哪些托管对象还“活着”,哪些已经不可达,可以回收
一般都是==被动触发(会造成卡顿)==,所以游戏一般需要手动执行垃圾回收
语法:GC.Collect();
==GC会造成卡顿,所以在加载Loading时一般会调用==
基础理解:
1. 只要对象还能从“根”访问到,就一般不会被回收
2. 访问不到的对象,才有机会被 GC 回收
3. GC 主要管理的是==托管堆上的对象==
垃圾回收有多种算法
引用计数
标记清楚
标记整理
复制集合
分代概念:
- .NET GC 常分为 0、1、2 代
- 新对象一般先进入 0 代
- 活得越久,越可能被提升到更高代
- 大对象通常会进入 LOH(大对象堆),阈值常见记法是 >= `85,000` 字节
注意:
GC只负责Heap内存的垃圾祸首
引用类型都是在Heap中的,所以他需要GC来进行管理
1. ==`GC.Collect()` 一般不建议手动乱调==
2. 手动触发 GC 可能带来性能抖动
3. “值类型一定在栈上、引用类型一定在堆上”不是绝对规则
4. GC 解决的是==托管内存==问题,不等于所有资源都会自动及时释放
栈Stack上的内存时系统自动管理的
所以不需要GC
----
# GC 和 IDisposable
首先区分资源类型:
C#中垃圾回收机制的大概原理
他会把内存分为012代内存
代的概念是垃圾回收中的一种算法需要
新分配的对象都会被配置在第0代内存中
每次分配都可能会进行垃圾回收(一般来说0代内存满了就会触发)
在第一次内存回收过程开始时,垃圾回收器会认为堆中全是垃圾,会进行一下两部
1.标记对象,从根(静态参数、方法参数)开始检查引用对象,标记后为可达对象,未标记就是不可大对象
不可达对象就会认为是垃圾
2.搬迁对象 压缩堆 (挂起执行内存托管代码线程:把第0代内存(还不是垃圾的内存)放到一代进行搬迁)释放未标记的对象、搬迁可达对象、修改引用地址
- **托管资源**
比如普通对象、字符串、集合。主要由 GC 管理。
- **非托管资源**
比如文件句柄、数据库连接、Socket、窗口句柄。需要更及时地手动释放。
大对象总被认为是二带内存,目的是减少性能损耗
不会对大对象进行压缩搬迁 大对象是85000kb以上的
为什么要用 `IDisposable`
- 因为 GC 只负责“对象内存”回收
- 但像文件、连接、句柄这类资源,经常需要你主动关闭
示意图
step1:程序放入内存
![[Pasted image 20260412210327.png|697]]
step2:发现沾满了
![[Pasted image 20260412210704.png]]
step3:原来的地址搬迁,无引用的垃圾丢掉
![[Pasted image 20260412210810.png]]
step4:D放入
![[Pasted image 20260412210946.png]]
step5:如果0代1代后续又满了,那么这套算法会同时做用在两代内存中,以此类推
类似多级缓存cache,L1最快但常换L3最慢
#基础
# GC和IDispose()
### 首先区分资源类型:
- **托管资源**:C# 自己管理的内存(比如普通对象、字符串)
- 不用你管,**垃圾回收器(GC)会自动释放**
- **非托管资源**:C# 管不了的外部资源
- 例子:文件流、数据库连接、网络套接字、图片句柄、系统内核对象
- **必须手动释放**,否则会造成**资源泄漏**(程序越跑越慢、卡死)
### 为什么要用 `IDisposable`
- 非托管资源 GC 不会自动清理
- 必须提供一个**标准、统一**的方式手动释放
- `IDisposable` 就是 .NET 专门定义的**释放资源标准接口**
### 标准释放模式
例子:
最常见写法:
```csharp
using System;
using var stream = new FileStream("demo.txt", FileMode.OpenOrCreate);
// 使用 stream
```
// 1. 实现 IDisposable 接口
说明:
- `using` 结束时会自动调用 `Dispose()`
- 这比“等 GC 什么时候想起来再收”更可靠
标准释放模式示意:
```csharp
public class MyResource : IDisposable
{
// 标记资源是否已释放
private bool _disposed = false;
private bool _disposed;
// 2. 公共释放方法(外部调用)
public void Dispose()
{
Dispose(true);
// 告诉GC:不用再管我了,我已经手动释放完了
GC.SuppressFinalize(this);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
// 3. 核心释放逻辑
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
{
// 释放 托管资源
}
if (disposing)
{
// 释放托管资源
}
// 释放 非托管资源(比如关闭文件、关闭数据库连接)
// 这里写你的资源释放代码
// 释放非托管资源
_disposed = true;
}
_disposed = true;
}
// 4. 析构函数:防止用户忘记调用 Dispose
~MyResource()
{
Dispose(false);
}
~MyResource()
{
Dispose(false);
}
}
```
##### 解释:
逻辑:
1. 用完实例化对象
2. 调用`.Dispose()`手写的`.Close()`等方法进行手动释放
3. 判断标记变量
4. 没释放就执行释放代码
5. 标记为`true`
`GC.SuppressFinalize(this);` 的作用
告诉.NET的GC这个对象的非托管资源,我**手动释放完了**,你不需要在垃圾回收的时候,再执行它的析构函数了。==它不会取消GC运行,只是取消执行析构函数!==
析构函数 `~MyResource()` 是干嘛的?和 GC 啥关系?
防止你忘记调用 `Dispose()`
场景模拟
1. 你写了代码,**马虎忘了**调用 `.Dispose()`
2. 非托管资源(文件 / 数据库)一直占着没释放
3. 等到 GC(清洁工)来回收垃圾时
4. **自动触发析构函数** `~MyResource()`
5. 函数里执行 `Dispose(false)`,强行把非托管资源释放了
==一个实例化对象dispose后就不会再用了(设计上就是如此,用过dispose后标签一直是true==
==而且由于它dispose了,内部引用是空的,所以只会留下一个实例化的小壳==
==实例化的小壳也会因为没有被引用被GC清理,不会造成太大的性能问题==
如果需要==一次性==彻底释放
例子:
```Csharp
var resource1 = new MyResource();
resource1.Dispose();
//手动切断引用
resource1 = null;
//手动触发GC
GC.Collection();
GC.WaitForPendingFinalizers();//等待回收完成
```
补充理解:
1. `Dispose()` 是“我现在就主动释放”
2. 析构函数是“万一你忘了,我最后帮你兜底一次”
3. `GC.SuppressFinalize(this)` 的意思是:
这个对象已经手动释放好了,不用再走终结器那一套
复习重点:
- GC 管托管内存
- `Dispose` 管需要及时释放的资源
- 最常用姿势不是手写 `GC.Collect()`,而是优先写好 `using` / `Dispose`
#封装
#核心
+35 -16
View File
@@ -1,36 +1,55 @@
# 索引器
概念:
让对象像数组一样可以通过下标访问,方便程序编写
- 索引器(Indexer)可以让对象像数组一样通过 `[]` 访问
- 它很像属性,只是访问器带参数
- 一般用一个数组和索引器写法来实现按照下标访问成员
通常用法:
- 封装内部数组或集合
- 让类的使用方式更自然
语法:
```Csharp
访 this[ 1 2.....]
```csharp
访 this[]
{
get{} ====
set{}
get { }
set { }
}
```
案例:
```Csharp
class Person
```csharp
class Team
{
private string name;
priavte int age;
private Person[] friends;
public Person this[int index]
private string[] members = new string[3];
public string this[int index]
{
get{return friends[index];}
set{friends[index] = value;}
get
{
return members[index];
}
set
{
members[index] = value;
}
}
}
Person p1 = new Person();
p[0] = new Person();
Team team = new Team();
team[0] = "小明";
Console.WriteLine(team[0]);
```
补充:
- 索引器也可以只有 `get`,做成只读
- 索引器也可以有多个参数,比如 `this[int row, int col]`
注意:
1. 索引器写法里的名字固定是 `this`,不是自定义名字
2. 索引器本质上还是类成员,不是数组本身
3. `[]` 不是直接重载出来的,==而是通过索引器语法提供的==
4. 如果内部是真实数组或列表,通常还要自己考虑越界问题
#封装
#核心
+168 -25
View File
@@ -11,28 +11,28 @@
3. 让子类在父类基础上增加自己的成员和功能
语法:
```Csharp
```csharp
class :
{
}
```
案例:
```Csharp
```csharp
class Animal
{
public string name;
protected int age;
public Animal(string name)
{
this.name = name;
}
public void Speak()
{
cw("动物发出声音");
Console.WriteLine("动物发出声音");
}
}
@@ -42,12 +42,12 @@ class Dog : Animal
{
this.age = age;
}
public void Bark()
{
cw(name);
cw(age);
cw("汪汪");
Console.WriteLine(name);
Console.WriteLine(age);
Console.WriteLine("汪汪");
}
}
@@ -59,11 +59,152 @@ d.Bark();
注意:
1. ==C# 的类只能直接继承一个父类==,也就是单继承
2. 继承可以传递,A 继承 B,B 继承 C,那么 A 也会继承到 C 的可用成员
2. 继承可以传递,A 继承 B,B 继承 C,那么 A 也会拥有 C 中可继承的成员
3. ==构造函数不会被继承==,子类只是可以调用父类构造函数
4. 父类的 `private` 成员子类==不能直接访问==,想给子类用一般写 `protected`
5. 结构体 `struct` ==不支持继承类==,但可以实现接口
6. 子类对象本质上也是父类对象的一种,所以继承是后面学[[多态vob|多态]]的基础
6. 子类对象本质上也是父类对象的一种,所以继承是后面学[[多态vob|多态]]和[[里氏替换原则]]的基础
----
# 继承中的构造函数
概念:
继承里最容易混淆的一点就是==“子类会继承父类成员,但不会继承父类构造函数”==
当你 `new` 一个子类对象时,不只是创建“子类自己的部分”,还要先把==父类那一部分==初始化好
所以子类构造函数最终一定会调用某个父类构造函数
通常用法:
1. 让父类负责初始化“大家共有”的数据
2. 让子类只负责补充自己的初始化逻辑
3. 保证对象创建出来时就是完整、可用的状态
核心规则:
1. ==父类构造函数不会被继承==
2. ==子类构造函数最终一定会调用某个父类构造函数==
3. 如果子类没有显式写 `: base(...)`,编译器会尝试补一个 `: base()`
4. 如果父类没有无参构造函数,子类就必须显式写 `: base(...)`
5. 构造函数体的执行顺序可以先记成:==先父类,后子类==
案例1:显式调用父类构造函数
```csharp
class Person
{
public string Name;
public Person(string name)
{
Name = name;
Console.WriteLine("Person构造");
}
}
class Student : Person
{
public int Score;
public Student(string name, int score) : base(name)
{
Score = score;
Console.WriteLine("Student构造");
}
}
Student s = new Student("小明", 100);
```
执行理解:
1. 先执行 `Person(string name)`
2. 再执行 `Student(string name, int score)` 的函数体
案例2:父类有无参构造函数时,可以省略 `base()`
```csharp
class Animal
{
public Animal()
{
Console.WriteLine("Animal构造");
}
}
class Dog : Animal
{
public Dog()
{
Console.WriteLine("Dog构造");
}
}
Dog d = new Dog();
```
↑ 这里虽然没写 `: base()`,但编译器会默认帮你补上
案例3:父类没有无参构造函数时,子类必须显式写 `base(...)`
```csharp
class Animal
{
public string Name;
public Animal(string name)
{
Name = name;
}
}
class Dog : Animal
{
public Dog(string name) : base(name)
{
}
}
```
错误示意:
```csharp
class Dog : Animal
{
public Dog()
{
}
}
```
↑ 这时会报错,因为编译器默认想调用 `base()`,但父类根本没有无参构造函数
案例4:子类构造函数之间也可以先用 `this(...)` 串起来
```csharp
class Animal
{
public string Name;
public Animal(string name)
{
Name = name;
}
}
class Dog : Animal
{
public int Age;
public Dog() : this("默认小狗", 0)
{
}
public Dog(string name, int age) : base(name)
{
Age = age;
}
}
```
理解:
`Dog()` 没有直接 `base(name)`,而是先调用同类里的 `Dog(string name, int age)`
然后再由那个构造函数去调用 `base(name)`
所以最终调用链还是会到父类构造函数
注意:
1. 一个构造函数初始化器里==只能写一个==,也就是写了 `this(...)` 就不能再写 `base(...)`
2.`this(...)` 调到的另一个构造函数里,仍然可以写 `base(...)`
3. 不要在子类里重复初始化父类已经负责的数据,父类自己的数据最好交给父类构造函数处理
4. 初学阶段先牢牢记住:==创建子类对象时,一定先把父类部分构造好==
----
# base关键字
@@ -76,34 +217,35 @@ d.Bark();
2. 在子类方法中访问父类同名成员或父类方法
语法:
```Csharp
```csharp
class Dog : Animal
{
public Dog(string name) : base(name)
{
}
}
```
调用父类成员:
```Csharp
```csharp
base.
```
案例:
```Csharp
```csharp
class Father
{
public string name;
public Father(string name)
{
this.name = name;
}
public void Say()
{
cw("我是父类");
Console.WriteLine("我是父类");
}
}
@@ -111,13 +253,12 @@ class Son : Father
{
public Son(string name) : base(name)
{
}
public void Test()
{
base.Say();
cw(base.name);
Console.WriteLine(base.name);
}
}
@@ -130,14 +271,16 @@ s.Test();
1. `base` 只能在==实例构造函数、实例方法、实例属性访问器==里用
2. ==静态方法里不能用 `base`==
3. 如果父类没有无参构造函数,子类通常就要显式写 `: base(...)`
4. `base` 是访问父类,不是创建一个新的父类对象
4. `base` 是访问父类成员,不是创建一个新的父类对象
----
引用:
1. Microsoft Learn: [Inheritance - derive types to create more specialized behavior](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/object-oriented/inheritance)
2. Microsoft Learn: [Inheritance in C# and .NET](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/tutorials/inheritance)
3. Microsoft Learn: [base (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/base)
4. Microsoft Learn: [protected (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/protected)
3. Microsoft Learn: [Constructors (C# Programming Guide)](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/constructors)
4. Microsoft Learn: [Using constructors](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/using-constructors)
5. Microsoft Learn: [base (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/base)
6. Microsoft Learn: [protected (C# Reference)](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/protected)
#继承
#核心
+71 -75
View File
@@ -1,86 +1,82 @@
# 运算符重载
概念:
==自定义类和结构体能够使用运算符==
让自定义类结构体可以进行运算
通常需要注意运算符需要几个参数,例如取反只有一个参数,加有两个参数
可重载的运算符:
- 算数运算符
- 逻辑运算符==非==
- 位运算符
- 条件运算符
不可重载的运算符:
- 逻辑与(&&)逻辑或(||
- 索引符\[]
- 墙砖运算符()
- 点.
- 三目运算符: ?
- 赋值符=
- 运算符重载可以让==自定义类型==像内置类型一样使用某些运算符
- 本质上是给运算符提供一套你自己定义的行为
通常用法:
- 坐标、向量、复数、金额等“天然适合运算”的类型
语法:
使用关键词operator
作为类的一个成员存在
public static 返回值类型 operator 运算符(参数列表)
案例:
```Csharp
class Point
```csharp
public static operator ()
{
public int x;
public int y;
public static Point operator +(Point p1, Point p2)
{
Point newPoint = new Point();
newPoint.x = p1.x + p2.x;
newPoint.y = p1.y + p2.y;
return newPoint;
}
public static Point operator +(Point p1, int value)
{
Point newPoint = new Point();
newPoint.x = p1.x + value;
newPoint.y = p1.y + value;
return newPoint;
}
}
class Program
{
public static void Main(string[] args)
{
Point p1 = new Point();
p1.x = 1;
p1.y = 1;
Point p2 = new Point();
p2.x = 2;
p2.y = 2;
Point p3 = new Point();
p3 = p1 + p2;
Console.WriteLine(p3.x);
p3 = null;
p3 = p1 + 10;
Console.WriteLine(p3.x);
}
// 自定义逻辑
}
```
第一个运算符重载了`+`号,可以让这个类的两个实例化对象相加
第二个运算符也重载了`+`号,可以让这个类加上一个value
==如果需要value在前也需要进行一次重载!!!==
案例:
```csharp
class Point
{
public int X;
public int Y;
public static Point operator +(Point p1, Point p2)
{
return new Point
{
X = p1.X + p2.X,
Y = p1.Y + p2.Y
};
}
public static Point operator +(Point p, int value)
{
return new Point
{
X = p.X + value,
Y = p.Y + value
};
}
}
Point p1 = new Point { X = 1, Y = 1 };
Point p2 = new Point { X = 2, Y = 2 };
Point p3 = p1 + p2;
Point p4 = p1 + 10;
Console.WriteLine($"{p3.X}, {p3.Y}");
Console.WriteLine($"{p4.X}, {p4.Y}");
```
补充:
- 如果你希望 `10 + p1` 也成立,需要再额外写一份对应重载
- 运算符重载最重要的是==语义自然==,不要让 `+` 去做奇怪的事
常见可重载运算符:
- 一元:`+` `-` `!` `~` `++` `--` `true` `false`
- 二元:`+` `-` `*` `/` `%` `&` `|` `^` `<<` `>>`
- 比较:`==` `!=` `<` `>` `<=` `>=`
不能直接重载的常见运算符:
- `&&` `||`
- `[]`
- `()`
- `.`
- `=`
- `?:`
- `??`
注意:
1. 一定是公共的静态方法
2. 返回值写在operator前
3. 逻辑处理自定义
4. 一个符号可以多个重载
5. 不能使用ref和out
6. ==条件运算符需要成对实现==
1. ==传统写法里,运算符重载通常写成 `public static`==
2. 参数里至少要有一个参数是当前类型
3. 不能给运算符参数加 `ref``out`
4. 某些运算符需要成对实现:
- `==``!=`
- `<``>`
- `<=``>=`
- `true``false`
5. 如果重载了 `==` / `!=`,通常也应该一起重写 `Equals``GetHashCode`
6. `&&` / `||` 不能直接重载,但可以结合 `true` / `false``&` / `|` 实现相关行为
#核心
+257
View File
@@ -0,0 +1,257 @@
# 里氏替换原则
概念:
- 里氏替换原则(Liskov Substitution PrincipleLSP)说的是:
==所有使用父类的地方,都应该能够无障碍地替换成子类,而程序行为仍然正确==
- 简单理解:
==子类必须真的能当父类来用,而不是“语法上能继承,逻辑上却不成立”==
通常用法:
1. 判断一个继承关系到底合不合理
2. 约束子类重写父类成员时,不要破坏原有规则
3. 提醒自己:如果继承后总要“特殊处理”,那通常应该改成组合或接口
核心理解:
1. 子类可以扩展父类功能
2. 但==不能破坏父类原本承诺的行为==
3. 如果父类能做的事,子类却突然“不支持”了,那往往就违反了里氏替换原则
----
# 经典反例
错误设计:
```csharp
class Bird
{
public virtual void Fly()
{
Console.WriteLine("鸟会飞");
}
}
class Sparrow : Bird
{
public override void Fly()
{
Console.WriteLine("麻雀飞走了");
}
}
class Penguin : Bird
{
public override void Fly()
{
throw new NotSupportedException("企鹅不会飞");
}
}
void LetBirdFly(Bird bird)
{
bird.Fly();
}
LetBirdFly(new Sparrow()); // 正常
LetBirdFly(new Penguin()); // 出错
```
问题:
- `LetBirdFly(Bird bird)` 这个函数相信:只要传进来的是 `Bird`,就能 `Fly()`
-`Penguin` 继承了 `Bird` 后,却把“会飞”这个规则破坏了
- 这就说明:==`Penguin` 其实不应该直接继承这种“默认会飞”的 Bird==
----
# 更合理的改法
改法一:把“鸟”和“会飞”拆开
```csharp
abstract class Bird
{
public string Name { get; set; }
}
interface IFlyable
{
void Fly();
}
class Sparrow : Bird, IFlyable
{
public void Fly()
{
Console.WriteLine("麻雀飞走了");
}
}
class Penguin : Bird
{
public void Swim()
{
Console.WriteLine("企鹅在游泳");
}
}
void LetBirdFly(IFlyable bird)
{
bird.Fly();
}
LetBirdFly(new Sparrow()); // 正常
// LetBirdFly(new Penguin()); // 编译期就不允许,更合理
```
这样做的好处:
1. `Bird` 只表达“它是一只鸟”
2. `IFlyable` 只表达“它会飞”
3. 不会飞的鸟就不用为了继承而硬实现一个错误行为
----
# 怎么判断有没有违反
可以拿下面几个问题自检:
1. 子类是不是父类的一种?
- 如果读起来都别扭,那继承关系大概率有问题
2. 把子类对象传到父类参数的位置,原有代码还能正常工作吗?
- 如果经常要加 `if (对象是某某子类)`,设计就开始危险了
3. 子类有没有把父类原本支持的行为变成“不支持”?
- 例如:父类能 `Fly()`,子类却直接抛异常
4. 子类有没有偷偷加强限制?
- 父类允许任意正数,子类却只允许 1 到 10
5. 子类有没有把父类承诺的结果变弱?
- 父类保证成功,子类却经常返回空结果或失败
----
# 违反里氏替换原则的常见信号
1. 子类重写后直接抛 `NotSupportedException`
2. 子类必须靠很多 `if/else` 特判才能安全使用
3. 继承只是为了复用代码,但语义上并不是“is-a”关系
4. 子类把父类的规则改掉了,而不是扩展
补充:
- “能继承”不等于“该继承”
- 很多违反 LSP 的代码,表面上编译没问题,真正出问题的是业务逻辑
----
# 和继承的关系
里氏替换原则不是反对继承,而是告诉你:
- ==继承必须建立在真正的替换关系上==
- 如果子类不能稳定替换父类,就不要硬继承
- 这时通常优先考虑:
- 组合
- 接口
- 更细的抽象拆分
所以可以把它和[[继承]]一起记:
- 继承解决“复用和扩展”
- 里氏替换原则约束“这个继承是不是合理”
----
# `is` 和 `as` 关键词怎么结合理解
先说结论:
- `is``as` 是==类型判断 / 类型转换工具==
- 它们能帮你在运行时判断“这个对象现在到底是什么类型”
- 但它们==不能证明一个继承设计就合理==
## `is`
作用:
- 判断对象是否兼容某个类型
- 也常用于模式匹配
例子:
```csharp
Bird bird = new Sparrow();
if (bird is Sparrow)
{
Console.WriteLine("这是麻雀");
}
if (bird is IFlyable flyable)
{
flyable.Fly();
}
```
理解:
- `bird is Sparrow` 的意思是:当前这个对象是不是 `Sparrow`
- `bird is IFlyable flyable` 不仅判断类型,还顺手把结果保存到变量 `flyable`
## `as`
作用:
- 尝试把对象安全转换成某个引用类型或可空值类型
- 转换失败不会抛异常,而是返回 `null`
例子:
```csharp
Bird bird = new Sparrow();
IFlyable flyable = bird as IFlyable;
if (flyable != null)
{
flyable.Fly();
}
```
补充:
- `as` 常用于引用类型
- 如果转换失败,结果是 `null`
- 对普通非可空值类型不能直接用 `as`
## 它和里氏替换原则的关系
`is` / `as` 本身没有错,但如果你发现代码里总是这样:
```csharp
void Handle(Bird bird)
{
if (bird is Sparrow)
{
// 一套逻辑
}
else if (bird is Penguin)
{
// 另一套逻辑
}
}
```
那通常说明一件事:
- 你嘴上写的是“我接收 `Bird`
- 但你心里真正依赖的却是“具体子类是谁”
这往往意味着:
1. 抽象不够稳定
2. 多态没有真正用起来
3. 继承关系可能不够合理
4. 有概率正在逼着代码违反里氏替换原则
更好的方向一般是:
- 让父类 / 接口直接提供调用方真正需要的能力
- 或者把能力拆到更合适的接口里
一句话记:
- `is` / `as` 是“识别类型”的工具
- 里氏替换原则是“约束继承是否合理”的原则
- ==如果父类代码里到处都要靠 `is` / `as` 分辨子类,通常该反过来检查抽象设计==
----
# 一句话记忆
==子类可以扩展父类,但不能打破父类原来的承诺==
再压缩一点:
==父类能出现的地方,子类也必须能正常出现==
----
# 面试/复习时怎么说
可以这样答:
“里氏替换原则的意思是,子类对象必须能够替换父类对象,并且不影响程序原本的正确性。
如果一个子类继承了父类,却把父类原本支持的行为改成异常、限制更多条件,或者导致调用方需要额外特殊判断,那这个继承关系通常就不合理。
这时候应该考虑重新抽象,或者改成接口和组合。”
----
# 注意
1. 里氏替换原则是==设计原则==,不是语法规则
2. 它常和[[继承]]、[[多态vob|多态]]、[[接口]]一起理解
3. `is` / `as` 可以辅助做类型判断,但不能代替抽象设计
4. 只要你发现“某个子类总是不太像父类”,就该警惕是不是违反了 LSP
#核心
#设计原则
+49 -32
View File
@@ -1,51 +1,68 @@
# 静态成员
概念:
static 修饰的成员变量、方法、属性
-`static` 修饰的成员叫静态成员
- 静态成员属于==类型本身==,不属于某个具体对象
- 常见的静态成员有:静态字段、静态方法、静态属性、静态事件
特点:
==直接用类名点出来使用 没有实例化!!!!==
- 一般通过==类名.成员名==访问
- 同一类型的所有对象共享同一份静态数据
通常用法:
PI之类的全局且唯一性的
语法:
static 变量名 = XXX
- 记录全局共享状态
- 写工具方法
- 定义不会因对象不同而变化的数据
案例:
```Csharp
class Test
```csharp
class Player
{
static public float PI = 3.1415926; //静态成员
public int testInt = 100;
public static float CalcCircle(float r) //静态方法
public static int Count;
public string Name;
public Player(string name)
{
return PI * r;
Name = name;
Count++;
}
public void TestFunc() //非静态方法
public static void PrintCount()
{
Console.WriteLine("123");
Console.WriteLine(Count);
}
}
static void Main(string[] args)
{
Console.WriteLine(Test.PI);
Console.WriteLine(Test.CalcCircle(2));
}
Player p1 = new Player("A");
Player p2 = new Player("B");
Player.PrintCount(); // 2
```
注意:
和程序同生共死,在程序使用后永远不会清除内存,直到程序结束
非静态函数可以调用静态成员
静态成员声明后可以不赋初始值
静态方法中不能使用==没有实例化的==非静态成员
1. ==静态成员属于类,不属于对象==
2. 静态方法里不能直接访问实例成员,因为它没有 `this`
3. 实例方法里可以访问静态成员
4. 静态字段通常会随着类型存在而存在;在大多数日常程序里,可以近似理解为“程序结束前一直都在”
5. 静态成员不是“永远不释放”的绝对规则,别把它和 GC 机制混为一谈
[[常量|const]] 和 `static readonly`
共同点:
- 都常用来表示“不希望随便改”的值
[[常量]]可以理解为特殊的静态
共同点
- 都可以通过类名点出来使用
不同点
- const必须初始化,不能修改 static 没有这个限制
- const值能修饰变量、static可以修饰很多
- const一定是写在访问修饰符后面的,static没有这个要求
区别:
1. `const`
- 必须声明时初始化
- 编译期常量
- 隐式就是静态语义
2. `static readonly`
- 可以声明时赋值,也可以在静态构造函数中赋值
- 运行期确定
- 更适合保存复杂初始化结果
例子:
```csharp
class MathInfo
{
public const float Pi = 3.1415926f;
public static readonly DateTime StartTime = DateTime.Now;
}
```
@@ -1,70 +1,80 @@
# 静态类
概念:
用static修饰
==只能包含静态成员==
==不能被实例化==
-`static` 修饰的类叫静态类
- ==只能包含静态成员==
- ==不能被实例化==
通常用法:
作为==工具类==方便使用
- 写工具类
- 存放通用辅助方法
语法:
访问修饰符 static 类名(){}
案例:
```Csharp
public static class fun()
{
}
```
注意:
静态类不能被实例化,所以不能new
# 静态构造函数
概念:
staitc修饰
通常用法:
在静态函数中初始化静态变量
语法:
案例:
```Csharp
static class StaticClass
```csharp
public static class MathTool
{
public static int testInt = 100;
public static int testInt2 = 100;
static StaticClass()
public static int Add(int a, int b)
{
Console.WriteLine("静态构造函数");
return a + b;
}
}
```
↑运行后,在第一次使用该类会直接打印
```Csharp
注意:
1. 静态类不能 `new`
2. 静态类不能包含实例成员
3. 静态类本质上也不能被继承,常可以理解为“天然密封”
----
# 静态构造函数
概念:
- 静态构造函数用于初始化静态数据
- 会在==第一次使用该类型前==自动执行
通常用法:
- 初始化复杂的静态字段
- 做一次性的类型级初始化
语法:
```csharp
class Test
{
public static int testInt = 200;
static Test()
{
Console.WriteLine("静态构造函数");
}
public Test()
{
Console.WriteLine("普通构造函数");
}
}
```
↑运行后,在第一次使用该类会在所有操作前先调用静态构造函数
案例:
```csharp
class Test
{
public static int Count;
static Test()
{
Count = 100;
Console.WriteLine("执行静态构造函数");
}
public Test()
{
Console.WriteLine("执行普通构造函数");
}
}
Console.WriteLine(Test.Count);
var t = new Test();
```
执行理解:
- 第一次访问 `Test.Count` 时,会先执行静态构造函数
- 之后再创建对象时,不会重复执行静态构造函数
注意:
1. 静态类和普通类都可以用
2. 不能使用访问修饰符
3. ==不能有参数==
4. 只会==自动调用一次==
1. ==类和结构体都可以有静态构造函数==
2. ==静态构造函数不能写访问修饰符==
3. ==静态构造函数不能有参数==
4. 一个类型最多只有一个静态构造函数
5. 静态构造函数只会自动执行一次
6. 如果只是简单赋值,优先考虑直接写静态字段初始化器,通常更直观