Quels sont les inconvénients des types immuables?

12

Je me vois utiliser de plus en plus de types immuables lorsque les instances de la classe ne devraient pas être modifiées . Il nécessite plus de travail (voir l'exemple ci-dessous), mais facilite l'utilisation des types dans un environnement multithread.

Dans le même temps, je vois rarement des types immuables dans d'autres applications, même lorsque la mutabilité ne profiterait à personne.

Question: Pourquoi les types immuables sont-ils si rarement utilisés dans d'autres applications?

  • Est-ce parce qu'il est plus long d'écrire du code pour un type immuable,
  • Ou est-ce que je manque quelque chose et qu'il y a des inconvénients importants lors de l'utilisation de types immuables?

Exemple de la vie réelle

Disons que vous obtenez à Weatherpartir d'une API RESTful comme ça:

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

Ce que nous verrions généralement est (nouvelles lignes et commentaires supprimés pour raccourcir le code):

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

D'un autre côté, je l'écrirais de cette façon, étant donné qu'obtenir un Weatherde l'API, puis le modifier serait bizarre: changer Temperatureou Skyne changerait pas la météo dans le monde réel, et changer CorrespondingCityn'a pas de sens non plus.

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; } }
}
Arseni Mourzenko
la source
3
«Cela demande plus de travail» - citation requise. D'après mon expérience, cela nécessite moins de travail.
Konrad Rudolph
1
@KonradRudolph: par plus de travail , je veux dire plus de code à écrire pour créer une classe immuable. L'exemple de ma question illustre cela, avec 7 lignes pour une classe mutable et 19 pour une classe immuable.
Arseni Mourzenko
Vous pouvez réduire la saisie de code en utilisant la fonctionnalité d'extraits de code dans Visual Studio si vous l'utilisez. Vous pouvez créer vos extraits personnalisés et laisser l'IDE vous définir le champ et la propriété en même temps avec quelques clés. Les types immuables sont essentiels pour le multithreading et largement utilisés dans des langages comme Scala.
Mert Akcakaya
@Mert: Les extraits de code sont parfaits pour les choses simples. Écrire un extrait de code qui créera une classe complète avec des commentaires de champs et de propriétés et un ordre correct ne serait pas une tâche facile.
Arseni Mourzenko
5
Je ne suis pas d'accord avec l'exemple donné, la version immuable fait plus de choses différentes. Vous pouvez supprimer les variables au niveau de l'instance en déclarant des propriétés avec des accesseurs {get; private set;}, et même la variable mutable devrait avoir un constructeur, car tous ces champs doivent toujours être définis et pourquoi ne pas appliquer cela? Apporter ces deux changements parfaitement raisonnables les amène à la fonctionnalité et à la parité LoC.
Phoshi

Réponses:

16

Je programme en C # et Objective-C. J'aime vraiment la frappe immuable, mais dans la vraie vie, j'ai toujours été contraint de limiter son utilisation, principalement pour les types de données, pour les raisons suivantes:

  1. Effort de mise en œuvre comparé aux types mutables. Avec un type immuable, vous auriez besoin d'un constructeur nécessitant des arguments pour toutes les propriétés. Votre exemple est bon. Essayez d'imaginer que vous avez 10 classes, chacune ayant 5 à 10 propriétés. Pour faciliter les choses, vous devrez peut-être avoir une classe de générateur pour construire ou créer des instances immuables modifiées d'une manière similaire StringBuilderou UriBuilderen C #, ou WeatherBuilderdans votre cas. C'est la principale raison pour laquelle je conçois de nombreuses classes ne valent pas un tel effort.
  2. Convivialité des consommateurs . Un type immuable est plus difficile à utiliser que le type mutable. L'instanciation nécessite d'initialiser toutes les valeurs. L'immutabilité signifie également que nous ne pouvons pas passer l'instance à une méthode pour modifier sa valeur sans utiliser de générateur, et si nous avons besoin d'un générateur, l'inconvénient est dans my (1).
  3. Compatibilité avec le framework du langage. De nombreuses infrastructures de données nécessitent des types mutables et des constructeurs par défaut pour fonctionner. Par exemple, vous ne pouvez pas effectuer de requête LINQ-to-SQL imbriquée avec des types immuables et vous ne pouvez pas lier des propriétés à modifier dans des éditeurs tels que TextBox de Windows Forms.

