Gnome Vala 语言基础

去年的一篇文章,从自己的博客搬了过来。

介绍

Vala 是一门新兴的编程语言,由 Gnome 主导开发,支持很多现代语言特性,借鉴了大量的 C# 语法,Python 的手感,C 的执行速度,Vala 最终会转换为 C 语言,然后把 C 代码编译为二进制文件,使用 Vala 编写应用程序和直接使用 C 语言编写应用程序,运行效率是一样的,但是 Vala 相比 C 语言更加容易,可以快速编写和维护。

编辑器选择

GNOME Builder 感觉不怎么好用,但是里面有项目模板,可以用它来新建项目,哈哈哈。

推荐用 VSCode 来写 vala,配上插件手感爽爽的。

编译

debian 系发行版可通过以下命令来安装 vala 编译器:
sudo apt install valac

Vala 的源代码是以 .vala 扩展名保存的,valac 是 Vala 语言的编译器,它负责将代码编译为二进制文件,编译 Vala 代码很简单,把所有源码文件作为命令行参数提供给 valac,以及编译器标志,类似 javac 编译方式,可以在 pkg 参数后加上需要链接的包,比如:

$ valac hello.vala --pkg gtk+-3.0

它会生成一个二进制文件。

第一个 Vala 程序

class Application : GLib.Object {
    public static int main(string[] args) {
        stdout.printf("hello world\n");
        return 0;
    }
}

可以看到 Vala 的类定义和其他语言非常相似,一个类代表了一个对象的基本类型,基于该类创建的对象拥有相同属性,vala 程序入口点在 main() 函数,Vala 的 main 函数是程序的入口点,和 C/C++ 是一样,main 方法不需要在类中定义,如果要在一个类中定义的话必须加上 static 修饰符;其中 stdout 是 GLib 命名空间中的对象,stdout.printf(...) 这行告诉 Vala 调用 stdout 对象中的 printf 方法,这是很常见的语法。

注释

// Comment continues until end of line

/* Comment lasts between delimiters */

/**
* Document comment
*/

这个跟 C/C++ Java 等这些流行编程语言风格一样,支持单行注释和多行注释,所以不需要过多的解释。

数据类型

基本数据类型

基本数据类型 概述
char 字符型
unichar 32 位 Unicode 字符
int, uint 整型
long, ulong 长整型
short, ushort 短整型
int8, int16, int32, int64, uint8... 固定长度的整型,其中的数字分别代表各类型所占的位的长度
float, double 浮点数
bool 布尔型
struct 结构体
enum 枚举型,内部表述为整型

使用例子:

char c = 'u';
float percent_tile = 0.75f;
const double PI = 3.141592654;
bool is_exists = false;

struct Vector {
    public double x;
    public double y;
    public double z;
}

enum WindowType {
    TOPLEVEL,
    POPUP
}

用法和 C 没有多大的区别,在不同平台上这些类型长度都是不一样的,可以使用 sizeof 操作符获取当前平台下相应类型的长度(以字节位单位):

ulong nbytes = sizeof(int32);

另外还可以对数据类型使用 .MIN 和 .MAX 的方法来获取相应数据类型的最小值和最大值,它会返回对应数据类型的值,比如使用 int.MAX 获取当前 int 整型所能表示数值范围,例子:

int32 max_value = int32.MAX;
int64 max_value2 = int64.MAX;
float max_value3 = float.MAX;
double max_value4 = double.MAX;

字符串类型

在 Vala 中使用 string 关键字来定义字符串类型变量,都是以 UTF-8 编码,而且还不可更改。

除了常规用法,Vala 还支持使用 @ 符号表示字符串模板,可以将内部以 $ 开头的变量或者表达式展开,得到最终的字符串:

// 常规用法
string text = "Hello World";

// 字符串模板用法
int a = 10;
int b = 20;
string s = @"$a * $b = $(a * b)"; // => "10 * 20 = 200"

