web-dev-qa-db-fra.com

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

J'ai récemment pensé à l'utilisation d'entiers non signés en C # (et je suppose qu'un argument similaire peut être dit à 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 Java 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?

24
Belgi

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 suffisamment 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 est pas 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 pour assurer des plages numériques valides. Une plage numérique réelle correspond rarement à un nombre compris entre zéro et 232-1 (ou quelle que soit la plage numérique native du type numérique que vous avez choisi), donc utiliser un uint pour contraindre votre contrat d'interface à des nombres positifs est un peu à côté du point.

24
Robert Harvey

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 #.
Premièrement, la plupart des développeurs C # n'utilisent que int, pour représenter des nombres entiers (sauf s'il y a une raison d'utiliser long). Cela signifie que d'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. Le deuxième problème, plus critique, est/était que opérateurs arithmétiques originaux ne supportait que int, uint, long, ulong, float, double et decimal *. C'est toujours le cas aujourd'hui (voir la section 7.8.4 dans spécification du langage C # 5. ). 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-byte est 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 à int à moins 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 le moins courant (vous en avez 100 millions à conserver en RAM, donc la différence entre un byte et un int ou un int et un long est critique, ou les différences arithmétiques de unsigned déjà mentionnées).

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.

Les énumérations ( enum ) sont l'une de mes exceptions personnelles aux directives ci-dessus. Si je n'ai que quelques options, je spécifier l'énumération pour être un octet ou un court. Si j'ai besoin de ce dernier bit dans une énumération signalée, je spécifierai le type à uint afin que je puisse utiliser hex pour définir la valeur de l'indicateur.

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 comme System.Int32 car 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 # provenaient d'un Java background ou nécessaire pour travailler dans les deux langues, les limites d’une langue seraient parfois artificiellement imposées à l’autre.

8
Trisped

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 que l'âge soit 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 nombre 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. Un byte peut avoir une valeur de 0-255 ou 0x00-0xFF. Mais si vous voulez instancier un octet supérieur à 127 (0x7F), vous devez soit l'écrire sous forme de nombre négatif, soit transtyper 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 m'énerve 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 lancer 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 numéro. 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é byte ci-dessus, la même chose s'applique aux entiers 32 bits et 64 bits. Ne pas avoir unsigned est 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 de 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.

7
Shaz