En bref, l'immuabilité est bonne pour les objets qui se comportent comme des valeurs ou qui n'ont que quelques propriétés. Avant de rendre immuable quelque chose, vous devez considérer l'effort nécessaire et l'utilisabilité de la classe elle-même après l'avoir rendue immuable.

tia
la source
11
"L'instanciation a besoin de toutes les valeurs à l'avance.": Un type mutable aussi, sauf si vous acceptez le risque d'avoir un objet avec des champs non initialisés flottant autour ...
Giorgio
@Giorgio Pour le type mutable, le constructeur par défaut doit initialiser l'instance à l'état par défaut et l'état de l'instance peut être modifié plus tard après l'instanciation.
tia
8
Pour un type immuable, vous pouvez avoir le même constructeur par défaut et effectuer une copie ultérieurement à l'aide d'un autre constructeur. Si les valeurs par défaut sont valides pour le type mutable, elles doivent également l'être pour le type immuable car dans les deux cas, vous modélisez la même entité. Ou quelle est la différence?
Giorgio
1
Une autre chose à considérer est ce que le type représente. Les contrats de données ne font pas de bons types immuables à cause de tous ces points, mais les types de service qui sont initialisés avec des dépendances ou des données en lecture seule puis effectuent des opérations sont parfaits pour l'immuabilité car les opérations s'exécuteront de manière cohérente et l'état du service ne peut pas être changé pour risquer cela.
Kevin
1
Je code actuellement en F #, où l'immuabilité est la valeur par défaut (donc plus facile à implémenter). Je trouve que votre point 3 est le gros obstacle. Dès que vous utilisez la plupart des bibliothèques .Net standard, vous devrez sauter à travers les cerceaux. (Et s'ils utilisent la réflexion et contournent ainsi l'immuabilité ... argh!)
Guran
5

Juste des types généralement immuables créés dans des langages qui ne tournent pas autour de l'immuabilité auront tendance à coûter plus de temps au développeur pour créer ainsi que potentiellement utiliser s'ils nécessitent un type d'objet "constructeur" pour exprimer les changements souhaités (cela ne signifie pas que l'ensemble le travail sera plus, mais il y a un coût initial dans ces cas). De plus, que le langage facilite ou non la création de types immuables, il aura tendance à toujours nécessiter un certain traitement et une surcharge de mémoire pour les types de données non triviaux.

Rendre les fonctions dépourvues d'effets secondaires

Si vous travaillez dans des langages qui ne tournent pas autour de l'immuabilité, je pense que l'approche pragmatique n'est pas de chercher à rendre immuable chaque type de données. Un état d'esprit potentiellement beaucoup plus productif qui vous offre plusieurs des mêmes avantages consiste à se concentrer sur la maximisation du nombre de fonctions dans votre système qui ne provoquent aucun effet secondaire .

À titre d'exemple simple, si vous avez une fonction qui provoque un effet secondaire comme celui-ci:

// Make 'x' the absolute value of itself.
void make_abs(int& x);

Ensuite, nous n'avons pas besoin d'un type de données entier immuable qui interdit aux opérateurs comme l'affectation post-initialisation de faire en sorte que cette fonction évite les effets secondaires. Nous pouvons simplement faire ceci:

// Returns the absolute value of 'x'.
int abs(int x);

Maintenant, la fonction ne joue pas avec xou quoi que ce soit en dehors de sa portée, et dans ce cas trivial, nous pourrions même avoir réduit certains cycles en évitant toute surcharge associée à l'indirection / aliasing. À tout le moins, la deuxième version ne devrait pas être plus coûteuse en termes de calcul que la première.

