这是「C# 学习加固」系列的第 4 篇,共 5 篇;可通过文末的系列导航继续阅读。
31. 万物基类Object#
如果class不设置基类,那么他的基类就是object,同时object下有几个虚函数可以重载,这里重点先关注ToString()
因为万物基类都是object,所以任何类型的对象都可以隐式转换为object
ToString()#
我们用Debug.Log函数时是可以不填写字符串的,那么函数其实会自动调用对象的ToString函数
32. 命名空间#
分文件编写#
我们的类、结构体、枚举等可以放一个文件里面写,也可以分开,理论上没有强制要求,但是要考虑有后续的维护。大多情况下一个类型一个文件
命名空间#
我们会对事物的数据和行为进行封装,这个过程产生类,但是有没有一种可能,我们将一系列目标较为相同的类也打包在一起呢?
这就是命名空间,比如我们去下载的一些插件,一般也是有专门的命名空间
是许多编程语言使用的一种代码组织的形式,通过命名空间来分类,区别不同的代码功能,避免不同的代码片段(通常由不同的人协同工作或调用已有的代码片段)同时使用时由于不同代码间变量名相同而造成冲突
namespace Animal // 包裹的内容归属这个命名空间
{
public abstract class AnimalObject
{
public string name;
public override string ToString()
{
return $"我是动物:{name}";
}
}
}namespace Animal // 包裹的内容归属这个命名空间
{
public class Dog : AnimalObject
{
}
}using Animal; // 在这个文件引入命名空间
using UnityEngine;
public class Hello : MonoBehaviour
{
void Start()
{
AnimalObject animal = new Dog();
}
}33. 接口#
大家来想象一种需求
- 人、鹦鹉、机器人三种类型,他们的共同特点是都会说话
- 分类上,我们不可能将他们分为一类,因为除了会说话,没有任何其他的共同特点
- 我们有一个公用函数,让会说话的对象进行说话
上面这种需求其实不相关的类型但是有共同特点,应该如何解决?
接口#
C#中的继承是单继承,也就是一个class只能继承一个基类,但是无论是否有继承某个class,都可以进行多个接口的继承
接口:抽象0个或多个函数,定义的函数一定是抽象函数,一定需要派生类进行实现
一个对象,首先是它自身的类型,其次可以是它的基类类型,也可以是它的接口类型,但是转换类型后只能使用这个类型的成员
using UnityEngine;
public class Hello : MonoBehaviour
{
void Start()
{
Robot robot = new Robot();
Person person = new Person();
Parrot parrot = new Parrot();
Talk(robot); // 隐式转换
Talk(person); // 隐式转换
Talk(parrot); // 隐式转换
ITalk talker = robot; // 隐式转换
Robot robot2 = (Robot)talker; // 显式转换,
}
// 让某个对象进行说话
public void Talk(ITalk talker)
{
talker.Talk();
}
}public interface ITalk
{
void Talk(); // 实际上是抽象函数,不需要定义访问修饰符,因为没意义,因为如果不公开就不可能被访问到了
}using UnityEngine;
public class Robot : ITalk
{
public void Talk() // 实现接口不需要写override,因为接口方法本身就是抽象的,不需要再声明为抽象
{
Debug.Log("机器人说话了!");
}
}34. 属性#
属性就是将成员变量和成员函数结合
using UnityEngine;
public class Hello : MonoBehaviour
{
void Start()
{
Hero hero = new Hero();
hero.HP = 100;
hero.MP = 50;
hero.HP -= 1;
hero.Hurt();
hero.HP -= 12;
hero.MP -= 35; // 修改属性意味着 访问 属性的 set 方法
Debug.Log(hero.MP); // 获取属性意味着 访问 属性的 get 方法
hero.MP -= 35;
Debug.Log(hero.MP);
hero.FlySpeed = 100; // 访问 FlySpeed 的 set 方法
Debug.Log(hero.FlySpeed); // 访问 FlySpeed 的 get 方法
}
}
public class Hero : Unit
{
public int HP;
private int mp;
// 成员属性
// 属性在外部和变量是类似的
// 属性相当于提供了两个函数,分别是 get 和 set,但是也可以只保留其中一个
// 属性的类型决定了get 和 set 的返回类型
public int MP
{
get
{
Debug.Log("有人在获取MP,我怀疑他要检查能不能放技能");
return mp;
}
set
{
Debug.Log("有人在设置MP,我怀疑他要放技能");
if(value < 0)
{
Debug.LogError("MP不能小于0");
mp = 0;
return; // 直接返回,避免后续代码执行
}
if (value < mp)
{
Debug.Log("MP减少了!MP:"+value);
}
else if(value > mp)
{
Debug.Log("MP增加了!MP:"+value);
}
else
{
Debug.Log("MP没有变化!");
}
mp = value;
}
}
public void Hurt()
{
Debug.Log("我受伤了!" +HP);
}
private int lv;
public override int LV
{
get
{
return lv + 1;
}
}
private int moveSpeed;
public override int MoveSpeed
{
get => 23;
set => moveSpeed = value;
}
}
public abstract class Unit
{
public virtual int LV { get { return 0; }}
public abstract int MoveSpeed { get; set; }
// 简写,相当于提供了默认的成员变量
// 并且自动在get的时返回、set的时赋值,但是无法直接访问这个成员变量,相当于只能通过属性访问
public int FlySpeed
{
get;
set;
// private set; // 保护起来,给get或set单独设置访问修饰符
}
}35. 引用与Null#
引用#
现实中,我们通过人的身份证号来“唯一的”确认一个人的身份,但是我们同样也会用姓名、昵称、代号等方式来表明某个人。
所以所谓引用就是一个别称
大家可以思考一个问题,C#中可以通过函数传递数据,那么到底传递的是什么?这里我们先只考虑class的情况(说明其他类型有所不同,我们之后会探讨其他类型)。这里其实就相当于起了一个别名,我们通过别名获取以及操作的依然是原先的那个对象。
即使不通过函数,C#中我们也可以主动给对象起别名,无论是创建对象时的名称还是另起的别名,这些都是引用
Null空引用#
null意味着空对象,也就是没有数据的对象,任何基于class定义的类型都可以用“object = null”的方式来表明这个对象为“空”、“无效”
只定义对象,但是没有new或者其他引用方式,默认也是null
using UnityEngine;
public class Hello : MonoBehaviour
{
void Start()
{
Hero hero = new Hero("张三");
TestFunction(hero);
Debug.Log(hero.name); // Start中的hero其实就是TestFunction中的unit
Hero hero3 = hero;
Debug.Log(hero3);
// 如果修改hero3,等于修改hero,因为他们指向同一个对象
hero3.name += "_修改一下";
Debug.Log(hero.name); // 输出: 张三_TestFunction_修改一下
Debug.Log(hero3.name); // 输出: 张三_TestFunction_修改一下
hero = null; //让hero无效
Debug.Log(hero3.name); // 如果一个引用 = null,其他引用仍然有效
Debug.Log(hero.name); // 如果对象是null,是不能调用任何成员的,否则会报错,要避免
Hero hero4; // 只有变量,没有实际引用的默认就是null
// Debug.Log(hero4.name); // 这会报错,因为hero4没有被初始化
// 目前的测试只对class类型有效,int、float等值类型不会影响原对象
}
void TestFunction(Hero unit)
{
unit.name += "_TestFunction";
unit = null; // 这只是让unit指向null,不影响原对象
}
}
public class Hero
{
public string name;
public Hero(string name)
{
this.name = name;
}
}36. 引用的比较#
class类型的相等(==)或不等(!=)对比,实际上就是比较引用是否相同(引用所指向的对象是否相同)
同时对象对比时null也可以参加,因为对象可能为null,也可以和null做对比
object.RefrenceEquals#
我们也可以用object.RefrenceEquals 这个函数来比较两个引用指向的对象是否相等,我们也可以采用“==”的方式来比较引用
C#中允许重载运算符,如果一定要看它们的引用是否相等,要用object.ReferenceEquals
重写Equals函数对比#
object中有个虚函数,Equals(),可以用来自定义对比结果,注意此时并不是对比引用是否相同,而是“我们认为是否相同”
只是逻辑相等,和引用无关
using UnityEngine;
public class Hello : MonoBehaviour
{
void Start()
{
int num1 = 3;
int num2 = 3;
Debug.Log(num1 == num2); // True,值类型只比较数值是否相等
Hero hero1 = new Hero("张三");
Hero hero2 = new Hero("张三");
// 引用的比较
//
Debug.Log(hero1 == hero2); // False,虽然他们的成员相等,但是不是一个对象
if (hero1 != null) // 如果英雄存在(是否不为null),则可以XXX
{
}
// object.ReferenceEquals方法比较两个对象是否是同一个对象
Debug.Log(object.ReferenceEquals(hero1, hero2)); // False
Debug.Log(object.ReferenceEquals(null, null)); // True
Debug.Log(object.ReferenceEquals(hero1, null)); // False,hero1不是null
// 类.Equals()实际上就是objectade函数,静态函数(后续学习)
Debug.Log(Hero.Equals(hero1, hero2));
Debug.Log(Hero.Equals(null, null));
Debug.Log(Hero.Equals(hero1, null));
// 对象.Equals()
Hero hero3 = hero1;
Hero hero4 = new Hero("李四");
Hero hero5 = new Hero("张三"); // 不同对象但是逻辑上同名
Debug.Log(hero1.Equals(hero3));
Debug.Log(hero1.Equals(hero4));
Debug.Log(hero1.Equals(hero5)); // True
}
}
public class Hero
{
public string name;
public Hero(string name)
{
this.name = name;
}
// 我们就强行要求,同名且同类型就是相等的(当然它们实际上还是两个对象,但是我们希望得到相等的结果)
public override bool Equals(object obj)
{
Hero temp = obj as Hero;
// 如果这个obj不是Hero类型,会转换失败,temp将为null
// 既然类型都不相同,当然不相等
if (temp == null)
{
return false;
}
else //else说明转换成功了,obj是 Hero 对象
{
return this.name == temp.name; // 如果名字相同,则认为是相等的
}
}
}37. 值类型与引用类型#
值类型#
int a = 3;
int b = a;
在b=a时,会重新开辟内存空间并完整的复制一次a的数据,相当于两者无关,只是在复制的时两者数据相等而已
主要的值类型:枚举、结构体、int、float、bool等(string不是值类型,它是特殊的引用类型)
比较时(相等/不等)一般是判断两者的值是否相同
结论:值类型在赋值时都是拷贝值,和原先的变量无关、
引用类型#
Dog dahuang = new Dog();
Dog dog = dahuang;
在dog = dahuang时,dog会引用dahuang的内存地址,相当于两者是一个对象,只是名称不一样
主要的引用类型:class、interface
比较时(相等/不等)一般是判断两者是否引用同一个对象
结论:引用类型在赋值时都是拷贝引用,和原先的变量是同一个对象
为什么需要这种区别?#
值类型都是轻量级数据,用完就放弃(销毁、回收),在C#中一般过了的作用域就回收掉。但是会有传递的需求,因为是轻量级数据,复制成本也不高,所以采用这种方案。
想对的,引用类型一般是重量级数据,那么它的复制成本就高。不适合采用值类型的方案。但是这也不意味着引用类型就不在销毁,当一个对象再也没有人引用时会被标记为“无用”状态等待系统自动的回收,但是这个回收是有成本
如果一个class中有一个成员变量是int、float这类值类型,会怎么样?此时值类型的作用域就在这个类产生的某个对象内,所以这个对象没有被回收(销毁)也就不会回收,但是即使是class中的值类型,传递时依然采用的是完全拷贝的形式
class中的值类型:修改对象内的成员都要用对象名.成员名称去做到
using UnityEngine;
public class Hello : MonoBehaviour
{
void Start()
{
int a = 3;
int b = a;
b = 4; // 此时a依然=3
Test(b); // Test(int num = 4) 函数调用的过程也是复制
// hero1和hero2其实是一个对象的引用
Hero hero1 = new Hero("张三",4);
Hero hero2 = hero1;
hero1 = null;
hero2 = null; // 此时张三这个英雄相当于无名,无名就意味着无用,等待销毁
TestHero(hero1);
SetHeroLv(hero1.friendHero);
Test(hero1.lv); // 相当于Test函数中的num= hero1.lv,但是两者依然无关联
// 引用类型的变量在除了它的有限范围(作用域)时候会失效,但是不意味着他引用的对象也失效
}
void SetHeroLv(Hero hero)
{
hero.lv = 10086;
// class中的值类型:修改对象内的成员都要用对象名.成员名称去做到
// 引用类型,因为指向引用的对象,所以修改对象内的成员变量,直接修改即可
}
void Test(int num)
{
int num2 = -1;
for (int i = 0; i < 100; i++)
{
num2 = i;
if (i==88)
{
SaveNum(i); // 复制给SaveNum函数中的num
}
}
}
private int num;
void SaveNum(int num)
{
this.num = num; // this关键字指向当前类的成员变量
}
void TestHero(Hero hero)
{
Debug.Log(hero.name);
}
}
public class Hero
{
public string name;
public int lv;
public Hero friendHero;
public Hero(string name, int lv)
{
this.name = name;
this.lv = lv;
}
}