PS:主要为自己学习,,,,看的时候顺便翻译的,,,渣翻(非全人工)勿喷.
介绍
V是一种用于构建可维护软件的静态类型编译编程语言。
它与Go相似,也受到Oberon、Rust、Swift的影响。
V是一种非常简单的语言,阅读这份文档大概只需要半小时的时间,读完之后,您将学习到V的全部内容.
尽管很简单,但是它为开发人员提供了很多功能,你能用其它编程语言做的任何事情,都可以用V做到.
Hello World
fn main() {
println('hello world')
}
函数用 fn
声明, 该函数返回值的类型在函数名后定义,在这个例子中,main函数不返回任何内容,所以函数返回值的类型被省略了.
就像在C语言和所有类C语言中一样,main
函数是程序的一个入口点。
println
是为数不多的内置函数之一。它用于打印值到标准输出。
fn main()
也可以在程序中被忽略, 这在编写小程序 脚本 或学习语言时非常有用. 为简洁起见, 在这个例子中,将忽略fn main()
这意味着"hello world"程序可以像下面一样简单
println('hello world')
注释
// 这是一个单行注释
/* 这是一个多行注释
/* 它可以嵌套 */
*/
函数
fn main() {
println(add(77, 33))
println(sub(100, 50))
}
fn add(x int, y int) int {
return x + y
}
fn sub(x, y int) int {
return x - y
}
同样,类型在参数名之后.
就像在Go和C中一样,函数不能重载,这使代码更简洁,提高了代码的可读性和可维护性.
函数可以在声明之前被使用,add
和 sub
两个函数的声明在 main
之后, 但仍然可以在 main
函数中调用. 这在V中的所有声明中都是对的.并且不需要考虑文件和声明的顺序.
变量
name := 'Bob'
age := 20
large_number := i64(9999999999)
println(name)
println(age)
println(large_number)
使用:=
将变量声明并初始化赋值,这是在V中声明变量的唯一方法.这意味着被声明的变量总会有一个初始值.
变量的类型是由给它赋的值判断出来的.要强制使用不同类型的值,请使用类型转换:表达式T(v)
将值v转换为类型T。
与大多数语言不同,V只允许在函数中定义变量。不允许全局(模块级)变量.在V中没有全局状态。
mut age := 20
println(age)
age = 21
println(age)
要改变变量的值,请使用=
. 在V中,变量在默认情况下是不可变的. 要更改变量的值,必须使用mut
声明它。
做个尝试 删除第一行中的mut
,然后再编译程序..
请注意:=和=之间的区别
:=用于声明和初始化,=用于赋值。
fn main() {
age = 21
}
这段代码不会编译成功,因为没有声明变量age
.在V中,所有变量都需要声明.
fn main() {
age := 21
}
这段代码也不会编译成功,因为声明的变量未使用会导致编译错误。(PS:和Go一样,)
fn main() {
a := 10
if true {
a := 20
}
}
不像大多数语言,在V中,用父范围中已经使用的名称声明变量将导致编译错误。
基本数据类型
bool 布尔
string 字符串
i8 i16 i32 i64 i128 (soon)
u8 u16 u32 u64 u128 (soon)
byte // u8的别名
int // i32的别名
rune // i32的别名, 表示Unicode
f32 f64
byteptr
voidptr
请注意,与C和Go不同,int
始终是32位整数.
字符串
name := 'Bob'
println('Hello, $name!') // `$`用作字符串插值
println(name.len)
bobby := name + 'by' // + 用作字符串拼接
println(bobby) // ==> "Bobby"
println(bobby.substr(1, 3)) // ==> "ob"
// println(bobby[1:3]) // 这个语法很可能替代上一行用的 substr() 函数
在V中, 字符串是一个只读的字节数组.字符串数据使用UTF-8编码,
单引号和双引号都可以用来表示字符串(TODO:双引号还不支持), 为了保持一致性,vfmt将双引号转换为单引号,除非字符串包含单引号字符。
字符串是不可变的,这意味着子字符串函数非常高效. 被执行时不需要复制,不需要额外的资源.
在V中,运算符的两边都必须是相同类型的数据. 在这个代码中,如果age
是一个int
类型,将不会编译成功.
println('age = ' + age)
我们可以把age
替换成string
类型
println('age = ' + age.str())
或者使用字符串插入 (首选):
println('age = $age')
数组
nums := [1, 2, 3]
println(nums)
println(nums[1]) // ==> "2"
mut names := ['John']
names << 'Peter'
names << 'Sam'
// names << 10 <-- 这将不会编译成功. `names` 是一个字符串数组.
println(names.len) // ==> "3"
println('Alex' in names) // ==> "false"
// 我们也可以预先分配一定数量的元素.
nr_ids := 50
mut ids := [0 ; nr_ids] // 这将创建一个包含50个 0 的数组
数组的类型由它的第一个元素决定: [1, 2, 3]
是一个整型数组([]int
).['a', 'b']
是一个字符串数组 ([]string
).
所有的元素必须是相同的类型, [1, 'a']
是不行滴.
<<
是一个将值追加到数组末尾的操作符.
.len
字段(field) 返回数组的长度. 注意, 这是一个只读的字段(field), 用户不能修改它. V中所有导出的字段(field)都是只读的。.
val in array
如果数组中包含val
的时候,返回true.
Maps
mut m := map[string]int{} // 目前只允许使用字符串类型的key
m['one'] = 1
println(m['one']) // ==> "1"
println(m['bad_key']) // ==> "0"
// TODO: 实现检查key是否存在的方法
numbers := { // TODO:此语法尚未实现
'one': 1,
'two': 2,
}
if
a := 10
b := 20
if a < b {
println('$a < $b')
} else if a > b {
println('$a > $b')
} else {
println('$a == $b')
}
if
语句十分简单,并且和大多数语言类似
与其他类c的语言不同,判断的条件不需要括号,但是执行语句始终需要大括号。
if
还可以这样用:
num := 777
s := if num % 2 == 0 {
'even'
}
else {
'odd'
}
println(s) // ==> "odd"
in
运算符
in
检查数组中是否包含元素
nums := [1, 2, 3]
println(1 in nums) // ==> true
它还有助于帮助我们编写更清晰简洁的布尔表达式:
if parser.token == .plus || parser.token == .minus || parser.token == .div || parser.token == .mult {
...
}
if parser.token in [.plus, .minus, .div, .mult] {
...
}
V 会优化这些表达式, 让上面的两个if语句都生成相同的机器码,不创建数组。
for
循环
V只有一个循环结构:for
numbers := [1, 2, 3, 4, 5]
for num in numbers {
println(num)
}
names := ['Sam', 'Peter']
for i, name in names {
println('$i) $name') // 输出: 0) Sam
} // 1) Peter
for .. in
循环可以用来遍历数组中的元素. 如果需要一个索引,可以使用 for index, value in
来实现
mut sum := 0
mut i := 0
for i <= 100 {
sum += i
i++
}
println(sum) // ==> "5050"
这种形式的循环类似于其它语言中的while
循环
如果条件判断的结果为false,将停止循环
同样,判断的条件不需要括号,执行代码始终需要大括号。
mut num := 0
for {
num++
if num >= 10 {
break
}
}
println(num) // ==> "10"
判断条件可以省略,结果会导致无限循环
for i := 0; i < 10; i++ {
println(i)
}
最后,这是传统C风格 的for
循环 . 它比 while
方式更安全, 因为使用后者很容易忘记更新计数器,陷入死循环.
这的 i
这里不需要用 mut
声明,因为根据定义它总是可变的。
switch语句
os := 'windows'
print('V is running on ')
switch os {
case 'darwin':
println('macOS.')
case 'linux':
println('Linux.')
default:
println(os)
}
// TODO: 用匹配表达式代替
switch语句用于在条件多的时候替代if - else
语句. 当遇到第一个case满足时,即运行,运行完后不再向下匹配.
与C不同, 每个语句块后不需要break
语句.
struct 结构体
struct Point {
x int
y int
}
p := Point{
x: 10
y: 20
}
println(p.x) // 结构体的字段使用点 . 访问
结构体是在堆栈上分配的。要在堆上分配一个结构并获得指向它的指针,使用&前缀:
pointer := &Point{10, 10} // 具有3个或更少字段的结构,可选初始化语法
println(pointer.x) // 指针有相同的语法访问字段
V 没有子类, 但它支持嵌入式结构
// TODO: 这将在6月晚些时候实现
struct Button {
Widget
title string
}
button := new_button('Click me')
button.set_pos(x, y)
// 没有嵌入,我们就得这么做
button.widget.set_pos(x,y)
访问修饰符
默认情况下,struct的字段是私有不可变的(同时也使Struct不可变),可以用它们的pub和mut访问修饰符更改,总共有五种选项:
struct Foo {
a int // 私有不可变的 (默认)
mut:
b int // 私有可变的
c int // (您可以列出具有相同访问修饰符的多个字段)
pub:
d int // 公共不可变 (只读)
pub mut:
e int // 公共, 但仅在父模块中可变
pub mut mut:
f int // public 父模块的内部和外部都是可变的
} // (不推荐使用,这就是它如此冗长的原因)
例如,这是在内置模块中定义的字符串类型:
struct string {
str byteptr
pub:
len int
}
从这个定义很容易看出string是一个不可变类型。
指向字符串数据的字节指针在内置之外根本无法访问,len字段是公共的,但不是可变的(只读的).
fn main() {
str := 'hello'
len := str.len // OK,一切正常
str.len++ // 编译错误(因为该字段是只读的)
}
方法
struct User {
age int
}
fn (u User) can_register() bool {
return u.age > 16
}
user := User{age: 10}
println(user.can_register()) // ==> "false"
user2 := User{age: 20}
println(user2.can_register()) // ==> "true"
V 没有类.但是你可以为类型定义方法
方法是一个带有特殊接收器参数的函数。
接收器出现在它自己的参数列表中,在fn关键字和方法名称之间。
在这个例子中, can_register
方法有一个被叫做 u
的User
类型接收器 . 惯例是不要使用self或this这样的接收方名称,而是使用一个简短的, 最好是一个字母的名称.
默认纯函数Pure functions by default
默认情况下,V函数是纯函数,这意味着它们的返回值只由它们的参数决定,它们的计算没有副作用。
这是由于缺少全局变量,并且默认情况下所有函数参数都是不可变的,即使在传递引用时也是如此。
然而,V并不是一种纯函数语言.可以使用相同的关键字mut
修改函数参数:
struct User {
mut:
is_registered bool
}
fn (u mut User) register() {
u.is_registered = true
}
mut user := User{}
println(user.is_registered) // ==> "false"
user.register()
println(user.is_registered) // ==> "true"
在这个例子中,接收器(第一个参数)被标记为可变的,所以register()
可以改变user对象,对于非接收器参数也是如此:
fn multiply_by_2(arr mut []int) {
for i := 0; i < arr.len; i++ {
arr[i] *= 2
}
}
mut nums := [1, 2, 3]
multiply_by_2(mut nums)
println(nums) // ==> "[2, 4, 6]"
注意,在你调用这个函数的时候,必须在nums
前面加上mnt
,这表明被调用的函数将修改值。
最好返回值而不是修改参数(最好是调用函数返回),修改参数应该只在应用程序的性能关键部分执行,以减少分配和复制。
使用 user.register()
或 user = register(user)
代替 register(mut user)
.
V使返回对象的修改版本变得很容易:
fn register(u User) User {
return { u | is_registered: true }
}
user = register(user)
常量
const (
PI = 3.14
World = '世界'
)
println(PI)
println(World)
常量使用 const
定义,它们只能在模块级别(函数之外)定义.
常量名称必须大写。这有助于将它们与变量区分开。
常量的值永远不会被修改
V常量比大多数语言更灵活,您可以分配更复杂的值:
struct Color {
r int
g int
b int
}
fn (c Color) str() string { return '{$c.r, $c.g, $c.b}' }
fn rgb(r, g, b int) Color { return Color{r: r, g: g, b: b} }
const (
Numbers = [1, 2, 3]
Red = Color{r: 255, g: 0, b: 0}
Blue = rgb(0, 0, 255)
)
println(Numbers)
println(Red)
println(Blue)
V不允许全局变量,所以这非常有用
模块Modules
V是一种非常模块化的语言,鼓励创建可重用模块,而且非常简单.要创建一个新模块,请创建一个模块名称的目录,在目录中创建包含代码的.v文件:
cd ~/code/modules
mkdir mymodule
vim mymodule/mymodule.v
// mymodule.v
module mymodule
// 要导出的函数必须使用`pub`修饰符
pub fn say_hi() {
println('hello from mymodule!')
}
您可以在mymodule/中创建任意数量的.v文件。
使用 v -lib ~/code/modules/mymodule
生成它
就这样,你现在可以在你的代码中使用它:
module main
import mymodule
fn main() {
mymodule.say_hi()
}
注意,每次调用外部函数时必须指定模块.乍一看,这似乎有些冗长,但它使代码更易于阅读和理解.因为总是很清楚从哪个模块调用哪个函数,特别是在大型代码库中。
模块名称应该简短,不超过10个字符。不允许循环导入。
您可以在任何地方创建模块。
所有模块都被静态地编译成一个可执行文件。
接口Interfaces
struct Dog {}
struct Cat {}
fn (d Dog) speak() string {
return 'woof'
}
fn (c Cat) speak() string {
return 'meow'
}
interface Speaker {
speak() string
}
fn perform(s Speaker) {
println(s.speak())
}
dog := Dog{}
cat := Cat{}
perform(dog) // ==> "woof"
perform(cat) // ==> "meow"
类型通过实现其方法来实现接口。没有明确的意图声明,没有“implementation”关键字。
枚举Enums
enum Color {
red green blue
}
mut color := Color.red
// V knows that `color` is a `Color`. No need to use `Color.green` here.
color = .green
println(color) // ==> "1" TODO: print "green"?
选项/结果 类型 &错误处理 Option/Result types & error handling
struct User {
id int
name string
}
struct Repo {
users []User
}
fn new_repo() Repo {
return Repo {
users: [User{1, 'Andrew'}, User {2, 'Bob'}, User {10, 'Charles'}]
}
}
fn (r Repo) find_user_by_id(id int) ?User {
for user in r.users {
if user.id == id {
// V automatically wraps this into an option type
return user
}
}
return error('User $id not found')
}
fn main() {
repo := new_repo()
user := repo.find_user_by_id(10) or { // Option types must be handled by `or` blocks
return // `or` block must end with `return`, `break`, or `continue`
}
println(user.id) // ==> "10"
println(user.name) // ==> 'Charles'
}
V将选项和结果组合成一种类型,所以您不需要决定使用哪种类型。
将一个函数“升级”为一个可选函数所需的工作量很小:您必须添加一个?返回类型,当出现错误时返回一个错误。
如果不需要返回错误,可以简单地返回None(TODO:然而None还没实现)
这是在v中处理错误的主要方法。它们仍然是值,就像在Go中一样,但是优点是错误不能被取消处理,而且处理它们要少很多麻烦。
你也可以这样传错误信息
resp := http.get(url)?
println(resp.body)
http.get
返回 ?http.Response
,返回?
,所以错误会被传递到调用函数的函数
上面的代码基本是:
resp := http.get(url) or {
panic(err)
}
println(resp.body)
泛型(7月)Generics(July)
struct Repo⟨T⟩ {
db DB
}
fn new_repo⟨T⟩(db DB) Repo⟨T⟩ {
return Repo⟨T⟩{db: db}
}
// 这是一个泛型函数. V将为它所使用的每种类型生成它。
fn (r Repo⟨T⟩) find_by_id(id int) ?T {
table_name := T.name // 在本例中,获取类型的名称将得到表名
return r.db.query_one⟨T⟩('select * from $table_name where id = ?', id)
}
db := new_db()
users_repo := new_repo⟨User⟩(db)
posts_repo := new_repo⟨Post⟩(db)
user := users_repo.find_by_id(1)?
post := posts_repo.find_by_id(1)?
为了可读性,⟨⟩允许而不是< >。vfmt自动替换< >⟨⟩。
并发性Concurrency
并发模型与Go非常相似。要同时运行foo(),只需用go foo()调用它。现在,它在一个新的系统线程中启动该函数。很快goroutines和调度程序将被实现。
Decoding JSON
struct User {
name string
age int
foo Foo [skip] // Use `skip` attribute to skip certain fields
}
data := '{ "name": "Frodo", "age": 25 }'
user := json.decode(User, data) or {
eprintln('Failed to decode json')
return
}
println(user.name)
println(user.age)
JSON现在非常流行,这就是内置JSON支持的原因。
decode函数的第一个参数是要解码的类型。第二个参数是JSON字符串。
V生成用于JSON编码和解码的代码。不使用运行时反射。这导致了更好的性能。
测试Testing
// hello.v
fn hello() string {
return 'Hello world'
}
// hello_test.v
fn test_hello() {
assert hello() == 'Hello world'
}
所有的测试函数都以test_开头必须放在*_test.v文件中。要运行测试,请执行 v hello_test.v
. 要测试整个模块, 运行 v test mymodule
.
内存管理Memory management
没有垃圾收集或引用计数。V在编译期间清理它所能清理的。例如:
fn draw_text(s string, x, y int) {
...
}
fn draw_scene() {
...
draw_text('hello $name1', 10, 10)
draw_text('hello $name2', 100, 10)
draw_text(strings.repeat('X', 10000), 10, 50)
...
}
字符串不转义draw_text,因此当函数退出时,字符串将被清除。
事实上,前两个调用根本不会导致任何分配。这两个字符串都很小,V将为它们使用预先分配的缓冲区。
对于更复杂的情况,需要手动内存管理。这个问题很快就会解决。
V将在运行时检测内存泄漏并报告它们。例如,要清理数组,可以使用free()方法:
numbers := [0; 1000000]
...
numbers.free()
Defer
TODO
vfmt
TODO
高级主题
V中调用C函数
#flag -lsqlite3
#include "sqlite3.h"
struct C.sqlite3
struct C.sqlite3_stmt
fn C.sqlite3_column_int(C.sqlite_stmt, int) int
fn main() {
path := 'sqlite3_users.db'
db := &C.sqlite3{}
C.sqlite3_open(path.cstr(), &db)
query := 'select count(*) from users'
stmt := &C.sqlite3_stmt{}
C.sqlite3_prepare_v2(db, query.cstr(), - 1, &stmt, 0)
C.sqlite3_step(stmt)
nr_users := C.sqlite3_column_int(res, 0)
C.sqlite3_finalize(res)
println(nr_users)
}
Compile time if
$if windows {
println('Windows')
}
$if linux {
println('Linux')
}
$if mac {
println('macOS')
}
编译时if
使用 $
. 现在它只能用来检测操作系统.