Skip to content

多态

多态是面向对象的基本特征之一,它让我们可以“用父类的变量,调用子类的实现”,从而实现灵活又优雅的代码复用与扩展

更具体的说,多态允许你将派生类(子类)的实例赋值给基类类型的变量,然后通过这个变量调用方法时,会自动根据实例的真实类型来执行子类的重写方法

在 GDScript 中,多态主要是通过以下方式实现的:

  • 虚方法:所有方法默认都是可重写的(不像 C# 或 Java 要加 virtual

  • 抽象类(Godot 4.5+):标记为 @abstract 的类不能直接实例化,用于统一接口

  • 抽象方法(Godot 4.5+):用 @abstract 标记的方法必须在子类中实现,否则会报错!

虚方法

虚方法可以在派生类中被重写,虚方法可以在父类中拥有自己的默认实现,也可以是一个空实现的方法,这使得同一个方法在不同的派生类中拥有不同的实现

与其他面向对象语言(如 C#,Java)不同,在GDScript中默认所有方法都是可以被重写的虚方法,并且不需要加 virtualoverride 修饰符,也不允许禁止重写,这让继承结构更加灵活

特别地,静态方法虽然也可以在子类中定义同名方法来实现自己的逻辑,但这其实属于方法遮蔽(shadowing),隐藏了父类的实现,并不是严格意义上的重写。不过呢,在 GDScript 中,通过实例访问静态方法时,居然也会根据实例的真实类型来决定调用哪个方法,表现出类似“多态”的行为,这点在其他语言中可是少见的

但注意!如果你是通过“类名”来访问静态方法,比如 MyClass.foo(),那就只会调用该类本身定义的方法,不会表现出多态行为。因为类名访问并不会走动态分派机制

下面的代码示例为你演示了GDScript方法重写的特别之处

示例一:普通虚方法的重写

gdscript
class_name MyClass

func foo() -> void:     
    print("Hello, world!")  
    
class_name MyChildClass extends MyClass  

func foo() -> void:     
    print("foo")  
    
extends Control   

func _ready():     
    var obj : MyClass = MyChildClass.new()     
    obj.foo()    # 输出 foo
    
    var obj2 : MyChildClass = MyChildClass.new()     
    obj2.foo()    # 输出 foo

示例二:静态方法的“重写”行为

gdscript
class_name MyClass  

static func foo() -> void:     
    print("Hello, world!")  
    
class_name MyChildClass extends MyClass  

static func foo() -> void:     
    print("foo")  
    
extends Control   

func _ready():     
    # 通过实例调用静态方法虽然可以,但并不推荐
    var obj : MyClass = MyChildClass.new()     
    obj.foo()    # 输出 foo
    
    var obj2 : MyChildClass = MyChildClass.new()     
    obj2.foo()   # 输出 foo       
    
    MyClass.foo()    # 输出 Hello, world!
    MyChildClass.foo()    # 输出 foo

最后要特别注意一点: 虽然 GDScript 中所有方法默认都可以被重写,但你不应该随便覆盖引擎内部的方法!只有那些以下划线开头的引擎内置方法(比如 _ready()_process())才是允许并且推荐你去覆盖的。

如果你打算定义一个希望“可被子类重写”的方法,也建议采用下划线开头的命名方式 _my_custom_logic(),这是官方 GDScript 风格指南推荐的习惯做法。但风格指南不是绝对命令,只要你在项目或团队中保持一致的代码风格即可

抽象类(Godot 4.5+)

抽象类是一种只能作为基类使用的类,它本身不能被实例化 它不代表某种具体的事物,通常用来表示某种“概念”或“行为模板”,比如“动物”、“敌人”等,具体的逻辑要交给子类来实现

在 GDScript 中使用 @abstract 注解来声明抽象类:

gdscript
@abstract class_name Animal

也可以用于内部类

gdscript
@abstract class Animal:
    pass

抽象类具备以下特性:

  • 可以包含成员变量、具体方法(有实现)和抽象方法(无实现)

  • 不能直接实例化,但可以声明构造函数供子类使用

  • 可以被具体类或其他抽象类继承,具体类必须实现抽象基类的所有抽象方法

  • 常用于构建具有统一接口的继承体系

gdscript
var animal : Animal = Animal.new() # 错误,抽象类无法被实例化

但可以用抽象类作为变量类型来引用子类对象:

gdscript
# animal.gd
@abstract class_name Animal

var name: String

func _init(name: String) -> void:
    self.name = name
    
# cat.gd
class_name Cat extends Animal

# main.gd
extends Node

func _ready():
    var animal : Animal = Cat.new("miko")

抽象方法(Godot 4.5+)

抽象类中的抽象方法是一种“必须由子类实现”的方法,它只包含方法签名,不提供任何实现。 在 GDScript 中使用 @abstract 注解标记抽象方法:

gdscript
@abstract func eat() -> void

抽象方法的规则如下:

  • 只能出现在抽象类中

  • 不能声明主体,只能声明签名

  • 所有具体子类都必须实现所有抽象方法,否则会导致编译错误

  • 不能用于静态方法

例子:

gdscript
# animal.gd
@abstract class_name Animal

@abstract func eat() -> void
@abstract func sleep() -> void

# cat.gd
class_name Cat extends Animal

func eat() -> void:
    print("cat eating")
    
func sleep() -> void:
    print("cat sleeping")

你甚至可以将引擎内置虚方法(如 Node._ready()、构造函数Object._init())声明为抽象方法,从而强制要求子类实现它们:

gdscript
@abstract func _ready() -> void
@abstrcat func _init() -> void

但是你不应该用@abstract标记引擎内置非虚方法(如Node.get_childNode.get_node)!

练习

  1. 使用虚方法实现不同动物的叫声,创建一个Animal基类和一个Cat子类、Dog子类,一个make_sound()方法,调用该方法时,不同的动物将发出不同的叫声

  2. 在主脚本定义一个方法 let_it_make_sound(animal: Animal),让传入的 animal 调用make_sound()试试看传入不同的实例时的结果