Mojo 学习 —— 基本语法
Mojo
首先是为高性能系统编程而设计的,它具有强大的类型检查、内存安全、新一代编译器技术等特性。因此,
Mojo
与
C++
和
Rust
等语言有很多共同之处。
我觉得它很多地方更像 rust
一点,例如它没有继承,但是可以用特性(trait
)来实现相同的功能。
文档中也提到了,Mojo
是一种年轻的语言,有许多特性还不完整。目前并不适合初学者,即使是这个基础部分的介绍也需要一些编程经验。^v^
语法
Mojo
是作为 Python
的超集而设计的,基本沿用了 Python
的语法,所以很多 Mojo
代码看起来和 Python
很像。
变量
变量是一个存放值或对象的名称。在 Mojo
中,所有变量都是可变的,如果要定义在运行时不可变的变量,可以使用定义别名(alias
)的方式。
在早期的
Mojo
版本中,可以用let
来声明不可变变量,但是后面为了简化语言,以及一些其他原因,把它删除了
未声明变量
Mojo
中允许使用 Python
形式的变量定义方式,即
name = value
但是这种声明方式只能在 def
函数和 REPL
环境中使用,不能在 fn
函数和 struct
范围内使用,所以尽量不要使用这种方式
声明变量
Mojo
使用 var
关键字来声明变量,可以在声明的时候直接赋值
var name = 'Tom'
编译器会自动推断出变量 name
的类型,此时 name
的类型为 StringIteral
,即字符串字面量
也可以先声明变量的类型,然后延迟赋值,即后面用到之后在给它赋值。例如
var age: Int
age = 19
这个可以理解为 Python
中的,先给变量赋值一个 None
,后面修改变量的值
age = None
age = 19
Mojo
声明是强类型的,不能将一种类型的值赋值给另一种类型的值,除非目标类型定义了隐式转换规则。例如,将一个字符串类型(StringIteral
)的值赋值给一个整数类型的变量,会报错
var age: Int = '19'
// error: cannot implicitly convert 'StringLiteral' value to 'Int' in 'var' initializer
反过来的话,将一个整数赋值给字符串,是可以的
var name: String = 19
这是为什么呢?
其实,这种赋值方式调用了 String
类型的构造函数(与 Python
中的 __init__
类似),即下面两种方式是等价的
var name: String = 19
var name = String(19)
也就是说,构造函数可以接受一个整数类型的值,并将其转换为 String
类型,这个构造函数的形式大概是这样子的
fn __init__(inout self, num: Int)
两种方式的区别
这两种方式在变量作用域上会有一些区别。例如,在 def
函数内
def lexical_scopes():
var num = 10
var dig = 1
if True:
print("num:", num) # Reads the outer-scope "num"
var num = 20 # Creates new inner-scope "num"
print("num:", num) # Reads the inner-scope "num"
dig = 2 # Edits the outer-scope "dig"
print("num:", num) # Reads the outer-scope "num"
print("dig:", dig) # Reads the outer-scope "dig"
lexical_scopes()
# num: 10
# num: 20
# num: 10
# dig: 2
var
在 if
语句块内定义一个新的变量,遮蔽了外部变量的值,同时它可以修改在外部定义的变量的值。而对于未声明的方式
def function_scopes():
num = 1
if num == 1:
print(num) # Reads the function-scope "num"
num = 2 # Updates the function-scope variable
print(num) # Reads the function-scope "num"
print(num) # Reads the function-scope "num"
function_scopes()
# 1
# 2
# 2
可以看成是赋值,会修改变量的值,而不是遮蔽外部变量
函数
Mojo
函数可以使用 fn
或 def
来声明。
fn
(Rust style
):强制类型检查和内存安全行为
fn greet2(name: String) -> String:
return "Hello, " + name + "!"
def
(Python style
):可以不声明类型且包含动态行为
def greet(name):
return "Hello, " + name + "!"
这两个函数返回结果是一样的,但是 fn
函数提供了编译时检查,以确保函数接收和返回正确的类型。而 def
函数如果接收到错误的类型,可能会在运行时失败。
目前 Mojo
不支持顶层代码,需要在 .mojo
中定义一个 mian
函数,作为程序的入口。可以使用 def
或 fn
定义
fn main():
print("Hello Mojo")
参数的值所有权问题
def
和 fn
在参数值传递上也存在区别,def
函数是“按值”接收参数。而 fn
函数可以指定值的传递方式,包括三种
owned
:传递值inout
:传递可变引用borrowed
:传递不可变引用(借用)
这个特性与 Mojo
的值所有权模式有关,这一模式确保在任何给定时间只有一个变量“拥有”一个值(但允许其他变量接收对它的引用)来保护您免受内存错误的影响。然后,所有权确保在所有者的生命周期结束时销毁值(并且没有未完成的引用)。这一点与 rust
基本上是一样
在后面的章节中再详细讨论这一问题
结构体
数据类型可以用 struct
定义为结构体,它与 Python
中的 class
类似:都支持方法、字段、运算符重载以及用于元编程的装饰器等。
Mojo
的结构体是完全静态的,在编译时就已经绑定,不允许动态派发或在运行时对结构体进行任何更改。(Mojo
未来还将支持 Python
风格的类,但现在还不行)。
例如,定义一个包含两个变量的结构体
struct MyPair:
var first: Int
var second: Int
fn __init__(inout self, first: Int, second: Int):
self.first = first
self.second = second
fn dump(self):
print(self.first, self.second)
使用也和 Python
中的类很像
fn main():
var mine = MyPair(2, 4)
mine.dump()
特性
特性这一点和 rust
基本一样,trait
我觉得它在某方面有点像是抽象类获取其他语言所说的接口,特性中定义的所有函数,结构体都要实现,才能说它符合该特性的。
目前特性只支持方法,且无法在 trait
中实现默认的行为。
使用特性,可以限制某些函数或结构体只能应用于实现了某种(或几种)特性的任何类型中,从而实现泛型
例如,定义一个必须包含 say
方法的特性
trait SomeTrait:
fn say(self, x: Int): ...
然后创建一个符合该特性的结构体,需要实现 say
方法。
@value
struct SomeStruct(SomeTrait):
fn say(self, x: Int):
print("hello traits", x)
暂时别管
@value
在干嘛,它只是给结构体加了点东西
然后将 trait
作为函数的编译时参数类型,并调用该类型包含的 say
方法。
fn fun_with_traits[T: SomeTrait](x: T):
x.say(42)
fn main():
var thing = SomeStruct()
fun_with_traits(thing)
// hello traits 42
关于中括号和小括号的区别,将在下面介绍
函数 fun_with_traits
定义了运行时参数 x
的类型是所有符合 SomeTrait
特性的数据类型,而所有符合 SomeTrait
特性的类型,都有一个 say
方法,所以这个变量一定可以调用 say
方法。
因此,fun_with_traits
被称为 “泛型函数”,因为它接受的是一种泛化的类型,而不是一种特定的类型。
参数化
在其他编程语言中,对于 parameter
和 argument
并没有明显的区分,很多时候都把它们当做一个东西。
但是在 Mojo
中,这两个单词指的是不同的东西
parameter
指的是编译时参数,是一个编译时变量,在运行时会变成常数,并在方括号([]
)中声明argument
指的是函数参数,是一个运行时参数,在圆括号(()
)中声明
看一个例子,定义一个 repeat
函数,可以打印字符串多次
fn repeat[count: Int](msg: String):
for i in range(count):
print(msg)
其中打印次数 count
是 parameter
,打印的字符串 msg
是 argument
fn main():
repeat[3]("Hello")
将会打印 Hello
三次
通过指定 count
作为编译时参数,Mojo
编译器可以优化函数,因为该值保证在运行时不会改变。编译器有效地生成了一个唯一版本的 repeat()
函数,该函数只重复信息 3
次。这使得代码的性能更高,因为运行时需要计算的内容更少。
类似地,也可以用在结构体上,更具体的内容后面再说。
与 Python 交互
目前 Mojo
还不是一个完整的 Python
超集,但可以导入 Python
中的模块
from python import Python
fn main():
try:
var re = Python.import_module('re')
var list = re.findall(r'\d+', 'hello 42 I\'m a 32 string 30')
print(list)
except e:
print(e)
# ['42', '32', '30']
其他
其他基本上和 Python
的语法差不多,像字符串可以使用单引号、双引号和三引号,使用缩进区分代码块,注释也是一样的,没啥好说