跳过正文
  1. 文章/

C#学习加固笔记

Lickba
作者
Lickba
一个热爱游戏的玩家
目录

C#学习加固笔记
#

1.注释以及 log
#

using UnityEngine;

// 注释是写给人看的
public class Hello : MonoBehaviour
{
    void Start()
    {
        Debug.Log("这是一个普通日志。");
        Debug.LogWarning("这是一个警告日志。");
        Debug.LogError("这是一个错误日志。");
    }
}

2.变量以及常用类型
#

  1. 变量是什么?

    • 数据的抽象表示,类似数学中的变量
    • 可以变化的,值可以变
    • 除了整数、小数等以外还可以表示玩家的攻击力,生命值,等级,游戏昵称或者其他我们自定义的信息
  2. 常用的变量类型
    start(字符串):用来存储用户名、密码、角色名称、技能名称等文本信息

    int(整数):存角色等级、纯数字ID等

    float(浮点数):小数,用来存时间这类对精度要求更高的数值

    整数、浮点数等数值类型都有取值范围,但是大多情况下以及目前情况下,可以忽视这个问题,感兴趣点链接了解:

    https:/learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/builtin-types/integral-numeric-types

  3. 变量的申明
    类型 变量名 = 变量初始值;

  4. 驼峰命名法

    • 大驼峰:首字母大写,之后每个单次的首字母也大写,其余字母小写,如:GameOver、Home、MyGameID
    • 小驼峰:首字母小写,之后每个单次的首字母也大写,其余字母小写,如:gameOver、home、myGameID
  5. 命名约束

    • 首字母不能是数字,但是后续可以是数字
using UnityEngine;

public class Hello : MonoBehaviour
{
    private void Start()
    {
        //变量的声明:类型 变量名 = 初始值;
        string myName = "Lickba"; //字符串的使用,需要加双引号
        int age = 18;           //非字符串的使用,不需要加引号
        float num = 15.654787354f; //浮点数的使用,后面加f表示这是一个浮点数

        //使用或输出变量的时候,不需要加双引号,f等
        Debug.Log(myName);
        Debug.Log(age);
        Debug.Log(num);
    }
}   

3.转义字符
#

  1. 转义字符

    转义字符\
    单引号'
    双引号"
    退格符 (unity不支持)\b
    换页符(unity不支持)\f
    换行符\n
    回车\r
    表格 水平\t
    表格 垂直\v
  2. 原义符

    用来取消转义字符的特殊意义,要写在双引号外面

    using UnityEngine;
    
    public class Hello : MonoBehaviour
    {
        private void Start()
        {
            // 转义字符是针对字符串的
            string myName = "张三\t李四\t王五";
    
            // 特殊需求:我们希望输出 双引号
            // 程序认为 双引号是特殊符号,包括起来的就是字符串
            // \:转义字符,它本身也是一个特殊符号
            string str = "\"";
            Debug.Log(myName);
    
            // 输出转义字符的方式,就是用转义字符去修饰它
            Debug.Log("\\\\");
    
            // @:原义符,取消转义字符的作用
            // 不能直接输出双引号,两个双引号会被认为是一个双引号
            string names2 = @"张三\t李四\t王五";
            string names3 = @"张三\t李四\t王五""";
            Debug.Log(names2);
            Debug.Log(names3);
        }
    }   
    

4.字符串拼接
#

+号拼接

$号格式化

using UnityEngine;

public class Hello : MonoBehaviour
{
    private void Start()
    {
        // +号拼接
        // 我是AA,我今年18岁

        string name = "AA";
        int age = 18;
        // int类型和字符串相加时,int会自动转换为字符串
        string info = "我是" + name + ",我今年" + age + "岁";
        Debug.Log(info);

        // $格式化输出,用{}去包裹变量
        // $格式化输出时,@符号会对整体进行取消转义
        string info2 = $"我是{name},我今年{age}岁";
        Debug.Log(info2);
    }
}   

5.变量的类型转换
#

  1. 重新学习变量的定义方式

    • 定义但是没有赋值,这种情况下无法使用变量

    • 定义并且赋值

    • 同类型变量批量定义

      • 全部赋值

      • 部分赋值

      • 全部不赋值

        // 批量定义,只需要写一次类型,变量名用逗号分隔
        int num1 = 1, num2 = 2, num3 = 3;
        
  2. =式的理解

    • 等式右边的结果会赋值给左边,所以左边只能是变量,不能是数值
    • 等式两边类型一直,不能int a = “123”
    • 变量可以在等式两边任何一边
  3. 数值也有类型

    C#内置的类型背后都有一个实际类型,鼠标放在类型关键字或数值上都能看到

    • string: String
    • int: Int32
    • float: Single
int age = 25;
Int32 age2 = 30; // Int32 等于 int
  1. 类型的转换方式

    • 隐式类型转换:无需人为干涉的自动转换

      1. 类型上的兼容

      2. 精度小的转为精度大的

        int num = 10;
        float fnum = num; // 把num的数值转为float类型
        
    • 显式类型转换:人为干涉的强制转换

      1. 类型上的兼容

      2. 如果将精度大的转为精度小的,会丢失那些无法保存的精度

         // 显式转换,丢失精度
         float fnum3 = 10.01f;
         int num3 = (int)fnum3; // 将float转换为int,丢失小数部分
        
    • Convert强行转换(支持不兼容的类型)

      本身对放进去的变量类型没什么限制,但是如果完全不具备转换的可能性,会报错

      • 转换为Init: Convert.ToInt32

      • 转换为float: Convert.ToSingle

      • 转换为string: Convert.ToString

         Convert.ToInt32("123");
         Convert.ToSingle("1233.45");
         Convert.ToString(123);
        
  2. 字符串转换

     // ToString(),转为字符串
     int age = 19;
     string ageStr1 = age + ""; // 不推荐的做法
     string ageStr2 = age.ToString(); // 推荐的做法
    

