Regel 1. So einfach wie möglich, aber nicht einfacher

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Programmieren ist schwer .

Ich schätze, du hast das schon herausgefunden. Jeder, der ein Buch mit dem Titel " Die Regeln des Programmierens " in die Hand nimmt und liest, wird wahrscheinlich beides tun:

  • Programmieren können, zumindest ein bisschen

  • Frustriert sein, dass es nicht einfacher ist, als es ist

Es gibt viele Gründe, warum Programmieren schwer ist, und viele Strategien, um es einfacher zu machen. Dieses Buch befasst sich mit einer sorgfältig ausgewählten Auswahl an häufigen Fehlern und Regeln, um diese Fehler zu vermeiden, die ich in den vielen Jahren, in denen ich selbst Fehler gemacht und mit den Fehlern anderer umgegangen bin, entwickelt habe.

Es gibt ein allgemeines Muster für die Regeln, ein gemeinsames Thema, das die meisten von ihnen teilen. Es lässt sich am besten mit einem Zitat von Albert Einstein zusammenfassen, das die Ziele eines Physiktheoretikers beschreibt: "So einfach wie möglich, aber nicht einfacher."1 Damit meinte Einstein, dass die beste physikalische Theorie die einfachste ist, die alle beobachtbaren Phänomene vollständig beschreibt.

Übertragen auf die Programmierung bedeutet das: Die beste Lösung für ein Problem ist die einfachste, die alle Anforderungen des Problems erfüllt. Der beste Code ist der einfachste Code.

Stell dir vor, du schreibst einen Code, der die Anzahl der gesetzten Bits in einer ganzen Zahl zählt. Es gibt viele Möglichkeiten, dies zu tun. Du könntest Bittricks verwenden2 um ein Bit nach dem anderen auf Null zu setzen und zu zählen, wie viele Bits auf Null gesetzt werden:

int countSetBits(int value)
{
    int count = 0;

    while (value)
    {
        ++count;
        value = value & (value - 1);
    }

    return count;
}

Oder du entscheidest dich für eine schleifenfreie Implementierung mit Bitverschiebung und Maskierung, um die Bits parallel zu zählen:

int countSetBits(int value)
{
    value = ((value & 0xaaaaaaaa) >> 1) + (value & 0x55555555);
    value = ((value & 0xcccccccc) >> 2) + (value & 0x33333333);
    value = ((value & 0xf0f0f0f0) >> 4) + (value & 0x0f0f0f0f);
    value = ((value & 0xff00ff00) >> 8) + (value & 0x00ff00ff);
    value = ((value & 0xffff0000) >> 16) + (value & 0x000ffff);

    return value;
}

Oder du schreibst einfach einen möglichst offensichtlichen Code:

int countSetBits(int value)
{
    int count = 0;

    for (int bit = 0; bit < 32; ++bit)
    {
        if (value & (1 << bit))
            ++count;
    }

    return count;
}

Die ersten beiden Antworten von sind clever... und das meine ich nicht als Kompliment.3 Ein kurzer Blick reicht nicht aus, um herauszufinden, wie die beiden Beispiele tatsächlich funktionieren - sie haben jeweils ein kleines Häppchen "Warte... was?"-Code in der Schleife versteckt. Mit ein bisschen Nachdenken kannst du herausfinden, was los ist, und es macht Spaß, den Trick zu sehen. Aber die Dinge zu entwirren, erfordert einige Mühe.

Und das mit einem Vorsprung! Ich habe dir gesagt, was die Funktionen tun, bevor ich den Code gezeigt habe, und die Funktionsnamen machen ihren Zweck deutlich. Wenn du nicht gewusst hättest, dass der Code gesetzte Bits zählt, wäre das Entwirren der ersten beiden Beispiele noch mehr Arbeit gewesen.

Das ist bei der endgültigen Antwort nicht der Fall. Es ist offensichtlich, dass es um das Zählen der gesetzten Bits geht. Sie ist so einfach wie möglich, aber nicht einfacher, und das macht sie besser als die ersten beiden Antworten.4

Einfachheit messen

Es gibt viele Möglichkeiten, darüber nachzudenken, was Code einfach macht.

