Mojo 学习 —— 特性(trait)

Mojo 学习 —— 特性(trait)

介绍

特性(trait)是一个类型必须要实现的一组要求。你可以把它看成是一种契约:符合特性的类型必须保证实现了该特性的所有功能。

特性类似于 Java interfaces, C++ concepts, Swift protocolsRust traits

背景

Python 这样的动态类型的语言中,不需要类似特性的东西。例如

# python
class Duck:
    def quack(self):
        print("Quack.")

class StealthCow:
    def quack(self):
        print("Moo!")

def make_it_quack_python(maybe_a_duck):
    try:
        maybe_a_duck.quack()
    except:
        print("Not a duck.")

make_it_quack_python(Duck())
make_it_quack_python(StealthCow())

我们可以看到 DuckStealthCow 两个类没有任何关联,它们的共同点是都定义了一个 quack 方法,所以它们可以在 make_it_quack 函数中以相同方式被调用。

因为 Python 使用动态调度,它会在运行时识别要调用的方法。所以,make_it_quack_python 并不关心传递给它的是什么类型,只关心它们是否实现了 quack 方法。

在静态类型的环境中,函数要求你指定每个参数的类型,所有可以使用函数重载的方法来实现类似的功能

例如,在 Mojo

@value
struct Duck:
    fn quack(self):
        print("Quack")

@value
struct StealthCow:
    fn quack(self):
        print("Moo!")

fn make_it_quack(definitely_a_duck: Duck):
    definitely_a_duck.quack()

fn make_it_quack(not_a_duck: StealthCow):
    not_a_duck.quack()

make_it_quack(Duck())
make_it_quack(StealthCow())

如果只有两个类,这种方法还不算太糟糕。但要支持的类越多,这种方法就越不实用。

Mojo 版本的 make_it_quack 并不需要 try/except 语句,因为 Mojo 的静态类型检查确保你只能向函数传递 DuckStealthCow 的实例。

使用特性

可以使用特性来解决这个问题,因为它可以让你定义一套共享的行为,然后让类型来实现这些行为。

让我们使用特性更新 make_it_quack 示例。第一步先定义一个特质:

trait Quackable:
    fn quack(self):
        ...

trait 看起来和 struct 很像,但目前 trait 只能包含方法签名,不能包含方法实现。每个方法签名后必须有三个点(...),表示该方法未被实现。

注意:在后续的版本中,可能会增加在特质中定义字段和默认方法。

接下来我们可以创建一些符合 Quackable 特性的结构体。要表示一个结构体符合某个特质,只要在结构体名称后的括号中包含特质名称。

如果包含多个特性,可以用逗号分隔。(这看起来就像 Python 的类继承语法)。

@value
struct Duck(Quackable):
    fn quack(self):
        print("Quack")

@value
struct StealthCow(Quackable):
    fn quack(self):
        print("Moo!")

结构体需要实现特性中声明的所有方法。如果一个结构体说它符合某个特质,那么它就必须实现该特质所要求的一切,否则代码将无法编译。

最后,定义一个这样的函数来接收一个 Quackable

fn make_it_quack[T: Quackable](maybe_a_duck: T):
    maybe_a_duck.quack()

这个签名的意思是 maybe_a_duck 是一个 T 类型的参数,其中 T 是一个必须符合 Quackable 特性的类型。定义方式基本与 Rust 一样

这个语法稍显啰嗦,所以在以后的版本中可能会重新设计,使它更符合人体工学。

使用方法也很简单:

make_it_quack(Duck())
make_it_quack(StealthCow())

请注意,在调用 make_it_quack 时不需要方括号:编译器会推断出参数的类型,并确保该类型具有所需的特性。

特性有一个限制,就是不能往现有的类型中添加特性。

例如,如果你定义了一个名为 Numeric 的特性,你不能将它添加到标准库中的 Float64Int 类型中。

不过,标准库中已经包含了一些特性,随着时间的推移,还会添加更多的特性。

特性需要静态方法

