web-dev-qa-db-fra.com

Si les valeurs nulles sont mauvaises, que faut-il utiliser lorsqu'une valeur peut être significativement absente?

C'est l'une des règles qui ne cessent de se répéter et qui me rendent perplexe.

Les nuls sont mauvais et doivent être évités autant que possible .

Mais, mais - de ma naïveté, permettez-moi de crier - parfois une valeur PEUT être significativement absente!

S'il vous plaît laissez-moi vous poser cette question sur un exemple qui vient de ce code horrible anti-modèle monté sur lequel je travaille en ce moment . Il s'agit, à la base, d'un jeu multijoueur au tour par tour sur le Web, où les tours des deux joueurs sont exécutés simultanément (comme dans Pokemon, et contrairement aux échecs).

Après chaque tour, le serveur diffuse une liste de mises à jour du code JS côté client. La classe Update:

public class GameUpdate
{
    public Player player;
    // other stuff
}

Cette classe est sérialisée en JSON et envoyée aux joueurs connectés.

La plupart des mises à jour ont naturellement un joueur associé - après tout, il est nécessaire de savoir quel joueur a fait quel mouvement ce tour-ci. Cependant, certaines mises à jour ne peuvent pas être associées de manière significative à un lecteur. Exemple: le jeu a été lié de force car la limite de tour sans action a été dépassée. Pour de telles mises à jour, je pense qu'il est significatif que le lecteur soit annulé.

Bien sûr, je pourrais "corriger" ce code en utilisant l'héritage:

public class GameUpdate
{
    // stuff
}

public class GamePlayerUpdate : GameUpdate
{
    public Player player;
    // other stuff
}

Cependant, je ne vois pas en quoi cela peut être amélioré, pour deux raisons:

  • Le code JS recevra désormais simplement un objet sans Player en tant que propriété définie, ce qui est le même que s'il était nul car les deux cas nécessitent de vérifier si la valeur est présente ou absente;
  • Si un autre champ nullable était ajouté à la classe GameUpdate, je devrais être capable d'utiliser l'héritage multiple pour continuer avec cette conception - mais MI est mauvais en soi (selon des programmeurs plus expérimentés) et plus important encore, C # ne le fait pas l'avoir donc je ne peux pas l'utiliser.

J'ai une idée que ce morceau de ce code est l'un des très nombreux où un bon programmeur expérimenté hurlerait d'horreur. En même temps, je ne vois pas comment, dans cet endroit, est-ce que cela peut nuire à quoi que ce soit et ce qui devrait être fait à la place.

Pourriez-vous m'expliquer ce problème?

26
gaazkam