Du könntest dich dafür entscheiden, die Einfachheit daran zu messen, wie einfach der Code für jemand anderen in deinem Team zu verstehen ist. Wenn ein zufällig ausgewählter Kollege einen Teil des Codes durchlesen und mühelos verstehen kann, dann ist der Code angemessen einfach.

Oder du entscheidest dich, die Einfachheit daran zu messen, wie einfach es ist, den Code zu erstellen - nicht nur die Zeit, die du zum Tippen brauchst, sondern auch die Zeit, die du brauchst, um den Code voll funktionsfähig und fehlerfrei zu machen.5 Komplizierter Code braucht eine Weile, bis er richtig funktioniert; einfacher Code ist leichter über die Ziellinie zu bringen.

Diese beiden Maßnahmen überschneiden sich natürlich stark. Code, der leicht zu schreiben ist, ist in der Regel auch leicht zu lesen. Und es gibt noch andere gültige Maßstäbe für Komplexität, die du verwenden kannst:

Wie viel Code geschrieben wird

Einfacher Code ist in der Regel kürzer, aber es ist möglich, eine Menge Komplexität in eine einzige Codezeile zu packen.

Wie viele Ideen werden eingeführt

Einfacher Code baut in der Regel auf den Konzepten auf, die jeder in deinem Team kennt; er führt keine neuen Denkweisen über Probleme oder neue Terminologie ein.

Wie viel Zeit es braucht, um zu erklären

Einfacher Code ist leicht zu erklären - bei einer Codeüberprüfung ist er so offensichtlich, dass der Prüfer ihn einfach durchgehen kann. Komplizierter Code braucht eine Erklärung.

Ein Code, der nach einem Maßstab einfach erscheint, wird auch nach den anderen Maßstäben einfach erscheinen. Du musst dich nur entscheiden, welcher der Maßstäbe den klarsten Fokus für deine Arbeit liefert - aber ich empfehle, mit der Einfachheit der Erstellung und der Verständlichkeit zu beginnen. Wenn du dich darauf konzentrierst, dass dein Code leicht zu lesen ist und schnell funktioniert, erstellst du einfachen Code.

...aber nicht einfacher

Es ist besser wenn der Code einfacher ist, aber er muss trotzdem das Problem lösen, das er lösen soll.

Stell dir vor, du versuchst zu zählen, wie viele Möglichkeiten es gibt, eine Leiter mit einer bestimmten Anzahl von Sprossen zu besteigen, wobei du mit jedem Schritt eine, zwei oder drei Sprossen gewinnst. Wenn die Leiter zwei Sprossen hat, gibt es zwei Möglichkeiten, sie zu besteigen - entweder du trittst auf die erste Sprosse oder nicht. Ebenso gibt es vier Möglichkeiten, eine Leiter mit drei Sprossen zu besteigen: Du kannst auf die erste Sprosse treten, auf die zweite Sprosse treten, auf die erste und zweite Sprosse treten oder direkt auf die oberste Sprosse treten. Eine Leiter mit vier Sprossen kann auf sieben Arten erklommen werden, eine Leiter mit fünf Sprossen auf dreizehn Arten und so weiter.

Du kannst einen einfachen Code schreiben, um dies rekursiv zu berechnen:

int countStepPatterns(int stepCount)
{
    if (stepCount < 0)
        return 0;
    
    if (stepCount == 0)
        return 1;

    return countStepPatterns(stepCount - 3) + 
           countStepPatterns(stepCount - 2) + 
           countStepPatterns(stepCount - 1);
}

Der Grundgedanke ist, dass jeder Weg auf der Leiter von einer der drei Sprossen darunter zur obersten Sprosse führen muss. Wenn du die Anzahl der Wege zu jeder dieser Sprossen addierst, erhältst du die Anzahl der Möglichkeiten, die oberste Sprosse zu erreichen. Danach geht es nur noch darum, die Basisfälle herauszufinden. Der vorangegangene Code erlaubt negative Schrittzahlen als Basisfall, um die Rekursion zu vereinfachen.