Choses qui coûtent cher à copier en entier

Bien sûr, la plupart des cas ne sont pas si simples si nous voulons éviter qu'une fonction ne provoque des effets secondaires. Un cas d'utilisation complexe dans le monde réel pourrait ressembler davantage à ceci:

// Transforms the vertices of the specified mesh by
// the specified transformation matrix.
void transform(Mesh& mesh, Matrix4f matrix);

À ce stade, le maillage peut nécessiter quelques centaines de mégaoctets de mémoire avec plus de cent mille polygones, encore plus de sommets et d'arêtes, de multiples textures, des cibles de morphing, etc. transformfonctionner sans effets secondaires, comme ceci:

// Returns a new version of the mesh whose vertices been 
// transformed by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);

Et c'est dans ces cas où la copie de quelque chose dans son intégralité serait normalement une surcharge épique où j'ai trouvé utile de se transformer Meshen une structure de données persistante et un type immuable avec le "générateur" analogique pour en créer des versions modifiées afin qu'il peut simplement copier peu profondément et compter des pièces qui ne sont pas uniques. Tout cela dans le but de pouvoir écrire des fonctions de maillage sans effets secondaires.

Structures de données persistantes

Et dans ces cas où tout copier est si incroyablement cher, j'ai trouvé que l'effort de concevoir un immuable était Meshvraiment rentable même s'il avait un coût légèrement élevé à l'avance, car cela ne simplifiait pas seulement la sécurité des threads. Il a également simplifié l'édition non destructive (permettant à l'utilisateur de superposer les opérations de maillage sans modifier sa copie d'origine), les systèmes d'annulation (désormais, le système d'annulation peut simplement stocker une copie immuable du maillage avant les modifications apportées par une opération sans faire exploser la mémoire). utilisation), et la sécurité des exceptions (maintenant, si une exception se produit dans la fonction ci-dessus, la fonction n'a pas besoin de revenir en arrière et d'annuler tous ses effets secondaires car elle n'en a pas causé au début).

Je peux dire avec confiance dans ces cas que le temps nécessaire pour rendre immuables ces structures de données lourdes a permis d'économiser plus de temps qu'il n'en a coûté, car j'ai comparé les coûts de maintenance de ces nouvelles conceptions avec les anciens qui tournaient autour de la mutabilité et des fonctions provoquant des effets secondaires, et les anciennes conceptions mutables coûtaient beaucoup plus de temps et étaient beaucoup plus sujettes aux erreurs humaines, en particulier dans les domaines qui sont vraiment tentants pour les développeurs de les négliger pendant les périodes critiques, comme la sécurité d'exception.

Je pense donc que les types de données immuables sont vraiment payants dans ces cas, mais tout ne doit pas être rendu immuable afin de rendre la majorité des fonctions de votre système sans effets secondaires. Beaucoup de choses sont assez bon marché pour simplement copier en entier. De nombreuses applications du monde réel devront également provoquer des effets secondaires ici et là (tout au moins comme enregistrer un fichier), mais il existe généralement beaucoup plus de fonctions qui pourraient être dépourvues d'effets secondaires.

Le but d'avoir certains types de données immuables pour moi est de s'assurer que nous pouvons écrire le nombre maximum de fonctions pour être exempt d'effets secondaires sans encourir de surcharge épique sous la forme de copies massives de structures de données massives à gauche et à droite en totalité lorsque seules de petites portions d'entre eux doivent être modifiés. La présence de structures de données persistantes dans ces cas finit par devenir un détail d'optimisation pour nous permettre d'écrire nos fonctions sans effets secondaires sans payer un coût épique.

Frais généraux immuables

