Devo evitare di usare unsigned int in C #?


23

Di recente ho pensato all'uso di numeri interi senza segno in C # (e immagino che si possa dire un argomento simile su altri "linguaggi di alto livello")

Quando ho bisogno di un numero intero di solito non mi trovo di fronte al dilemma della dimensione di un numero intero, un esempio sarebbe una proprietà age di una classe Person (ma la domanda non si limita alle proprietà). Con questo in mente, per quanto posso vedere, c'è solo un vantaggio nell'usare un intero senza segno ("uint") rispetto a un intero con segno ("int"): la leggibilità. Se desidero esprimere l'idea che un'età può essere solo positiva, posso raggiungerlo impostando il tipo di età su uint.

D'altra parte, i calcoli su numeri interi senza segno possono portare a errori di ogni tipo e rende difficile eseguire operazioni come sottrarre due età. (Ho letto questo è uno dei motivi per cui Java ha omesso numeri interi senza segno)

Nel caso di C # posso anche pensare che una clausola di guardia sul setter sarebbe una soluzione che offre il meglio di due mondi, ma ciò non sarebbe applicabile quando, ad esempio, un'età sarebbe passata a un metodo. Una soluzione alternativa sarebbe quella di definire una classe chiamata Age e che l'età della proprietà sia l'unica cosa presente, ma questo modello mi farebbe creare molte classi e sarebbe fonte di confusione (altri sviluppatori non saprebbero quando un oggetto è solo un wrapper e quando è qualcosa di più sofisticato).

Quali sono alcune best practice generali relative a questo problema? Come dovrei affrontare questo tipo di scenario?



1
Inoltre int unsigned non è conforme a CLS, il che significa che non è possibile chiamare API che le utilizzano da altri linguaggi .NET.
Nathan Cooper,

2
@NathanCooper: ... "non può chiamare le API che li utilizzano da alcune altre lingue". I metadati per loro sono standardizzati, quindi tutti i linguaggi .NET che supportano tipi senza segno funzioneranno perfettamente.
Ben Voigt,

5
Per rispondere al tuo esempio specifico, in primo luogo non avrei una proprietà chiamata Age. Avrei una proprietà chiamata Birthday o CreationTime o qualsiasi altra cosa, e calcolerei l'età da essa.
Eric Lippert,

2
"... ma questo schema mi farebbe creare molte classi e sarebbe fonte di confusione" in realtà è la cosa giusta da fare. Cerca il famigerato anti pattern di Primitive Obsession .
Songo

Risposte:


24

