面向对象三大特性

1. 封装

类与对象

Class Student{}
int main(){
    Student stu1;  // 表示在栈中加入了stu1
    stu1 = new Student(); // 在堆中开辟了一点内存。stu1指向这个内存
}

构造函数和析构函数

注意点:

如果没有写构造函数,默认会有一个空参的构造函数。

如果只写了有参构造函数,但没写空参构造函数,它默认不会创建空参构造函数。

class Student {
  int age;
  string name;
  // 无参构造函数
  public Student(){}
  // 有参构造函数
  public Student(int age)
  {
    this.age = age;
  }
  // 特殊的构造函数
  // 在构造函数后面写 :this(age)表示我先要去调用前面的只有一个参数的构造函数,再来调用2个参数的构造函数。
  public Student(int age, string name):this(age){
    this.name = name;
  }
}

析构函数(不重要)

当引用类型的堆内存被回收时,会调用这个函数

```

class Person{

~Person(){

}

}

```

### 垃圾回收机制

字段与属性

思考:字段是类中的成员变量,而属性就像是访问字段的方法,通常包含get和set访问器。那么我们为什么要隐藏字段而公开属性呢?

试想一下如果字段是公开的,那么我们可以在任何地方去修改这个字段的值,而且修改某个地方字段的值,可能会破化依赖该字段的代码。而且也会导致无效(比如把年龄修改为负数),所以我们可以通过属性去添加验证逻辑,计算逻辑等,保证数据的有效性。

所以一个正确的代码应该如下面一样。

// ❌ 不推荐:直接暴露公有字段
public class BadExample
{
    public int Age; // 外部可直接修改,无法控制数据合法性
}

// ✅ 推荐:通过属性封装字段
public class GoodExample
{
    private int _age;
    public int Age
    {
        get => _age;
        //  value关键字表示外部传入的值
        set => _age = value >= 0 ? value : throw new ArgumentException();
    }
}

自动属性

自动属性是C#3.0引入的,用来简化属性定义的语法。传统属性需要手动定义字段和get/set方法,但自动属性可以更简洁。

我们先看看传统属性和自动属性之间的区别

// 传统属性(需要手动定义字段)
private string _name;  // 显式声明字段
public string Name
{
    get { return _name; }
    set { _name = value; }
}

// 自动属性(编译器自动生成字段和逻辑)
public string Name { get; set; }  // 完全等价于传统写法

自动属性的一些特点

// 1. 从 C# 6 开始,支持直接在属性声明中初始化默认值
public string Name { get; set; } = "Unknown";

// 2. 支持只读属性(只能在构造函数中赋值)
public string Id { get; } 
public MyClass() => Id = Guid.NewGuid().ToString();

// 3. C# 9+ 的 init 访问器:允许在对象初始化期间赋值
public string Id { get; init; }  // 初始化时赋值后不可修改
var obj = new MyClass { Id = "123" };  // 合法

自动属性的缺点

// 1. 无法直接访问后台字段(隐藏字段)
public int Age { get; set; }
Console.WriteLine(_age);  // 编译错误:无法直接访问隐藏字段

// 2. 无法添加自定义逻辑
// 自动属性无法实现以下逻辑
private int _age;
public int Age
{
    get => _age;
    set => _age = value >= 0 ? value : throw new ArgumentException();
}

箭头语法

  1. 方法简化

// 传统方法
public int Add(int a, int b) 
{
    return a + b;
}

// 箭头语法简化
public int Add(int a, int b) => a + b;

  1. 只读属性

// 传统属性
public string FullName 
{
    get { return $"{FirstName} {LastName}"; }
}

// 箭头语法简化
public string FullName => $"{FirstName} {LastName}";

  1. 构造函数

public class Person 
{
    private string _name;
    // 构造函数
    public Person(string name) => _name = name;
    
    // 析构函数(C# 7.3+)
    ~Person() => Console.WriteLine("对象已销毁");
}

索引器

让对象可以像数组一样通过索引访问其中元素,使程序看起来更直观,更容易编写

😄 注意:结构体里面也是支持索引器

```

class Person

{

private int[,] array;

public int this[int i, int j]

{

get

{

return array[i, j];

}

set

{

array[i, j] = value;

}

}

}

/// 使用

Person p = new Person();

p[0, 0] = 10;

```

静态

#### 静态成员

程序中是不能无中生有的。我们要使用的对象,变量,函数都是要在内存中分配内存空间的。之所以要实例化对象,目的就是分配内存空间,在程序中产生一个抽象的对象。

静态成员的特点

