Просто обычно неизменяемые типы, созданные в языках, которые не вращаются вокруг неизменности, будут иметь тенденцию тратить больше времени на разработку, а также потенциально использовать, если им потребуется какой-то объект типа «конструктор» для выражения желаемых изменений (это не означает, что в целом работы будет больше, но в этих случаях есть предоплата). Кроме того, независимо от того, позволяет ли язык действительно легко создавать неизменяемые типы или нет, он будет всегда требовать некоторой обработки и дополнительных затрат памяти для нетривиальных типов данных.
Создание функций, лишенных побочных эффектов
Если вы работаете на языках, которые не вращаются вокруг неизменности, то я думаю, что прагматический подход заключается не в стремлении сделать каждый тип данных неизменным. Потенциально гораздо более продуктивный образ мышления, который дает вам много таких же преимуществ, заключается в том, чтобы сосредоточиться на максимизации количества функций в вашей системе, которые вызывают нулевые побочные эффекты .
В качестве простого примера, если у вас есть функция, которая вызывает побочный эффект, подобный этому:
// Make 'x' the absolute value of itself.
void make_abs(int& x);
Тогда нам не нужен неизменяемый целочисленный тип данных, который запрещает операторам, таким как назначение после инициализации, чтобы эта функция избегала побочных эффектов. Мы можем просто сделать это:
// Returns the absolute value of 'x'.
int abs(int x);
Теперь функция не связывается с x
чем-либо и выходит за ее пределы, и в этом тривиальном случае мы могли бы даже сократить некоторые циклы, избегая любых издержек, связанных с косвенным обращением / псевдонимами. По крайней мере, вторая версия не должна быть дороже в вычислительном отношении, чем первая.
Вещи, которые дорого копировать в полном объеме
Конечно, в большинстве случаев это не так тривиально, если мы хотим, чтобы функция не вызывала побочных эффектов. Сложный реальный вариант использования может быть больше похож на это:
// Transforms the vertices of the specified mesh by
// the specified transformation matrix.
void transform(Mesh& mesh, Matrix4f matrix);
В этот момент сетке может потребоваться пара сотен мегабайт памяти с более чем сотней тысяч полигонов, еще больше вершин и ребер, множественные карты текстур, морфинговые цели и т. Д. Было бы действительно дорого скопировать всю эту сетку, просто чтобы сделать это transform
функция без побочных эффектов, например так:
// Returns a new version of the mesh whose vertices been
// transformed by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);
И именно в этих случаях копирование чего-либо во всей полноте, как правило, было бы эпической затратами, когда я нашел полезным превратить Mesh
в постоянную структуру данных и неизменный тип с аналогичным «строителем» для создания модифицированных версий этого, чтобы может просто поверхностное копирование и подсчет ссылок, которые не являются уникальными. Все дело в том, чтобы писать функции меша без побочных эффектов.
Постоянные структуры данных
И в тех случаях, когда копирование всего настолько невероятно дорого, я обнаружил, что попытка создать неизменяемый объект Mesh
действительно окупится, даже если он стоил немного дороже, потому что он не просто упростил безопасность потоков. Это также упростило неразрушающее редактирование (позволяя пользователю наслоить операции с сеткой без изменения его оригинальной копии), отменить системы (теперь система отмены может просто сохранять неизменную копию сетки до изменений, сделанных операцией, не разрушая память use) и безопасность исключений (теперь, если исключение возникает в вышеупомянутой функции, функция не должна откатываться и отменять все свои побочные эффекты, поскольку она не вызывала их с самого начала).
В этих случаях я могу с уверенностью сказать, что время, необходимое для того, чтобы сделать эти здоровенные структуры данных неизменяемыми, сэкономило больше времени, чем стоило, так как я сравнил затраты на обслуживание этих новых конструкций с прежними, которые вращались вокруг изменчивости и функций, вызывающих побочные эффекты, и прежние изменяемые конструкции стоили гораздо больше времени и были гораздо более подвержены человеческим ошибкам, особенно в тех областях, где разработчикам действительно хочется пренебречь во время кризиса, например, безопасность исключений.
Поэтому я считаю, что неизменяемые типы данных действительно оправдывают себя в этих случаях, но не все должно быть сделано неизменным, чтобы большинство функций в вашей системе было лишено побочных эффектов. Многие вещи достаточно дешевы, чтобы просто скопировать их полностью. Также многие приложения реального мира должны вызывать некоторые побочные эффекты здесь и там (по крайней мере, как сохранение файла), но обычно существует гораздо больше функций, которые могут быть лишены побочных эффектов.
Смысл в том, чтобы иметь несколько неизменных типов данных для меня, чтобы убедиться, что мы можем написать максимальное количество функций, чтобы избежать побочных эффектов, не неся эпических издержек в виде глубокого копирования массивных структур данных влево и вправо полностью, когда только небольшими порциями из них должны быть изменены. Наличие постоянных структур данных в этих случаях в конечном итоге превращается в деталь оптимизации, позволяющую нам писать наши функции без побочных эффектов, не затрачивая при этом огромных затрат.
Неизменные накладные расходы
Теперь концептуально изменяемые версии всегда будут иметь преимущество в эффективности. Всегда есть вычислительные затраты, связанные с неизменяемыми структурами данных. Но я нашел это достойным обменом в случаях, которые я описал выше, и вы можете сосредоточиться на том, чтобы сделать накладные расходы достаточно минимальными по своей природе. Я предпочитаю такой подход, когда правильность становится проще, а оптимизация становится сложнее, чем оптимизация легче, а правильность становится сложнее. Это не так уж и деморализующе - иметь код, который функционирует совершенно корректно и нуждается в дополнительных настройках кода, который в первую очередь работает некорректно, независимо от того, насколько быстро он достигает своих неверных результатов.