Dois-je éviter d'utiliser un entier non signé en C #?

23

J'ai récemment pensé à l'utilisation d'entiers non signés en C # (et je suppose que l'on peut dire un argument similaire à propos d'autres "langages de haut niveau")

Lorsque j'ai besoin d'un entier, je ne suis normalement pas confronté au dilemme de la taille d'un entier, un exemple serait une propriété d'âge d'une classe Person (mais la question ne se limite pas aux propriétés). Dans cet esprit, il n'y a, à ma connaissance, qu'un seul avantage à utiliser un entier non signé ("uint") par rapport à un entier signé ("int") - la lisibilité. Si je souhaite exprimer l'idée qu'un âge ne peut être que positif, je peux y parvenir en définissant le type d'âge sur uint.

En revanche, les calculs sur des entiers non signés peuvent entraîner des erreurs de toutes sortes et il est difficile d'effectuer des opérations telles que la soustraction de deux âges. (J'ai lu que c'est l'une des raisons pour lesquelles Java a omis des entiers non signés)

Dans le cas de C #, je peux également penser qu'une clause de garde sur le setter serait une solution qui donne le meilleur des deux mondes, mais, cela ne serait pas applicable lorsque je par exemple, un âge serait passé à une certaine méthode. Une solution de contournement consisterait à définir une classe appelée Age et à ce que la propriété age soit la seule chose, mais ce modèle me ferait créer de nombreuses classes et serait une source de confusion (les autres développeurs ne sauraient pas quand un objet n'est qu'un wrapper et quand c'est quelque chose de plus sofisticadé).

Quelles sont les meilleures pratiques générales concernant ce problème? Comment dois-je gérer ce type de scénario?

Belgi
la source
1
De plus, unsigned int n'est pas compatible CLS, ce qui signifie que vous ne pouvez pas appeler les API qui les utilisent à partir d'autres langages .NET.
Nathan Cooper
2
@NathanCooper: ... « ne peut pas appeler les API qui les utilisent à partir des autres langues ». Les métadonnées pour eux sont normalisées, donc tous les langages .NET qui prennent en charge les types non signés fonctionneront très bien.
Ben Voigt
5
Pour répondre à votre exemple spécifique, je n'aurais pas de propriété appelée Age en premier lieu. J'aurais une propriété appelée Birthday ou CreationTime ou autre chose, et j'en calculerais l'âge.
Eric Lippert
2
"... mais ce modèle me ferait créer de nombreuses classes et serait une source de confusion" en fait, c'est la bonne chose à faire. Recherchez simplement le fameux motif anti- obsession primitive .
Songo

Réponses:

24

Les concepteurs du .NET Framework ont ​​choisi un entier signé 32 bits comme "numéro à usage général" pour plusieurs raisons:

  1. Il peut gérer des nombres négatifs, en particulier -1 (que le Framework utilise pour indiquer une condition d'erreur; c'est pourquoi un entier signé est utilisé partout où l'indexation est requise, même si les nombres négatifs ne sont pas significatifs dans un contexte d'indexation).
  2. Il est assez grand pour répondre à la plupart des besoins, tout en étant suffisamment petit pour être utilisé économiquement presque partout.

La raison d'utiliser des entiers non signés n'est pas la lisibilité; il a la capacité d'obtenir les calculs que seul un int non signé fournit.

Les clauses de garde, la validation et les conditions préalables au contrat sont des moyens parfaitement acceptables d'assurer des plages numériques valides. Une plage numérique réelle correspond rarement à un nombre compris entre zéro et 2 32 -1 (ou quelle que soit la plage numérique native du type numérique que vous avez choisi), donc utiliser un uintpour contraindre votre contrat d'interface à des nombres positifs est une sorte de sans rapport.