Maintenant, conceptuellement, les versions modifiables auront toujours un avantage en termes d'efficacité. Il y a toujours cette surcharge de calcul associée aux structures de données immuables. Mais je l'ai trouvé un échange digne dans les cas que j'ai décrits ci-dessus, et vous pouvez vous concentrer à rendre la surcharge suffisamment minime dans la nature. Je préfère ce type d'approche où l'exactitude devient facile et l'optimisation devient plus difficile que l'optimisation étant plus facile mais l'exactitude devenant plus difficile. Ce n'est pas aussi démoralisant d'avoir du code qui fonctionne parfaitement correctement, qui a besoin de quelques mises au point supplémentaires sur du code qui ne fonctionne pas correctement en premier lieu, quelle que soit la rapidité avec laquelle il obtient ses résultats incorrects.


la source
3

Le seul inconvénient auquel je peux penser est qu'en théorie, l'utilisation de données immuables peut être plus lente que les données mutables - il est plus lent de créer une nouvelle instance et de collecter la précédente que de modifier une existante.

L'autre «problème» est que vous ne pouvez pas utiliser uniquement des types immuables. En fin de compte, vous devez décrire l'état et vous devez utiliser des types mutables pour le faire - sans changer d'état, vous ne pouvez faire aucun travail.

Mais la règle générale reste d'utiliser des types immuables partout où vous le pouvez et de rendre les types mutables uniquement lorsqu'il y a vraiment une raison de le faire ...

Et pour répondre à la question " Pourquoi les types immuables sont-ils si rarement utilisés dans d'autres applications? " - Je ne pense vraiment pas qu'ils le soient ... où que vous regardiez, tout le monde recommande de rendre vos classes aussi immuables que possible ... par exemple: http://www.javapractices.com/topic/TopicAction.do?Id=29

mrpyo
la source
1
Cependant, vos deux problèmes ne sont pas à Haskell.
Florian Margaine
@FlorianMargaine Pourriez-vous élaborer?
mrpyo
La lenteur n'est pas vraie grâce à un compilateur intelligent. Et dans Haskell, même les E / S se font via une API immuable.
Florian Margaine
2
Un problème plus fondamental que la vitesse est qu'il est difficile pour les objets immuables de maintenir une identité pendant que leur état change. Si un Carobjet mutable est continuellement mis à jour avec l'emplacement d'une automobile physique particulière, alors si j'ai une référence à cet objet, je peux trouver rapidement et facilement où se trouve cette automobile. Si elles Carétaient immuables, il serait probablement beaucoup plus difficile de trouver où se trouve actuellement.
supercat
Vous devez parfois coder assez intelligemment pour que le compilateur comprenne qu'il n'y a aucune référence à l'objet précédent et qu'il peut donc le modifier en place, ou effectuer des transformations de déforestation, et al. Surtout dans les grands programmes. Et comme le dit @supercat, l'identité peut en effet devenir un problème.
Macke
0

Pour modéliser n'importe quel système du monde réel où les choses peuvent changer, l'état mutable devra être codé quelque part. Il existe trois façons principales pour un objet de conserver un état mutable:

  • Utilisation d'une référence mutable à un objet immuable
  • Utilisation d'une référence immuable à un objet mutable
  • Utilisation d'une référence mutable à un objet mutable

La première utilisation permet à un objet de créer facilement un instantané immuable de l'état actuel. L'utilisation de la seconde permet à un objet de créer facilement une vue en direct de l'état actuel. L'utilisation du troisième peut parfois rendre certaines actions plus efficaces dans les cas où il n'y a guère de besoin prévu d'instantanés immuables ni de vues en direct.

