内容简介:欢迎关注“网易云课·光谷码农课堂”,V语言入门视频教程!
欢迎关注“网易云课·光谷 码农 课堂”,V语言入门视频教程!
-
中文文档:https://vlang-zh.cn/docs.html
-
中文译者:柴树杉https://github.com/chai2010
V语言是一个简单、快速、安全的编译型语言,比较适合于开发可维护的软件。
简介
V语言是一种静态类型的编译语言,用于构建可维护的软件。它与 Go 类似,同时也受到Oberon,Rust,Swift等语言设计的影响。
V语言也是一种非常简单的语言。通读本教程只要半个小时,你就可以掌握语言的全部特性。
尽管语言简单,但是为开发人员提供了强大的特性。任何其它语言可以实现的功能,V语言都可以实现。
QQ群和微信群
-
V语言QQ群:878358520
-
V语言微信群请关注“光谷码农”微信公众号,然后从底部菜单进群。
Hello World
fn main() { println('hello V语言中文网:https://vlang-zh.cn') }
函数用 fn
关键字定义或声明。返回类型在函数名称后面。在这个例子中main函数没有返回值,因此返回值类型被忽略了。
和 C语言 一样,main函数是程序的入口函数。println是内置函数之一,它打印到标准输出。
在一个单一文件的V程序中,main函数可以被忽略。这对于学习语言的一些小代码片段很友好。为了演示,后面的例子就忽略了main函数。
因此“Hello World”程序可以写的再简单一点:
println('hello V语言中文网:https://vlang-zh.cn')
注释
// 单行注释 /* 多行注释. /* 支持嵌套注释. https://vlang-zh.cn */ */
函数
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语言唯一定义变量的方式。因此,V语言中所有的变量必须指定一个初始化的值。
变量的类型是从右值推导来的。要使用其它类型,必须手工强制转换类型:使用 T(v)
表达式将v转换为T类型。
和其它主流语言不同的是,V语言只能在函数内部定义变量。V语言没有模块级别的全局变量,因此也没有全局状态。
mut age := 20 println(age) age = 21 println(age)
使用 =
给变量重新赋值。不过在V语言中,变量默认是不可再次改变的。如果需要再次改变变量的值,必须用 mut
修饰变量。可以尝试删除 mut
,然后再编译上面的代码。
需要注意 :=
和 =
的差异,前者是用于声明和初始化变量,后者是重新给变量赋值。
fn main() { age = 21 }
上面的代码将不能编译,因为变量没有被声明过。V语言中所有的变量必须要先声明。
fn main() { age := 21 }
上面的代码依然不能被编译,因为V语言中禁止声明没有被使用的变量。
fn main() { a := 10 if true { a := 20 } }
和很多其它语言不同的是,不同块作用域的变量不得重名。上面的例子中,变量a已经在外层被声明过,因此不能再声明a名字的变量。
基础类型
bool string i8 i16 i32 i64 i128 (soon) u8 u16 u32 u64 u128 (soon) byte // alias for u8 int // alias for i32 rune // alias for i32, represents a Unicode code point f32 f64 byteptr voidptr
和C语言、Go语言不同的是,int始终是32bit大小。
字符串
name := 'Bob' println('Hello, $name!') // `$` is used for string interpolation println(name.len) bobby := name + 'by' // + is used to concatenate strings println(bobby) // ==> "Bobby" println(bobby.substr(1, 3)) // ==> "ob" // println(bobby[1:3]) // This syntax will most likely replace the substr() method
V语言中,字符串是一个只读的字节数组。字符串数据采用UTF8编码。
单引号和双引号都可以用户包含字符串面值(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 <-- This will not compile. `names` is an array of strings. println(names.len) // ==> "3" println('Alex' in names) // ==> "false" // We can also preallocate a certain amount of elements. nr_ids := 50 mut ids := [0 ; nr_ids] // This creates an array with 50 zeroes
数组的第一个元素决定来数组的类型,比如 [1, 2, 3]
对应整数类型的数组 []int
。而 ['a', 'b']
对应字符串数组 []string
。
数组中的每个元素必须有相同的类型,比如 [1, 'a']
将不能编译。
<<
运算符用于向数组的末尾添加元素。
而数组的 .len
成员返回数组元素的个数。这是一个只读的属性,用户不能修改。V语言中所有导出的成员默认都是只读的。
val in array
表达式判断val值是否是在数组中。
Maps
mut m := map[string]int{} // Only maps with string keys are allowed for now m['one'] = 1 println(m['one']) // ==> "1" println(m['bad_key']) // ==> "0" // TODO: implement a way to check if the key exists numbers := { // TODO: this syntax is not implemented yet '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语言会优化上述的表达式,因此两种方式产生的目标代码都是差不多的。
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') // Output: 0) Sam }
其中 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循环更安全,因为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: replace with match expressions
switch
对应多个 if - else
分支的简化。当遇到相等的第一个case对应的语句执行相应的语句。和C语言不同的是,不需要在每个case写break。
结构体
struct Point { x int y int } p := Point{ x: 10 y: 20 } println(p.x) // Struct fields are accessed using a dot
上面的结构体都在栈上分配。如果需要在堆上分布,需要用取地址的 &
操作符:
pointer := &Point{10, 10} // Alternative initialization syntax for structs with 3 fields or fewer println(pointer.x) // Pointers have the same syntax for accessing fields
V语言不支持子类继承,但是可以嵌入匿名结构体成员:
// TODO: this will be implemented later in June struct Button { Widget title string } button := new_button('Click me') button.set_pos(x, y) // Without embedding we'd have to do button.widget.set_pos(x,y)
结构体成员访问修饰符
结构体成员默认是私有并且不可修改的(结构体模式是只读)。但是可以通过 pub
设置为公开的,通过 mut
设置为可写的。总体来说有以下五种组合类型:
struct Foo { a int // private immutable (default) mut: b int // private mutable c int // (you can list multiple fields with the same access modifier) pub: d int // public immmutable (readonly) pub mut: e int // public, but mutable only in parent module pub mut mut: f int // public and mutable both inside and outside parent module } // (not recommended to use, that's why it's so verbose)
例如在builtin模块定义的字符串类型:
struct string { str byteptr pub: len int }
可以看出字符串是一个只读类型。
字符串结构体中的byte指针在builtin模块之外不可访问。而len成员是模块外部可见的,但是外部是只读的。
fn main() { str := 'hello' len := str.len // OK str.len++ // Compilation error }
方法
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方法有一个User类型的接收者参数u。V语言的习惯是不要用self或this这类名字作为接收者参数名,而是使用短小有意义的名字。
默认都是纯函数
V语言的函数默认是纯函数,也就是函数的输出结果只依赖输入的参数,并且没有其它的副作用。
因为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"
在这个例子中,接收者参数u用mut关键字标注为可变的,因此方法内部可以修改user状态。mut也可以用于其它的普通参数:
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增加mut关键字。这样可以清楚表达被调用的函数可能要修改这个值。
最好是通过返回值返回结果,而不是修改输入的函数参数。修改参数尽量控制在程序性能比较关键的部分,这样可以即使那分配和复制的开销。
使用 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 = 'https://vlang-zh.cn' ) 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是一个模块化的语言。它鼓励创建可复用的模块,而且创建模块也很简单。要创建模块需要先创建一个同名的目录,然后里面包含 .v
后缀名的文件:
cd ~/code/modules mkdir mymodule vim mymodule/mymodule.v
// mymodule.v module mymodule // To export a function we have to use `pub` pub fn say_hi() { println('hello from https://vlang-zh.cn!') }
在 mymodule
目录下可以有多个v源代码文件。
然后通过 v -lib ~/code/modules/mymodule
命令编译模块。
然后就可以在自己的代码中使用了:
module main import mymodule fn main() { mymodule.say_hi() }
每次调用模块中的函数必须在函数前面指定模块名。这虽然有点冗长,但是代码更容易阅读和为何,我们一眼就可以看出函数是属于那个模块的。在大型代码库中这很重要。
模块名要短小,一般不要超出10个字符。而且模块也不能出现循环依赖。
所以的模块都将静态编译到单一的可执行程序中。
接口
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"
类型通过实现的方法满足接口。和Go语言一样,V语言也是隐式接口,类型不需要显式实现接口。
枚举
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"?
可选类型和错误处理
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语言针对函数返回值增加了一个可选的属性,这样可以用于处理失败的情况。
将函数升级到可选类型的返回值很简单,只需要给返回值类型增加一个 ?
就可以,这样就可以区别错误和真正的返回值。
如果不需要返回错误信息,可以简单返回Node(TODO:还没有实现)。
这是V语言处理错误的主要手段。函数的返回值依然是值,但是错误处理要简洁很多。
当然,错误还可以继续传播:
resp := http.get(url)? println(resp.body)
http.get
返回的是 ?http.Response
可选类型。如果错误发生,将传播到调用函数,这里是导致main函数抛出异常。
上面代码是下面代码的简写:
resp := http.get(url) or { panic(err) } println(resp.body)
七月的泛型
struct Repo⟨T⟩ { db DB } fn new_repo⟨T⟩(db DB) Repo⟨T⟩ { return Repo⟨T⟩{db: db} } // This is a generic function. V will generate it for every type it's used with. fn (r Repo⟨T⟩) find_by_id(id int) ?T { table_name := T.name // in this example getting the name of the type gives us the table 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最终会将 ⟨⟩
替换为 <>
。
并发
并发模型和Go语言类似。通过 go foo()
来并发执行 foo()
函数调用。目录每个并发函数运行在独立的系统线程。稍后我们会实现和goroutine类似的调度器。
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是目前流行的格式,因此V语言内置了JSON的支持。
json.decode
解码函数的第一个参数表示要解码的类型,第二个参数是JSON字符串。
V语言会重新生成JSON的编码和解码的代码。因为不使用运行时的反射机制,因此编码和解码的速度都非常快。
单元测试
// hello.v fn hello() string { return 'Hello world' } // hello_test.v fn test_hello() { assert hello() == 'Hello world' }
所有测试函数都必须放在 *_test.v
文件中,测试函数以 test_
开头。通过 v hello_test.v
运行单个测试代码,通过 v test mymodule
测试整个模块。
内存管理
V语言没有自动内存回收(GC)和引用计数。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()
欢迎关注“网易云课·光谷码农课堂”,V语言入门视频教程!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Go语言中结构体的使用-第2部分OOP
- Go语言中结构体的使用-第1部分结构体
- 用 Go 创建一个新的智能合约语言 - 词法分析器部分
- 木兰编程语言 0.0.14.8:WebSocket 聊天演示;部分比较 Python 语法
- 你负责人工智能哪部分?人工那部分;知识图谱的构建主要靠人工还是机器?
- cocosdx接bugly,上传符号表,有一部分内容解析出来了, 另一部分没有解析出来
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。