半小时学会V语言

半小时学会V语言

1. V语言简介
V是一个静态类型、编译型的编程语言,目标是构建可维护软件。与Go语言相似,并受Oberon,Rust和Swift语言影响。
V语言非常简单,只需要半小时就能学会这门语言的绝大多数核心内容。
尽管语法极致简单,但V语言给予了开发者无尽的能力。能通过其他语言中实现的程序,都可以通过V语言来做到。

2. Hello World
展示一下V语言的Hello world 代码:
fn main() {
println('hello world')
}

函数通过关键字 fn 来声明。函数返回类型,写在函数名后面。这里,main() 没有返回类型,因此被省略。
与C语言相同,main是程序的入口点。
V语言的内置函数很少,println 是其中之一,它向标准输出进行打印输出。
如果一个程序的代码,只由一个文件构成,那么main() 也可以省略。这样,编写一些小程序,script脚本,会非常方便。
本文后续的例子,都会省略main()。因此,hello world 程序,可以精简为只有一句:
println('hello world')


3. 注释
// 这是单行注释

/* 这是多行注释
/* 并且多行注释可以嵌套 */
*/

4. 函数
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相同,函数不能被重载(overload)。这简化了代码,提高了可维护性、可读性。
函数可以在声明前被使用。上例中,add 和 sub 的声明在main后面,但可以在main() 中被调用。V语言中所有的声明,都遵循此原则,这样就消除了头文件,也无需考虑文件和声明的顺序关系。

5. 变量
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语言中,变量缺省是不可修改的(immutable)。如果变量的值需要被修改,那么变量必须用 mut来声明。
注意这两个操作符的区别:
:= 用于变量声明和初始化,而 = 用于赋值。

fn main() {
age = 21
}
这个代码无法通过编译,因为变量age 没有被声明。 V语言中所有的变量都必须声明后才能被使用。

fn main() {
age := 21
}
这个代码也无法通过编译,因为未被使用的变量会导致编译报错。


fn main() {
a := 10
if true {
a := 20
}
}
变量遮蔽(variable shadow) 是不允许的。声明一个与父作用域中同名的变量,会导致编译报错。


6. 基本数据类型
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

注意: int 永远是32位整数。


7. Strings
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语言中,string 是只读的字节数组,总是采用UTF-8 编码。
单引号和双引号都可以用于表示字符串(现在双引号还未实现)。出于一致性,vfmt 会把双引号换成单引号,除非字符串中有单引号。
string 也是不可修改的,因此substring 非常高效:不需要内存copy,不需要额外的内存分配。

V语言中,所有的操作符,两边的操作数必须类型完全一致。下面的代码,如果age是个int,则无法通过编译:
println('age = ' + age)
把age转换成string才可以:
println('age = ' + age.str())
或者,使用字符串格式化方法(推荐):
println('age = $age')

8. 数组 Arrays
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 成员返回数组的长度. 注意,这是个只读属性,不能被修改。V语言中所有导出的域(export field),缺省都是只读的。
val in array 当array中包含val 时返回true,否则返回false。


9. Maps
mut m := map[string]int{} // 当前,map的key仅支持string
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,
}


10. If
a := 10
b := 20
if a < b {
println('$a < $b')
} else if a > b {
println('$a > $b')
} else {
println('$a == $b')
}
if 语句,与其它语法非常类似。与C语言不同的是,V语言if 后面的条件,是没有括号包围的。

if 也可以作为表达式:
num := 777
s := if num % 2 == 0 {
'even'
}
else {
'odd'
}
println(s) // ==> "odd"

11. in 操作符
in 用于检查数组中是否包含某个元素。
nums := [1, 2, 3]
println(1 in nums) // ==> true

in 也用于编写更加干净简洁的boolean表达式。比如下面这个例子:
if parser.token == .plus || parser.token == .minus || parser.token == .div || parser.token == .mult {
...
}
用 in 改写为:
if parser.token in [.plus, .minus, .div, .mult] {
...
}
V编译器对这样的表达式进行优化,因此上面的in和if都产生同样的机器码,没有array 被创建。

12. 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
} // 1) Peter


for value in array 用于遍历一个数组中的所有元素。如果需要用下标,可以用 for index, value in array 形式。
注意,value是只读的。如果要在循环过程中修改数组的元素值,那么必须用下标:
mut numbers := [1, 2, 3, 4, 5]
for i, num in numbers {
println(num)
numbers[i] = 0
}

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 风格的循环也支持,它比while形式的循环要安全,因为while循环中容易忘记更新计数器,从而产生无限循环。
这里, i 不需要用 mut 声明,因为这种场合它永远都是要被修改的。


13. 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 的语句。它按顺序进行匹配,匹配成功后,执行对应的分支代码。
与C不同,每个case 块不需要break语句。


14. struct
struct Point {
x int
y int
}

p := Point{
x: 10
y: 20
}
println(p.x) // Struct fields are accessed using a dot

struct在栈上分配内存。如果要在堆上分配struct,并获得指向它的指针,可以使用 & 前缀:
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)


15. 成员访问修饰符(Access modifiers)
struct 的成员,缺省是私有的(private),并且不可修改(immutable)。 可以用 pub 和 mut 来修饰,总计有5种可能的组合:
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 模块中string的类型定义:
struct string {
str byteptr
pub:
len int
}
从这个定义,可以清晰看出,string是个不可修改的类型(immutable type)
string的byte指针,无法在bultin模块之外被访问。 len 是public,但不能被修改。

fn main() {
str := 'hello'
len := str.len // OK
str.len++ // Compilation error
}

