Язык программирования Си Брайан Керниган, Деннис Ритчи 3е издание Версия 1 Table of Contents
Скачать 2.33 Mb.
|
8.3. Системные вызовы open, creat, close, unlink В отличие от стандартных файлов ввода, вывода и ошибок, которые открыты по умолчанию, остальные файлы нужно открывать явно. Для этого есть два системных вызова: open и сreat Функция open почти совпадает с fopen , рассмотренной в главе 7. Разница между ними в том, что первая возвращает не файловый указатель, а дескриптор файла типа int . При любой ошибке open возвращает -1. #include Как и в fopen , аргумент name — это строка, содержащая имя файла. Второй аргумент, flags , имеет тип int и специфицирует, каким образом должен быть открыт файл. Его основными значениями являются: O_RDONLY — открыть только на чтение; O_WRONLY — открыть только на запись; O_RDWR — открыть и на чтение, и на запись. В System V UNIX эти константы определены в , а в версиях Berkeley (BSD) — в Чтобы открыть существующий файл на чтение, можно написать fd = open(name, O_RDONLY, 0); Далее везде, где мы пользуемся функцией open , ее аргумент perms равен нулю. Попытка открыть несуществующий файл является ошибкой. Создание нового файла или перезапись старого обеспечивается системным вызовом creat . Например int creat(char *name, int perms); fd = сreat(name, perms); Функция creat возвращает дескриптор файла, если файл создан, и -1, если по каким-либо причинам файл создать не удалось. Если файл уже существует, creat "обрежет" его до нулевой длины, что равносильно выбрасыванию предыдущего содержимого данного файла; создание уже существующего файла не является ошибкой. Если строится действительно новый файл, то creat его создаст с правами доступа, специфицированными в аргументе реrms . В системе UNIX с каждым файлом ассоциированы девять битов, содержащие информацию о правах пользоваться этим файлом для чтения, записи и исполнения лицам трех категорий: собственнику файла, определенной им группе лиц и всем остальным. Таким образом, права доступа удобно специфицировать с помощью трех восьмеричных цифр. Например, 0755 специфицирует чтение, запись и право исполнения собственнику файла, а также чтение и право исполнения группе и всем остальным. Для иллюстрации приведем упрощенную версию программы ср системы UNIX, которая копирует один файл в другой. В нашей версии копируется только один файл, не позволяется во втором аргументе указывать директорий (каталог), и права доступа не копируются, а задаются константой. #include #include #include "syscalls.h" #define PERMS 0666 /* RW для собственника, группы и остальных */ void error(char *, ...); /* ср: копирование f1 в f2 */ main(int argc, char *argv[]) { int f1, f2, n; char buf[BUFSIZ]; if (argc != 3) еrrоr("Обращение: ср откуда куда"); if ((f1 = open(argv[1], 0_RDONLY, 0)) == -1) error ("ср: не могу открыть файл %s", argv[1]); if ((f2 = creat(argv[2], PERMS)) == -1) error ("ср: не могу создать файл %s, режим %03о", argv[2], PERMS); while ((n = read(f1, buf, BUFSIZ)) > 0) if (write(f2, buf, n) != n) error ("ср: ошибка при записи в файл %s", argv[2]); return 0; } Данная программа создает файл вывода с фиксированными правами доступа, определяемыми кодом 0666 С помощью системного вызова stat , который будет описан в параграфе 8.6, мы можем определить режим использования существующего файла и задать тот же режим для копии. Заметим, что функция error , вызываемая с различным числом аргументов, во многом похожа на printf Реализация error иллюстрирует, как пользоваться другими программами семейства printf. Библиотечная функция vprintf аналогична printf , с той лишь оговоркой, что переменная часть списка аргументов заменена в ней одним аргументом, который инициализируется макросом va_start . Подобным же образом соотносятся функции vfprintf с fprintf и vsprintf с sprintf #include #include /* error: печатает сообщение об ошибке и умирает */ void error(char *fmt, ...) { va_list args; va_start(args, fmt); fprintf(stderr, "ошибка: "); vfprintf(stderr, fmt, args); fprintf(stderr, "\n"); va_end(args); exit(1); } На количество одновременно открытых в программе файлов имеется ограничение (обычно их число колеблется около 20). Поэтому любая программа, которая намеревается работать с большим количеством файлов, должна быть готова повторно использовать их дескрипторы. Функция close(int fd) разрывает связь между файловым дескриптором и открытым файлом и освобождает дескриптор для его применения с другим файлом. Она аналогична библиотечной функции fclose с тем лишь различием, что никакой очистки буфера не делает. Завершение программы с помощью exit или return в главной программе закрывает все открытые файлы. Функция unlink(char *name) удаляет имя файла из файловой системы. Она соответствует функции remove стандартной библиотеки. Упражнение 8.1. Перепишите программу cat из главы 7, используя функции read , write , open и close Замените ими соответствующие функции стандартной библиотеки. Поэкспериментируйте, чтобы сравнить быстродействие двух версий. 8.4. Произвольный доступ (lseek) Ввод-вывод обычно бывает последовательным, т. е. каждая новая операция чтения-записи имеет дело с позицией файла, следующей за той, что была в предыдущей операции (чтения-записи). При желании, однако, файл можно читать или производить запись в него в произвольном порядке. Системный вызов lseek предоставляет способ передвигаться по файлу, не читая и не записывая данные. Так, функция long lseek(int fd, long offset, int origin); в файле с дескриптором fd устанавливает текущую позицию, смещая ее на величину offset относительно места, задаваемого значением origin . Значения параметра origin 0, 1 или 2 означают, что на величину offset отступают соответственно от начала, от текущей позиции или от конца файла. Например, если требуется добавить что-либо в файл (когда в командном интерпретаторе shell системы UNIX ввод перенаправлен оператором >> в файл или когда в fopen задан аргумент "а" ), то прежде чем что-либо записывать, необходимо найти конец файла с помощью вызова функции lseek(fd, 0L, 2); Чтобы вернуться назад, в начало файла, надо выполнить lseek(fd, 0L, 0); Следует обратить внимание на аргумент 0L : вместо 0L можно было бы написать (long) 0 или, если функция lseek должным образом объявлена, просто 0. Благодаря lseek с файлами можно работать так, как будто это большие массивы, правда, с замедленным доступом. Например, следующая функция читает любое число байтов из любого места файла. Она возвращает число прочитанных байтов или -1 в случае ошибки. #include "syscalls.h" /* get: читает n байт из позиции роз */ int get(int fd, long pos, char *buf, int n) { if (lseek(fd, pos, 0) >= 0) /* установка позиции */ return read(fd, buf, n); else return -1; } Возвращаемое функцией lseek значение имеет тип long и является новой позицией в файле или, в случае ошибки, равно -1. Функция fseek из стандартной библиотеки аналогична lseek ; от последней она отличается тем, что в случае ошибки возвращает некоторое ненулевое значение, а ее первый аргумент имеет тип FILE* 8.5. Пример. Реализация функций fopen и getc Теперь на примере функций fopen и getc из стандартной библиотеки покажем, как описанные выше части согласуются друг с другом. Напомним, что файлы в стандартной библиотеке описываются файловыми указателями, а не дескрипторами. Указатель файла — это указатель на структуру, содержащую информацию о файле: указатель на буфер, позволяющий читать файл большими кусками; число незанятых байтов буфера; указатель на следующую позицию в буфере; дескриптор файла; флажки, описывающие режим (чтение/запись), ошибочные состояния и т. д. Структура данных, описывающая файл, содержится в , который необходимо включать (с помощью #include ) в любой исходный файл, если в том осуществляется стандартный ввод-вывод. Этот же заголовочный файл включен и в исходные тексты библиотеки ввода-вывода. В следующем фрагменте, типичном для файла , имена, используемые только в библиотечных функциях, начинаются с подчеркивания. Это сделано для того, чтобы они случайно не совпали с именами, фигурирующими в программе пользователя. Такое соглашение соблюдается во всех программах стандартной библиотеки. #define NULL О #define EOF (-1) #define BUFSIZ 1024 #define OPEN_MAX 20 /* max число одновременно открытых файлов */ typedef struct _iobuf { int cnt; /* количество оставшихся символов */ char *ptr; /* позиция следующего символа */ char *base; /* адрес буфера */ int flag; /* режим доступа */ int fd; /* дескриптор файла */ } FILE; extern FILE _iob[OPEN_MAX]; #define stdin (&iob[0]) #define stdout (&_iob[1]) #define stderr (&_iob[2]) enum _flags { _READ =01, /* файл открыт на чтение */ _WRITE = 02, /* файл открыт на запись */ _UNBUF = 04, /* файл не буферизуется */ _EOF = 010, /* в данном файле встретился EOF */ _ERR = 020 /* в данном файле встретилась ошибка */ }; int _fillbuf(FILE *); int _flushbuf(int, FILE *); #define feof(p) (((p)->flag & _EOF) != 0) #define ferror(p) (((p)->flag & _ERR) != 0) #define fileno(p) ((p)->fd) #define getc(p) (--(p)->cnt >= 0 \ ? (unsigned char) *(p)->ptr++ : _fillbuf(p)) #define putc(x.p) (--(p)->cnt >= 0 \ ? *(p)->ptr++ = (x) : _flushbuf((x),p)) #define getchar() getc(stdin) #define putchar(x) putc((x), stdout) Макрос getc обычно уменьшает счетчик числа символов, находящихся в буфере, и возвращает символ, после чего приращивает указатель на единицу. (Напомним, что длинные #define с помощью обратной наклонной черты можно продолжить на следующих строках.) Когда значение счетчика становится отрицательным, getc вызывает _fillbuf , чтобы снова заполнить буфер, инициализировать содержимое структуры и выдать символ. Типы возвращаемых символов приводятся к unsigned ; это гарантирует, что все они будут положительными. Хотя в деталях ввод-вывод здесь не рассматривается, мы все же привели полное определение putc . Сделано это, чтобы показать, что она действует во многом так же, как и getc , вызывая функцию _flushbuf , когда буфер полон. В тексте имеются макросы, позволяющие получать доступ к флажкам ошибки и конца файла, а также к его дескриптору. Теперь можно написать функцию fopen . Большая часть инструкций fopen относится к открытию файла, к соответствующему его позиционированию и к установке флажковых битов, предназначенных для индикации текущего состояния. Сама fopen не отводит места для буфера; это делает _fillbuf при первом чтении файла. #include #include "syscalls.h" #define PERMS 0666 /* RW для собственника, группы и проч. */ /* fopen: открывает файл, возвращает файловый указатель */ FILE *fopen(char *name, char *mode) { int fd; FILE *fp; if (*mode != 'r' && *mode != 'w' && *mode != 'a') return NULL; for (fp = _iob; fp < _iob + OPEN_MAX; fp++) if ((fp->flag & (_READ | _WRITE)) == 0) break; /* найдена свободная позиция*/ if (fp >= _iob + OPEN_MAX) /* нет свободной позиции */ return NULL; if (*mode == 'w' ) fd = creat(name, PERMS); else if (*mode == 'a' ) { if ((fd = open(name, O_WRONLY, 0)) == -1) fd = creat(name, PERMS); lseek(fd, 0L, 2); } else fd = open(name, O_RDONLY, 0); if (fd == -1) /* невозможен доступ по имени name */ return NULL; fp->fd = fd; fp->cnt = 0; fp->base = NULL; fp->flag = (*mode == 'r' ) ? _READ : _WRITE; return fp; } Приведенная здесь версия fopen реализует не все режимы доступа, оговоренные стандартом; но, мы думаем, их реализация в полном объеме ненамного увеличит длину программы. Наша fopen не распознает буквы b , сигнализирующей о бинарном вводе-выводе (поскольку в системах UNIX это не имеет смысла), и знака + , указывающего на возможность одновременно читать и писать. Для любого файла в момент первого обращения к нему с помощью макровызова getc счетчик cnt равен нулю. Следствием этого будет вызов _fillbuf . Если выяснится, что файл на чтение не открыт, то функция _fillbuf немедленно возвратит EOF . В противном случае она попытается запросить память для буфера (если чтение должно быть с буферизацией). После получения области памяти для буфера _fillbuf обращается к read , чтобы его наполнить, устанавливает счетчик и указатели и возвращает первый символ из буфера. В следующих обращениях _fillbuf обнаружит, что память для буфера уже выделена. #include "syscalls.h" /* _fillbuf: запрос памяти и заполнение буфера */ int _fillbuf(FILE *fp) { int bufsize; if ((fp->flag&(_READ | _EOF | _ERR)) != _READ) return EOF; bufsize = (fp->flag & _UNBUF) ? 1 : BUFSIZ; if (fp->base == NULL) /* буфера еще нет */ if ((fp->base = (char *) malloc(bufsize)) == NULL) return EOF; /* нельзя получить буфер */ fp->ptr = fp->base; fp->cnt = read(fp->fd, fp->ptr, bufsize); if (--fp->cnt < 0) { if (fp->cnt == -1) fp->flag |= _EOF; else fp->flag |= _ERR; fp->cnt = 0; return EOF; } return (unsigned char) *fp->ptr++; } Единственное, что осталось невыясненным, — это каким образом организовать начало счета. Массив _iob следует определить и инициализировать так, чтобы перед тем как программа начнет работать, в нем уже была информация о файлах stdin , stdout и stderr FILE _iob[OPEN_MAX] = { /* stdin, stdout, stderr: */ { 0, (char *) 0, (char *) 0, _READ, 0 }, { 0, (char *) 0, (char *) 0, _WRITE, 1 }, { 0, (char *) 0, (char *) 0, _WRITE | _UNNBUF, 2 } }; Инициализация flag как части структуры показывает, что stdin открыт на чтение, stdout — на запись, a stderr — на запись без буферизации. Упражнение 8.2. Перепишите функции fopen и _fillbuf , работая с флажками как с полями, а не с помощью явных побитовых операций. Сравните размеры и скорости двух вариантов программ. Упражнение 8.3. Разработайте и напишите функции _flushbuf , fflush и fclose Упражнение 8.4. Функция стандартной библиотеки int fseek(FILE *fp, long offset, int origin) идентична функции lseek с теми, однако, отличиями, что fp — это файловый указатель, а не дескриптор, и возвращает она значение int , означающее состояние файла, а не позицию в нем. Напишите свою версию fseek . Обеспечьте, чтобы работа вашей fseek по буферизации была согласована с буферизацией, используемой другими функциями библиотеки. 8.6. Пример. Печать каталогов При разного рода взаимодействиях с файловой системой иногда требуется получить только информацию о файле, а не его содержимое. Такая потребность возникает, например, в программе печати каталога файлов, работающей аналогично команде ls системы UNIX. Она печатает имена файлов каталога и по желанию пользователя другую дополнительную информацию (размеры, права доступа и т. д.). Аналогичной командой в MS-DOS является dir Так как в системе UNIX каталог — это тоже файл, функции ls , чтобы добраться до имен файлов, нужно только его прочитать. Но чтобы получить другую информацию о файле (например узнать его размер), необходимо выполнить системный вызов. В других системах (в MS-DOS, например) системным вызовом приходится пользоваться даже для получения доступа к именам файлов. Наша цель — обеспечить доступ к информации по возможности системно-независимым способом несмотря на то, что реализация может быть существенно системно-зависима. Проиллюстрируем сказанное написанием программы fsize . Функция fsize — частный случай программы ls ; она печатает размеры всех файлов, перечисленных в командной строке. Если какой-либо из файлов сам является каталогом, то, чтобы получить информацию о нем, fsize обращается сама к себе. Если аргументов в командной строке нет, то обрабатывается текущий каталог. Для начала вспомним структуру файловой системы в UNIXe. Каталог — это файл, содержащий список имен файлов и некоторую информацию о том, где они расположены. "Место расположения" — это индекс, обеспечивающий доступ в другую таблицу, называемую "списком узлов inode". Для каждого файла имеется свой inode, где собрана вся информация о файле, за исключением его имени. Каждый элемент каталога состоит из двух частей: из имени файла и номера узла inode. К сожалению, формат и точное содержимое каталога не одинаковы в разных версиях системы. Поэтому, чтобы переносимую компоненту отделить от непереносимой, разобьем нашу задачу на две. Внешний уровень определяет структуру, названную Dirent , и три подпрограммы opendir , readdir и closedir ; в результате обеспечивается системно-независимый доступ к имени и номеру узла inode каждого элемента каталога. Мы будем писать программу fsize , рассчитывая на такой интерфейс, а затем покажем, как реализовать указанные функции для систем, использующих ту же структуру каталога, что и Version 7 и System V UNIX. Другие варианты оставим для упражнений. Структура Dirent содержит номер узла inode и имя. Максимальная длина имени файла равна NAME_MAX — это значение системно-зависимо. Функция opendir возвращает указатель на структуру, названную DIR (по аналогии с FILE ), которая используется функциями readdir и closedir . Эта информация сосредоточена в заголовочном файле dirent.h #define NAME_MAX 14 /* максимальная длина имени файла; */ /* системно-зависимая величина */ typedef struct { /* универс. структура элемента каталога: */ long ino; /* номер inode */ char name[NAME_MAX+1]; /* имя + завершающий '\0' */ } Dirent; typedef struct { /* минимальный DIR: без буферизации и т.д. */ int fd; /* файловый дескриптор каталога */ Dirent d; /* элемент каталога */ } DIR; DIR *opendir(char *dirname); Dirent *readdir(DIR *dfd); void closedir(DIR *dfd); Системный вызов stat получает имя файла и возвращает полную о нем информацию, содержащуюся в узле inode, или -1 в случае ошибки. Так, char *name; struct stat stbuf; int stat(char *, struct stat *); stat(name, &stbuf); заполняет структуру stbuf информацией из узла inode о файле с именем name . Структура, описывающая возвращаемое функцией stat значение, находится в и выглядит примерно так: struct stat { /* информация из inode, возвращаемая stat */ dev_t st_dev; /* устройство */ ino_t st_ino; /* номер inode */ short st_mode; /* режимные биты */ short st_nlink; /* число связей с файлом */ short st_uid; /* имя пользователя-собственника */ short st_gid; /* имя группы собственника */ dev_t st_rdev; /* для специальных файлов */ off_t st_size; /* размер файла в символах */ time_t st_atime; /* время последнего использования */ time_t st_mtime; /* время последней модификации */ time_t st_ctime; /* время последнего изменения inode */ }; Большинство этих значений объясняется в комментариях. Типы, подобные dev_t и ino_t , определены в файле , который тоже нужно включить посредством #include Элемент st_mode содержит набор флажков, составляющих дополнительную информацию о файле. Определения флажков также содержатся в ; нам потребуется только та его часть, которая имеет дело с типом файла: #define S_IFMT 0160000 /* тип файла */ #define S_IFDIR 0040000 /* каталог */ #define S_IFCHR 0020000 /* символьно-ориентированный */ #define S_IFBLK 0060000 /* блочно-ориентированный */ #define S_IFREG 0100000 /* обычный */ Теперь мы готовы приступить к написанию программы fsize . Если режимные биты ( st_mode ), полученные от stat , указывают, что файл не является каталогом, то можно взять его размер ( st_size ) и напечатать. Однако если файл — каталог, то мы должны обработать все его файлы, каждый из которых в свою очередь может быть каталогом. Обработка каталога — процесс рекурсивный. Программа main просматривает параметры командной строки, передавая каждый аргумент функции fsize #include #include #include "syscalls.h" #include #include #include #include "dirent.h" void fsize(char *); /* печатает размеры файлов */ main(int argc, char **argv) { if (argc == 1) /* по умолчанию берется текущий каталог */ fsize("."); else while (--argc > 0) fsize(*++argv); return 0; } Функция fsize печатает размер файла. Однако, если файл — каталог, она сначала вызывает dirwalk , чтобы обработать все его файлы. Обратите внимание на то, как используются имена флажков S_IFMT и S_IFDIR из при проверке, является ли файл каталогом. Здесь нужны скобки, поскольку приоритет оператора & ниже приоритета оператора == int stat(char *, struct stat *); void dirwalk(char *, void (*fcn)(char *)); /* fsize: печатает размер файла "name" */ void fsize(char *name) { struct stat stbuf; if (stat(name, Sstbuf) == -1) { fprintf(stderr, "fsize: нет доступа к %s\n", name); return; } if ((stbuf.stjnode & S_IFMT) == S_IFDIR) dirwalk(name, fsize); printf("%81d %s\n", stbuf.st_size, name); } Функция dirwalk — это универсальная программа, применяющая некоторую функцию к каждому файлу каталога. Она открывает каталог, с помощью цикла перебирает содержащиеся в нем файлы, применяя к каждому из них указанную функцию, затем закрывает каталог и осуществляет возврат. Так как fsize вызывает dirwalk на каждом каталоге, в этих двух функциях заложена косвенная рекурсия. #define MAX_PATH 1024 /* dirwalk: применяет fcn ко всем файлам из dir */ void dirwalk(char *dir, void (*fcn)(char *)) { char name[MAX_PATH]; Dirent *dp; DIR *dfd; if ((dfd = opendir(dir)) == NULL) { fprintf(stderr, "dirwalk: не могу открыть %s\n", dir); return; } while ((dp = readdir(dfd)) != NULL) { if (strcmp(dp->name, ".") == 0 || strcmp(dp->name, "..") == 0) continue; /* пропустить себя и родителя */ if (strlen(dir)+strlen(dp->name)+2 > sizeof(name)) fprintf (stderr, "dirwalk: слишком длинное имя %s/%s\n", dir, dp->name); else { sprintf(name, "%s/%s", dir, dp->name); (*fcn)(name); } } closedir(dfd); } Каждый вызов readdir возвращает указатель на информацию о следующем файле или NULL , если все файлы обработаны. Любой каталог всегда хранит в себе информацию о себе самом в файле под именем " " и о своем родителе в файле под именем " "; их нужно пропустить, иначе программа зациклится. Обратите внимание: код программы этого уровня не зависит от того, как форматированы каталоги. Следующий шаг — представить минимальные версии opendir , readdir и closedir для некоторой конкретной системы. Здесь приведены программы для систем Version 7 и System V UNIX. Они используют информацию о каталоге, хранящуюся в заголовочном файле , который выглядит следующим образом: #ifndef DIRSIZ #define DIRSIZ 14 #endif struct direct /* элемент каталога */ { ino_t d_ino; /* номер inode */ char d_name[DIRSIZ]; /* длинное имя не имеет '\0' */ }; Некоторые версии системы допускают более длинные имена и имеют более сложную структуру каталога. Тип ino_t задан с помощью typedef и описывает индекс списка узлов inode. В системе, которой пользуемся мы, этот тип есть unsigned short , но в других системах он может быть иным, поэтому его лучше определять через typedef . Полный набор "системных" типов находится в Функция opendir открывает каталог, проверяет, является ли он действительно каталогом (в данном случае это делается с помощью системного вызова fstat , который аналогичен stat , но применяется к дескриптору файла), запрашивает пространство для структуры каталога и записывает информацию. int fstat(int fd, struct stat *); /* opendir: открывает каталог для вызовов readdir */ DIR *opendir(char *dirname) { int fd; struct stat stbuf; DIR *dp; if ((fd = open(dirname, 0_RDONLY, 0)) == -1 || fstat(fd, Sstbuf) == -1 || (stbuf.stjnode & S_IFMT) != S_IFDIR || (dp = (DIR *) malloc(sizeof(DIR))) == NULL) return NULL; dp->fd = fd; return dp; } Функция closedir закрывает каталог и освобождает пространство. /* closedir: закрывает каталог, открытый opendir */ void closedir(DIR *dp) { if (dp) { close(dp->fd); free(dp); } } Наконец, readdir с помощью read читает каждый элемент каталога. Если некий элемент каталога в данный момент не используется (соответствующий ему файл был удален), то номер узла inode у него равен нулю, и данная позиция пропускается. В противном случае номер inode и имя размещаются в статической ( static ) структуре, и указатель на нее выдается в качестве результата. При каждом следующем обращении новая информация занимает место предыдущей. #include /* readdir: последовательно читает элементы каталога */ Dirent *readdir(DIR *dp) { struct direct dirbuf; /* структура каталога на данной системе */ static Dirent d; /* возвращает унифицированную структуру */ while (read(dp->fd, (char *) &dirbuf, sizeof (dirbuf )) == sizeof (dirbuf)) { if (dirbuf.d_ino == 0) /* пустой элемент, не используется */ continue; d.ino = dirbuf.d_ino; strncpy(d.name, dirbuf.d_name, DIRSIZ); d.name[DIRSIZ] = '\0'; /* завершающий символ '\0' */ return &d; } return NULL; } Хотя программа fsize — довольно специализированная, она иллюстрирует два важных факта. Первый: многие программы не являются "системными"; они просто используют информацию, которую хранит операционная система. Для таких программ существенно то, что представление информации сосредоточено исключительно в стандартных заголовочных файлах. Программы включают эти файлы, а не держат объявления в себе. Второе наблюдение заключается в том, что при старании системно-зависимым объектам можно создать интерфейсы, которые сами не будут системно-зависимыми. Хорошие тому примеры — функции стандартной библиотеки. |