C#学习加固笔记#
1.注释以及 log#
using UnityEngine;
// 注释是写给人看的
public class Hello : MonoBehaviour
{
void Start()
{
Debug.Log("这是一个普通日志。");
Debug.LogWarning("这是一个警告日志。");
Debug.LogError("这是一个错误日志。");
}
}
2.变量以及常用类型#
变量是什么?
- 数据的抽象表示,类似数学中的变量
- 可以变化的,值可以变
- 除了整数、小数等以外还可以表示玩家的攻击力,生命值,等级,游戏昵称或者其他我们自定义的信息
常用的变量类型
start(字符串):用来存储用户名、密码、角色名称、技能名称等文本信息int(整数):存角色等级、纯数字ID等
float(浮点数):小数,用来存时间这类对精度要求更高的数值
整数、浮点数等数值类型都有取值范围,但是大多情况下以及目前情况下,可以忽视这个问题,感兴趣点链接了解:
变量的申明
类型 变量名 = 变量初始值;驼峰命名法
- 大驼峰:首字母大写,之后每个单次的首字母也大写,其余字母小写,如:GameOver、Home、MyGameID
- 小驼峰:首字母小写,之后每个单次的首字母也大写,其余字母小写,如:gameOver、home、myGameID
命名约束
- 首字母不能是数字,但是后续可以是数字
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.转义字符#
转义字符
转义字符 \ 单引号 ' 双引号 " 退格符 (unity不支持) \b 换页符(unity不支持) \f 换行符 \n 回车 \r 表格 水平 \t 表格 垂直 \v 原义符
用来取消转义字符的特殊意义,要写在双引号外面
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.变量的类型转换#
重新学习变量的定义方式
定义但是没有赋值,这种情况下无法使用变量
定义并且赋值
同类型变量批量定义
全部赋值
部分赋值
全部不赋值
// 批量定义,只需要写一次类型,变量名用逗号分隔 int num1 = 1, num2 = 2, num3 = 3;
=式的理解
- 等式右边的结果会赋值给左边,所以左边只能是变量,不能是数值
- 等式两边类型一直,不能int a = “123”
- 变量可以在等式两边任何一边
数值也有类型
C#内置的类型背后都有一个实际类型,鼠标放在类型关键字或数值上都能看到
- string: String
- int: Int32
- float: Single
int age = 25;
Int32 age2 = 30; // Int32 等于 int
类型的转换方式
隐式类型转换:无需人为干涉的自动转换
类型上的兼容
精度小的转为精度大的
int num = 10; float fnum = num; // 把num的数值转为float类型
显式类型转换:人为干涉的强制转换
类型上的兼容
如果将精度大的转为精度小的,会丢失那些无法保存的精度
// 显式转换,丢失精度 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);
字符串转换
// ToString(),转为字符串 int age = 19; string ageStr1 = age + ""; // 不推荐的做法 string ageStr2 = age.ToString(); // 推荐的做法
6.作用域和常量#
作用域#
C#中用大括号{}表示语句块、层次关系
而变量具备一个有效范围,这个范围就是他的作用域
变量只能在定义之后使用
变量只在它所在的“范围内有效”,这个范围可以简单的理解成大括号{}内
但是大括号{}有嵌套的情况时
“父级”的变量可以用在“子级”中
“子级”的变量不可以用在父级中
常量#
常量是固定的值,程序运行时不会修改的值,比如圆周率π就不会因任何原因而修改
非常类似变量,但是它在定义时必须赋值,其他时刻无法改变
常量同样有作用域
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:布尔类型一般用来描述“是与否”、“真与假”、“能与不能”、“正确与错误”等情况
有两个值:
- True:代表是
- 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

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:以上条件都不满足则执行
注意事项#
- 短路机制,if如果执行成功则不会执行else if 或 else 的语句,短路机制不只是不执行{}中的代码,判断条件本身也不会运行(因为判断条件有时候本身是有计算过程的)
- if语句中大括号也存在变量的作用域问题
14.枚举类型#
特别适合用来描述状态、阶段
当某个数据可以预期有确定性的几种结构时,我们可以使用枚举类型来保存,比如
AI:待机、攻击、死亡
时间:周一、周二、周三
游戏状态:选择英雄、加载界面、战斗中、游戏结束
枚举是一种由我们自己去创建的类型,也是我们首次去创建我们自己的类型
定义枚举类型,一般我们会将它定义在class同级中或单独新建一个脚本文件去保存,不要定义在函数中
定义这个枚举类型的变量
使用这个变量
/// <summary> /// AI状态 /// </summary> enum AIState { /// <summary> /// 待机 /// </summary> Idle, /// <summary> /// 攻击 /// </summary> Attack, /// <summary> /// 死亡 /// </summary> Die = 1000 // 自定义状态对应的值 }
15.Switch语句#
switch语句和if else if 非常接近,甚至理论上不存在也没有任何关系,不过是特定情况下会更加方便
语法:#

