Mojo 最近引入了特征,所以我想尝试一下。目前,内置特征包括CollectionElement 、 Copyable 、 Destructable 、 Intable 、 Movable 、 Sized和Stringable (看起来“-able”后缀是这些命名约定中的一个东西!)。
特征适用于结构。要实现特征,您只需将一个方法添加到与该特征匹配的结构中即可;然后将特征名称作为参数传递:
@value struct Duck(Quackable): fn quack(self): print("Quack!")
Mojo 中的@value
装饰器将__init__()
、 __copyinit__()
和__moveinit__()
等生命周期方法插入到结构中,稍微简化了我们的生活,因为我们不必自己添加它们。
Mojo 中的 Traits 尚不支持方法的默认实现,因此,上面的Quackable
方法主体中的...
。您还可以使用pass
,这将具有与 Python 中相同的效果。
尽管名称不同,Mojo 中特征的基本方法让我想起了 Go 接口。在 Go 中,您可以定义一个Duck
结构并实现一个Quackable
接口,如下所示:
type Quackable interface { quack() }
并创建一个满足此接口的结构:
type Duck struct {} func (d Duck) quack() { fmt.Println("Quack!") }
将其与 Mojo 实现进行比较:
trait Quackable: fn quack(self): ... @value struct Duck(Quackable): fn quack(self): print("Quack!")
我认为 Mojo 版本更加简洁,将quack()
方法定义嵌套在Duck
结构类型中。除了处理结构之外,Mojo 中的特征不需要implements
关键字,就像 Go 中一样。
有趣的是,Mojo 文档中指出,目前还不允许默认实现,这意味着将来可能会允许它们。这意味着 Mojo 可能会追求一种与 Go 不同的方法,Go 侧重于满足接口的结构,而不是实现它。
Go 中不需要默认方法实现,并且通过嵌入可以实现类似的效果,因为接口/特征实现只是 Go 中不存在的概念。 Mojo 的情况可能有所不同。
特征和接口的价值在于使代码可重用。例如,在 Mojo 中,您可以编写接受特征类型的函数……
fn make_it_quack[T: Quackable](could_be_duck: T): could_be_duck.quack()
然后传递实现相同特征的不同结构作为输入 - 一切都会正常工作!例如,下面是一个实现Quackable
Goose
结构:
@value struct Goose(Quackable): fn quack(self): print("Honk!")
在这里,要在Goose
和Duck
上调用make_it_quack
(请记住,您需要 Mojo 中的main
函数作为程序的入口点):
def main(): make_it_quack(Duck()) make_it_quack(Goose())
其输出将是
Quack! Honk!
如果我尝试将未实现Quackable
特征的内容传递给make_it_quack
函数,例如StealthCow
,则程序将无法编译:
@value struct StealthCow(): pass
make_it_quack(StealthCow())
下面的错误消息可能更具描述性;也许Mojo团队会改进它?
error: invalid call to 'make_it_quack': callee expects 1 input parameter, but 0 were specified
如果我从Goose
中删除quack
方法,情况也是如此;在这里,我们得到一个很好的描述性错误:
struct 'Goose' does not implement all requirements for 'Quackable' required function 'quack' is not implemented
与纯 Python 方法相比,这种静态类型检查的优点是我们可以轻松捕获错误并避免将任何错误传送到生产环境,因为代码根本无法编译。
Mojo 中的特征已经支持继承,因此我们的“Quackable”特征可以扩展“Audible”特征,如下所示:
trait Audible: fn make_sound(self): ...
trait Quackable(Audible): fn quack(self): ...
这意味着Duck
结构必须实现quack
和make_sound
才能符合Quackable
特征。
这类似于 Go 中“接口嵌入”的概念,要创建一个继承其他接口的新接口,您可以像这样嵌入父接口:
type Quackable interface { Audible // includes methods of Audible in Quackable's method set }
Traits 还接受无需创建结构体实例即可工作的静态方法:
trait Swims: @staticmethod fn swim(): ... @value struct Duck(Quackable, Swims): fn quack(self): print("Quack!") @staticmethod fn swim(): print("Swimming")
fn make_it_swim[T: Swims](): T.swim()
您可以像这样调用静态方法:
def main(): make_it_quack(Duck()) make_it_quack(Goose()) Duck.swim()
这将输出:
Quack! Honk! Swimming
请注意,在最后一个方法调用中,未创建 Duck 实例。这就是 Python 中静态方法的工作方式,与面向对象编程有些不同。 Mojo 建立在这个 Python 功能之上。
有趣的是,带有空interface{}
的 Go 技巧允许传递任何类型,并且在 Go 泛型引入之前在 Go 社区中很流行,但它不适用于 Mojo 类型的函数fn
。
您的结构必须至少实现一种生命周期方法,例如__len__
或__str__
,在本例中,这将使其符合Sized
或Stringable
,以便与接受特征类型的函数一起使用。
这实际上并不是一个真正的限制,并且在 Mojo 中很有意义,因为使用 Mojo,如果您需要动态类型,您可以退回到更灵活的def
函数,然后应用通常的 Python 魔法来处理未知类型。
更严格的 Mojo fn
函数也适用于使用DynamicVector
等类型的通用结构;在这里阅读更多相关内容。
我看到的另一个限制与结构及其方法而不是特征有关,并且是实现清洁架构/将代码分成不同部分的潜在障碍。
考虑前面使用 Go 与 Mojo 结构方法定义的示例之一:
type Duck struct {} func (d Duck) quack() { fmt.Println("Quack!") }
@value struct Duck(Quackable): fn quack(self): print("Quack!")
Mojo 示例遵循更像 Python 的语法,将方法定义直接嵌套在结构内部,而 Go 版本允许将方法定义与结构本身分开。在这个版本中,如果我有一个包含许多类型和方法的很长的结构,那么阅读起来会有些困难。
但这并不是一个关键的区别,只是需要注意的事情。
尽管 Mojo 语言还处于早期阶段,但 Mojo 特征已经非常有用了。虽然 Go 哲学强调简单性并试图将功能保持在最低限度,但未来我们很可能会看到 Mojo 特征中添加更多功能,使它们更加强大并支持大量不同的用例。