D - молодой язык программирования с многолетней историей. Не смотря на то, что язык с таким названием появился очень давно, то, что сейчас называется D2 или просто D, появилось недавно и слабо напоминает предшественника.
Рассказывать про все прелести синтаксиса, компилируемости, сборщика мусора и всего остального нет смысла, про это уже много написано. Рассказывать о том, что в языке есть, пользы мало, настоящий интерес в том, что делать с тем, чего нет? Программисту всегда хочется большего, чем язык может дать, поэтому гибкость языка - вот основной инструмент разработчика.
Для демонстрации гибкости и возможностей расширения в статье приведены несколько реализованных на D конструкций, которые можно встретить в других языках.
Для демонстрации гибкости и возможностей расширения в статье приведены несколько реализованных на D конструкций, которые можно встретить в других языках.
DEBUG_V
Начнём с одного типичного для C++ макроса
Макрос придуман для логирования значения переменных, чтобы каждый раз не слеплять строку с именем и значение. Например, debug_v(id) выведет строку вида “id = 0”. Кончено, для полноценного логгера нужно добавить время, файл, строку кода и прочие полезные данные, но сейчас не об этом. Тут нас интересует именно упрощения логирования значения переменной. Кстати, этот макрос отлично справляется с выражениями, а так же несколькими переменными разделёнными <<. То же самое повторить в D сложно. Полноценную замену сделать у меня так и не получилось, но есть хорошие альтернативы.
Тут просто создаём строку кода, которая делает то, что надо и внедряем её с помощью mixin. Кто не в курсе, mixin это альтернатива сишному #define. Mixin берёт переданную строку, известную на этапе компиляции, и вставляет её в код, где был сам mixin. С помощью mixin D позволяет делать в compile-time всё, ну или почти всё. Если вдруг кто-то знает, как решить эту задачу лучше, буду рад, если поделитесь идеями.
#define debug_v(VAR) Log() << #VAR << " = " << VAR << std::endl
Макрос придуман для логирования значения переменных, чтобы каждый раз не слеплять строку с именем и значение. Например, debug_v(id) выведет строку вида “id = 0”. Кончено, для полноценного логгера нужно добавить время, файл, строку кода и прочие полезные данные, но сейчас не об этом. Тут нас интересует именно упрощения логирования значения переменной. Кстати, этот макрос отлично справляется с выражениями, а так же несколькими переменными разделёнными <<. То же самое повторить в D сложно. Полноценную замену сделать у меня так и не получилось, но есть хорошие альтернативы.
void debug_v(alias name)(typeof(name) val = name)
{
writeln(__traits(identifier, name),"=",val);
}
Эта функция может принимать только переменные. То есть выражение a + b, или поле класса onj.mem она не осилит. Дело в том, что как alias можно передавать только то, что имеет имя, а выражения собственного имени не имеют. Зато пользоваться просто и удобно.
{
writeln(__traits(identifier, name),"=",val);
}
int a = 10;
debug_v!a; // output: a=10
Другой вариант универсальнее, понятнее, но не так красив в использовании:
debug_v!a; // output: a=10
string mix_debug_v(string var)
{
return "writeln(\"" ~ var ~ " = \" ~ to!string(" ~ var ~ "));";
}
{
return "writeln(\"" ~ var ~ " = \" ~ to!string(" ~ var ~ "));";
}
int c = 20;
mixin(mix_debug_v("a+c")); // output: a+c = 30
mixin(mix_debug_v("a+c")); // output: a+c = 30
Тут просто создаём строку кода, которая делает то, что надо и внедряем её с помощью mixin. Кто не в курсе, mixin это альтернатива сишному #define. Mixin берёт переданную строку, известную на этапе компиляции, и вставляет её в код, где был сам mixin. С помощью mixin D позволяет делать в compile-time всё, ну или почти всё. Если вдруг кто-то знает, как решить эту задачу лучше, буду рад, если поделитесь идеями.
Указатель на член
Ещё в языке нет указателей на член класса. Это привычная конструкция для С++, но тут её нет. Зачем её используют другие С++ программисты я не знаю, но у меня это чаще всего способ избежать дублирования одинаковой логики для разных членов. Искусственным примером может послужить сортировка массива объектов по полю. Да, для этого придуманы компораторы, но они более громоздкие, и не забываем, что мы рассматриваем пример. Функции надо передать поле, по которому будет производится сортировка. Использование выглядит так:
struct A
{
int mem1;
int mem2;
}
A[] arr = new A[100];
void sortOn(string name)(A[] array)
{
foreach (ref a; arr)
{
a.memberRef!name = 10;
}
}
sortOn!("mem2")(arr);
Это пример, поэтому он ничего не сортирует, а просто присваивает значение. Здесь string name - compile-time строка с именем поля, а a.memberRef!name - взятие ссылки на поле конкретного объекта a. Реализовано так:
@property ref auto memberRef(string fieldName, Class)(ref Class t)
{
mixin("return t."~fieldName~";");
}
Не претендую на звание автора лучшего кода, возможно, это делается проще и красивее, чем mixin, но это работает. Этот шаблон позволяет вытаскивать ссылку на поле объекта по строковому названию поля, известному на момент компиляции. Задача решена: можно вместо указателя на член класса просто передать compile-time аргумент с его именем. Основной плюс – compile-time, что повышает производительность кода, так как нет нужды идти по указателю, не бывает нулевых и невалидных указателей – в случае ошибки вы об этом узнаете при сборке, а не при падении программы неожиданным образом.
struct A
{
int mem1;
int mem2;
}
A[] arr = new A[100];
void sortOn(string name)(A[] array)
{
foreach (ref a; arr)
{
a.memberRef!name = 10;
}
}
sortOn!("mem2")(arr);
Это пример, поэтому он ничего не сортирует, а просто присваивает значение. Здесь string name - compile-time строка с именем поля, а a.memberRef!name - взятие ссылки на поле конкретного объекта a. Реализовано так:
@property ref auto memberRef(string fieldName, Class)(ref Class t)
{
mixin("return t."~fieldName~";");
}
Не претендую на звание автора лучшего кода, возможно, это делается проще и красивее, чем mixin, но это работает. Этот шаблон позволяет вытаскивать ссылку на поле объекта по строковому названию поля, известному на момент компиляции. Задача решена: можно вместо указателя на член класса просто передать compile-time аргумент с его именем. Основной плюс – compile-time, что повышает производительность кода, так как нет нужды идти по указателю, не бывает нулевых и невалидных указателей – в случае ошибки вы об этом узнаете при сборке, а не при падении программы неожиданным образом.
Elvis Operator
Ещё в D нет оператора .?, или как он выглядит в вашем любимом языке. Это оператор, который обращается к методу или свойству класса как обычная точка, но только в том случае, если объект не нулевой. Этот оператор позволяет упростить код, убрав проверки на null, в случае, если эти ситуации не требуют дополнительной обработки. Полезность сомнительна, но допустим, нам очень хочется такой оператор. Повторить именно со знаком вопроса не получится, но получится не хуже.
A a;
a.ifThis.foo();
a = new A;
a.ifThis.foo();
writeln("Elvis is alive");
Реализовано так:
Этот класс – простая обёртка. Он хранит объект и делегирует ему все вызовы методов. opDispatch это функция, которая вызывается в compile-time, если у объекта этого класса зовут метод, который в нём не объявлен. Если opDispatch нет вообще, то обращение к несуществующему методу вызывает привычную ошибку компиляции. Если есть, то имя несуществующего метода передаётся в виде параметра шаблона (параметра времени компиляции), так же как и передаются все аргументы, хотя их использование возможно уже в рантайме. В нашем случае opDispatch просто проверяет валидность хранимой ссылки на объект и собирает строку кода, которая позовёт нужный метод. Если этого метода у объекта нет, то получим ошибку компиляции. Пользоваться этой обёрткой напрямую неудобно, поэтому реализуем обобщённое property, чтобы работало сразу для всех классов.
@property IfClass!T ifThis(T)(T obj)
{
return IfClass!T(obj);
}
Можно заменить имя ifThis на что-нибудь лучше, жаль только знак вопроса – недопустимое имя.
A a;
a.ifThis.foo();
a = new A;
a.ifThis.foo();
writeln("Elvis is alive");
Реализовано так:
struct IfClass(T)
{
public:
auto opDispatch(string m, Args...)(Args args)
{
if (o)
{
mixin("return o." ~ m ~ ("(args);"));
}
}
private:
T o;
}
{
public:
auto opDispatch(string m, Args...)(Args args)
{
if (o)
{
mixin("return o." ~ m ~ ("(args);"));
}
}
private:
T o;
}
Этот класс – простая обёртка. Он хранит объект и делегирует ему все вызовы методов. opDispatch это функция, которая вызывается в compile-time, если у объекта этого класса зовут метод, который в нём не объявлен. Если opDispatch нет вообще, то обращение к несуществующему методу вызывает привычную ошибку компиляции. Если есть, то имя несуществующего метода передаётся в виде параметра шаблона (параметра времени компиляции), так же как и передаются все аргументы, хотя их использование возможно уже в рантайме. В нашем случае opDispatch просто проверяет валидность хранимой ссылки на объект и собирает строку кода, которая позовёт нужный метод. Если этого метода у объекта нет, то получим ошибку компиляции. Пользоваться этой обёрткой напрямую неудобно, поэтому реализуем обобщённое property, чтобы работало сразу для всех классов.
@property IfClass!T ifThis(T)(T obj)
{
return IfClass!T(obj);
}
Можно заменить имя ifThis на что-нибудь лучше, жаль только знак вопроса – недопустимое имя.
Множественное наследование
По идеологии хорошего ООП в D нет множественного наследования. Не понимаю, кто придумал, что множественное наследование это плохо. Мне очень нравится им пользоваться в С++. Ну раз нет, значит есть альтернативы.
Первая альтернатива – интерфейсы. В отличие от многих других языков, D позволяет писать реализацию методов в интерфейсах. Эти методы должны быть финальными, что не даёт полноценного множественного наследования, но несколько кейсов его использования перекрывает.
Вторая альтернатива – alias this. Эта магическая конструкция позволяет делегировать полю класса вызовы методов и обращения к членам.
Ну и третье решение – сделать самому. Язык даёт достаточно инструментов, чтобы сделать что угодно. Можно свой язык описать и писать на нём, а оно потом транслируется в D и соберётся в бинарь. Поэтому вперёд. Нам снова поможет метод opDispatch. Мысль простая – храним ссылки на объекты всех базовых классов, которые получили в качестве шаблона, а opDispatch делегирует вызовы кому надо. Виртуальное наследование в С++ так и работает. Впервые я понял, при чём тут dispatch в названии, потому что именно этим он и должен заниматься. Первая проверка возможностей:
class B1
{
public string foo()
{
return "foo";
}
public void foo(int val)
{
writeln("B1.foo", val);
}
}
class B2
{
public int selfVal = 0;
this (int val = 0)
{
selfVal = val;
}
public int bar(int val)
{
return val + selfVal;
}
}
class SimpleD : SimpleMulty!(B1,B2)
{
}
Реализация:
class SimpleMulty (Base1, Base2)
{
Base1 b1 = new Base1;
Base2 b2 = new Base2;
auto opDispatch(string name, Args ...) (Args args)
{
static if (__traits(hasMember, b1, name))
{
mixin("return b1." ~ name ~ "(args);");
}
else static if (__traits(hasMember, b2, name))
{
mixin("return b2." ~ name ~ "(args);");
}
else
{
static assert(false);
}
}
}
Как можно видеть методы foo и bar отлично вызываются от объекта m. Даже перегрузка работает и void foo(int val) не путается с string foo(). Очередной раз отмечу, что всё это – compile time, и никаких накладных расходов. Вывод программы:
class Multy (Bases ...)
{
this (Bases bases)
{
bs = bases;
}
Bases bs;
auto opDispatch(string name, Args ...) (Args args)
{
auto b = bs[findMethodOwner!(name, 0, Bases).value];
mixin("return b." ~ name ~ "(args);");
}
}
Всё тот же тест работает как надо. Реализация всё ещё не идеальна: opDispatch не вызывается для свойств и членов, явные проблемы с модификаторами доступа, переопределение будет без override, но это всё-таки множественное наследование и оно работает. Проблемы, кстати, вполне решаемые. Для свойств и членов добавляется @property opDispatch, модификаторы доступа можно проверять через __traits(getAttributes, a) и ему подобные, а синтаксис override добавляется сгенерированными интерфейсами. Просто пока инициатива кончилась.
struct S
{
int x;
alias x this;
}
int foo(int i) { return i * 2; }
void test()
{
S s;
s.x = 7;
int i = -s; // i == -7
i = s + 8; // i == 15
i = s + s; // i == 14
i = 9 + s; // i == 16
i = foo(s); // implicit conversion to int
}
Точно так же работает для классов. Выглядит почти как наследование, язык позволяет множественный alias this. Если хочется переопределить метод «отнаследованного» через alias класса, то можно добавить вложенный (inner) класс, в котором всё переопределить. С точки зрения использования это предельно похоже на множественное наследование. Проблема одна – текущие реализации поддерживают только один alias this на класс. Что-то мне подсказывает, что кто-то ещё не придумал, как решать коллизии имён. Может и до C3 линеаризации додумаются. На данный момент реализация множественных alias this – высокоприоритетная задача разработчиков dmd, за неё даже $100 дают :) Судя по этому топику есть значительный прогресс.{
int x;
alias x this;
}
int foo(int i) { return i * 2; }
void test()
{
S s;
s.x = 7;
int i = -s; // i == -7
i = s + 8; // i == 15
i = s + s; // i == 14
i = 9 + s; // i == 16
i = foo(s); // implicit conversion to int
}
Ну и третье решение – сделать самому. Язык даёт достаточно инструментов, чтобы сделать что угодно. Можно свой язык описать и писать на нём, а оно потом транслируется в D и соберётся в бинарь. Поэтому вперёд. Нам снова поможет метод opDispatch. Мысль простая – храним ссылки на объекты всех базовых классов, которые получили в качестве шаблона, а opDispatch делегирует вызовы кому надо. Виртуальное наследование в С++ так и работает. Впервые я понял, при чём тут dispatch в названии, потому что именно этим он и должен заниматься. Первая проверка возможностей:
class B1
{
public string foo()
{
return "foo";
}
public void foo(int val)
{
writeln("B1.foo", val);
}
}
class B2
{
public int selfVal = 0;
this (int val = 0)
{
selfVal = val;
}
public int bar(int val)
{
return val + selfVal;
}
}
class SimpleD : SimpleMulty!(B1,B2)
{
}
SimpleD sm = new SimpleD;
writeln(sm.foo());
writeln(sm.bar(30));
sm.foo(2);
writeln(sm.foo());
writeln(sm.bar(30));
sm.foo(2);
Реализация:
class SimpleMulty (Base1, Base2)
{
Base1 b1 = new Base1;
Base2 b2 = new Base2;
auto opDispatch(string name, Args ...) (Args args)
{
static if (__traits(hasMember, b1, name))
{
mixin("return b1." ~ name ~ "(args);");
}
else static if (__traits(hasMember, b2, name))
{
mixin("return b2." ~ name ~ "(args);");
}
else
{
static assert(false);
}
}
}
Как можно видеть методы foo и bar отлично вызываются от объекта m. Даже перегрузка работает и void foo(int val) не путается с string foo(). Очередной раз отмечу, что всё это – compile time, и никаких накладных расходов. Вывод программы:
foo
50
Всё круто, но два базовых класса мало. Да и конструктор только по умолчанию. Добавим variadic template и инициализацию полей:B1.foo2
class Multy (Bases ...)
{
this (Bases bases)
{
bs = bases;
}
Bases bs;
auto opDispatch(string name, Args ...) (Args args)
{
auto b = bs[findMethodOwner!(name, 0, Bases).value];
mixin("return b." ~ name ~ "(args);");
}
}
Всё тот же тест работает как надо. Реализация всё ещё не идеальна: opDispatch не вызывается для свойств и членов, явные проблемы с модификаторами доступа, переопределение будет без override, но это всё-таки множественное наследование и оно работает. Проблемы, кстати, вполне решаемые. Для свойств и членов добавляется @property opDispatch, модификаторы доступа можно проверять через __traits(getAttributes, a) и ему подобные, а синтаксис override добавляется сгенерированными интерфейсами. Просто пока инициатива кончилась.
Вместо вывода
Велосипед на D можно построить любой. Изначально вся это шаблонная магия слегка отталкивает, но она однозначно приятнее, чем в С++, и при этом гораздо функциональнее.
Все исходники можно скачать здесь: https://dl.dropboxusercontent.com/u/7162973/MultipleInheritance.7z
Для компиляции и запуска юниттестов нужен компилятор(например dmd: http://dlang.org/download.html) и сборщик/пакетный менеджер dub http://code.dlang.org/download
Если всё это есть, идём с консоли в папку и пишем dub. Всё соберётся и запустится. Кроме того открывать проекты dub умеют некоторые IDE, у меня это Mono-D.
Пример аутпута тестов:
Building package multipleinheritance in C:\Users\geser\Documents\Projects\Multip
leInheritance
multipleinheritance: ["multipleinheritance"]
Target is up to date. Using existing build in C:\Users\geser\Documents\Projects\
MultipleInheritance\.dub\build\application-debug-windows-x86-dmd-B0C01613073A1BA
10F860F241137AAD3\. Use --force to force a rebuild.
Running .\multipleinheritance.exe
a=10
a+c = 30
Elvis is alive
10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10
,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,1
0,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,
10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,
foo
30
B1.foo2
foo
50
B1.foo2
Все исходники можно скачать здесь: https://dl.dropboxusercontent.com/u/7162973/MultipleInheritance.7z
Для компиляции и запуска юниттестов нужен компилятор(например dmd: http://dlang.org/download.html) и сборщик/пакетный менеджер dub http://code.dlang.org/download
Если всё это есть, идём с консоли в папку и пишем dub. Всё соберётся и запустится. Кроме того открывать проекты dub умеют некоторые IDE, у меня это Mono-D.
Пример аутпута тестов:
Building package multipleinheritance in C:\Users\geser\Documents\Projects\Multip
leInheritance
multipleinheritance: ["multipleinheritance"]
Target is up to date. Using existing build in C:\Users\geser\Documents\Projects\
MultipleInheritance\.dub\build\application-debug-windows-x86-dmd-B0C01613073A1BA
10F860F241137AAD3\. Use --force to force a rebuild.
Running .\multipleinheritance.exe
a=10
a+c = 30
Elvis is alive
10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10
,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,1
0,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,
10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,
foo
30
B1.foo2
foo
50
B1.foo2
Комментариев нет:
Отправить комментарий