Au-delà du fait que la mise à jour de l'état stocké à l'aide d'une référence mutable vers un objet immuable est souvent plus lente que la mise à jour de l'état stocké à l'aide d'un objet mutable, l'utilisation d'une référence mutable obligera à renoncer à la possibilité de construire une vue en direct bon marché de l'état. Si l'on n'a pas besoin de créer une vue en direct, ce n'est pas un problème; si, cependant, il fallait créer une vue en direct, une incapacité à utiliser une référence immuable fera toutes les opérations avec la vue - à la fois en lecture et en écriture- beaucoup plus lent qu'ils ne le seraient autrement. Si le besoin d'instantanés immuables dépasse le besoin de vues en direct, l'amélioration des performances des instantanés immuables peut justifier l'atteinte des performances pour les vues en direct, mais si l'on a besoin de vues en direct et n'a pas besoin d'instantanés, l'utilisation de références immuables aux objets mutables est le moyen aller.

supercat
la source
0

Dans votre cas, la réponse est principalement due au fait que C # a un faible support pour Immuabilité ...

Ce serait formidable si:

  • tout sera immuable par défaut, sauf indication contraire (c'est-à-dire avec un mot clé 'mutable'), mélanger les types immuables et mutables est déroutant

  • les méthodes de mutation ( With) seront automatiquement disponibles - bien que cela puisse déjà être fait, voir avec

  • il y aura un moyen de dire que le résultat d'un appel de méthode spécifique (c'est-à-dire ImmutableList<T>.Add) ne peut pas être rejeté ou au moins produira un avertissement

  • Et surtout si le compilateur pouvait autant que possible garantir l'immuabilité là où cela était demandé (voir https://github.com/dotnet/roslyn/issues/159 )

kofifus
la source
1
Concernant le troisième point, ReSharper a un MustUseReturnValueAttributeattribut personnalisé qui fait exactement cela. PureAttributea le même effet et est encore mieux pour cela.
Sebastian Redl
-1

Pourquoi les types immuables sont-ils si rarement utilisés dans d'autres applications?

Ignorance? Inexpérience?

Les objets immuables sont aujourd'hui largement considérés comme supérieurs, mais il s'agit d'un développement relativement récent. Les ingénieurs qui ne se sont pas tenus au courant ou qui sont simplement coincés dans «ce qu'ils savent» ne les utiliseront pas. Et il faut un peu de modifications de conception pour les utiliser efficacement. Si les applications sont anciennes ou si les ingénieurs ont peu de compétences en conception, leur utilisation peut être gênante ou gênante.

Telastyn
la source
"Ingénieurs qui ne se sont pas tenus à jour": on pourrait dire qu'un ingénieur devrait également se renseigner sur les technologies non traditionnelles. L'idée d'immuabilité n'est que récemment devenue courante, mais c'est une idée assez ancienne et est prise en charge (sinon appliquée) par des langages plus anciens comme Scheme, SML, Haskell. Ainsi, quiconque a l'habitude de regarder au-delà des langues traditionnelles aurait pu en prendre connaissance il y a 30 ans.
Giorgio
@Giorgio: dans certains pays, de nombreux ingénieurs écrivent encore du code C # sans LINQ, sans FP, sans itérateurs et sans génériques, donc en fait, ils ont en quelque sorte raté tout ce qui s'est passé avec C # depuis 2003. S'ils ne connaissent même pas leur langue de préférence , J'imagine à peine qu'ils connaissent une langue non traditionnelle.
Arseni Mourzenko
@MainMa: C'est bien que vous ayez écrit le mot ingénieurs en italique.
Giorgio
@Giorgio: dans mon pays, ils sont aussi appelés architectes , consultants et beaucoup d'autres termes vantards, jamais écrits en italique. Dans l'entreprise dans laquelle je travaille actuellement, je m'appelle analyste développeur et je dois passer mon temps à écrire du CSS pour du code HTML hérité. Les titres d'emploi dérangent à tant de niveaux.
Arseni Mourzenko
1
@MainMa: Je suis d'accord. Des titres comme ingénieur ou consultant ne sont souvent que des mots à la mode sans signification largement acceptée. Ils sont souvent utilisés pour rendre quelqu'un ou son poste plus important / prestigieux qu'il ne l'est réellement.
Giorgio