* 程序开始运行时 就会分配内存空间。所以我们就能直接使用。

* 静态成员和程序同生共死,只要使用了它,直到程序结束时内存空间才会被释放

* 一个静态成员就会有自己唯一的一个“内存小房间”这让静态成员就有了唯一性

* 在任何地方使用都是用的小房间里的内容,改变了它也是改变小房间里的内容。

> 👀️ 根据上面的描述,可以很容易解释下面的注释点

>

> 1. 静态函数中不能使用非静态成员(程序运行后,对象都没有new出来,怎么使用非静态成员)

> 2. 非静态函数可以使用静态成员

```

class Test{

public const float G = 9.8f;

}

// 使用

Console.WriteLine(Test.PI);

```

常量与静态变量

const(常量)可以理解为特殊的static(静态)

【const与static的区别】

* const必须初始化,不能修改,而static没有这个规则

* const只能修饰变量,而static可以修饰很多

* const一定是写在访问修饰符后面的 ,而static没有这个要求

```

// 定义一个常量

class Test{

public const float G = 9.8f; // 必须声明后就初始化,而且初始化后就不能修改了

}

```

#### 静态类

【特点】:只能包含静态成员,不能被实例化

【作用】:常用的静态成员写在静态类中 方便使用,作为一个工具类。

```

static class Tools{

// 1. 静态成员变量

public static int testIndex = 0;

// 2. 静态成员方法

public static void TestFun(){}

// 3. 静态成员属性

public static int TestIndex{get;set}

}

```

#### 静态构造函数

【特点】

* 静态类和普通类都可以有

* 不能使用访问修饰符

* 不能有参数

* 只会自动调用一次

> 静态构造函数:

>

> 1. 静态类中的构造函数:在第一次使用静态类中的静态成员时,调用该构造函数。之后就不会再次调用了。

> 2. 普通类中的静态构造函数:在第一次创建对象时,调用该构造函数。之后就不会再次调用了。

### 拓展方法

为现有非静态变量类型添加新方法

【特点】

* 一定是写在静态类中

* 一定是个静态函数

* 第一个参数为拓展目标

* 第一个参数用this修饰

【基本语法】`访问修饰符 static 返回值 函数名(this 拓展类名 参数名, 参数类型 参数名,参数类型 参数名....)`

```

using UnityEngine;

static class Tools

{

// 为int扩展方法

public static void SpeakValue(this int value)

{

Debug.Log("当int对象调用这个方法时,处理的逻辑" + value.ToString());

}

// 为string扩展方法,该方法带有两个参数

public static void PrintStringInfo(this string value, string str1, string str2)

{

Debug.Log("为string扩展方法,下面是扩展的逻辑");

Debug.Log("对象调用该方法的参数" + str1 + str2);

}

// 为自定义类扩展方法

public static void Fun2(this MyTest test)

{

Debug.Log("自定义扩展的方法");

}

}

class MyTest

{

public void Fun1()

{

Debug.Log("自带的方法");

}

}

public class Test : MonoBehaviour

{

private void Start()

{

int a = 0;

a.SpeakValue();

string s = "hahha";

s.PrintStringInfo("param1", "param2");

MyTest test = new MyTest();

test.Fun1();

test.Fun2();

}

}

```

#### 练习1:为整型扩展一个求平方的方法

```

using UnityEngine;

static class Tools

{

public static int GetSquare(this int value)

{

return value * value;

}

}

public class Test : MonoBehaviour

{

private void Start()

{

int a = 10;

print(a.GetSquare());

}

}

```

#### 练习2:玩家消亡

【需求】写一个玩家类,包括姓名,血量,攻击力,防御力等特征,攻击,移动,受伤等方法,为该玩家类扩展一个消亡的方法。

```

using UnityEngine;

static class Tools

{

public static void Perish(this Player value)

{

value.Hp = 0;

Debug.Log("玩家死亡,当前血量为" + value.Hp);

}

}

public class Player

{

public Player(string name, int hp, int attackPower, int defensivePower)

{

Name = name;

Hp = hp;

AttackPower = attackPower;

DefensivePower = defensivePower;

}

public string Name { get; private set; }

public int Hp { get; set; }

public int AttackPower { get; private set; }

public int DefensivePower { get; private set; }

public void Attack(int AttackPower)

{

Debug.Log("玩家攻击:" + AttackPower);

}

public void Move()

{

Debug.Log("玩家移动");

}

public void Injury(int power)

{

Hp -= power;

Debug.Log("玩家血量:" + Hp);

}

}

public class Test : MonoBehaviour

{

private void Start()

{

Player player = new Player("李四", 10, 10, 10);

player.Perish();

}

}

```