I progettisti di .NET Framework hanno scelto un numero intero con segno a 32 bit come "numero generico" per diversi motivi:

  1. Può gestire numeri negativi, in particolare -1 (che il Framework utilizza per indicare una condizione di errore; per questo motivo viene utilizzato un int firmato ovunque sia richiesta l'indicizzazione, anche se i numeri negativi non sono significativi in ​​un contesto di indicizzazione).
  2. È abbastanza grande per servire la maggior parte degli scopi, mentre è abbastanza piccolo per essere utilizzato economicamente quasi ovunque.

Il motivo per utilizzare ints non firmati non è la leggibilità; sta avendo la capacità di ottenere la matematica fornita solo da un int senza segno.

Le clausole di guardia, i presupposti di validazione e contratto sono modi perfettamente accettabili per assicurare intervalli numerici validi. Di rado un intervallo numerico del mondo reale corrisponde esattamente a un numero compreso tra zero e 2 32 -1 (o qualunque sia l'intervallo numerico nativo del tipo numerico che hai scelto), quindi usare a uintper vincolare il contratto dell'interfaccia a numeri positivi è un po ' a parte il punto.


2
Bella risposta! Inoltre potrebbero esserci alcuni casi in cui un int senza segno può effettivamente inavvertitamente produrre più errori (anche se probabilmente quelli immediatamente individuati, ma un po 'confusi) - immagina di eseguire il ciclo inverso con un contatore int senza segno perché alcune dimensioni sono un numero intero: for (uint j=some_size-1; j >= 0; --j)- whoops ( non sono sicuro se questo è un problema in C #)! Ho riscontrato questo problema nel codice prima del quale ho provato a utilizzare int senza segno sul lato C il più possibile - e abbiamo finito per cambiarlo per favorirlo solo in intseguito, e le nostre vite sono state molto più facili con meno avvisi del compilatore.

14
"Raramente un intervallo numerico reale corrisponde a un numero compreso tra zero e 2 ^ 32-1." Nella mia esperienza, se hai bisogno di un numero maggiore di 2 ^ 31, molto probabilmente finirai per avere bisogno anche di numeri maggiori di 2 ^ 32, quindi potresti anche passare a (firmato) int64 su quel punto.
Mason Wheeler,

3
@Panzercrisis: è un po 'grave. Probabilmente sarebbe più accurato dire "Usa la intmaggior parte del tempo perché questa è la convenzione stabilita, ed è ciò che la maggior parte delle persone si aspetta di vedere di routine usata. Usa uintquando hai bisogno delle speciali capacità di a uint." Ricorda, i progettisti di Framework hanno deciso di seguire ampiamente questa convenzione, quindi non puoi nemmeno usarli uintin molti contesti Framework (non è compatibile con i tipi).
Robert Harvey,

2
@Panzercrisis Potrebbe essere un fraseggio troppo forte; ma non sono sicuro di aver mai usato tipi senza segno in C # tranne quando stavo chiamando a apis win32 (dove la convenzione è che costanti / flag / etc sono senza segno).
Dan Neely,

4
È davvero piuttosto raro. L'unica volta in cui utilizzo mai interi senza segno è in scenari di bit-twiddling.
Robert Harvey,

8

Generalmente, dovresti sempre utilizzare il tipo di dati più specifico per i tuoi dati possibili.

Se, ad esempio, si utilizza Entity Framework per estrarre i dati da un database, EF utilizzerà automaticamente il tipo di dati più vicino a quello utilizzato nel database.

Ci sono due problemi con questo in C #.
Innanzitutto, la maggior parte degli sviluppatori C # usa solo int, per rappresentare numeri interi (a meno che non ci sia un motivo per usare long). Ciò significa che altri sviluppatori non penseranno di controllare il tipo di dati, quindi otterranno gli errori di overflow sopra menzionati. La seconda, e la questione più critica, è / era che di .NET operatori aritmetici originali supportate solo int, uint, long, ulong, float, doppio, e decimal*. Questo è ancora il caso oggi (vedere la sezione 7.8.4 nelle specifiche del linguaggio C # 5.0 ). Puoi testarlo tu stesso usando il seguente codice:

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;

Il risultato del nostro byte- byteè un int( System.Int32).

Queste due questioni hanno dato origine alla pratica "usare solo int per numeri interi" che è così comune.

Quindi, per rispondere alla tua domanda, in C # di solito è una buona idea attenersi a intmeno che:

  • Un generatore di codice automatizzato utilizzava un valore diverso (come Entity Framework).
  • Tutti gli altri sviluppatori del progetto sono consapevoli del fatto che stai utilizzando i tipi di dati meno comuni (includi un commento che sottolinea che hai utilizzato il tipo di dati e perché).
  • I tipi di dati meno comuni sono già comunemente utilizzati nel progetto.
  • Il programma richiede i vantaggi del tipo di dati meno comune (hai 100 milioni di questi che devi conservare nella RAM, quindi la differenza tra a bytee an into a inte a longè critica, o le differenze aritmetiche di unsigned già menzionate).

Se è necessario eseguire calcoli matematici sui dati, attenersi ai tipi comuni.
Ricorda, puoi trasmettere da un tipo all'altro. Questo può essere meno efficiente dal punto di vista della CPU, quindi probabilmente stai meglio con uno dei 7 tipi comuni, ma è un'opzione se necessario.

Enumerations ( enum) è una delle mie eccezioni personali alle linee guida di cui sopra. Se ho solo alcune opzioni, specificherò l'enum come un byte o un corto. Se avessi bisogno di quell'ultimo bit in un enumerato contrassegnato, specificherò il tipo in uintmodo da poter usare hex per impostare il valore per il flag.

Se usi una proprietà con un codice che limita il valore, assicurati di spiegare nel tag di riepilogo quali sono le restrizioni e perché.

* Gli alias C # vengono utilizzati al posto dei nomi .NET come System.Int32poiché si tratta di una domanda C #.

Nota: c'era un blog o un articolo degli sviluppatori .NET (che non riesco a trovare), che sottolineava il numero limitato di funzioni aritmetiche e alcuni motivi per cui non si preoccupavano. Come ricordo, hanno indicato che non avevano in programma di aggiungere supporto per gli altri tipi di dati.

Nota: Java non supporta tipi di dati senza segno e in precedenza non supportava numeri interi a 8 o 16 bit. Poiché molti sviluppatori C # provenivano da un background Java o dovevano lavorare in entrambe le lingue, i limiti di una lingua venivano talvolta imposti artificialmente sull'altra.


La mia regola generale è semplicemente "usa int, a meno che tu non possa".
PerryC,

@PerryC Credo che sia la convenzione più comune. Il punto della mia risposta è stato quello di fornire una convenzione più completa che consenta di utilizzare le funzionalità del linguaggio.
Trisped

6

Devi principalmente conoscere due cose: i dati che stai rappresentando e qualsiasi passaggio intermedio nei tuoi calcoli.

Ha certamente senso avere l'età unsigned int, perché di solito non consideriamo le età negative. Ma poi accenni di sottrarre un'età all'altra. Se sottraiamo ciecamente un intero da un altro, allora è sicuramente possibile ottenere un numero negativo, anche se in precedenza avevamo concordato che le età negative non hanno senso. Quindi in questo caso vorresti che il tuo calcolo fosse fatto con un intero con segno.

Per quanto riguarda se i valori non firmati sono cattivi o meno, direi che è una grande generalizzazione dire che i valori non firmati sono cattivi. Java non ha valori senza segno, come hai detto, e mi infastidisce costantemente. A bytepuò avere un valore compreso tra 0-255 o 0x00-0xFF. Ma se si desidera creare un'istanza di un byte maggiore di 127 (0x7F), è necessario scriverlo come numero negativo o eseguire il cast di un numero intero su un byte. Si finisce con un codice simile al seguente:

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

Quanto sopra mi infastidisce senza fine. Non mi è permesso avere un byte con un valore di 197, anche se è un valore perfettamente valido per la maggior parte delle persone sane che si occupano di byte. Posso lanciare il numero intero o posso trovare il valore negativo (197 == -59 in questo caso). Considera anche questo:

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

Come puoi vedere, aggiungendo due byte con valori validi e finendo con un byte con un valore valido, si finisce per cambiare il segno. Non solo, ma non è immediatamente ovvio che 70 + 80 == -106. Tecnicamente si tratta di un overflow, ma nella mia mente (come essere umano) un byte non dovrebbe overflow per valori inferiori a 0xFF. Quando faccio un po 'di aritmetica su carta, non considero l'ottavo bit come un segno.

Lavoro con molti numeri interi a livello di bit e avere tutto da firmare di solito rende tutto meno intuitivo e più difficile da gestire, perché devi ricordare che lo spostamento a destra di un numero negativo ti dà nuove 1s nel tuo numero. Mentre spostare a destra un numero intero senza segno non lo fa mai. Per esempio:

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;

Aggiunge solo ulteriori passaggi che ritengo non debbano essere necessari.

Mentre ho usato bytesopra, lo stesso vale per gli interi a 32 e 64 bit. Non avere unsignedè paralizzante e mi sorprende il fatto che ci siano linguaggi di alto livello come Java che non li permettono affatto. Ma per la maggior parte delle persone questo non è un problema, perché molti programmatori non si occupano dell'aritmetica a livello di bit.

Alla fine, è utile usare numeri interi senza segno se li consideri come bit ed è utile utilizzare numeri interi con segno quando li consideri come numeri.


7
Condivido la tua frustrazione per le lingue senza tipi integrali non firmati (specialmente per i byte) ma temo che questa non sia una risposta diretta alla domanda qui posta. Forse potresti aggiungere una conclusione, che credo, potrebbe essere: "Usa numeri interi senza segno se stai pensando al loro valore come bit e numeri interi con segno se pensi a loro come numeri."
5gon12eder

1
è quello che ho detto in un commento sopra. felice di vedere qualcun altro pensare allo stesso modo.
robert bristow-johnson il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.