// 支持 ==、!= 运算符
string a1 = "hello";
string a2 = "world";
bool is_right = (a1 == a2);

string 可以使用 [start : end] 运算符来分割字符串,start 是起始位置,end 是末尾位置,如果使用负值则是从右方向到左方向的偏移。

string greeting = "hello, world";
string s1 = greeting[0 : 5];    // => "hello"
string s2 = greeting[-5 : -1];  // => "worl"

当然使用 [index] 访问字符串的索引,访问字符串的某个字节,但是不能给字符串中的某个字节赋新值,因为 Vala 字符串不能改动。

许多基本类型都可以转换为字符串类型,或者字符串类型转换为其类型:

// 字符串型转换为基本数据类型
bool b = bool.parse("false");
int i = int.parse("21323");
double d = double.parse("3.14");
string s1 = i.to_string();
string s2 = 22222.to_string();

真是神奇,还可以直接从常量使用 to_string() 转换为字符串类,还可以使用 in 操作符来判断一个字符串是否被另一个字符串所包含:

string s1 = "keep on";
string s2 = "keep";
bool contains = s2 in s1;  // true

其他高级特性可以从这里了解一下:https://valadoc.org/glib-2.0/string.html

数组

定义二维数组首先要指定一个数据类型,然后紧跟着 [] 符号,用 new 操作符来创建,比如 int[] arr = new int[10],创建了一个长度为 10 的整型数组。数组的长度包含在数组中的 length 数据成员中,比如 int count = arr.length,所以定义数组的方法就是:

数据类型[] 数组名 = new 数据类型[数组长度]

int[] a = new int[5];
int[] b = { 22, 222, 2222, 333, 444 };

它跟字符串类似,可以使用 [start : end] 来分割数组,而且不影响源数组:

int[] c = b[1 : 3];

类型推断

Vala 拥有类型推断的机制,定义一个变量时只需用 var 关键字声明一个模糊不清的类型,而不需要指定一个具体类型,具体类型取决于赋值表达式右边,这种机制有助于在不牺牲静态类型功能的前提下减少不必要的沉余代码;在 C++11 新标准后也有对应的 auto 关键字,让编译器帮助我们分析表达式的类型。

var a = new Application();  // 等价于 Application a = new Application();
var s = "hello";            // 等价于 string s = "hello";
var list = new List<int>();

缺点就是只能用于局部变量,优点就是让风格更简洁,对于那些拥有多参数类型的方法、长名称的类...

操作符

=

赋值操作符,操作符左边必须是标识符,右边必须是相应类型的值或者引用。

+, -, *, /, %

基本算术运算符,需要提供左右操作数,符号 + 可用于串联字符串。

+=, -=, /=, *=, %=

算术运算符,用在两个操作数中间。

++, --

自增、自减操作符,可以放在操作数的前面或后面,分别代表先运算赋值再返回值,和先返回值再运算赋值。

|, ^, &, ~, |=, &=, ^=

位操作符,分别位按位或、按位异获、按位与、按位取反,第二组带等号的操作符和前面的算术运算符类似。

<<, >>

位移操作符,对操作数左边的数字按位移动操作符右边数值大小的位数。

<<=, >>=

位移操作符,对该标志符的值按位移动操作符右边数值大小的位数,并将结果
的值赋予操作符左边的标志符。

==

相等操作符

<, >, <=, >=, !=

不等操作符,根据符号代表的意思来对左右两边不等的形式,返回结果为 bool 型的值。

!, &&, ||

逻辑操作符,分别为逻辑非、逻辑与、逻辑或,第一个操作数为单个,其他两个则是两个操作数。

?:

三元条件操作符,比如 condition ? value if true : value if false

??

空否操作符,表达式 a ?? b 等价于 a != null ? a : b,这个操作符在引用一个空值的时候非常有用:

stdout.printf("Hello, %s!\n", name ??? "unknown person");

in