16. Methods
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 语言并不支持 class。但是你可以在类型上定义方法(method).
方法(method) 也是函数,只是带特殊的接收者。接收者在fn 和方法名之间。
上例中,can_register 方法有一个接收者,类型为User,参数名为u. 依照惯例,接收者不使用self或this 这样的名字,推荐使用一个字母的名字。


17. 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]"

注意:调用multiply_by_2 函数时,必须在nums 前面增加 mut。这清晰地表达了,这个调用会修改这个值。

如果能够通过返回值来实现,就不要通过修改参数参数来实现。修改参数,应该仅用于对性能有严格要求的场景,减少对象的分配和复制。
推荐采用 user.register() 或 user = register(user) , 而不用 register(mut user)。

V语言中,非常容易返回一个对象的修改后副本:
fn register(u User) User {
return { u | is_registered: true }
}

user = register(user)

18. 常量 Constants
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)

由于不支持全局变量,因此这个特性非常有用。


19. 模块 Modules
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 mymodule!')
}

在 mymodule/ 目录下,你可以有很多.v 文件。
用 v -lib ~/code/modules/mymodule 来编译这个模块(会被自动放到 vlib路径中)。

在其它工程中,使用这个模块:
module main

import mymodule

fn main() {
mymodule.say_hi()
}

注意,每次调用外部函数时,都必须指定模块名。这看起来非常麻烦,但使代码更易读,容易理解,清晰表达了哪个模块哪个函数被调用,尤其在大工程中。
模块名应该简短,不超过10个字符。 循环 import 是不允许的。
你可以在任何位置,创建模块。
所有的模块,都被静态链接,最终形成一个单一的可执行文件。

20. 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"

一个类型,实现了接口的方法,就表示实现了接口。没有显式的声明,也没有implements 关键字。


21. 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"?


22. 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 把Option和Result 合并成一个类型。
产生的额外工作是:函数的返回类型前面增加?, 函数体中出现错误时返回一个error。
如果你不需要返回error, 可以简单返回 None。 (TODO: None is not implemented yet).
这是V语言中处理错误的主要方式。优点是 error 必须被处理,并且处理起来也非常简洁。
也可以继续传播 error:

resp := http.get(url)?
println(resp.body)


http.get 返回 ?http.Response. 调用时带 ?, 会使error 继续向上传播给父调用者。 这里,传播到main() ,会引发 panic。
因此上面的代码,是下面代码的简化:
resp := http.get(url) or {
panic(err)
}
println(resp.body)


23. 范型Generics (七月份实现)
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 自动会把 <> 替换为 ⟨⟩.


24. Concurrency
V语言的并发模型,与Go语言非常类似。为了并发运行 foo() ,只需要用 go foo() 调用它即可。 当前,用一个新的系统线程来执行这个函数。 后续, goroutines 和 scheduler会实现。


25. 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现在非常流行,因此V语言内置支持了JSON。
json.decode 函数的第一个参数是解码的目标类型,第二个参数是JSON字符串。
V自动生成JSON编码和解码的代码。不使用运行时反射,因此性能更好。

 

26. 测试 Testing
// 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

 


27. 内存管理 Memory management
没有垃圾回收(garbage collection), 也没有引用计数。 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 函数紧密关联,因此函数执行完毕后字符串即被清理。
实际上,前2个调用根本没有参数任何内存分配。这两个字符串比较小,因此V用一个预分配的buffer来放他们。
对于更复杂的例子,需要手工管理内存。后续会很快修复。

V 会在运行时检测内存泄漏,并且汇报它们。 要清除内存,需要调用free() 方法:

numbers := [0; 1000000]
...
numbers.free()


28. Defer
TODO


29. vfmt
TODO


30. Advanced Topics
30.1 Calling C functions from V
#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)
}

C 字符串,可以通过 string(cstring) 转换成 V的string。


30.2 Compile time if
$if windows {
println('Windows')
}
$if linux {
println('Linux')
}
$if mac {
println('macOS')
}
Compile time if 用 $if 开头. 当前,仅能用于检测OS.


30.3 Reflection via codegen
内置了JSON支持,非常好的特性。但V语言允许你对任何对象创建高效的序列化器:
// TODO: planned in June
fn decode<T>(data string) T {
mut result := T{}
for field in T.fields {
if field.typ == 'string' {
result.$field = get_string(data, field.name)
} else if field.typ == 'int' {
result.$field = get_int(data, field.name)
}
}
return result
}

// generates to:
fn decode_User(data string) User {
mut result := User{}
result.name = get_string(data, 'name')
result.age = get_int(data, 'age')
return result
}

30.4 Limited operator overloading
struct Vec {
x int
y int
}

fn (a Vec) str() string {
return '{$a.x, $a.y}'
}

fn (a Vec) + (b Vec) Vec {
return Vec {
a.x + b.x,
a.y + b.y
}
}

fn (a Vec) - (b Vec) Vec {
return Vec {
a.x - b.x,
a.y - b.y
}
}

fn main() {
a := Vec{2, 3}
b := Vec{4, 5}
println(a + b) // ==> "{6, 8}"
println(a - b) // ==> "{-2, -2}"
}

操作符重载,与V的简单、可预期性相矛盾。但科学计算、GUI程序都是V的主打领域,因此操作符重载是提高代码可读性的重要手段:
a.add(b).add(c.mul(d)) 不如 a + b + c * d 清晰易读

为了提供安全性和可维护性,操作符重载有一些限制:
- 仅能重载 +, -, *, / 操作符
- 操作符函数中不能调用其它函数
- 操作符函数不能修改它们的参数
- 两个参数必须类型完全一致

 

更多V语言的介绍,可以关注微信号 vlangdev  或扫描二维码  

posted on 2019-07-08 09:35  vlangdev  阅读(...)  评论(...编辑  收藏

导航