Leider funktioniert diese Lösung nicht. Nun, sie funktioniert, zumindest für kleine stepCount Werte, aber countStepPatterns(20) braucht etwa doppelt so lange wie countStepPatterns(19). Computer sind wirklich schnell, aber ein exponentielles Wachstum wie dieses wird diese Geschwindigkeit einholen. In meinem Test wurde der Beispielcode ziemlich langsam, als stepCount in die Zwanziger Jahre kam.

Wenn du die Anzahl der Wege auf längeren Leitern zählen sollst, dann ist dieser Code zu einfach. Das Hauptproblem ist, dass alle Zwischenergebnisse von countStepPatterns immer wieder neu berechnet werden, was zu exponentiellen Laufzeiten führt. Eine Standardantwort auf ist die Memoisierung - das Festhalten der berechneten Zwischenwerte und ihre Wiederverwendung, wie in diesem Beispiel:

int countStepPatterns(unordered_map<int, int> * memo, int rungCount)
{
    if (rungCount < 0)
        return 0;
    
    if (rungCount == 0)
        return 1;

    auto iter = memo->find(rungCount);
    if (iter != memo->end())
        return iter->second;

    int stepPatternCount = countStepPatterns(memo, rungCount - 3) + 
                           countStepPatterns(memo, rungCount - 2) + 
                           countStepPatterns(memo, rungCount - 1);

    memo->insert({ rungCount, stepPatternCount });
    return stepPatternCount;
}

int countStepPatterns(int rungCount)
{
    unordered_map<int, int> memo;
    return countStepPatterns(&memo, rungCount);
}

Mit der Memoisierung wird jeder Wert einmal berechnet und in die Hash-Map eingefügt. Nachfolgende Aufrufe finden den berechneten Wert in der Hash-Map in annähernd konstanter Zeit, und das exponentielle Wachstum verschwindet. Der memoisierte Code ist ein bisschen komplizierter, aber er stößt nicht an die Leistungsgrenze.

Du könntest dich auch für die dynamische Programmierung entscheiden und so einen Kompromiss zwischen konzeptioneller Komplexität und einfacherem Code eingehen:

int countStepPatterns(int rungCount)
{
    vector<int> stepPatternCounts = { 0, 0, 1 };

    for (int rungIndex = 0; rungIndex < rungCount; ++rungIndex)
    {
        stepPatternCounts.push_back(
            stepPatternCounts[rungIndex + 0] +
            stepPatternCounts[rungIndex + 1] +
            stepPatternCounts[rungIndex + 2]);
    }

    return stepPatternCounts.back();
}

Auch dieser Ansatz läuft schnell genug und ist noch einfacher als die memoisierte rekursive Version.

Manchmal ist es besser, das Problem zu vereinfachen , als die Lösung

Die Probleme in der ursprünglichen, rekursiven Version von countStepPatterns traten bei längeren Leitern auf. Der einfachste Code funktionierte bei einer kleinen Anzahl von Sprossen einwandfrei, stieß aber bei einer großen Anzahl von Sprossen an eine exponentielle Leistungsgrenze. Spätere Versionen umgingen die exponentielle Leistungsgrenze um den Preis einer etwas höheren Komplexität... aber sie stießen bald auf ein anderes Problem.

Wenn ich den vorherigen Code zur Berechnung von countStepPatterns(36) aufrufe, erhalte ich die richtige Antwort: 2.082.876.103. Wenn ich jedoch countStepPatterns(37) aufrufe, erhalte ich -463.960.867. Das ist eindeutig nicht richtig!

Das liegt daran, dass die Version von C++, die ich verwende, ganze Zahlen als vorzeichenbehaftete 32-Bit-Werte speichert, und die Berechnung von countStepPatterns(37) die verfügbaren Bits übersteigt. Es gibt 3.831.006.429 Möglichkeiten, eine Leiter mit 37 Sprossen hochzuklettern, und diese Zahl ist zu groß, um in eine 32-Bit-Ganzzahl mit Vorzeichen zu passen.

Vielleicht ist der Code immer noch zu einfach. Es ist doch vernünftig zu erwarten, dass countStepPatterns für alle Leiterlängen funktioniert, oder? C++ hat keine Standardlösung für wirklich große Ganzzahlen, aber es gibt (viele) Open-Source-Bibliotheken, die verschiedene Arten von Ganzzahlen mit beliebiger Genauigkeit implementieren. Mit ein paar hundert Codezeilen könntest du auch deine eigene implementieren:

struct Ordinal
{
public:

    Ordinal() :
        m_words()
        { ; }
    Ordinal(unsigned int value) :
        m_words({ value })
        { ; }

    typedef unsigned int Word;

    Ordinal operator + (const Ordinal & value) const
    {
        int wordCount = max(m_words.size(), value.m_words.size());

        Ordinal result;
        long long carry = 0;

        for (int wordIndex = 0; wordIndex < wordCount; ++wordIndex)
        {
            long long sum = carry + 
                            getWord(wordIndex) + 
                            value.getWord(wordIndex);

            result.m_words.push_back(Word(sum));
            carry = sum >> 32;
        }

        if (carry > 0)
            result.m_words.push_back(Word(carry));

        return result;
    }

protected:

    Word getWord(int wordIndex) const
    {
        return (wordIndex < m_words.size()) ? m_words[wordIndex] : 0;
    }

    vector<Word> m_words;
};

Wenn du im letzten Beispiel Ordinal anstelle von int einfügst, erhältst du genaue Antworten für längere Leitern:

Ordinal countStepPatterns(int rungCount)
{
    vector<Ordinal> stepPatternCounts = { 0, 0, 1 };

    for (int rungIndex = 0; rungIndex < rungCount; ++rungIndex)
    {
        stepPatternCounts.push_back(
            stepPatternCounts[rungIndex + 0] +
            stepPatternCounts[rungIndex + 1] +
            stepPatternCounts[rungIndex + 2]);
    }

    return stepPatternCounts.back();
}

Also...Problem gelöst? Mit der Einführung von Ordinal kann eine genaue Antwort für viel längere Leitern berechnet werden. Sicher, ein paar hundert Codezeilen zur Implementierung von Ordinal hinzuzufügen, ist nicht toll, vor allem, wenn man bedenkt, dass die eigentliche Funktion countStep​Pat⁠terns nur 14 Zeilen lang ist, aber ist das nicht der Preis, den man zahlen muss, um das Problem korrekt zu lösen?

Wahrscheinlich nicht. Wenn es keine einfache Lösung für ein Problem gibt, hinterfrage das Problem, bevor du eine komplizierte Lösung akzeptierst. Ist das Problem, das du zu lösen versuchst, tatsächlich das Problem, das gelöst werden muss? Oder machst du unnötige Annahmen über das Problem, die deine Lösung verkomplizieren?

Wenn du in diesem Fall tatsächlich Sprossenmuster für echte Leitern zählst, kannst du wahrscheinlich von einer maximalen Leiterlänge ausgehen. Wenn die maximale Leiterlänge z. B. 15 Sprossen beträgt, dann ist jede der Lösungen in diesem Abschnitt vollkommen ausreichend, sogar das naive rekursive Beispiel, das zuerst vorgestellt wurde. Füge eine assert hinzu, die auf die eingebauten Grenzen der Funktion hinweist und erkläre den Sieg:

int countStepPatterns(int rungCount)
{
    // NOTE (chris) can't represent the pattern count in an int
    // once we get past 36 rungs...

    assert(rungCount <= 36);

    vector<int> stepPatternCounts = { 0, 0, 1 };

    for (int rungIndex = 0; rungIndex < rungCount; ++rungIndex)
    {
        stepPatternCounts.push_back(
            stepPatternCounts[rungIndex + 0] +
            stepPatternCounts[rungIndex + 1] +
            stepPatternCounts[rungIndex + 2]);
    }

    return stepPatternCounts.back();
}

Oder wenn es darum geht, wirklich lange Leitern zu tragen - z. B. die Inspektionsleiter für eine Windkraftanlage - würde dann eine ungefähre Anzahl von Schritten ausreichen? Wahrscheinlich, und wenn ja, ist es einfach, Ganzzahlen durch Fließkommazahlen zu ersetzen. So einfach, dass ich den Code nicht einmal zeigen werde.

Sieh mal, alles läuft irgendwann über. Die extremen Grenzfälle eines Problems zu lösen, wird immer zu einer übermäßig komplizierten Lösung führen. Lass dich nicht in die Falle locken, die strengste Definition eines Problems zu lösen. Es ist viel besser, eine einfache Lösung für den Teil des Problems zu haben, der tatsächlich gelöst werden muss, als eine komplizierte Lösung für eine breitere Definition des Problems. Wenn du die Lösung nicht vereinfachen kannst, versuche, das Problem zu vereinfachen.

Einfache Algorithmen

Manchmal ist es eine schlechte Wahl des Algorithmus, die deinen Code komplizierter macht. Schließlich gibt es viele Möglichkeiten, ein bestimmtes Problem zu lösen, manche komplizierter als andere. Einfache Algorithmen führen zu einfachem Code. Das Problem ist nur, dass der einfache Algorithmus nicht immer offensichtlich ist!

Sagen wir du schreibst einen Code zum Sortieren eines Kartenspiels. Ein naheliegender Ansatz ist es, das Mischen zu simulieren, das du wahrscheinlich schon als Kind gelernt hast: Du teilst den Stapel in zwei Haufen und fächerst sie dann ineinander auf, so dass die Karten auf jeder Seite ungefähr die gleiche Chance haben, als nächstes im neu gemischten Stapel zu landen. Wiederhole den Vorgang, bis das Deck gemischt ist.6

Das könnte so aussehen:

vector<Card> shuffleOnce(const vector<Card> & cards)
{
    vector<Card> shuffledCards;

    int splitIndex = cards.size() / 2;
    int leftIndex = 0;
    int rightIndex = splitIndex;

    while (true)
    {
        if (leftIndex >= splitIndex)
        {
            for (; rightIndex < cards.size(); ++rightIndex)
                shuffledCards.push_back(cards[rightIndex]);

            break;
        }
        else if (rightIndex >= cards.size())
        {
            for (; leftIndex < splitIndex; ++leftIndex)
                shuffledCards.push_back(cards[leftIndex]);

            break;
        }
        else if (rand() & 1)
        {
            shuffledCards.push_back(cards[rightIndex]);
            ++rightIndex;
        }
        else
        {
            shuffledCards.push_back(cards[leftIndex]);
            ++leftIndex;
        }
    }

    return shuffledCards;
}

vector<Card> shuffle(const vector<Card> & cards)
{
    vector<Card> shuffledCards = cards;

    for (int i = 0; i < 7; ++i)
    {
        shuffledCards = shuffleOnce(shuffledCards);
    }

    return shuffledCards;
}

Der simulierte Riffle-Shuffle-Algorithmus funktioniert, und der Code, den ich hier geschrieben habe, ist eine ziemlich einfache Implementierung dieses Algorithmus. Du musst zwar ein wenig Energie aufwenden, um sicherzustellen, dass alle Indexprüfungen korrekt sind, aber das ist nicht weiter schlimm.

Aber es gibt einfachere Algorithmen, um ein Kartenspiel zu mischen. Du könntest zum Beispiel einen gemischten Kartensatz Karte für Karte aufbauen. Bei jeder Iteration nimmst du eine neue Karte und tauschst sie mit einer zufälligen Karte aus dem Deck der Iteration aus. Du kannst das sogar an Ort und Stelle tun:

vector<Card> shuffle(const vector<Card> & cards)
{
    vector<Card> shuffledCards = cards;

    for (int cardIndex = shuffledCards.size(); --cardIndex >= 0; )
    {
        int swapIndex = rand() % (cardIndex + 1);
        swap(shuffledCards[swapIndex], shuffledCards[cardIndex]);
    }

    return shuffledCards;
}

Nach den zuvor vorgestellten Maßstäben der Einfachheit ist diese Version besser. Es hat weniger Zeit gekostet, sie zu schreiben.7 Sie ist leichter zu lesen. Sie besteht aus weniger Code. Sie ist leichter zu erklären. Sie ist einfacher und besser - nicht wegen des Codes, sondern wegen der besseren Wahl des Algorithmus.

Verliere nicht den Faden

Einfacher Code ist leicht zu lesen - und der einfachste Code kann von oben bis unten durchgelesen werden, so wie ein Buch. Programme sind aber keine Bücher. Wenn der Code nicht einfach ist, kann es leicht passieren, dass er schwer zu verstehen ist. Wenn der Code verworren ist, wenn du von einer Stelle zur anderen springen musst, um dem Fluss der Ausführung zu folgen, ist er viel schwerer zu lesen.

