Erzwingt einen "End" -Ruf, wenn ein entsprechender "Start" -Aufruf erfolgt

7

Nehmen wir an, ich möchte eine Regel erzwingen:

  

Jedes Mal, wenn Sie in Ihrer Funktion "StartJumping ()" aufrufen, müssen Sie vor der Rückkehr "EndJumping ()" aufrufen.

Wenn ein Entwickler seinen Code schreibt, vergessen sie vielleicht einfach, EndSomething anzurufen - also möchte ich es leicht merken.

Ich kann mir nur eine Möglichkeit vorstellen: und es missbraucht das "using" -Schlüsselwort:

%Vor%

Gibt es einen besseren Weg, dies zu tun?

    
Jeff Meatball Yang 27.04.2010, 21:56
quelle

12 Antworten

9

Ich stimme Eric nicht zu: Wann das zu tun ist, hängt von den Umständen ab. An einem Punkt habe ich meine große Codebasis überarbeitet, um eine Semantik für das Annehmen / Freigeben von allen Zugriffen auf eine benutzerdefinierte Bildklasse aufzunehmen. Bilder wurden ursprünglich in unbeweglichen Speicherblöcken zugewiesen, aber wir hatten nun die Möglichkeit, die Bilder in Blöcke zu setzen, die verschoben werden durften, wenn sie nicht erfasst worden waren. In meinem Code ist es ein schwerwiegender Fehler für einen Speicherblock, nachdem er nicht gesperrt ist.

Daher ist es wichtig, dies durchzusetzen. Ich habe diese Klasse erstellt:

%Vor%

was mir erlaubt, diese Unterklasse zu erstellen:

%Vor%

Was mich wiederum diesen Code schreiben lässt:

%Vor%

Das macht die Arbeit, die ich brauche, und eine schnelle Suche sagt mir, dass ich sie an 186 Stellen brauchte, von denen ich garantieren kann, dass sie nie wieder fehlschlagen werden. Und ich muss in der Lage sein, diese Garantie zu geben - sonst könnte ich einen großen Teil des Speichers im Heap meines Kunden einfrieren. Ich kann das nicht tun.

Aber in einem anderen Fall, in dem ich mit der Verschlüsselung von PDF-Dokumenten arbeite, werden alle Strings und Streams in PDF-Wörterbüchern verschlüsselt, außer wenn dies nicht der Fall ist. Ja wirklich. Es gibt eine kleine Anzahl von Randfällen, bei denen es falsch ist, die Wörterbücher zu verschlüsseln oder zu entschlüsseln, also während ich ein Objekt austrete, mache ich das:

%Vor%

Warum habe ich das über den RAII-Ansatz gewählt? Nun, jede Ausnahme, die im ... mehr Code passiert ... bedeutet, dass du tot im Wasser bist. Es gibt keine Wiederherstellung. Es kann keine Erholung geben. Sie müssen von Anfang an von vorne anfangen und das Kontextobjekt muss rekonstruiert werden, so dass der Zustand sowieso abgespritzt wird. Und im Vergleich dazu musste ich diesen Code nur 4 mal machen - die Möglichkeit für Fehler ist viel kleiner als im Speichersperrcode, und wenn ich einen in der Zukunft vergesse, wird das generierte Dokument sofort gebrochen (fail schnell).

Wählen Sie also RAII, wenn Sie unbedingt den geklammerten Anruf haben müssen und nicht scheitern können. Ärgere dich nicht mit RAII, wenn es sonst einfach ist.

    
plinth 28.04.2010, 14:53
quelle
13

Im Wesentlichen ist das Problem:

  • Ich habe einen globalen Status ...
  • und ich möchte diesen globalen Staat mutieren ...
  • aber ich möchte sicherstellen, dass ich es zurückmache.

Sie haben festgestellt, dass es schmerzt, wenn Sie das tun . Mein Rat ist eher, als zu versuchen, einen Weg zu finden, ihn weniger weh zu tun, zu versuchen, einen Weg zu finden, das schmerzhafte Ding überhaupt nicht zu tun.

Ich bin mir bewusst, wie schwer das ist. Als wir in v3 Lambda zu C # hinzufügten, hatten wir ein großes Problem. Berücksichtigen Sie Folgendes:

%Vor%

Wie um alles in der Welt binden wir das erfolgreich? Nun, was wir tun, ist beides zu versuchen (x ist int, oder x ist eine Zeichenkette) und zu sehen, was, wenn überhaupt, uns einen Fehler gibt. Diejenigen, die keine Fehler geben, werden Kandidaten für die Überladungsauflösung.

Das Fehlerberichtsmodul im Compiler ist globaler Status . In C # 1 und 2 gab es nie eine Situation, in der wir sagen mussten: "Binden Sie diesen gesamten Methodenkörper, um zu bestimmen, ob er Fehler hatte, aber melden Sie die Fehler nicht ". Schließlich wollen Sie in diesem Programm nicht den Fehler "int hat keine Eigenschaft namens Length" erhalten, Sie möchten, dass es das entdeckt, notiert und nicht meldet .

Was ich getan habe, war genau das, was Sie getan haben. Beginnen Sie, die Fehlerberichterstattung zu unterdrücken, aber vergessen Sie nicht, die Fehlerberichterstattung zu unterdrücken.

Es ist schrecklich. Was wir wirklich tun sollten, ist den Compiler so neu zu gestalten, dass Fehler Ausgabe des semantischen Analysators sind, nicht globaler Zustand des Compilers . Es ist jedoch schwierig, das durch Hunderttausende von Zeilen bestehenden Codes einzufädeln, der von diesem globalen Status abhängt.

Wie auch immer, etwas anderes zum Nachdenken. Ihre "using" -Lösung hat den Effekt, dass der Sprung beendet wird, wenn eine Ausnahme ausgelöst wird. Ist das richtig? Vielleicht nicht. Schließlich wurde eine unerwartete, nicht behandelte Ausnahme ausgelöst. Das gesamte System könnte massiv instabil sein. Keiner Ihrer internen Statusinvarianten kann in diesem Szenario tatsächlich invariant sein.

Sieh es so an: Ich mutierte den globalen Staat. Ich habe dann eine unerwartete, unbehandelte Ausnahme bekommen. Ich weiß, ich werde den globalen Zustand wieder verändern! Das wird helfen! Scheint wie eine sehr, sehr schlechte Idee.

Natürlich hängt es davon ab, was die Mutation zum globalen Staat ist. Wenn es "beginnt, dem Benutzer erneut Fehler zu melden", wie es im Compiler der Fall ist, dann sollte die richtige für eine unbehandelte Ausnahme beginnen, Fehler an den Benutzer erneut zu melden: Sie müssen den Fehler melden, dass der Compiler gerade eine unbehandelte Ausnahme hatte!

Wenn andererseits die Mutation zum globalen Zustand "die Ressource freischalten und zulassen, dass sie von nicht vertrauenswürdigem Code beobachtet und verwendet wird", dann ist es möglicherweise eine SEHR SCHLECHTE IDEE, sie automatisch zu entsperren. Diese unerwartete, nicht behandelte Ausnahme könnte ein Hinweis auf einen Angriff auf Ihren Code sein, der von einem Angreifer erwartet wird, der hofft, den Zugriff auf den globalen Status jetzt in einer anfälligen, inkonsistenten Form zu ermöglichen.

    
Eric Lippert 27.04.2010 22:42
quelle
8

Wenn Sie eine Bereichsoperation steuern möchten, würde ich eine Methode hinzufügen, die ein Action<Jumper> benötigt, um die erforderlichen Operationen auf der Jumper-Instanz zu enthalten:

%Vor%     
Lee 27.04.2010 22:04
quelle
6

Ein alternativer Ansatz, der unter bestimmten Umständen (d. h. wenn die Aktionen am Ende alle stattfinden können) funktionieren würde, wäre eine Reihe von Klassen mit einer fließenden Schnittstelle und einer abschließenden Execute () -Methode zu erstellen.

%Vor%

Execute () kann die Zeile zurückketten und alle Regeln erzwingen (oder Sie können die Schlusssequenz erstellen, während Sie die Eröffnungssequenz erstellen).

Der einzige Vorteil, den diese Technik gegenüber anderen hat, ist, dass Sie nicht durch Scoping-Regeln eingeschränkt sind. z.B. Wenn Sie die Sequenz basierend auf Benutzereingaben oder anderen asynchronen Ereignissen erstellen möchten, können Sie das tun.

    
Ian Mercer 28.04.2010 00:04
quelle
4

Jeff,

Was Sie erreichen möchten, wird im Allgemeinen als Aspektorientierte Programmierung (AOP) bezeichnet. Die Programmierung mit AOP Paradigmen in C # ist nicht einfach - oder zuverlässig ... noch nicht. Es gibt einige Fähigkeiten, die direkt in das CLR- und .NET-Framework eingebaut sind, die AOP in bestimmten engen Fällen ermöglichen. Wenn Sie zum Beispiel eine Klasse von ContextBoundObject ableiten, können Sie ContextAttribute verwenden, um Logik vorher zu injizieren / nach Methode ruft die CBO-Instanz auf. Sie können hier Beispiele dafür sehen .

Das Ableiten einer CBO-Klasse ist ärgerlich und restriktiv - und es gibt noch eine Alternative. Sie können ein Tool wie PostSharp verwenden, um AOP auf jede C # -Klasse anzuwenden. PostSharp ist wesentlich flexibler als CBOs, weil es Ihren IL-Code in einem Postcompilationsschritt grundlegend umschreibt. Dies mag ein wenig beängstigend wirken, aber es ist sehr mächtig, weil es Ihnen erlaubt, Code in fast jeder Art und Weise zu weben, die Sie sich vorstellen können. Hier ist ein PostSharp-Beispiel, das auf Ihrem Verwendungsszenario basiert:

%Vor%

PostSharp erreicht die Magie durch Neuschreiben des kompilierten IL-Codes mit Anweisungen zum Ausführen des Codes, den Sie in den Methoden JumperAttribute class ' OnEntry und OnExit definiert haben.

Ob in Ihrem Fall PostSharp / AOP eine bessere Alternative als "repurposing" ist, ist mir die using-Anweisung unklar. Ich stimme eher mit @Eric Lippert überein, dass das using-Schlüsselwort wichtige Semantiken Ihres Browsers verschleiert code und fügt dem } -Symbol am Ende eines Verwendungsblocks Seiteneffekte und Semantik ein - was unerwartet ist. Aber ist das anders als das Anwenden von AOP-Attributen auf Ihren Code? Sie verbergen auch wichtige Semantiken hinter einer deklarativen Syntax ... aber das ist der Sinn von AOP.

Ein Punkt, an dem ich von ganzem Herzen mit Eric übereinstimme, ist, dass die Neugestaltung Ihres Codes zur Vermeidung eines globalen Zustands (wenn möglich) wahrscheinlich die beste Option ist. Es vermeidet nicht nur das Problem, die korrekte Verwendung zu erzwingen, sondern kann auch dazu beitragen, zukünftige Multithreading-Herausforderungen zu vermeiden - welcher globaler Zustand sehr anfällig ist.

    
LBushkin 28.04.2010 14:15
quelle
4

Ich sehe das nicht als Missbrauch von using ; Ich verwende dieses Idiom in verschiedenen Kontexten und hatte nie Probleme ... vor allem, da using nur ein syntaktischer Zucker ist. Eine Möglichkeit, um ein globales Flag in einer der Bibliotheken von Drittanbietern zu setzen, die ich verwende, so dass die Änderung beim Beenden von Operationen zurückgesetzt wird:

%Vor%

Beachten Sie, dass Sie keine Variable in using clause zuweisen müssen. Das ist genug:

%Vor%

Ich vermisse C ++ 's RAII-Idiom etwas, wo der Destruktor immer aufgerufen wird. using ist am ehesten in C # zu finden, denke ich.

    
liori 27.04.2010 22:05
quelle
2

Wir haben fast genau das getan, was Sie vorgeschlagen haben, um Methodenprotokollierung in unseren Anwendungen hinzuzufügen. Beats müssen 2 Logging-Aufrufe machen, einen zum Betreten und einen zum Beenden.

    
Rob Goodwin 27.04.2010 22:13
quelle
1

Wäre es hilfreich, eine abstrakte Basisklasse zu haben? Eine Methode in der Basisklasse könnte StartJumping () aufrufen, die Implementierung der abstrakten Methode, die von untergeordneten Klassen implementiert wird, und dann EndJumping () aufrufen.

    
Jon Onstott 27.04.2010 22:01
quelle
1

Ich mag diesen Stil und setze ihn häufig ein, wenn ich ein gewisses Abnutzungsverhalten garantieren möchte: oft ist es viel sauberer zu lesen als ein Versuch - endlich. Ich glaube nicht, dass Sie sich damit beschäftigen sollten, die Referenz j zu deklarieren und zu benennen, aber ich denke, Sie sollten es vermeiden, die EndJumping-Methode zweimal aufzurufen, Sie sollten prüfen, ob sie bereits entsorgt wurde. Und mit Bezug auf Ihre nicht verwalteten Code-Notiz: Es ist ein Finalizer, der in der Regel für diese implementiert ist (obwohl Dispose und SuppressFinalize normalerweise aufgerufen werden, um die Ressourcen früher freizugeben.)

    
Jono 27.04.2010 22:11
quelle
1

Ich habe einige der Antworten hier ein wenig darüber kommentiert, was IDisposable ist und was nicht, aber ich wiederhole den Punkt, dass IDisposable eine deterministische Bereinigung ermöglicht, aber keine deterministische Bereinigung garantiert. Es ist nicht garantiert, dass es aufgerufen wird, und nur etwas garantiert, wenn es mit einem using -Block gepaart wird.

%Vor%

Nun, ich werde Ihre Verwendung von using nicht kommentieren, weil Eric Lippert eine viel bessere Arbeit geleistet hat.

Wenn Sie eine IDisposable-Klasse haben, ohne einen Finalizer zu benötigen, muss ich ein Muster sehen, das entdeckt, wenn vergessen wird, Dispose() aufzurufen, einen Finalizer hinzuzufügen, der bedingt in DEBUG-Builds kompiliert ist Finalizer wird aufgerufen.

Ein realistisches Beispiel ist eine Klasse, die das Schreiben in eine Datei auf besondere Weise kapselt. Da MyWriter eine Referenz auf eine FileStream enthält, die auch IDisposable ist, sollten wir auch IDisposable als höflich implementieren.

%Vor%

Autsch. Das ist ziemlich kompliziert, aber das erwarten die Leute, wenn sie IDisposable sehen.

Die Klasse tut noch gar nichts, aber öffnet eine Datei, aber das bekommen Sie mit IDisposable und die Protokollierung ist extrem vereinfacht.

%Vor%

Finalisten sind teuer, und die MyWriter oben benötigen keinen Finalizer, also ist es nicht sinnvoll, einen außerhalb von DEBUG-Builds hinzuzufügen.

    
Robert Paulson 27.04.2010 23:37
quelle
1

Mit dem using Muster kann ich einfach einen grep (?<!using.*)new\s+Jumper benutzen, um alle Orte zu finden, wo es ein Problem geben könnte.

Mit StartJumping muss ich jeden Aufruf manuell überprüfen, um herauszufinden, ob es eine Möglichkeit gibt, dass eine Ausnahme, Rückkehr, Pause, Fortfahren, Goto usw. dazu führen kann, dass EndJumping nicht aufgerufen wird.

    
adrianm 29.04.2010 11:07
quelle
0
  1. Ich glaube nicht, dass Sie diese Methoden statisch machen wollen
  2. Sie müssen die Entsorgung einchecken, wenn das Ende springen bereits aufgerufen wurde.
  3. Wenn ich wieder anfange zu springen, was passiert?

Sie können einen Referenzzähler oder eine Flagge verwenden, um den Status des "Springens" zu verfolgen. Einige Leute würden sagen IDisposable ist nur für nicht verwaltete Ressourcen, aber ich denke, das ist in Ordnung. Andernfalls sollten Sie den Start- und Endsprung als privat markieren und einen Destruktor verwenden, um mit dem Konstruktor zu gehen.

%Vor%     
James Westgate 27.04.2010 22:01
quelle

Tags und Links