man!( C => D )

от автора

Каждый С-программист с опытом накапливает привычный багаж техник и идиом. Зачастую бывает сложно понять, как сделать то же самое в новом языке. Так вот, вашему вниманию предлагается коллекция распространенных паттернов на C и их эквивалентов на D. Если вы собираетесь перевести свою программу с C на D или ещё сомневаетесь стоит ли это делать, то эта статья для вас.

Получаем размер типа в байтах

В C используем специальную функцию:

sizeof( int ) sizeof( char * ) sizeof( double ) sizeof( struct Foo ) 

В D у каждого типа есть специальное свойство:

int.sizeof (char*).sizeof double.sizeof Foo.sizeof 

Получаем максимальное и минимальное значение типа

Было на C:

#include <limits.h> #include <math.h>  CHAR_MAX CHAR_MIN ULONG_MAX DBL_MIN

Стало на D:

char.max char.min ulong.max double.min 

Таблица соответствия типов C => D

bool               =>        bool char               =>        char signed char        =>        byte unsigned char      =>        ubyte short              =>        short unsigned short     =>        ushort wchar_t            =>        wchar int                =>        int unsigned           =>        uint long               =>        int unsigned long      =>        uint long long          =>        long unsigned long long =>        ulong float              =>        float double             =>        double long double        =>        real _Imaginary long double =>    ireal _Complex long double   =>    creal

Особые значения чисел с плавающей точкой

Было на C:

#include <fp.h>  NAN INFINITY  #include <float.h>  DBL_DIG DBL_EPSILON DBL_MANT_DIG DBL_MAX_10_EXP DBL_MAX_EXP DBL_MIN_10_EXP DBL_MIN_EXP 

Стало на D:

double.nan double.infinity double.dig double.epsilon double.mant_dig double.max_10_exp double.max_exp double.min_10_exp double.min_exp 

Остаток от деления вещественных чисел

В C используем специальную функцию:

#include <math.h>  float f = fmodf( x , y ); double d = fmod( x , y ); long double r = fmodl( x , y ); 

D имеет специальный оператор для этой операции:

float f = x % y; double d = x % y; real r = x % y; 

Обработка NaN значений

В C сравнение с NaN является неопределённым поведением и разные компиляторы по-разному реагируют (от игнорирования до возбуждения исключения), поэтому приходится использовать специальные функции:

#include <math.h>  if( isnan( x ) || isnan( y ) ) {     result = FALSE; } else {     result = ( x < y ); } 

В D сравнение с NaN — всегда возвращает false:

result = ( x < y );        // false if x or y is nan 

Асерты — полезный механизм выявления ошибок

В C нет встроенного механизма асертов, но он поддерживает псевдоконстанты __FILE__, __LINE__ и макросы, с помощью которых можно реализовать асерты (по факту, у этих констант нет другого практического применения):

#include <assert.h>  assert( e == 0 ); 

D поддерживает асерты на уровне языка:

assert( e == 0 ); 

Итерирование по массиву

На C в задаёте длину массива константой, а потом пробегаетесь по массиву громоздким for-циклом:

#define ARRAY_LENGTH 17 int array[ ARRAY_LENGTH ]; for( i = 0 ; i < ARRAY_LENGTH ; i++ ) {     func( array[i] ); } 

Вы также можете использовать неуклюжее выражение с sizeof(), но это не сильно меняет дело:

int array[17]; for( i = 0 ; i < sizeof( array ) / sizeof( array[0] ) ; i++ ) {     func( array[i] ); } 

В D у массивов есть свойство length:

int array[17]; foreach( i ; 0 .. array.length ) {      func( array[i] ); } 

Но, если есть возможность, лучше использовать foreach-цикл:

int array[17]; foreach( value ; array ) {     func( value ); } 

Инициализация элементов массива

На C вы вынуждены были пробегаться по массиву в цикле (или опять же использовать макрос):

#define ARRAY_LENGTH 17 int array[ ARRAY_LENGTH ]; for( i = 0 ; i < ARRAY_LENGTH ; i++ ) {     array[i] = value; } 

D имеет специальную простую нотацию для этого частого случая:

int array[17]; array[] = value; 

Создание массивов переменной длины

C не поддерживает такие массивы, поэтому приходится заводить отдельную переменную для длины вручную управлять выделением памяти:

#include <stdlib.h>  int array_length; int *array; int *newarray;  newarray = (int *) realloc( array , ( array_length + 1 ) * sizeof( int ) ); if( !newarray ) error( "out of memory" ); array = newarray; array[ array_length++ ] = x; 

D имеет встроенную поддержку массивов переменной длины и сам обеспечивает правильную работу с памятью:

int[] array; int x; array.length = array.length + 1; array[ array.length - 1 ] = x; 

Соединение строк

На C приходится решать множество проблем типа «когда память может быть освобождена», «как обрабатывать нулевые указатели», «как узнать длину строки», «сколько памяти выделить» и другие:

#include <string.h>  char *s1; char *s2; char *s;  // Concatenate s1 and s2, and put result in s free(s); s = (char *) malloc( ( s1 ? strlen( s1 ) : 0 ) + ( s2 ? strlen( s2 ) : 0 ) + 1 ); if( !s ) error( "out of memory" ); if( s1 ) {     strcpy( s, s1 ); } else {     *s = 0; } if( s2 ) {     strcpy( s + strlen( s ) , s2 ); }  // Append "hello" to s char hello[] = "hello"; char *news; size_t lens = s ? strlen( s ) : 0; news = (char *) realloc( s , ( lens + sizeof( hello ) + 1 ) * sizeof( char ) ); if( !news ) error( "out of memory" ); s = news; memcpy( s + lens , hello , sizeof( hello ) ); 

В D есть специальные перегружаемые операторы ~ и ~= предназначенные для соединения списков:

char[] s1; char[] s2; char[] s;  s = s1 ~ s2; s ~= "hello"; 

Форматированный вывод

В C основной способ форматированного вывода — это функция printf():

#include <stdio.h>  printf( "Calling all cars %d times!\n" , ntimes ); 

Что мы напишем в D? Да почти то же самое:

import std.stdio;  writefln( "Calling all cars %s times!" , ntimes ); 

Но в отличие от printf, writef типобезопасен, то есть компилятор проверит соответствие типов переданных параметров типам в шаблоне.

Обращение к функциям до объявления

В C компилятор не позволяет обращаться к функции до того, как встретил её объявление, поэтому приходится либо переносить саму функцию, либо, если перенос не возможен, то вставлять специальную декларацию, говорящую компилятору, что функция будет объявлена позже:

void forwardfunc();  void myfunc() {     forwardfunc(); }  void forwardfunc() {     ... } 

Компилятор D анализирует файл целиком, при этом игнорирует порядок следования объявлений в исходниках:

void myfunc() {     forwardfunc(); }  void forwardfunc() {     ... } 

Функции без аргументов

Было на C:

void foo( void ); 

Стало на D:

void foo() {     ... } 

Выход из нескольких блоков кода

В C операторы break и continue позволяют выйти лишь на один уровень вверх. Чтобы выйти сразу из нескольких блоков кода, приходится использовать goto:

for( i = 0 ; i < 10 ; i++ ) {     for( j = 0 ; j < 10 ; j++ ) {         if( j == 3 ) goto Louter;         if( j == 4 ) goto L2;     }     L2:; } Louter:; 

В D вы можете пометить блок кода и затем выйти из него с любой глубины вложенности:

Louter: for( i = 0 ; i < 10 ; i++ ) {     for( j = 0 ; j < 10 ; j++ ) {         if (j == 3) break Louter;         if (j == 4) continue Louter;     } } // break Louter goes here 

Пространство имён структур

В C несколько напрягает, что у структур отдельное пространство имён, из-за чего каждый раз перед именем структуры приходится указывать ключевое слово struct. Поэтому, типичный способ объявления структур выглядит так:

typedef struct ABC { ... } ABC; 

В D ключевое слово struct используется для объявления структур в том же пространстве имён, что и все остальные объявления, так что достаточно писать просто:

struct ABC { ... } 

Ветвление по строковым значениям (например, обработка аргументов командной строки)

На C вы вынуждены заводить для этого массив строк, синхронный с ним список констант, последовательно итерироваться по массиву в поисках нужной строки, а потом делать switch-case по этим константам:

#include <string.h> void dostring( char *s ) {     enum Strings { Hello, Goodbye, Maybe, Max };     static char *table[] = { "hello", "goodbye", "maybe" };     int i;      for( i = 0 ; i < Max ; i++ ) {         if( strcmp( s , table[i] ) == 0 ) break;     }     switch( i ) {         case Hello:   ...         case Goodbye: ...         case Maybe:   ...         default:      ...     } } 

