Mojo 学习 —— 特性(trait)
文章目录
介绍
特性(trait)是一个类型必须要实现的一组要求。你可以把它看成是一种契约:符合特性的类型必须保证实现了该特性的所有功能。
特性类似于 Java interfaces, C++ concepts, Swift protocols 和 Rust 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())
我们可以看到 Duck 和 StealthCow 两个类没有任何关联,它们的共同点是都定义了一个 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 的静态类型检查确保你只能向函数传递 Duck 或 StealthCow 的实例。
使用特性
可以使用特性来解决这个问题,因为它可以让你定义一套共享的行为,然后让类型来实现这些行为。
让我们使用特性更新 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 的特性,你不能将它添加到标准库中的 Float64 和 Int 类型中。
不过,标准库中已经包含了一些特性,随着时间的推移,还会添加更多的特性。
特性需要静态方法
除了常规的实例方法外,特性还可以指定静态方法。例如
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_sound 和 fly。
反过来,由于每只 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 的类型。任何符合 Intable 或 IntableRaising 的类型都可以使用内置的 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
Intable 和 Stringable 特性分别标识了可隐式转换为 Int 和 String 的类型。
任何符合 Stringable 或 StringableRaising 特性的类型都可以使用内置的 print 和 str 函数,只需在类型中实现 __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 是满足 Copyable 和 Movable 特性的特性。更多的特性可以查看标准库。
带有特性的泛型结构体
定义泛型容器时也可以使用特性。泛型容器是一个可以容纳不同数据类型的容器(例如数组或 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=' ')
1235

被折叠的 条评论
为什么被折叠?