6.作用域和常量
#

作用域
#

C#中用大括号{}表示语句块、层次关系

而变量具备一个有效范围,这个范围就是他的作用域

  1. 变量只能在定义之后使用

  2. 变量只在它所在的“范围内有效”,这个范围可以简单的理解成大括号{}内

  3. 但是大括号{}有嵌套的情况时

    “父级”的变量可以用在“子级”中

    “子级”的变量不可以用在父级中

常量
#

  1. 常量是固定的值,程序运行时不会修改的值,比如圆周率π就不会因任何原因而修改

  2. 非常类似变量,但是它在定义时必须赋值,其他时刻无法改变

  3. 常量同样有作用域

     const float pi = 3.1415926f;
    

7.算数运算符
#

运算符描述
+
-
*
/
%取余
++自增1
自减1

前++、后++的区别(–同理)

int a = 1;
int b = a++; // b = 1

int a = 1;
int b = ++a; // b = 2

前++是先自加再使用

而后++是先使用再自加

8.赋值运算符
#

运算符描述
=等号
+=A+=B相当于A = A + B
-=A-=B相当于A = A - B
*=A*=B相当于A = A * B
/=A/=B相当于A = A / B
%=A%=B相当于A = A % B
int a = 3;
a += 4; // 7  等同于a = a + 4;
a -= 2; // 5  等同于a = a - 2;
a *= 2; // 10 等同于a = a * 2;
a /= 5; // 2  等同于a = a / 5;
a %= 3; // 2  等同于a = a % 3;

Debug.Log(a); // 输出结果为2

9.bool类型与关系运算符
#

bool:布尔类型一般用来描述“是与否”、“真与假”、“能与不能”、“正确与错误”等情况

有两个值:

  1. True:代表是
  2. False:代表否

10.关系运算符
#

运算符描述
==检查两个值是否相等
!=检查两个值是否不相等
>左边是否大于右边
<左边是否小于右边
>=左边是否大于或等于右边
<=左边是否小于或等于右边

11.逻辑运算符
#

运算符描述
&同时,左右都是true返回true,一个为false就返回false
|或,左右都是false返回false,一个为true就返回true
&&同时,左右都是true返回true,一个为false就返回false
||或,左右都是false返回false,一个为true就返回true
!取反,true转为false,false转为true

&:实际上是按位与,但是目前如果深究这块,会比较复杂,所以可以按照上面的理解

|:实际上是按位或,但是目前如果深究这块,会比较复杂,所以可以按照上面的理解

&&、||都会发生短路机制,也就是如果左边已经为false则不会执行右边,实际开发中使用较多

短路机制:一但确定了结论,后面就不执行,确定的顺序是从左往右(性能会更好)

 string userName = "User2";
 string password = "Password123";

 bool checkUserName = userName == "User";
 bool checkPassword = password == "Password123";

 bool online = true; // 当前网络在线

 bool canLogin = checkUserName & checkPassword & online; // 能登录 = 用户名正确 同时 密码正确
 // bool canLogin = (checkUserName & checkPassword) & online;
 Debug.Log($"玩家可以登录: {canLogin}");

 bool admin = true; // 是否是管理员
 bool adminCanLogin = checkUserName | admin; // 管理员可以登录 = 用户名正确 或者 是管理员
 Debug.Log($"管理员可以登录: {adminCanLogin}");

 Debug.Log(!false);
 Debug.Log(!true);
 // ! 是逻辑非运算符,用于取反布尔值。
 // 它会将 true 变为 false,将 false 变为 true。

// &  // 按位与运算符
// && // 逻辑与运算符
// |  // 按位或运算符
// || // 逻辑或运算符
// & 跟 && 的区别:
// & 是按位与运算符,会对每一位进行比较,只有当两个操作数的对应位都为1时,结果才为1。
// && 是逻辑与运算符,只有当两个操作数都为true时,结果才为true,并且如果第一个操作数为false,则不会计算第二个操作数(短路求值)。
// | 跟 || 的区别:
// | 是按位或运算符,会对每一位进行比较,只要有一个操作数的对应位为1,结果就为1。
// || 是逻辑或运算符,只要有一个操作数为true,结果就为true,并且如果第一个操作数为true,则不会计算第二个操作数(短路求值)。

12.三目运算符
#

也被称为条件运算符

三目运算符并不是一种必要的运算符,是为了方便

结果 = 如果条件为真 ?则为X :否则为Y

image

int value = -80;
int num = value < 0 ? 0 : value; // 如果num小于0,则num赋值为0,否则不变
Debug.Log(num);

// 最多是100
int num2 = value > 100 ? 100 : value; // 如果num大于100,则num2赋值为100,否则num2赋值为num
Debug.Log(num2);

// 控制在0到100之间
int num3 = value < 0 ? 0 : value;
num3 = num3 > 100 ? 100 : num3;
Debug.Log(num3);

13.if判断语句
#

if(如果):用来判断条件是否成立,即是否为true

分支语句:不能单独使用

else if (其他可能性):if不成立,但是满足我的条件,则执行。

else:以上条件都不满足则执行

注意事项
#

  1. 短路机制,if如果执行成功则不会执行else if 或 else 的语句,短路机制不只是不执行{}中的代码,判断条件本身也不会运行(因为判断条件有时候本身是有计算过程的)
  2. if语句中大括号也存在变量的作用域问题

14.枚举类型
#

特别适合用来描述状态、阶段

当某个数据可以预期有确定性的几种结构时,我们可以使用枚举类型来保存,比如

  1. AI:待机、攻击、死亡

  2. 时间:周一、周二、周三

  3. 游戏状态:选择英雄、加载界面、战斗中、游戏结束

枚举是一种由我们自己去创建的类型,也是我们首次去创建我们自己的类型

  1. 定义枚举类型,一般我们会将它定义在class同级中或单独新建一个脚本文件去保存,不要定义在函数中

  2. 定义这个枚举类型的变量

  3. 使用这个变量

     /// <summary>
    /// AI状态
    /// </summary>
    enum AIState
    {
        /// <summary>
        /// 待机
        /// </summary>
        Idle,
        /// <summary>
        /// 攻击
        /// </summary>
        Attack,
        /// <summary>
        /// 死亡
        /// </summary>
        Die = 1000 // 自定义状态对应的值
    }
    

15.Switch语句
#

switch语句和if else if 非常接近,甚至理论上不存在也没有任何关系,不过是特定情况下会更加方便

语法:
#

image

对比if语句
#

if else if的特点是判断的因素可以完全不相关,只要条件都是bool类型即可,比如:

if(3==3)
{

}
else if (a2 > a3)
{

}

但是Switch一般用在围绕一个因素去做判断,比如当前是星期几,AI处于什么状态,所以和枚举类型特别搭配

break
#

并不是每一个case都需要break,如果case 语句后面没有要执行的代码,则可以不包含 break 关键字, 这时程序会继续执行后续的case语句,直至遇到break关键字为止;

这种情况的目的一般在于,如果我们希望当AI状态为巡逻、待机时候要做的事情是一样的

image

16.for循环
#

一般用于循环固定次数的情况

语法
#

image

运行步骤:
#

  1. 初始化代码,只会执行一次,也可以不写

  2. 判断条件是否满足

    1. 满足则执行{}中的主代码片段

      • 执行更新时间
      • 判断条件,满足则继续执行…(后续循环)
    2. 不满足则退出循环

// 案例:1+2+3+4+5....+100
int res = 0;
for (int i = 1; i <= 100; i++)
{
    res += i; // 将i的值累加到res中
    Debug.Log($"i = {i}, res = {res}");

}
Debug.Log($"1+2+3+4+5....+100 = {res}");

17.while循环语句
#

基于条件的循环,也就是条件满足就一直循环

// 避免死循环
// 从1加到100
int num = 1;
int res = 0;
while (num<=100)
{
    res += num;
    num++;
    Debug.Log($"当前num={num}, res={res}");
}
Debug.Log(res);

还有一个变种 do while,但是用的少

// 至少执行一次
do
{
    // 首先执行一次
    // 循环体
    Debug.Log("至少执行一次");
} while (false); // 如果条件满足才会执行下一次

18.函数的基本使用
#

函数的基本使用
#

函数一般也叫方法,将可以复用的代码包装起来,方便之后调用

image

private void Start()
{
    int num = 100;
    Function(num,"Test"); //调用函数
    Debug.Log($"实参num = {num}"); //输出 num 的值

    int num2 = 33;
    num2 = Add(num2, 333);
    Debug.Log($"Add(100, 333) = {num2}"); // 输出 Add 函数的返回值

}

// 函数的参数名称理论是无所谓是什么的,但是还是要见名知意
// 函数的参数叫做形参,也就是形式参数,和外部调用处的函数变量名称没有关系
// 实参和形参没有关系:一方改变不影响另外一方,相当于变成了两个类型一致但是数值没有关联的变量
void Function(int num,string info)
{
    num += 200;
    for (int i = 0; i <= 100; i++)
    {
        //Debug.Log($"{i}");
    }
    Debug.Log($"形参num : {num}");
}

// 一个返回值类型为int的函数
int Add(int a, int b)
{
    int res = a + b;
    return res; // return的结果要跟函数的返回值类型一致
}
void Do()
{
	return; //return意味着不执行之后的逻辑
}

19.函数重载
#

同名函数不同参数称之为重载

private void Start()
{
    int res = Add(1, 2);
    int res2 = Add(1, 2, 3);
    int res3 = Add(1, "2");
}

int Add(int a, int b)
{
    int res = a + b;
    return res; // return的结果要跟函数的返回值类型一致
}

// int.Parse:转为int类型
int Add(int a, string b)
{
    int res = a + int.Parse(b);
    return res;
}

int Add(int a, int b, int c)
{
    int res = a + b + c;
    return res;
}

20.函数的ref和out关键字
#

ref:通常情况下,形参和实参只是值一样,但是并不是同一个变量。如果通过ref则可以视为一个变量

out:函数的返回值只有一个,如果希望有两个及以上,则可以使用

函数的参数可以有默认值,在定义的时候直接=即可

private void Start()
{
    int a = 3;
    RefFunction(ref a);    // 实参
    Debug.Log($"实参: {a}");

    // 需求:
    // 1.请求登录,提供一个账号
    ///2.如果有此账号,返回true,并且返回密码
    if (CheckLogin("user123", out string password)) // 相当于定义在if的外面
    {
        Debug.Log($"登录成功,密码是:{password}");
    }
    else
    {
        Debug.Log("登录失败,账号不存在");
    }

    TestFunction(100,200);
    TestFunction(10);
    TestFunction(1);
}

void RefFunction(ref int a)    // 形参
{
    a += 1000;
    Debug.Log($"形参: {a}");
}