重载运算符

让自定义的类可以进行算术运算、逻辑运算、条件运算等。这些运算可以自己定规则。

【基本语法】`public static 返回类型 operator 运算符(参数列表)`

#### 算术运算符

```

public class Point

{

public static Point operator -(Point p1, Point P2)

{

return null;

}

public static Point operator *(Point p1, Point P2)

{

return null;

}

public static Point operator /(Point p1, Point P2)

{

return null;

}

public static Point operator %(Point p1, Point P2)

{

return null;

}

public static Point operator ++(Point p1)

{

return null;

}

public static Point operator --(Point p1)

{

return null;

}

}

```

#### 逻辑运算符

```

public class Point

{

public static bool operator !(Point p1)

{

return false;

}

}

```

#### 位运算符

```

public class Point

{

public static Point operator |(Point p1, Point p2)

{

return null;

}

public static Point operator &(Point p1, Point p2)

{

return null;

}

public static Point operator ^(Point p1, Point p2)

{

return null;

}

public static Point operator ~(Point p1)

{

return null;

}

public static Point operator <<(Point p1, int num)

{

return null;

}

public static Point operator >>(Point p1, int num)

{

return null;

}

}

```

#### 条件运算符

```

public class Point

{

//1.返回值一般是bool值 也可以是其它的

//2.相关符号必须配对实现

public static bool operator >(Point p1, Point p2)

{

return false;

}

public static bool operator <(Point p1, Point p2)

{

return false;

}

public static bool operator >=(Point p1, Point p2)

{

return false;

}

public static bool operator <=(Point p1, Point p2)

{

return false;

}

public static bool operator ==(Point p1, Point p2)

{

return false;

}

public static bool operator !=(Point p1, Point p2)

{

return false;

}

public static bool operator true(Point p1)

{

return false;

}

public static bool operator false(Point p1)

{

return false;

}

}

```

#### 练习1:判断是否相等

【需求】定义一个位置结构体或类,为其重载判断是否相等的运算符,如果(x1,y1)=(x2,y2),只有两个值都相等返回true

```

using UnityEngine;

public class Point

{

public Point(int x, int y) {

this.x = x;

this.y = y;

}

int x, y;

public static bool operator ==(Point a, Point b)

{

if(a.x b.x && a.y b.y) return true;

else return false;

}

public static bool operator !=(Point a, Point b)

{

if (a.x != b.x || a.y != b.y) return true;

else return false;

}

}

public class Test : MonoBehaviour

{

private void Start()

{

Point p1= new Point(0,0);

Point p2= new Point(1,1);

print(p1 == p2);

print(p2 != p1);

}

}

```

#### 练习2:实现复杂运算符

【需求】定义一个Vector3类(x,y,z)通过重载运算符定义以下运算

* (x1,y1,z1) + (x2,y2,z2) = (x1+x2,y1+y2,z1+z2)

* (x1,y1,z1) - (x2,y2,z2) = (x1-x2,y1-y2,z1-z2)

(x1,y1,z1) num = (x1 num, y1 num , z1 * num)

```

using UnityEngine;

public class Vector3

{

public Vector3(int x, int y) {

this.x = x;

this.y = y;

this.z = 0;

}

public Vector3(int x, int y, int z) : this(x, y)

{

this.z = z;

}

int x, y, z;

public static Vector3 operator +(Vector3 v1, Vector3 v2)

{

return new Vector3(v1.x+v2.x, v1.y+v2.y, v1.z + v2.z);

}

public static Vector3 operator -(Vector3 v1, Vector3 v2)

{

return new Vector3(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z);

}

public static Vector3 operator *(Vector3 v1, int num)

{

return new Vector3(v1.x num, v1.y num, v1.z * num);

}

}

```

### 内部类(了解)

2. 继承

继承中的构造函数

【构造函数的执行顺序】创建子类对象时,会自动调用父类的构造方法,且父类的构造方法先执行,子类的构造方法后执行

当子类创建对象时, 默认调用父类的无参构造方法。

😕 如果父类没有无参构造方法(写了有参构造方法,没有写无参构造方法),这创建子类对象时会报错。

Base关键字

我们研究一下下面的代码。Dog类 的构造方法多了 base关键字,意味着它会先去调用父类只有一个参数的构造方法,这个参数是传进来name,然后才会调用Dog类的构造方法。这就引出了Base关键字的第一个功能:调用基类构造方法

