多态
多态是面向对象的基本特征之一,它让我们可以“用父类的变量,调用子类的实现”,从而实现灵活又优雅的代码复用与扩展
更具体的说,多态允许你将派生类(子类)的实例赋值给基类类型的变量,然后通过这个变量调用方法时,会自动根据实例的真实类型来执行子类的重写方法
在 GDScript 中,多态主要是通过以下方式实现的:
虚方法:所有方法默认都是可重写的(不像 C# 或 Java 要加
virtual)抽象类(Godot 4.5+):标记为
@abstract的类不能直接实例化,用于统一接口抽象方法(Godot 4.5+):用
@abstract标记的方法必须在子类中实现,否则会报错!
虚方法
虚方法可以在派生类中被重写,虚方法可以在父类中拥有自己的默认实现,也可以是一个空实现的方法,这使得同一个方法在不同的派生类中拥有不同的实现
与其他面向对象语言(如 C#,Java)不同,在GDScript中默认所有方法都是可以被重写的虚方法,并且不需要加 virtual 或 override 修饰符,也不允许禁止重写,这让继承结构更加灵活
特别地,静态方法虽然也可以在子类中定义同名方法来实现自己的逻辑,但这其实属于方法遮蔽(shadowing),隐藏了父类的实现,并不是严格意义上的重写。不过呢,在 GDScript 中,通过实例访问静态方法时,居然也会根据实例的真实类型来决定调用哪个方法,表现出类似“多态”的行为,这点在其他语言中可是少见的
但注意!如果你是通过“类名”来访问静态方法,比如 MyClass.foo(),那就只会调用该类本身定义的方法,不会表现出多态行为。因为类名访问并不会走动态分派机制
下面的代码示例为你演示了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示例二:静态方法的“重写”行为
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 注解来声明抽象类:
@abstract class_name Animal也可以用于内部类
@abstract class Animal:
pass抽象类具备以下特性:
可以包含成员变量、具体方法(有实现)和抽象方法(无实现)
不能直接实例化,但可以声明构造函数供子类使用
可以被具体类或其他抽象类继承,具体类必须实现抽象基类的所有抽象方法
常用于构建具有统一接口的继承体系
var animal : Animal = Animal.new() # 错误,抽象类无法被实例化但可以用抽象类作为变量类型来引用子类对象:
# 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 注解标记抽象方法:
@abstract func eat() -> void抽象方法的规则如下:
只能出现在抽象类中
不能声明主体,只能声明签名
所有具体子类都必须实现所有抽象方法,否则会导致编译错误
不能用于静态方法
例子:
# 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())声明为抽象方法,从而强制要求子类实现它们:
@abstract func _ready() -> void
@abstrcat func _init() -> void但是你不应该用@abstract标记引擎内置非虚方法(如Node.get_child,Node.get_node)!
练习
使用虚方法实现不同动物的叫声,创建一个
Animal基类和一个Cat子类、Dog子类,一个make_sound()方法,调用该方法时,不同的动物将发出不同的叫声在主脚本定义一个方法
let_it_make_sound(animal: Animal),让传入的animal调用make_sound()试试看传入不同的实例时的结果