Vala 语言中一些好玩的

0

由于我对不同的编程语言涉足不广,因此文中我认为是好玩的东西可能在其他语言中早已存在。可以这样理解我说的『好玩』,由于 Vala 语言是编译到 C 的,因此凡是 C 语言中没有的东西,在我看来都可能是好玩的……这个基线可能真的是太低了,C 语言似乎除了指针之外,似乎没有什么特性 :)

谨慎用于生产

已经搞了 9 年的东西,到现在也没有一份像样的文档。现有的文档里,经常遇到过时的代码。

与 C 的关系

Vala 编译器可以将 Vala 代码编译为 C 代码。这些 C 代码调用了 GLib 与 GObject 库中的函数,因此系统中需要安装这些库,才能用 C 编译器将 C 代码编译为可执行文件。

Vala 语言可以用于编写 C 库,因为 Vala 编译器能生成库的头文件,然后由 C 编译器产生库文件。另外,Vala 语言也能生成 Vala 的 API 文件,这样一来,用 Vala 语言写的库可以直接在 Vala 代码中调用。

对于现有的 C 库,要想在 Vala 代码中调用它们,需要手写 Vala API,也就是一组类或函数的声明,这个任务对于 C 程序猿而言,不算太繁重。例如:

[CCode (cheader_filename = "mylib.h")]
public class MyLib : GLib.Object {
    public MyLib ();
    public void hello ();
    public int sum (int x, int y);
}

如果 C 库支持 GObject-Introspection(GIR)规范,借助 Vala 提供的一个工具,可以将 GIR 文件转换为 Vala API 文件。

对于那些对自己控制内存有信心的程序猿,Vala 为他们保留了指针:

int i = 42;
int* i_ptr = &i;    // address-of
int j = *i_ptr;     // indirection

Foo f = new Foo();
Foo* f_ptr = f;    // address-of
Foo g = f_ptr;     // indirection

unowned Foo f_weak = f;  // equivalent to `Foo* f_ptr = f'

Object* o = new Object();
o->method_1();
o->data_1;
delete o;

泛型

Vala 支持泛型,采用的技术与 Java 相似,即类型擦除。他人觉得这是伪泛型,而对于我这种死不悔改的 C 语言爱好者而言,这是真的不能再真的泛型。相关讨论见『谁是真泛型』一文。

示例:

public interface With <T> {
        public abstract void sett(T t);
        public abstract T gett();
}


public class One : Object, With <int> {
        public int t;
        
        public void sett(int t) {
                this.t = t;
        }
        public int gett() {
                return t;
        }
}

public class Two <T, U> : Object, With <T> {
        public T t;
        
        public void sett(T t) {
                this.t = t;
        }
        public T gett() {
                return t;
        }
        
        public U u;
}

public class Test : GLib.Object {
        
        public static void main(string[] args) {
                var o = new One ();
                o.sett(5);
                stdout.printf("%d\n", o.t);
                
                var t = new Two <int, double?> ();
                t.sett(5);
                stdout.printf("%d\n", t.t);
                
                t.u = 5.0f;
                stdout.printf("%f\n", t.u);
        }
}

得益于类型擦除,以至于下面的代码竟然是正确的。

class TestClass : GLib.Object {
}

void accept_object_wrapper(Wrapper<Glib.Object> w) {
}

var test_wrapper = new Wrapper<TestClass>();
accept_object_wrapper(test_wrapper);

容器

Vala 实现了一些常用的泛型容器,例如单向列表、双向列表、并发式列表、队列、哈希表、红黑树等。这些容器是以库的形式实现的,这个库叫 Gee,其 API 文档见 http://valadoc.org/#!wiki=gee-0.8/index

引用

在上述的泛型示例中的模板实例化代码 Two <int, double?> 中,double 后面跟随了一个 ? 号。这是因为模板参数只能是指针或者与指针等宽的值类型(例如整型、布尔型、引用等类型)。只要某种类型名称具有 ? 后缀,那么 Vala 编译器会自动将其转化为 C 指针类型。除此以外,Vala 编译器会将所有的 Class 视为指针类型。由于大家都很恐惧 C 指针,所以可以将我说的『指针』理解为『引用』。

如果从 C++ 的角度来理解,可以认为 Vala 自动将占用很多字节的那些类型转换为引用,也就是说在代码中,你不需要显示的去将某种类型设定为引用。对于一个类的实例,也就是所谓的对象,即使你不想去引用它,只是想让它以一个值的形式存在,但是 Vala 说 no,它必须是引用。例如,有一个 Vala 类 TestClass,要将它实例化,Vala 只提供一种途径:

