Учебник Vala
Contents
-
Учебник Vala
- Введение
- Первая программа
- Основы
- Object Oriented Programming
- Advanced Features
- Экспериментальные фичи
Введение
Отказ от ответственности: Vala всё ещё находится в разработке, и её функции могут измениться. По возможности учебник обновляется в соответствии с текущим положением, но это не всегда является возможным. Также методы описанные в учебнике могут быть на практике не самыми лучшими, но будем пытаться идти в ногу со временем.
Что такое Vala?
Vala - это новый язык программирования, предназначенный для прикладного и системного программирования на основе библиотек GLib Object System (GObject) рабочей среды GNOME/GTK+. Эта платформа предлагает полноценную программную среду, с такими наворотами как динамическая типизация и сборщик мусора. До появления Vala единственными способами программирования под GNOME/GTK+ было либо использования C API, который предоставлял много лишних деталей, либо использование языков высокого уровня, которые выполняются в сопутствующих виртуальных машинах, таких как Python или Mono C#, или используя C++ c библиотеками-обертками.
Vala отличается от всех других методов, так как она транслируется в C код, который может быть собран для запуска без дополнительной поддержки библиотек за пределами платформы GNOME. Это имеет несколько особенностей, главные из которых:
- Программы, написанные на Vala должны иметь хорошую скорость выполнения, близкую к коду написанному на C, а также их легче и быстрее писать и поддерживать.
- Приложения на Vala не могут делать ничего, чего бы не смогла бы сделать аналогичная программа на C. Несмотря на это Vala предоставляет множество языковых возможностей, которые не доступны в C, хотя они они и транслируются в С конструкции, но на их написание и отладку ушло бы гораздо больше времени.
Таким образом, Vala является современным языком со всеми возможностями, которые вы бы хотели видеть в языке. Она получает функционал от GNOME/GTK+, и в некоторой степени зависит от правил установленным ими.
Кому подойдет этот учебник?
В учебнике не будут рассматриваться основные приемы программирования. Мы только вкратце объясним принципы объектно-ориентированного программирования, а сосредоточим внимание на том, как в Vala они реализуются. Учебник будет полезным, если у вас уже есть опыт программирования, хотя глубоких знаний и не требуется.
Vala многое наследует из синтаксиса C#, но я буду стараться избегать описания функций с точки зрения их сходства или различия с C# или Java, для того чтобы сделать урок более доступным.
Что будет полезным, так это знания C. Хотя это не является необходимым для понимания Vala как таковой, но важно понимать, что Vala программы транслируются в C и часто взаимодействуют с его библиотеками.
Соглашения
Код будет в моноширинном шрифте, приглашение командной строки будет начинаться с $. Кроме того, всё должно быть и так очевидно.
Первая программа
К сожалению, предсказуемо, но всё же: (сохраните в hello.vala)
class Demo.HelloWorld : GLib.Object {
public static int main(string[] args) {
stdout.printf("Привет, Мир!\n");
return 0;
}
}
Конечно же это "helloword" на Vala. Я надеюсь, вы понимаете что здесь происходит, но я все же кратко пройдусь по коду:
class Demo.HelloWorld : GLib.Object {
Этот код определяет класс. Классы в Vala очень похожи по концепции на другие языки. Класс является прототипом объекта, из которого можно создать объект содержащий все определенные в классе свойства и методы. Реализацией классов типов занимается GObject библиотека, но для общего использования знать это не обязательно.
Что важно запомнить, так это то, что этот класс описан как потомок GLib.Object. Это потому что Vala разрешает разные типы классов, но в большинстве случаев это то, что вам нужно. Ведь фактически некоторые возможности Vala доступны только если класс наследуется от GLib.Object.
Другие части это строки показывают пространство имен и полное имя, однако мы остановимся на этом позже.
public static int main(string[] args) {
Это начало определения метода. Метод является функцией, связанной с классом объекта, который может быть выполнен как объект класса. Статический метод означает, что метод может быть вызван не имея конкретного экземпляра этого класса. Тот факт что метод называется main означает, что он является точкой входа в Vala программу.
Метод main не обязан быть определен в классе. Однако если он определен в классе он должен быть static. Не имеет значение является ли он public или private. Тип возвращаемого значения должен быть int или void. Если тип возвращаемого значения void, то неявно всё равно будет возвращаться 0. Массив args содержит аргументы командной строки.
stdout.printf("Привет, Мир!\n");
stdout является объектом пространства имен GLib, и Vala гарантирует его доступность по требованию. Эта строка указывает Vala вызвать метод printf объекта stdout и передает ему строку в качестве аргумента. В Vala, это всегда синтаксис для вызова метода объекта, или для доступа к данным объекта. \n является управляющей последовательностью для перехода на новую строку.
return 0;
return возвращает значение и прекращает выполнение метода "main", который также завершает выполнение программы. Возвращенное методом "main" значение является кодом выхода с программы.
Компиляция и Запуск
Если Vala уже установлен, то для компиляции и запуска нужно сделать следующее:
$ valac hello.vala $ ./hello
valac - это компилятор Vala, который скомпилирует Vala код в бинарный. Окончательный бинарный код будет иметь то же имя, что и исходный файл и может быть напрямую запущен на компьютере.
Основы
Исходники и Компиляция
Код Vala хранится в файлах с расширением .vala. Vala не навязывает такую структуру как язык Java - здесь так же нет концепции пакетов или файлов класса. Вместо структуры, определяемой текстом в отдельных файлах, здесь используются логические конструкции, такие как пространство имен. Во время компиляции Vala кода вы указываете компилятору список необходимых файлов и Vala самостоятельно решит как они должны взаимодействовать.
Это означает, что вы можете использовать столько классов или функций в файле сколько захотите, даже объединяя части разных пространств имен вместе. Конечно, так делать не очень хорошая идея. Есть некоторые соглашения, которым вы, вероятно, захотите следовать. Хорошим примером того, как структурировать проект в Vala является сам проект Vala.
Все исходные файлы одного проекта подставляются в качестве параметров командной строки для компилятора valac, наряду с флагами компилятора. По такому же принципу компилируется исходный код Java. Например:
$ valac compiler.vala --pkg libvala
создаст бинарный файл compiler, который будет сопряжён с libvala. Таким образом собирается компилятор valac!
Если нужно, чтобы у скомпилированного файла было другое имя или если вы указали больше одного исходника для компиляции, то необходимо указать имя выходного файла с помощью ключа -o:
$ valac source1.vala source2.vala -o myprogram $ ./myprogram
Если вы передадите valac ключ -C, то бинарника не будет. Вместо этого будет промежуточный C код для каждого исходника на Vala в соответствующем С файле. В этом случае source1.c и source2.c. Если посмотреть на содержание этих файлов, то можно там увидеть что программирование класса на Vala эквивалентно тому же занятию на С, но в целом более кратко. Вы заметите, что класс регистрируется динамически на запущенной системе. И это хороший пример мощи платформы GNOME, но, как я сказал ранее, чтобы использовать Vala это знать не обязательно.
Если вам нужно иметь заголовочный C файл в вашем проекте, то можете воспользоваться ключом -H:
$ valac hello.vala -C -H hello.h
Обзор синтаксиса
Синтаксис Vala является смесью всего, но в большей степени C#. Как результат, он будет близок программистам, которые знакомы с С-подобными языками, и в свете этого я буду краток.
Область видимости определяется скобками. Объект или ссылка действительны только между { и }. Скобки также являются разделителями, используемыми для определения классов, методов, блоков кода и т.д. так как они автоматически имеют свою область видимости. Vala не строга к месту объявления переменных.
Идентификатор определяется путем указания его типа и имени, например int c подразумевает целочисленный тип и именем c. В этом случае заданного типа также создается объект данного типа. Для ссылочных типов это только определяет новую ссылку, которая изначально не указывает ни на что.
Для имен идентификаторов применяются те же правила, что и для С-идентификаторов: первый символ должен быть [a-z], [A-Z] или символ подчеркивания, последующие символы могут также содержать цифры [0-9]. Другие символы Юникода не разрешены. Есть возможность использовать зарезервированное слово как идентификатор, ставя перед ним символ собаки @. Этот символ не является частью имени. Например, вы можете назвать метод @foreach, однако это зарезервированное слово Vala.
Ссылочные типы инстанцируются с помощью оператора new и имени метода конструктора, который обычно имеет имя типа. Например, Object o = new Object() создает новый Object и делает o ссылкой на него.
Комментарии
Vala разрешает комментирование несколькими способами
// Comment continues until end of line
/* Comment lasts between delimiters */
/**
* Documentation comment
*/
Они обрабатываются так же как и в большинстве других языков, так что не требуют большого объяснения. Комментарии документации в действительности не предусмотрены в Vala, однако генератор документации наподобие Valadoc распознает их.
Типы данных
Рассмотрим в общих чертах два типа данных в Vala: ссылочные типы и value-тип (тип-значение). Эти названия описываются как экземпляры типов ведут себя в системе. (Здесь аналогия между передачей параметров в функцию С++ по ссылке и по значению.) Рассмотрим value-тип. Когда вы присваиваете уже созданный объект (Object А) value-типа новому идентификатору, то создается копия (Object В) уже созданного ранее объекта (Object А). Когда вы присваиваете переменной объект ссылочного типа - то создается только ссылка на уже созданный ранее объект (Object А). Копия объекта (Object В) не создается.
Константы определяются словом const перед типом. Соглашением именования для констант является ALL_UPPER_CASE.
Value-типы
Vala поддерживает множество простых типов, как и многие другие языки:
Байт, char, uchar; они названы char по историческим причинам.
Символ, unichar; 32-битный символ Юникод
Целый, int, uint
Длинный целый, long, ulong
Малый целый, short, ushort
Int с гарантированным размером, int8, int16, int32, int64 также и беззнаковые uint8, uint16, uint32, uint64. Номера показывают длину в битах.
Число с плавающей точкой, float, double
Булев, bool; возможные значения true и false
Составной, struct
Перечисление, enum; представленное целыми значениями, а не классами, как перечисления в Java
Вот парочка примеров:
/* atomic types */
unichar c = 'u';
float percentile = 0.75f;
const double MU_BOHR = 927.400915E-26;
bool the_box_has_crashed = false;
/* defining a struct */
struct Vector {
public double x;
public double y;
public double z;
}
/* defining an enum */
enum WindowType {
TOPLEVEL,
POPUP
}
Большинство этих типов могут иметь разные размеры в зависимости от платформы, кроме целочисленных типов гарантированного размера. Оператор sizeof возвращает размер в байтах, который занимает переменная.
ulong nbytes = sizeof(int32); // nbytes будет равно 4 (= 32 bits)
Вы можете узнать минимальные и максимальные значения численного типа с помощью .MIN and .MAX, например int.MIN и int.MAX.
Строки
Тип данных для строки является string. Строки Vala кодируются в UTF-8 и неизменяемы.
string text = "A string literal";
Vala располагает фичей с названием дословные строки. Эти строки, в которых символ обратной черты не бутут интерпретироваться, обрывы строки будут сохранены и символы кавычек не должны быть замаскированы. Они заключены в тройные кавычки. Возможные отступы после конца строки так же являются ее частью.
string verbatim = """This is a so-called "verbatim string". Verbatim strings don't process escape sequences, such as \n, \t, \\, etc. They may contain quotes and may span multiple lines.""";
Строки с префиксом собаки @ являются шаблонами. Они могут содержать встроенные переменные и выражения с помощью префикса доллара $:
int a = 6, b = 7;
string s = @"$a * $b = $(a * b)"; // => "6 * 7 = 42"
Операторы равенства == и != сравнивают содержание двух строк, в противовес Java, которая в этом случае сравнивает на равенство ссылки объектов.
Вы можете обрезать строки с помощью [начало:конец]. Отрицательные числа будут олицетворять относительные позиции от конца строки:
string greeting = "hello, world";
string s1 = greeting[7:12]; // => "world"
string s2 = greeting[-4:-2]; // => "or"
Кстати, индексы в Vala начинаются с 0 как и в большинстве других языков. Начиная с Vala 0.11 вы можно получить доступ к одиночному байту строки с помощью [index]:
uint8 b = greeting[7]; // => 0x77
Однако вы не можете вставить другой байт на это место, так как Vala строки неизменяемы.
Множество базовых типов имеют причинные метода для парсинга и конвертации в строку, например:
bool b = bool.parse("false"); // => false
int i = int.parse("-52"); // => -52
double d = double.parse("6.67428E-11"); // => 6.67428E-11
string s1 = true.to_string(); // => "true"
string s2 = 21.to_string(); // => "21"
Два полезных метода для написания и чтения строк в/из консоли (и для вашего первого исследования Vala) stdout.printf() и stdin.read_line():
stdout.printf("Hello, world\n");
stdout.printf("%d %g %s\n", 42, 3.1415, "Vala");
string input = stdin.read_line();
int number = int.parse(stdin.read_line());
Вы уже знаете stdout.printf() из Hello World примера. На самом деле эта штука берет произвольное число аргументов разных типов, в то время как первый аргумент является строкой форматирования, и следует тем же правилам, что и для форматирования строк С. Если вам нужно выводить сообщения об ошибках, вы можете использовать stderr.printf() вместо stdout.printf().
В дополнение операция in может быть использована для определения содержит ли одна строка другую:
if ("ere" in "Able was I ere I saw Elba.") ...
Для более подробной информации, обращайтесь к полному обзору класса string.
Доступна также простая программка, демонстрирующая работу со строками.
Массивы
Массив объявляется с помощью имени и следующего далее [] и создается с помощью оператора new. Например, int[] a = new int[10] для создания массива целых. Длинна конкретного массива может быть получена с помощью свойства length, то есть int count = a.length. Заметьте, что если вы напишите Object[] a = new Object[10] никаких объектов не создается. Только массив, который может их содержать.
int[] a = new int[10];
int[] b = { 2, 4, 6, 8 };
Можно получать срезы с помощью [начало:конец]:
int[] c = b[1:3]; // => { 4, 6 }
Результатом обрезания массива будет ссылка на запрашиваемые данные, а не копия. Однако, присвоение обрезания owned-переменной (как показано ниже) будет приводить к копированию. Если вы хотите избежать копирования, вы обязаны либо присвоить срез какой-нибудь unowned-переменной или передать его прямо как аргумент (аргументы по умолчанию unowned):
unowned int[] c = b[1:3]; // => { 4, 6 }
Массивы большей размерности определяются с помощью [,] или [,,] и тд.
int[,] c = new int[3,4];
int[,] d = {{2, 4, 6, 8},
{3, 5, 7, 9},
{1, 3, 5, 7}};
d[2,3] = 42;
Этот вид массива представлен одним непрерывным блоком памяти. Неровные многомерные массивы ( [] [] , также известные как «составные массивы», «зубчатые массивы» или «массивы массивов»), где каждая строка может иметь различную длину, пока не поддерживаются.
Иллюстрация: прямоугольные vs зубчатые массивы больших размерностей
Чтобы найти длину каждого измерения в многомерном массиве, элемент длины становится массивом, в котором хранится длина каждого соответствующего измерения.
int[,] arr = new int[4,5];
int r = arr.length[0];
int c = arr.length[1];
Заметьте, что вы не можете получить одномерный массив из многомерного, и даже с помощью среза многомерного массива:
int[,] arr = {{1,2},
{3,4}};
int[] b = arr[0]; // won't work
int[] c = arr[0,]; // won't work
int[] d = arr[:,0]; // won't work
int[] e = arr[0:1,0]; // won't work
int[,] f = arr[0:1,0:1]; // won't work
Вы можете добавлять элементы динамически с помощью +=. Однако, это работает только для локально определенных или private массивов. Массив будет автоматически переразмещен, если потребуется. Внутренне, это переразмещение происходит, когда размер массива переваливает через еще одну степень двойки, это сделано по причинам эффективности. Однако .length содержит реальное число элементов массива, а не внутренний размер.
int[] e = {};
e += 12;
e += 5;
e += 37;
Вы можете изменять размер массива вызовом resize() на нем. Он будет содержать оригинальный контент (сколько вместит).
int[] a = new int[5];
a.resize(12);
Вы можете переместить элементы в массиве вызвав move(src, dest, lenght). Освободившиеся позиции будут заполнены нулями.
uint8[] chars = "hello world".data;
chars.move(6,0,5);
print((string) chars); //"world "
Если вы поставите квадратные скобки после идентификатора вместе с указанием размера, вы получите массив фиксированного размера. Массивы фиксированного размера размещаются в стеке (если используются в качестве локальных переменных) или размещаются в строке (если используются в качестве полей), и вы не сможете перераспределить их позже.
int f[10]; // без 'new ...'
Vala не выполняет никаких проверок границ для доступа к массиву во время выполнения. Если вам нужно больше безопасности, вы должны использовать более сложную структуру данных, такую как ArrayList. Вы узнаете больше об этом позже в разделе о коллекциях.
Ссылочные типы
Ссылочные типы - это типы, определенные как класс, независимо происходят ли они от Object или нет. Vala будет гарантировать, что когда вы передаете объект по ссылке - система будет проверять номер ссылки на живучесть для обеспечения контроля памяти за вас. Значения ссылки не ссылается на что-то подобное null. Больше об объектно-ориентированном программировании читайте в разделе Классы и их Фичи.
/* defining a class */
class Track : GLib.Object { /* subclassing 'GLib.Object' */
public double mass; /* a public field */
public double name { get; set; } /* a public property */
private bool terminated = false; /* a private field */
public void terminate() { /* a public method */
terminated = true;
}
}
Статическое приведение типов
В Vala, вы можете переводить переменные из одного типа в другой. Для статического приведения типов, переменная конвертируется указанием желаемого типа в кавычках. Статическое приведения не создает какой-либо безопасной проверки типов во время выполнения. Это работает для всех типов Vala. Например:
int i = 10;
float j = (float) i;
Vala поддерживает другой механизм преобразования, называемый динамическим преобразованием, который выполняет проверку типов во время исполнения и это описано в разделе объектно-ориентированного программирования.
Вывод типов
Vala поддерживает механизм под названием вывод типов (неявная типизация) для локальных переменных: Локальные переменные могут быть объявлены с использованием ключевого слова 'var' вместо типа, если компилятор может вывести тип из инициализирующего аргумента. Это помогает избегать ненужной избыточности и особенно полезно для общих типов. Примерчики:
var p = new Person(); // same as: Person p = new Person();
var s = "hello"; // same as: string s = "hello";
var l = new List<int>(); // same as: List<int> l = new List<int>();
var i = 10; // same as: int i = 10;
Это работает только на локальных переменных. Вывод типов особенно полезен для типов с общими аргументами типов (об это позже). Сравните:
MyFoo<string, MyBar<string, int>> foo = new MyFoo<string, MyBar<string, int>>();
vs.
var foo = new MyFoo<string, MyBar<string, int>>();
Определение нового типа из другого
Определение нового типа иногда равносильно вопросу из чего его вывести. Вот пример:
/* Define a new type from a container like GLib.List with elements type GLib.Value */
public class ValueList : GLib.List<GLib.Value> {
[CCode (has_construct_function = false)]
protected ValueList ();
public static GLib.Type get_type ();
}
Операторы
=
равно. Левый операнд должен быть идентификатором, а правый должен выдавать результат ссылкой или значением по необходимости.
+, -, /, *, %
базовая арифметика, применение к левому операнду правого. Оператор + может также объединять строки.
+=, -=, /=, *=, %=
Арифметические операции между левым и правым операндами, где левый обязан быть идентификатор, к которому присваивается результат.
++, --
Операторы инкремента и декремента с неявным присваиванием. Они берут только один аргумент, который обязан быть идентификатором простого типа данных. Это значение будет изменено и присвоено обратно идентификатору. Эти операции могут быть размещены префиксным или постфиксным образом - с первым, оцененное значение выражения будет заново вычисленным значением, в последнем случае возвращается исходное значение.
|, ^, &, ~, |=, &=, ^=
битовые операторы: или, исключающее или, и, не. Второе множество включает присвоение и аналогичные арифметические версии. Они могут быть применены к любому простому значению типов. (Нет оператора присвоение для ~, так как это унарный оператор. Эквивалентом является a=~a).
<<, >>
битовые операторы сдвига, сдвигают левый операнд на число битов, указанные в правом операнде.
<<=, >>=
то же самое, только левый операнд должен быть идентификатор, к которому прибавляется результат.
==
Проверка на равенство. В случае value-типов это подразумевает равенство значений, в случае ссылочных типов подразумевается равенство равенство ссылок объектов (один и тот же объект). Исключение составляют string тип, которые проверяются по контенту.
<, >, >=, <=, !=
проверка на неравенство. Сводит к булеву значению, в зависимости от левого и правого операндов. Отличаются по способу сравнения. Они верны для простых value-типов, и для string. Для строк эти операторы сравнивают лексикографический порядок. (a>b)
!, &&, ||
логические операторы: не, и, или. Эти операции могут быть применены к Булевым значениям - первый берет один аргумент, другие - два.
? :
тернарный условный оператор. Оценивает условие и возвращает значения первого или правого подвыражения, основываясь на том, что значение условия равно либо true либо false: condition ? value if true : value if false
??
null-сливающий оператор: a ?? b эквивалентно a != null ? a : b. Этот оператор особенно полезен, например, для предоставление дефолтного в том случае, если ссылка равна null:
stdout.printf("Hello, %s!\n", name ?? "unknown person");
in
проверяет, содержит ли правый операнд левый. Этот оператор работает на массивах, строках, коллекциях и любых других типах, и на любом другом типе данных, которые имеют соответствующий contains() метод. Для строк он выполняет поиск подстроки.
Операторы не могут быть перегружены в Vala. Есть экстра операторы, которые верны в контексте лямба определений и других специфичных заданий - они описаны в контексте их применения.
Управляющие конструкции
while (a > b) { a--; }
будет уменьшать a неоднократно, проверяя перед этим на каждой итерации что a больше b.
do { a--; } while (a > b);
будет уменьшать a неоднократно, проверяя после этим на каждой итерации что a больше b.
for (int a = 0; a < 10; a++) { stdout.printf("%d\n", a); }
будет инициализировать a нулем, затем неоднократно печатать а, пока a будет меньше 10, инкрементируя a после каждой итерации.
foreach (int a in int_array) { stdout.printf("%d\n", a); }
напечатает каждый Integer в массиве, или другой итерабельной коллекции. Значение слова "итерабельный" будет описано позже.
Всеми четырьмя предыдущими типами цикла можно управлять с помощью ключевых слов break и continue. Команда break приведет к немедленному завершению цикла, а continue продолжит переход к тестовой части итерации.
if (a > 0) { stdout.printf("a is greater than 0\n"); }
else if (a < 0) { stdout.printf("a is less than 0\n"); }
else { stdout.printf("a is equal to 0\n"); }
выполняет определенный фрагмент кода на основе набора условий. Первое условие для соответствия решает, какой код будет выполняться, если a больше 0, он не будет проверяться, является ли он меньше 0. Любое количество блоков, else if разрешено, и ноль или один блок else.
switch (a) {
case 1:
stdout.printf("one\n");
break;
case 2:
case 3:
stdout.printf("two or three\n");
break;
default:
stdout.printf("unknown\n");
break;
}
Оператор switch выполняет ровно один или ноль разделов кода на основе переданного ему значения. В Vala нет проверки на вхождение в cases, кроме пустых(как в case 2:). Для обеспечения этого каждый непустой случай должен заканчиваться оператором break, return или throw. Можно использовать операторы Switch со строками.
Примечание для C программистов: условия всегда должны принимать логическое значение. Это означает, что если вы хотите проверить переменную на null или 0, вы должны сделать это явно: if (object! = Null) {} или if (number! = 0) {} . (Комментарий от переводчика: сейчас у меня работает и так, возможно эта информация устарела)
Элементы языка
Методы
В Vala функции называются методами, независимо от того, где они определены: в классе или вне его. Здесь и далее мы будем придерживаться термина метод.
int method_name(int arg1, Object arg2) {
return 1;
}
Этот код определяет метод, имеющий имя method_name, принимающий два аргумента: один целый, а другой типа Object (первый передается по значению, второй передается по ссылке). Метод возвращает целое, которое в данном случае равно 1.
Все методы Vala являются функциями С и, следовательно, принимают неограниченное число аргументов и возвращают одно значение (или ничего, если метод описан как void). Они могут аппроксимировать большее число возвращаемых значений, помещая данные в место, известное вызывающему коду (принятие аргументов по ссылке/значению). О том, как это сделать, подробнее рассказано в разделе "Направления параметров" в продвинутой части этого руководства.
При выборе наименования методов в Vala принято придерживаться стиля all_lower_case (все_в_нижнем_регистре) с символами подчеркивания как разделителями слов. Это может быть немного непривычно для программистов, пишущих на C# или Java, которые привыкли к CamelCase или mixedCamelCase. Однако благодаря этому стилю обеспечивается единообразие вашего кода с другими библиотеками Vala и C/GObject.
Не допускается создавать в одной и той же области видимости несколько методов с одинаковым именем, но с разными сигнатурами ("перегрузка методов"):
void draw(string text) { }
void draw(Shape shape) { } // не допускается
Это объясняется тем, что библиотеки, написанные на Vala, должны быть пригодны для использования также и программистами, использующими C. Вместо перегрузки можно прибегнуть приблизительно к такому решению:
void draw_text(string text) { }
void draw_shape(Shape shape) { }
Используя несколько различающиеся имен функций, вы можете избежать конфликта имен. В языках, которые поддерживают перегрузку методов, эта возможность нередко используется для создания удобных методов с меньшим числом параметров, соотносящихся с наиболее общим методом:
void f(int x, string s, double z) { }
void f(int x, string s) { f(x, s, 0.5); } // не допускается
void f(int x) { f(x, "hello"); } // не допускается
В этом примере вы можете используется стандартная функциональность Vala для достижения аналогичного поведения. Вы можете определить значения по умолчанию чтобы не передавать их явно при вызове метода:
void f(int x, string s = "hello", double z = 0.5) { }
Вызовы этого метода могут выглядеть так:
f(2);
f(2, "hi");
f(2, "hi", 0.75);
Можно даже определить методы с помощью реальных списков аргументов переменной длины ( varargs ), таких как stdout.printf () , хотя это не обязательно. Вы узнаете, как это сделать позже.
Vala выполняет базовую проверку на null параметров метода и возвращаемых значений. Если допустимо, чтобы параметр метода или возвращаемое значение были нулевыми, название типа переменной должно иметь ? модификатор. Эта дополнительная информация помогает компилятору Vala выполнять статическую проверку и добавлять runtime утверждения для предусловий методов(как в контрактном программировании, о нем будет ниже), что может помочь избежать связанных с этим ошибок, таких как разыменование указателя на null.
string? method_name(string? text, Foo? foo, Bar bar) {
// ...
}
В этом примере text, foo и возвращаемое значение могут принимать значение null, однако bar не должен быть нулевым.
Делегаты
delegate void DelegateType(int a);
Делегаты представляют собой методы, разрешающие кускам кода вести себя подобно объектам. В примере ниже определяется новый тип, названный DelegateType, который олицетворяет метод, берущий int и не возвращающий значение. Любой метод, который совпадает с этой сигнатурой может быть передан переменнай этого типа или передан как параметр в метод с аргументом такого типа.
delegate void DelegateType(int a);
void f1(int a) {
stdout.printf("%d\n", a);
}
void f2(DelegateType d, int a) {
d(a); // Calling a delegate
}
void main() {
f2(f1, 5); // Passing a method as delegate argument to another method
}
Этот код выполнит метод f2, передав ссылку на методf1 и с число 5. f2 затем выполнит метод f1, передавая ему число.
Делегаты также могут быть созданы локально. Метод-член класса также может быть присвоен делегату, например:
class Foo {
public void f1(int a) {
stdout.printf("a = %d\n", a);
}
delegate void DelegateType(int a);
public static int main(string[] args) {
Foo foo = new Foo();
DelegateType d1 = foo.f1;
d1(10);
return 0;
}
}
Анонимные методы/Замыкания
(a) => { stdout.printf("%d\n", a); }
"Анонимыный метод" также, также известный как лямбда выражение, функциональный литерал или замыкание, может быть определены в Vala оператором =>. Список параметров слева, тело метода справа.
Анонимный метод, сам по себе не несет никакого смысла. Он особенно полезен, если вы присваиваете его прямо в переменную delegate типа или передаете в метод как аргумент.
Обратите внимание, что ни параметры, ни возвращаемые типы не указаны явно. Вместо этого типы выводятся из подписи делегата, с которым замыкание используется.
Присвоение анонимного метода переменной-делегату:
delegate void PrintIntFunc(int a);
void main() {
PrintIntFunc p1 = (a) => { stdout.printf("%d\n", a); };
p1(10);
// Curly braces are optional if the body contains only one statement:
PrintIntFunc p2 = (a) => stdout.printf("%d\n", a);
p2(20):
}
Передача анонимного метода другому методу:
delegate int Comparator(int a, int b);
void my_sorting_algorithm(int[] data, Comparator compare) {
// ... 'compare' is called somewhere in here ...
}
void main() {
int[] data = { 3, 9, 2, 7, 5 };
// An anonymous method is passed as the second argument:
my_sorting_algorithm(data, (a, b) => {
if (a < b) return -1;
if (a > b) return 1;
return 0;
});
}
Анонимные методы являются настоящими замыканиями. Это значит, что вы имеете доступ к локальным переменным внешнего метода из лямбда выражения:
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));
}
В этом примере curried_add (смотрите Currying) возвращает новосозданный метод, который сохраняет значение a. Он возвращает метод, напрямую вызываемый позже, с 4 в качестве аргумента, выдавая сумму двух чисел.
Пространства имен
namespace NameSpaceName {
// ...
}
Все между скобками в этом выражении в пространстве имен NameSpaceName и ссылаются на внутреннюю часть следующим образном. Любой код снаружи этого пространства обязан либо использовать полные имена для всех с указанием имени пространства имен, или быть в файле с соответствующим определением using для импорта этого пространства:
using NameSpaceName;
// ...
Например, если пространство имен Gtk импортировано с помощью using Gtk;, то вы можете просто писать Window вместо Gtk.Window. Полное имя можеть быть необходимо в случае неясности, например между GLib.Object и Gtk.Object.
Пространство имен Glib импортируется по умолчанию. Вообразите невидимую строчку using GLib в начале каждого Vala файла.
Все, что вы не помещаете в отдельное пространство имен будет относится к анонимному глобальному пространству. Если вы должны ссылаться на глобальную облать явно ввиду неясности, вы можете это сделать используя префикс global::
Пространства имен могут быть вложенными, либо вложением одного определения в другой, или придавая имени форму NameSpace1.NameSpace2.
Некоторые другие типы определений могут определять самих себя внутри пространства имен, следуя следующему соглашению об именовании, например class NameSpace1.Test { ... }. Заметьте, что когда вы так делаете, финальным пространством имен определения будет то, в котором сделано определение плюс пространство имен, указанное в определении.
Structs
struct StructName {
public int a;
}
defines a struct type, i.e. a compound value type. A Vala struct may have methods in a limited way and also may have private members, meaning the explicit public access modifier is required.
struct Color {
public double red;
public double green;
public double blue;
}
This is how you can initialise a struct:
// without type inference
Color c1 = Color();
Color c2 = { 0.5, 0.5, 1.0 };
Color c3 = Color() {
red = 0.5,
green = 0.5,
blue = 1.0
};
// with type inference
var c4 = Color();
var c5 = Color() {
red = 0.5,
green = 0.5,
blue = 1.0
};
Structs are stack/inline allocated and copied on assignment.
To define an array of structs, please see the FAQ.
Classes
class ClassName : SuperClassName, InterfaceName {
}
defines a class, i.e. a reference type. In contrast to structs, instances of classes are heap allocated. There is much more syntax related to classes, which is discussed more fully in the section about object oriented programming.
Interfaces
interface InterfaceName : SuperInterfaceName {
}
defines an interface, i.e. a non instantiable type. In order to create an instance of an interface you must first implement its abstract methods in a non-abstract class. Vala interfaces are more powerful than Java or C# interfaces. In fact, they can be used as mixins. The details of interfaces are described in the section about object oriented programming.
Code Attributes
Code attributes instruct the Vala compiler details about how the code is supposed to work on the target platform. Their syntax is [AttributeName] or [AttributeName(param1 = value1, param2 = value2, ...)].
They are mostly used for bindings in vapi files, [CCode(...)] being the most prominent attribute here. Another example is the [DBus(...)] attribute for exporting remote interfaces via D-Bus.
Object Oriented Programming
Хотя Vala не заставляет вас работать с объектами, некоторые функции недоступны без них. Таким образом, вы наверняка будите программировать в объектно-ориентированном стиле большую часть времени. Как и в большинстве современных языков, для определения объектов ваших собственных типов вы пишете определение класса.
Определение класса указывает, какие данные имеет каждый объект его типа, на какие другие типы объектов он может ссылаться, и какие методы у него есть. Определение может включать имя другого класса, новым классом которого должен быть подкласс. Экземпляр класса также является экземпляром всех суперклассов(родителей) его класса, поскольку он наследует от них все их методы и данные, хотя он может не иметь доступа ко всему этому сам. Класс может также реализовывать любое количество интерфейсов, которые представляют собой наборы определений методов, которые должны быть реализованы классом, экземпляр класса также является экземпляром каждого интерфейса, реализуемого его классом или суперклассами.
В Vala поля и методы классов также могут быть "статическими". Этот модификатор позволяет определять данные или методы как принадлежащие к классу в целом, а не к его конкретному экземпляру. К таким членам можно получить доступ, не имея экземпляра класса.
Основы
Простой класс может быть определен следующим образом:
public class TestClass : GLib.Object {
/* Поля */
public int first_data = 0;
private int second_data;
/* Конструктор */
public TestClass() {
this.second_data = 5;
}
/* Метод */
public int method_1() {
stdout.printf("private data: %d", this.second_data);
return this.second_data;
}
}
Этот код определит новый тип (который регистрируется автоматически в системе типов библиотеки gobject ). Есть два поля, first_data и second_data, оба типа int, и один метод с именем method_1 , который возвращает целое число. В объявлении класса указано, что этот класс является подклассом GLib.Object , и следовательно его экземпляры также являются объектами и содержат также все члены родительского класса. Тот факт, что этот класс произошел от Object, также означает, что существуют специальные функции Vala, которые можно использовать для быстрого доступа к некоторым фичам этого класса .
Этот класс описывается как public(по умолчанию все классы internal). Следствием этого является то, что к нему можно обращаться непосредственно за пределами этого файла.
Все члены класса так же могут быть public или private. Элемент first_data является public , поэтому он виден непосредственно любому пользователю класса и может быть изменен без уведомления об этом содержащего его экземпляра. Второй элемент данных является закрытым , и к нему может обратиться только из кода, принадлежащий этому классу. Vala поддерживает четыре разных модификатора доступа:
public |
Нет ограничений на доступ |
private |
Доступ ограничен содержащим классом |
protected |
Доступ ограничен определением класса и любым классом, который наследуется от класса |
internal |
Access is limited exclusively to classes defined within the same package(TODO?) |
Конструктор вызывается ключевым словом new при объявлении экземпляра класса. Имя конструктора совпадает с именем класса, конструктор может принимать 0 и больше аргументов, и определяется без return.
Осталось только определение метода. Он имеет название "method", и возвращает int. Тк кк этот метод не статический, он не может существовать без своего класса, и использовать его можно только из экземпляра класса. У него так же есть доступ к указателю this, который указывает на класс в котором находиться метод.
Создавать экземпляры классов можно следующим образом:
TestClass t = new TestClass();
t.first_data = 5;
t.method_1();
Конструктор
Vala поддерживает 2 схемы использования конструктора: Java/C#-Style и GObject-Style. Сейчас мы сосредоточимся на первом, а второй рассмотрим в конце главы.
Vala не поддерживает перегрузку конструкторов по той же причине по которой недоступна перегрузка функций(требуется сохранять совместимость с C-ABI). Но это не проблема, тк кк Vala поддерживает именованные конструкторы, как в C++ или PHP. Если вы хотите создать несколько конструкторов просто добавьте к ним дополнительные уточнения.
public class Button : Object {
public Button() {
}
public Button.with_label(string label) {
}
public Button.from_stock(string stock_id) {
}
}
Вот так будут выглядеть вызовы этих конструкторов:
new Button();
new Button.with_label("Click me");
new Button.from_stock(Gtk.STOCK_OK);
В конструкторе доступно ключевое слово this.поле_класса,а также синтаксический сахар в виде this(перечисление полей через запятую):
public class Point : Object {
public double x;
public double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public Point.rectangular(double x, double y) {
this(x, y);
}
public Point.polar(double radius, double angle) {
this.rectangular(radius * Math.cos(angle), radius * Math.sin(angle));
}
}
void main() {
var p1 = new Point.rectangular(5.7, 1.2);
var p2 = new Point.polar(5.7, 1.2);
}
Деструктор
Хотя Vala управляет памятью для вас, вам может потребоваться добавить собственный деструктор, если вы решите вручную управлять памятью с помощью указателей (подробнее об этом позже) или если вам придется освободить другие ресурсы. Синтаксис такой же, как в C # и C ++:
class Demo : Object {
~Demo() {
stdout.printf("in destructor");
}
}
Поскольку управление памятью в Vala основано на подсчете ссылок, а не на отслеживании мусора в runtime, деструкторы являются детерминированными и могут использоваться для реализации шаблона RAII для управления ресурсами (закрытие потоков, соединения с базой данных, ...).
Сигналы
Сигналы — система предоставляемая для GObject библиотекой GLib. В C# под этим понимают event, для джавистов это альтернативный способ реализации шаблона event — listemers. Вкратце сигналы это просто способ привязать выполнение некоторого действия к событию. (Тк кк сигналы являются частью GObject — использовать их можно только с объектами унаследованными от GObject(Все GTK виджеты уже унаследованы))
Сигнал объявляется в классе и выглядит как метод без тела. Затем к сигналу можно добавить обработчики с помощью метода connect (). В следующем примере также представлены лямбда-выражения, очень полезный способ написать код обработки сигнала в Vala:
public class Test : GLib.Object {
public signal void sig_1(int a);
public static int main(string[] args) {
Test t1 = new Test();
t1.sig_1.connect((t, a) => {
stdout.printf("%d\n", a);
});
t1.sig_1(5);
return 0;
}
}
В этом коде объявляется новый класс Test с помощью уже привычного синтаксиса. Первым членом этого класса является сигнал sig_1 возвращающий передающий int a. В main() создается экземпляр класса Test (сигналы всегда принадлежат экземплярам класса(то есть не могут быть использованы в статическом)). Затем сигнал sig_1 привязывается к обработчику, в качестве которого выступает лямбда-функция. Мы можем не указывать типы аргументов тк кк Vala уже знает их из определения сигнала.
Причина по которой обработчик имеет 2 параметра заключается в том что объект издавший сигнал передается в качестве первого аргумента(t). Второй(и все следующие) аргументы являются способом передать данные.
Наконец, мы решаем подать сигнал. Мы делаем это, вызывая сигнал, как если бы это был метод нашего класса, и позволяем gobject позаботиться о пересылке сообщения всем прикрепленным обработчикам. Понимание механизма, используемого для этого, не требуется для использования сигналов из Vala.
Примечание: Сигналы всегда должны быть объявлены как public, потому что они могут быть связаны или вызваны из любой части кода.
Примечание: С апреля 2010 г. сигналы можно аннотировать любой комбинацией флагов: (TODO)
[Signal (action=true, detailed=true, run=true, no_recurse=true, no_hooks=true)]
public signal void sig_1 ();
Properties
Свойство — способ доступа к внутреннему состоянию объекта, имитирующий переменную некоторого типа. Обращение к свойству объекта выглядит так же, как и обращение к структурному полю (в структурном программировании), но, в действительности, реализовано через вызов функции. При попытке задать значение данного свойства вызывается один метод, а при попытке получить значение данного свойства — другой.
При применении свойств: 1) можно задать значение по умолчанию, которое будет храниться в данном свойстве (или указать, что никакого значения по умолчанию не предполагается); 2) можно указать, что это свойство только для чтения.
Если вы программист на Java, вы, вероятно, подумаете о чем-то вроде этого:
class Person : Object {
private int age = 32;
public int get_age() {
return this.age;
}
public void set_age(int age) {
this.age = age;
}
}
Это сработает, но Vala может сделать лучше. Проблема в том, что с этими методами трудно работать. Предположим, вы хотите увеличить возраст человека на один год:
var alice = new Person();
alice.set_age(alice.get_age() + 1);
Здесь в игру вступают properties:
class Person : Object {
private int _age = 32; // нижнее подчеркивание-префикс чтобы избежать совпадения имен с property
/* Property */
public int age {
get { return _age; }
set { _age = value; }
}
}
Этот синтаксис должен быть знаком программистам на C#. Свойство имеет блоки get и set для получения и задания его значения. value - это ключевое слово, которое означает новое значение, которое должно быть назначено свойству.
Теперь вы можете получить доступ к свойству, как если бы это было публичное поле. Но за кулисами выполняется код в блоках get и set.
var alice = new Person();
alice.age = alice.age + 1; // or even shorter:
alice.age++;
Если вы только меняете значение свойства по умолчанию, вы можете воспользоваться синтаксическим сахаром:
class Person : Object {
/* Property with standard getter and setter and default value */
public int age { get; set; default = 32; }
}
С помощью свойств вы можете менять внутреннее устройство классов, не меняя публичный API доступа к ним. Например:
static int current_year = 2525;
class Person : Object {
private int year_of_birth = 2493;
public int age {
get { return current_year - year_of_birth; }
set { year_of_birth = current_year - value; }
}
}
В этом примере возраст рассчитывается на лету из года рождения. Обратите внимание что в блоках set/get можно делать больше чем просто получить доступ к переменной или ее присваиванию. Вы можете получить доступ к внешней базе данных, реализовать ведение журнала, обновление кэша и т.д.
Если вы хотите сделать свойство read-only для использующего его класса — обозначьте его как private:
public int age { get; private set; default = 32; }
Или, в качестве альтернативы, вы можете не объявлять блок set:
class Person : Object {
private int _age = 32;
public int age {
get { return _age; }
}
}
Свойства могут иметь не только имя, но также краткое описание (называемое ник ) и длинное описание (называемое blurb ). Вы можете комментировать это со специальным атрибутом:
[Description(nick = "age in years", blurb = "This is the person's age in years")]
public int age { get; set; default = 32; }
Свойства и их дополнительные описания могут быть запрошены во время выполнения. Некоторые программы, такие как дизайнер графического интерфейса пользователя Glade, используют эту информацию. Таким образом, Glade может представлять понятные человеку описания свойств виджетов GTK.
Каждый экземпляр класса , производного от GLib.Object имеет сигнал с именем notify. Этот сигнал испускается каждый раз, когда изменяется свойство его объекта. Таким образом, вы можете подключиться к этому сигналу, если вы заинтересованы в уведомлениях об изменениях в целом:
obj.notify.connect((s, p) => {
stdout.printf("Property '%s' has changed!\n", p.name);
});
s является источником сигнала ( в данном примере obj ), p является информацией свойства типа ParamSpec для измененного свойства. Если вы заинтересованы только в изменениях одного свойства вы можете использовать этот синтаксический сахар:
alice.notify["age"].connect((s, p) => {
stdout.printf("age has changed\n");
});
Обратите внимание, что в этом случае вы должны использовать строковое представление имени свойства, где подчеркивания заменяются черточками: my_property_name становится my-property-name в этом представлении, которое является соглашением об именовании свойств GObject.
Change notifications can be disabled with a CCode attribute tag immediately before the declaration of the property:
public class MyObject : Object {
[CCode(notify = false)]
// notify signal is NOT emitted upon changes in the property
public int without_notification { get; set; }
// notify signal is emitted upon changes in the property
public int with_notification { get; set; }
}
There's another type of properties called construct properties that are described later in the section about gobject-style construction.
Наследование
«Опираясь на практику их использования, исключает некоторые модели, зарекомендовавшие себя как проблематичные при разработке программных систем, например, C# в отличие от C++ и некоторых других языков, не поддерживает множественное наследование классов (между тем допускается множественное наследование интерфейсов).» Тоже самое справедливо и для Vala.
При написании определения класса можно осуществлять точный контроль над тем, кто может получить доступ к методам и данным в объекте. В следующем примере демонстрируется ряд этих параметров:
class SuperClass : GLib.Object {
private int data;
public SuperClass(int data) {
this.data = data;
}
protected void protected_method() {
}
public static void public_static_method() {
}
}
class SubClass : SuperClass {
public SubClass() {
base(10);
}
}
Data являются экземпляром данных SuperClass. Элемент этого типа будет присутствовать в каждом экземпляре SuperClass, и тк кк он объявлен как private, он будет доступен только для кода, который является частью SuperClass.
protected_method является методом экземпляра SuperClass. Вы сможете выполнить этот метод только из кода экземпляра SuperClass или одного из его подклассов.
У public_static_method есть два модификатора. Static модификатор означает , что этот метод может быть вызван без экземпляра SuperClass или одного из его подклассов. В результате этот метод не будет иметь доступа к ссылке this при выполнении. Public модификатор означает, что этот метод может быть вызван из любого кода, независимо от его отношения с SuperClass или его подклассов.
С учетом этих определений экземпляр SubClass будет содержать все три члена SuperClass, но будет иметь доступ ко всем элементам кроме private.
С помощью base конструктор подкласса может быть связан с конструктором его базового класса.
Абстрактные классы
There is another modifier for methods, called abstract. This modifier allows you to describe a method that is not actually implemented in the class. Instead, it must be implemented by subclasses before it can be called. This allows you to define operations that can be called on all instances of a type, whilst ensuring that all more specific types provide their own version of the functionality.
A class containing abstract methods must be declared abstract as well. The result of this is to prevent any instantiation of the type.
public abstract class Animal : Object {
public void eat() {
stdout.printf("*chomp chomp*\n");
}
public abstract void say_hello();
}
public class Tiger : Animal {
public override void say_hello() {
stdout.printf("*roar*\n");
}
}
public class Duck : Animal {
public override void say_hello() {
stdout.printf("*quack*\n");
}
}
The implementation of an abstract method must be marked with override. Properties may be abstract as well.
Interfaces / Mixins
A class in Vala may implement any number of interfaces. Each interface is a type, much like a class, but one that cannot be instantiated. By "implementing" one or more interfaces, a class may declare that its instances are also instances of the interface, and therefore may be used in any situation where an instance of that interface is expected.
The procedure for implementing an interface is the same as for inheriting from classes with abstract methods in - if the class is to be useful it must provide implementations for all methods that are described but not yet implemented.
A simple interface definition looks like:
public interface ITest : GLib.Object {
public abstract int data_1 { get; set; }
public abstract void method_1();
}
This code describes an interface "ITest" which requires GLib.Object as parent and contains two members. "data_1" is a property, as described above, except that it is declared abstract. Vala will therefore not implement this property, but instead require that classes implementing this interface have a property called "data_1" that has both get and set accessors - it is required that this be abstract as an interface may not have any data members. The second member "method_1" is a method. Here it is declared that this method must be implemented by classes that implement this interface.
The simplest possible full implementation of this interface is:
public class Test1 : GLib.Object, ITest {
public int data_1 { get; set; }
public void method_1() {
}
}
And may be used as follows:
var t = new Test1();
t.method_1();
ITest i = t;
i.method_1();
Interfaces in Vala may not inherit from other interfaces, but they may declare other interfaces to be prerequisites, which works in roughly the same way. For example, it may be desirable to say that any class that implements a "List" interface must also implement a "Collection" interface. The syntax for this is exactly the same as for describing interface implementation in classes:
public interface List : Collection {
}
This definition of "List" may not be implemented in a class without "Collection" also being implemented, and so Vala enforces the following style of declaration for a class wishing to implement "List", where all implemented interfaces must be described:
public class ListClass : GLib.Object, Collection, List {
}
Vala interfaces may also have a class as a prerequisite. If a class name is given in the list of prerequisites, the interface may only be implemented in classes that derive from that prerequisite class. This is often used to ensure that an instance of an interface is also a GLib.Object subclass, and so the interface can be used, for example, as the type of a property.
The fact that interfaces can not inherit from other interfaces is mostly only a technical distinction - in practice Vala's system works the same as other languages in this area, but with the extra feature of prerequisite classes.
There's another important difference between Vala interfaces and Java/C# interfaces: Vala interfaces may have non-abstract methods! Vala actually allows method implementations in interfaces, hence the requirement that abstract methods be declared abstract. Due to this fact Vala interfaces can act as mixins. This is a restricted form of multiple inheritance.
Polymorphism
Polymorphism describes the way in which the same object can be used as though it were more than one distinct type of thing. Several of the techniques already described here suggest how this is possible in Vala: An instance of a class may be used as in instance of a superclass, or of any implemented interfaces, without any knowledge of its actual type.
A logical extension of this power is to allow a subtype to behave differently to its parent type when addressed in exactly the same way. This is not a very easy concept to explain, so I'll begin with an example of what will happen if you do not directly aim for this goal:
class SuperClass : GLib.Object {
public void method_1() {
stdout.printf("SuperClass.method_1()\n");
}
}
class SubClass : SuperClass {
public void method_1() {
stdout.printf("SubClass.method_1()\n");
}
}
These two classes both implement a method called "method_1", and "SubClass" therefore contains two methods called "method_1", as it inherits one from "SuperClass". Each of these may be called as the following code shows:
SubClass o1 = new SubClass();
o1.method_1();
SuperClass o2 = o1;
o2.method_1();
This will actually result in two different methods being called. The second line believes "o1" to be a "SubClass" and will call that class's version of the method. The fourth line believes "o2" to be a "SuperClass" and will call that class's version of the method.
The problem this example exposes, is that any code holding a reference to "SuperClass" will call the methods actually described in that class, even in the actual object is of a subclass. The way to change this behaviour is using virtual methods. Consider the following rewritten version of the last example:
class SuperClass : GLib.Object {
public virtual void method_1() {
stdout.printf("SuperClass.method_1()\n");
}
}
class SubClass : SuperClass {
public override void method_1() {
stdout.printf("SubClass.method_1()\n");
}
}
When this code is used in the same way as before, "SubClass"'s "method_1" will be called twice. This is because we have told the system that "method_1" is a virtual method, meaning that if it is overridden in a subclass, that new version will always be executed on instances of that subclass, regardless of the knowledge of the caller.
This distinction is probably familiar to programmers of some languages, such as C++, but it is in fact the opposite of Java style languages, in which steps must be taken to prevent a method being virtual.
You will probably now also have recognised that when method is declared as abstract it must also be virtual. Otherwise, it would not be possible to execute that method given an apparent instance of the type it was declared in. When implementing an abstract method in a subclass, you may therefore choose to declare the implementation as override, thus passing on the virtual nature of the method, and allowing subtypes to do the same if they desire.
It's also possible to implement interface methods in such a way that subclasses can change the implementation. The process in this case is for the initial implementation to declare the method implementation to be virtual, and then subclasses can override as required.
When writing a class, it is common to want to use functionality defined in a class you have inherited from. This is complicated where the method name is used more than one in the inheritance tree for your class. For this Vala provides the base keyword. The most common case is where you have overridden a virtual method to provide extra functionality, but still need the parent class' method to be called. The following example shows this case:
public override void method_name() {
base.method_name();
extra_task();
}
Vala also allows properties to be virtual:
class SuperClass : GLib.Object {
public virtual string prop_1 {
get {
return "SuperClass.prop_1";
}
}
}
class SubClass : SuperClass {
public override string prop_1 {
get {
return "SubClass.prop_1";
}
}
Method Hiding
By using the new modifier you can hide an inherited method with a new method of the same name. The new method may have a different signature. Method hiding is not to be confused with method overriding, because method hiding does not exhibit polymorphic behaviour.
class Foo : Object {
public void my_method() { }
}
class Bar : Foo {
public new void my_method() { }
}
You can still call the original method by casting to the base class or interface:
void main() {
var bar = new Bar();
bar.my_method();
(bar as Foo).my_method();
}
Run-Time Type Information
Since Vala classes are registered at runtime and each instance carries its type information you can dynamically check the type of an object with the is operator:
bool b = object is SomeTypeName;
You can get the type information of Object instances with the get_type() method:
Type type = object.get_type();
stdout.printf("%s\n", type.name());
With the typeof() operator you can get the type information of a type directly. From this type information you can later create new instances with Object.new():
Type type = typeof(Foo);
Foo foo = (Foo) Object.new(type);
Which constructor will be called? It's the construct {} block that will be described in the section about gobject-style construction.
Dynamic Type Casting
For the dynamic cast, a variable is casted by a postfix expression as DesiredTypeName. Vala will include a runtime type checking to ensure this casting is reasonable - if it is an illegal casting, null will be returned. However, this requires both the source type and the target type to be referenced class types.
For example,
Button b = widget as Button;
If for some reason the class of the widget instance is not the Button class or one of its subclasses or does not implement the Button interface, b will be null. This cast is equivalent to:
Button b = (widget is Button) ? (Button) widget : null;
Generics
Vala includes a runtime generics system, by which a particular instance of a class can be restricted with a particular type or set of types chosen at construction time. This restriction is generally used to require that data stored in the object must be of a particular type, for example in order to implement a list of objects of a certain type. In that example, Vala would make sure that only objects of the requested type could be added to the list, and that on retrieval all objects would be cast to that type.
In Vala, generics are handled while the program is running. When you define a class that can be restricted by a type, there still exists only one class, with each instance customised individually. This is in contrast to C++ which creates a new class for each type restriction required - Vala's is similar to the system used by Java. This has various consequences, most importantly: that static members are shared by the type as a whole, regardless of the restrictions placed on each instance; and that given a class and a subclass, a generic refined by the subclass can be used as a generic refined by the class.
The following code demonstrates how to use the generics system to define a minimal wrapper class:
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;
}
}
This "Wrapper" class must be restricted with a type in order to instantiate it - in this case the type will be identified as "G", and so instances of this class will store one object of "G" type, and have methods to set or get that object. (The reason for this specific example is to provide reason explain that currently a generic class cannot use properties of its restriction type, and so this class has simple get and set methods instead.)
In order to instantiate this class, a type must be chosen, for example the built in string type (in Vala there is no restriction on what type may be used in a generic). To create an briefly use this class:
var wrapper = new Wrapper<string>();
wrapper.set_data("test");
var data = wrapper.get_data();
As you can see, when the data is retrieved from the wrapper, it is assigned to an identifier with no explicit type. This is possible because Vala knows what sort of objects are in each wrapper instance, and therefore can do this work for you.
The fact that Vala does not create multiple classes out of your generic definition means that you can code as follows:
class TestClass : GLib.Object {
}
void accept_object_wrapper(Wrapper<Glib.Object> w) {
}
...
var test_wrapper = new Wrapper<TestClass>();
accept_object_wrapper(test_wrapper);
...
Since all "TestClass" instances are also Objects, the "accept_object_wrapper" method will happily accept the object it is passed, and treat its wrapped object as though it was a GLib.Object instance.
GObject-Style Construction
As pointed out before, Vala supports an alternative construction scheme that is slightly different to the one described before, but closer to the way GObject construction works. Which one you prefer depends on whether you come from the GObject side or from the Java or C# side. The gobject-style construction scheme introduces some new syntax elements: construct properties, a special Object(...) call and a construct block. Let's take a look at how this works:
public class Person : Object {
/* Construction properties */
public string name { get; construct; }
public int age { get; construct set; }
public Person(string name) {
Object(name: name);
}
public Person.with_age(string name, int years) {
Object(name: name, age: years);
}
construct {
// do anything else
stdout.printf("Welcome %s\n", this.name);
}
}
With the gobject-style construction scheme each construction method only contains an Object(...) call for setting so-called construct properties. The Object(...) call takes a variable number of named arguments in the form of property: value. These properties must be declared as construct properties. They will be set to the given values and afterwards all construct {} blocks in the hierarchy from GLib.Object down to our class will be called.
The construct block is guaranteed to be called when an instance of this class is created, even if it is created as a subtype. It does neither have any parameters, nor a return value. Within this block you can call other methods and set member variables as needed.
Construct properties are defined just as get and set properties, and therefore can run arbitrary code on assignment. If you need to do initialisation based on a single construct property, it is possible to write a custom construct block for the property, which will be executed immediately on assignment, and before any other construction code.
If a construct property is declared without set it is a so-called construct only property, which means it can only be assigned on construction, but no longer afterwards. In the example above name is such a construct only property.
Here's a summary of the various types of properties together with the nomenclature usually found in the documentation of gobject-based libraries:
public int a { get; private set; } // Read
public int b { private get; set; } // Write
public int c { get; set; } // Read / Write
public int d { get; set construct; } // Read / Write / Construct
public int e { get; construct; } // Read / Write-Construct-Only
In some cases you may also want to perform some action - not when instances of a class is created - but when the class itself is created by the GObject runtime. In GObject terminology we are talking about a snippet of code run inside the class_init function for the class in question. In Java this is known as static initializer blocks. In Vala this looks like:
/* This snippet of code is run when the class
* is registered with the type system */
static construct {
...
}
Advanced Features
Assertions and Contract Programming
With assertions a programmer can check assumptions at runtime. The syntax is assert(condition). If an assertion fails the program will terminate with an appropriate error message. There are a few more assertion methods within the GLib standard namespace, e.g.:
assert_not_reached() |
return_if_fail(bool expr) |
return_if_reached() |
warn_if_fail(bool expr) |
warn_if_reached() |
You might be tempted to use assertions in order to check method arguments for null. However, this is not necessary, since Vala does that implicitly for all parameters that are not marked with ? as being nullable.
void method_name(Foo foo, Bar bar) {
/* Not necessary, Vala does that for you:
return_if_fail(foo != null);
return_if_fail(bar != null);
*/
}
Vala supports basic contract programming features. A method may have preconditions (requires) and postconditions (ensures) that must be fulfilled at the beginning or the end of a method respectively:
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;
}
result is a special variable representing the return value.
Обработка ошибок
GLib имеет систему для управления исключениями во время выполнения GError. Vala транслирвет это в форму, схожую с большинством языков программирования, но реализация этого подразумевает, что это не то же самое, что в Java или C#. Это важно иметь в виду, когда используйте этот метод обработки ошибок - GError очень особенно спроектирован для работы с восстанавливаемыми ошибками исполнения, то есть факторами, которые не известны, пока программа не запущена на живой системе, и это не фатально для исполнения. Вы не должны использовать GError для проблем, которые предусмотрены, такие как неправильное значения было передано в метод. Если метод, например, требует значения больше чем 0 как параметр, провал должен быть засечен используя техники контрактного программирования, такие как преусловии или ассерты, описанные в предыдущем разделе.
Ошибки Vala так же называются проверенными исключениями, что подраземевает, что большинство ошибок может быть обработано в той же точке. Однако, если вы не словите ошибку, Vala компилятор выдаст только предупреждения без остановик процесса компиляции.
Использования исключений (или ошибок в терминах Vala) это вопрос:
1) Объявления, что метод может возбуждать ошибку:
void my_method() throws IOError {
// ...
}
2) Бросая ошибку, когда нужно:
if (something_went_wrong) {
throw new IOError.FILE_NOT_FOUND("Requested file could not be found.");
}
3) Ловя ошибку из вызывающего кода:
try {
my_method();
} catch (IOError e) {
stdout.printf("Error: %s\n", e.message);
}
4) Сравнивая код ошибки с помощью "is" оператора
Все это выглядит более или менее как в других языках, но объявляние типа ошибки довольно уникально. Ошибки имеют три компонента, известные как "домен", "код" и сообщение. Сообщения мы уже видели, это просто кусок текста, предоставляемый, когда ошибка создается. Домены ошибок описывают тип проблемы, и приравниваются к подклассам "Exception" в Java или т.п. В примерах выше мы вообразили домен ошибки, называемый "IOError". Третяя часть, код ошибки, является уточнением, описывающием какая точно произошла проблема. Каждый домен ошибок имеет один или много кодов ошибок - в примере есть код, называемый "FILE_NOT_FOUND".
The way to define this information about error types is related to the implementation in glib. In order for the examples here to work, a definition is needed such as:
errordomain IOError {
FILE_NOT_FOUND
}
When catching an error, you give the error domain you wish to catch errors in, and if an error in that domain is thrown, the code in the handler is run with the error assigned to the supplied name. From that error object you can extract the error code and message as needed. If you want to catch errors from more than one domain, simply provide extra catch blocks. There is also an optional block that can be placed after a try and any catch blocks, called finally. This code is to be run always at the end of the section, regardless of whether an error was thrown or any catch blocks were executed, even if the error was in fact no handled and will be thrown again. This allows, for example, any resources reserved in the try block be freed regardless of any errors raised. A complete example of these features:
public errordomain ErrorType1 {
CODE_1A
}
public errordomain ErrorType2 {
CODE_2A,
CODE_2B
}
public class Test : GLib.Object {
public static void thrower() throws ErrorType1, ErrorType2 {
throw new ErrorType1.CODE_1A("Error");
}
public static void catcher() throws ErrorType2 {
try {
thrower();
} catch (ErrorType1 e) {
// Deal with ErrorType1
} finally {
// Tidy up
}
}
public static int main(string[] args) {
try {
catcher();
} catch (ErrorType2 e) {
// Deal with ErrorType2
if (e is ErrorType2.CODE_2B) {
// Deal with this code
}
}
return 0;
}
}
This example has two error domains, both of which can be thrown by the "thrower" method. Catcher can only throw the second type of error, and so must handle the first type if "thrower" throws it. Finally the "main" method will handle any errors from "catcher".
Parameter Directions
A method in Vala is passed zero or more arguments. The default behaviour when a method is called is as follows:
- Any value type parameters are copied to a location local to the method as it executes.
- Any reference type parameters are not copied, instead just a reference to them is passed to the method.
This behaviour can be changed with the modifiers 'ref' and 'out'.
- 'out' from the caller side
- you may pass an uninitialised variable to the method and you may expect it to be initialised after the method returns
- 'out' from callee side
- the parameter is considered uninitialised and you have to initialise it
- 'ref' from caller side
- the variable you're passing to the method has to be initialised and it may be changed or not by the method
- 'ref' from callee side
- the parameter is considered initialised and you may change it or not
void method_1(int a, out int b, ref int c) { ... }
void method_2(Object o, out Object p, ref Object q) { ... }
These methods can be called as follows:
int a = 1;
int b;
int c = 3;
method_1(a, out b, ref c);
Object o = new Object();
Object p;
Object q = new Object();
method_2(o, out p, ref q);
The treatment of each variable will be:
- "a" is of a value type. The value will be copied into a new memory location local to the method, and so changes to it will not be visible to the caller.
"b" is also of a value type, but passed as an out parameter. In this case, the value is not copied, instead a pointer to the data is passed to the method, and so any change to the method parameter will be visible to the calling code.
- "c" is treated in the same way as "b", the only change is in the signalled intent of the method.
- "o" is of a reference type. The method is passed a reference to the same object as the caller has. The method can therefore change that object, but if it reassigns to the parameter, that change will not be visible to the caller.
"p" is of the same type, but passed as an out parameter. This means that the method will receive a pointer to the reference to the object. It may therefore replace the reference with a reference to another object, and when the method returns the caller will instead own a reference to that other object. When you use this type of parameter, if you do not assign a new reference to the parameter, it will be set to null.
"q" is again of the same type. This case is treated like "p" with the important differences that the method may choose not to change the reference, and may access the object referred to. Vala will ensure that in this instance "q" actually refers to any object, and is not set to null.
Here is an example of how to implement method_1():
void method_1(int a, out int b, ref int c) {
b = a + c;
c = 3;
}
When setting the value to the out argument "b", Vala will ensure that "b" is not null. So you can safely pass null as the second argument of method_1() if you are not interested by this value.
Collections
Gee is a library of collection classes, written in Vala. The classes should all be familiar to users of libraries such as Java's Foundation Classes. Gee consists of a set of interfaces and various types that implement these in different ways.
If you want to use Gee in your own application, install the library separately on your system. Gee can be obtained from http://live.gnome.org/Libgee. In order to use the library you must compile your programs with --pkg gee-1.0.
The fundamental types of collection are:
- Lists: Ordered collections of items, accessible by numeric index.
- Sets: Unordered collections of distinct.
- Maps: Unordered collection of items, accessible by index of arbitrary type.
All the lists and sets in the library implement the Collection interface, and all maps the Map interface. Lists also implement List and sets Set. These common interfaces means not only that all collections of a similar type can be used interchangeably, but also that new collections can be written using the same interfaces, and therefore used with existing code.
Also common to every Collection type is the Iterable interface. This means that any object in this category can be iterated through using a standard set of methods, or directly in Vala using the foreach syntax.
All classes and interfaces use the generics system. This means that they must be instantiated with a particular type or set of types that they will contain. The system will ensure that only the intended types can be put into the collections, and that when objects are retrieved they are returned as the correct type.
Full Gee API documentation, Gee Examples
Some important Gee classes are:
ArrayList<G>
Implementing: Iterable<G>, Collection<G>, List<G>
An ordered list of items of type G backed by a dynamically resizing array. This type is very fast for accessing data, but potentially slow at inserting items anywhere other than at the end, or at inserting items when the internal array is full.
HashMap<K,V>
Implementing: Iterable<Entry<K,V>>, Map<K,V>
A 1:1 map from elements of type K to elements of type V. The mapping is made by computing a hash value for each key - this can be customised by providing pointers to functions for hashing and testing equality of keys in specific ways.
You can optionally pass custom hash and equal functions to the constructor, for example:
var map = new Gee.HashMap<Foo, Object>(foo_hash, foo_equal);
For strings and integers the hash and equal functions are detected automatically, objects are distinguished by their references by default. You have to provide custom hash and equal functions only if you want to override the default behaviour.
HashSet<G>
Implementing: Iterable<G>, Collection<G>, Set<G>
A set of elements of type G. Duplicates are detected by computing a hash value for each key - this can be customised by providing pointers to functions for hashing and testing equality of keys in specific ways.
Read-Only Views
You can get a read-only view of a collection via the read_only_view property, e.g. my_map.read_only_view. This will return a wrapper that has the same interface as its contained collection, but will not allow any form of modification, or any access to the contained collection.
Methods With Syntax Support
Vala recognizes some methods with certain names and signatures and provides syntax support for them. For example, if a type has a contains() method objects of this type can be used with the in operator. The following table lists these special methods. T and Tn are only type placeholders in this table and meant to be replaced with real types.
Indexers |
|
T2 get(T1 index) |
index access: obj[index] |
void set(T1 index, T2 item) |
index assignment: obj[index] = item |
Indexers with multiple indices |
|
T3 get(T1 index1, T2 index2) |
index access: obj[index1, index2] |
void set(T1 index1, T2 index2, T3 item) |
index assignment: obj[index1, index2] = item |
(... and so on for more indices) |
|
Others |
|
T slice(long start, long end) |
slicing: obj[start:end] |
bool contains(T needle) |
in operator: bool b = needle in obj |
string to_string() |
support within string templates: @"$obj" |
Iterator iterator() |
iterable via foreach |
T2 get(T1 index) |
iterable via foreach |
The Iterator type can have any name and must implement one of these two protocols:
bool next() |
standard iterator protocol: iterating until .next() returns false. The current element is retrieved via .get(). |
T? next_value() |
alternative iterator protocol: If the iterator object has a .next_value() function that returns a nullable type then we iterate by calling this function until it returns null. |
This example implements some of these methods:
public class EvenNumbers {
public int get(int index) {
return index * 2;
}
public bool contains(int i) {
return i % 2 == 0;
}
public string to_string() {
return "[This object enumerates even numbers]";
}
public Iterator iterator() {
return new Iterator(this);
}
public class Iterator {
private int index;
private EvenNumbers even;
public Iterator(EvenNumbers even) {
this.even = even;
}
public bool next() {
return true;
}
public int get() {
this.index++;
return this.even[this.index - 1];
}
}
}
void main() {
var even = new EvenNumbers();
stdout.printf("%d\n", even[5]); // get()
if (4 in even) { // contains()
stdout.printf(@"$even\n"); // to_string()
}
foreach (int i in even) { // iterator()
stdout.printf("%d\n", i);
if (i == 20) break;
}
}
Multi-Threading
Threads in Vala
A program written in Vala may have more than one thread of execution, allowing it it do more than one thing at a time. Exactly how this is managed is outside of Vala's scope - threads may be sharing a single processor core or not, depending on the environment.
A thread in Vala is not defined at compile time, instead it is simply a portion of Vala code that is requested at runtime to be executed as a new thread. This is done using the static methods of the Thread class in GLib, as shown in the following (very simplified) example:
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;
}
This short program will request a new thread be created and executed. The code to be run being that in thread_func. Also note the test at the start of the main method - a Vala program will not be able to use threads unless compiled appropriately, so if you build this example in the usual way, it will just display an error and stop running. Being able to check for thread support at runtime allows a program to be built to run either with or without threads if that is wanted. In order to build with thread support, run:
$ valac --thread threading-sample.vala
This will both include required libraries and make sure the threading system is initialised whenever possible.
The program will now run without segmentation faults, but it will still not act as expected. Without any sort of event loop, a Vala program will terminate when its primary thread (the one created to run "main") ends. In order to control this behaviour, you can allow threads to cooperate. This can be done powerfully using event loops and asynchronous queues, but in this introduction to threading we will just show the basic capabilities of threads.
It is possible for a thread to tell the system that it currently has no need to execute, and thereby suggest that another thread should be run instead, this is done using the static method Thread.yield(). If this statement was placed at the end of the above main method, the runtime system will pause the main thread for an instant and check if there are other threads that can be run - on finding the newly created thread in a runnable state, it will run that instead until it is finished - and the program will act is it appears it should. However, there is no guarantee that this will happen still. The system is able to decide when threads run, and as such might not allow the new thread to finish before the primary thread is restarted and the program ends.
In order to wait for a thread to finish entirely there is the join() method. Calling this method on a Thread object causes the calling thread to wait for the other thread to finish before proceeding. It also allows a thread to receive the return value of another, if that is useful. To implement joining threads:
try {
unowned Thread thread = Thread.create(thread_func, true);
thread.join();
} catch (ThreadError e) {
return 1;
}
This time, when we create the thread we give true as the last argument. This marks the thread as "joinable". We also remember the value returned from the creation - an unowned reference to a Thread object (unowned references are explained later and are not vital to this section.) With this reference it is possible to join the new thread to the primary thread. With this version of the program it is guaranteed that the newly created thread will be allowed to fully execute before the primary thread continues and the program terminates.
All these examples have a potential problem, in that the newly created thread doesn't know the context in which it should run. In C you would supply the thread creation method with some data, in Vala instead you would normally pass an instance method to Thread.create, instead of a static method.
Resource Control
Whenever more than one thread of execution is running simultaneously, there is a chance that data are accessed simultaneously. This can lead to race conditions, where the outcome depends on when the system decides to switch between threads.
In order to control this situation, you can use the lock keyword to ensure that certain blocks of code will not be interrupted by other threads that need to access the same data. The best way to show this is probably with an example:
public class Test : GLib.Object {
private int a { get; set; }
public void action_1() {
lock (a) {
int tmp = a;
tmp++;
a = tmp;
}
}
public void action_2() {
lock (a) {
int tmp = a;
tmp--;
a = tmp;
}
}
}
This class defines two methods, where both need to change the value of "a". If there were no lock statements here, it would be possible for the instructions in these methods to be interweaved, and the resulting change to "a" would be effectively random. As there are the lock statements here, Vala will guarantee that if one thread has locked "a", another thread that needs the same lock will have to wait its turn.
In Vala it is only possible to lock members of the object that is executing the code. This might appear to be a major restriction, but in fact the standard use for this technique should involve classes that are individually responsible for controlling a resource, and so all locking will indeed be internal to the class. Likewise, in above example all accesses to "a" are encapsulated in the class.
The Main Loop
GLib включает систему для запуска цикла обработки событий в классах вокруг Main Loop. Цель этой системы - позволить вам написать программу, которая ожидает события и реагирует на них, вместо того, чтобы постоянно проверять условия. Это модель, которую использует GTK +, так что программа может ожидать взаимодействия с пользователем, не имея никакого запущенного в данный момент кода.
Следующая программа создает и запускает Main Loop, а затем присоединяет к нему источник событий. В этом случае источником является простой таймер, который будет выполнять данный метод через 2000 мс. Метод фактически просто остановит Main Loop, что в данном случае приведет к выходу из программы.
void main() {
var loop = new MainLoop();
var time = new TimeoutSource(2000);
time.set_callback(() => {
stdout.printf("Time!\n");
loop.quit();
return false;
});
time.attach(loop.get_context());
loop.run();
}
При использовании GTK Main Loop будет создан автоматически и будет запущен при вызове метода `Gtk.main ()'. Это отмечает точку, когда программа готова к запуску и начинает принимать события от пользователя или из другого места. Код в GTK + эквивалентен приведенному выше короткому примеру, и поэтому вы можете добавлять источники событий почти таким же образом, хотя, конечно, вам нужно использовать методы GTK для управления основным циклом.
void main(string[] args) {
Gtk.init(ref args);
var time = new TimeoutSource(2000);
time.set_callback(() => {
stdout.printf("Time!\n");
Gtk.main_quit();
return false;
});
time.attach(null);
Gtk.main();
}
A common requirement in GUI programs is to execute some code as soon as possible, but only when it will not disturb the user. For this, you use IdleSource instances. These send events to the programs main loop, but request they only be dealt with when there is nothing more important to do.
For more information about event loops, see the GLib and GTK+ documentation.
Asynchronous Methods
Asynchronous methods are methods whose execution can be paused and resumed under the control of the programmer. They are often used in the main thread of an application where a method needs to wait for an external slow task to complete, but must not stop other processing from happening. (For example, one slow operation must not freeze the whole GUI). When the method has to wait, it gives control of the CPU back to its caller (i.e. it yields), but it arranges to be called back to resume execution when data becomes ready. External slow tasks that async methods might wait for include: waiting for data from a remote server, or waiting for calculations in another thread to complete, or waiting for data to load from a disk drive.
Asynchronous methods are normally used with a GLib main loop running, because idle callbacks are used to handle some of the internal callbacks. However under certain conditions async may be used without the GLib main loop, for example if the async methods always yield and Idle.add() is never used. (FIXME: Check what are the exact conditions.)
Asynchronous methods are designed for interleaving the processing of many different long-lived operations within a single thread. They do not by themselves spread the load out over different threads. However, an async method may be used to control a background thread and to wait for it to complete, or to queue operations for a background thread to process.
Async methods in Vala use the GIO library to handle the callbacks, so must be built with the --pkg=gio-2.0 option.
An asynchronous method is defined with the async keyword. For example:
async void display_jpeg(string fnam) {
// Load JPEG in a background thread and display it when loaded
[...]
}
or:
async int fetch_webpage(string url, out string text) throws IOError {
// Fetch a webpage asynchronously and when ready return the
// HTTP status code and put the page contents in 'text'
[...]
text = result;
return status;
}
The method may take arguments and return a value like any other method. It may use a yield statement at any time to give control of the CPU back to its caller.
An async method may be called with any of these three forms:
display_jpeg("test.jpg");
display_jpeg.begin("test.jpg");
display_jpeg.begin("test.jpg", (obj, res) => {
display_jpeg.end(res);
});
The first two forms are equivalent, and start the async method running with the given arguments. The third form in addition registers an AsyncReadyCallback which is executed when the method finishes. In the callback the .end() method should be called to receive the return value of the asynchronous method if it has one. If the async method can throw an exception, the .end() call is where the exception arrives and must be caught. If the method has out arguments, then these should be omitted from the .begin() call and added to the .end() call instead.
For example:
fetch_webpage.begin("http://www.example.com/", (obj, res) => {
try {
string text;
var status = fetch_webpage.end(res, out text);
// Result of call is in 'text' and 'status' ...
} catch (IOError e) {
// Problem ...
}
});
When an asynchronous method starts running, it takes control of the CPU until it reaches its first yield statement, at which point it returns to the caller. When the method is resumed, it continues execution immediately after that yield statement. There are several common ways to use yield:
This form gives up control, but arranges for the GLib main loop to resume the method when there are no more events to process:
Idle.add(fetch_webpage.callback);
yield;
This form gives up control, and stores the callback details for some other code to use to resume the method's execution:
SourceFunc callback = fetch_webpage.callback;
[... store 'callback' somewhere ...]
yield;
Some code elsewhere must now call the stored SourceFunc in order for the method to be resumed. This could be done by scheduling the GLib main loop to run it:
Idle.add((owned) callback);
or alternatively a direct call may be made if the caller is running in the main thread:
callback();
If the direct call above is used, then the resumed asynchronous method takes control of the CPU immediately and runs until its next yield before returning to the code that executed callback(). The Idle.add() method is useful if the callback must be made from a background thread, e.g. to resume the async method after completion of some background processing. (The (owned) cast is necessary to avoid a warning about copying delegates.)
The third common way of using yield is when calling another asynchronous method, for example:
yield display_jpeg(fnam);
or
var status = yield fetch_webpage(url, out text);
In both cases, the calling method gives up control of the CPU and does not resume until the called method completes. The yield statement automatically registers a callback with the called method to make sure that the caller resumes correctly. The automatic callback also collects the return value from the called method.
When this yield statement executes, control of the CPU first passes to the called method which runs until its first yield and then drops back to the calling method, which completes the yield statement itself, and then gives back control to its own caller.
Examples
See Async Method Samples for examples of different ways that async may be used.
Weak References
Vala's memory management is based on automatic reference counting. Each time an object is assigned to a variable its internal reference count is increased by 1, each time a variable referencing an object goes out of scope its internal reference count is decreased by 1. If the reference count reaches 0 the object will be freed.
However, it is possible to form a reference cycle with your data structures. For example, with a tree data structure where a child node holds a reference to its parent and vice versa, or a doubly-linked list where each element holds a reference to its predecessor and the predecessor holds a reference to its successor.
In these cases objects could keep themselves alive simply by referencing to each other, even though they should be freed. To break such a reference cycle you can use the weak modifier for one of the references:
class Node {
public weak Node prev;
public Node next;
}
This topic is exlpained in detail on this page: Vala's Memory Management Explained.
Ownership
Unowned References
Normally when creating an object in Vala you are returned a reference to it. Specifically this means that as well as being passed a pointer to the object in memory, it is also recorded in the object itself that this pointer exists. Similarly, whenever another reference to the object is created, this is also recorded. As an object knows how many references there are to it, it can automatically be removed when needed. This is the basis of Vala's memory management.
Unowned references conversely are not recorded in the object they reference. This allows the object to be removed when it logically should be, regardless of the fact that there might be still references to it. The usual way to achieve this is with a method defined to return an unowned reference, e.g.:
class Test {
private Object o;
public unowned Object get_unowned_ref() {
this.o = new Object();
return this.o;
}
}
When calling this method, in order to collect a reference to the returned object, you must expect to receive a weak reference:
unowned Object o = get_unowned_ref();
The reason for this seemingly overcomplicated example because of the concept of ownership.
- If the Object "o" was not stored in the class, then when the method "get_unowned_ref" returned, "o" would become unowned (i.e. there would be no references to it). If this were the case, the object would be deleted and the method would never return a valid reference.
- If the return value was not defined as unowned, the ownership would pass to the calling code. The calling code is, however, expecting an unowned reference, which cannot receive the ownership.
If the calling code is written as
Object o = get_unowned_ref();
Vala will try to either obtain a reference of or a duplicate of the instance the unowned reference pointing to.
In contrast to normal methods, properties always have unowned return value. That means you can't return a new object created within the get method. That also means, you can't use an owned return value from a method call. The somewhat irritating fact is because of that a property value is owned by the object that HAS this property. A call to obtain this property value should not steal or reproduce (by duplicating, or increasing the reference count of) the value from the object side.
As such, the following example will result in a compilation error
public Object property {
get {
return new Object(); // WRONG: property returns an unowned reference,
// the newly created object will be deleted when
// the getter scope ends the caller of the
// getter ends up receiving an invalid reference
// to a deleted object.
}
}
nor can you do this
public string property {
get {
return getter_method(); // WRONG: for the same reason above.
}
}
public string getter_method() {
return "some text"; // "some text" is duplicated and returned at this point.
}
on the other hand, this is perfectly fine
public string property {
get {
return getter_method(); // GOOD: getter_method returns an unowned value
}
}
public unowned string getter_method() {
return "some text";
// Don't be alarmed that the text is not assigned to any strong variable.
// Literal strings in Vala are always owned by the program module itself,
// and exist as long as the module is in memory
}
The unowned modifier can be used to make automatic property's storage unowned. That means
public unowned Object property { get; private set; }
is identical to
private unowned Object _property;
public Object property {
get { return _property; }
}
The keyword owned can be used to specifically ask a property to return a owned reference of the value, therefore causing the property value be reproduced in the object side. Think twice before adding the owned keyword. Is it a property or simply a get_xxx method? There may also be problems in your design. Anyways, the following code is a correct segment,
public owned Object property { owned get { return new Object(); } }
Unowned references play a similar role to pointers which are described later. They are however much simpler to use than pointers, as they can be easily converted to normal references. However, in general they should not be widely used in the programs unless you know what you are doing.
Ownership Transfer
The keyword owned is used to transfer ownership.
- As a prefix of a parameter type, it means that ownership of the object is transferred into this code context.
- As an type conversion operator, it can be used to avoid duplicating non-reference counting classes, which is usually impossible in Vala. For example,
Foo foo = (owned) bar;
This means that bar will be set to null and foo inherits the reference/ownership of the object bar references.
Variable-Length Argument Lists
Vala supports C-style variable-length argument lists ("varargs") for methods. They are declared with an ellipsis ("...") in the method signature. A method with varargs requires at least one fixed argument:
void method_with_varargs(int x, ...) {
var l = va_list();
string s = l.arg();
int i = l.arg();
stdout.printf("%s: %d\n", s, i);
}
In this example x is a fixed argument to meet the requirements. You obtain the varargs list with va_list(). Then you can retrieve the arguments one after another by calling the generic method arg<T>() sequently on this list, with T being the type that the argument should be interpreted as. If the type is evident from the context (as in our example) the type is inferred automatically and you can just call arg() without the generic type argument.
This example parses an arbitrary number of string - double argument pairs:
void method_with_varargs(int fixed, ...) {
var l = va_list();
while (true) {
string? key = l.arg();
if (key == null) {
break; // end of the list
}
double val = l.arg();
stdout.printf("%s: %g\n", key, val);
}
}
void main() {
method_with_varargs(42, "foo", 0.75, "bar", 0.25, "baz", 0.32);
}
It checks for null as a sentinel to recognize the end of the varargs list. Vala always implicitly passes null as the last argument of a varargs method call.
Varargs have a serious drawback that you should be aware of: they are not type-safe. The compiler can't tell you whether you are passing arguments of the right type to the method or not. That's why you should consider using varargs only if you have a good reason, for example: providing a convenience function for C programmers using your Vala library, binding a C function. Often an array argument is a better choice.
A common pattern with varargs is to expect alternating string - value pairs as arguments, usually meaning gobject property - value. In this case you can write property: value instead, e.g.:
actor.animate (AnimationMode.EASE_OUT_BOUNCE, 3000, x: 100.0, y: 200.0, rotation_angle_z: 500.0, opacity: 0);
is equivalent to:
actor.animate (AnimationMode.EASE_OUT_BOUNCE, 3000, "x", 100.0, "y", 200.0, "rotation-angle-z", 500.0, "opacity", 0);
Pointers
Pointers are Vala's way of allowing manual memory management. Normally when you create an instance of a type you receive a reference to it, and Vala will take care of destroying the instance when there are no more references left to it. By requesting instead a pointer to an instance, you take responsibility for destroying the instance when it is no longer wanted, and therefore get greater control over how much memory is used.
This functionality is not necessarily needed most of the time, as modern computers are usually fast enough to handle reference counting and have enough memory that small inefficiencies are not important. The times when you might resort to manual memory management are:
- When you specifically want to optimise part of a program.
- When you are dealing with an external library that does not implement reference counting for memory management (probably meaning one not based on gobject.)
In order to create an instance of a type, and receive a pointer to it:
Object* o = new Object();
In order to access members of that instance:
o->method_1();
o->data_1;
In order to free the memory pointed to:
delete o;
Vala also supports the address-of (&) and indirection (*) operators known from C:
int i = 42;
int* i_ptr = &i; // address-of
int j = *i_ptr; // indirection
The behavior is a bit different with reference types, you can omit the address-of and indirection operator on assignment:
Foo f = new Foo();
Foo* f_ptr = f; // address-of
Foo g = f_ptr; // indirection
unowned Foo f_weak = f; // equivalent to the second line
The usage of reference-type pointers is equivalent to the use of unowned references.
Non-Object classes
Classes defined as not being descended from GLib.Object are treated as a special case. They are derived directly from GLib's type system and therefore much lighter in weight. In a more recent Vala compiler, one can also implement interfaces, signals and properties with these classes.
One obvious case of using these non-Object classes stays in the GLib bindings. Because GLib is at a lower level than GObject, most classes defined in the binding are of this kind. Also, as mentioned before, the lighter weight of non-object classes make them useful in many practical situations (e.g. the Vala compiler itself). However the detailed usage of non-Object classes are outside the scope of this tutorial. Be aware that these classes are fundamentally different from structs.
D-Bus Integration
D-Bus is tightly integrated into the language and has never been easier than with Vala.
To export a custom class as a D-Bus service you just need to annotate it with the DBus code attribute and register an instance of this class with your local D-Bus session.
[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 inteface) */
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);
}
}
Register an instance of the service and start a main loop:
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 succeded */
() => {}, /* callback on name register succeded */
() => stderr.printf ("Could not aquire name\n"));
/* callback on name lost */
// start main loop
new MainLoop ().run ();
}
You must compile this example with the gio-2.0 package:
$ valac --pkg gio-2.0 dbus-demo-service.vala $ ./dbus-demo-service
All member names are automatically mangled from Vala's lower_case_with_underscores naming convention to D-Bus CamelCase names. The exported D-Bus interface of this example will have a property Something, a signal Sig1 and a method SomeMethod. You can open a new terminal window and call the method from command line with:
$ dbus-send --type=method_call \ --dest=org.example.DemoService \ /org/example/demo \ org.example.DemoService.SomeMethod
or
$ dbus-send --type=method_call \ --dest=org.example.DemoService \ /org/example/demo \ org.example.DemoService.SomeMethodSender \ string:'hello world'
You can also use a graphical D-Bus debugger like D-Feet to browse your D-Bus interfaces and call methods.
Some comprehensive examples: Vala/DBusClientSamples and Vala/DBusServerSample
Профили
Vala поддерживает парочку разных профилей:
- gobject (default)
- posix
- dova
Провиль определяет фичи какого языка доступны и на от какой С библиотеки результирующий С-код будет зависеть.
Для выбора различных профилей используйте ключ valac --profile, например:
valac --profile=posix somecode.vala
Экспериментальные фичи
Некоторые фичи в Vala являются экспериментальными. Это значит, что они не полностью протестированы и могут быть выпилены в будущих версиях.
Относительные выражения в виде цепочки
Эта фича разрешает вам писать сложные относительные вырежения наподобие
if (1 < a && a < 5) {}
if (0 < a && a < b && b < c && c < d && d < 255) {
// do something
}
более естественный способ:
if (1 < a < 5) {}
if (0 < a < b < c < d < 255) {
// do something
}
Литералы регулярных выражений
Регулярные выражения, являются мощным средством для сопоставления с шаблоном в строках. Vala имеет экспериментальную поддержку регулярных выражения (/regex/). Пример:
string email = "tux@kernel.org";
if (/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.match(email)) {
stdout.printf("Валидный email адрес\n");
}
Модификатор "i" делает выражение нечувствительным к регистру. Вы можете хранить регулярное выражение в переменных типа Regex::
Regex regex = /foo/;
Пример регулярного выражения поиска с заменой:
var r = /(foo|bar|cow)/;
var o = r.replace ("this foo is great", -1, 0, "thing");
print ("%s\n", o);
Могут использоваться следующие модификаторы:
i, выражению соответствуют символы как верхнего так и нижнего регистра
m, данный модификатор будет искать соответствие только в одной строке, а не по всему тексту
s, в противоположность модификатору m, ищет соответствие по всему тексту
x, включает игнорирование пробельных символов
Строгий Non-Null режим
Если вы скомпилируете свой код задав ключ --enable-experimental-non-null, то компилятор Vala запустится в строгом not-null режиме проверки и рассмотрит каждый тип на недопустимость значения Null. Можно выборочно убрать проверку с помощью вопросительного знака:
Object o1 = new Object(); // not nullable
Object? o2 = new Object(); // nullable
Компилятор будет следить, чтобы никакие потенциально содержащие null ссылки, не были присвоены ссылкам, которые не могут его содержать. Это присваивание не возможно:
o1 = o2;
o2 может содержать null, а o1 объявлен ненулевым, так что это присваивание запрещено. Однако, если вы уверены, что o1 не null, вы можете изменить это поведение, приведя o1 к не null:
o1 = (!) o2;
Строгий not-null режим, помогает избежать нежелательное использование ссылок содержащих null. Данная возможность раскроется в полную мощь, если все ссылочные типы будут помечены правильным образом, на предмет содержания null.