[DotNetBook] Ausnahmen: Typ Systemarchitektur

Mit diesem Artikel veröffentliche ich weiterhin eine Reihe von Artikeln, deren Ergebnis ein Buch über die Arbeit von .NET CLR und .NET im Allgemeinen sein wird. Für Links - willkommen bei cat.


Ausnahmearchitektur


Wahrscheinlich ist eines der wichtigsten Probleme beim Thema Ausnahmen das Erstellen einer Ausnahmearchitektur in Ihrer Anwendung. Diese Frage ist aus vielen Gründen interessant. Für mich ist die Hauptsache die scheinbare Einfachheit, mit der nicht immer klar ist, was zu tun ist. Diese Eigenschaft ist allen grundlegenden Konstruktionen inhärent, die überall verwendet werden: IEnumerable , IDisposable und IObservable und andere. Einerseits locken sie durch ihre Einfachheit und beteiligen sich daran, sich in einer Vielzahl von Situationen einzusetzen. Und andererseits sind sie voller Strudel und Furten, aus denen man nicht weiß, wie man manchmal überhaupt nicht rauskommt. Und vielleicht ist Ihre Frage im Hinblick auf das zukünftige Volumen gereift: Was ist es also in Ausnahmesituationen?


Hinweis


Das auf Habré veröffentlichte Kapitel ist nicht aktualisiert und wahrscheinlich bereits etwas veraltet. Wenden Sie sich daher für einen neueren Text dem Original zu:



Um jedoch zu einigen Schlussfolgerungen hinsichtlich der Konstruktion der Architektur von Klassen von Ausnahmesituationen zu gelangen, müssen wir einige Erfahrungen mit Ihnen hinsichtlich ihrer Klassifizierung sammeln. Wenn Sie erst verstanden haben, womit wir uns befassen werden, wie und in welchen Situationen der Programmierer die Art des Fehlers auswählen soll und in welcher - treffen Sie die Wahl hinsichtlich des Abfangens oder Überspringens von Ausnahmen, können Sie verstehen, wie Sie ein Typsystem so erstellen können, dass es für Sie offensichtlich wird Code. Daher werden wir versuchen, Ausnahmesituationen (nicht die Arten von Ausnahmen selbst, sondern genau die Situationen) nach verschiedenen Kriterien zu klassifizieren.


Nach der theoretischen Möglichkeit, die projizierte Ausnahme zu fangen


In Bezug auf das theoretische Abfangen können Ausnahmen leicht in zwei Typen unterteilt werden: diejenigen, die genau abfangen, und diejenigen, die sehr wahrscheinlich abfangen. Warum mit hoher Wahrscheinlichkeit ? Denn es wird immer jemanden geben, der versucht, abzufangen, obwohl dies nicht vollständig getan werden musste.


Lassen Sie uns zunächst die Merkmale der ersten Gruppe aufzeigen: Ausnahmen, die abfangen sollten und werden.


Wenn wir eine Ausnahme dieses Typs einführen, informieren wir einerseits das externe Subsystem darüber, dass wir in einer Position sind, in der weitere Aktionen in unseren Daten keinen Sinn ergeben. Auf der anderen Seite meinen wir, dass nichts Globales kaputt war und wenn wir entfernt werden, wird sich nichts ändern, und daher kann diese Ausnahme leicht abgefangen werden, um die Situation zu verbessern. Diese Eigenschaft ist sehr wichtig: Sie bestimmt die Kritikalität des Fehlers und die Annahme, dass Sie den Code sicher weiter ausführen können, wenn Sie die Ausnahme abfangen und nur die Ressourcen löschen.


