内存管理基础概念
栈内存 vs 堆内存
__del__ 方法定义析构行为。值语义 vs 引用语义
这是 Mojo 内存模型的核心概念,理解这对区别是掌握所有权系统的基础:
# 值语义(Value Semantics)
# 赋值时产生独立副本
var a = 42
var b = a # b 是 a 的副本
b = 100 # 只改变 b
print(a) # 42(a 不受影响)
# Mojo struct 默认是值语义
var v1 = Vector2D(1.0, 2.0)
var v2 = v1 # 完整复制
v2.x = 99.0 # 只改 v2
print(v1.x) # 1.0(v1 不变)
# Python 引用语义(参考对比)
# 赋值时共享引用
a = [1, 2, 3]
b = a # b 指向同一对象
b.append(4) # 通过 b 修改
print(a) # [1, 2, 3, 4]!
# a 也被改了
# Python 这种行为常导致难以发现的 bug
三种参数约定:owned、borrowed、inout
核心设计思想
Mojo 函数的每个参数都有明确的所有权约定(ownership convention),通过关键字声明,编译器据此做内存安全检查和优化。
&T。这是 fn 函数参数的默认约定,不需要显式写 borrowed 关键字(写了也没错)。零拷贝开销,最高效。&mut T 或 C++ 的引用参数 T&。函数内的修改直接作用于调用者的变量。用于需要就地修改参数的场景(如自增、追加等)。三种约定的代码示例
struct BigData:
var values: List[Float64]
fn __init__(inout self, size: Int):
self.values = List[Float64](size)
# 1. borrowed(默认):只读访问,无拷贝
fn compute_sum(data: BigData) -> Float64:
# data 是借用,不能修改 data
var total: Float64 = 0.0
for v in data.values:
total += v[]
return total
# 2. inout:可修改引用
fn normalize(inout data: BigData):
# 可以直接修改 data.values
var total = compute_sum(data)
for i in range(len(data.values)):
data.values[i] = data.values[i] / total
# 3. owned:获取所有权
fn consume_and_process(owned data: BigData) -> Float64:
# data 的所有权已转移到这个函数
var result = compute_sum(data)
# 函数结束,data 在这里被析构
return result
fn main():
var d = BigData(1000)
# 调用 borrowed 函数:d 依然有效
let s = compute_sum(d)
# 调用 inout 函数:d 被修改
normalize(d)
# 调用 owned 函数:d 的所有权被转移
let r = consume_and_process(d^) # ^ 表示显式转移所有权
# 之后不能再使用 d!
在 Mojo 中,调用 owned 参数的函数时,使用 ^(transfer operator)显式表示"我愿意转让这个值的所有权"。这提高了代码的可读性——当你看到 func(x^) 时,立刻知道 x 之后不能再使用了。
生命周期与析构 __del__
struct ManagedBuffer:
var ptr: UnsafePointer[Float32]
var size: Int
fn __init__(inout self, size: Int):
self.size = size
# 分配堆内存
self.ptr = UnsafePointer[Float32].alloc(size)
print("分配", size, "个 Float32")
fn __del__(owned self):
# 析构函数:对象离开作用域时自动调用
self.ptr.free()
print("释放", self.size, "个 Float32")
fn main():
print("开始")
{
var buf = ManagedBuffer(1024)
print("使用 buf")
# buf 在这里离开作用域,__del__ 自动调用
}
print("结束")
# 输出:
# 开始
# 分配 1024 个 Float32
# 使用 buf
# 释放 1024 个 Float32
# 结束
UnsafePointer:底层内存操作
什么时候使用 UnsafePointer?
UnsafePointer 是 Mojo 中逃逸所有权系统、直接操作裸内存的工具。名字中的 "Unsafe" 是明确警告:使用它需要开发者手动保证内存安全,编译器不再保护你。
合理使用场景:实现高性能数据结构(自定义内存布局)、与 C 库交互(FFI)、SIMD 内存加载/存储、绝对不允许拷贝开销的热路径。
fn unsafe_pointer_demo():
# 分配 8 个 Float32 的连续内存
var ptr = UnsafePointer[Float32].alloc(8)
# 初始化内存(必须手动,否则是垃圾值)
for i in range(8):
ptr[i] = Float32(i) * 1.5
# 读取内存
for i in range(8):
print(ptr[i], end=" ") # 0.0 1.5 3.0 4.5 ...
# 指针算术:偏移访问
var mid_ptr = ptr + 4 # 指向第 5 个元素
print(mid_ptr[0]) # 6.0
# 必须手动释放!(否则内存泄漏)
ptr.free()
1. 分配后必须初始化,否则读取到垃圾值
2. 使用完毕必须调用 .free(),否则内存泄漏
3. 不要访问已释放的内存(use-after-free)
4. 不要越界访问(buffer overflow)
5. 优先考虑用 Mojo 标准库(List、String 等),只在确实需要时才用 UnsafePointer
Mojo vs Rust 所有权模型对比
| 特性 | Rust | Mojo |
|---|---|---|
| 只读借用 | &T | 默认 / borrowed |
| 可变借用 | &mut T | inout |
| 所有权转移 | 移动语义(默认) | owned + ^ 显式转移 |
| 生命周期标注 | 必须(复杂场景) | 无(编译器自动推断) |
| 借用检查器 | 编译期强制执行 | 编译期强制执行 |
| 逃逸安全 | unsafe 块 | UnsafePointer |
| 析构函数 | Drop trait | __del__ 方法 |
| 学习曲线 | 陡峭(尤其生命周期) | 较平缓(更接近 Python) |
值语义:Mojo struct 默认值语义,赋值产生副本,避免 Python 式的隐式共享引用导致的 bug。
三种参数约定:borrowed(只读借用,默认)、inout(可变引用)、owned(获取所有权),显式表达意图。
__del__ 析构:RAII 模式,对象离开作用域自动调用,确保资源释放。
UnsafePointer:底层裸指针,逃逸编译器保护,需要手动保证安全,仅在性能极端敏感时使用。
与 Rust 对比:Mojo 去掉了 Rust 复杂的生命周期标注,使用更直观的关键字约定,对 Python 背景开发者更友好。