Unity C# 爆破计划(十一):动态多态

15 篇文章 1 订阅


十一、动态多态

Covers:多态(抽象类)、多态(接口)

认识多态

多态 就是一个行为可以具有多种形式或效果。在生活中你不可能没见过多态性:同一个行为“着装”,穿着校服你表现得就像是在上学,穿着西装你看起来就像是要出席重大场合,穿着睡衣你看起来就像是刚起床 —— 人就是个衣架子(夸你长得好看),总要接受一件“衣服”,但衣服的种类不是固定的,当获得了一件具体的衣服时,“穿衣服”这一行为就产生不同的效果。

多态分为 静态多态动态多态静态多态发生在编译时,动态多态发生在运行时,我们这一节只讨论动态多态;C# 的动态多态是对象多态,是依靠引用实现的多态 —— 听不懂就对这几个名词看个眼熟即可,稍后会有例子。

动态多态有两种方式实现:抽象类接口,这是两种特殊的类。

抽象类

“衣服”就和“食品”、“动物”等概念一样,是一大类事物的抽象。你能实例化一个抽象的对象吗?不能,否则就会出现下列欠抽的对话:

“你穿的这是什么衣服啊?”

“我穿的是 衣服~”

“你吃的是什么啊?”

“我在吃 食品~”

“你家养宠物吗?”

“养啊,养了一只 动物~”

你很快发现,上面这些抽象的概念,如果从中继承出具体的事物,如“玩偶服”、“牛肉汉堡”、“哈士奇”,带入上面的加粗部分,那么对话就正常了。由此我们可以总结出一句堪称哲学的真理:抽象的类不能也不应该被实例化,其存在的意义就是被继承 —— 恭喜你掌握了本节最重要的知识点。

像上面这种抽象的类就叫做 抽象类C# 用 abstract 关键字来定义抽象类抽象类还可以具有抽象方法,也用这个关键字来定义,抽象方法不能具有执行体(要直接在函数签名后面写个分号)在抽象类之外不能定义抽象方法

派生自抽象类的子类,要重写(实现)基类中的抽象方法。用 override 关键字来定义一个重写方法,且名称、返回值类型、形参表都必须和基类给出的抽象方法一样。

我们写一下本节开头的“穿衣服”例子来演示:

using System;

namespace Polymorphism
{
    abstract class Clothing
    {
        public abstract void Describe();
    }

    class SchoolUniform : Clothing
    {
        public override void Describe()
        {
            Console.WriteLine("You seem like a nerd.");
        }
    }

    class Pyjamas : Clothing
    {
        public override void Describe()
        {
            Console.WriteLine("You feel like going to bed.");
        }
    }

    class PinruArmor : Clothing
    {
        public override void Describe()
        {
            Console.WriteLine("How come you're in Pin-ru's outfits?");
        }
    }

    class ClothingWearer
    {
        public void PutOn(Clothing cloth)
        {
            cloth.Describe();
        }
    }
    
    class Program
    {
        static void Main()
        {
            var you = new ClothingWearer();
            Clothing[] wardrobe =
            {
                new SchoolUniform(),
                new Pyjamas(),
                new PinruArmor()
            };

            foreach (var clothing in wardrobe)
            {
                you.PutOn(clothing);
            }
        }
    }
}

我们先定义了所有衣服的抽象基类 Clothing,它具有抽象方法 Describe,预期的行为是向控制台打印一句对“穿衣体验”的描述;派生出三种不同的衣服,每种衣服重写 Describe 方法,产生自己独特的描述,这些派生类可以实例化;我们写了一个简单的类 ClothingWearer 用来穿上这些衣服,它有一个方法 PutOn,接受一个 Clothing 类的对象(并未指定是哪种 Clothing,只要是 Clothing 就行),并调用传入对象的 Describe 方法;最后,我们在测试代码中定义了一个 ClothingWearer 对象“you”,定义了一个衣服数组“wardrobe”用来存放衣服,向数组中 new 出三件不同的衣服,并让“you”来 PutOn 所有的衣服。

是不是有点设计游戏内味儿了?

控制台输出:

You seem like a nerd.
You feel like going to bed.
How come you're in Pin-ru's outfits?

需要指出,这里创建数组是为了便于 PutOn 所有衣服,实际上也可以只 new 出某个种类的衣服,直接把衣服对象传递给 PutOn 方法:

var you = new ClothingWearer();
you.PutOn(new Pyjamas());

“You put on new pyjamas”,多么自然的句子……向代码中引入多态后,是否有一瞬间,你觉得程序仿佛拥有了生命?

虚方法

有时抽象和具体的界限并不那么清晰。“狗”既可以作为一种具体的动物被实例化,也可以作为一个抽象的概念被继承。我们常希望某个类本身能够实例化、具有能够执行的方法,同时又保有被继承、方法被重写的潜质。虚方法 是抽象方法的推广,它不仅可以被子类重写,本身也能够执行

普通类不能具有抽象方法,但可以有虚方法;抽象类既可以有抽象方法,也可以有虚方法。我们 virtual 关键字来定义虚方法,下面是一个关于两只狗的例子:

using System;

namespace Learning
{
    class Dog
    {
        public virtual void Bark()
        {
            Console.WriteLine("Woof!");
        }
    }

    class Husky : Dog
    {
        public override void Bark()
        {
            Console.WriteLine("23333");
        }
    }

    class Program
    {
        static void Main()
        {
            var lucio = new Dog();
            var rex = new Husky();
            lucio.Bark();
            rex.Bark();
        }
    }
}