TestClass t = new TestClass();

Vala 编译器会将这行代码翻译为以下 C 代码:

TestClass* t = NULL;
TestClass* _tmp0_ = NULL;
_tmp0_ = testclass_new ();
t = _tmp0_;

如果你声明一个函数 foo,它的参数是一个类类型:

void foo(TestClass t) {
        ... ... ...
}

Vala 会将其转换为以下 C 代码:

void foo (TestClass* t) {
    g_return_if_fail (t != NULL);
    ... ... ...
}

你要注意的是,Vala 不仅会自动的将函数参数中的类类型转换为指针类型,而且还会检测指针是否为 NULL(下文还要重提此事)。

Vala 将 C 指针掩盖的非常好。这样做的好处就是,将对象作为参数传递给函数或赋给同类,不需要显式的进行引用设定(还记得 C++ 代码中漫天飞的 & 么?),也不需要考虑 Copy 构造函数的那堆破事。对象就是用来被引用的,而对象的基本成分(它们的类型往往是语言内建的那些基本类型)可以被复制,这类似于你可以 clone 人体器官,但是人类禁止你去 clone 一个人。

命名空间

没什么好说的,我认识的比 C 高级一点的语言差不多都有这个空间。Vala 的命名空间也没什么特殊之处,它是这样的:

namespace NameSpaceName {
        ... ... ...
}

要使用某个命名空间,可以这样:

using NameSpaceName;

命名空间可以有效降低用户自定义标识符对全局命名空间的污染。Vala 的命名空间也可以嵌套:

namespace NameSpaceName_0 {
        namespace NameSpaceName_1 {
                ... ... ...
        }
}

闭包

没什么好说的,要将匿名函数作为值来用,需要配合 delegate。总之以后可以玩高阶函数抽象了。

delegate int IntOperation(int i);

IntOperation curried_add(int a) {
    return (b) => a + b;  // 'a' is an outer variable
}

void main() {
    stdout.printf("2 + 4 = %d\n", curried_add(2)(4));
}

面向对象

Vala 的面向对象,单纯从类型的角度看,其实是面向引用或面向指针。因为每个对象都是在堆中分配的内存,在栈中引用。

在面向对象的世界里,函数就不叫函数了,而是叫方法。面向对象编程范式与泛型编程范式对立之处就在函数应该是函数,还是方法?将函数称为方法,就是在承认有一类东西,它有一些处理事务的方法。将函数称为函数,就是承认有一些公式,可将数据代入公式里,然后由公式产生相应的数据,这些公式能处理不同类型的数据。我总觉得所谓的泛型编程,只不过是在静态类型语言上发展起来的一种不支持高阶函数的函数式编程范式……不再谈泛型编程了。

Vala 为面向对象编程范式提供了类、信号、单重继承、抽象类与抽象方法、虚方法、接口、运行时类型识别等元素。其实没啥好说的,这个时代谁还不知道面向对象那些事……值得一提的是 Vala 支持 Mixin 的方式来模拟『多重继承』。

再值得提的是一个小小的语法糖——运行时类型转换:

class Foo : Glib.Object {
    public  void my_method() {stdout.printf("foo\n");}
}

class Bar : Foo {
    public new void my_method() {stdout.printf("bar\n");}
}

void main() {
    var bar = new Bar();
    bar.my_method();
    (bar as Foo).my_method();
}

其中 bar as Foo 就是将 bar 的类型转换为其父类类型 Foo。由于 Vala 具有运行时类型识别的功能,因此可在一定程度上保证类型转换的合理性。再看个例子:

Button b = (widget is Button) ? (Button) widget : null;

契约式编程

在语法层面上支持契约,要比在函数体内写上一堆条件判断语句优雅一些。

double method_name(int x, double d)
        requires (x > 0 && x < 10)
        requires (d >= 0.0 && d <= 1.0)
        ensures (result >= 0.0 && result <= 10.0)
{
    return d * x;
}

Vala 会对函数的引用类型的参数自动进行 null 检测。用 C 语言来说,就是 Vala 可以自动检测指针类型的参数是否为 NULL指针。那些带 ? 后缀的类型,Vala 不会检测它们是否为 null。因此,怎么利用 Vala 的这个 null 检测特性,自己看着办。

参数的方向

Vala 中的函数可以接受 0 个或多个参数,值类型的参数会被复制,引用类型的参数不会被复制。函数参数的这种默认机制会被用两个修饰符 outref 修改。例如:

void method(int a, out int b, ref int c) { ... }

int b 本来是值类型的参数,被 out 改成引用类型,而且 b 可以是未初始化的变量,在 method 函数内部可以对 b 进行赋值,这样 b 就是 method 的一个结果。int c 本来是值类型的参数,被 ref 改成了引用类型,但它必须是已经初始化了的变量,method 可以修改它。

进程通信

Vala 集成了 D-Bus,看下面的示例:

[DBus(name = "org.example.DemoService")]
public class DemoService : Object {
    /* Private field, not exported via D-Bus */
    int counter;

    /* Public field, not exported via D-Bus */
    public int status;

    /* Public property, exported via D-Bus */
    public int something { get; set; }

    /* Public signal, exported via D-Bus
     * Can be emitted on the server side and can be connected to on the client side.
     */
    public signal void sig1();

    /* Public method, exported via D-Bus */
    public void some_method() {
        counter++;
        stdout.printf("heureka! counter = %d\n", counter);
        sig1();  // emit signal
    }

    /* Public method, exported via D-Bus and showing the sender who is
       is calling the method (not exported in the D-Bus interface) */
    public void some_method_sender(string message, GLib.BusName sender) {
        counter++;
        stdout.printf("heureka! counter = %d, '%s' message from sender %s\n",
                      counter, message, sender);
    }
}

void on_bus_aquired (DBusConnection conn) {
    try {
        // start service and register it as dbus object
        var service = new DemoService();
        conn.register_object ("/org/example/demo", service);
    } catch (IOError e) {
        stderr.printf ("Could not register service: %s\n", e.message);
    }
}

void main () {
    // try to register service name in session bus
    Bus.own_name (BusType.SESSION, "org.example.DemoService", /* name to register */
                  BusNameOwnerFlags.NONE, /* flags */
                  on_bus_aquired, /* callback function on registration succeeded */
                  () => {}, /* callback on name register succeeded */
                  () => stderr.printf ("Could not acquire name\n"));
                                                     /* callback on name lost */

    // start main loop
    new MainLoop ().run ();
}

将这个示例编译为可执行文件 vala-dbus-demo,然后在终端中运行它:

$ valac --pkg gio-2.0 vala-dbus.vala
$ ./vala-dbus-demo

vala-dbus-demo 运行后不会停止,因为它借助 Vala 提供的主事件循环变成了一个始终在后台运行的程序。

这时,再打开一个终端,执行以下命令:

$ dbus-send --type=method_call                   \
              --dest=org.example.DemoService       \
              /org/example/demo                    \
              org.example.DemoService.SomeMethodSender \
              string:'hello world'

那个正在运行 vala-dbus-demo 的终端便会作出响应:

eureka! counter = 1, 'hello world' message from sender :1.514

线程

摩尔定律失效了,多核时代了,大数据时代了,并行/并发计算是王道了,函数式编程的机遇来了……这些口号已经喊了多少年了?

我的多核是这样用的,一个核心在刷网页,两三个核心有时在更新我的 Gentoo 系统(我设置了 make -j3)。谁说多核时代就必须让每个程序都是多线程,同时运行几个进程不行么?多线程的程序已经运行了几十年了,早在 CPU 单核时代它们已经在运行了……

这个世界,也许任何一个行业不会比 IT 业更善于吹牛,而且吹牛的人自己都信了……即使每个 CPU 都是单核的,很多台机器构成的分布式网络计算也已经搞了几十年了……如果你用 Gentoo,很容易就用到分布式计算了,例如 distcc,你可以让很多个别人的机器为你的机器编译软件包。

Haskell 依然没多少人用,Lisp 依然还停留在 SICP 里,大家还在一边说着 Rust 与 Nim 的爹不怎么样,一边在黑爹很厉害的 go 语言……

不管怎么样,C11 标准中的多线程,现在 GCC 依然不支持。C++ 11 的多线程似乎已经支持了,但是支持与不支持还有什么区别么?连 Vala 这芝麻大的语言都支持多线程:

using GLib;

public class MyThread : Object {
    public int x_times { get; private set; }

    public MyThread (int times) {
        this.x_times = times;
    }

    public int run () {
        for (int i = 0; i < this.x_times; i++) {
            stdout.printf ("ping! %d/%d\n", i + 1, this.x_times);
            Thread.usleep (10000);
        }

        // return & exit have the same effect
        Thread.exit (42);
        return 43;
    }
}