Robert Harvey
la source
2
Bonne réponse! Il peut également y avoir des cas où un entier non signé peut en fait produire par inadvertance plus d'erreurs (bien que probablement immédiatement détectées, mais un peu déroutantes) - imaginez boucler en sens inverse avec un compteur entier non signé parce qu'une certaine taille est un entier: for (uint j=some_size-1; j >= 0; --j)- whoops ( Je ne sais pas si c'est un problème en C #)! J'ai trouvé ce problème dans le code avant qui essayait d'utiliser autant que possible un entier non signé du côté C - et nous avons fini par le changer pour le favoriser intplus tard, et nos vies étaient beaucoup plus faciles avec moins d'avertissements du compilateur.
14
"Une plage numérique réelle correspond rarement à un nombre compris entre zéro et 2 ^ 32-1." D'après mon expérience, si vous avez besoin d'un nombre supérieur à 2 ^ 31, il est fort probable que vous finissiez également par avoir besoin de nombres supérieurs à 2 ^ 32, vous pourriez donc aussi bien passer à (signé) int64 à ce point.
Mason Wheeler
3
@Panzercrisis: C'est un peu sévère. Il serait probablement plus exact de dire «Utilisez la intplupart du temps parce que c'est la convention établie, et c'est ce que la plupart des gens s'attendent à voir utilisé régulièrement. Utilisez-le uintlorsque vous avez besoin des capacités spéciales d'un uint. N'oubliez pas que les concepteurs de Framework ont ​​décidé de suivre cette convention de manière approfondie, vous ne pouvez donc même pas l'utiliser uintdans de nombreux contextes de Framework (il n'est pas compatible avec le type).
Robert Harvey
2
@Panzercrisis Ce pourrait être une formulation trop forte; mais je ne sais pas si j'ai déjà utilisé des types non signés en C # sauf lorsque j'appelais vers le bas pour des API win32 (où la convention est que les constantes / drapeaux / etc ne sont pas signés).
Dan Neely
4
C'est en effet assez rare. La seule fois où j'utilise des entiers non signés, c'est dans des scénarios de bits.
Robert Harvey
8

En règle générale, vous devez toujours utiliser le type de données le plus spécifique possible pour vos données.

Si, par exemple, vous utilisez Entity Framework pour extraire des données d'une base de données, EF utilisera automatiquement le type de données le plus proche de celui utilisé dans la base de données.

Il y a deux problèmes avec cela en C #.
Tout d'abord, la plupart des développeurs C # utilisent uniquement int, pour représenter des nombres entiers (sauf s'il y a une raison d'utiliser long). Cela signifie que les autres développeurs ne penseront pas à vérifier le type de données, ils obtiendront donc les erreurs de débordement mentionnées ci-dessus. La deuxième et question plus critique, est / était que de .NET opérateurs arithmétiques d' origine uniquement pris en charge int, uint, long, ulong, float, double, et decimal*. C'est toujours le cas aujourd'hui (voir la section 7.8.4 dans les spécifications du langage C # 5.0 ). Vous pouvez le tester vous-même à l'aide du code suivant:

byte a, b;
a = 1;
b = 2;
var c = a - b;      //In visual studio, hover over "var" and the tip will indicate the data type, or you can get the value from cName below.
string cName = c.GetType().Namespace + '.' + c.GetType().Name;

Le résultat de notre byte- byteest un int( System.Int32).

Ces deux problèmes ont donné lieu à la pratique de "n'utiliser que des nombres entiers" qui est si courante.

Donc, pour répondre à votre question, en C #, c'est généralement une bonne idée de s'en tenir à intmoins que:

  • Un générateur de code automatisé a utilisé une valeur différente (comme Entity Framework).
  • Tous les autres développeurs du projet savent que vous utilisez les types de données les moins courants (incluez un commentaire indiquant que vous avez utilisé le type de données et pourquoi).
  • Les types de données les moins courants sont déjà couramment utilisés dans le projet.
  • Le programme nécessite les avantages du type de données moins courant (vous en avez 100 millions que vous devez conserver en RAM, donc la différence entre un byteet un intou un intet un longest critique, ou les différences arithmétiques des éléments non signés déjà mentionnés).

Si vous devez faire des calculs sur les données, respectez les types courants.
N'oubliez pas que vous pouvez effectuer un cast d'un type à un autre. Cela peut être moins efficace du point de vue du processeur, vous êtes donc probablement mieux avec l'un des 7 types courants, mais c'est une option si nécessaire.

Enumerations ( enum) est l'une de mes exceptions personnelles aux directives ci-dessus. Si je n'ai que quelques options, je spécifierai que l'énumération est un octet ou un court. Si j'ai besoin de ce dernier bit dans une énumération signalée, je spécifierai le type à utiliser uintafin de pouvoir utiliser hex pour définir la valeur du drapeau.

Si vous utilisez une propriété avec un code de restriction de valeur, assurez-vous d'expliquer dans la balise récapitulative quelles sont les restrictions et pourquoi.

* Les alias C # sont utilisés à la place des noms .NET, System.Int32car il s'agit d'une question C #.

Remarque: il y avait un blog ou un article des développeurs .NET (que je ne peux pas trouver), qui soulignait le nombre limité de fonctions arithmétiques et certaines raisons pour lesquelles ils ne s'en préoccupaient pas. Si je me souviens bien, ils ont indiqué qu'ils n'avaient pas l'intention d'ajouter la prise en charge des autres types de données.

Remarque: Java ne prend pas en charge les types de données non signés et ne prenait auparavant pas en charge les nombres entiers 8 ou 16 bits. Étant donné que de nombreux développeurs C # venaient d'un arrière-plan Java ou devaient travailler dans les deux langues, les limitations d'une langue étaient parfois imposées artificiellement à l'autre.

Trisped
la source
Ma règle générale est simplement "utilisez int, sauf si vous ne pouvez pas".
PerryC
@PerryC Je pense que c'est la convention la plus courante. Le point de ma réponse était de fournir une convention plus complète qui vous permet d'utiliser les fonctionnalités du langage.
Trisped
6

Vous devez principalement être conscient de deux choses: les données que vous représentez et toutes les étapes intermédiaires de vos calculs.

Il est certainement logique d'avoir l'âge unsigned int, car nous ne considérons généralement pas les âges négatifs. Mais vous mentionnez ensuite la soustraction d'un âge à un autre. Si nous soustrayons aveuglément un entier d'un autre, il est certainement possible de se retrouver avec un nombre négatif, même si nous avons convenu précédemment que les âges négatifs n'ont pas de sens. Donc, dans ce cas, vous voudriez que votre calcul soit fait avec un entier signé.

Quant à savoir si les valeurs non signées sont mauvaises ou non, je dirais que c'est une énorme généralisation de dire que les valeurs non signées sont mauvaises. Java n'a pas de valeurs non signées, comme vous l'avez mentionné, et cela m'agace constamment. A bytepeut avoir une valeur comprise entre 0-255 ou 0x00-0xFF. Mais si vous souhaitez instancier un octet supérieur à 127 (0x7F), vous devez soit l'écrire sous la forme d'un nombre négatif, soit convertir un entier en octet. Vous vous retrouvez avec un code qui ressemble à ceci:

byte a = 0x80; // Won't compile!
byte b = (byte) 0x80;
byte c = -128; // Equal to b

Ce qui précède m'ennuie sans fin. Je ne suis pas autorisé à avoir un octet ayant une valeur de 197, même si c'est une valeur parfaitement valide pour la plupart des gens sensés traitant des octets. Je peux convertir l'entier ou trouver la valeur négative (197 == -59 dans ce cas). Considérez également ceci:

byte a = 70;
byte b = 80;
byte c = a + b; // c == -106

Donc, comme vous pouvez le voir, l'ajout de deux octets avec des valeurs valides et la fin avec un octet avec une valeur valide finissent par changer le signe. Non seulement cela, mais il n'est pas immédiatement évident que 70 + 80 == -106. Techniquement, c'est un débordement, mais dans mon esprit (en tant qu'être humain), un octet ne devrait pas déborder pour les valeurs sous 0xFF. Quand je fais de l'arithmétique sur papier, je ne considère pas que le 8e bit soit un bit de signe.