/// <summary>
/// 检查登录
/// </summary>
/// <param name="account">用户名</param>
/// <param name="password">密码</param>
/// <returns>是否登录成功</returns>
bool CheckLogin(string account, out string password)
{
    // 模拟账号和密码
    if (account == "user123")
    {
        password = "password123"; // 返回密码
        return true; // 登录成功
    }
    else
    {
        password = null; // 无效账号
        return false; // 登录失败
    }
}

// 测试可选参数(默认参数)
// 如果你传递,则使用传递的值;如果不传递,则使用默认值
void TestFunction(int c,int a = 10099, int b = 9999)
{
    Debug.Log(a+b+c);
}

21.结构体
#

结构体是一种自定义类型,用于创建我们游戏或者实际业务中的自定义类型

比如Moba游戏中的英雄角色,那必然存在英雄的血量、攻击力、等级等关键属性,这些属性我们可以用int、float等类型来描述,但是往往我们需要打包来使用,因为我们往往需要“这个英雄的攻击力”、“这个英雄的防御力”、我们会可以抽象出“英雄”这个数据概念。所以我们可以创建一个“英雄类型”。

同时也为我们之后学习非常重要的“类”做一个铺垫

using UnityEngine;

public class Hello : MonoBehaviour
{
    private void Start()
    {
        Hero tanke = new Hero(); // 构造一个结构体
        tanke.Name = "超级坦克"; // 修改结构体的成员变量
        Debug.Log($"英雄: {tanke.Name}");
        tanke.Show(); // 调用结构体的成员函数

        Hero tanke2 = new Hero("X刺客", 30, 500); // 构造另一个结构体
        tanke2.Show(); // 调用另一个结构体的成员函数
    }

}
/// <summary>
/// 英雄结构体
/// </summary>
struct Hero
{
    // 成员
    // 成员字段(变量)
    // 访问修饰符
    // 1. public: 公共的,可以被外部访问
    // 2. private: 私有的,只能在结构体内部访问
    public string Name; // 默认值是 null
    public int Attack;  // 成员存在默认值,数值类型(int, float, double)默认值是 0
    public int Hp;

    // 默认有一个无参构造函数
    // 构造函数
    public Hero(string name, int attack, int hp)
    {
        Name = name;
        Attack = attack;
        Hp = hp;
    }

    // 成员函数
    public void Show()
    {
        Debug.Log($"英雄: {Name}, 攻击力: {Attack}, 生命值: {Hp}");
    }
}

22.结构体使用细节
#

  1. 构造函数也可以重载

  2. this关键字

    代表着当前对象,也就是在外部声明的变量

  3. 访问修饰符详解

    private修饰的成员外界无法访问,只能在结构体内部使用

    public修饰的成员,外界和内部都可以随意使用

using UnityEngine;

public class Hello : MonoBehaviour
{
    private void Start()
    {
        Hero tanke2 = new Hero("X刺客", 30, 500); // 构造另一个结构体
        tanke2.Show(); // 调用另一个结构体的成员函数

        HeroClass mage = new HeroClass("法师", 50); // 构造一个类
        mage.Show(); // 调用类的成员函数
        // 结构体和类的区别
        // 1. 结构体是值类型,类是引用类型
        // 2. 结构体的实例在栈上分配内存,类的实例在堆上分配内存
        // 3. 结构体的实例在赋值时会复制所有成员,类的实例在赋值时只复制引用
        // 4. 结构体不能继承其他结构体或类,但可以实现接口
    }
}
/// <summary>
/// 英雄结构体
/// </summary>
struct Hero
{
    // 成员
    // 成员字段(变量)
    // 访问修饰符
    // 1. public: 公共的,可以被外部访问
    // 2. private: 私有的,只能在结构体内部访问
    public string Name; // 默认值是 null
    public int Attack;  // 成员存在默认值,数值类型(int, float, double)默认值是 0
    public int Hp;

    // 默认有一个无参构造函数
    // 构造函数
    public Hero(string Name, int attack, int hp)
    {
        // 同名变量的就近原则,就近:定义的位置
        Name = Name;
        this.Name = Name; // this 关键字指代当前结构体实例
        Attack = attack;
        Hp = hp;
    }

    public Hero(string name,int attack, int hp,int attackBuff)
    {
        Name = name;
        Hp = hp;
        Attack = attack + attackBuff; // 攻击力加上攻击增益
    }

    // 成员函数
    public void Show()
    {
        Debug.Log($"英雄: {Name}, 攻击力: {Attack}, 生命值: {Hp}");
    }

}

/// <summary>
/// 英雄类
/// </summary>
public class HeroClass
{
    public string Name;
    public int Attack;

    public HeroClass(string name, int attack)
    {
        Name = name;
        Attack = attack;
    }
    public void Show()
    {
        Debug.Log($"英雄: {Name}, 攻击力: {Attack}");
    }
}

23.面向对象概念
#

面向对象是最主流的编程思想

核心就是把相关数据和行为组织成一个整体来看待

比如我们学习的结构体,我们将英雄的数据(攻击力、生命值等)与英雄的行为(移动、攻击等)包装到一个结构体中,这个过程就是抽象

其中“英雄”是类型,而具体的某个英雄“大法师”是对象,所以我们下节课就会学习非常重要的概念:“类与对象”

所以最重要的两件事情要明白:

  1. 分类:对游戏中很多食物进行分类包装,比如小兵、英雄等等,大多情况下我们将较为相同的单位归为同一个类型
  2. 对象:生活中“狗”是一个类型,而具体的“大黄”才是实体,也就是对象,所以生成对象也叫“实例化”

