Unity之C#学习笔记(10):接口和多态 Interfaces and Polymorphism

前篇链接: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)。

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值