3

Vala语言

 2 years ago
source link: https://z-rui.github.io/post/2016/06/vala/
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.
neoserver,ios ssh client

Vala语言

Wed Jun 8, 2016

Vala是一个建立在GLib上的语言。GLib是基于C语言的基础功能库,和Boost之于C++的地位有几分类似。GLib除了提供了一些数据结构、线程操作、输入输出外,还有一套完全基于C语言的对象系统GObject。在此基础之上,才有了GTK+,这个在Linux上比较流行的图形工具包。

GLib很不错,善用之可以避免重新造很多轮子。GObject可能是摆脱C++后不得已的选择,因为图形界面这样的东西太依赖于面向对象的特性了。它们都基于C,设计得或许很漂亮,可用起来就比较麻烦了,因为C编译器不像C++那样能自动生成代码。例如,C++的shared_ptr可以以几乎透明的方式实现引用计数:每一份拷贝在进入和退出作用域的地方,C++编译器可以自动插入增加引用和减少引用的代码。换作C的话,则需要在代码中明确写出来。

为了减轻开发人员的负担,提高开发效率,Vala被发明出来,它模仿C#的语法,在语法层面上支持面向对象。值得指出的是,Vala的编译器先将vala源文件转换成C源文件,然后调用C编译器来编译。因为幕后是C语言,所以一方面不会带来性能上的退化,另一方面也不会引入兼容性的困难。

下面说说Vala的几个特点。

首先,Vala的基础设施建立在GLib上。具体表现在

  • 所有内置(built-in)类型都是GLib定义的类型,例如intgintboolgboolean,等等。
  • 内置的数据结构,例如列表List,基于GLib的GList,但是语法上增加了泛型的支持。因此用起来要更加方便些。
  • 动态对象的分配和释放,都是使用GLib提供的函数g_malloc, g_free等等。从这个角度看,Vala可以看作是一个对GLib的封装。

其次,Vala的面向对象机制非常有趣。它支持很多种设计。一方面是基于GObject的面向对象模型。如果要实现一些高级的面向对象的功能,这个类必须声明为GObject的子类。这部分可以看作是对GObject的封装。对GTK+的支持也就是从这里而来的。使用Vala编写GTK+程序非常容易:

void main(string[] args)
{
	Gtk.init(ref args);
	var window = new Gtk.Window();
	window.destroy.connect(Gtk.main_quit);
	window.show_all();
	Gtk.main();
}

我还用Vala结合GTK+写了一个Nim游戏作为练习。

另一方面,Vala还可以把很多并非基于GObject的C语言库,用面向对象的方法封装起来。

很多程序库出于兼容性考虑,使用C语言的API,但是设计上采用的是面向对象的思路:

Foo *x = Foo_new(42);
Foo_bar(x, baz);
Foo_free(x);

一般情况下,较少用到高级的面向对象的特性,所以C语言已经足够。使用Vala可以将其封装为

Foo x = new Foo(42);
x.bar(baz);
// 自动释放

和Java/C#的语法很像了。而且可以在作用域的末尾自动释放这个对象,相当于C++中的RAII的思路。

用C++的语法,如果使用RAII,可以表述为

ClassFoo x(42);
x.bar(baz);

问题在于,C++不允许向已有类型绑定新的方法。所以必须另外编写一个新的类,其中包含原来的Foo类型的一个指针,然后,为原来所有的API调用编写对应的方法。即

class ClassFoo {
	Foo *m_ptr;
	Foo(int arg)
	{
		m_ptr = Foo_new(arg);
	}
	~Foo()
	{
		Foo_free(m_ptr);
	}
	int bar(int baz)
	{
		return Foo_bar(m_ptr, baz);
	}
};

诸如此类。但是,写成这样还不够。因为存在析构函数,所以还要编写相应的拷贝构造函数(copy constructor)和拷贝赋值运算符(copy assignment operator),它们被称作C++的三大件(the big three)。这是一件很有挑战性的事情。想要达到的目的是:

Foo x(42); // x.m_ptr = xxx;
Foo y = x; // x.m_ptr = 0; y.m_ptr = xxx;
x = y;    // x.m_ptr = xxx; y.m_ptr = 0;

也就是说,等号右边的变量将要失去对原对象的引用,因为同一时刻只能存在一份引用(类似unique_ptr的语义),否则当作用域退出时,两个对象分别调用他们的析构函数,就会造成一个对象被释放多次,引发内存错误。

与此同时,我们有时候希望用一个新的对象覆盖掉原来的对象,即

Foo x(42);   // x.m_ptr = xxx;
x = Foo(43); // x.m_ptr = yyy; xxx已被释放;

然而,这个功能想要实现却不太容易。我认为这是C++设计上的一个缺陷(C++11引入了一些新机制来解决这个问题,但却让语法更加复杂了)。

Vala采用了另一种策略,不是试图兼容C的语法,然后在此基础上创建一个新的类进行封装,而是描述C的API采用了什么样的约定,从而自动生成对应的面向对象的模型。在上面的例子中,只要知道

  1. 类名Foo
  2. 类的方法的调用约定:x.bar(baz) -> Foo_bar(x, baz)
  3. 创建函数Foo_new, 释放函数Foo_free

只需在.vapi文件中写:

[Compact]
class Foo {
	public int bar(int baz);
	public Foo(int arg);
}

那么Vala编译器就能自动地将vala代码

Foo x = new Foo(42);
x.bar(baz);

转换为C代码

_tmp0_ = Foo_new (42);
x = _tmp0_;
Foo_bar (x, baz);

这是纯粹的代码转换,不会增加额外的胶水代码。当然,机器生成代码未必适合人阅读,但编译后的效果是一样的。

和C++一样,这里也会遇到拷贝赋值的问题。这里分为两种情况:

  1. 对象支持引用计数,则拷贝赋值对应于引用计数增加。
  2. 对象提供了拷贝函数,则拷贝赋值对应于调用该函数。

不满足上述条件的对象无法被拷贝赋值。即便如此,仍然支持两种操作:

  1. 用一个新的对象覆盖原来的对象。这里并不涉及拷贝的问题。即
x = new Foo(43);
  1. (owned)修饰符转移所有权。即
x = (owned) y;

这样y的值将传递给x,自己则变为null

C++11发明了一些新的规则(移动构造函数,移动赋值运算符)来实现类似的语义。它试图用一种通用的规则,从原理上处理这个问题。一定程度上讲,继承了C语言“只提供机制,不提供政策”的哲学。

对于C和C++而言,很多看似不解的问题,只要从大的规则向下推理,答案总是很清楚,没有什么例外。这是这两个语言美的地方。为什么C++98中x = Foo(43)不可行?因为表达式Foo(43)的类型是Foo,和其他相同类型的表达式(例如,y),地位上没有任何区别。而这是一个赋值表达式,按照语法规则,必须调用Foo类中的拷贝赋值运算符。如果没法调用这个运算符,那就没有办法了。虽然大家心里都清楚,Foo(43)只是一个临时的对象,这个赋值只是为了重写x罢了,不存在“拷贝”的问题(和x = y不同,这个赋值之后显然多了一份拷贝),但是C++编译器的目的不是“尽最大的可能实现代码的语义”,而是“严格遵守语法规则来解释代码”。就是那样不近人情,一丝不苟。

而Vala则相反,以实现功能为主要目的,更加实在些(毕竟没有想要成为通用语言的野心)。



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK