Интернет - первое, что построило человечество и чего человечество не понимает, крупнейший эксперимент в анархии за всю нашу историю .
-- Эрик Шмидт
Контекст
Очень часто кто-то где-нибудь на каком-нибудь форуме жалуется на нехватку инкапсуляции и изоляции в языке программирования C. Это происходит с такой регулярностью, что я намерен раз и навсегда разрушить этот миф. Благодаря этому когда в следующий раз кто-то будет делать подобные заявления, я смогу просто дать ссылку на эту страницу, а не писать объяснение заново.
Нужно сказать, что C – это старый язык, в котором не хватает множества современных возможностей. Но чего в нём хватает, так это инкапсуляции и изоляции.
Миф: поля структур не могут быть скрытыми
Давайте взглянем на определение класса, имеющего члены private
; в конце-концов, ведь именно так возникает изоляция, правда? Если бы все поля были публичными, то это был бы просто C, но с наследованием1.
Это сарказм, так что расслабьтесь.
class StringBuilder {
private String payload;
public void Append(String snippet) {
payload = payload + snippet;
}
};
Любая попытка вызывающей стороны получить доступ к полю payload
приведёт к ошибке компиляции. Довольно удобно. Вы понимаете, когда это может быть полезно.
В языке программирования C нет классов, но есть struct
, которые выглядят так:
struct StringBuilder {
char *payload;
};
void Append(struct StringBuilder *obj, const char *snippet);
В нём нет модификаторов доступа; все поля в структуре публичны. Именно поэтому кто-то всегда жалуется на отсутствие в C инкапсуляции или изоляции: всё в структуре видно всем и всегда, даже вызывающей стороне функции Append()
.
Реальность
Но эта жалоба не всегда справедлива. Хотя вы конечно можете скинуть всё в один файл исходного кода и на этом закончить, чаще всего код разделяется на разные файлы и модули.
Когда код находится в разных файлах исходного кода, они инкапсулированы в модуль. Каждый «модуль» в C состоит из файла интерфейса, который вызывающие могут использовать для вызова функций, находящихся в файле реализации.
Файл интерфейса, называемый файлом заголовка (с расширением .h
) - это контракт, сообщающий пользователю модуля, какие функции и типы нужны для использования реализации, достаточно часто называемой файлом исходного кода. После компиляции у вас получается скомпилированная реализация, и таким образом вы можете не иметь доступа к исходному коду Append()
.
И… та-да!
Вызывающие программы обязаны использовать файл заголовка для выполнения законных (по стандартам C) вызовов Append()
; в конечном итоге, реализация может быть доступна только в скомпилированном виде, а не в виде исходного кода.
Поэтому в заголовке мы делаем следующее:
typedef struct StringBuilder StringBuilder;
void Append(StringBuilder *obj, const char *snippet);
А в реализации следующее:
struct StringBuilder {
char *payload;
};
void Append(StringBuilder *obj, const char *snippet)
{
...
}
Вот и всё!
Теперь любой код, использующий структуру типа StringBuilder
, сможет использовать всё нужное ему для создания строки, но никогда не увидит строки внутри неё.
Да он даже не сможет выполнить malloc()
для своего собственного экземпляра StringBuilder
, потому что скрыт даже размер StringBuilder
. Ему придётся использовать функции создания и удаления, предоставленные в реализации и указанные в интерфейсе.
Но и это ещё не всё…
Итак, теперь у нас есть возможность создать экземпляр объекта, все поля которого скрыты от любой вызывающей стороны. Одновременно вы запретили всем вызывающим сторонам вмешиваться в поля своего объекта – весь доступ к объекту защищён функциями в реализации (как указано в заголовке).
У нас нет наследования, зато есть одна очень важная характеристика: из этого класса можно создавать объекты и использовать их, даже в Python.
Или в PHP.
И даже в Ruby.
Это будет работать с большинством реализаций Lisp.
Их можно вызывать из Java.
На самом деле, я не думаю, что какой-то из распространённых языков программирования не сможет использовать этот объект. Во многих случаях программисту на этом языке даже не придётся особо трудиться, чтобы использовать этот класс2.
См. swig
В моих Makefiles
уже есть правила автоматической генерации интерфейса, чтобы написанный в этой манере код на C можно было вызывать из приложений для Android.
Ладно, вы всё поняли, пора заканчивать
Вот так можно получить инкапсуляцию и изоляцию в C с надёжными гарантиями. Не верьте всему, что читаете в Интернете3.
Кроме моего блога, конечно. Очевидно, что я образец честности и мудрости, за исключением случаев, когда это не так.