除了常规的实例方法外,特性还可以指定静态方法。例如

trait HasStaticMethod:
    @staticmethod
    fn do_stuff(): ...

fn fun_with_traits[T: HasStaticMethod]():
    T.do_stuff()

隐式特性的一致性

所谓的隐式特质一致性。就是说,如果一个类型实现了某个特性所需的所有方法,那么即使它没有在声明中明确包含该特性,也会被视为符合该特质:

struct RubberDucky:
    fn quack(self):
        print("Squeak!")

make_it_quack(RubberDucky())

如果你在定义一个特性时,希望它能与那些不是你定义的类型(如标准库或第三方库中的类型)一起使用,那么隐式一致性就会非常方便。

但还是强烈建议尽可能明确地声明类型所遵从的特性。这样做有两个好处:

  • 它可以清楚地说明该类型符合的特性,而无需扫描其所有方法。
  • 未来的特性支持。当默认方法实现被添加到 trait 时,它们只对显式符合 trait 的类型有效。

特性继承

特性可以从其他特性继承,并包含其父特性声明的所有要求。例如

trait Animal:
    fn make_sound(self):
        ...

trait Bird(Animal):
    fn fly(self):
        ...

由于 Bird 继承自 Animal,因此符合 Bird 特性的结构体需要同时实现 make_soundfly

反过来,由于每只 Bird 都符合 Animal,因此符合 Bird 特性的结构体可以传递给任何需要 Animal 的函数。

要继承多个特性,可以在括号内添加以逗号分隔的特性列表。例如,您可以定义一个 NamedAnimal 特性,它结合了 Animal 特性和一个新的 Named 特性的要求:

trait Named:
    fn get_name(self) -> String:
        ...

trait NamedAnimal(Animal, Named):
    pass

特性和生命周期方法

特性可以指定所需的生命周期方法,包括构造函数、复制构造函数和移动构造函数。

例如

trait DefaultConstructible:
    fn __init__(inout self): ...

trait MassProducible(DefaultConstructible, Movable):
    pass

fn factory[T: MassProducible]() -> T:
    return T()

struct Thing(MassProducible):
    var id: Int

    fn __init__(inout self):
        self.id = 0

    fn __moveinit__(inout self, owned existing: Self):
        self.id = existing.id

var thing = factory[Thing]()

上面的代码创建了一个名为 MassProducible 的特性。该类型有一个默认(无参数)构造函数,并且可以移动。

它还使用了内置的 Movable 特性,该特性要求该类型具有一个移动构造函数(即可移动)。

factory[]() 函数将会返回一个新构建的 MassProducible 类型实例。

请注意,@register_passable("trivial") 类型的生命周期方法有限制:它们不能定义复制或移动构造函数,因为它们不需要任何自定义逻辑。

为了符合特性,编译器会将 trivial 类型视为可复制和可移动类型。

内建特性

目前,Mojo 标准库包含了一些特性。它们由许多标准库类型实现,你也可以在自己的类型中实现它们:

  • AnyType
  • Boolable
  • Copyable
  • Movable
  • Sized
  • Intable
  • Stringable
  • PathLike
  • CollectionElement
  • KeyElement

AnyType

AnyType 特性描述了一个具有析构函数的类型。

Mojo 中,定义了析构函数的类型向语言表明,它是一个有生存期的对象,当对象实例的生存期结束时,就需要调用它的析构函数。因此,只有 non-trivial 类型才有析构函数。

在泛型函数中使用的所有类型都需要一个析构函数。因此,所有的 Mojo 特性都被认为继承自 AnyType,它会为可能需要的类型实现默认的无操作析构函数。

例如,在 Foo 上实现 AnyType 特性并释放已分配内存:

@value
struct Foo(AnyType):
    var p: UnsafePointer[Int]
    var size: Int

    fn __init__(inout self, size: Int):
        self.p = UnsafePointer[Int].alloc(size)
        self.size = size

    fn __del__(owned self):
        print("--freeing allocated memory--")
        self.p.free()

Boolable

Boolable 特性描述了一种可转换为 bool 的类型。该特质要求类型实现 __bool__ 方法。例如:

@value
struct Foo(Boolable):
    var val: Bool

    fn __bool__(self) -> Bool:
        return self.val

Sized

Sized 特性用于识别具有可计算长度的类型,如字符串和数组。

具体来说,Sized 要求类型实现 __len__ 方法。内置的 len 函数会使用该特性。

例如,为你的自定义列表类型实现这个特性,你就可以使用 len 计算列表的长度:

struct MyList(Sized):
    var size: Int
    # ...

    fn __init__(inout self):
        self.size = 0

    fn __len__(self) -> Int:
        return self.size

print(len(MyList()))

Intable

Intable 特性描述了一种可转换为 Int 的类型。任何符合 IntableIntableRaising 的类型都可以使用内置的 int 函数将对象转换为 Int 类型

该特质要求类型实现 __int__ 方法。例如

@value
struct Foo(Intable):
    var i: Int

    fn __int__(self) -> Int:
        return self.i

现在,您可以使用 int 函数将 Foo 转换为 Int

var foo = Foo(42)
print(int(foo) == 42)

如果 __int__ 方法会引发错误,需要使用 IntableRaising 特性。

Stringable

IntableStringable 特性分别标识了可隐式转换为 IntString 的类型。

任何符合 StringableStringableRaising 特性的类型都可以使用内置的 printstr 函数,只需在类型中实现 __str__ 魔法方法。例如

@value
struct Pet(Stringable):
    var name: String
    var type: String

    fn __str__(self) -> String:
        return "This is a " + self.type + " named " + self.name

var spot = Pet("Spot", "dog")
print(spot)

如果 __str__ 方法可能引发错误,则需要使用 StringableRaising 特性。

其他

从前面的例子中,我们可以发现,标准库的特性都定义了一些魔法方法,需要符合哪种特性,只要定义对应的方法即可。

例如 Copyable 对应的是 __copyinit__ 方法, Movable 对应的是 __moveinit__ 方法,PathLike 对应于 __fspath__ 等。例如

import os.path as op

@value
struct MyPath(PathLike, Stringable):
    var path: String

    fn __init__(inout self, path: String):
        self.path = path

    fn __fspath__(self) -> String:
        return self.path

    fn __str__(self) -> String:
        return self.path

fn main():
    var path = MyPath('/usr/bin')
    if op.isfile(path):
        print(path)
    else:
        print(path, "is not a file!")

还有一些特性是其他特性的组合,例如 CollectionElement 是满足 CopyableMovable 特性的特性。更多的特性可以查看标准库。

带有特性的泛型结构体

定义泛型容器时也可以使用特性。泛型容器是一个可以容纳不同数据类型的容器(例如数组或 hashmap)。

Python 这样的动态语言中,很容易向容器中添加不同类型的项。但在静态类型环境中,编译器需要能够在编译时识别类型。例如,如果容器需要复制一个值,编译器就需要验证该类型是否可以被复制。

List 类型就是通用容器,一个 List 只能容纳一种数据类型。例如,您可以创建这样一个整数值列表:

from collections import List

var list = List[Int](1, 2, 3)
for i in range(len(list)):
    print(list[i], sep=" ", end="")

你可以使用特性来定义对存储在容器中的元素的要求。

例如,List 要求元素可以移动和复制。只要结构体需要符合 CollectionElement 特性,就可以存放在 List 中。例如

from collections import List

struct Element(CollectionElement, Stringable):
    var elem: String
    fn __init__(inout self, elem: String):
        self.elem = elem

    fn __copyinit__(inout self, other: Self):
        self.elem = other.elem

    fn __moveinit__(inout self, owned existing: Self):
        self.elem = existing.elem

    fn __str__(self) -> String:
        return self.elem

fn main():
    var list = List[Element](Element('mojo'))
    list.append(Element('python'))
    for e in list:
        print(e[], end=' ')
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

名本无名

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值
>