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

栏目: C · 发布时间: 4年前

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

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

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

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

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

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

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

简介

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

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

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

QQ群和微信群

  • V语言QQ群:878358520

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

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 语言中文教程:基础部分

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

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

你不知道的JavaScript(上卷)

你不知道的JavaScript(上卷)

[美] Kyle Simpson / 赵望野、梁杰 / 人民邮电出版社 / 2015-4 / 49.00元

JavaScript语言有很多复杂的概念,但却用简单的方式体现出来(比如回调函数),因此,JavaScript开发者无需理解语言内部的原理,就能编写出功能全面的程序;就像收音机一样,你无需理解里面的管子和线圈都是做什么用的,只要会操作收音机上的按键,就可以收听你喜欢的节目。然而,JavaScript的这些复杂精妙的概念才是语言的精髓,即使是经验丰富的JavaScript开发者,如果没有认真学习也无......一起来看看 《你不知道的JavaScript(上卷)》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码