24.类与对象基本语法
#

和结构体非常类似,但是struct要改为class,后期我们会详细介绍结构体与类的区别

25.继承
#

封装
#

将事物的数据、行为抽象成类,这个过程就是封装。并且我们自由的决定哪些成员对外公开或隐藏,所以上节课我们的行为其实叫封装

也可以理解为:类的产生过程就是封装

继承
#

  1. 继承的核心意义是在结构上获得父类中的所有成员

    比如,动物都可以吃,猫、狗等继承动物后,也具备了吃的能力

  2. 父类也叫基类,子类也叫派生类

  3. 支持多层继承,但是一个类只能有一个父类(比如动物-狗-柯基)

26.继承后的构造函数
#

*基类中存在的构造函数,子类必须也做对应的实现

this
#

  1. this表示当前类型的对象

base
#

  1. base也是当前类型的对象
  2. 但是在程序上他的类型的基类,所以一般用来专门访问基类成员的
  3. 一般情况下,是不需要用base的,因为默认就可以访问基类成员

*但是实际上在执行上this和base是同一个对象

using UnityEngine;

public class Hello : MonoBehaviour
{
    private void Start()
    {
        Animal animal = new Animal("动物");
        animal.Name = "小动物";
        animal.Eat();

        Dog daHuang = new Dog("大黄","柯基");
        Debug.Log(daHuang.Name);
        daHuang.Eat();

        Cat xiaoBai = new Cat("小白");
        Debug.Log(xiaoBai.Name);
        xiaoBai.Eat();
    }

    // 基类/父类
    public class Animal
    {
        public string Name;

        public Animal(string name)
        {
            Name = name;
        }

        // 函数也是对象的成员
        public void Eat()
        {
            Debug.Log($"{Name}Eating");
        }
    }

    // 派生类/子类
    public class Dog : Animal // 获得 Animal 的所有属性和方法
    {
        public string dogType;
        // 我们已经约束了,实例化一个Animal必须有名字
        // 但是这里出现了冲突,因为实例化一个Dog却没有名字

        // this:谁调用了这个函数,谁就是this
        // base:当前类的基类
        // this和base都是对象,但是程序上认为这个结构(类)不一样
        // 要调用当前类型的成员,用this
        // 要调用父类的成员,用base,但是一般不太会用,除非需要特别声明是基类中
        public Dog(string name,string dogType):base(name)
        {
            // base.dogType 不能通过base访问当前类的成员
            Debug.Log($"名字:{name},品种:{dogType}诞生了!");
            this.dogType = dogType;
        }
    }

    public class Cat : Animal
    {
        public Cat(string name) : base(name)
        {
        }
    }
}

27.多态-虚函数
#

多态
#

先有封装,才有继承

先有继承,才有多态

多态:字面意思,多种形态,同一操作的不同行为,这里的操作可以理解为函数,举例:

父类类动物中有一个“叫”函数,子类猫,狗当然也可以叫,但是实际上猫狗的叫声并不相同,但是也许他们具备一些共同点,比如张嘴、闭嘴

using UnityEngine;

public class Hello : MonoBehaviour
{
    private void Start()
    {
        Dog daHuang = new Dog("大黄","汪汪汪!","柯基");
        Cat xiaoBai = new Cat("小白","喵喵喵~");
        daHuang.Cry();
        xiaoBai.Cry();
    }

    // 基类/父类
    public class Animal
    {
        public string Name;
        public string CryString;

        public Animal(string name, string cryString)
        {
            Name = name;
            CryString = cryString;
        }

        // 虚函数
        // 1.提供默认逻辑,因为有一些子类不想要重写可以不重写
        // 2.提供可选的逻辑
        public virtual void Cry()
        {
            // 下列是可选的
            // 可选的:就是子类想要就可以要,不想要也可以不要
            Debug.Log($"{Name}张嘴了!");
        }
    }

    // 派生类/子类
    public class Dog : Animal // 获得 Animal 的所有属性和方法
    {
        public string dogType;

        public Dog(string name,string cryString,string dogType):base(name,cryString)
        {
            Debug.Log($"名字:{name},品种:{dogType}诞生了!");
            this.dogType = dogType;
        }

        // 重写
        // 重载:是同名函数,但是不同参数
        // 重写:是同名函数,参数相同,但是实现不同
        public override void Cry()
        {
            Debug.Log($"{Name}开始蓄力!");
            base.Cry(); // 调用基类的 Cry 方法
            Debug.Log($"{Name}:{CryString}");
            Debug.Log($"它是一只狗!");
        }
    }

    public class Cat : Animal
    {
        public Cat(string name,string cryString) : base(name,cryString)
        {

        }
        public override void Cry()
        {
            base.Cry(); // 调用基类的 Cry 方法
            Debug.Log($"{Name}:{CryString}");
            Debug.Log($"它是一只猫!");
        }
    }

}

28.多态-抽象类与抽象函数
#

抽象类
#

抽象:从许多事物中,舍弃个别的、非本质的属性,抽出共同的、本质的属性,叫抽象,是形成概念的必要手段

我们学习了类与对象,过程中我们会发现一些问题:

类是一种结构/概念,对象是一个实体,比如英雄是类,而大魔法师是对象。

我们之前学习的案例中有两种情况:

第一种:我们认为Animal是个类,基于这个类去实例化对象,也就是没有使用继承,是一种比较简单的结构

第二种:Animal是最上层,Cat和Dog继承Animal,最终我们实例化对象时其实是使用Cat、Dog这种类,因为我们认为如果产生一个具体的猫,我们使用猫的结构,但是我们不去直接实例化一个动物,因为动物是一个更加抽象的概念。他就应该停在概念层面。

