Vala 語言中一些好玩的

因爲我對不一樣的編程語言涉足不廣,所以文中我認爲是好玩的東西可能在其餘語言中早已存在。能夠這樣理解我說的『好玩』,因爲 Vala 語言是編譯到 C 的,所以凡是 C 語言中沒有的東西,在我看來均可能是好玩的……這個基線可能真的是過低了,C 語言彷佛除了指針以外,彷佛沒有什麼特性 :)html

謹慎用於生產

已經搞了 9 年的東西,到如今也沒有一份像樣的文檔。現有的文檔裏,常常遇到過期的代碼。算法

與 C 的關係

Vala 編譯器能夠將 Vala 代碼編譯爲 C 代碼。這些 C 代碼調用了 GLib 與 GObject 庫中的函數,所以系統中須要安裝這些庫,才能用 C 編譯器將 C 代碼編譯爲可執行文件。編程

Vala 語言能夠用於編寫 C 庫,由於 Vala 編譯器能生成庫的頭文件,而後由 C 編譯器產生庫文件。另外,Vala 語言也能生成 Vala 的 API 文件,這樣一來,用 Vala 語言寫的庫能夠直接在 Vala 代碼中調用。c#

對於現有的 C 庫,要想在 Vala 代碼中調用它們,須要手寫 Vala API,也就是一組類或函數的聲明,這個任務對於 C 程序猿而言,不算太繁重。例如:segmentfault

[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 爲他們保留了指針:bash

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 語言愛好者而言,這是真的不能再真的泛型。相關討論見『誰是真泛型』一文。markdown

示例:網絡

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);
        }
}

得益於類型擦除,以致於下面的代碼居然是正確的。session

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 豎中指,我甚至不敢對英語豎中指。好久之前,我花了好幾頓飯錢去考英文四六級,卻不懂中國的文言文。

酒喝多了,就不會再有邏輯。

相關文章
相關標籤/搜索