Итак, в какой момент класс становится слишком сложным, чтобы быть неизменным?
На мой взгляд, не стоит беспокоиться о том, чтобы сделать небольшие классы неизменяемыми в языках, подобных тому, который вы показываете. Я использую маленький здесь и не сложный , потому что даже если вы добавите десять полей к этому классу, и он действительно над ними работает, я сомневаюсь, что это займет килобайты, не говоря уже о мегабайтах, не говоря уже о гигабайтах, так что любая функция, использующая экземпляры вашего Класс может просто сделать дешевую копию всего объекта, чтобы избежать изменения оригинала, если он хочет избежать внешних побочных эффектов.
Постоянные структуры данных
Где я нахожу личное использование для неизменности, так это для больших центральных структур данных, которые объединяют кучу маленьких данных, таких как экземпляры класса, который вы показываете, например, тот, который хранит миллион NamedThings
. Принадлежность к постоянной структуре данных, которая является неизменной, и находящаяся за интерфейсом, который разрешает только доступ только для чтения, элементы, которые принадлежат контейнеру, становятся неизменяемыми, и элементу class ( NamedThing
) не приходится иметь дело с ним.
Дешевые копии
Постоянная структура данных позволяет преобразовывать ее области и делать их уникальными, избегая модификаций в оригинале без необходимости полностью копировать структуру данных. Это настоящая красота этого. Если вы хотите наивно писать функции, которые избегают побочных эффектов, которые вводят структуру данных, которая занимает гигабайты памяти и изменяет только объем памяти в мегабайтах, то вам нужно было бы скопировать всю чертову вещь, чтобы не касаться ввода, и вернуть новый выход. Это либо копирование гигабайт, чтобы избежать побочных эффектов, либо побочные эффекты в этом сценарии, поэтому вам придется выбирать между двумя неприятными вариантами.
Благодаря постоянной структуре данных это позволяет вам написать такую функцию и избежать копирования всей структуры данных, требуя только около мегабайта дополнительной памяти для вывода, если ваша функция только преобразовала объем памяти в мегабайтах.
обременять
Что касается бремени, то, по крайней мере, в моем случае, есть непосредственное. Мне нужны те конструкторы, о которых говорят люди, или «переходные процессы», как я их называю, чтобы они могли эффективно выражать преобразования в эту массивную структуру данных, не касаясь ее. Код как это:
void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
// Transform stuff in the range, [first, last).
for (; first != last; ++first)
transform(stuff[first]);
}
... тогда должно быть написано так:
ImmList<Stuff> transform_stuff(ImmList<Stuff> stuff, int first, int last)
{
// Grab a "transient" (builder) list we can modify:
TransientList<Stuff> transient(stuff);
// Transform stuff in the range, [first, last)
// for the transient list.
for (; first != last; ++first)
transform(transient[first]);
// Commit the modifications to get and return a new
// immutable list.
return stuff.commit(transient);
}
Но в обмен на эти две дополнительные строки кода функцию теперь можно безопасно вызывать через потоки с одним и тем же исходным списком, она не вызывает побочных эффектов и т. Д. Кроме того, это действительно позволяет легко сделать эту операцию отменяемым действием пользователя, поскольку отменить можно просто хранить дешевую мелкую копию старого списка.
Исключение-Безопасность или Восстановление после ошибок
Не все могут получить такую же выгоду, как я, от постоянных структур данных в подобных контекстах (я нашел их столь полезными в системах отмены и неразрушающем редактировании, которые являются центральными понятиями в моей области VFX), но одна вещь применима только к каждый должен учитывать исключительную безопасность или исправление ошибок .
Если вы хотите сделать исходную функцию мутации исключительной, то для нее требуется логика отката, для которой простейшая реализация требует копирования всего списка:
void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
// Make a copy of the whole massive gigabyte-sized list
// in case we encounter an exception and need to rollback
// changes.
MutList<Stuff> old_stuff = stuff;
try
{
// Transform stuff in the range, [first, last).
for (; first != last; ++first)
transform(stuff[first]);
}
catch (...)
{
// If the operation failed and ran into an exception,
// swap the original list with the one we modified
// to "undo" our changes.
stuff.swap(old_stuff);
throw;
}
}
На этом этапе изменяемая на исключение изменяемая версия еще более затратна в вычислительном отношении и, возможно, даже сложнее написать правильно, чем неизменяемая версия, использующая «конструктор». И многие разработчики на C ++ просто пренебрегают безопасностью исключений и, возможно, это хорошо для их домена, но в моем случае я хотел бы убедиться, что мой код работает правильно даже в случае исключения (даже при написании тестов, которые намеренно генерируют исключения для проверки исключений). безопасность), и это делает меня таким, чтобы я был в состоянии откатить любые побочные эффекты, которые функция вызывает на полпути в функцию, если что-то выбрасывает.
Если вы хотите быть безопасными для исключений и корректно восстанавливаться после ошибок без сбоев и прожогов вашего приложения, тогда вам нужно отменить / отменить любые побочные эффекты, которые может вызвать функция в случае ошибки / исключения. И там конструктор может фактически сэкономить больше времени программиста, чем это стоит вместе с вычислительным временем, потому что: ...
Вам не нужно беспокоиться об откате побочных эффектов в функции, которая не вызывает их!
Итак, вернемся к основному вопросу:
В какой момент неизменные классы становятся бременем?
Они всегда обременительны для языков, которые вращаются вокруг изменчивости, а не неизменности, поэтому я думаю, что вы должны использовать их там, где выгоды значительно перевешивают затраты. Но на достаточно широком уровне для достаточно больших структур данных, я верю, что во многих случаях это достойный компромисс.
Кроме того, у меня есть только несколько неизменных типов данных, и все они представляют собой огромные структуры данных, предназначенные для хранения огромного количества элементов (пикселей изображения / текстуры, объектов и компонентов ECS, а также вершин / ребер / многоугольников). сетка).