第二种情况下,实际上是我们依然可以new一个动物,但是往往我们不希望如此,所以我们需要一种技术去避免如此,但是要注意,这种避免不是必然,而是由程序员去决定是否需要避免

避免的方式就是抽象类

避免的方式就是抽象类

抽象类:高度抽象,不需要实例化的类型,但是其子类可以实例化,一般处于继承链的上端,需要实例化对象的类型不能使用抽象类

抽象函数
#

抽象类除了无法直接实例化外,还具备定义抽象函数的功能

抽象函数:我不知道我的子类会怎么实现,所以不会像虚函数那样提供一个基本/默认实现,因为事实上我都不知道我的子类有哪些,但是我认为继承我的一定要有这些功能

先知道语法、特点,后面我们会研究这些东西的妙用

using UnityEngine;

public class Hello : MonoBehaviour
{
    private void Start()
    {
        // Animal animal = new Animal("小动物", "吱吱吱~"); // 不允许实例化抽象类
        KeJiDog daHuang = new KeJiDog("大黄","汪汪汪!","柯基");
        Cat xiaoBai = new Cat("小白","喵喵喵~");
        daHuang.Move();
        xiaoBai.Move();
    }

    // 基类/父类
    // 抽象类
    public abstract class Animal
    {
        public string Name;
        public string CryString;

        public Animal(string name, string cryString)
        {
            Name = name;
            CryString = cryString;
        }

        // 虚函数
        // 1.提供默认逻辑,因为有一些子类不想要重写可以不重写
        // 2.提供可选的逻辑
        public virtual void Cry()
        {
            // 下列是可选的
            // 可选的:就是子类想要就可以要,不想要也可以不要
            Debug.Log($"{Name}张嘴了!");
        }

        // 抽象函数
        // 业务上存在飞翔、跑步、游泳等各种情况,我们并不知道具体的子类如何实现
        // 没有函数体
        public abstract void Move(); // 抽象方法,子类必须实现
}

    // 派生类/子类
    // 第二层,Dog也是抽象类
    // 抽象类不需要实现基类中的抽象函数(我也不知道我下面的子类会怎么实现)
    public abstract class Dog : Animal // 获得 Animal 的所有属性和方法
    {
        public string dogType;

        public Dog(string name,string cryString,string dogType):base(name,cryString)
        {
            Debug.Log($"名字:{name},品种:{dogType}诞生了!");
            this.dogType = dogType;
        }

        // 重写
        // 重载:是同名函数,但是不同参数
        // 重写:是同名函数,参数相同,但是实现不同
        public override void Cry()
        {
            Debug.Log($"{Name}开始蓄力!");
            base.Cry(); // 调用基类的 Cry 方法
            Debug.Log($"{Name}:{CryString}");
            Debug.Log($"它是一只狗!");
        }

        // 重写的是基类中的抽象函数
        public override void Move()
        {
            Debug.Log($"{Name}开始奔跑!");
        }
    }

    public class KeJiDog : Dog
    {
        // 他的构造函数约束是自身基类的
        public KeJiDog(string name, string cryString, string dogType) : base(name, cryString, dogType)
        {
        }
        public override void Cry()
        {
            base.Cry(); // 调用基类的 Cry 方法
            Debug.Log($"抖了抖小短腿!");
        }
        public override void Move()
        {
            base.Move(); // 调用基类的 Move 方法
            Debug.Log($"抖了抖柯基翘臀!");
        }
    }

    public class Cat : Animal
    {
        public Cat(string name,string cryString) : base(name,cryString)
        {

        }
        public override void Cry()
        {
            base.Cry(); // 调用基类的 Cry 方法
            Debug.Log($"{Name}:{CryString}");
            Debug.Log($"它是一只猫!");
        }

        public override void Move()
        {
            Debug.Log($"{Name}开始跳跃!");
        }
    }
}

29.里氏转换
#

在面向对象的程序设计中,里氏转换原则(Liskov Substitution principle)是对子类型的特别定义。它由芭芭拉·拉斯科夫(Barbara Liskov)在1987年的一次会议上名为“数据的抽象与层次”的演说中首先提出。

概念
#

里氏转换主要是:子类和父类的类型转换

  1. 一个猫也是一个动物,一定的
  2. 一个动物可能是一个猫,可能但不一定的

应用场景
#

假设我们提供一个函数,让动物作为参数,然后间接调用动物的Cry函数,我们应该如何实现?

我们要解决的是,我们具体的对象都是Cat、KejiDog等类型,那岂不是每一种类型都要提供一个函数作为重载?

结论
#

  1. 隐式转换,子类转父类,没有风险的,内容一定兼容

  2. 显式转换,父类转子类,有风险,要确保类型兼容

    1. Cat cat = (Cat)animal ,转换过程就可能报错
    2. Cat cat = animal as Cat ,转换过程不会报错,但是使用过程可能会报错
using UnityEngine;

public class Hello : MonoBehaviour
{
    private void Start()
    {
        // Animal animal = new Animal("小动物", "吱吱吱~"); // 不允许实例化抽象类
        KeJiDog daHuang = new KeJiDog("大黄","汪汪汪!","柯基");
        Cat xiaoBai = new Cat("小白","喵喵喵~");

        DoCry(xiaoBai); // 里氏转换-隐式转换,子类转父类
        KejiMove(daHuang); // 传递参数时依然是子类转父类
    }

    public void DoCry(Animal animal)
    {
        Debug.Log("开始叫");
        animal.Cry();
    }

