31

V 语言中文教程:基础部分

 5 years ago
source link: https://www.tuicool.com/articles/eAriieQ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

7na2imy.gif

欢迎关注“网易云课·光谷码农课堂”,V语言入门视频教程!

  • 中文文档:https://vlang-zh.cn/docs.html

  • 中文译者:柴树杉https://github.com/chai2010

V语言是一个简单、快速、安全的编译型语言,比较适合于开发可维护的软件。

fyyYza2.jpg!web

简介

V语言是一种静态类型的编译语言,用于构建可维护的软件。它与Go类似,同时也受到Oberon,Rust,Swift等语言设计的影响。

V语言也是一种非常简单的语言。通读本教程只要半个小时,你就可以掌握语言的全部特性。

尽管语言简单,但是为开发人员提供了强大的特性。任何其它语言可以实现的功能,V语言都可以实现。

QQ群和微信群

  • V语言QQ群:878358520

  • V语言微信群请关注“光谷码农”微信公众号,然后从底部菜单进群。

mMRj2ma.jpg!web

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()

mMRj2ma.jpg!web

欢迎关注“网易云课·光谷码农课堂”,V语言入门视频教程!

IBbeqam.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK