Sollte ich vermeiden, Int ohne Vorzeichen in C # zu verwenden?

23

Ich habe kürzlich über die Verwendung von Ganzzahlen ohne Vorzeichen in C # nachgedacht (und ich denke, ein ähnliches Argument kann für andere "Hochsprachen" angeführt werden).

Wenn ich eine Ganzzahl benötige, bin ich normalerweise nicht mit dem Dilemma der Größe einer Ganzzahl konfrontiert. Ein Beispiel wäre eine Alterseigenschaft einer Personenklasse (aber die Frage ist nicht auf Eigenschaften beschränkt). In diesem Sinne gibt es meines Erachtens nur einen Vorteil der Verwendung einer Ganzzahl ohne Vorzeichen ("uint") gegenüber einer Ganzzahl mit Vorzeichen ("int") - die Lesbarkeit. Wenn ich die Idee zum Ausdruck bringen möchte, dass ein Alter nur positiv sein kann, kann ich dies erreichen, indem ich den Alterstyp auf uint setze.

Andererseits können Berechnungen mit Ganzzahlen ohne Vorzeichen zu Fehlern aller Art führen, und es ist schwierig, Operationen wie das Subtrahieren von zwei Zeitaltern durchzuführen. (Ich habe gelesen, dass dies einer der Gründe ist, warum Java vorzeichenlose Ganzzahlen weggelassen hat.)

Im Falle von C # kann ich auch denken, dass eine Schutzklausel für den Setter eine Lösung wäre, die das Beste aus zwei Welten bietet, aber dies wäre nicht anwendbar, wenn ich zum Beispiel ein Alter an eine Methode weitergeben würde. Eine Problemumgehung wäre, eine Klasse mit dem Namen Age zu definieren und das Eigenschaftsalter als einziges Element anzugeben. Dieses Muster würde jedoch dazu führen, dass ich viele Klassen erstelle, was Verwirrung stiftet (andere Entwickler würden nicht wissen, wann ein Objekt nur ein Wrapper ist und wenn es etwas mehr sofisticaded ist).

Was sind einige allgemeine Best Practices in Bezug auf dieses Problem? Wie soll ich mit einem solchen Szenario umgehen?

Belgi
quelle
1
Darüber hinaus ist unsigned int nicht CLS-kompatibel. Dies bedeutet, dass Sie keine APIs aufrufen können, die sie aus anderen .NET-Sprachen verwenden.
Nathan Cooper
2
@NathanCooper: ... „können APIs nicht nennen , die sie von verwenden einigen anderen Sprachen“. Die Metadaten für sie sind standardisiert, sodass alle .NET-Sprachen, die nicht signierte Typen unterstützen, problemlos zusammenarbeiten können.
Ben Voigt
5
Um auf Ihr spezielles Beispiel einzugehen, hätte ich überhaupt keine Eigenschaft namens Age. Ich würde eine Eigenschaft namens Birthday oder CreationTime oder was auch immer haben und das Alter daraus berechnen.
Eric Lippert
2
"... aber dieses Muster hätte mich veranlasst, viele Klassen zu erstellen und würde Verwirrung stiften." Eigentlich ist das die richtige Vorgehensweise. Suche einfach nach dem berüchtigten Primitive Obsession- Anti-Muster.
Songo

Antworten:

23

Die Designer von .NET Framework haben aus mehreren Gründen eine 32-Bit-Ganzzahl mit Vorzeichen als "Allzwecknummer" ausgewählt:

  1. Es kann negative Zahlen verarbeiten, insbesondere -1 (die vom Framework verwendet werden, um einen Fehlerzustand anzuzeigen. Aus diesem Grund wird überall dort ein vorzeichenbehafteter int verwendet, wo eine Indizierung erforderlich ist, obwohl negative Zahlen in einem Indizierungskontext keine Bedeutung haben).
  2. Es ist groß genug, um den meisten Zwecken zu dienen, und klein genug, um praktisch überall wirtschaftlich eingesetzt zu werden.

