¿Debo evitar usar unsigned int en C #?

23

Recientemente pensé en el uso de enteros sin signo en C # (y supongo que se puede decir un argumento similar sobre otros "lenguajes de alto nivel")

Cuando necesito un número entero, normalmente no me enfrento al dilema del tamaño de un número entero, un ejemplo sería una propiedad de edad de una clase Persona (pero la pregunta no se limita a las propiedades). Con eso en mente, hasta donde puedo ver, solo hay una ventaja de usar un entero sin signo ("uint") sobre un entero con signo ("int"): legibilidad. Si deseo expresar la idea de que una edad solo puede ser positiva, puedo lograrlo estableciendo el tipo de edad en uint.

Por otro lado, los cálculos en enteros sin signo pueden conducir a errores de todo tipo y dificulta la realización de operaciones como restar dos edades. (Leí que esta es una de las razones por las que Java omitió enteros sin signo)

En el caso de C #, también puedo pensar que una cláusula de protección en el setter sería una solución que ofrece lo mejor de dos mundos, pero esto no sería aplicable cuando, por ejemplo, una edad se pasaría a algún método. Una solución alternativa sería definir una clase llamada Age y hacer que la edad de la propiedad sea lo único allí, pero este patrón me haría crear muchas clases y sería una fuente de confusión (otros desarrolladores no sabrían cuándo un objeto es solo un contenedor y cuando es algo más sofisticado).

¿Cuáles son algunas de las mejores prácticas generales con respecto a este tema? ¿Cómo debo lidiar con este tipo de escenario?

Belgi
fuente
1
Además, unsigned int no es compatible con CLS, lo que significa que no puede llamar a las API que las usan desde otros lenguajes .NET.
Nathan Cooper el
2
@NathanCooper: ... "no se puede llamar a las API que los utilizan de algunos otros idiomas". Los metadatos para ellos están estandarizados, por lo que todos los lenguajes .NET que sí admiten tipos sin firmar funcionarán correctamente.
Ben Voigt
55
Para abordar su ejemplo específico, no tendría una propiedad llamada Age en primer lugar. Tendría una propiedad llamada Birthday o CreationTime o lo que sea, y calcularía la edad a partir de ella.
Eric Lippert el
2
"... pero este patrón me haría crear muchas clases y sería una fuente de confusión", en realidad eso es lo correcto. Simplemente busque el infame patrón anti Obsesión primitiva .
Songo

Respuestas:

24

Los diseñadores de .NET Framework eligieron un número entero con signo de 32 bits como su "número de propósito general" por varias razones:

  1. Puede manejar números negativos, especialmente -1 (que el Framework usa para indicar una condición de error; es por eso que se usa un int con signo en todas partes donde se requiere indexación, aunque los números negativos no sean significativos en un contexto de indexación).
  2. Es lo suficientemente grande como para servir a la mayoría de los propósitos, mientras que es lo suficientemente pequeño como para usarse económicamente en casi cualquier lugar.

La razón para usar ints sin firmar no es la legibilidad; tiene la capacidad de obtener las matemáticas que solo proporciona un int sin firmar.

Las cláusulas de protección, la validación y las condiciones previas del contrato son formas perfectamente aceptables de asegurar rangos numéricos válidos. Rara vez, un rango numérico del mundo real corresponde exactamente a un número entre cero y 2 32 -1 (o cualquiera que sea el rango numérico nativo del tipo numérico que elija), por lo que usar un uintpara restringir su contrato de interfaz a números positivos es una especie de no viene al caso.

