12.1 Пример

Предыдущая  Содержание  Следующая V*D*V

Наш калькулятор содержит таблицу символов с переменными, константами и функциями. В то время как константы и математические функции предопределены, значения всех переменных и определяемых пользователем функциональных определений теряются каждый раз, когда завершается выполнение приложения. В качестве реалистичного примера использования постоянных объектов мы добавляем в калькулятор два оператора: save сохранит некоторых или все переменные и определения функций в файлах; load вернёт их оттуда.

 

$ value

def sqr = $ * $

def one = sqr(sin($)) + sqr(cos($))

let n = one(10)

    1

save

 

В этом сеансе с помощью value мы определяем функции sqr() и one(), чтобы протестировать, что sin2x + cos2x ≡ 1. Кроме того, мы создаём переменную n со значением 1. save без параметров записывает эти три определения в файл value.stb.

 

$ value

load

n + one(20)

    2

 

При новом запуске value мы можем использовать load, чтобы вернуть эти определения. Это выражение демонстрирует восстановление значения переменной и определений обоих функций.

save реализована в функции синтаксического анализатора stmt(), точно так же, как let или def. Аргумента нет, необходимо сохранить все переменные и функции; поэтому мы просто передаём эту задачу в Symtab:

 

#define SYMTABFILE "value.stb"

#define SYMBOLFILE "%s.sym"

 

static void * stmt (void) {

    void * sym, * node;

 

    switch (token) {

        ...

    case SAVE:

        if (! scan(0)) {            /* вся таблица символов целиком */

            if (save(table, 0, SYMTABFILE))

                error("cannot save symbol table");

        }

        else                        /* список символов */

        do {

            char fnm [BUFSIZ];

 

            sprintf(fnm, SYMBOLFILE, name(symbol));

            if (save(table, symbol, fnm))

                error("cannot save %s", name(symbol));

        } while (scan(0));

        return 0;

 

Более сложный синтаксис мог бы позволить указать имя файла как часть save. А пока мы предопределяем SYMTABFILE и SYMBOLFILE: вся таблица символов сохраняется в value.stb, а единственный символ, подобный one, был бы сохранён в файл one.sym. Приложение управляет именем файла, то есть оно создано в parse.c и передаётся в save().

 

Symtab.d

 

% Class Symtab: Object {

    ...

%

 

    ...

    int save (const _self, const Var @ entry, const char * fnm);

    int load (_self, Symbol @ entry, const char * fnm);

%}

 

Symtab.dc

 

% Symtab save {

    const struct Symtab * self = cast(Symtab(), _self);

    FILE * fp;

 

    if (entry) {                    // один символ

        if (! respondsTo(entry, "move"))

            return EOF;

        if (! (fp = fopen(fnm, "w")))

            return EOF;

        puto(entry, fp);

    }

 

Единственный символ передан как entry. Нет никакого смысла в сохранении не определённых символов, констант или математических функций. Поэтому сохраняем только символы, которые поддерживают метод move(); как мы увидим позже, этот метод используется при загрузке символа из файла. save() открывает выходной файл и позволяет puto() выполнить фактическую работу. Сохранение всех символов почти какое же простое:

 

    else {                           // вся таблица целиком

        int i;

        if (! (fp = fopen(fnm, "w")))

            return EOF;

        for (i = 0; i < self -> count; ++ i)

            if (respondsTo(self -> buf[i], "move"))

                puto(self -> buf[i], fp);

    }

    return fclose(fp);              // 0 или EOF

}

 

save() определена как метод Symtab, поскольку необходим цикл по элементам в .buf[]. Тест, должен ли символ быть сохранён в файле, не повторяется за пределами save(). Однако, Symtab не должен знать, какие виды символов мы имеем. Поэтому решение о сохранении основывается на наличии move(), а не на членстве в определённых подклассах Symbol.

Похоже, что загрузка должна быть полностью симметричной к сохранению. Снова, parse.c принимает решение об имени файла и позволяет load() выполнить фактическую работу:

 

    case LOAD:

        if (! scan(0)) {            /* вся таблица символов целиком */

            if (load(table, 0, SYMTABFILE))

                error("cannot load symbol table");

        }

        else                        /* список символов */

            do {

                char fnm [BUFSIZ];

 

                sprintf(fnm, SYMBOLFILE, name(symbol));

                if (load(table, symbol, fnm))

                    error("cannot load %s", name(symbol));

            } while (scan(0));

        reclaim(Node(), sunder);

        return 0;

 

К сожалению, load() сильно отличается от save(). Есть две причины: необходимо по крайней мере попытаться защитить наш калькулятор от кого-то, кто переделает имена файлов или содержание; и хотя save() может просто вывести на экран что-то из таблицы символов с помощью puto(), довольно вероятно, что потребуется добавить или изменить символы в этой таблице в ходе работы save(). Возвращение постоянных объектов очень похоже на первоначальное выделение им памяти и конструирование.

Давайте посмотрим на работу load(). Если должен быть загружен единственный символ, его имя уже находится в таблице символов. Поэтому мы ищем или не определённый Symbol или символ, знающий как ответить на move():

 

% Symtab load {

    struct Symtab * self = cast(Symtab(), _self);

    const char * target = NULL;

    FILE * fp;

    int result = EOF;

    void * in;

 

    if (entry)

        if (isOf(entry, Symbol())

                || respondsTo(entry, "move"))

            target = name(entry);

        else

            return EOF;

 

Если entry существует, начальная проверка предохраняет нас от напрасной работы. Затем обращаемся к файлу и пытаемся прочитать из него столько символов, сколько сможем:

 

    if (! (fp = fopen(fnm, "r")))

        return EOF;

 

    while (in = retrieve(fp))

        ...

    if (! target && feof(fp))

        result = 0;

 

    fclose(fp);

    return result;

}

 

Если мы не ищем определенную entry, то будем рады достичь конца файла. retrieve() возвращает объект из потока; это будет обсуждаться в разделе 12.4.

Тело цикла while обрабатывает по одному символу за раз. У нас большая беда, если полученный объект не знает о move(), потому что поток, вероятно, был записан не save(). Если это произошло, пора выйти из цикла и позволить load() вернуть EOF. В противном случае, если мы ищем определенную entry, пропускаем все символы с другими именами.

 

{

    const char * nm;

    void ** pp;

 

    if (! respondsTo(in, "move"))

        break;

 

    if (target && strcmp(name(in), target))

        continue;

 

Технически parse.c настраивает всё так, что файл должен либо содержать одну желаемую запись, либо всю таблицу символов, а strcmp() защищает нас от переименованных или изменённых файлов.

Мы готовы занести полученный символ в таблицу символов. Вот почему load() является методом Symtab. Этот процесс очень похож на то, что делает screen(): мы предполагаем, что retrieve() позаботился динамически сохранить имя, и используем search(), чтобы определить местоположение имени в таблице. Если возвращено то же имя, мы только что прочитали новый символ, и можем просто вставить его в таблицу.

 

    nm = name(in);

 

    pp = search(self, & nm);

 

    if (* pp == nm) // ещё не в таблице

        * pp = in;

 

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

 

    else if (isA(* pp, Symbol()))

                                // ещё не определён

    {

        nm = name(* pp), delete(* pp), free((void *) nm);

        * pp = in;

    }        // можно освободить значение, но затем ниже мы выходим

 

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

 

    else if (! respondsTo(* pp, "move")) {

        nm = name(in); delete(in); free((void *) nm);

        continue;               // не должно произойти

    }

    else {

        move(* pp, in);

        delete(in), free((void *) nm);

    }

 

    if (target) {

        result = 0;

        break;

    }

}

 

Если желаемая entry найдена, можно покинуть цикл.

Мы должны быть очень осторожны, чтобы не заменить существующий символ, потому что какое-нибудь выражение могло бы уже указывать на него. Вот поэтому для передачи значения от одного символа другому добавлен динамически компонуемый метод move() .

 

Symbol.d

 

% VarClass: Class Var: Symbol {

    ...

%-

    void move (_self, _from);

%}

 

Symbol.dc

 

% Var move {

%casts

    setvalue(self, from -> value);

}

 

% : Const move {        // don’t respondTo move

}

 

% Fun move {

%casts

    setfun(self, from -> fun), from -> fun = 0;

}

 

Var и Fun подготовлены для move(), но Const — нет. move() выполняет работу, подобную операции поверхностного копирования (shallow copy): для Fun, который указывает на выражение, она передаёт указатель, но не копирует всё выражение. move() фактически очищает исходный указатель так, чтобы исходный объект мог быть удалён без уничтожения переданного выражения.

 

 

Предыдущая  Содержание  Следующая