此操作符判断右操作数是否包含左操作数,适用于 arrays, strings, collections 等其他拥有 contains() 方法的类型,用于 string 类型的时候代表字符串匹配。

控制结构

Vala 基本控制结构包括:顺序结构、选择结构、循环结构;顺序结构就是按照顺序执行每一条语句;选择结构就是给定的条件进行判断,然后根据判断结果决定执行哪一段代码;循环结构就是在条件成立下反复执行某一段代码。

while (a > b) { a--; }

do { a--; } while (a > b);

for (int i = 0; i < 100; ++i) {
    // 跳过本次循环
    if (i == 60) {
        continue;
    }

    // 终止循环
    if (i == 70) {
        break;
    }
    stdout.printf("%d\n", i);
}

switch (a) {
  case 1:
     stdout.printf("one\n");
     break;
  case 2:
     stdout.printf("two\n");
     break;
  default:
     stdout.printf("unknown\n");
     break;
}

可以看到这跟 C 语言语法是一样的,包括 if else... 有一点 C 程序员需要注意的:条件判断必须是以布尔型为准,比如判断一个非0或者非 null 条件的时候,必须显式调用,比如:if (object != null) 或 if (number != 0),而不是 C 程序员惯用的 if (!object) 和 if (!number) 形式。

方法

在 Vala 中函数被称为方法(method),不论是定义在类中还是类外,Vala 官方文档坚持使用“方法”这个术语。

int method_name(int arg1, Object arg2) {
    return 1;
}

所有 Vala 中的方法都有对应 C 的函数,所以可以随意附带任意数量的参数;Vala 不支持方法重载,就是不允许多个方法拥有相同的方法名,但是可以通过提供的“默认参数”特性来完成,只需定义一个方法并定义参数的默认值,之后不需要显式的指明参数值就可以调用方法。

void f(int x, string s = "hello", double z = 0.5) { }

调用此方法的可能情况有:

f(2);
f(2, "world");
f(2, "world", 0.15);

匿名方法

匿名方法(Anonymous Methods)也被称为 lambda 表达式,或者闭包,在 Vala 中可以使用 => 符号来定义。

命名空间

命名空间的用法和 C++ 没什么两样:

namespace NamespaceName {
    // ...
}

以上两个中括号中所有元素都属于命名空间 NamespaceName 的,在外部代码中引用其命名空间中的内容必须使用完整的命名,或者在文件中导入命名空间后直接使用,比如:"using Gtk",导入了一个名为 Gtk 的命名空间,那么就可以简单使用:Window 来替代 Gtk.Window。但在某些情况下还是推荐用完整名,避免歧义,比如 GLib.Object 和 Gtk.Object,在 Vala 编程中,命名空间 GLib 是被默认导入的。

Vala 定义类的方法和 C# 是类似的,与结构体不同,基于类的的内存是在堆上分配的,一个简单类可以这么定义:

public class MyClass : GLib.Object {
    public int first_data = 0;
    private int second_data;

    // 构造函数
    public MyClass() {
        this.second_data = 0;
    }

    // 方法
    public int method_1() {
        return this.second_data;
    }
}

该类是 GLib.Object 的子类,拥有 GLib.Object 所有成员,由于继承了 GLib.Object,那么 Vala 中某些特性可以直接访问 Object 的特性来获取。

此类被声明为 public 型,此声明表示该类可以被外部代码文件直接引用,等同于将该类声明在一个 C 代码的头文件中。

Vala 提供了四个不同的访问权限修饰符:

关键字 描述
public 没有访问限制
private 仅限于类内部,没有指明访问修饰符情况下默认是 private
protected 访问仅限该类,或者继承自该类的内部
internal 访问仅限于从属同个包的类

使用类的方法:

MyClass t = new MyClass();
t.first_data = 10;
t.method_1();

由于 Vala 不支持重载方法,因此重载构造方法也是不支持,Vala 支持命名构造方法,如果需要提供多个构造方法只需要增加不同的命名即可:

public class Button : Object {
    public Button() {
    }

    public Button.width_label(string name) {
    }

    public Button.from_stock(string stock_id) {
    }
}

// 实例化方法
new Button();
new Button.width_label("click me");
new Button.from_stock(Gtk.STOCK_OK);

虽然 Vala 负责管理内存,但是还是提供析构方法,定义析构的方法和 C#、C++ 类似:

class Demo : Object {
    ~Demo() {
        stdout.printf("in destructor");
    }
}

信号

信号这个特性是 GLib 库中的 Object 类提供的,可以继承该类来使用这个特性,Vala 的信号相当于 C# 的事件。

信号是作为一个成员在类中定义的,信号处理方法可以使用 connect() 方法来进行动态添加:

public Test : GLib.Object {
    public signal void sig_1(int);

    public static int main(string[] args) {
        Test t1 = new Test();

        t1.sig_1.connect((t, a) => {
            stdout.printf("%d\n", a);
        });

        t1.sig_1(100);
        return 0;
    }
}

以上代码新建了一个 Test 类,然后将一个信号的处理方法赋给 sig_1 成员,在这使用了 lambda 表达式,从这个 lambda 表达式来看,此方法接受了两个参数,分别是 t 和 a,并没有指明参数类型。目前只能使用 public 修饰符,发出信号直接调用 signal 类型。

泛型

Vala 有一套运行时泛型系统,某个类的实例指定一个或一组数据,比如可以实现一个针对特定类型对象的链表:

public class Wrapper<G> : GLib.Object {
    private G data;
    public void set_data(G data) {
        this.data = data;
    }
    public G get_data() {
        return this.data;
    }
}

为了实例化该类,必须指定一个数据类型,比如 Vala 内置的 string 类型,可以简单如下创建实例:

var wrapper = new Wrapper<string>();
wrapper.set_data("test");
var data = wrapper.get_data();

容器

Gee 是由 Gnome 使用 Vala 开发的一套容器类库,如果要使用 Gee 需要在系统中安装该库,然后链接 Gee 库,需要在编译的时候加上 --pkg gee-0.8 参数。

基本容器类型有:

关键字 描述
Lists 一组相同类型有序元素,使用索引来访问
Sets 一组无序任意类型元素
Maps 一组无序任意类型元素,使用索引来访问

对于容器来说普遍接口是可送代接口(Iterable),也就是可以使用一组标准的方法来遍历容器类中的各个存储对象,或者使用 Vala 提供的 foreach 语法。

多线程

Vala 程序可以拥有多个线程,允许在同一时间内做多个事情,可以通过使用 GLib 中的 Thread 类静态方法来完成。

void *thread_func() {
    stdout.printf("Thread running!\n");
    return null;
}

int main(string[] args) {
    if (!Thread.supported()) {
        stderr.printf("Cannot run without threads.\n");
        return 1;
    }

    try {
        Thread.create(thread_func, false);
    } catch (ThreadError e) {
        return 1;
    }

    return 0;
}

这是一个简单的程序,尝试创建运行一个新线程,代码在 thread_func() 方法中执行,在运行时检查线程是否支持,为了支持多线程,可以在编译时加上下面的参数:

valac --thread simple.vala

为了等待一个线程完成,可以使用 join() 方法来完成。

使用 GLib 库

GLib 包含了大量的工具,包含绝大多数标准 C 函数实现的封装,这些工具可以用于 Vala 平台上,其中参考是基于 GLib 的 C API 形式:

C API Vala Example
g_topic_foobar() GLib.Topic.foodbar() GLib.Path.get_basename()

相关链接

官方文档:https://wiki.gnome.org/Projects/Vala/Documentation

源码地址:https://github.com/GNOME/vala

Vala 语言中一些好玩的:https://segmentfault.com/a/1190000004179876

文章来源:https://rekols.github.io/2018/10/28/vala-beginner/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章