Beaucoup de choses valent mieux que null.

  • Une chaîne vide ("")
  • Une collection vide
  • Une monade "facultative" ou "peut-être"
  • Une fonction qui ne fait rien tranquillement
  • Un objet plein de méthodes qui ne font rien tranquillement
  • Une valeur significative qui rejette la prémisse incorrecte de la question
    (c'est ce qui vous a fait considérer comme nul en premier lieu)

L'astuce consiste à savoir quand le faire. Null est vraiment bon pour exploser dans votre visage, mais seulement lorsque vous le détachez sans le vérifier. Ce n'est pas bon pour expliquer pourquoi.

Lorsque vous n'avez pas besoin d'exploser et que vous ne voulez pas vérifier la valeur null, utilisez l'une de ces alternatives. Il peut sembler étrange de renvoyer une collection si elle ne contient que 0 ou 1 élément, mais elle est vraiment bonne pour vous permettre de traiter silencieusement les valeurs manquantes.

Les monades ont l'air fantaisistes, mais ici, tout ce qu'elles font, c'est de vous faire comprendre que les tailles valides pour la collection sont 0 et 1. Si vous les avez, considérez-les.

N'importe lequel de ceux-ci fait un meilleur travail pour clarifier votre intention que null. C'est la différence la plus importante.

Il peut être utile de comprendre pourquoi vous revenez du tout plutôt que de simplement lever une exception. Les exceptions sont essentiellement un moyen de rejeter une hypothèse (ce n'est pas le seul moyen). Lorsque vous demandez le point où deux lignes se croisent, vous supposez qu'elles se croisent. Ils pourraient être parallèles. Devriez-vous lever une exception "lignes parallèles"? Eh bien, vous pourriez mais maintenant vous devez gérer cette exception ailleurs ou la laisser arrêter le système.

Si vous préférez rester où vous êtes, vous pouvez transformer le rejet de cette hypothèse en une sorte de valeur qui peut exprimer des résultats ou des rejets. Ce n'est pas si étrange. Nous le faisons en algèbre tout le temps. L'astuce consiste à rendre cette valeur significative afin que nous puissions comprendre ce qui s'est passé.

Si la valeur retournée peut exprimer des résultats et des rejets, elle doit être envoyée à un code qui peut gérer les deux. Le polymorphisme est vraiment puissant à utiliser ici. Plutôt que de simplement échanger des contrôles nuls contre des contrôles isPresent() , vous pouvez écrire du code qui se comporte bien dans les deux cas. Soit en remplaçant les valeurs vides par des valeurs par défaut, soit en ne faisant rien.

Le problème avec null est qu'il peut signifier beaucoup trop de choses. Cela finit donc par détruire des informations.


Dans mon expérience de pensée nulle préférée, je vous demande d'imaginer une machine complexe Rube Goldbergian qui signale les messages codés en utilisant la lumière colorée en ramassant les ampoules colorées d'un tapis roulant, en les vissant dans une prise et en les alimentant vers le haut. Le signal est contrôlé par les différentes couleurs des ampoules placées sur la bande transporteuse.

On vous a demandé de rendre cette chose affreusement chère conforme à la RFC3.14159 qui stipule qu'il devrait y avoir un marqueur entre les messages. Le marqueur ne doit pas être du tout lumineux. Cela devrait durer exactement une impulsion. La même quantité de temps qu'une lumière colorée unique brille normalement.

Vous ne pouvez pas simplement laisser des espaces entre les messages car l'engin déclenche des alarmes et s'arrête s'il ne trouve pas une ampoule à mettre dans la douille.

Tous ceux qui connaissent ce projet frissonnent à l'idée de toucher la machine ou son code de commande. Ce n'est pas facile de changer. Que faire? Eh bien, vous pouvez plonger et commencer à casser des choses. Peut-être mettre à jour ce design hideux. Ouais tu pourrais faire tellement mieux. Ou vous pouvez parler au concierge et commencer à ramasser les ampoules grillées.

Voilà la solution polymorphe. Le système n'a même pas besoin de savoir que quelque chose a changé.


C'est vraiment bien si vous pouvez retirer cela. En encodant un rejet significatif d'une hypothèse en une valeur, vous n'avez pas à forcer la question à changer.

Combien de pommes me devrez-vous si je vous donne 1 pomme? -1. Parce que je te devais 2 pommes d'hier. Ici, l'hypothèse de la question "vous me devez" est rejetée avec un nombre négatif. Même si la question était erronée, des informations significatives sont encodées dans cette réponse. En la rejetant de manière significative, la question n'est pas obligée de changer.

Cependant, changer la question est parfois la voie à suivre. Si l'hypothèse peut être rendue plus fiable, tout en restant utile, envisagez de le faire.

Votre problème est que, normalement, les mises à jour proviennent des joueurs. Mais vous avez découvert le besoin d'une mise à jour qui ne vient pas du joueur 1 ou du joueur 2. Vous avez besoin d'une mise à jour indiquant que le temps est écoulé. Aucun joueur ne dit cela, alors que devez-vous faire? Laisser le joueur nul semble si tentant. Mais cela détruit l'information. Vous essayez de coder la connaissance dans un trou noir.

Le champ du joueur ne vous dit pas qui vient de bouger. Vous pensez que oui, mais ce problème prouve que c'est un mensonge. Le champ du joueur vous indique d'où vient la mise à jour. Votre mise à jour expirée provient de l'horloge du jeu. Ma recommandation est de donner à l'horloge le crédit qu'elle mérite et de changer le nom du champ player en quelque chose comme source.

De cette façon, si le serveur s'arrête et doit suspendre le jeu, il peut envoyer une mise à jour qui signale ce qui se passe et se répertorie comme source de la mise à jour.

Est-ce à dire que vous ne devez jamais laisser de côté quelque chose? Non. Mais vous devez considérer dans quelle mesure rien peut être supposé signifier vraiment une seule valeur par défaut bien connue. Rien n'est vraiment facile à utiliser. Soyez donc prudent lorsque vous l'utilisez. Il existe une école de pensée qui privilégie rien . Cela s'appelle convention sur la configuration .

J'aime cela moi-même, mais seulement lorsque la convention est clairement communiquée d'une manière ou d'une autre. Ce ne devrait pas être un moyen de brouiller les débutants. Mais même si vous l'utilisez, null n'est toujours pas la meilleure façon de le faire. Null est un rien dangereux . Null prétend être quelque chose que vous pouvez utiliser jusqu'à ce que vous l'utilisiez, puis il fait sauter un trou dans l'univers (brise votre modèle sémantique). À moins de faire un trou dans l'univers, c'est le comportement dont vous avez besoin, pourquoi jouez-vous avec ça? Utilisez autre chose.

20
candied_orange

null n'est pas vraiment le problème ici. Le problème est de savoir comment la plupart des langages de programmation actuels gèrent les informations manquantes; ils ne prennent pas ce problème au sérieux (ou du moins ne fournissent pas le support et la sécurité dont la plupart des terriens ont besoin pour éviter les pièges). Cela conduit à tous les problèmes que nous avons appris à craindre (Javas NullPointerException, Segfaults, ...).

Voyons comment mieux gérer ce problème.

D'autres ont suggéré le Null-Object Pattern . Bien que je pense que cela s'applique parfois, il existe de nombreux scénarios où il n'y a tout simplement pas de valeur par défaut unique. Dans ces cas, les consommateurs des informations éventuellement absentes doivent pouvoir décider eux-mêmes quoi faire si ces informations sont manquantes.

Il existe des langages de programmation qui offrent une sécurité contre les pointeurs nuls (ce qu'on appelle la sécurité nulle) au moment de la compilation. Pour donner quelques exemples modernes: Kotlin, Rust, Swift. Dans ces langues, le problème des informations manquantes a été pris au sérieux et l'utilisation de null est totalement indolore - le compilateur vous empêchera de déréférencer les nullpointers.
En Java, des efforts ont été faits pour établir les annotations @ Nullable/@ NotNull avec les plugins compilateur + IDE pour obtenir le même effet. Ce n'était pas vraiment réussi mais c'est hors de portée pour cette réponse.

Un type générique explicite qui dénote une absence possible est une autre façon de le gérer. Par exemple. dans Java il y a Java.util.Optional. Cela peut fonctionner:
Au niveau d'un projet ou d'une entreprise, vous pouvez établir des règles/directives

  • utiliser uniquement des bibliothèques tierces de haute qualité
  • toujours utilisez Java.util.Optional au lieu de null

Votre communauté/écosystème de langues a peut-être trouvé d'autres moyens de résoudre ce problème mieux que le compilateur/interprète.

15
marstato

Je ne vois aucun mérite dans l'affirmation selon laquelle null est mauvais. J'ai vérifié les discussions à ce sujet et elles ne font que m'impatienter et m'énerver.

Null peut être mal utilisé, en tant que "valeur magique", alors qu'il signifie réellement quelque chose. Mais il faudrait faire une programmation assez tordue pour y arriver.

Null devrait signifier "pas là", qui n'est pas créé (encore) ou (déjà) disparu ou non fourni s'il s'agit d'un argument. Ce n'est pas un état, le tout manque. Dans un paramètre OO où les choses sont créées et détruites tout le temps, c'est une condition valide et significative différente de tout état.

Avec null, vous êtes giflé au moment de l'exécution où vous êtes giflé au moment de la compilation avec une sorte d'alternative null de type sécurisé.

Pas entièrement vrai, je peux obtenir un avertissement pour ne pas vérifier la valeur null avant d'utiliser la variable. Et ce n'est pas pareil. Je ne veux peut-être pas épargner les ressources d'un objet qui n'a aucun but. Et plus important encore, je devrai vérifier l'alternative tout de même afin d'obtenir le comportement souhaité. Si j'oublie de le faire, je préfère obtenir une exception NullReferenceException au moment de l'exécution plutôt qu'un comportement non défini/involontaire.

Il y a un problème à prendre en compte. Dans un contexte multithread, vous devez vous protéger contre les conditions de concurrence en attribuant à une variable locale avant d'effectuer la vérification de null.

Le problème avec null est qu'il peut signifier beaucoup de choses. Cela finit donc par détruire des informations.

Non, cela ne signifie/ne devrait rien signifier donc il n'y a rien à détruire. Le nom et le type de la variable/argument diront toujours ce qui manque, c'est tout ce qu'il y a à savoir.

Modifier:

Je suis à mi-chemin de la vidéo candied_orange liée à l'une de ses autres réponses sur la programmation fonctionnelle et c'est très intéressant. Je peux en voir l'utilité dans certains scénarios. L'approche fonctionnelle que j'ai vue jusqu'à présent, avec des morceaux de code volant autour, me fait généralement grincer des dents lorsqu'il est utilisé dans un paramètre OO. Mais je suppose qu'il a sa place. Quelque part. Et je suppose il y aura des langages qui font du bon travail en cachant ce que je perçois comme un désordre déroutant chaque fois que je le rencontre en C #, des langages qui fournissent à la place un modèle propre et réalisable.

Si ce modèle fonctionnel est votre état d'esprit/cadre, il n'y a vraiment pas d'autre alternative que de pousser les incidents en tant que descriptions d'erreurs documentées et finalement de laisser l'appelant gérer les ordures accumulées qui ont été traînées tout le long du chemin jusqu'au point d'initiation. Apparemment, c'est exactement comme cela que fonctionne la programmation fonctionnelle. Appelez cela une limitation, appelez-le un nouveau paradigme. Vous pouvez considérer que c'est un hack pour les disciples fonctionnels qui leur permet de continuer un peu plus loin sur le chemin profane, vous pouvez être ravi de cette nouvelle façon de programmer qui ouvre de nouvelles possibilités passionnantes. Quoi qu'il en soit, il me semble inutile d'entrer dans ce monde, de revenir à la manière traditionnelle OO de faire les choses (oui, nous pouvons lever des exceptions, désolé), choisir un concept, tirer il hors de son contexte et crier meurtre bleu parce qu'il ne correspond pas à votre nouveau monde récemment découvert.

Tout concept peut être utilisé dans le mauvais sens. D'où je me tiens, les "solutions" présentées ne sont pas des alternatives pour une utilisation appropriée de null. Ce sont des concepts d'un autre monde.

10
Martin Maat

Ta question.

Mais, mais - de ma naïveté, laissez-moi crier - parfois une valeur PEUT être significativement absente!

Entièrement à bord avec vous là-bas. Cependant, il y a quelques mises en garde que j'entrerai dans une seconde. Mais d'abord:

Les nuls sont mauvais et doivent être évités autant que possible.

Je pense que c'est une déclaration bien intentionnée mais trop généralisée.

Les nuls ne sont pas mauvais. Ils ne sont pas un contre-modèle. Ils ne doivent pas être évités à tout prix. Cependant, les valeurs nulles sont sujettes à erreur (faute de vérifier la valeur null).

Mais je ne comprends pas comment le fait de ne pas vérifier null est en quelque sorte la preuve que null est intrinsèquement mauvais à utiliser.

Si null est mauvais, les index de tableau et les pointeurs C++ le sont aussi. Ils ne sont pas. Ils sont faciles à tirer avec le pied, mais ils ne sont pas censés être évités à tout prix.

Pour le reste de cette réponse, je vais adapter l'intention de "les nuls sont mauvais" à un plus nuancé " les nuls doivent être traités de manière responsable ".


Votre solution.

Bien que je partage votre opinion sur l'utilisation de null comme règle globale; Je ne suis pas d'accord avec votre utilisation de null dans ce cas particulier.

Pour résumer les commentaires ci-dessous: vous comprenez mal le but de l'héritage et du polymorphisme. Bien que votre argument pour utiliser null soit valide, votre "preuve" supplémentaire de la raison pour laquelle l'autre manière est mauvaise est basée sur une mauvaise utilisation de l'héritage, rendant vos points quelque peu hors de propos pour le problème nul.

La plupart des mises à jour ont naturellement un joueur associé - après tout, il est nécessaire de savoir quel joueur a fait quel mouvement ce tour-ci. Cependant, certaines mises à jour ne peuvent pas être associées de manière significative à un lecteur.

Si ces mises à jour sont significativement différentes (ce qu'elles sont), vous avez besoin de deux types de mise à jour distincts.

Prenons l'exemple des messages. Vous avez des messages d'erreur et des messages de chat IM. Mais bien qu'ils puissent contenir des données très similaires (un message de chaîne et un expéditeur), ils ne se comportent pas de la même manière. Ils doivent être utilisés séparément, en tant que classes différentes.

Ce que vous faites maintenant, c'est de différencier efficacement deux types de mise à jour en fonction de la nullité de la propriété Player. Cela revient à choisir entre un message instantané et un message d'erreur basé sur l'existence d'un code d'erreur. Cela fonctionne, mais ce n'est pas la meilleure approche.

  • Le code JS recevra désormais simplement un objet sans Player en tant que propriété définie, ce qui est le même que s'il était nul puisque les deux cas nécessitent de vérifier si la valeur est présente ou absente;

Votre argument repose sur des erreurs de polymorphisme. Si vous avez une méthode qui retourne un objet GameUpdate, elle ne devrait généralement pas se soucier de faire la différence entre GameUpdate, PlayerGameUpdate ou NewlyDevelopedGameUpdate.

Une classe de base doit avoir un contrat pleinement fonctionnel, c'est-à-dire que ses propriétés sont définies sur la classe de base et non sur la classe dérivée (cela étant dit, la valeur de ces propriétés de base peut bien sûr être remplacé dans les classes dérivées).

Cela rend votre argument théorique. Dans la bonne pratique, vous ne devriez jamais vous soucier du fait que votre objet GameUpdate possède ou non un lecteur. Si vous essayez d'accéder à la propriété Player d'un GameUpdate, vous faites quelque chose d'illogique.

Les langages typés tels que C # ne vous permettraient même pas d'essayer d'accéder à la propriété Player d'un GameUpdate car GameUpdate n'a tout simplement pas la propriété. Javascript est considérablement plus laxiste dans son approche (et il ne nécessite pas de précompilation), il ne prend donc pas la peine de vous corriger et le fait exploser au moment de l'exécution.

  • Si un autre champ nullable était ajouté à la classe GameUpdate, je devrais être capable d'utiliser l'héritage multiple pour continuer avec cette conception - mais MI est mauvais en soi (selon des programmeurs plus expérimentés) et plus important encore, C # ne le fait pas l'avoir donc je ne peux pas l'utiliser.

Vous ne devriez pas créer de classes basées sur l'ajout de propriétés nullables. Vous devez créer des classes basées sur des types de mises à jour de jeu fonctionnellement uniques .


Comment éviter les problèmes avec null en général?

Je pense qu'il est pertinent de préciser comment vous pouvez exprimer de manière significative l'absence de valeur. Il s'agit d'un ensemble de solutions (ou de mauvaises approches) sans ordre particulier.

1. Utilisez null de toute façon.

C'est l'approche la plus simple. Cependant, vous vous inscrivez à une tonne de vérifications nulles et (en cas d'échec) à dépanner les exceptions de référence null.

2. Utilisez une valeur sans signification non nulle

Un exemple simple ici est indexOf():

"abcde".indexOf("b"); // 1
"abcde".indexOf("f"); // -1

Au lieu de null, vous obtenez -1. Sur le plan technique, il s'agit d'une valeur entière valide. Cependant, une valeur négative est une réponse absurde car les index devraient être des entiers positifs.

Logiquement, cela ne peut être utilisé que dans les cas où le type de retour autorise plus de valeurs possibles que ce qui peut être renvoyé de manière significative (indexOf = entiers positifs; int = nombres négatifs et positifs)

Pour les types de référence, vous pouvez toujours appliquer ce principe. Par exemple, si vous avez une entité avec un ID entier, vous pouvez renvoyer un objet factice avec son ID défini sur -1, par opposition à null.
Il vous suffit de définir un "état factice" par lequel vous pouvez mesurer si un objet est réellement utilisable ou non.

Notez que vous ne devez pas vous fier à des valeurs de chaîne prédéterminées si vous ne contrôlez pas la valeur de chaîne. Vous pouvez envisager de définir le nom d'un utilisateur sur "null" lorsque vous souhaitez renvoyer un objet vide, mais vous rencontrerez des problèmes lorsque Christopher Null s'inscrit pour votre service .

3. Jetez une exception à la place.

Non, juste non. Les exceptions ne doivent pas être gérées pour le flux logique.

Cela étant dit, dans certains cas, vous ne souhaitez pas masquer l'exception (nullreference), mais plutôt afficher une exception appropriée. Par exemple:

var existingPerson = personRepository.GetById(123);

Cela peut lancer un PersonNotFoundException (ou similaire) lorsqu'il n'y a personne avec l'ID 123.

De manière assez évidente, cela n'est acceptable que dans les cas où le fait de ne pas trouver un article rend impossible tout autre traitement. S'il est impossible de trouver un élément et que l'application peut continuer, , vous ne devez JAMAIS lever d'exception . Je ne peux insister assez sur ce point.

7
Flater

La raison pour laquelle les valeurs NULL sont mauvaises n'est pas que la valeur manquante est codée comme nulle, mais que toute valeur de référence peut être nulle. Si vous avez C #, vous êtes probablement perdu de toute façon. Je ne vois pas un point ro utiliser certains facultatifs, car un type de référence est déjà facultatif. Mais pour Java il y a des annotations @Notnull, que vous semblez pouvoir configurer au niveau du package , alors pour les valeurs qui peuvent être manquées, vous pouvez utiliser l'annotation @ Nullable.

5
max630

Les nuls ne sont pas mauvais, il n'y a en réalité aucun moyen de mettre en œuvre l'une des alternatives sans à un moment donné traiter quelque chose qui ressemble à un nul.

Les nuls sont cependant dangereux. Tout comme toute autre construction de bas niveau, si vous vous trompez, il n'y a rien en dessous pour vous attraper.

Je pense qu'il y a beaucoup d'arguments valables contre null:

  • Que si vous êtes déjà dans un domaine de haut niveau, il existe des modèles plus sûrs que l'utilisation du modèle de bas niveau.

  • À moins que vous n'ayez besoin des avantages d'un système de bas niveau et que vous soyez prêt à être prudent, vous ne devriez peut-être pas utiliser un langage de bas niveau.

  • etc ...

Ceux-ci sont tous valides et en outre ne pas avoir à faire l'effort d'écrire des getters et des setters, etc. est considéré comme une raison courante de ne pas avoir suivi ces conseils. Il n'est pas vraiment surprenant que beaucoup de programmeurs soient arrivés à la conclusion que les valeurs nulles sont dangereuses et paresseuses (une mauvaise combinaison!), Mais comme tant d'autres bits de conseils "universels", elles ne sont pas aussi universelles qu'elles sont formulées.

Les bonnes personnes qui ont écrit la langue ont pensé qu'elles seraient utiles à quelqu'un. Si vous comprenez les objections et pensez toujours qu'elles vous conviennent, personne d'autre ne saura mieux.

1
drjpizzle

VOITURE. Hoare a déclaré nul son "erreur d'un milliard de dollars". Cependant, il est important de noter qu'il pensait que la solution n'était pas "pas de null nulle part" mais plutôt "des pointeurs non nullables comme valeur par défaut et null uniquement là où ils sont sémantiquement requis".

Le problème avec null dans les langues qui répètent son erreur est que vous ne pouvez pas dire qu'une fonction attend des données non nullables; c'est fondamentalement inexprimable dans la langue. Cela oblige les fonctions à inclure un grand nombre de passe-partout inutiles de gestion des valeurs nulles, et l'oubli d'une vérification nulle est une source courante de bogues.

Pour contourner ce manque fondamental d'expressivité, certaines communautés (par exemple la communauté Scala) n'utilisent pas null. Dans Scala, si vous voulez une valeur nullable de type T, vous prenez un argument de type Option[T], et si vous voulez une valeur non nullable vous prenez simplement un T. Si quelqu'un passe un null dans votre code, votre code explosera. Cependant, il est considéré que le code appelant est le code buggé. Option est un remplacement plutôt agréable pour null, car les modèles courants de gestion des valeurs nulles sont extraits dans des fonctions de bibliothèque comme .map(f) ou .getOrElse(someDefault).

0
user311781

Java a une classe Optional avec un ifPresent + lambda explicite pour accéder à n'importe quelle valeur en toute sécurité. Cela peut aussi être fait dans JS:

Voir cette question StackOverflow .

Soit dit en passant: beaucoup de puristes trouveront cet usage douteux, mais je ne le pense pas. Des fonctionnalités optionnelles existent.

0
Joop Eggen