前篇链接:Unity之C#学习笔记(9):抽象类和抽象方法 Abstract Classes and Methods
深入理解接口:为什么放弃了多继承?接口是什么?
在上一节末尾,我们提出了一个问题:对于一个现有的类,如果想对其扩展,让它支持某种“特性”,该怎么做?很自然的想法是利用继承,使这个类继承于包含了满足这个特性的方法的基类。然后可能正如你刚学接口的概念时发现的,Java和C#都不支持多继承,一个类只能有一个基类。有的地方还会告诉你,接口解决了这个问题。没错,的确是解决了,但为什么在C++中被允许的多继承,到了Java和C#却被取消了呢?
显然,这说明多继承是有缺点的。最突出的问题就是,多继承会使你的类之间的关系变得复杂混乱,导致诸如菱形继承引发的二义性等问题的发生。反思一下,继承的初衷,是为了表现类之间“is a”的关系,例如用抽象类做一个“模板”就是很好的例子。但“实现一个单独的特性”并不是这种关系。说到底,它根本不应该用一个类来描述。我们现在就需要这样一种描述了“特性”的“规格”的类型。
应运而生的就是接口(Interface)。如何理解接口:接口是一种“Contract”,一个“契约”。契约规定了要实现的内容,具体来说就是一些函数的格式(像抽象函数那样)。任何类,如果你“签”了(实现了)这样一份契约,你就要把契约中规定的内容(那些函数)实现出来。这样,当其他人拿着这份契约来找人时,你就是一个符合要求的对象。
接口不是一个类,用关键字interface修饰:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface IDamagable
{
void Damage(int damageAmount);
}
Tips:Java和C#对接口有不同的命名规范。在Java中,接口名称一般是以小写字母开头的形容词,例如throwable,serializable。而在C#中,接口名称以大写字母“I”开头,紧接一个大写字母开头的形容词,例如IComparable或上例中的IDamagable。
这里,我们声明了一个接口IDamagable,代表可以被伤害,并在其中规定了一个方法Damage。我们不需要去实现这个方法。谁实现了这个接口,谁就要去把这个方法实现出来。这样,我们就强迫实现IDamagable接口的类去实现了Damage方法。反过来说,我们任何通过IDamagable特性可以找到的对象,都是有Damage这个方法的。而通过TakeDamage方法,我们就可以实现伤害的效果。
接口不可以被实例化。在接口中,不能有变量,只能有属性和方法,而且方法不能给出实现。所有方法都默认为public。关于属性我们会在后面的部分讲到,如果不了解的同学可以暂时理解为:如果在接口中涉及到对变量的操作,也就是我们本来可能想通过接口访问到变量,就要以属性的方式声明。属性名应该以大写开头。我们为上面的例子添加一个属性:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface IDamagable
{
int Health { get; set; }
void Damage(int damageAmount);
}
get; set; 代表这个属性既可以被读,也可以被改写。这样,实现IDamagable接口的类还必须有一个Health属性,它可以对应到具体的变量。
现在接口定义好了,我们创建一个Player和一个Enemy物体和对应的脚本,让它们实现这个接口。语法很简单,在继承的基类后面加一个逗号,再写接口名即可(而在Java中,继承和接口分别要使用extends和implements关键字)。然后我们立刻看到VS的报错:
这就是在提示我们必须要实现Health属性和Damage方法。在Damage方法中,我们让Player的health变量减去一定量,并且颜色变为红色。对于Enemy,我们让它变成绿色。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour, IDamagable
{
private int _health = 100;
public int Health
{
get { return _health; }
set { _health = value; }
}
public void Damage(int damageAmount)
{
GetComponent<MeshRenderer>().material.color = Color.red;
_health -= damageAmount;
if (_health < 0) _health = 0;
Debug.Log("Player takes" + damageAmount + " damage! Remaining health is: " + _health);
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : MonoBehaviour, IDamagable
{
private int _health = 200;
public int Health
{
get { return _health; }
set { _health = value; }
}
public void Damage(int damageAmount)
{
GetComponent<MeshRenderer>().material.color = Color.green;
_health -= damageAmount;
if (_health < 0) _health = 0;
Debug.Log("Enemy takes" + damageAmount + " damage! Remaining health is: " + _health);
}
}
接下来,让我们看看如何发挥接口的作用。新建一个脚本Main,实现当鼠标点击Player或Enemy时,就调用对应的Damage方法。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Ray rayOrigin = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hitInfo;
if (Physics.Raycast(rayOrigin, out hitInfo))
{
// How to call Damage method?
}
}
}
}
关于鼠标点击使用射线判断碰撞的内容我们这里不展开了。当if语句成立时,hitInfo.collider会储存一个被我们点击到的碰撞体。现在问题来了:如何调用对应的Damage方法?按照之前的做法,我们应该先判断这个对象是一个Player还是一个Enemy,再get到Player或Enemy的脚本,然后调用相应的Damage方法,像这样:
Collider obj = hitInfo.collider;
if (obj.name == "Player")
{
obj.GetComponent<Player>().Damage(20);
}
else if (obj.name == "Enemy")
{
obj.GetComponent<Enemy>().Damage(20);
}
但如果有50种有可以Damage的对象呢?难道要写50个if else吗?当然不应该。事实上,我们根本不需要管这个可以Damage的对象是Player还是Enemy。我们只需要知道,这个对象是IDamagable的就可以了。所以,我们直接去获取一个IDamagable组件,只要它不为null,就说明点击的对象实现了这个接口,那么就可以调用Damage方法。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Main : MonoBehaviour
{
void Update()
{
if (Input.GetMouseButtonDown(0))
{
Ray rayOrigin = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hitInfo;
if (Physics.Raycast(rayOrigin, out hitInfo))
{
IDamagable damageObj = hitInfo.collider.GetComponent<IDamagable>();
if (damageObj != null)
{
damageObj.Damage(20);
}
}
}
}
}
不知道你是否回想起了在继承中实现多态的方法?声明一个基类引用,因为派生类对象可以赋给基类引用,对指向不同派生类的基类引用调用方法,就能实现调用不同派生类方法的多态。这里展示的,就是用接口实现的多态。我们获取的不再是一个基类引用,而是一个接口引用,达到了相同的效果。
至此,对于类的继承、抽象类、接口这些内容(我比较愿意将其概括为类的结构和关系的塑造)就讲完了。关于类,还有一个很重要的知识点泛型在这里没讲,是因为泛型涉及的内容比较多,类的方法、类自身、接口以及后面会讲到的委托等等都有泛型,所以打算放在后面用一两期集中总结。
在下一节,我们来学习静态类型(static)。