Unübersichtlicher Code kann entstehen, wenn du zu sehr versuchst, jede Idee an genau einer Stelle auszudrücken. Nimm den Riffle-Shuffle-Code von vorhin. Die Teile des Codes, die sich mit dem rechten und linken Kartenstapel befassen, sehen einander ziemlich ähnlich. Die Logik, um eine Karte oder eine Reihe von Karten auf den gemischten Stapel zu verschieben, könnte in separate Funktionen aufgeteilt werden, die dann von shuffleOnce aufgerufen werden:

void copyCard(
    vector<Card> * destinationCards, 
    const vector<Card> & sourceCards,
    int * sourceIndex)
{
    destinationCards->push_back(sourceCards[*sourceIndex]);
    ++(*sourceIndex);
}

void copyCards(
    vector<Card> * destinationCards, 
    const vector<Card> & sourceCards,
    int * sourceIndex,
    int endIndex)
{
    while (*sourceIndex < endIndex)
    {
        copyCard(destinationCards, sourceCards, sourceIndex);
    }
}

vector<Card> shuffleOnce(const vector<Card> & cards)
{
    vector<Card> shuffledCards;

    int splitIndex = cards.size() / 2;
    int leftIndex = 0;
    int rightIndex = splitIndex;

    while (true)
    {
        if (leftIndex >= splitIndex)
        {
            copyCards(&shuffledCards, cards, &rightIndex, cards.size());
            break;
        }
        else if (rightIndex >= cards.size())
        {
            copyCards(&shuffledCards, cards, &leftIndex, splitIndex);
            break;
        }
        else if (rand() & 1)
        {
            copyCard(&shuffledCards, cards, &rightIndex);
        }
        else
        {
            copyCard(&shuffledCards, cards, &leftIndex);
        }
    }

    return shuffledCards;
}

Die vorherige Version von shuffleOnce war von oben nach unten lesbar, diese hier ist es nicht. Das macht es schwieriger zu lesen. Wenn du den Code von shuffleOnce durchliest, stößt du auf die Funktion copyCard oder copyCards. Dann musst du diese Funktionen ausfindig machen, herausfinden, was sie tun, zur ursprünglichen Funktion zurückkehren und dann die von shuffleOnce übergebenen Argumente mit deinem neuen Verständnis von copyCard oder copyCards abgleichen. Das ist viel schwieriger, als nur die Schleifen im Original shuffleOnce zu lesen.

Die "Nicht wiederholen"-Version der Funktion hat also mehr Zeit zum Schreiben gebraucht8 und ist schwieriger zu lesen. Es ist auch mehr Code! Der Versuch, Doppelungen zu entfernen, hat den Code nicht einfacher, sondern komplizierter gemacht.

Es spricht natürlich einiges dafür, die Anzahl der Duplikate in deinem Code zu reduzieren! Aber es ist wichtig zu erkennen, dass das Entfernen von Duplikaten seinen Preis hat - und bei kleinen Mengen an Code und einfachen Ideen ist es besser, die Duplikate einfach zu lassen. Der Code ist dann einfacher zu schreiben und leichter zu lesen .

Eine Regel, die alle regiert

Viele der übrigen Regeln in diesem Buch werden auf dieses Thema der Einfachheit zurückkommen, darauf, den Code so einfach wie möglich zu halten, aber nicht einfacher.

Im Grunde genommen ist Programmieren ein Kampf mit der Komplexität. Wenn du neue Funktionen hinzufügst, wird der Code oft komplizierter - und je komplizierter der Code wird, desto schwieriger wird es, mit ihm zu arbeiten, und desto langsamer wird der Fortschritt. Schließlich kannst du einen Ereignishorizont erreichen, an dem jeder Versuch, voranzukommen - einen Fehler zu beheben oder eine Funktion hinzuzufügen - so viele Probleme verursacht, wie er löst. Weitere Fortschritte sind dann praktisch unmöglich.

Am Ende wird es die Komplexität sein, die dein Projekt tötet.

