面向对象三大特性
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();
}
箭头语法
方法简化
// 传统方法
public int Add(int a, int b)
{
return a + b;
}
// 箭头语法简化
public int Add(int a, int b) => a + b;
只读属性
// 传统属性
public string FullName
{
get { return $"{FirstName} {LastName}"; }
}
// 箭头语法简化
public string FullName => $"{FirstName} {LastName}";
构造函数
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();
}
```
### 密封方法
评论区