Der Grund für die Verwendung von nicht signierten Ints ist nicht lesbar . Es hat die Fähigkeit, die Mathematik zu erhalten, die nur ein Int ohne Vorzeichen liefert.

Schutzklauseln, Validierung und Vertragsvoraussetzungen sind absolut akzeptable Möglichkeiten, um gültige Zahlenbereiche zu gewährleisten. Selten entspricht ein realer numerischer Bereich genau einer Zahl zwischen 0 und 2 32 -1 (oder was auch immer der native numerische Bereich des von Ihnen gewählten numerischen Typs ist). Die Verwendung von a uint, um Ihren Schnittstellenvertrag auf positive Zahlen zu beschränken, ist also eine Art von neben dem Fakt.

Robert Harvey
quelle
2
Gute Antwort! Es kann auch Fälle geben, in denen ein vorzeichenloses int tatsächlich versehentlich mehr Fehler erzeugt (obwohl diese wahrscheinlich sofort entdeckt werden, aber etwas verwirrend sind). Stellen Sie sich vor, Sie for (uint j=some_size-1; j >= 0; --j)wiederholen die Schleife mit einem vorzeichenlosen int-Zähler, da manche Größe eine ganze Zahl ist: whoops ( Ich bin mir nicht sicher, ob dies ein Problem in C # ist. Ich habe dieses Problem im Code gefunden, in dem versucht wurde, unsigniertes int auf der C-Seite so weit wie möglich zu verwenden - und wir haben es letztendlich geändert, um es später zu bevorzugen int, und unser Leben war viel einfacher, da weniger Compiler-Warnungen ausgegeben wurden.
14
"Ein realer Zahlenbereich entspricht selten einer Zahl zwischen Null und 2 ^ 32-1." Wenn Sie eine Zahl größer als 2 ^ 31 benötigen, werden Sie meiner Erfahrung nach sehr wahrscheinlich auch Zahlen größer als 2 ^ 32 benötigen, also können Sie genauso gut auf (signierte) int64 um aufsteigen dieser Punkt.
Mason Wheeler
3
@Panzercrisis: Das ist ein bisschen schwerwiegend. Es wäre wahrscheinlich genauer zu sagen: "Verwenden Sie die intmeiste Zeit, da dies die etablierte Konvention ist und die meisten Leute erwarten, dass sie routinemäßig verwendet werden. Verwenden uintSie diese Konvention, wenn Sie die besonderen Fähigkeiten von a benötigen uint." Denken Sie daran, dass die Framework-Designer beschlossen haben, diese Konvention weitgehend einzuhalten, sodass Sie sie nicht einmal uintin vielen Framework-Kontexten verwenden können (sie ist nicht typkompatibel).
Robert Harvey
2
@Panzercrisis Es könnte eine zu starke Formulierung sein; Ich bin mir jedoch nicht sicher, ob ich jemals nicht signierte Typen in C # verwendet habe, es sei denn, ich habe win32 apis aufgerufen (wobei die Konvention lautet, dass Konstanten / Flags / etc nicht signiert sind).
Dan Neely
4
Es ist in der Tat ziemlich selten. Das einzige Mal, dass ich unsignierte Ints verwende, sind Bit-Twiddling-Szenarien.
Robert Harvey
8

Im Allgemeinen sollten Sie für Ihre Daten immer den spezifischsten Datentyp verwenden.

Wenn Sie beispielsweise Entity Framework zum Abrufen von Daten aus einer Datenbank verwenden, verwendet EF automatisch den Datentyp, der dem in der Datenbank verwendeten am nächsten kommt.

Es gibt zwei Probleme damit in C #.
Erstens verwenden die meisten C # -Entwickler nur intdie Darstellung ganzer Zahlen (es sei denn, es gibt einen Grund für die Verwendung long). Dies bedeutet, dass andere Entwickler nicht daran denken, den Datentyp zu überprüfen, und daher die oben genannten Überlauffehler erhalten. Die zweite und kritischere Problem ist / war , dass .NET die ursprünglichen arithmetischen Operatoren nur unterstützt int, uint, long, ulong, float, Doppel-, und decimal*. Dies ist auch heute noch der Fall (siehe Abschnitt 7.8.4 in der C # 5.0-Sprachspezifikation ). Sie können dies mit dem folgenden Code selbst testen:

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;

Das Ergebnis unseres byte- byteist ein int( System.Int32).

Diese beiden Probleme führten zu der so verbreiteten Praxis, "nur int für ganze Zahlen zu verwenden".

Um Ihre Frage zu beantworten, ist es in C # normalerweise eine gute Idee, sich an intFolgendes zu halten:

  • Ein automatisierter Codegenerator verwendete einen anderen Wert (wie Entity Framework).
  • Allen anderen Entwicklern im Projekt ist bekannt, dass Sie die selteneren Datentypen verwenden (einschließlich eines Kommentars, in dem darauf hingewiesen wird, dass Sie den Datentyp verwendet haben und warum).
  • Die weniger gebräuchlichen Datentypen werden im Projekt bereits häufig verwendet.
  • Das Programm benötigt die Vorteile des weniger verbreiteten Datentyps (100 Millionen davon müssen im RAM gespeichert werden, sodass der Unterschied zwischen a byteund an intoder an intund a longkritisch ist oder die arithmetischen Unterschiede der bereits erwähnten vorzeichenlosen Datentypen).

Wenn Sie mit den Daten rechnen müssen, halten Sie sich an die gebräuchlichen Typen.
Denken Sie daran, dass Sie von einem Typ in einen anderen umwandeln können. Dies kann unter CPU-Gesichtspunkten weniger effizient sein, sodass Sie wahrscheinlich mit einem der 7 gängigen Typen besser zurechtkommen. Bei Bedarf ist dies jedoch eine Option.

Enumerations ( enum) ist eine meiner persönlichen Ausnahmen zu den oben genannten Richtlinien. Wenn ich nur einige Optionen habe, gebe ich die Enumeration als Byte oder Kurzform an. Wenn ich das letzte Bit in einer markierten Aufzählung benötige, gebe ich den Typ an, uintdamit ich hex verwenden kann, um den Wert für die Markierung festzulegen .

Wenn Sie eine Eigenschaft mit Code verwenden, der den Wert einschränkt, müssen Sie im Zusammenfassungstag erläutern, welche Einschränkungen bestehen und warum.

* C # -Aliasnamen werden anstelle von .NET-Namen verwendet, System.Int32da dies eine C # -Frage ist.

Hinweis: Es gab ein Blog oder einen Artikel der .NET-Entwickler (den ich nicht finden kann), in dem auf die begrenzte Anzahl von Rechenfunktionen und einige Gründe hingewiesen wurde, warum sie sich keine Gedanken darüber machten. Wie ich mich erinnere, gaben sie an, dass sie keine Pläne hatten, Unterstützung für die anderen Datentypen hinzuzufügen.

Hinweis: Java unterstützt keine vorzeichenlosen Datentypen und hatte zuvor keine Unterstützung für 8- oder 16-Bit-Ganzzahlen. Da viele C # -Entwickler einen Java-Hintergrund hatten oder in beiden Sprachen arbeiten mussten, wurden die Einschränkungen einer Sprache manchmal der anderen künstlich auferlegt.

Trisped
quelle
Meine allgemeine Faustregel lautet einfach "use int, sofern Sie nicht können".
PerryC
@PerryC Ich glaube, das ist die häufigste Konvention. In meiner Antwort ging es darum, eine vollständigere Konvention bereitzustellen, mit der Sie die Sprachfunktionen verwenden können.
Trisped
6

Sie müssen sich hauptsächlich zweier Dinge bewusst sein: der Daten, die Sie darstellen, und etwaiger Zwischenschritte bei Ihren Berechnungen.

Es ist sicherlich sinnvoll, Alter zu haben unsigned int, da wir in der Regel kein negatives Alter berücksichtigen. Aber dann erwähnen Sie das Subtrahieren eines Alters von einem anderen. Wenn wir nur blind eine ganze Zahl von einer anderen subtrahieren, ist es definitiv möglich, dass wir eine negative Zahl erhalten, auch wenn wir uns zuvor darauf geeinigt haben, dass negative Alter keinen Sinn ergeben. In diesem Fall möchten Sie also, dass Ihre Berechnung mit einer Ganzzahl mit Vorzeichen durchgeführt wird.

In Bezug darauf, ob Werte ohne Vorzeichen schlecht sind oder nicht, würde ich sagen, dass es eine große Verallgemeinerung ist, zu sagen, dass Werte ohne Vorzeichen schlecht sind. Java hat keine vorzeichenlosen Werte, wie Sie erwähnt haben, und es nervt mich ständig. A bytekann einen Wert von 0-255 oder 0x00-0xFF haben. Wenn Sie jedoch ein Byte größer als 127 (0x7F) instanziieren möchten, müssen Sie es entweder als negative Zahl schreiben oder eine Ganzzahl in ein Byte umwandeln. Am Ende haben Sie Code, der so aussieht:

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

Das oben Genannte ärgert mich ohne Ende. Ich darf nicht, dass ein Byte einen Wert von 197 hat, obwohl das für die meisten vernünftigen Leute, die mit Bytes zu tun haben, ein vollkommen gültiger Wert ist. Ich kann die ganze Zahl umwandeln oder den negativen Wert finden (197 == -59 in diesem Fall). Beachten Sie auch Folgendes:

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

Wie Sie sehen, ändert sich das Vorzeichen, wenn Sie zwei Bytes mit gültigen Werten hinzufügen und am Ende ein Byte mit einem gültigen Wert erhalten. Nicht nur das, aber es ist nicht sofort offensichtlich, dass 70 + 80 == -106. Technisch ist dies ein Überlauf, aber in meinen Augen (als Mensch) sollte ein Byte für Werte unter 0xFF nicht überlaufen. Wenn ich auf Papier ein bisschen rechne, halte ich das achte Bit nicht für ein Vorzeichen.

Ich arbeite mit vielen ganzen Zahlen auf der Bit-Ebene, und wenn alles signiert ist, ist normalerweise alles weniger intuitiv und schwieriger zu handhaben, da Sie sich daran erinnern müssen, dass Sie durch Verschieben einer negativen Zahl nach rechts neue 1s in Ihrer Zahl erhalten. Während die Rechtsverschiebung einer vorzeichenlosen ganzen Zahl dies niemals tut. Beispielsweise:

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;

Es werden nur zusätzliche Schritte hinzugefügt, die meines Erachtens nicht notwendig sein sollten.

Während ich byteoben verwendet habe, gilt das gleiche für 32-Bit- und 64-Bit-Ganzzahlen. Nicht haben unsignedist lähmend und es schockiert mich, dass es Hochsprachen wie Java gibt, die sie überhaupt nicht zulassen. Für die meisten Menschen ist dies jedoch kein Problem, da sich viele Programmierer nicht mit Bit-Level-Arithmetik befassen.

Am Ende ist es nützlich, vorzeichenlose Ganzzahlen zu verwenden, wenn Sie sie als Bits betrachten, und es ist nützlich, vorzeichenbehaftete Ganzzahlen zu verwenden, wenn Sie sie als Zahlen betrachten.

Shaz
quelle
7
Ich teile Ihre Frustration über Sprachen ohne vorzeichenlose ganzzahlige Typen (insbesondere für Bytes), befürchte jedoch, dass dies keine direkte Antwort auf die hier gestellte Frage ist. Vielleicht könnten Sie eine Schlussfolgerung hinzufügen, von der ich glaube, dass sie
lautet
1
es ist das, was ich oben in einem Kommentar gesagt habe. froh zu sehen, dass jemand anders genauso denkt.
Robert Bristow-Johnson