我们定义了 Dog 类,它具有一种“默认”的 Bark 方法,这是一个虚方法,用来在控制台打印狗的叫声;从 Dog 派生出 Husky 类,它重写了 Dog 类虚方法 Bark,使之转而打印另一种独特的叫声;在测试代码中,我们实例化了两只狗,此时可以分别调用二者的 Bark 方法,得到的效果是不一样的,从而实现了动态多态。

有时我们希望在子类中调用已被重写的父类虚方法,使用 base 来指代父类即可:

class Base
{
    public virtual void Method()
    {
        ...
    }
}

class Deprived : Base
{
    public override void Method()
    {
        base.Method();
        ...
    }
}
接口

接口 是一种特殊的类,它与抽象类非常相似,也不能实例化,最大的区别是:

  1. 接口名的第一个字母约定为大写字母 I,这是应用范围很广的规范,不要打破它;
  2. 接口不能包含非静态字段,但 可以包含静态字段、常量和属性
  3. 接口不能包含非静态构造函数或析构函数(废话,因为没法实例化),但 可以包含静态构造函数,这种函数用来初始化接口类中的静态字段,我们在之前的“类与对象”一节中介绍过;
  4. 接口的成员默认是 public 权限,且 C# 语言规范 8.0 之前(非常新,我们不用)不允许定义其权限;
  5. 任何类只能继承一个类或抽象类,但可以继承任意多的接口,且 只要继承了某一接口,就必须实现其定义的所有方法,且实现的方法必须为公有
  6. 抽象类继承接口时,可以将接口实现为抽象的,留给子类去进一步实现,子类只要继承抽象类即可,不需要再直接继承接口就具有接口所带来的一切特性。

说了这么多,其实接口最常见的作用就是规定“继承了它的类必须具有哪些方法”。换言之,它是 程序员之间的一种编程约定:“如果你实现了我的接口,你的类就需要具有哪些功能(方法)”。

我们目前为止都没有讨论“多继承”这个词,它的意思是一个类继承多个(普通)类。多继承是 C++、Python 等一众语言最受人诟病的特性之一,在实际开发中必须用到的场景几乎没有,并且会带来可怕的 菱形继承。在 C# 中不允许多继承,这是为了增强代码的健壮性和可读性。然而 C# 对继承接口的数量没有限制,这是因为接口中不能定义非静态字段,从而不可能为子类实例增加额外的非静态成员(不会更改子类实例的存储结构),而仅仅向子类“注入”了一些方法,因此不会带来多继承的负面影响。

我们可以认为,接口是用于规范一个类必须具有哪些方法的“协定”,一个接口就是一种“概念”,它不带来非静态的数据,所有有关接口的话题都是围绕“方法”展开的,因此 与其说“继承”了一个接口,不如说“实现”了一个接口

我们用 interface 关键字取代 class 定义接口,接口成员不规定访问权限,也没有 virtualabstract 关键字;实现某个接口时,也不需要加 override 关键字。

之前的“服装类”例子就很适合用接口技术改写:

using System;

namespace Game
{
    interface IDescribable
    {
        string Describe();
    }

    class ConsoleDescriber
    {
        public static void PrintDescription(IDescribable item)
        {
            Console.WriteLine(item.Describe());
        }
    }

    abstract class Clothing : IDescribable
    {
        public abstract string Describe();
    }

    class Pyjamas : Clothing
    {
        public override string Describe() => "You feel like going to bed.";
    }
}

我们定义接口 IDescribable(可描述的),任何可以描述的东西都应当实现它,它定义了唯一的方法 Describe,返回一个字符串作为一句描述。接下来,我们进行了两方面的“面向接口编程”:

  1. ConsoleDescriber 类使用了 IDescribable 创造的概念,它无关任何具体(或抽象)的事物,所做的只是提供“字符串与屏幕的交互方法”,它接受一个实现了 IDescribable 的对象,调用其 Describe 方法获取该对象的描述,并调用 System 提供的 API(这里是 Console 类)将描述打印到屏幕上;
  2. Clothing 类抽象地实现了 IDescribable,把返回字符串的具体内容留给子类去定义;睡衣类 Pyjamas 继承了 Clothing,它的编写极为简单,只要重写抽象方法,返回一个真正的字符串即可(因为是直接返回的方法,还使用了 => 简写)。通过这两个类,我们不仅定义了“何为衣服”(“衣服是一种可描述的东西”),还定义了一种具体的衣服,并且只要我们喜欢,可以用类似的语法定义无数种衣服 —— 交给策划部门去做吧。

字母 I 加上一个“动词-able”形式的形容词,这是接口名常用的命名方法,可以更好地让别人了解该接口所定义的概念。

现在你的朋友只会简单的 C# 语法,不会用 System.Console 类,更不会继承,他想只用你写的 Clothing 类,和你造的工具 ConsoleDescriber 来做出“穿上衣服、把穿着体验打印到控制台上”的效果。没问题,他写出了 ClothingWearer 类,没有用到一句 Console.WriteLine:

class ClothingWearer
{
    public void PutOn(Clothing clothing)
    {
        ConsoleDescriber.PrintDescription(clothing);
    }
}

然后另一个朋友(他不会使用任何静态方法,也不会定义函数)甚至为你俩的成果写了个小场景来测试:

class Program
{
    static void Main()
    {
        var you = new ClothingWearer();
        you.PutOn(new Pyjamas());
    }
}

控制台输出:

You feel like going to bed.

任务大获成功,并且你们的代码很容易维护:以后要是想加别的服装、想加“裤子类”、“帽子类”、“地图类”……任何可描述的东西,都没问题!—— 一切都是因为你用接口创造了一个概念。

你想说“哪个学过 C# 的不会 Console.WriteLine 啊!”,那么假如 Describer 要与游戏的装备系统 API 协作、负责显示装备的描述呢?


T.B.C.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值