При большом числе вариантов становится сложно поддерживать синхронность этих трёх структур данных, что ведёт к ошибкам. Кроме того, последовательный перебор вариантов — не слишком эффективен при большом их числе, а значит требуется ещё более сложный код, чтобы искать не линейно, а, например, двоичным поиском или через хеш таблицу.

D же расширяет функционал switch в том числе и на строки, что упрощает исходный код и позволяет компилятору сгенерировать наиболее оптимальный машинный код:

void dostring( string s ) {     switch( s ) {         case "hello":   ...         case "goodbye": ...         case "maybe":   ...         default:        ...     } } 

Выравнивание полей структур

В C управление выравниванием происходит через аргументы компилятора и влияет сразу на всю программу и боже упаси вас не перекомпилировать какой-нибудь модуль или библиотеку. Для решения этой проблемы используются директивы препроцессора #pragma pack, но директивы эти не портабельны и сильно зависят от используемого компилятора:

#pragma pack(1) struct ABC {     ... }; #pragma pack() 

В D есть специальный синтаксис, с помощью которого вы можете детально настроить как выравнивать те или иные поля (По умолчанию поля выравниваются в совместимой с C манере):

struct ABC {     int z;              // z is aligned to the default      align(1) int x;    // x is byte aligned     align(4) {         ...             // declarations in {} are dword aligned     }     align(2):          // switch to word alignment from here on      int y;              // y is word aligned } 

Анонимные структуры и объединения

C требует всем структурам давать имена, даже если они излишни:

struct Foo {     int i;     union Bar {         struct Abc { int x; long y; } _abc;         char *p;     } _bar; };  #define x _bar._abc.x #define y _bar._abc.y #define p _bar.p  struct Foo f;  f.i; f.x; f.y; f.p; 

Этот код не просто громоздкий, но и с использованием макросов для инкапсуляции внутренней структуры, что приводит к тому, что символьный отладчик не понимает что тут происходит, да ещё и макросы эти имеют глобальную область видимости, а не ограничены одной лишь структурой.

D поддерживает анонимные структуры, что позволяет выражать вложенные сущности более естественным образом:

struct Foo {     int i;     union {         struct { int x; long y; }         char* p;     } }  Foo f;  f.i; f.x; f.y; f.p; 

Определение структур и переменных

На C вы можете объявить и структуру и переменную одним выражением:

struct Foo { int x; int y; } foo; 

Или по отдельности:

struct Foo { int x; int y; };   // note terminating ; struct Foo foo; 

В D всегда используются отдельные выражения:

struct Foo { int x; int y; }    // note there is no terminating ; Foo foo; 

Получение смещения поля структуры

В C, опять же, используются макросы:

#include <stddef> struct Foo { int x; int y; };  off = offsetof( Foo , y ); 

В D у каждого поля есть специальное свойство:

struct Foo { int x; int y; }  off = Foo.y.offsetof; 

Инициализация объединений

В C инициализируется первое подходящее по типу поле, что может приводить к скрытым багам при изменении их состава и порядка:

union U { int a; long b; }; union U x = { 5 };                // initialize member 'a' to 5 

В D вам необходимо явно указать какому полю вы присваиваете значение:

union U { int a; long b; } U x = { a : 5 }; 

Инициализация структур

В C поля инициализируются в порядке их объявления, что не является проблемой для маленьких структур, но становится настоящей головной болью в случае структур больших, а также в случаях, когда необходимо изменить порядок следования и состав полей:

struct S { int a; int b; int d; int d; }; struct S x = { 5 , 3 , 2 , 10 }; 

В D вы тоже можете инициализировать поля по порядку, но лучше всё же явно указывать имена инициализируемых полей:

struct S { int a; int b; } S x = { b : 3 , a : 5 }; 

Инициализация массивов

В C массивы инициализируются по порядку следования элементов:

int a[3] = { 3 , 2 , 2 }; 

Вложенные массивы в C могут не окружаться фигурными скобками:

int b[3][2] = { 2,3 , { 6 , 5 } , 3,4 }; 

В D, разумеется, элементы инициализируются также по порядку, но вы можете и явно указывать смещения. Следующие объявления приводят к одному и тому же результату:

int[3] a = [ 3, 2, 0 ]; int[3] a = [ 3, 2 ];            // unsupplied initializers are 0, just like in C int[3] a = [ 2 : 0, 0 : 3, 1 : 2 ]; int[3] a = [ 2 : 0, 0 : 3, 2 ];     // if not supplied, the index is the previous one plus one. 

Явное указание индексов очень полезно, когда в качестве смещений необходимо иметь значение из какого-либо набора:

enum color { black, red, green } int[3] c = [ black : 3, green : 2, red : 5 ]; 

Скобки для вложенных массивов обязательны:

int[2][3] b = [ [ 2 , 3 ] , [ 6 , 5 ] , [ 3 , 4 ] ];  int[2][3] b = [ [ 2 , 6 , 3 ] , [ 3 , 5 , 4 ] ];            // error 

Экранирование спецсимволов в строках

В C проблемно использовать символ обратной косой черты, так как он означает начало специальной последовательности, поэтому его необходимо дублировать:

char file[] = "c:\\root\\file.c"; // c:\root\file.c char quoteString[] = "\"[^\\\\]*(\\\\.[^\\\\]*)*\""; // /"[^\\]*(\\.[^\\]*)*"/ 

В D в дополнение к обычным строкам с экранированием в стиле C, есть и так называемые «сырые строки», где экранирование не работает, и вы получаете ровно то, что ввели:

string file = r"c:\root\file.c";  // c:\root\file.c string quotedString = `"[^\\]*(\\.[^\\]*)*"`;  // "[^\\]*(\\.[^\\]*)*" 

ASCII против многобайтных кодировок

В C используется отдельный тип символов wchar_t и специальный префикс L у строковых констант:

#include <wchar.h> char foo_ascii[] = "hello"; wchar_t foo_wchar[] = L"hello"; 

Но из-за этого есть проблема с написанием универсального кода, совместимого с разными типами символов, что решается специальными макросами, добавляющими необходимые конвертации:

#include <tchar.h> tchar string[] = TEXT( "hello" ); 

Компилятор D выводит типы констант из контекста использования, снимая с программиста бремя указывать типы символов вручную:

string  utf8  = "hello";     // UTF-8 string wstring utf16 = "hello";     // UTF-16 string dstring utf32 = "hello";     // UTF-32 string 

Однако, есть и специальные суффиксы, указывающие тип символов строковых констант:

auto str    = "hello";       // UTF-8 string auto _utf8  = "hello"c;      // UTF-8 string auto _utf16 = "hello"w;      // UTF-16 string auto _utf32 = "hello"d;      // UTF-32 string 

Отображение перечисления на массив

В C вы отдельно объявляете перечисление, отдельно массив, что довольно сложно поддерживать, когда число элементов разрастается:

enum COLORS { red , blue , green , max }; char *cstring[ max ] = { "red" , "blue" , "green" }; 

В D такое отображение задаётся парами ключ-значение, что гораздо проще в поддержке:

enum COLORS { red, blue, green }  string[ COLORS.max + 1 ] cstring = [     COLORS.red : "red",     COLORS.blue : "blue",     COLORS.green : "green", ]; 

Создание новых типов

В C оператор typedef на самом деле создаёт не новый тип, а всего лишь псевдоним:

typedef void *Handle; void foo( void * ); void bar( Handle );  Handle h; foo( h ); // coding bug not caught bar( h ); // ok 

При этом, для задания значения по умолчанию, приходится использовать макросы:

#define HANDLE_INIT ( (Handle) -1 )  Handle h = HANDLE_INIT; h = func(); if( h != HANDLE_INIT ) {     ... } 

Чтобы в C реально создать новый тип, с которым будет работать как проверка типов, так и перегрузка функций, необходимо создать создать структуру:

struct Handle__ { void *value; } typedef struct Handle__ *Handle; void foo( void * ); void bar( Handle );  Handle h; foo( h ); // syntax error bar( h ); // ok 

А работа со значениями по умолчанию превращается в чёрную магию:

struct Handle__ HANDLE_INIT;  // call this function upon startup void init_handle() {     HANDLE_INIT.value = (void *)-1; }  Handle h = HANDLE_INIT; h = func(); if( memcmp( &h , &HANDLE_INIT , sizeof( Handle ) ) != 0 ) {     ... } 

D же обладает мощными возможностями метапрограммирования, что позволяет реализовать typedef самостоятельно и подключать из библиотеки:

import std.typecons;  alias Handle = Typedef!( void* ); void foo( void* ); void bar( Handle );  Handle h; foo( h ); // syntax error bar( h ); // ok 

Вторым параметром шаблона Typedef можно указать значение по умолчанию, которое и попадёт в стандартное свойство всех типов — init:

alias Handle = Typedef!( void* , cast( void* ) -1 ); Handle h; h = func(); if( h != Handle.init ) {     ... } 

Сравнение структур

На C нет простого способа сравнить две структуры, поэтому приходится использовать сравнение диапазонов памяти:

#include <string.h>  struct A x , y; ... if( memcmp( &x , &y , sizeof( struct A ) ) == 0 ) {     ... } 

Отсутствие проверки типов оказывается не самой серьёзной проблемой этого кода. Дело в том, что поля структуры хранятся выровненными по границам машинного слова из соображений производительности, но компилятор C не гарантирует, что в промежутках между полями не будет мусора, оставшегося от ранее хранящихся в том же месте памяти данных, что приведёт к тому, что вроде бы одинаковые структуры признаются различными.

В D вы просто сравниваете значения, а компилятор обо всём позаботится:

A x , y; ... if( x == y ) {     ... } 

Сравнение строк

В C используется специальная функция, которая последовательно сравнивает байты до нулевого байта, которым заканчиваются все строки:

char str[] = "hello";  if( strcmp( str , "betty" ) == 0 ) {  // do strings match?     ... } 

В D же вы просто используете стандартный оператор сравнения:

string str = "hello";  if( str == "betty" ) {     ... } 

Строка в D являются не более чем массивом символов, перед которым сохранена его длина, что позволяет сравнивать строки с гораздо большей эффективностью, посредством сравнения диапазонов памяти. Более того D поддерживает и операции отношения в отношении строк:

string str = "hello";  if( str < "betty" ) {     ... } 

Сортировка массивов

Хоть многие C программисты и велосипедят из раза в раз пузырьковые сортировки, правильный путь — это использовать библиотечную функцию qsort():

int compare( const void *p1 , const void *p2 ) {     type *t1 = (type *) p1;     type *t2 = (type *) p2;      return *t1 - *t2; }  type array[10]; ... qsort( array , sizeof( array ) / sizeof( array[0] ), sizeof( array[0] ), compare ); 

К сожалению, функция compare() должна быть объявлена явно и подходить к сортируемым типам.

D имеет мощную библиотеку алгоритмов, работающую как со встроенными, так и с пользовательскими типами:

import std.algorithm; type[] array; ... sort( array ); // sort array in-place array.sort!"a>b" // using custom compare function array.sort!( ( a , b ) => ( a > b ) )  // same as above 

Строковые литералы

C не поддерживает многострочные строковые константы, однако с помощью экранирования перевода строки можно добиться их подобия:

"This text \"spans\"\n\ multiple\n\ lines\n" 

В D экранировать необходимо лишь кавычки, что позволяет вставлять текст в исходники практически как есть:

"This text \"spans\" multiple lines " 

Обход структур данных

Рассмотрим простую функцию поиска строки в бинарном дереве. В C мы вынуждены создать вспомогательную функцию membersearchx, которая используется для непосредственно обхода дерева. Чтобы она не просто ходила, но и делала что-то полезное мы передаём ей ссылку на контекст в виде специальной структуры Paramblock:

struct Symbol {     char *id;     struct Symbol *left;     struct Symbol *right; };  struct Paramblock {     char *id;     struct Symbol *sm; };  static void membersearchx( struct Paramblock *p , struct Symbol *s ) {     while( s ) {         if( strcmp( p->id , s->id ) == 0 ) {             if( p->sm ) error( "ambiguous member %s\n" , p->id );             p->sm = s;         }          if( s->left ) {             membersearchx(p,s->left);         }         s = s->right;     } }  struct Symbol *symbol_membersearch( Symbol *table[] , int tablemax , char *id ) {     struct Paramblock pb;     int i;      pb.id = id;     pb.sm = NULL;     for( i = 0 ; i < tablemax ; i++ ) {         membersearchx( pb , table[i] );     }     return pb.sm; } 

В D всё гораздо проще — достаточно объявить вспомогательную функцию внутри реализуемой, и первая получит доступ к переменным второй, так что нам не приходится прокидывать в неё дополнительный контекст через параметры:

class Symbol {     char[] id;     Symbol left;     Symbol right; }  Symbol symbol_membersearch( Symbol[] table , char[] id ) {     Symbol sm;      void membersearchx( Symbol s ) {         while( s ) {             if( id == s.id ) {                 if( sm ) error( "ambiguous member %s\n" , id );                 sm = s;             }              if( s.left ) {                 membersearchx(s.left);             }             s = s.right;         }     }      for( int i = 0 ; i < table.length ; i++ ) {         membersearchx( table[i] );     }      return sm; } 

Динамические замыкания

Рассмотрим простой контейнерный тип. Чтобы быть реиспользуемым, ему необходимо уметь применять некоторый сторонний код к каждому элементу. В C это реализуется посредством передачи ссылки на функцию, которая и вызывается с каждым элементом в качестве параметра. В большинстве случаев дополнительно ей нужно передавать и некоторый контекст с состоянием. Для примера, передадим функцию вычисляющую максимальное значение чисел из списка:

void apply( void *p , int *array , int dim , void (*fp) ( void* , int ) ) {     for( int i = 0 ; i < dim ; i++ ) {         fp( p , array[i] );     } }  struct Collection {     int array[10]; };  void comp_max( void *p , int i ) {     int *pmax = (int *) p;      if( i > *pmax ) {         *pmax = i;     } }  void func( struct Collection *c ) {     int max = INT_MIN;      apply( &max , c->array , sizeof( c->array ) / sizeof( c->array[0] ) , comp_max ); } 

В D вы можете передать так называемый делегат — функцию, привязанную к некоторому контексту. Когда вы передаёте куда-либо ссылку на функцию, которая зависит от контекста, в котором она объявлена, то на самом деле передаётся именно делегат.

class Collection {     int[10] array;      void apply( void delegate( int ) fp ) {         for( int i = 0 ; i < array.length ; i++ ) {             fp( array[i] );         }     } }  void func( Collection c ) {     int max = int.min;      void comp_max( int i ) {         if( i > max ) max = i;     }      c.apply( &comp_max ); } 

Или вариант по проще, с анонимным делегатом:

void func( Collection c ) {     int max = int.min;      c.apply( ( int i ) {         if( i > max ) max = i;     } ); } 

Переменное число аргументов

Простой пример, как на C написать функцию, суммирующую все переданные ей аргументы, сколько бы их ни было:

#include <stdio.h> #include <stdarg.h>  int sum( int dim , ... ) {     int i;     int s = 0;     va_list ap;      va_start( ap , dim );     for( i = 0 ; i < dim ; i++) {         s += va_arg( ap , int );     }     va_end( ap );     return s; }  int main() {     int i;      i = sum(3, 8 , 7 , 6 );     printf( "sum = %d\n" , i );      return 0; } 

Как видим, нам пришлось явно указать при вызове сколько параметров мы собираемся передать в функцию, что не просто избыточно с точки зрения программиста, но и является потенциальным источником трудноуловимых багов. Ну и куда же без традиционной проблемы — проверка передаваемых в функцию типов лежит целиком и полностью на совести программиста.

В D же есть специальная конструкция "…" позволяющая принять несколько параметров в качестве одного типизированного массива:

import std.stdio;  int sum( int[] values ... ) {     int s = 0;      foreach( int x ; values ) {         s += x;     }     return s; }  int main() {     int i = sum( 8 , 7 , 6 );          writefln( "sum = %d", i );      return 0; } 

И наоборот, вы можете передать массив в функцию, которая принимает переменное число параметров:

int main() { 	int[] ints = [ 8 , 7 , 6 ];      int i = sum( ints );      writefln( "sum = %d", i );      return 0; } 

Заключение

В этой статье мы рассмотрели преимущественно низкоуровневые возможности языка D, во многом являющиеся небольшим эволюционным шагом относительно языка C. В следующих статьях мы рассмотрим вопрос перехода с более мощного языка C++ и более простого Go. Оставайтесь на связи.

Ну что, на какой системный язык переходим?

Проголосовал 1 человек. Воздержавшихся нет.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

ссылка на оригинал статьи https://habrahabr.ru/post/276227/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *