Які недоліки незмінних типів?


12

Я бачу себе в застосуванні все більше непорушних типів, коли екземпляри класу не будуть змінені . Це вимагає більше роботи (див. Приклад нижче), але полегшує використання типів у багатопотоковому середовищі.

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

Питання: Чому незмінні типи так рідко використовуються в інших програмах?

  • Це тому, що довше писати код для непорушного типу,
  • Або я щось пропускаю, і є якісь важливі недоліки при використанні незмінних типів?

Приклад з реального життя

Скажімо, ви отримуєте Weatherтакий API RESTful:

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; }
}

З іншого боку, я б написав це таким чином, враховуючи, що отримання WeatherAPI-інтерфейсу, а потім його зміна було б дивним: змінити 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 для незмінного.
Арсеній Муренко

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

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

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

Відповіді:


16

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

  1. Зусилля щодо впровадження порівняно з типами, що змінюються. З незмінним типом вам потрібно мати конструктор, який вимагає аргументів для всіх властивостей. Ваш приклад хороший. Спробуйте уявити, що у вас є 10 класів, кожен з яких має 5-10 властивостей. Щоб зробити речі простіше, вам , можливо , доведеться мати клас будівельника побудувати або створити модифіковані незмінні примірники у манері , аналогічної StringBuilderабо UriBuilderв C #, або WeatherBuilderв вашому випадку. Це головна причина для мене, оскільки багато класів, які я проектую, не варті таких зусиль.
  2. Споживча зручність . Незмінний тип важче використовувати порівняно з мутаційним типом. Моменталізація вимагає ініціалізації всіх значень. Незмінюваність також означає, що ми не можемо передати екземпляр методу, щоб змінити його значення без використання конструктора, і якщо нам потрібно мати конструктор, то недолік є в моєму (1).
  3. Сумісність з мовними рамками. У багатьох структурах даних для роботи потрібні типи змінних та конструктори за замовчуванням. Наприклад, ви не можете робити вкладений LINQ-в-SQL запит із незмінними типами, і ви не можете прив’язувати властивості до редагування в таких редакторах, як TextBox Windows Forms.

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


11
"Екземпляру потрібні всі значення заздалегідь.": Так само змінюється тип, якщо ви не приймаєте ризик появи об'єкта з неініціалізованими полями, що пливуть навколо ...
Giorgio

@Giorgio Для змінного типу конструктор за замовчуванням повинен ініціалізувати екземпляр до стану за замовчуванням, а стан екземпляра може бути змінено пізніше після інстанції.
tia

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

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

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

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

Незмінний накладний

Тепер концептуально модифіковані версії завжди матимуть перевагу в ефективності. Завжди є обчислювальні витрати, пов'язані з незмінними структурами даних. Але я вважав його гідним обміном у вищеописаних випадках, і ви можете зосередитися на тому, щоб накладні витрати були настільки мінімальними. Я віддаю перевагу такому типу підходу, коли правильність стає легкою та оптимізація стає складнішою, ніж оптимізація стає легшою, але правильність стає жорсткішою. Мало того, що деморалізувати наявність коду, який функціонує ідеально правильно, потребуючи ще декількох налаштувань над кодом, який функціонує не в першу чергу, незалежно від того, наскільки швидко він досягає невірних результатів.


3

Єдиний недолік, про який я можу подумати, - це те, що теоретично використання змінних даних може бути повільнішим за змінні - повільніше створювати новий екземпляр та збирати попередній, ніж змінювати існуючий.

Інша "проблема" полягає в тому, що ви не можете використовувати лише незмінні типи. Зрештою, ви повинні описати стан, і для цього вам доведеться використовувати зміни, що змінюються - без зміни стану ви не можете виконати жодної роботи.

Але все-таки загальним правилом є використання незмінних типів, де можна, і типи змінюються лише тоді, коли дійсно є причина для цього ...

І щоб відповісти на запитання " Чому незмінні типи так рідко використовуються в інших програмах? " - Я дійсно не думаю, що вони є ... де б ви не подивилися, кожен рекомендує зробити свої заняття настільки непорушними, наскільки вони можуть бути ... наприклад: http://www.javapractices.com/topic/TopicAction.do?Id=29


1
Обидві ваші проблеми не в Хаскеллі.
Флоріан Маргайн

@FlorianMargaine Не могли б ви детальніше розробитись?
mrpyo

Повільність не відповідає дійсності завдяки розумному компілятору. І в Haskell навіть введення / виведення здійснюється через незмінний API.
Флоріан Маргайн

2
Більш фундаментальна проблема, ніж швидкість, полягає в тому, що незмінним об'єктам важко зберігати ідентичність, тоді як їх стан змінюється. Якщо Carпредмет, що змінюється , постійно оновлюється з місцезнаходженням певного фізичного автомобіля, то, якщо у мене є посилання на цей об’єкт, я можу швидко та легко дізнатися, де знаходиться цей автомобіль. Якби вони Carбули непорушними, знайти поточне місцеперебування було б набагато важче.
supercat

Іноді вам доводиться досить чітко кодувати, щоб компілятор зрозумів, що немає посилання на попередній об’єкт, залишений позаду, і, таким чином, може змінювати його на місці, або робити перетворення вирубки лісів тощо. Особливо у великих програмах. І як каже @supercat, ідентичність справді може стати проблемою.
Макке

0

Для моделювання будь-якої системи реального світу, в якій все може змінитись, стан, що змінюється, потрібно буде десь закодувати. Існує три основні способи, яким об’єкт може утримувати стан, що змінюється:

  • Використання змінної посилання на незмінний об'єкт
  • Використання незмінної посилання на предмет, що змінюється
  • Використання змінної посилання на об'єкт, що змінюється

Використання першого полегшує об'єкт зробити незмінний знімок сучасного стану. Використання другого полегшує об'єкту створення прямого перегляду поточного стану. Використання третього може іноді зробити певні дії більш ефективними у випадках, коли мало очікувана потреба у незмінних знімках, ані в режимі реального часу.

Крім того, що оновлення стану, що зберігається за допомогою змінної посилання на незмінний об'єкт, часто відбувається повільніше, ніж оновлення стану, що зберігається за допомогою змінного об'єкта, використання змінної посилання вимагає відмовитись від можливості побудови дешевого режиму перегляду стану. Якщо вам не потрібно буде створювати перегляд у прямому ефірі, це не проблема; якщо, однак, потрібно створити перегляд у прямому ефірі, неможливість використання незмінної посилання змусить усі операції з представленням - і читати, і записувати- набагато повільніше, ніж вони були б інакше. Якщо потреба у незмінних знімках перевищує потребу в режимах прямого перегляду, покращена продуктивність незмінних знімків може виправдати хіт продуктивності для перегляду в прямому ефірі, але якщо потрібні перегляди в реальному часі і не потрібні знімки, спосіб використання незмінних посилань на змінні об'єкти йти.


0

У вашому випадку відповідь головним чином тому, що C # погано підтримує незмінність ...

Було б чудово, якби:

  • все буде незмінним за замовчуванням, якщо не зазначено інше (тобто із ключовим словом 'mutable'), змішування типів, що змінюються та змінюються, є заплутаним

  • мутує методи ( With) будуть автоматично доступні - хоча це вже може бути досягнуто см З

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

  • І головним чином, якщо компілятор міг максимально забезпечити незмінність, де це вимагається (див. Https://github.com/dotnet/roslyn/isissue/159 )


1
По-третє, у ReSharper є MustUseReturnValueAttributeспеціальний атрибут, який робить саме це. PureAttributeмає такий же ефект і для цього навіть краще.
Себастьян Редл

-1

Чому незмінні типи так рідко використовуються в інших програмах?

Невігластво? Недосвідченість?

Незмінні об'єкти сьогодні вважаються вищими, але це відносно недавній розвиток. Інженери, які не були в курсі, або просто застрягли в тому, що вони знають, не використовуватимуть їх. І для їх ефективного використання потрібні невеликі зміни в дизайні. Якщо програми старі або інженери слабкі в дизайнерських навичках, то користування ними може бути незручним або іншим чином складно.


"Інженери, які не були в курсі": Можна сказати, що інженер також повинен дізнатися про непрофільні технології. Ідея незмінності лише останнім часом стає мейнстрімом, але це досить стара ідея і підтримується (якщо не застосовується) більш старими мовами, такими як Scheme, SML, Haskell. Тож кожен, хто звик дивитись за межі основних мов, міг дізнатися про це ще 30 років тому.
Джорджіо

@Giorgio: у деяких країнах багато інженерів все ще пишуть код C # без LINQ, без FP, без ітераторів і без дженериків, так що насправді вони якось пропустили все, що сталося з C # з 2003 року. Якщо вони навіть не знають своєї мови уподобань , Я навряд чи думаю, щоб вони знали будь-яку непрофільну мову.
Арсеній Муренко

@MainMa: Добре, що ви написали слово інженери курсивом.
Джорджіо

@Giorgio: у моїй країні їх також називають архітекторами , консультантами та багатьма іншими доблесними умовами, ніколи не написаними курсивом. У компанії, в якій я зараз працюю, мене називають аналітиком-аналітиком , і, як очікується, я витрачу свій час на написання CSS для шаленого застарілого HTML-коду. Назви роботи турбують на стільки рівнях.
Арсеній Муренко

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