Je travaille avec beaucoup d'entiers au niveau du bit, et le fait que tout soit signé rend généralement tout moins intuitif et plus difficile à gérer, car vous devez vous rappeler que le décalage à droite d'un nombre négatif vous donne de nouveaux 1s dans votre nombre. Alors que déplacer vers la droite un entier non signé ne fait jamais cela. Par exemple:

signed byte b = 0b10000000;
b = b >> 1; // b == 0b1100 0000
b = b & 0x7F;// b == 0b0100 0000

unsigned byte b = 0b10000000;
b = b >> 1; // b == 0b0100 0000;

Cela ajoute simplement des étapes supplémentaires qui, selon moi, ne devraient pas être nécessaires.

Alors que j'ai utilisé byteci-dessus, la même chose s'applique aux entiers 32 bits et 64 bits. Ne pas avoir unsignedest paralysant et cela me choque qu'il existe des langages de haut niveau comme Java qui ne les autorisent pas du tout. Mais pour la plupart des gens, ce n'est pas un problème, car de nombreux programmeurs ne traitent pas avec l'arithmétique au niveau du bit.

En fin de compte, il est utile d'utiliser des entiers non signés si vous les considérez comme des bits, et il est utile d'utiliser des entiers signés lorsque vous les considérez comme des nombres.

Shaz
la source
7
Je partage votre frustration concernant les langues sans types intégraux non signés (en particulier pour les octets), mais je crains que ce ne soit pas une réponse directe à la question posée ici. Peut-être pourriez-vous ajouter une conclusion qui, je pense, pourrait être: «Utilisez des entiers non signés si vous pensez à leur valeur en tant que bits et des entiers signés si vous les considérez comme des nombres.»
5gon12eder
1
c'est ce que j'ai dit dans un commentaire ci-dessus. heureux de voir quelqu'un d'autre penser de la même façon.
robert bristow-johnson