Connect with us

Entwicklung & Code

Programmiersprache C++: Was reinterpret_cast nicht tut


Im heutigen Beitrag werde ich eine der größten Fallstricke von C++ erläutern: reinterpret_cast. Ein anderer Titel für diesen Beitrag könnte lauten: „Das ist nicht der Cast, den du suchst!“

Weiterlesen nach der Anzeige


Portrait von Andreas Fertig

Portrait von Andreas Fertig

Andreas Fertig ist erfahrener C++-Trainer und Berater, der weltweit Präsenz- sowie Remote-Kurse anbietet. Er engagiert sich im C++-Standardisierungskomitee und spricht regelmäßig auf internationalen Konferenzen. Mit C++ Insights ( hat er ein international anerkanntes Tool entwickelt, das C++-Programmierenden hilft, C++ noch besser zu verstehen.

Meine Motivation für diesen Blogbeitrag stammt aus mehreren Schulungen und einigen Vorträgen, die ich gehalten habe. Seit C++23 gibt es in der Standardbibliothek eine neue Funktion: std::start_lifetime_as. Wenn ich Kurse mit Schwerpunkt auf eingebetteten Umgebungen unterrichte oder Vorträge mit diesem Schwerpunkt halte, habe ich begonnen, std::start_lifetime_as in das Material aufzunehmen. Mit einem interessanten Ergebnis.

Das Feedback, das ich bekomme, lautet in etwa:

  • Warum benötige ich std::start_lifetime_as, ich habe doch schon reinterpret_cast?
  • Warum kann ich reinterpret_cast nicht verwenden?

Wer noch nie von start_lifetime_as gehört hat, findet weitere Informationen in meinem englischen Artikel „The correct way to do type punning in C++ – The second act“. Ich verwende folgendes Beispiel daraus:


struct ConfigValues {
  uint32_t                  chksum;
  std::array values;
};

bool ProcessData(std::span bytes)
{
  if(bytes.size() < sizeof(ConfigValues)) { return false; }

  // #A
  ConfigValues* cfgValues = reinterpret_cast(bytes.data());

  return HandleConfigValues(cfgValues);
}


Die Idee hier ist, eine Reihe von rohen Bytes in eine bekannte Struktur umzuwandeln – hier mit dem Namen ConfigValues. Zusammen mit start_lifetime_as gerate ich immer öfter in Gespräche, in denen mir Leute sagen, dass der Name reinterpret impliziert, dass solcher Code wie erwartet funktionieren sollte. Die Erwartung ist, dass solcher Code frei von undefiniertem Verhalten ist und tatsächlich einen Zeiger auf ein ConfigValues-Objekt zurückgibt.

Weiterlesen nach der Anzeige

Zwar kann ich einer solchen Erwartung aufgrund des Wortlauts des Standards und des C++-Objektmodells nicht widersprechen, doch eine solche Erwartung führt zu undefiniertem Verhalten. In einer typsicheren Sprache kann ein Objekt nicht in ein anderes, nicht verwandtes Objekt konvertiert werden.

Der wichtigste Wortlaut ist [expr.reinterpret.cast § 7], der besagt:

An object pointer can be explicitly converted to an object pointer of a different type. When a prvalue v of object pointer type is converted to the object pointer type “pointer to cv T”, the result is static_cast(static_cast(v)). [Note 5: Converting a pointer of type “pointer to T1” that points to an object of type T1 to the type “pointer to T2” (where T2 is an object type and the alignment requirements of T2 are no stricter than those of T1) and back to its original type yields the original pointer value. — end note]

Zunächst einmal handelt dieser ganze Absatz von Zeigern auf Objekte und nicht von Objekten selbst. Es heißt, dass du ein ConfigValues in einen void* oder einen beliebigen anderen Datentyp konvertieren kannst, der ein Objekttyp ist. Ein Objekttyp ist alles außer einem Funktionstyp, einem Referenztyp und void.

Weiter unten in der Anmerkung bestätigt der Standard ausdrücklich, dass du einen Rundlauf durchführen kannst. Zum Beispiel:


ConfigValues  cfg{};
ConfigValues* val{&cfg};
void*         typeErased         = reinterpret_cast(val);
ConfigValues* roundTripBackToVal = reinterpret_cast(typeErased);


Dies ermöglicht Konstrukte, die sogenannte Typlöschung (Type Erasure) nutzen, wie std::any. Du kannst einen Alias eines anderen Zeigertyps erhalten.

In diesem Absatz ist von einer Konvertierung des Objekts selbst keine Rede. Nur den Zeiger kann man konvertieren.

Im Sinne des C++-Objektmodells muss eine Anwendung ein gültiges Objekt erstellen (und später zerstören). Aber alles, was jemals erstellt wurde, ist ein ConfigValues-Objekt. reinterpret_cast ist ein Werkzeug, das es ermöglicht, einen Zeiger eines anderen Typs zu speichern. Sobald du ihn verwenden möchtest, musst du den Zeiger wieder in seinen ursprünglichen Typ zurückkonvertieren.

Nehmen wir einmal an, reinterpret_cast würde so funktionieren, wie manche Leute es erwarten:


struct Apple {
  int x;
};

struct Orange {
  int y;
};

Apple*  grannySmith{new Apple{4}};
Orange* bali{reinterpret_cast(grannySmith)};  // #A

int y = bali->y;
int x = grannySmith->x;


Nach den Regeln von C++ muss ein Objekt erstellt und zerstört werden. Wenn #A ein Objekt erstellen würde, müsste es das grannySmith-Objekt zerstören. Was überraschend wäre. Dann hättest du keine Möglichkeit, eine Typlöschung wie std::any zu implementieren, da das Speichern eines gelöschten Typs void* das ursprüngliche Objekt aus Sicht der abstrakten Maschinerie von C++ zerstören würde. Das würde dem Compiler ermöglichen, verschiedene andere Optimierungen vorzunehmen, die das Programm zum Absturz bringen würden.

Mit reinterpret_cast hast du eine Möglichkeit, ein Objekt A als einen anderen Typ B zu aliasieren, jedoch ohne das Recht, jemals über diesen Zeiger auf ein B-Objekt zuzugreifen. Andererseits erstellt start_lifetime_as implizit ein B-Objekt am Zielort des Zeigers A, während gleichzeitig die Lebensdauer von A beendet wird.

Meist will man in diesen Situationen ein Objekt eines anderen Typs zum Leben erwecken. Und genau dafür ist std::start_lifetime_as gedacht.

Wenn du std::start_lifetime_as auf einen Zeiger anwendest, versteht die abstrakte Maschine, dass du ein neues Objekt dieses Typs erstellst. Im Gegensatz zu einem Aufruf von new oder einem Stack-Objekt wird kein Konstruktor ausgeführt. Alles geschieht nur innerhalb des C++-Objektmodells.

Es gibt noch eine weitere Funktion von std::start_lifetime_as: Wenn es die Lebensdauer eines neuen Objekts startet, wird die Lebensdauer der Quelle automatisch beendet, wiederum ohne einen tatsächlichen Destruktor aufzurufen. Das ist hier entscheidend.

Wenn ich std::start_lifetime_as auf mein vorheriges Beispiel anwende, sieht die korrekte Implementierung wie folgt aus:


struct Apple {
  int x;
};

struct Orange {
  int y;
};

Apple*  grannySmith{new Apple{4}};
Orange* bali{std::start_lifetime_as(grannySmith)};  // #A

int y = bali->y;

grannySmith = std::start_lifetime_as(bali);  // #B
int x       = grannySmith->x;


Der Code in #B startet die Lebensdauer des Zeigers erneut als Apple-Objekt.

Mit reinterpret_cast erhältst du nur eine Zeigerkonvertierung. Du darfst diesen neuen Zeiger nicht verwenden, um auf ein Objekt des Typs zuzugreifen.

Wenn du die Lebensdauer eines Objekts starten möchtest, um auf Daten als neuen Typ zuzugreifen, benötigst du std::start_lifetime_as.


(rme)



Source link

Entwicklung & Code

Won’t fix! – Teil 1: Warum Softwareschätzungen so zuverlässig falsch sind


Manche Probleme der Softwareentwicklung sind keine Fehler, die sich beheben ließen, sondern strukturelle Eigenschaften der Disziplin. Es sind Dauerthemen, die Entwicklerinnen und Entwickler seit Jahrzehnten begleiten und auch in den nächsten Jahrzehnten begleiten werden.

Weiterlesen nach der Anzeige

Dieser Artikel ist der erste Teil einer Serie, die Probleme beleuchtet, die sich nicht wegoptimieren lassen: Won’t fix – wie irreparable Issues in GitHub-Repositories heißen.

Wie lange braucht man, um ein Osterei zu bemalen? Ein bisschen Wiese, einen Osterhasen, eine Sonne und einige Wolken am Himmel, einschließlich Ausblasen und das Einfädeln des an ein Streichholz geknoteten Fadens. 10 Minuten? 15 Minuten? Eine halbe Stunde? Oder gar eine ganze Stunde? Selbst bei einer Aufgabe, deren Anforderungen klar und deren Prozess seit der Kindheit bestens bekannt sind, bewegen sich die Antworten zwischen 10 Minuten und einer Stunde, was einer Spanne von 500 Prozent entspricht.


Golo Roden

Golo Roden

Golo Roden ist Gründer und CTO von the native web GmbH. Er beschäftigt sich mit der Konzeption und Entwicklung von Web- und Cloud-Anwendungen sowie -APIs, mit einem Schwerpunkt auf Event-getriebenen und Service-basierten verteilten Architekturen. Sein Leitsatz lautet, dass Softwareentwicklung kein Selbstzweck ist, sondern immer einer zugrundeliegenden Fachlichkeit folgen muss.

Die Serie „Wont‘ fix“ behandelt Probleme in der Softwareentwicklung, die sich nicht wegoptimieren lassen, mit denen man aber lernen kann umzugehen:

In der Softwarebranche gelten solche Schwankungen als inakzeptabel. Dort sollen Entwicklerinnen und Entwickler bei unvergleichlich komplexeren Aufgaben, deren Anforderungen häufig noch nicht einmal vollständig bekannt sind, verlässliche Vorhersagen über Kosten und Zeitrahmen treffen. „Wie teuer wird es, und wie lange wird es dauern?“ sind die gängigen Fragen, auf die Kundinnen und Kunden eine belastbare Antwort erwarten.

Doch Studien in der Softwarebranche zeigen seit Jahrzehnten ein konsistentes Bild: Projekte dauern länger als geplant, kosten mehr als veranschlagt und liefern weniger als versprochen. Dieses Muster zieht sich durch alle Themen, alle Unternehmensgrößen und alle Methoden. Weder Wasserfall noch Agile noch irgendetwas dazwischen haben daran grundlegend etwas geändert. Dass verlässliche Vorhersagen regelmäßig scheitern, überrascht bei näherer Betrachtung weniger als die Erwartung, dass es passen sollte.

Bevor sich die Frage beantworten lässt, warum Softwareschätzungen so häufig danebenliegen, lohnt es sich, einen Schritt zurückzutreten: Warum wird überhaupt geschätzt? Die Antwort ist weniger offensichtlich, als sie scheint, denn hinter der Frage „Wie lange dauert das?“ verbergen sich ganz unterschiedliche Anliegen.

Weiterlesen nach der Anzeige

Manchmal geht es um eine Kostenvorhersage. Ein Unternehmen möchte wissen, ob sich eine Investition lohnt, und benötigt eine Zahl für die Budgetplanung. Manchmal geht es um eine Zeitvorhersage, etwa weil ein Marktereignis einen Termin diktiert. Und manchmal geht es schlicht um die Machbarkeit: Ist das überhaupt realisierbar und wenn ja, in welcher Größenordnung bewegt sich der Aufwand?

Diese Unterscheidung wird in der Praxis selten getroffen, obwohl sie entscheidend ist. Wer die Machbarkeit prüfen will, benötigt keine Stundenzahl. Wer ein Jahresbudget plant, benötigt keine Präzision auf Tagesebene. Und wer einen festen Liefertermin benötigt, dem hilft eine Aufwandsschätzung in Story-Points wenig. Hinzu kommt ein weiteres Missverständnis: Eine Schätzung wird oft als Zusage behandelt, obwohl sie ihrem Wesen nach eine Annäherung unter Unsicherheit ist. Wer eine Schätzung abgibt, kommuniziert eine Erwartung. Wer eine Schätzung entgegennimmt, versteht häufig ein Versprechen. Dieser Unterschied mag subtil klingen, ist aber in der Praxis die Quelle zahlloser Konflikte zwischen Entwicklungsteams und ihren Auftraggebern.

Die Softwarebranche leistet einer fundamentalen Fehleinschätzung Vorschub, indem sie über Produktionseffizienz, Softwarefabriken und Durchsatz spricht, als ginge es um die Fertigung von Bauteilen am Fließband. Diese Formulierungen suggerieren einen industriellen, reproduzierbaren Prozess, bei dem Schätzungen möglich und sinnvoll sind, weil das Fließband den Takt bestimmt.

Softwareentwicklung aber ist keine Produktion. Der Name sagt es bereits: Es ist Entwicklung. Ein kreativer, in gewissem Sinne sogar künstlerischer Prozess. Der herausragende US-amerikanische Informatiker Donald E. Knuth nannte sein Lebenswerk „The Art of Computer Programming“ und prägte mit dem Konzept des Literate Programming die Idee, dass Programme nicht nur funktionieren, sondern auch lesbar und ästhetisch sein sollten. Seine damit verbundene Frage hat nichts von ihrer Aktualität verloren: „Wann haben Sie das letzte Mal einen angenehmen Abend in einem bequemen Sessel verbracht und dabei ein gutes Programm gelesen?“ Dass diese Frage für die meisten Menschen absurd klingt, sagt viel über das Missverständnis der Branche gegenüber ihrem eigenen Tun.

Software ist ausführbares Wissen. Und Wissen lässt sich nicht auf Verlangen in einem vorgegebenen zeitlichen oder finanziellen Rahmen erzeugen. Es entsteht durch Entdecken, Erfinden und Verstehen. Und dieser Prozess lässt sich nicht takten. Ideen kommen, wenn der kreative Freiraum dafür besteht, nicht wenn der Projektplan sie vorsieht. Fred Brooks unterschied bereits 1986 in seinem Essay „No Silver Bullet“ zwischen wesentlicher und akzidentieller Komplexität. Die akzidentielle Komplexität lässt sich durch bessere Werkzeuge und Methoden reduzieren: bessere Editoren, leistungsfähigere Frameworks, effizientere Build-Systeme. Die wesentliche Komplexität aber steckt im Problem selbst und kein Tool der Welt kann sie beseitigen. Sie ist der Teil der Aufgabe, der übrig bleibt, wenn alle technischen Hürden beseitigt sind.

Wer Software schätzt, versucht im Grunde vorherzusagen, wie lange es dauert, ein Problem zu verstehen, das noch niemand vollständig verstanden hat. Dass diese Vorhersage selten stimmt, ist weniger erstaunlich als die Tatsache, dass sie trotzdem ständig verlangt wird.

Eine Metapher macht das greifbar. Angenommen, jemand soll vorhersagen, wie lange eine Wanderung von Berlin nach München dauert. Die Rechnung scheint einfach: Entfernung auf der Karte abmessen, eine durchschnittliche Gehgeschwindigkeit ansetzen, fertig. Auf dem Papier sieht das überzeugend aus.

In der Realität nicht. Die Karte zeigt keine Höhenunterschiede, zumindest nicht im erforderlichen Detail. Sie zeigt nicht, dass Wege gesperrt sein können, dass Flüsse überquert werden müssen und erst eine Brücke gefunden werden will, dass das Wetter umschlagen kann und dass Umwege unvermeidlich sind. Wer schon einmal gewandert ist, weiß, dass selbst auf einer vermeintlich bekannten Route Überraschungen lauern: ein umgestürzter Baum, eine Baustelle, ein falsch abgebogener Pfad, der einen Kilometer extra kostet. Der Plan und der tatsächliche Weg haben wenig miteinander zu tun.

Softwareprojekte verhalten sich genauso. Die Spezifikation ist die Karte, der Code ist der Weg. Und zwischen beiden liegt das unbekannte Gelände: Bibliotheken, die sich anders verhalten als dokumentiert, Anforderungen, die sich erst während der Umsetzung als widersprüchlich herausstellen, technische Schulden in bestehenden Systemen, die niemand auf der Karte verzeichnet hat.

In der Softwareentwicklung beschreibt der Cone of Uncertainty genau dieses Phänomen. Er besagt, dass die Unschärfe einer Schätzung zu Projektbeginn am größten ist und erst im Verlauf des Projekts abnimmt, je mehr über das Problem und seine Lösung bekannt wird. Zu Beginn eines Projekts kann die tatsächliche Dauer um den Faktor vier nach oben oder unten vom Schätzwert abweichen. Erst wenn ein signifikanter Teil der Arbeit bereits geleistet ist, nähert sich die Schätzung der Realität an. Das Paradoxe daran: Genau zu Beginn, wenn die Unsicherheit am größten ist, werden Budgets festgelegt, Verträge geschlossen und Timelines kommuniziert. Es ist, als würde man die Wanderung planen, ohne je einen Blick auf das Gelände geworfen zu haben, und dann den errechneten Zeitplan als verbindlich betrachten.

Auch wenn die strukturellen Schwierigkeiten bekannt sind, bleibt ein zweites Problem: Der menschliche Verstand arbeitet systematisch gegen realistische Einschätzungen. Die Psychologen Daniel Kahneman und Amos Tversky beschrieben in den 1970er-Jahren die Planning Fallacy, die Beobachtung, dass Menschen Projekte systematisch zu optimistisch einschätzen, selbst wenn sie über Erfahrung mit ähnlichen Vorhaben verfügen. Das Bemerkenswerte an der Planning Fallacy ist, dass Wissen um ihre Existenz kaum hilft: Selbst wer weiß, dass die eigene letzte Schätzung um den Faktor drei daneben lag, wird die nächste Schätzung nicht entsprechend korrigieren. Der Optimism Bias sorgt dafür, dass man Risiken und Hindernisse unterschätzt. Der Ankereffekt bewirkt, dass die erste genannte Zahl alle weiteren Schätzungen in ihre Richtung zieht, unabhängig davon, wie gut diese erste Zahl begründet war.

In Teams kommen Gruppeneffekte hinzu. Beim Planning-Poker etwa, einer verbreiteten Schätzmethode in agilen Teams, schätzen alle Beteiligten gleichzeitig, um genau diese gegenseitige Beeinflussung zu reduzieren. Die Methode enthält noch ein weiteres stilles Eingeständnis: Die verwendeten Werte entsprechen einer angenäherten Fibonacci-Folge. Kleine Aufgaben lassen sich feingranular einordnen, deshalb gibt es die Werte 1, 2, 3 und 5. Große Aufgaben hingegen lassen sich nur noch grob schätzen. Dort genügt eine 13, eine 20 oder eine 40. Die Skala selbst bildet ab, was die Branche längst weiß: je größer die Aufgabe, desto unzuverlässiger die Schätzung.

Ein weiterer oft unterschätzter Faktor ist die Verwechslung von Code Complete mit Feature Complete. Viele Schätzungen enden gedanklich beim abschließenden Commit. Aber zum fertigen Feature gehören auch Debugging, Fehlersuche, Testen, Integration, Code Review, Dokumentation und Deployment. Diese Tätigkeiten machen häufig den größeren Teil des Gesamtaufwands aus, tauchen in der ursprünglichen Schätzung aber häufig nicht auf.

Dazu kommt ein Phänomen, das Fred Brooks bereits 1975 in „The Mythical Man-Month“ beschrieb und das als Brook’s Law bekannt ist: Wenn ein Softwareprojekt in Verzug gerät und zusätzliche Mitarbeiterinnen und Mitarbeiter hinzugezogen werden, wird es noch später fertig. Der Kommunikationsaufwand in einem Team wächst quadratisch mit der Teamgröße und die Einarbeitung neuer Kolleginnen und Kollegen bindet Kapazitäten bei denen, die ohnehin schon unter Druck stehen. Zwischen je zwei Personen entsteht ein Kommunikationskanal. Ein Team von 5 Personen hat deshalb 10 solcher Kanäle, ein Team von 10 bereits 45. Jede zusätzliche Person bringt nicht nur einen neuen Kanal mit, sondern so viele, wie es bereits Teammitglieder gibt. Mit anderen Worten: Jede zusätzliche Person erhöht nicht nur die Kapazität, sondern auch den Koordinationsaufwand und ab einem bestimmten Punkt überwiegt der Aufwand den Nutzen. Diese Erkenntnis ist seit einem halben Jahrhundert bekannt und wird trotzdem regelmäßig ignoriert.

Wenn Schätzungen aus strukturellen und psychologischen Gründen unzuverlässig sind, stellt sich die Frage, ob es bessere Ansätze gibt. Die gibt es tatsächlich, auch wenn keiner das Grundproblem löst.

Die Bewegung unter dem Schlagwort No Estimates vertritt die radikalste Position: Statt besser zu schätzen, sollte man die Frage ändern. Wenn Arbeit in so kleine Einheiten zerlegt wird, dass jede davon in wenigen Tagen abgeschlossen werden kann, verliert die Frage nach der Gesamtschätzung an Bedeutung. Der Fortschritt wird nicht prognostiziert, sondern gemessen. Was gestern geliefert wurde, ist ein besserer Indikator für morgen als jede Schätzung von vor drei Monaten.

Wer dennoch eine Vorhersage benötigt, findet in probabilistischen Methoden einen Mittelweg. Monte-Carlo-Simulationen etwa nutzen historische Daten, um nicht einen einzelnen Schätzwert, sondern eine Wahrscheinlichkeitsverteilung zu erzeugen: „Mit 85 Prozent Wahrscheinlichkeit sind wir in acht bis zwölf Wochen fertig“ ist eine ehrlichere Aussage als „Das dauert zehn Wochen“. Sie macht die Unsicherheit sichtbar, statt sie hinter einer Pseudopräzision zu verstecken. Und sie zwingt alle Beteiligten, über Risikotoleranz zu sprechen, statt über vermeintlich feste Termine.

Der vielleicht wirksamste Ansatz ist jedoch ein grundsätzlich anderer Umgang mit der Frage. Statt ein Projekt als monolithischen Block zu denken, der vorab vollständig geschätzt und geplant werden muss, lässt sich in vielen Fällen iterativ vorgehen. Schritt für Schritt, solange, bis das Ergebnis gut genug ist oder bis das investierte Budget erschöpft ist. Wenn eine Software von Anfang an Wertschöpfung liefert, wenn jede Iteration ein nutzbares Ergebnis hervorbringt, dann verschiebt sich die Frage von „Wann ist alles fertig?“ zu „Lohnt sich der nächste Schritt noch?“. Die Entscheidung über Fortführung oder Abbruch wird dann nicht auf Basis einer Schätzung getroffen, die Monate zurückliegt, sondern auf Basis des tatsächlich erreichten Fortschritts und des investierten Aufwands.

Das funktioniert nicht in jedem Kontext, etwa wenn regulatorische Anforderungen ein vollständiges System voraussetzen oder wenn ein physisches Produkt zu einem festen Termin ausgeliefert werden muss. Aber in überraschend vielen Fällen ist es ein tragfähiger Ansatz, der zumindest eine ehrliche Diskussion wert ist. Er verlagert das Risiko von einer großen Vorabentscheidung auf viele kleine Entscheidungen im Verlauf, bei denen jeweils mehr Wissen zur Verfügung steht als zu Beginn.

Der deutsche Typograph Paul Renner schrieb einst: „Der Glaube an das Zählen und Messen verführt in allen Künsten zu den gröbsten Fehlern.“ Die Softwarebranche ist diesem Trugschluss erlegen, dass sich ein kreativer Prozess mit denselben Methoden planen ließe wie ein industrieller. Softwareschätzungen scheitern nicht an mangelnder Disziplin, nicht an fehlender Erfahrung und nicht an schlechten Werkzeugen. Sie scheitern an der Natur von Software als ausführbares Wissen und an der Natur menschlichen Denkens, gleichzeitig und auf eine Weise, die sich nicht einfach wegoptimieren lässt.

Wer das akzeptiert, kann anders mit Schätzungen umgehen. Andere Fragen stellen, andere Formate wählen, andere Erwartungen setzen. Schätzungen als das behandeln, was sie sind: Annäherungen unter Unsicherheit, die regelmäßig überprüft und angepasst werden müssen. Und vor allem aufhören, Schätzungen als Versprechen zu behandeln, denn genau das sind sie nicht, und genau das können sie nicht sein.

Die Illusion, ein kreativer Prozess ließe sich wie eine Fertigungsstraße planen, ist dabei nur eine von mehreren, denen die Softwarebranche erlegen ist. Eine andere, mindestens ebenso hartnäckige, betrifft die Frage nach der Fehlerfreiheit: die Vorstellung, dass sich Bugs mit genügend Sorgfalt und den richtigen Werkzeugen vollständig vermeiden ließen.

Warum auch das eine Illusion ist, und warum selbst „Hello World“ einen Bug enthält, ist Thema des zweiten Teils.


(who)



Source link

Weiterlesen

Entwicklung & Code

Software Testing: So ändert Agentic Engineering die Softwareentwicklung


Richard Seidl spricht im Interview mit Benedikt Stemmildt über die Veränderung, die Agentic Engineering in der Softwareentwicklung anstößt. Im Mittelpunkt steht die Frage, warum dieser Begriff besser passt als der des Vibe Coding. Die Diskussion dreht sich um den praktischen Umgang mit Code, Qualitätsprinzipien und die wachsende Rolle von Architekturarbeit.

Weiterlesen nach der Anzeige


Richard Seidl

Richard Seidl

Richard Seidl ist Berater, Speaker und Podcast-Host. Für ihn ist klar: Wer heute exzellente Software kreieren möchte, denkt den Entwicklungsprozess ganzheitlich: Menschen, Kontext, Methoden und Tools. Er hat seine Erfahrungen in acht Fachbüchern veröffentlicht, betreibt erfolgreich zwei Community-Podcasts und ist Beirat der heise-Konferenz betterCode() Testing.

„Wenn du schlechte Prinzipien hast und AI anwendest, dann wirst du schneller schlechter. Und wenn du gute Prinzipien hast, wirst du schneller besser.“ – Benedikt Stemmildt

Als Technologe seit Kindheitstagen widmet sich Benedikt Stemmildt seit über 20 Jahren der Aufgabe, die Arbeitswelt von Entwicklerinnen und Entwicklern zu verbessern. Developer Experience ist seine Leidenschaft und Mission: Teams dabei zu unterstützen, die neue Arbeitsweise des Agentic Software Engineering zu adaptieren und gewinnbringend einzusetzen.

Dieses Format fokussiert sich auf Softwarequalität: Ob Testautomatisierung, Qualität in agilen Projekten, Testdaten oder Testteams – Richard Seidl und seine Gäste betrachten die Dinge, die die Qualität in der Softwareentwicklung steigern.

Die aktuelle Episode ist auch auf Richard Seidls Blog verfügbar.

Weiterlesen nach der Anzeige


(who)



Source link

Weiterlesen

Entwicklung & Code

Neu in .NET 10.0 [24]: LINQ-Operatoren RightJoin() und LeftJoin() in EF Core


.NET 10.0 führt die LINQ-Operatoren LeftJoin() und RightJoin() ein, die auch in Entity Framework Core 10.0 mit allen Datenbankmanagementsystemen funktionieren: Der Object Relational Mapper (ORM) übersetzt sie in entsprechende SQL-Befehle, siehe die zugehörigen Issues zu LeftJoin und RightJoin.

Weiterlesen nach der Anzeige


Der Dotnet-Doktor – Holger Schwichtenberg

Der Dotnet-Doktor – Holger Schwichtenberg

Dr. Holger Schwichtenberg ist technischer Leiter des Expertennetzwerks www.IT-Visions.de, das mit 53 renommierten Experten zahlreiche mittlere und große Unternehmen durch Beratungen und Schulungen sowie bei der Softwareentwicklung unterstützt. Durch seine Auftritte auf zahlreichen nationalen und internationalen Fachkonferenzen sowie mehr als 90 Fachbücher und mehr als 1500 Fachartikel gehört Holger Schwichtenberg zu den bekanntesten Experten für .NET und Webtechniken in Deutschland.

Zuvor musste man einen Left (Outer) Join und Right (Outer) Join wie in LINQ-to-Objects umständlich mit GroupJoin() und SelectMany() sowie DefaultIfEmpty() bilden.

Einige Codebeispiele zeigen im Folgenden den Einsatz von RightJoin() und LeftJoin() bei Entity Framework Core 10.0 in Verbindung mit einer relationalen Datenbank. Auch in Entity Framework konnte man das Ergebnis von RightJoin() und LeftJoin() bisher schon erzielen via GroupJoin() und SelectMany() mit DefaultIfEmpty(). Die Beispiele beginnen mit der alten Variante und zeigen dann die neue Variante.


betterCode() .NET 11.0

betterCode() .NET 11.0

(Bild: King / stock.adobe.com)

Das ist neu in .NET 11.0: Dr. Holger Schwichtenberg und weitere Experten präsentieren am 17. November 2026 auf der Online-Konferenz betterCode() .NET 11.0 die Änderungen für Entwicklerinnen und Entwickler in .NET SDK, C# 15.0 und mehr. Bis zur Veröffentlichung des Programms sind vergünstigte Blind-Bird-Tickets verfügbar.

Folgender Code zeigt LeftJoin() in einer Datenbank mit den Tabellen „Flight“ und „Pilot“:


/// 
 /// LeftJoin(): Suche alle Flüge, zu denen es keinen Piloten gibt
 /// 
 public void EFC10_LeftJoin()
 {
  CUI.Demo(nameof(EFC10_LeftJoin));
  var ctx = new DA.WWWings.WwwingsV1EnContext();
 
  #region --------------- ALT
  CUI.H2("ALT: Suche alle Flüge, zu denen es keinen Piloten gibt via GroupJoin() und SelectMany()");
  var fluegeOhnePilotAlt = ctx.Flights
    .GroupJoin(
        ctx.Pilots,
        f => f.PilotPersonId,
        p => p.PersonId,
        (f, pilots) => new { Flight = f, Pilots = pilots.DefaultIfEmpty() }
    )
    .SelectMany(
        fp => fp.Pilots,
        (fp, p) => new
        {
         fp.Flight.FlightNo,
         fp.Flight.Departure,
         fp.Flight.Destination,
         fp.Flight.FlightDate,
         PilotId = fp.Flight.PilotPersonId == null ? "n/a" : fp.Flight.PilotPersonId.ToString(),
         GivenName = p.Employee.Person.GivenName,
         Surname = p.Employee.Person.Surname
        }
    )
    .Where(x => x.Surname == null)
    .Take(20)
    .ToList();
 
  Console.WriteLine("Gefundene Flüge: " + fluegeOhnePilotAlt.Count);
  foreach (var item in fluegeOhnePilotAlt)
  {
   Console.WriteLine($"{item.FlightNo} {item.Departure}->{item.Destination} am {item.FlightDate}: Pilot {item.PilotId} {item.GivenName} {item.Surname}");
  }
  #endregion
 
  #region --------------- NEU
  CUI.H2("NEU: Suche alle Flüge, zu denen es keinen Piloten gibt via LeftJoin()");
 
  var fluegeOhnePilotNeu = ctx.Flights
    .LeftJoin(
        ctx.Pilots,
        f => f.PilotPersonId,
        p => p.PersonId,
        (f, p) => new
        {
         f.FlightNo,
         f.Departure,
         f.Destination,
         f.FlightDate,
         PilotId = f.PilotPersonId == null ? "n/a" : f.PilotPersonId.ToString(),
         p.Employee.Person.GivenName,
         p.Employee.Person.Surname,
        }).Where(x => x.Surname == null).Take(20).ToList();
 
  Console.WriteLine("Gefundene Flüge: " + fluegeOhnePilotNeu.Count);
  foreach (var item in fluegeOhnePilotNeu)
  {
   Console.WriteLine($"{item.FlightNo} {item.Departure}->{item.Destination} am {item.FlightDate}: Pilot {item.PilotId} {item.GivenName} {item.Surname}");
  }
  #endregion
 
  #region --------------- Kontrolle
  CUI.H3("Zur Kontrolle:");
  // Zur Kontrolle:
  if (fluegeOhnePilotNeu.Count() > 0)
  {
   var f = ctx.Flights.Find(fluegeOhnePilotNeu[0].FlightNo);
   Console.WriteLine(f.ToNameValueString());
  }
  #endregion
 
 }


Aus diesem LINQ-Befehl mit LeftJoin() entsteht folgender SQL-Befehl:

Weiterlesen nach der Anzeige


SELECT TOP(@p) [f].[FlightNo], [f].[Departure], [f].[Destination], [f].[FlightDate], CASE
  WHEN [f].[Pilot_PersonID] IS NULL THEN 'n/a'
  ELSE COALESCE(CONVERT(varchar(11), [f].[Pilot_PersonID]), '')
END AS [PilotId], [p0].[GivenName], [p0].[Surname]
FROM [Operation].[Flight] AS [f]
LEFT JOIN [People].[Pilot] AS [p] ON [f].[Pilot_PersonID] = [p].[PersonID]
LEFT JOIN [People].[Employee] AS [e] ON [p].[PersonID] = [e].[PersonID]
LEFT JOIN [People].[Person] AS [p0] ON [e].[PersonID] = [p0].[PersonID]
WHERE [p0].[Surname] IS NULL


Folgender Code zeigt RightJoin() in einer Datenbank mit den Tabellen „Flight“ und „Pilot“:


/// 
 /// Gibt zu den letzten drei angelegten Piloten alle Flüge aus
 /// 
 public void EFC10_RightJoin()
 {
  CUI.Demo(nameof(EFC10_RightJoin));
  var ctx = new DA.WWWings.WwwingsV1EnContext();
 
  CUI.H2("Alt: Gibt zu den letzten drei angelegten Piloten alle Flüge aus via GroupJoin() und SelectMany()");
 
  var ctx2 = new DA.WWWings.WwwingsV1EnContext();
 
  #region --------------- ALT
  var pilotenMitFlugAlt = ctx.Pilots
    .OrderByDescending(x => x.PersonId)
    .Take(3)
    .GroupJoin(
        ctx.Flights,
        p => p.PersonId,
        f => f.PilotPersonId,
        (p, flights) => new { Pilot = p, Flights = flights.DefaultIfEmpty() }
    )
    .SelectMany(
        pf => pf.Flights,
        (pf, f) => new
        {
         PilotId = pf.Pilot.PersonId,
         pf.Pilot.Employee.Person.GivenName,
         pf.Pilot.Employee.Person.Surname,
         Flight = f,
         f.Departure,
         f.Destination,
        }
    )
    .OrderBy(x => x.PilotId)
    .ToList();
 
  foreach (var p in pilotenMitFlugAlt)
  {
   Console.WriteLine($"Pilot #{p.PilotId} {p.GivenName} {p.Surname} fliegt " + (p.Flight != null ? $"Flug #{p.Flight?.FlightNo} {p.Flight.Departure}->{p.Flight.Destination} am {p.Flight.FlightDate}" : "bisher keinen Flug"));
  }
  Console.WriteLine("Anzahl: " + pilotenMitFlugAlt.Count);
  #endregion
 
  #region --------------- NEU
  CUI.H2("Neu: Gibt zu den letzten drei angelegten Piloten alle Flüge aus via RightJoin()");
  var pilotenMitFlugNeu = ctx.Flights
    .RightJoin(
        ctx.Pilots.OrderByDescending(x => x.PersonId).Take(3),
        f => f.PilotPersonId,
        p => p.PersonId,
        (f, p) => new
        {
         PilotId = p.PersonId,
         p.Employee.Person.GivenName,
         p.Employee.Person.Surname,
         Flight = f,
         f.Departure,
         f.Destination,
        }).OrderBy(x => x.PilotId).ToList();
  
  foreach (var p in pilotenMitFlugNeu)
  {
   Console.WriteLine($"Pilot #{p.PilotId} {p.GivenName} {p.Surname} fliegt " + (p.Flight != null ? $"Flug #{p.Flight?.FlightNo} {p.Flight.Departure}->{p.Flight.Destination} am {p.Flight.FlightDate}" : "bisher keinen Flug"));
  }
  Console.WriteLine("Anzahl: " + pilotenMitFlugNeu.Count);
  #endregion
 
  #region --------------- Kontrolle
  CUI.H2("Nur zur Kontrolle: Gibt zu den letzten drei angelegten Piloten alle Flüge aus via Navigation Property");
 
  var pilotenMitFlug2 = ctx.Pilots.Include(p => p.Employee).ThenInclude(p => p.Person).Include(p => p.Flights).OrderByDescending(x => x.PersonId).Take(3).OrderBy(x => x.PersonId).ToList();
  int count = 0;
  foreach (Pilot p in pilotenMitFlug2)
  {
   if (p.Flights.Count == 0)
   {
    count++;
    Console.WriteLine($"Pilot #{p.PersonId} {p.Employee.Person.GivenName} {p.Employee.Person.Surname} fliegt bisher keinen Flug");
   }
   else
   {
    foreach (var f in p.Flights)
    {
     count++;
     Console.WriteLine($"Pilot #{p.PersonId} {p.Employee.Person.GivenName} {p.Employee.Person.Surname} fliegt Flug #{f.FlightNo} {f.Departure}->{f.Destination} am {f.FlightDate}");
   
    }
   }
  }
  Console.WriteLine("Anzahl: " + count);
  #endregion
 }


Aus dem LINQ-Befehl mit RightJoin() entsteht folgender SQL-Befehl:


SELECT [p0].[PersonID] AS [PilotId], [p1].[GivenName], [p1].[Surname], [f].[FlightNo], [f].[Airline], [f].[Departure], [f].[Destination], [f].[FlightDate], [f].[FreeSeats], [f].[Memo], [f].[NonSmokingFlight], [f].[Pilot_PersonID], [f].[Seats], [f].[Timestamp]
FROM [Operation].[Flight] AS [f]
RIGHT JOIN (
  SELECT TOP(@p) [p].[PersonID]
  FROM [People].[Pilot] AS [p]
  ORDER BY [p].[PersonID] DESC
) AS [p0] ON [f].[Pilot_PersonID] = [p0].[PersonID]
INNER JOIN [People].[Employee] AS [e] ON [p0].[PersonID] = [e].[PersonID]
INNER JOIN [People].[Person] AS [p1] ON [e].[PersonID] = [p1].[PersonID]
ORDER BY [p0].[PersonID]


Es sei zudem explizit darauf hingewiesen, dass man die neuen Operatoren RightJoin() und LeftJoin() einschließlich der vorher schon vorhandenen Operatoren Join() und GroupJoin() nur für die Verbindung von Tabellen braucht, für die es im Objektmodell keine Navigationsbeziehung gibt.

So kann man statt des aufwendigen RightJoin() bei einer vorhandenen Navigationsbeziehung im Objektmodell dasselbe Ausgabeergebnis mit einem Include() erreichen. In diesem Fall erhält man allerdings keine flache Liste mit Daten aus Pilot und Flug, sondern eine Objekthierarchie, daher zwei verschachtelte foreach-Schleifen.

Folgender Code verwendet ein Include() über Navigationsbeziehung statt RightJoin():


CUI.H2("Gibt zu den letzten drei angelegten Piloten alle Flüge aus via Navigation Property");
 
 var pilotenMitFlug2 = ctx.Pilots.Include(p => p.Employee).ThenInclude(p => p.Person).Include(p => p.Flights).OrderByDescending(x => x.PersonId).Take(3).OrderBy(x => x.PersonId).ToList();
 
 foreach (Pilot p in pilotenMitFlug2)
 {
  if (p.Flights.Count == 0)
  {
   Console.WriteLine($"Pilot #{p.PersonId} {p.Employee.Person.GivenName} {p.Employee.Person.Surname} fliegt bisher keinen Flug");
  }
  else
  {
   foreach (var f in p.Flights)
   {
    Console.WriteLine($"Pilot #{p.PersonId} {p.Employee.Person.GivenName} {p.Employee.Person.Surname} fliegt Flug #{f.FlightNo} {f.Departure}->{f.Destination} am {f.FlightDate}");
   }
  }
 }


Entity Framework Core führt dabei nur Inner Joins aus:


SELECT [p0].[PersonID], [p0].[FlightHours], [p0].[FlightSchool], [p0].[LicenseDate], [p0].[LicenseType], [e].[PersonID], [e].[EmployeeNo], [e].[HireDate], [e].[Supervisor_PersonId], [p1].[PersonID], [p1].[Birthday], [p1].[City], [p1].[Country], [p1].[EMail], [p1].[GivenName], [p1].[Memo], [p1].[Photo], [p1].[Surname], [f].[FlightNo], [f].[Airline], [f].[Departure], [f].[Destination], [f].[FlightDate], [f].[FreeSeats], [f].[Memo], [f].[NonSmokingFlight], [f].[Pilot_PersonID], [f].[Seats], [f].[Timestamp]
FROM (
  SELECT TOP(@p) [p].[PersonID], [p].[FlightHours], [p].[FlightSchool], [p].[LicenseDate], [p].[LicenseType]
    FROM [People].[Pilot] AS [p]
    ORDER BY [p].[PersonID] DESC
   ) AS [p0]
INNER JOIN [People].[Employee] AS [e] ON [p0].[PersonID] = [e].[PersonID]
INNER JOIN [People].[Person] AS [p1] ON [e].[PersonID] = [p1].[PersonID]
LEFT JOIN [Operation].[Flight] AS [f] ON [p0].[PersonID] = [f].[Pilot_PersonID]
ORDER BY [p0].[PersonID], [e].[PersonID], [p1].[PersonID]



(rme)



Source link

Weiterlesen

Beliebt