    public void KejiMove(Animal animal) // 此处约定传递过来的参数要么是Animal,要么是Animal的子类通过隐式转换传递过来的
    {
        // 强制转换,会报错
        KeJiDog keji = (KeJiDog)animal; // 显式转换,父类转子类,有风险,要确保类型兼容
        keji.Move(); // 调用子类特有的方法

        //Cat cat = (Cat)animal; // 类型不兼容,会立刻报错
        //cat.Move();

        // as转换,不会报错
        Cat keji2 = animal as Cat; // 安全转换,如果类型不兼容,不会立刻报错,但是返回的是 null
        keji2?.Move(); // 使用安全转换后,调用 Move 方法前需要检查是否为 null
    }

    // 基类/父类
    // 抽象类
    public abstract class Animal
    {
        public string Name;
        public string CryString;

        public Animal(string name, string cryString)
        {
            Name = name;
            CryString = cryString;
        }

        // 虚函数
        // 1.提供默认逻辑,因为有一些子类不想要重写可以不重写
        // 2.提供可选的逻辑
        public virtual void Cry()
        {
            // 下列是可选的
            // 可选的:就是子类想要就可以要,不想要也可以不要
            Debug.Log($"{Name}张嘴了!");
        }

        // 抽象函数
        // 业务上存在飞翔、跑步、游泳等各种情况,我们并不知道具体的子类如何实现
        // 没有函数体
        public abstract void Move(); // 抽象方法,子类必须实现
        

}

    // 派生类/子类
    // 第二层,Dog也是抽象类
    // 抽象类不需要实现基类中的抽象函数(我也不知道我下面的子类会怎么实现)
    public abstract class Dog : Animal // 获得 Animal 的所有属性和方法
    {
        public string dogType;

        public Dog(string name,string cryString,string dogType):base(name,cryString)
        {
            Debug.Log($"名字:{name},品种:{dogType}诞生了!");
            this.dogType = dogType;
        }

        // 重写
        // 重载:是同名函数,但是不同参数
        // 重写:是同名函数,参数相同,但是实现不同
        public override void Cry()
        {
            Debug.Log($"{Name}开始蓄力!");
            base.Cry(); // 调用基类的 Cry 方法
            Debug.Log($"{Name}:{CryString}");
        }

        // 重写的是基类中的抽象函数
        public override void Move()
        {
            Debug.Log($"{Name}开始奔跑!");
        }
    }

    public class KeJiDog : Dog
    {
        // 他的构造函数约束是自身基类的
        public KeJiDog(string name, string cryString, string dogType) : base(name, cryString, dogType)
        {
        }
        public override void Cry()
        {
            base.Cry(); // 调用基类的 Cry 方法
            Debug.Log($"抖了抖小短腿!");
        }
        public override void Move()
        {
            base.Move(); // 调用基类的 Move 方法
            Debug.Log($"抖了抖柯基翘臀!");
        }
    }

    public class Cat : Animal
    {
        public Cat(string name,string cryString) : base(name,cryString)
        {

        }
        public override void Cry()
        {
            base.Cry(); // 调用基类的 Cry 方法
            Debug.Log($"{Name}:{CryString}");
            Debug.Log($"它是一只猫!");
        }

        public override void Move()
        {
            Debug.Log($"{Name}开始跳跃!");
        }
    }

}

30.访问修饰符和类中类
#

public:公开的

private:私有的,只有自己可以访问,子类都不行

protected:自己以及子类都可以访问,但是不相关的不行

如果不写访问修饰符,默认就是private

访问:就是“对象.成员”,成员包括了成员函数和构造函数,但是构造函数一般都是public

public string Name; 	// 公开属性,可以在任何地方访问
private string age; 	// 私有属性,只能在 Animal 类内部访问
protected string color; // 受保护属性,可以在 Animal 类及其派生类中访问

类中类/嵌套类/内部类
#

也就是class中也可以定义一个class的情况

如果使用public,用 类.内部类 来访问类型,如Cat.CatData,其中CatData定义在Cat中

如果使用private,就相当于保护起来了,外界无法访问

using UnityEngine;

public class Hello : MonoBehaviour
{
    void Start()
    {
        // 通过类型名.类型名去访问嵌套类
        Animal.AnimaTest animaTest = new Animal.AnimaTest();
        animaTest.Test();
    }
}

// 没有被嵌套的,只能被定义成公开的
public class Animal
{
    public void Test()
    {
        // 内部使用和平时没什么区别
        AnimaTest animaTest = new AnimaTest();
        animaTest.Test();
    }

    // 嵌套类可以修改访问修饰符
    // private:只能被嵌套自身的上级访问
    public class AnimaTest
    {
        public void Test()
        {
            Debug.Log("Hello from AnimaTest!");
        }
    }
}

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.接口
#

大家来想象一种需求

  1. 人、鹦鹉、机器人三种类型,他们的共同特点是都会说话
  2. 分类上,我们不可能将他们分为一类,因为除了会说话,没有任何其他的共同特点
  3. 我们有一个公用函数,让会说话的对象进行说话

上面这种需求其实不相关的类型但是有共同特点,应该如何解决?

接口
#

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;
    }
}

38.特殊的引用类型string
#

常用成员
#

Length:字符串长度

IndexOf:A字符串在B字符串中第一次出现的位置

LastIndexOf:A字符串在B字符串中最后一次出现的位置

Replace:替换指定的字符

ToUpper:转为大写

ToLower:转为小写

字符串的特殊性
#

