Werden Codeverträge garantiert ausgewertet, bevor verkettete Konstruktoren aufgerufen werden?

8

Bevor ich Code Contracts verwendet habe, bin ich bei der Verwendung von Konstruktorverkettungen manchmal auf die Fehlerhaftigkeit der Parametervalidierung gestoßen.

Dies lässt sich am einfachsten mit einem (erfundenen) Beispiel erklären:

%Vor%

Ich möchte, dass der Test(string) -Konstruktor den Test(int) -Konstruktor verkettet, und dazu verwende ich int.Parse() .

Natürlich gefällt es int.Parse() nicht, ein Null-Argument zu haben. Wenn also s null ist, wird es geworfen, bevor ich die Validierungszeilen erreiche:

%Vor%

was diese Überprüfung unbrauchbar macht.

Wie behebt man das? Nun, manchmal habe ich das gemacht:

%Vor%

Das ist ein bisschen fummelig, und der Stack-Trace ist nicht ideal, wenn er fehlschlägt, aber es funktioniert.

Jetzt kommen Code Contracts, also benutze ich sie:

%Vor%

Alles gut und gut. Es funktioniert gut. Aber dann entdecke ich, dass ich das tun kann:

%Vor%

Und wenn ich var test = new Test(null) mache, wird Contract.Requires(s != null) vor this(int.Parse(s)) ausgeführt. Das bedeutet, dass ich den convertArg() -Test ganz weglassen kann!

Also zu meinen aktuellen Fragen:

  • Ist dieses Verhalten irgendwo dokumentiert?
  • Kann ich mich auf dieses Verhalten verlassen, wenn ich Codeverträge für verkettete Konstruktoren wie diese schreibe?
  • Gibt es eine andere Art, wie ich mich diesem nähern sollte?
Matthew Watson 12.06.2013, 13:59
quelle

1 Antwort

7

Die kurze Antwort

Ja, das Verhalten ist in der Definition von "precondition" dokumentiert, und in der Art und Weise, wie Legacy Verification (if / then / throw) ohne Aufruf von Contract.EndContractBlock behandelt wird.

Wenn Sie Contract.Requires nicht verwenden möchten, können Sie Ihren Konstruktor in

ändern %Vor%

Die lange Antwort

Wenn Sie einen Contract.* -Aufruf in Ihren Code einfügen, rufen Sie nicht tatsächlich ein Mitglied im System.Diagnostics.Contracts -Namespace auf. Zum Beispiel ist Contract.Requires(bool) wie folgt definiert:

%Vor%

AssertMustUseRewriter gibt bedingungslos eine ContractException aus. Wenn die kompilierte Binärdatei nicht überschrieben wird, stürzt der Code einfach ab, wenn CONTRACTS_FULL definiert ist. Wenn es nicht definiert ist, wird die Vorbedingung niemals überprüft, da der Aufruf von Requires vom C # -Compiler aufgrund des Vorhandenseins von Attribut [Conditional] .

Der Rewriter

Ausgehend von den in den Projekteigenschaften ausgewählten Einstellungen definiert Visual Studio CONTRACTS_FULL und ruft ccrewrite auf, um die entsprechende IL zu generieren, um die Verträge zur Laufzeit zu überprüfen.

Beispielvertrag:

%Vor%

Kompiliert mit csc program.cs /out:nocontract.dll , erhalten Sie:

%Vor%

Kompiliert mit csc program.cs /define:CONTRACTS_FULL /out:prerewrite.dll und durchlaufen ccrewrite -assembly prerewrite.dll -out postrewrite.dll erhalten Sie den Code, der tatsächlich Laufzeitprüfung durchführt:

%Vor%

Von vorrangigem Interesse ist, dass unser Ensures (eine Nachbedingung) an den unteren Rand der Methode gerückt wurde und unser Requires (eine Voraussetzung) nicht wirklich verschoben wurde, da es bereits an der Spitze der Methode war.

Dies passt zu der Definition der Dokumentation :

  

[Vorbedingungen] sind Verträge über den Zustand der Welt, wenn eine Methode aufgerufen wird.
  ...
  Nachbedingungen sind Verträge über den Zustand einer Methode beim Beenden. Mit anderen Worten, die Bedingung wird kurz vor dem Beenden einer Methode überprüft.

Nun besteht die Komplexität in Ihrem Szenario bereits in der Definition einer Vorbedingung. Ausgehend von der oben aufgeführten Definition wird die Vorbedingung ausgeführt, bevor die Methode ausgeführt wird. Das Problem ist, dass die C # -Spezifikation besagt, dass der Konstruktorinitialisierer (verketteter Konstruktor) unmittelbar vor dem Konstruktorkörper aufgerufen werden muss. [CSHARP 10.11.1] , was im Widerspruch zur Definition einer Vorbedingung steht.

Magic lebt hier

Der Code, den ccrewrite generiert, kann daher nicht als C # dargestellt werden, da die Sprache keinen Mechanismus zum Ausführen von Code vor dem verketteten Konstruktor bietet (außer durch Aufrufen von statischen Methoden in der Liste der verketteten Konstruktorparameter wie erwähnt). ccrewrite , wie von der Definition gefordert, nimmt Ihren Konstruktor

%Vor%

welches als

kompiliert wird

und verschiebt den Aufruf zu Requests vor dem Aufruf des verketteten Konstruktors:

Was bedeutet ...

Um zu vermeiden, dass Sie bei der Argumentvalidierung auf statische Methoden zurückgreifen müssen, verwenden Sie den Contract Rewriter. Sie können den Rewriter mit Contract.Requires aufrufen oder indem Sie angeben, dass ein Codeblock Voraussetzung ist, indem Sie ihn mit Contract.EndContractBlock(); beenden. Dadurch wird der Rewriter vor dem Aufruf des Konstruktorinitialisierers an den Anfang der Methode gesetzt.

    
Mitch 15.02.2014, 00:18
quelle