Das bedeutet, dass es beim effektiven Programmieren darum geht, das Unvermeidliche hinauszuzögern. Füge so wenig Komplexität wie möglich hinzu, wenn Funktionen hinzugefügt und Fehler behoben werden. Suche nach Möglichkeiten, Komplexität zu beseitigen oder Dinge so zu gestalten, dass neue Funktionen die Gesamtkomplexität des Systems nicht zu sehr erhöhen. Sorge dafür, dass die Zusammenarbeit in deinem Team so einfach wie möglich ist.

Wenn du fleißig bist, kannst du das Unvermeidliche auf unbestimmte Zeit hinauszögern. Die ersten Codezeilen von Sucker Punch habe ich vor 25 Jahren geschrieben, und seitdem hat sich die Codebasis ständig weiterentwickelt. Ein Ende ist nicht in Sicht - unser Code ist viel komplizierter als vor 25 Jahren, aber wir haben es geschafft, diese Komplexität unter Kontrolle zu halten, und wir können immer noch effektive Fortschritte machen.

Wir haben es geschafft, die Komplexität zu bewältigen, und das kannst du auch. Bleib wachsam, erinnere dich daran, dass Komplexität der größte Feind ist, und du wirst Erfolg haben.

1 Mit ziemlicher Sicherheit hat er nicht genau diese Worte verwendet - die Hochstapelei hat Einstein einen Gefallen getan, indem sie seine Aphorismen geschärft hat. Die nächste Übereinstimmung in den schriftlichen Aufzeichnungen lautet: "Es kann kaum geleugnet werden, dass das oberste Ziel jeder Theorie darin besteht, die irreduziblen Grundelemente so einfach und so wenig wie möglich zu machen, ohne auf die angemessene Darstellung eines einzigen Erfahrungsdatums verzichten zu müssen." Das ist so ziemlich dasselbe, nur nicht so bissig. Außerdem ist das eigentliche Einstein-Zitat ein bisschen lang für einen Regeltitel.

2 Ich entschuldige mich bei allen Nicht-C++-Programmierern für die Bitverdreher in den nächsten drei Beispielen. Ich verspreche, dass der Rest des Buches nicht mehr so viel mit bitweisen Operationen zu tun haben wird.

3 In einem plausiblen alternativen Universum heißt diese Regel "Cleverness Is Not a Virtue".

4 Moderne Prozessoren haben einen eigenen Befehl, um die Anzahl der in einem Wert gesetzten Bits zu zählen -popcnt auf x86-Prozessoren zum Beispiel, der in einem einzigen Zyklus ausgeführt wird. Man kann sich auch mit SIMD-Befehlen hinreißen lassen, um viele Bits noch schneller zu zählen als popcnt. Aber alle diese Ansätze sind schwer zu verstehen, und welche Befehle unterstützt werden, hängt davon ab, welchen Prozessor du genau hast. Ich würde lieber die einfachste countSetBits sehen, es sei denn, es gibt einen wirklich guten Grund, etwas Komplizierteres zu verwenden.

5 Natürlich fehlerfrei innerhalb der experimentellen Fehler. Es gibt immer Bugs, die du noch nicht gefunden hast.

6 Ein Kartenspiel ist nach sieben Riffle-Mischungen ziemlich gut randomisiert. Nach vier oder fünf Riffle-Mischungen ist das Deck überhaupt nicht mehr zufällig. Und ja, meine Familie ist genervt davon, wie oft ich ein Kartenspiel mische, bevor ich die nächste Runde gebe. "Wir sind hier, um Karten zu spielen, Chris, nicht um dir beim Mischen zuzusehen." Ein wenig Wissen ist eine gefährliche Sache.

7 Wie experimentell gemessen; deine Erfahrungen können variieren. Ich habe beim Schreiben des Riffle-Shuffle-Codes ein wenig mit den Indizes und Bedingungen herumgespielt und brauchte ein paar Versuche, bis es funktionierte. Der Code für die Zufallsauswahl funktionierte beim ersten Mal.

8 Wiederum experimentell ermittelt. Ich habe ein paar Versuche gebraucht, um es zu kompilieren, da ich zwischen Zeigern und Referenzen schwankte.

Get Die Regeln der Programmierung now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.