public static int main (string[] args) {
    // Check whether threads are supported:
    if (Thread.supported () == false) {
        stderr.printf ("Threads are not supported!\n");
        return -1;
    }

    try {
        // Start a thread:
        MyThread my_thread = new MyThread (10);
        Thread<int> thread = new Thread<int>.try ("My fst. thread", my_thread.run);

        // Wait until thread finishes:
        int result = thread.join ();
        // Output: `Thread stopped! Return value: 42`
        stdout.printf ("Thread stopped! Return value: %d\n", result);
    } catch (Error e) {
        stdout.printf ("Error: %s\n", e.message);
    }

    return 0;
}

这个世界上,没有任何一个行业会比 IT 业善于吹牛!整个机械设计制造及其自动化只是沉默于牛顿力学与电学基本定律的基础之上,而 IT 业有过程式编程,面向对象编程,设计模式,泛型编程,函数式编程,并行/并发编程,同步/异步编程,还有神经网络,遗传算法,机器学习,特征识别,数据挖掘,还有分布式计算,网格计算,云计算,GPU 计算……整个世界非 IT 业的名词术语加起来不见得比 IT 业多。

总之,Vala 很容易的就实现了多线程编程……因为 GLib 封装了 pthread 与 Windows 的多线程机制,GLib 与 GObject 就是 Vala 世界的牛顿力学与电学基本定律。

异步

当一个线程调用的一个异步函数时,该函数会立即返回尽管其规定的任务还没有完成,这样线程就会执行异步函数的下一条语句,而不会被挂起。

异步函数所规定的工作是如何被完成的呢?当然是通过一个新的线程完成的。那么这个新的线程是从哪里来的呢?我们可以在异步函数中新创建一个线程。例如:

async void list_dir() {
    var dir = File.new_for_path (Environment.get_home_dir());
    try {
        var e = yield dir.enumerate_children_async(
            FileAttribute.STANDARD_NAME, 0, Priority.DEFAULT, null);
        while (true) {
            var files = yield e.next_files_async(
                 10, Priority.DEFAULT, null);
            if (files == null) {
                break;
            }
            foreach (var info in files) {
                print("%s\n", info.get_name());
            }
        }
    } catch (Error err) {
        warning("Error: %s\n", err.message);
    }
}

void main() {
    var loop = new MainLoop();
    list_dir.begin((obj, res) => {
            list_dir.end(res);
            loop.quit();
        });
    loop.run();
}

BTW,Vala 提供了异常机制,即 try ... cactch

内存管理

Vala 充分利用了 GObject 的内存引用计数机制,实现了垃圾自动回收,亦即 GC。

没有任何悬念,凡是基于引用计数的垃圾内存回收机制都无法处理循环引用情况,而解决这个问题的主流办法就是借助『弱引用』。不解决这个问题可不可以?

我觉得没什么不可以的,就连 C++ 领域的大神(微软的人且身兼 C++ 标准委员会的委员)都觉得内存泄漏一点也是无所谓的,详见:http://flyingfrogblog.blogspot.co.uk/2013/10/herb-sutters-favorite-c-10-liner.html

但是,Vala 依然中规中矩的借助弱引用来解决这个问题。

如果人类都不想自己去做垃圾回收这种脏活,那就意味着世界上最好的 GC 算法也无法回收它无法回收的一部分内存。

与内存的紧俏相比,多核时代……即使有 10000 个核也不如弱引用管用。

我打算洗洗睡了,无论有多少个核,无论有多少种内存回收算法,也无法解决 Linux 世界的中文输入法问题。

你知道我要打这些字的前提是什么吗?我要 pkill fcitx,然后 source ~/.xprofile,然后再 fcitx &, 最后再 emacs my.markdown。别问我为什么,现在我不这样做我就无法在 GNOME 3 环境中的 Emacs 里输入中文。

没有任何悬念,中文输入永远都是中文 Linux 用户的痛,除非内核层面就原生的支持各国语言的输入法。Linus 可以对 Nvidia 竖中指,我却不敢对 Linus 竖中指,我甚至不敢对英语竖中指。很久以前,我花了好几顿饭钱去考英文四六级,却不懂中国的文言文。

酒喝多了,就不会再有逻辑。


如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

chemzqm · 2015年12月22日

整个世界非 IT 业的名词术语加起来不见得比 IT 业多?

回复

nerdneilsfield · 2016年11月03日

你说的很对啊,现在互联网炒作概念实在是太过分了啊。

回复

载入中...