首先string是引用类型,底层使用了“常量池”的技术,也就是如果存在某个字符串,则不会实例化一个新的字符串,而是复用之前的引用

string str1 = "ddd";
string str2 = "ddd";
str1 = "CCC"

上面代码中str1和str2是同一个引用,但是str1修改后,str2并不会修改,这也是string的特殊性

// 这些函数都是返回一个新的字符串,不会修改原有的字符串,因为使用了常量池的技术
//Length: 字符串长度
//IndexOf:A字符串在B字符串中第一次出现的位置
//LastIndexOf:A字符串在B字符串中最后一次出现的位置
//Replace:替换字符串中指定的字符
//ToUpper:将字符串转换为大写
//ToLower:将字符串转换为小写

string name1 = "张三Abc";
string name2 = "张三Abc";
string name3 = new string("张三Abc");

// C#中允许重载运算符,如果一定要看它们的引用是否相等,要用object.ReferenceEquals
Debug.Log(name1 == name3); // 字符串的==被修改,不再是对比是否同一个引用,而是值是否相等
Debug.Log(object.ReferenceEquals(name1, name3));
Debug.Log(object.ReferenceEquals(name1, name2));

39.数组
#

数组也是一种内置类型,如同字面意思,用来管理多个数据的

语法
#

关键点
#

  1. 数组本身是引用类型
  2. 数组中可以存放任何类型
  3. 数组中的数据如果没有设置值,会是该类型的默认值,引用就是null
using UnityEngine;
public class Hello : MonoBehaviour
{
    void Start()
    {
        // 定义数组
        // 定义时如果不确定值,数组内是默认值
        int[] nums1 = new int[10];
        int[] nums2 = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; // 定义时就填充数据
        Hero[] heros = new Hero[] {
            new Hero(),new Hero(),new Hero()
        };

        int num;
        // 修改数据,编程领域普遍情况下,计数从0开始
        nums1[0] = 33;

        // 获取数据
        Debug.Log(nums1); // 33

        // 理论上不存在删除数据的情况,但是可以让数据无效
        nums1[0] = 0;
        heros[0] = null; // 让数组中的数据无效

        Test(nums1);
        Test(nums2);
        // Debug.Log(nums2[nums2.Length]); Length是数组的长度,但是索引从0开始,所以最后一个元素的索引是Length - 1

        object[] objs = new object[] {1, "hello", new Hero() };
    }
    private void Test(int[] nums)
    {
        // 数组的长度,但是注意,不是有效长度,而是定义时的长度
        for (int i = 0; i < nums.Length; i++)
        {
            var num = nums[i];
            Debug.Log(num);
        }
    }
}
public class Hero
{

}

40.交错数组
#

上节课有个特殊的地方:“数组中可以存放任何类型”,那么是否也可以将数组放数组中?

注意事项
#

  1. 因为数组中可以放任何类型,所以也可以放数组。但是数组中的类型是确认的,所以可以产生int[][]这样的存在
  2. 交错数组中每个元素虽然类型一致,但是他们的元素数量可以不相同
int[] nums = new int[] {0,1,2,3,4,5,6,7,7,7 }; // 10个int
// 交错数组只是约束成员都是某个类型,但是不约束里面的内容
int[][] nums2 = new int[][] // 元素是int[]
{
    new int[] {0,1,2,3,4,5,6,7,7,7 },
    new int[] {0,1,2,3,4,5,6,7,7,7 },
    new int[] {0,1,2,3,4,5,6,7,7,7 },
    new int[] {0,1,2,3},
};
int num = nums2[0][3]; // 访问nums2[0]的第三个元素
Debug.Log(num); // 输出3

41.多维数组
#

多维数组是确定行列的数组

    // 多维数组
    int[,] nums3 = new int[2, 3] // 4行3列
    {
        {1,2,3},
        {4,5,6},
    };
    Debug.Log(nums3.Length); // 输出6 多维数组元素的总长度
    Debug.Log(nums3[0, 0]); // 输出1
    Debug.Log(nums3[1, 0]); // 输出4
    Debug.Log(nums3[0, 1]); // 输出2

    for (int i = 0;i < nums3.GetLength(0);i++)
    {
        for (int j = 0; j < nums3.GetLength(1); j++)
        {
            Debug.Log(nums3[i, j]);
        }
    }
}

42.面向对象总结
#

using UnityEngine;
public class Hello : MonoBehaviour
{
    void Start()
    {
        Animal[] animals = new Animal[3];
        animals[0] = new Dog();
        animals[1] = new Cat();
        animals[2] = new Fish();

        for (int i = 0; i < animals.Length; i++)
        {
            animals[i].Eat();
        }

        for (int i = 0; i < animals.Length; i++)
        {
            // 如果动物实现了IRun接口,则调用Run方法
            IRun runer = animals[i] as IRun;
            if (runer != null)
            {
                runer.Run();
            }
            else
            {
                Debug.Log("该动物不能跑!");
            }
        }
    }
}

public abstract class Animal
{
    public abstract void Eat();
    public virtual void Cry()
    {
        Debug.Log("");
    }
}

public interface IRun
{
    void Run();
}

public class Dog : Animal,IRun
{
    public override void Eat()
    {
        Debug.Log("小狗吃东西!");
    }
    public void Run()
    {
        Debug.Log("小狗跑!");
    }
}

public class Cat : Animal, IRun
{
    public override void Eat()
    {
        Debug.Log("小猫吃东西!");
    }
    
    public void Run()
    {
        Debug.Log("小猫跑!");
    }
}

public class Fish : Animal
{
    public override void Eat()
    {
        Debug.Log("小鱼吃东西!");
    }
    
}