Каковы недостатки неизменяемых типов?


12

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

В то же время я редко вижу неизменяемые типы в других приложениях, даже если изменчивость никому не пойдет на пользу.

Вопрос: почему неизменяемые типы так редко используются в других приложениях?

  • Это потому, что писать код для неизменяемого типа длиннее,
  • Или я что-то упустил и есть некоторые важные недостатки при использовании неизменяемых типов?

Пример из реальной жизни

Допустим, вы получаете Weatherот RESTful API вот так:

public Weather FindWeather(string city)
{
    // TODO: Load the JSON response from the RESTful API and translate it into an instance
    // of the Weather class.
}

Как правило, мы видим (новые строки и комментарии удалены для сокращения кода):

public sealed class Weather
{
    public City CorrespondingCity { get; set; }
    public SkyState Sky { get; set; } // Example: SkyState.Clouds, SkyState.HeavySnow, etc.
    public int PrecipitationRisk { get; set; }
    public int Temperature { get; set; }
}

С другой стороны, я бы написал это так, учитывая, что получать Weatherот API, а затем модифицировать его было бы странно: менять Temperatureили Skyне менять погоду в реальном мире, и изменение CorrespondingCityне имеет смысла.

public sealed class Weather
{
    private readonly City correspondingCity;
    private readonly SkyState sky;
    private readonly int precipitationRisk;
    private readonly int temperature;

    public Weather(City correspondingCity, SkyState sky, int precipitationRisk,
        int temperature)
    {
        this.correspondingCity = correspondingCity;
        this.sky = sky;
        this.precipitationRisk = precipitationRisk;
        this.temperature = temperature;
    }

    public City CorrespondingCity { get { return this.correspondingCity; } }
    public SkyState Sky { get { return this.sky; } }
    public int PrecipitationRisk { get { return this.precipitationRisk; } }
    public int Temperature { get { return this.temperature; } }
}

3
«Требуется больше работы» - цитирование требуется. По моему опыту, это требует меньше работы.
Конрад Рудольф

1
@KonradRudolph: под большей работой я имею в виду больше кода для написания неизменного класса. Пример из моего вопроса иллюстрирует это: 7 строк для изменяемого класса и 19 для неизменяемого.
Арсений Мурзенко

Вы можете уменьшить количество типов кода, используя функцию фрагментов кода в Visual Studio, если вы ее используете. Вы можете создавать свои собственные фрагменты и позволить IDE одновременно определять поле и свойство с помощью нескольких ключей. Неизменяемые типы необходимы для многопоточности и широко используются в таких языках, как Scala.
Мерт Акчакая

@Mert: фрагменты кода отлично подходят для простых вещей. Написание фрагмента кода, который создаст полный класс с комментариями полей и свойств и правильным порядком, не будет легкой задачей.
Арсений Мурзенко

5
Я не согласен с приведенным примером, неизменная версия делает больше и разные вещи. Вы можете удалить переменные уровня экземпляра, объявив свойства с помощью методов доступа {get; private set;}, и даже изменяемый должен иметь конструктор, потому что все эти поля должны быть всегда установлены, и почему бы вам не применять это? Внесение этих двух совершенно разумных изменений приводит к их характеристике и паритету LoC.
Фоши

Ответы:


16

Я программирую на C # и Objective-C. Мне действительно нравится неизменяемая типизация, но в реальной жизни мне всегда приходилось ограничивать ее использование, главным образом для типов данных, по следующим причинам:

  1. Реализация усилий по сравнению с изменчивыми типами. В случае неизменяемого типа вам потребуется конструктор, требующий аргументы для всех свойств. Ваш пример хороший. Попробуйте представить, что у вас есть 10 классов, каждый из которых имеет 5-10 свойств. Чтобы упростить задачу, вам может потребоваться класс построителя для конструирования или создания измененных неизменяемых экземпляров способом, аналогичным StringBuilderили UriBuilderв C #, или WeatherBuilderв вашем случае. Это главная причина для меня, так как многие классы, которые я проектирую, не стоят таких усилий.
  2. Удобство использования . Неизменяемый тип сложнее использовать по сравнению с изменяемым типом. Инстанциация требует инициализации всех значений. Неизменность также означает, что мы не можем передать экземпляр методу для изменения его значения без использования компоновщика, и если нам нужен компоновщик, то недостатком является my (1).
  3. Совместимость с языковой структурой. Для работы многих структур данных требуются изменяемые типы и конструкторы по умолчанию. Например, вы не можете выполнять вложенный запрос LINQ-to-SQL с неизменяемыми типами и не можете связывать свойства, которые будут редактироваться в редакторах, таких как TextBox в Windows Forms.

Короче говоря, неизменность хороша для объектов, которые ведут себя как значения или имеют только несколько свойств. Прежде чем сделать что-либо неизменным, вы должны подумать о необходимых усилиях и удобстве использования самого класса после того, как сделаете его неизменным.


11
«Инстанциации нужны все значения заранее.»: Также изменяемый тип делает, если вы не принимаете на себя риск наличия объекта с неинициализированными полями, плавающими вокруг ...
Джорджио

@Giorgio Для изменяемого типа конструктор по умолчанию должен инициализировать экземпляр в состояние по умолчанию, и состояние экземпляра может быть изменено позже после создания экземпляра.
ТИА

8
Для неизменяемого типа вы можете иметь тот же конструктор по умолчанию и сделать копию позже, используя другой конструктор. Если значения по умолчанию действительны для изменяемого типа, они должны быть действительными и для неизменяемого типа, потому что в обоих случаях вы моделируете одну и ту же сущность. Или какая разница?
Джорджио

1
Еще одна вещь, которую следует учитывать, - это то, что представляет тип. Контракты на данные не подходят для хороших неизменяемых типов из-за всех этих моментов, но типы служб, которые инициализируются с зависимостями или данными только для чтения, а затем выполняют операции, хороши для неизменности, поскольку операции будут выполняться согласованно, а состояние службы не может быть изменено. изменил рискнуть этим.
Кевин

1
В настоящее время я пишу код на F #, где неизменяемость является значением по умолчанию (следовательно, проще в реализации). Я считаю, что ваша точка 3 является большим препятствием. Как только вы используете большинство стандартных библиотек .Net, вам нужно будет прыгать через обручи. (И если они используют отражение и, таким образом, обходят неизменность ... аааа!)
Гуран

5

Просто обычно неизменяемые типы, созданные в языках, которые не вращаются вокруг неизменности, будут иметь тенденцию тратить больше времени на разработку, а также потенциально использовать, если им потребуется какой-то объект типа «конструктор» для выражения желаемых изменений (это не означает, что в целом работы будет больше, но в этих случаях есть предоплата). Кроме того, независимо от того, позволяет ли язык действительно легко создавать неизменяемые типы или нет, он будет всегда требовать некоторой обработки и дополнительных затрат памяти для нетривиальных типов данных.

Создание функций, лишенных побочных эффектов

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

В качестве простого примера, если у вас есть функция, которая вызывает побочный эффект, подобный этому:

// 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) и безопасность исключений (теперь, если исключение возникает в вышеупомянутой функции, функция не должна откатываться и отменять все свои побочные эффекты, поскольку она не вызывала их с самого начала).

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

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

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

Неизменные накладные расходы

Теперь концептуально изменяемые версии всегда будут иметь преимущество в эффективности. Всегда есть вычислительные затраты, связанные с неизменяемыми структурами данных. Но я нашел это достойным обменом в случаях, которые я описал выше, и вы можете сосредоточиться на том, чтобы сделать накладные расходы достаточно минимальными по своей природе. Я предпочитаю такой подход, когда правильность становится проще, а оптимизация становится сложнее, чем оптимизация легче, а правильность становится сложнее. Это не так уж и деморализующе - иметь код, который функционирует совершенно корректно и нуждается в дополнительных настройках кода, который в первую очередь работает некорректно, независимо от того, насколько быстро он достигает своих неверных результатов.


3

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

Другая «проблема» заключается в том, что вы не можете использовать только неизменяемые типы. В конце вы должны описать состояние и использовать изменяемые типы для этого - без изменения состояния вы не сможете выполнять какую-либо работу.

Но все же общее правило заключается в том, чтобы использовать неизменяемые типы везде, где это возможно, и делать типы изменяемыми только тогда, когда для этого действительно есть причина ...

И чтобы ответить на вопрос « Почему неизменяемые типы так редко используются в других приложениях? » - я действительно не думаю, что они… куда бы вы ни посмотрели, все рекомендуют сделать ваши классы настолько неизменными, насколько это возможно… например: http://www.javapractices.com/topic/TopicAction.do?Id=29


1
Обе ваши проблемы не в Хаскеле, хотя.
Флориан Маргейн

@FlorianMargaine Не могли бы вы уточнить?
mrpyo

Медлительность не верна благодаря умному компилятору. А в Haskell даже ввод / вывод осуществляется через неизменный API.
Флориан Маргэйн

2
Более фундаментальная проблема, чем скорость, заключается в том, что неизменным объектам трудно сохранять идентичность, пока их состояние изменяется. Если изменчивый Carобъект постоянно обновляется с указанием местоположения конкретного физического автомобиля, то, если у меня есть ссылка на этот объект, я могу быстро и легко узнать местонахождение этого автомобиля. Если бы они Carбыли неизменными, найти текущее местонахождение было бы гораздо сложнее.
суперкат

Иногда приходится довольно умно писать код, чтобы компилятор выяснил, что нет ссылки на предыдущий оставленный объект, и, следовательно, может изменить его на месте или выполнить преобразования обезлесения, и др. Особенно в больших программах. И, как говорит @supercat, личность действительно может стать проблемой.
Маке

0

Чтобы смоделировать любую реальную систему, в которой вещи могут меняться, изменяемое состояние нужно будет где-то каким-то образом кодировать. Существует три основных способа, которыми объект может содержать изменяемое состояние:

  • Использование изменяемой ссылки на неизменный объект
  • Использование неизменяемой ссылки на изменяемый объект
  • Использование изменяемой ссылки на изменяемый объект

Использование first облегчает объекту создание неизменного снимка текущего состояния. Использование второго облегчает объекту создание живого представления текущего состояния. Использование третьего может иногда сделать некоторые действия более эффективными в тех случаях, когда нет ожидаемой потребности в неизменяемых снимках или живых представлениях.

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


0

В вашем случае ответ в основном потому, что C # плохо поддерживает неизменяемость ...

Было бы здорово, если бы:

  • все будет неизменным по умолчанию, если не указано иное (например, с ключевым словом 'mutable'), смешивание неизменяемых и изменяемых типов сбивает с толку

  • методы мутации ( With) будут автоматически доступны - хотя это уже может быть достигнуто, см. с

  • будет способ сказать, что результат определенного вызова метода (то есть ImmutableList<T>.Add) не может быть отброшен или, по крайней мере, выдаст предупреждение

  • И главным образом, если компилятор мог максимально обеспечить неизменность там, где это требуется (см. Https://github.com/dotnet/roslyn/issues/159 )


1
MustUseReturnValueAttributeЧто касается третьего пункта, у ReSharper есть собственный атрибут, который делает именно это. PureAttributeимеет тот же эффект и даже лучше для этого.
Себастьян Редл

-1

Почему неизменяемые типы так редко используются в других приложениях?

Невежество? Неопытность?

Неизменные объекты сегодня широко рассматриваются как превосходные, но это относительно недавняя разработка. Инженеры, которые не идут в ногу со временем или просто застряли в «том, что они знают», не будут их использовать. И для их эффективного использования требуются небольшие изменения в дизайне. Если приложения старые или у инженеров слабые навыки проектирования, их использование может быть неудобным или иным образом хлопотным.


«Инженеры, которые не идут в ногу со временем»: можно сказать, что инженер должен также узнать о неосновных технологиях. Идея неизменности только недавно стала господствующей, но это довольно старая идея, которая поддерживается (если не применяется) более старыми языками, такими как Scheme, SML, Haskell. Так что любой, кто привык выходить за рамки основных языков, мог узнать об этом даже 30 лет назад.
Джорджио

@ Джорджио: в некоторых странах многие инженеры до сих пор пишут код на C # без LINQ, без FP, без итераторов и без шаблонов, поэтому на самом деле они почему-то упустили все, что происходило с C # с 2003 года. Если они даже не знают своего предпочтительного языка Я не представляю, чтобы они знали какой-либо неосновной язык.
Арсений Мурзенко

@MainMa: Хорошо, что вы написали слово « инженеры» курсивом.
Джорджио

@ Джорджио: в моей стране их также называют архитекторами , консультантами и множеством других бессмысленных терминов, никогда не написанных курсивом. В компании, в которой я сейчас работаю, меня называют разработчиком-аналитиком , и я ожидаю, что потрачу свое время на написание CSS для дрянного унаследованного HTML-кода. Названия должностей вызывают беспокойство на многих уровнях.
Арсений Мурзенко

1
@MainMa: я согласен. Названия, такие как инженер или консультант, часто являются просто модными словами без общепринятого значения. Они часто используются, чтобы сделать кого-то или их положение более важным / престижным, чем оно есть на самом деле.
Джорджио
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.