// 基类
public class Animal 
{
    private string _name;
    public Animal(string name) 
    {
        _name = name;
    }
}

// 派生类
public class Dog : Animal 
{
    private int _age;
    // 通过 base 显式调用基类构造函数
    public Dog(string name, int age) : base(name) 
    {
        _age = age;
    }
}

我们继续看看下面的代码,在重写基类的Log方法后,使用base,其实就是代表父类,通过base.xx 可以调用父类的方法。即调用基类方法

// 基类
public class Logger 
{
    public virtual void Log(string message) 
    {
        Console.WriteLine($"Base Log: {message}");
    }
}

// 派生类
public class FileLogger : Logger 
{
    public override void Log(string message) 
    {
        // 先调用基类的 Log 方法
        base.Log(message);  
        // 再添加新功能
        File.WriteAllText("log.txt", message);
    }
}

访问基类字段

public class Vehicle 
{
    protected int _speed;  // 基类字段(protected 允许派生类访问)
}

public class Car : Vehicle 
{
    public void Accelerate() 
    {
        base._speed += 10;  // 通过 base 访问基类字段
    }
}

拆箱和装箱

【装箱】把值类型 用 引用类型存储,栈内存会迁移到堆内存中

```

int a = 3;

object o = a;

```

1. 在栈中,将3入栈

2. 在堆中开辟一块地址,该堆中存储内容是栈中的3

3. 在栈中,将o入栈,o指向堆中地址

【拆箱】把引用类型存储的值类型取出来,堆内存会迁移到栈内存中

```

int a = (int)o;

```

【优缺点】

* 好处:不确定类型时可以方便参数的存储和传递

* 坏处:存在内存迁移,增加性能消耗

密封类

【作用】让类无法被继承

```

sealed class Worker{}

```

3. 多态

多态:多种形态。体现为子类可以被父类应用

new关键字

在讲解new关键字时,我们看看那下面的这串代码

public new const string NAME = "PlayerProxy";

先说说const关键字,const关键字默认是静态,可以在外部通过xx类.Name得到常量

在说说new关键字,它用于隐藏基类中的同名成员。会明确告诉编译器:当前类(派生类)定义了一个与基类中同名的成员(如字段、方法、属性等),派生类中的成员将“覆盖”基类中的同名成员

既然可以隐藏关键字,那么也可以隐藏方法,下面就是通过子类隐藏父类的方法

public class Animal
{
    public void Eat()
    {
        Debug.Log("父类吃的方法");
    }
}

public class Cat : Animal
{
    // new关键字是隐藏父类Eat的方法
    public new void Eat()
    {
        Debug.Log("子类猫吃的方法");
    }
}

public class Test : MonoBehaviour
{
    private void Start()
    {
        Animal a = new Cat();
        a.Eat(); // 调用的是 父类的Eat方法
        (a as Cat).Eat(); // 调用的是 子类的Eat方法
    }
}

虚方法

用vritual关键字修饰的已经实现的方法,即是虚方法

```

public class Animal

{

public virtual void Eat()

{

Debug.Log("虚方法,可以被重写");

}

}

```

方法重写

```

public class Animal

{

public virtual void Eat()

{

Debug.Log("虚方法,可以被重写");

}

}

public class Cat : Animal

{

// 使用override重写父类的方法

public override void Eat()

{

// base指向的是父类,表示调用父类的Eat方法

base.Eat();

// TODO: 下面是子类自己的逻辑

}

}

public class Test : MonoBehaviour

{

private void Start()

{

Animal a = new Cat();

a.Eat(); // 重写了父类的方法,调用了子类的Eat方法

}

}

```

#### 练习1:鸭子嘎嘎叫

【需求】真的鸭子嘎嘎叫,木头鸭子吱吱叫,橡皮鸭子唧唧叫。

```

public class Duck

{

public virtual void Scream()

{

Debug.Log("嘎嘎叫");

}

}

public class WoodDuck : Duck

{

public override void Scream()

{

Debug.Log("吱吱叫");

}

}

public class RuberDuck : Duck

{

public override void Scream()

{

Debug.Log("唧唧叫");

}

}

public class Test : MonoBehaviour

{

private void Start()

{

Duck[] duck = new Duck[] {new Duck(), new RuberDuck(), new WoodDuck()};

for (int i = 0; i < duck.Length; i++)

{

duck[i].Scream();

}

}

}

```

#### 练习2:员工打卡

【需求】所有员工9点打卡,但经理十一点打卡,程序员不打卡。

```

public class Worker

{

public virtual void ClockIn()

{

Debug.Log("9点打卡");

}

}

public class Manager : Worker

{

public override void ClockIn()

{

Debug.Log("11点打卡");

}

}

public class Programmer : Worker

{

public override void ClockIn()

{

Debug.Log("不打卡");

}

}

```

#### 练习3:图形

【需求】创建一个图形类,有求面积和周长两个方法,创建矩形类,正方形类,圆形类继承图形类,实例化矩形、正方形、圆形对象求面积和周长。

#### 4. 动态绑定与静态绑定

* 绑定:系统确定一个类型能调用哪些方法的过程

* 比如:Animal类、Dog类、BigDog类,三层继承,当创建BigDog类时,会去调用Dog类的构造函数,而Dog类会去调用Animal类的构造函数,确定这些构造函数的先后顺序,就是绑定

* 静态绑定(编译时绑定):调用关系是在运行之前绑定的

* 动态绑定(运行时绑定):调用关系是在运行过程中绑定的

* 静态绑定在编译时绑定,不占用运行时间,所以调用速度比动态绑定块

* 动态绑定在运行时绑定,会占用运行时间,但是<span style="color:red">灵活性高</span>,速度比静态慢

* 方法隐藏是静态绑定

* 方法重写是动态绑定

### 5. 抽象类和抽象方法

#### 抽象类

【特点】

1. 抽象类不能创建对象(实例化),但可以被继承

2. 抽象类可能包含抽象成员(可以有,也可以没有)

希望做基类,能够对多个类进行统一管理

比如:动物类就可以作为抽象类,它需要管理猫类、狗类、兔子类等

#### 抽象方法

【定义】只声明了定义,没有实现的,就是抽象方法

【特点】

1. 实现类必须实现所有抽象方法

2. 抽象方法必须放在抽象类和接口中

3. 放在抽象类的抽象方法必须加abstract,实现类实现抽象方法的方法,必须加override

### 接口

【定义】使用interface创建的数据类型,接口名建议用大写“I”开头

> 接口是抽象的,接口是规范。实现类必须实现接口的所有成员

【接口的用途】

1. 扩展一个已经有的类的行为。

如:手机5个功能,又发明了一个功能。在不修改原本的代码时,需要为这个功能添加一个接口

2. 提取不同类别的共性行为,让这个行为实现最大限度的复用

比如:鸟类:小鸟,老鹰,鸵鸟。昆虫:蝴蝶,蜻蜓。小鸟和蝴蝶都有飞的行为,所以可以添加一个飞的接口。

#### 接口的实现规范

1. 不包含成员变量

2. 只包含方法、属性、索引器、事件

3. 成员不能被实现

4. 成员可以不用写访问修饰符,不能是私有的

5. 接口不能继承类,但是可以继承另一个接口

```

interface IFly

{

void Fly(); // 方法

string Name // 属性

{

get;

set;

}

int this[int index] // 索引器

{

get;

set;

}

event Action doSomthing; // 事件

}

```

#### 显示实现接口

当一个类继承两个接口,但是接口中存在着同名方法时

注意:显示实现接口时 不能写访问修饰符

```

interface IAtk

{

void Atk();

}

interface ISuperAtk

{

void Atk();

}

class Player : IAtk, ISuperAtk

{

//显示实现接口 就是用 接口名.行为名 去实现

void IAtk.Atk()

{

}

void ISuperAtk.Atk()

{

}

public void Atk()

{

}

}

```

#### 练习1:登记注册

【需求】人、汽车、房子都需要登记,人需要到派出所登记,汽车需要去车管所登记,房子需要去房管局登记,使用接口实现登记方法

```csharp

interface IRegister{

void register();

}

class People:IRegister{

void register(){}

}

class Car:IRegister{

void register(){}

}

class House:IRegister{

void register(){}

}

```

#### 练习2:飞

【需求】麻雀、鸵鸟、企鹅、直升机、天鹅。直升机和部分鸟能飞,鸵鸟和企鹅不能飞,企鹅和天鹅能游泳,除直升机,其他都能走。

```csharp

interface Ifly{

void fly();

}

interface ISwim{

void swim();

}

interface IWalk{

void walk();

}

class Sparrow:Ifly,IWalk{

public void fly(){}

public void walk(){}

}

class Ostrich:IWalk{

public void walk(){}

}

class Penguin:ISwim, IWalk{

public void walk(){}

public void Swim(){}

}

class Helicopter:Ifly{

public void fLy();

}

class Swan:Ifly,ISwim,IWalk{

public void fly();

public void swim();

public void Walk();

}

```

### 密封方法