对比if语句#
if else if的特点是判断的因素可以完全不相关,只要条件都是bool类型即可,比如:
if(3==3)
{
}
else if (a2 > a3)
{
}
但是Switch一般用在围绕一个因素去做判断,比如当前是星期几,AI处于什么状态,所以和枚举类型特别搭配
break#
并不是每一个case都需要break,如果case 语句后面没有要执行的代码,则可以不包含 break 关键字, 这时程序会继续执行后续的case语句,直至遇到break关键字为止;
这种情况的目的一般在于,如果我们希望当AI状态为巡逻、待机时候要做的事情是一样的

16.for循环#
一般用于循环固定次数的情况
语法#

运行步骤:#
初始化代码,只会执行一次,也可以不写
判断条件是否满足
满足则执行{}中的主代码片段
- 执行更新时间
- 判断条件,满足则继续执行…(后续循环)
不满足则退出循环
// 案例: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.函数的基本使用#
函数的基本使用#
函数一般也叫方法,将可以复用的代码包装起来,方便之后调用

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.结构体使用细节#
构造函数也可以重载
this关键字
代表着当前对象,也就是在外部声明的变量
访问修饰符详解
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.面向对象概念#
面向对象是最主流的编程思想
核心就是把相关数据和行为组织成一个整体来看待
比如我们学习的结构体,我们将英雄的数据(攻击力、生命值等)与英雄的行为(移动、攻击等)包装到一个结构体中,这个过程就是抽象
其中“英雄”是类型,而具体的某个英雄“大法师”是对象,所以我们下节课就会学习非常重要的概念:“类与对象”
所以最重要的两件事情要明白:
- 分类:对游戏中很多食物进行分类包装,比如小兵、英雄等等,大多情况下我们将较为相同的单位归为同一个类型
- 对象:生活中“狗”是一个类型,而具体的“大黄”才是实体,也就是对象,所以生成对象也叫“实例化”
24.类与对象基本语法#
和结构体非常类似,但是struct要改为class,后期我们会详细介绍结构体与类的区别
25.继承#
封装#
将事物的数据、行为抽象成类,这个过程就是封装。并且我们自由的决定哪些成员对外公开或隐藏,所以上节课我们的行为其实叫封装
也可以理解为:类的产生过程就是封装
继承#
继承的核心意义是在结构上获得父类中的所有成员
比如,动物都可以吃,猫、狗等继承动物后,也具备了吃的能力
父类也叫基类,子类也叫派生类
支持多层继承,但是一个类只能有一个父类(比如动物-狗-柯基)
26.继承后的构造函数#
*基类中存在的构造函数,子类必须也做对应的实现
this#
- this表示当前类型的对象
base#
- base也是当前类型的对象
- 但是在程序上他的类型的基类,所以一般用来专门访问基类成员的
- 一般情况下,是不需要用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年的一次会议上名为“数据的抽象与层次”的演说中首先提出。
概念#
里氏转换主要是:子类和父类的类型转换
- 一个猫也是一个动物,一定的
- 一个动物可能是一个猫,可能但不一定的
应用场景#
假设我们提供一个函数,让动物作为参数,然后间接调用动物的Cry函数,我们应该如何实现?
我们要解决的是,我们具体的对象都是Cat、KejiDog等类型,那岂不是每一种类型都要提供一个函数作为重载?
结论#
隐式转换,子类转父类,没有风险的,内容一定兼容
显式转换,父类转子类,有风险,要确保类型兼容
- Cat cat = (Cat)animal ,转换过程就可能报错
- 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.接口#
大家来想象一种需求
- 人、鹦鹉、机器人三种类型,他们的共同特点是都会说话
- 分类上,我们不可能将他们分为一类,因为除了会说话,没有任何其他的共同特点
- 我们有一个公用函数,让会说话的对象进行说话
上面这种需求其实不相关的类型但是有共同特点,应该如何解决?
接口#
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.数组#
数组也是一种内置类型,如同字面意思,用来管理多个数据的
语法#
关键点#
- 数组本身是引用类型
- 数组中可以存放任何类型
- 数组中的数据如果没有设置值,会是该类型的默认值,引用就是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.交错数组#
上节课有个特殊的地方:“数组中可以存放任何类型”,那么是否也可以将数组放数组中?
注意事项#
- 因为数组中可以放任何类型,所以也可以放数组。但是数组中的类型是确认的,所以可以产生int[][]这样的存在
- 交错数组中每个元素虽然类型一致,但是他们的元素数量可以不相同
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("小鱼吃东西!");
}
}