Robert Harvey
fuente
2
¡Buena respuesta! También puede haber algunos casos en que un entero sin signo en realidad puede inadvertidamente producir más errores (aunque probablemente los vistos de inmediato, pero un poco confuso) - imaginar un bucle en sentido inverso con un contador unsigned int porque algunos tamaño es un número entero: for (uint j=some_size-1; j >= 0; --j)- chillidos ( ¡No estoy seguro si esto es un problema en C #)! Encontré este problema en el código anterior que intentaba usar unsigned int en el lado C tanto como sea posible, y terminamos cambiándolo para favorecerlo intmás adelante, y nuestras vidas fueron mucho más fáciles con menos advertencias del compilador también.
14
"Raramente un rango numérico del mundo real corresponde a un número entre cero y 2 ^ 32-1". En mi experiencia, si va a necesitar un número mayor que 2 ^ 31, es muy probable que termine también necesitando números mayores que 2 ^ 32, por lo que podría simplemente pasar a (firmado) int64 en ese punto.
Mason Wheeler
3
@Panzercrisis: Eso es un poco severo. Probablemente sería más exacto decir "Usar la intmayor parte del tiempo porque esa es la convención establecida, y es lo que la mayoría de la gente esperará que se use de manera rutinaria. Úselo uintcuando requiera las capacidades especiales de a uint". Recuerde, los diseñadores de Framework decidieron seguir esta convención ampliamente, por lo que ni siquiera puede usarla uinten muchos contextos de Framework (no es compatible con el tipo).
Robert Harvey
2
@Panzercrisis Puede ser una frase demasiado fuerte; pero no estoy seguro de si alguna vez he usado tipos sin signo en C #, excepto cuando estaba llamando a win32 apis (donde la convención es que las constantes / flags / etc no están firmadas).
Dan Neely
44
De hecho, es bastante raro. La única vez que uso entradas sin signo es en escenarios de bit bitiddling.
Robert Harvey
8

En general, siempre debe usar el tipo de datos más específico posible para sus datos.

Si, por ejemplo, está utilizando Entity Framework para extraer datos de una base de datos, EF usará automáticamente el tipo de datos más cercano al utilizado en la base de datos.

Hay dos problemas con esto en C #.
Primero, la mayoría de los desarrolladores de C # usan solo intpara representar números enteros (a menos que haya una razón para usar long). Esto significa que otros desarrolladores no pensarán en verificar el tipo de datos, por lo que obtendrán los errores de desbordamiento mencionados anteriormente. El segundo, y más importante cuestión, es / fue que de .NET operadores aritméticos originales sólo se admite int, uint, long, ulong, float, doble, y decimal*. Este sigue siendo el caso hoy (consulte la sección 7.8.4 en la especificación del lenguaje C # 5.0 ). Puede probar esto usted mismo usando el siguiente código:

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;

El resultado de nuestro byte- bytees un int( System.Int32).

Estos dos problemas dieron lugar a la práctica de "solo usar int para números enteros", que es tan común.

Entonces, para responder su pregunta, en C # generalmente es una buena idea seguir a intmenos que:

  • Un generador de código automatizado utilizó un valor diferente (como Entity Framework).
  • Todos los demás desarrolladores del proyecto son conscientes de que está utilizando los tipos de datos menos comunes (incluya un comentario que indique que utilizó el tipo de datos y por qué).
  • Los tipos de datos menos comunes ya se usan comúnmente en el proyecto.
  • El programa requiere los beneficios del tipo de datos menos común (tiene 100 millones de estos que necesita mantener en la RAM, por lo que la diferencia entre a bytey an into an inty a longes crítica, o las diferencias aritméticas de unsigned ya mencionadas).

Si necesita hacer cálculos matemáticos con los datos, siga los tipos comunes.
Recuerda, puedes lanzar de un tipo a otro. Esto puede ser menos eficiente desde el punto de vista de la CPU, por lo que probablemente sea mejor con uno de los 7 tipos comunes, pero es una opción si es necesario.

Enumeraciones ( enum) es una de mis excepciones personales a las pautas anteriores. Si solo tengo algunas opciones, especificaré que la enumeración sea ​​un byte o un short. Si necesito ese último bit en una enumeración marcada, especificaré el tipo para uintque pueda usar hexadecimal para establecer el valor de la bandera.

Si utiliza una propiedad con código de restricción de valor, asegúrese de explicar en la etiqueta de resumen qué restricciones existen y por qué.

* Se usan alias C # en lugar de nombres .NET como, System.Int32ya que esta es una pregunta C #.

Nota: hubo un blog o artículo de los desarrolladores de .NET (que no puedo encontrar), que señalaba el número limitado de funciones aritméticas y algunas razones por las que no se preocupaban por ello. Como recuerdo, indicaron que no tenían planes para agregar soporte para los otros tipos de datos.

Nota: Java no admite tipos de datos sin signo y anteriormente no tenía soporte para números enteros de 8 o 16 bits. Dado que muchos desarrolladores de C # provenían de un entorno Java o necesitaban trabajar en ambos idiomas, las limitaciones de un idioma a veces se impondrían artificialmente en el otro.

Trisped
fuente
Mi regla general es simplemente, "use int, a menos que no pueda".
PerryC
@PerryC Creo que es la convención más común. El punto de mi respuesta fue proporcionar una convención más completa que le permita utilizar las funciones del lenguaje.
Trisped
6

Principalmente debe tener en cuenta dos cosas: los datos que está representando y los pasos intermedios en sus cálculos.

Ciertamente tiene sentido tener edad unsigned int, porque generalmente no consideramos edades negativas. Pero luego mencionas restar una edad de otra. Si solo restamos ciegamente un número entero de otro, entonces definitivamente es posible terminar con un número negativo, incluso si previamente estuvimos de acuerdo en que las edades negativas no tienen sentido. Entonces, en este caso, desearía que su cálculo se haga con un entero con signo.

En cuanto a si los valores sin signo son malos o no, diría que es una gran generalización decir que los valores sin signo son malos. Java no tiene valores sin signo, como mencionaste, y constantemente me molesta. A bytepuede tener un valor de 0-255 o 0x00-0xFF. Pero si desea crear una instancia de un byte mayor que 127 (0x7F), debe escribirlo como un número negativo o convertir un entero en un byte. Terminas con un código que se ve así:

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

Lo anterior me molesta sin fin. No se me permite que un byte tenga un valor de 197, a pesar de que es un valor perfectamente válido para la mayoría de las personas sensatas que se ocupan de bytes. Puedo emitir el entero o puedo encontrar el valor negativo (197 == -59 en este caso). Considere también esto:

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

Como puede ver, agregar dos bytes con valores válidos y terminar con un byte con un valor válido termina cambiando el signo. No solo eso, sino que no es inmediatamente obvio que 70 + 80 == -106. Técnicamente esto es un desbordamiento, pero en mi opinión (como ser humano) un byte no debería desbordarse para valores por debajo de 0xFF. Cuando hago aritmética de bits en papel, no considero que el octavo bit sea un bit de signo.

Trabajo con muchos enteros a nivel de bit, y tener todo firmado generalmente hace que todo sea menos intuitivo y más difícil de manejar, porque debes recordar que desplazar a la derecha un número negativo te da nuevos 1s en tu número. Mientras que desplazar a la derecha un entero sin signo nunca hace eso. Por ejemplo:

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;

Simplemente agrega pasos adicionales que creo que no deberían ser necesarios.

Si bien lo utilicé byteanteriormente, lo mismo se aplica a los enteros de 32 bits y 64 bits. No tener unsignedes paralizante y me sorprende que haya lenguajes de alto nivel como Java que no los permiten en absoluto. Pero para la mayoría de las personas esto no es un problema, porque muchos programadores no se ocupan de la aritmética de nivel de bits.

Al final, es útil usar enteros sin signo si los está pensando como bits, y es útil usar números enteros con signo cuando los piensa como números.

Shaz
fuente
77
Comparto su frustración por los idiomas sin tipos integrales sin signo (especialmente para bytes), pero me temo que esta no es una respuesta directa a la pregunta que se hace aquí. Tal vez usted podría agregar una conclusión, que creo, podría ser: “Use enteros sin signo si usted está pensando en su valor como bits y enteros con signo si usted está pensando en ellos como números.”
5gon12eder
1
Es lo que dije en un comentario anterior. Me alegra ver a alguien más pensando de la misma manera.
Robert Bristow-Johnson