2013-08-18 11 views
22

Ich denke, das sieht nach einem Fehler im C# -Compiler aus.Warum wertet der Compiler den Rest MinValue% -1 anders als die Laufzeit aus?

Betrachten Sie diesen Code (innerhalb einer Methode):

const long dividend = long.MinValue; 
const long divisor = -1L; 
Console.WriteLine(dividend % divisor); 

Es ohne Fehler kompiliert (oder Warnungen). Scheint wie ein Fehler. Wenn ausgeführt, druckt 0 auf der Konsole.

Dann ohne const, der Code:

long dividend = long.MinValue; 
long divisor = -1L; 
Console.WriteLine(dividend % divisor); 

Wenn dies ausgeführt wird, es ergibt sich korrekt in einem OverflowException geworfen.

Die C# -Sprachbeschreibung erwähnt diesen Fall speziell und sagt, dass ein System.OverflowException geworfen werden soll. Es hängt nicht vom Kontext checked oder unchecked es scheint (auch der Fehler mit der Kompilierzeit Konstante Operanden zum Rest-Operator ist der gleiche mit checked und unchecked).

Derselbe Fehler passiert mit int (System.Int32), nicht nur long (System.Int64).

Zum Vergleich behandelt der Compiler dividend/divisor mit const Operanden viel besser als dividend % divisor.

Meine Fragen:

Bin ich dieses Recht ist ein Bug? Wenn ja, ist es ein bekannter Fehler, den sie nicht beheben möchten (wegen der Abwärtskompatibilität, auch wenn es ziemlich albern ist, % -1 mit einer Kompilierzeitkonstante -1 zu verwenden)? Oder sollten wir es melden, damit sie es in zukünftigen Versionen des C# -Compilers beheben können?

+0

Erwähnen @EricLippert könnte die richtige Menge für diese Frage zu ziehen :) –

+0

@Morten, an dieser Stelle könnte er gerade verwirrt von seinem Sitz in Coverity starren. ;) –

+0

Ich denke du solltest ein Kopfgeld darauf legen, da es mich irritiert warum das passiert. Die Spezifikationen sagen, dass jeder konstante Ausdruck, der eine Laufzeitausnahme verursachen könnte, einen Kompilierungsfehler bei der Kompilierung verursachen sollte !! –

Antwort

19

Dieser Corner-Case wird im Compiler sehr spezifisch angesprochen.Die meisten relevante Kommentare und Code in der Roslyn source:

// Although remainder and division always overflow at runtime with arguments int.MinValue/long.MinValue and -1  
// (regardless of checked context) the constant folding behavior is different.  
// Remainder never overflows at compile time while division does.  
newValue = FoldNeverOverflowBinaryOperators(kind, valueLeft, valueRight); 

Und:

// MinValue % -1 always overflows at runtime but never at compile time  
case BinaryOperatorKind.IntRemainder: 
    return (valueRight.Int32Value != -1) ? valueLeft.Int32Value % valueRight.Int32Value : 0; 
case BinaryOperatorKind.LongRemainder: 
    return (valueRight.Int64Value != -1) ? valueLeft.Int64Value % valueRight.Int64Value : 0; 

Auch das Verhalten der Legacy-C++ Version des Compilers, gehen den ganzen Weg zurück zu Version 1. Vom SSCLI v1. 0 Verteilung, clr/src/csharp/sccomp/fncbind.cpp Quelldatei:

case EK_MOD: 
    // if we don't check this, then 0x80000000 % -1 will cause an exception... 
    if (d2 == -1) { 
     result = 0; 
    } else { 
     result = d1 % d2; 
    } 
    break; 

So Schluss zu ziehen, dass dies etwa nicht übersehen oder vergessen, zumindest von den Programmierern, dass w Orked auf dem Compiler, könnte es möglicherweise als ungenügend präzise Sprache in der C# -Sprachspezifikation qualifiziert werden. Mehr zu den Laufzeitproblemen, die durch diesen Killer verursacht werden, stochern in this post.

4

Ich denke, es ist kein Fehler; Es ist eher, wie C# Compiler berechnet % (Es ist eine Vermutung). Es scheint, dass der C# -Compiler zuerst % für positive Zahlen berechnet und dann das Zeichen anwendet. Mit Abs(long.MinValue + 1) == Abs(long.MaxValue) wenn wir schreiben:

static long dividend = long.MinValue + 1; 
static long divisor = -1L; 
Console.WriteLine(dividend % divisor); 

Jetzt werden wir 0 als die Antwort sehen, die da jetzt richtig ist Abs(dividend) == Abs(long.MaxValue), die in Reichweite befindet.

Warum funktioniert es, wenn wir es als const Wert dann deklarieren? (Erneut eine Vermutung) Es scheint, dass C# -Compiler tatsächlich den Ausdruck zur Kompilierzeit berechnet und den Typ der Konstante nicht berücksichtigt und darauf als BigInteger oder etwas (Bug?) Reagiert. Denn wenn wir eine Funktion wie erklären:

static long Compute(long l1, long l2) 
{ 
    return l1 % l2; 
} 

Und nennen Console.WriteLine(Compute(dividend, divisor)); werden wir die gleiche Ausnahme erhalten. Und wieder, wenn wir die Konstante wie folgt deklarieren:

const long dividend = long.MinValue + 1; 

Wir würden nicht die Ausnahme bekommen.

+1

Ich wusste das alles schon. Beachten Sie, dass in der Spezifikation Folgendes steht: _Das Ergebnis von 'x% y' ist der Wert, der von' x - (x/y) * y' erzeugt wird. Wenn "y" gleich Null ist, wird eine 'System.DivideByZeroException' ausgelöst. ↵↵ Wenn der linke Operand der kleinste 'int' oder' long' Wert und der rechte Operand '-1' ist, wird eine' System.OverflowException' geworfen. [...] Es ist aus Ihren Beobachtungen (und meiner) offensichtlich, dass der Compiler der Spezifikation nicht folgt, wenn der Rest zur Kompilierungszeit berechnet wird. Die Laufzeit folgt der Spezifikation. –

+0

Ich entschuldige mich; Ich habe die Spezifikation nicht gelesen. Ja; Ich sah es jetzt auch in meiner Antwort auf "handelt darauf als BigInteger oder sowas (Bug?)". Du hast Recht. –