Die zweite Gruppe, egal wie seltsam sie auch klingen mag, ist für Ausnahmen verantwortlich, die nicht abgefangen werden müssen. Sie können nur zum Schreiben in das Fehlerprotokoll verwendet werden, aber nicht, um die Situation irgendwie zu korrigieren. Das einfachste Beispiel sind die Gruppenausnahmen ArgumentException und NullReferenceException . In einer normalen Situation sollten Sie beispielsweise die ArgumentNullException Ausnahme nicht abfangen, da die Ursache des Problems hier Sie und niemand anderes sind. Wenn Sie diese Ausnahme abfangen, gehen Sie davon aus, dass Sie einen Fehler gemacht und die Methode angegeben haben, der Sie sie nicht geben konnten:


 void SomeMethod(object argument) { try { AnotherMethod(argument); } catch (ArgumentNullException exception) { // Log it } } 

Bei dieser Methode versuchen wir, eine ArgumentNullException . Aber meiner Meinung nach sieht das Abfangen sehr seltsam aus: Die richtigen Argumente für die Methode zu verwenden, ist ganz und gar unser Anliegen. Es wäre nicht richtig, nachträglich zu reagieren: In einer solchen Situation ist es am korrektesten, die übertragenen Daten im Voraus zu überprüfen, bevor die Methode aufgerufen wird, oder noch besser, den Code so zu konstruieren, dass der Empfang falscher Parameter einfach nicht möglich wäre.


Eine andere Gruppe ist die Beseitigung schwerwiegender Fehler. Wenn ein bestimmter Cache kaputt ist und der Betrieb des Subsystems auf keinen Fall korrekt ist? Dann ist dies ein schwerwiegender Fehler, und es wird nicht garantiert, dass der dem Stapel am nächsten liegende Code ihn abfängt:


 T GetFromCacheOrCalculate() { try { if(_cache.TryGetValue(Key, out var result)) { return result; } else { T res = Strategy(Key); _cache[Key] = res; return res; } } catch (CacheCorreptedException exception) { RecreateCache(); return GetFromCacheOrCalculate(); } } 

Und lassen Sie CacheCorreptedException eine Ausnahme sein, die bedeutet, dass "Cache auf der Festplatte nicht konsistent ist". Dann stellt sich heraus, dass, wenn die Ursache eines solchen Fehlers für das Caching-Subsystem schwerwiegend ist (z. B. keine Berechtigungen für die Cache-Datei vorhanden sind), der weitere Code, wenn er den Cache nicht mit dem Befehl RecreateCache neu erstellen kann, und daher die Tatsache, dass diese Ausnahme RecreateCache wird, ein Fehler an sich ist.


Über das tatsächliche Abfangen einer Ausnahme


Eine andere Frage, die unseren Gedankengang bei Programmieralgorithmen stoppt, ist das Verständnis: Lohnt es sich, diese oder andere Ausnahmen zu erfassen, oder lohnt es sich, jemanden zu verstehen, der sie durchlässt? In die Sprache der Begriffe zu übersetzen, ist die Frage, die wir lösen müssen, zwischen Verantwortungsbereichen zu unterscheiden. Schauen wir uns den folgenden Code an:


 namespace JetFinance.Strategies { public class WildStrategy : StrategyBase { private Random random = new Random(); public void PlayRussianRoulette() { if(DateTime.Now.Second == (random.Next() % 60)) { throw new StrategyException(); } } } public class StrategyException : Exception { /* .. */ } } namespace JetFinance.Investments { public class WildInvestment { WildStrategy _strategy; public WildInvestment(WildStrategy strategy) { _strategy = strategy; } public void DoSomethingWild() { ?try? { _strategy.PlayRussianRoulette(); } catch(StrategyException exception) { } } } } using JetFinance.Strategies; using JetFinance.Investments; void Main() { var foo = new WildStrategy(); var boo = new WildInvestment(foo); ?try? { boo.DoSomethingWild(); } catch(StrategyException exception) { } } 

Welche der beiden vorgeschlagenen Strategien ist korrekter? Der Verantwortungsbereich ist sehr wichtig. Zunächst scheint es WildStrategy , als ob WildInvestment und seine Konsistenz vollständig von WildStrategy . Wenn WildInvestment diese Ausnahme einfach ignoriert, wird es auf eine höhere Ebene gebracht und es besteht keine Notwendigkeit, etwas anderes zu tun. Beachten Sie jedoch, dass es ein rein architektonisches Problem gibt: Die Main Methode fängt eine Ausnahme von einer architektonisch eine Ebene ab, indem sie eine architektonisch andere Methode aufruft. Wie sieht es in Bezug auf die Nutzung aus? Ja, im Allgemeinen sieht es so aus:


  • Die Sorge um diese Ausnahme wurde von uns einfach aufgewogen.
  • Der Benutzer dieser Klasse ist sich nicht sicher, ob diese Ausnahme durch eine Reihe von Methoden speziell vor uns ausgelöst wird
  • Wir fangen an, unnötige Abhängigkeiten zu zeichnen, die wir losgeworden sind und eine Zwischenschicht verursachen.

Aus dieser Schlussfolgerung folgt jedoch eine andere Schlussfolgerung: Wir müssen catch in der DoSomethingWild Methode DoSomethingWild . Und das ist etwas seltsam für uns: WildInvestment scheint sehr von jemandem abhängig zu sein. Das heißt, Wenn PlayRussianRoulette nicht funktionieren konnte, dann auch DoSomethingWild : Es hat keine Rückkehrcodes, aber es muss Roulette spielen. Was tun in einer scheinbar hoffnungslosen Situation? Die Antwort ist eigentlich einfach: DoSomethingWild in einer anderen Ebene befindet, sollte es eine eigene Ausnahme DoSomethingWild , die sich auf diese Ebene bezieht, und das Original als ursprüngliche InnerException des Problems InnerException - in InnerException :


 namespace JetFinance.Strategies { pubilc class WildStrategy { private Random random = new Random(); public void PlayRussianRoulette() { if(DateTime.Now.Second == (random.Next() % 60)) { throw new StrategyException(); } } } public class StrategyException : Exception { /* .. */ } } namespace JetFinance.Investments { public class WildInvestment { WildStrategy _strategy; public WildInvestment(WildStrategy strategy) { _strategy = strategy; } public void DoSomethingWild() { try { _strategy.PlayRussianRoulette(); } catch(StrategyException exception) { throw new FailedInvestmentException("Oops", exception); } } } public class InvestmentException : Exception { /* .. */ } public class FailedInvestmentException : Exception { /* .. */ } } using JetFinance.Investments; void Main() { var foo = new WildStrategy(); var boo = new WildInvestment(foo); try { boo.DoSomethingWild(); } catch(FailedInvestmentException exception) { } } 

Wenn wir die Ausnahme auf eine andere umstellen, übertragen wir die Probleme im Wesentlichen von einer Anwendungsschicht auf eine andere, wodurch die Arbeit aus Sicht des Benutzers dieser Klasse vorhersehbarer wird: der Main Methode.


Bei Problemen mit der Wiederverwendung


Sehr oft stehen wir vor einer schwierigen Aufgabe: Einerseits sind wir zu faul, um eine neue Art von Ausnahme zu erstellen, und wenn wir uns entscheiden, ist nicht immer klar, worauf wir uns stützen sollen: welche Art als Basis dienen soll. Aber genau diese Entscheidungen bestimmen die gesamte Architektur von Ausnahmesituationen. Lassen Sie uns beliebte Lösungen durchgehen und einige Schlussfolgerungen ziehen.


Bei der Auswahl des Ausnahmetyps können Sie versuchen, eine bereits vorhandene Lösung zu verwenden: Suchen Sie eine Ausnahme mit einer ähnlichen Bedeutung im Namen und verwenden Sie sie. Wenn wir beispielsweise eine Entität über einen Parameter erhalten haben, der irgendwie nicht zu uns passt, können wir eine InvalidArgumentException , die die Fehlerursache in Message angibt. Dieses Szenario sieht gut aus, insbesondere wenn man InvalidArgumentException dass InvalidArgumentException zu der Gruppe von Ausnahmen gehört, für die kein obligatorischer Fang erforderlich ist. Die Auswahl von InvalidDataException ist jedoch schlecht, wenn Sie mit Daten arbeiten. Nur weil sich dieser Typ in der System.IO Zone befindet und Sie dies kaum tun. Das heißt, Es stellt sich heraus, dass es fast immer der falsche Ansatz ist, den vorhandenen Typ zu finden, weil Sie träge Ihren eigenen Typ machen. Es gibt fast keine Ausnahmen, die für den allgemeinen Aufgabenkreis erstellt werden. Fast alle von ihnen sind für bestimmte Situationen geschaffen und ihre Wiederverwendung wird eine grobe Verletzung der Architektur von Ausnahmesituationen darstellen. Nachdem der Benutzer eine Ausnahme eines bestimmten Typs erhalten hat (z. B. dieselbe System.IO.InvalidDataException ), ist er verwirrt: Einerseits sieht er die System.IO des Problems in System.IO als Ausnahme-Namespace und andererseits als völlig anderen Namespace für den Wurfpunkt. Wenn Sie über die Regeln für das Auslösen dieser Ausnahme nachdenken, gehen Sie zu referencesource.microsoft.com und suchen Sie alle Stellen, an denen sie ausgelöst wird :


  • internal class System.IO.Compression.Inflater

Und das wird er verstehen Nur jemand hat krumme Hände Die Wahl des Ausnahmetyps verwirrte ihn, da die Methode, mit der die Ausnahme ausgelöst wurde, nicht an der Komprimierung beteiligt war.


Um die Wiederverwendung zu vereinfachen, können Sie einfach eine einzige Ausnahme erstellen, indem Sie ein ErrorCode Feld mit einem Fehlercode deklarieren und glücklich leben. Es scheint: eine gute Lösung. Überall die gleiche Ausnahme auslösen, den Code festlegen, nur einen catch dadurch die Stabilität der Anwendung erhöhen: und es gibt nichts mehr zu tun. Bitte stimmen Sie dieser Position jedoch nicht zu. Wenn Sie während der gesamten Anwendung auf diese Weise handeln, vereinfachen Sie natürlich einerseits Ihr Leben. Auf der anderen Seite verwerfen Sie jedoch die Möglichkeit, eine Untergruppe von Ausnahmen abzufangen, die durch einige gemeinsame Merkmale verbunden sind. Wie dies zum Beispiel mit ArgumentException , die unter sich eine ganze Gruppe von Ausnahmen durch Vererbung kombiniert. Das zweite schwerwiegende Minus sind zu große und unlesbare Codeblätter, die das Filtern nach Fehlercode organisieren. Wenn Sie jedoch eine andere Situation einnehmen: Wenn die Finalisierung des Fehlers für den Endbenutzer nicht wichtig sein sollte, sieht die Einführung eines Generalisierungstyps und eines Fehlercodes bereits nach einer viel korrekteren Anwendung aus:


 public class ParserException : Exception { public ParserError ErrorCode { get; } public ParserException(ParserError errorCode) { ErrorCode = errorCode; } public override string Message { get { return Resources.GetResource($"{nameof(ParserException)}{Enum.GetName(typeof(ParserError), ErrorCode)}"); } } } public enum ParserError { MissingModifier, MissingBracket, // ... } // Usage throw new ParserException(ParserError.MissingModifier); 

Der Code, der den Parser-Aufruf schützt, ist fast immer gleichgültig, aus welchem ​​Grund das Parsen blockiert wurde: Die Fehler-Tatsache selbst ist für ihn wichtig. Wenn dies dennoch wichtig wird, kann der Benutzer den Fehlercode immer aus der ErrorCode . Um dies zu tun, ist es überhaupt nicht notwendig, nach den erforderlichen Wörtern durch Teilzeichenfolge in Message zu suchen.


Wenn Sie Probleme bei der Wiederverwendung ignorieren, können Sie für jede Situation einen Ausnahmetyp erstellen. Einerseits sieht es logisch aus: Eine Art von Fehler ist eine Art von Ausnahme. Wie bei allem ist es hier jedoch nicht wichtig, es zu übertreiben: Wenn Sie an jedem Release-Punkt außergewöhnliche Operationen ausführen, verursachen Sie Probleme beim Abfangen: Der Code der aufrufenden Methode wird mit catch Blöcken überladen. Schließlich muss er alle Arten von Ausnahmen behandeln, die Sie ihm geben möchten. Ein weiteres Minus ist rein architektonisch. Wenn Sie keine Vererbung verwenden, desorientieren Sie den Benutzer dieser Ausnahmen: Es kann viele Gemeinsamkeiten zwischen ihnen geben, und Sie müssen sie einzeln abfangen.


Dennoch gibt es gute Szenarien für die Einführung bestimmter Typen für bestimmte Situationen. Zum Beispiel, wenn eine Aufschlüsselung nicht für die gesamte Entität als Ganzes auftritt, sondern für eine bestimmte Methode. Dann sollte sich dieser Typ an einer solchen Stelle in der Vererbungshierarchie befinden, damit nicht daran gedacht wird, ihn zusammen mit etwas anderem abzufangen: zum Beispiel durch einen separaten Vererbungszweig auszuwählen.


Wenn Sie diese beiden Ansätze kombinieren, erhalten Sie außerdem eine sehr leistungsfähige Toolbox für die Arbeit mit einer Gruppe von Fehlern: Sie können einen verallgemeinernden abstrakten Typ einführen, von dem bestimmte bestimmte Situationen geerbt werden können. Die Basisklasse (unser Generalisierungstyp) muss mit einer abstrakten Eigenschaft ausgestattet sein, in der der Fehlercode gespeichert ist, und die Erben überschreiben diese Eigenschaft, um diesen Fehlercode anzugeben:


 public abstract class ParserException : Exception { public abstract ParserError ErrorCode { get; } public override string Message { get { return Resources.GetResource($"{nameof(ParserException)}{Enum.GetName(typeof(ParserError), ErrorCode)}"); } } } public enum ParserError { MissingModifier, MissingBracket } public class MissingModifierParserException : ParserException { public override ParserError ErrorCode { get; } => ParserError.MissingModifier; } public class MissingBracketParserException : ParserException { public override ParserError ErrorCode { get; } => ParserError.MissingBracket; } // Usage throw new MissingModifierParserException(ParserError.MissingModifier); 

Was sind die wunderbaren Eigenschaften, die wir mit diesem Ansatz erhalten?


  • Einerseits haben wir den Fang einer Ausnahme durch den Grundtyp beibehalten;
  • Auf der anderen Seite war es immer noch möglich, eine bestimmte Situation herauszufinden, da eine Ausnahme vom Grundtyp abgefangen wurde.
  • und außerdem ist es möglich, für einen bestimmten Typ und nicht für einen grundlegenden Typ abzufangen, ohne die flache Struktur von Klassen zu verwenden.

Für mich ist dies eine sehr bequeme Option.


In Bezug auf eine einzelne Gruppe von Verhaltenssituationen


Welche Schlussfolgerungen können aus den zuvor beschriebenen Überlegungen gezogen werden? Versuchen wir sie zu formulieren:


Lassen Sie uns zunächst entscheiden, was unter Situationen zu verstehen ist. Wenn wir über Klassen und Objekte sprechen, sind wir in erster Linie daran gewöhnt, Entitäten mit einem internen Status zu betreiben, über den wir Aktionen ausführen können. Es stellt sich heraus, dass wir auf diese Weise die erste Art von Verhaltenssituation gefunden haben: Aktionen auf eine bestimmte Entität. Wenn Sie die Grafik von Objekten wie von außen betrachten, werden Sie feststellen, dass sie logisch in Funktionsgruppen zusammengefasst ist: Die erste befasst sich mit dem Caching, die zweite mit Datenbanken, die dritte mit mathematischen Berechnungen. Ebenen können alle diese Funktionsgruppen durchlaufen: eine Protokollierungsschicht mit verschiedenen internen Zuständen, Prozessprotokollierung, Ablaufverfolgung von Methodenaufrufen. Ebenen können umfassender sein: Kombinieren mehrerer Funktionsgruppen. Zum Beispiel eine Modellschicht, eine Controller-Schicht, eine Präsentationsschicht. Diese Gruppen können sich in derselben oder in völlig unterschiedlichen Gruppen befinden, aber jede von ihnen kann ihre eigenen Ausnahmesituationen erzeugen.


Es stellt sich heraus, dass Sie, wenn Sie auf diese Weise argumentieren, eine Hierarchie von Typen von Ausnahmesituationen erstellen können, basierend auf dem Typ, der zu einer bestimmten Gruppe oder Ebene gehört, wodurch die Möglichkeit geschaffen wird, Ausnahmen vom Code für eine einfache semantische Navigation in dieser Typhierarchie abzufangen.


Schauen wir uns den Code an:


 namespace JetFinance { namespace FinancialPipe { namespace Services { namespace XmlParserService { } namespace JsonCompilerService { } namespace TransactionalPostman { } } } namespace Accounting { /* ... */ } } 

Wie sieht es aus? Für mich sind Namespaces eine großartige Möglichkeit, Arten von Ausnahmen auf natürliche Weise nach ihren Verhaltenssituationen zu gruppieren: Alles, was zu bestimmten Gruppen gehört, sollte vorhanden sein, einschließlich Ausnahmen. Wenn Sie eine bestimmte Ausnahme erhalten, wird zusätzlich zum Namen des Typs der Namespace angezeigt, der die Zugehörigkeit eindeutig bestimmt. Erinnern Sie sich an das schlechte Wiederverwendungsbeispiel des InvalidDataException Typs, der tatsächlich im System.IO Namespace definiert ist? Die Zugehörigkeit zu diesem Namespace bedeutet, dass im Wesentlichen eine Ausnahme dieses Typs aus Klassen entfernt werden kann, die sich im System.IO Namespace oder in einem verschachtelten befinden. Aber die Ausnahme selbst wurde von einem völlig anderen Ort weggeworfen, was den Forscher über das aufgetretene Problem verwirrte. Indem Sie Ausnahmetypen auf dieselben Namespaces wie die Typen konzentrieren, die diese Ausnahmen auslösen, halten Sie einerseits die Typarchitektur konsistent und erleichtern andererseits dem Endentwickler das Verständnis der Gründe für das, was passiert ist.


Was ist die zweite Art der Gruppierung auf Codeebene? Vererbung:


 public abstract class LoggerExceptionBase : Exception { protected LoggerExceptionBase(..); } public class IOLoggerException : LoggerExceptionBase { internal IOLoggerException(..); } public class ConfigLoggerException : LoggerExceptionBase { internal ConfigLoggerException(..); } 

Wenn bei gewöhnlichen Anwendungsentitäten Vererbung die Vererbung von Verhalten und Daten bedeutet, wobei Typen durch Zugehörigkeit zu einer einzelnen Gruppe von Entitäten kombiniert werden, bedeutet Vererbung bei Ausnahmen die Zugehörigkeit zu einer einzelnen Gruppe von Situationen , da das Wesen der Ausnahme nicht das Wesentliche, sondern das Problem ist.


Wenn wir beide Gruppierungsmethoden kombinieren, können wir einige Schlussfolgerungen ziehen:


  • Innerhalb der Assembly ( Assembly ) muss der grundlegende Ausnahmetyp vorhanden sein, den diese Assembly auslöst. Diese Art von Ausnahme muss sich im Root-Namespace für die Assembly befinden. Dies ist die erste Ebene der Gruppierung.
  • Ferner können sich innerhalb der Baugruppe selbst ein oder mehrere verschiedene Namespaces befinden. Jeder von ihnen unterteilt die Baugruppe in einige Funktionsbereiche, wodurch die Gruppen von Situationen definiert werden, die in dieser Baugruppe auftreten. Dies können Zonen von Controllern, Datenbankentitäten, Datenverarbeitungsalgorithmen und anderen sein. Für uns sind diese Namespaces eine Gruppierung von Typen nach funktionaler Zugehörigkeit und aus Sicht der Ausnahmen eine Gruppierung nach Problemzonen derselben Assembly.
  • Die Vererbung von Ausnahmen kann nur von Typen im selben Namespace oder im Stammverzeichnis erfolgen. Dies garantiert ein eindeutiges Verständnis der Situation durch den Endbenutzer und das Fehlen des Abfangens von linken Ausnahmen beim Abfangen gemäß dem Grundtyp. : global::Finiki.Logistics.OhMyException , catch(global::Legacy.LoggerExeption exception) , :

 namespace JetFinance.FinancialPipe { namespace Services.XmlParserService { public class XmlParserServiceException : FinancialPipeExceptionBase { // .. } public class Parser { public void Parse(string input) { // .. } } } public abstract class FinancialPipeExceptionBase : Exception { } } using JetFinance.FinancialPipe; using JetFinance.FinancialPipe.Services.XmlParserService; var parser = new Parser(); try { parser.Parse(); } catch (XmlParserServiceException exception) { // Something wrong in parser } catch (FinancialPipeExceptionBase exception) { // Something else wrong. Looks critical because we don't know real reason } 

, : , , , XmlParserServiceException . , , , JetFinance.FinancialPipe.FinancialPipeExceptionBase , : XmlParserService , . , catch : .


?


  • . . — , : , -, UI. Das heißt, ;
  • , : , catch ;
  • – . ;
  • , . : , , , . , - : , — , , , ;
  • , : ;
  • , Mixed Mode c ErrorCode.


. , , :


  • unsafe , . : , (, ) ;
  • , , , .. . , , . , , . — — InnerExcepton . — ;
  • Unser eigener Code, der zufällig in einen nicht konsistenten Zustand eingegeben wurde. Das Parsen von Text ist ein gutes Beispiel. Es gibt keine externen Abhängigkeiten, es gibt keine Auszahlung unsafe, aber es gibt einen Analysefehler.

Link zum ganzen Buch



Source: https://habr.com/ru/post/de419927/


All Articles