Über Generika der C++ TpeScript-Reihe

Über Generika der C++ TpeScript-Reihe

Vorwort:

Bei Vorstellungsgesprächen stelle ich den Kandidaten normalerweise gerne seltsame Fragen. Wenn Sie beispielsweise Autor einer Bibliothek sind, wie implementieren Sie eine bestimmte Funktion? Auf diese Art von Fragen gibt es im Allgemeinen keine richtige Antwort. Der Hauptzweck besteht darin, zu testen, ob der Kandidat ein tieferes Verständnis dieser Bibliothek hat. Der sekundäre Zweck besteht darin, dass es Spaß macht. Es macht Spaß, Spaß zu haben, aber man muss auch ernst sein, wenn es an der Zeit ist, ernst zu sein. Ich habe einmal einen Klassenkameraden interviewt, der TypeScript verwendet hat. Das hat mir die Augen geöffnet (meiner Erfahrung nach verwenden es einige große Unternehmen in China gelegentlich, kleine Unternehmen jedoch grundsätzlich nicht). Dann fragte ich: Was verstehen Sie unter Generika? Nachdem ich gefragt hatte, bereute ich es, weil ich die Antwort auch nicht wusste. Die Antwort habe ich im Nachhinein jedoch nicht bereut, denn der Kandidat entgegnete mir: „Ich weiß nicht, was Generika sind…“

Die Auswirkungen dieses Vorfalls auf die Kandidaten mögen groß oder klein sein, aber auf mich hatte er große Auswirkungen. Es hat mich dazu veranlasst, einen Artikel über Generika zu schreiben. Aber seit ich diesen Samen gepflanzt habe, beginne ich, es zu bereuen. Je mehr ich über Generika in TS erfahre, desto mehr habe ich das Gefühl, dass es zu diesem Thema nicht viel zu schreiben gibt. Zunächst einmal sind Generika in TS wie Luft: Sie werden oft verwendet, sind aber schwer zu beschreiben. Zweitens ist es zu umfangreich und es ist schwierig, alles abzudecken.

Der heutige Beitrag wird sich von den vorherigen dieser Reihe unterscheiden. Dieser Artikel beginnt mit den Problemen, die C++-Vorlagen lösen müssen, stellt die Probleme vor, die TS-Generika lösen müssen, und führt kurz in einige etwas fortgeschrittene Verwendungsszenarien ein.

1. Vorlage

Wenn wir von Generika sprechen, müssen wir den Urheber der Generika erwähnen: Vorlagen. Vorlagen in C++ gelten als mühsam und leistungsstark und werden seit vielen Jahren in verschiedenen wichtigen Lehrbüchern besprochen. Derzeit können Generika in Java, .NET oder TS zur Implementierung einer Teilmenge von C++-Vorlagen in Betracht gezogen werden. Ich bin mit der Teilmengenaussage nicht einverstanden. Denn vom Zweck her sind TS- und C++-Vorlagen völlig unterschiedlich.

C++-Vorlagen wurden erstellt, um typsichere Allzweckcontainer zu erstellen. Lassen Sie uns zunächst über allgemeine Container sprechen. Wenn ich beispielsweise eine verknüpfte Liste oder ein Array schreibe, kümmert sich diese Datenstruktur nicht viel um den Typ der darin enthaltenen spezifischen Daten, sie kann die entsprechenden Operationen implementieren. Aber js selbst achtet nicht auf Typ und Größe, daher ist das Array in js ursprünglich ein universeller Container. Für TS kann die Einführung von Generika dieses Problem lösen. Ein weiterer Punkt, der einen Vergleich wert ist, ist die Generierung. C++-Vorlagen erzeugen letztlich entsprechende Klassen oder Funktionen, für TS hingegen kann TS nichts generieren. Einige Studenten fragen sich vielleicht: Generiert TS nicht letztendlich JS-Code? Dies ist etwas ungenau, da TS den JS-Code letztendlich aufspaltet, ohne etwas an der ursprünglichen Logik zu ändern.

Ein weiterer Zweck von C++-Vorlagen ist die Metaprogrammierung. Diese Metaprogrammierung ist ziemlich leistungsfähig und optimiert hauptsächlich die Programm-Ausführung durch Programmierkonstrukte zur Kompilierungszeit. Was TS betrifft, nimmt es derzeit nur eine ähnliche Optimierung vor, d. h., const enum kann am Ausführungsort integriert werden, das ist alles. In Bezug auf diese Art der Optimierung wurde am Ende des vorherigen Artikels auch die auf Typinferenz basierende Optimierung erwähnt, derzeit verfügt TS jedoch nicht über diese Funktion. Wenn diese einfachen Optimierungen nicht unterstützt werden, ist eine komplexere Metaprogrammierung noch unmöglicher (Metaprogrammierung erfordert die logische Ableitung generischer Parameter und deren letztendliche Einbindung dort, wo sie verwendet werden).

Das ist alles, was ich zu C++-Vorlagen zu sagen habe. Schließlich handelt es sich hier nicht um einen Artikel über Metaprogrammierung von Vorlagen, und ich bin auch kein Experte. Wenn Sie weitere Fragen zu Vorlagen haben, können Sie Bruder Lunzi fragen. Nachdem ich über so viele Vorlagen gesprochen habe, möchte ich hauptsächlich sagen, dass Generika und Vorlagen in TS sehr unterschiedlich sind! Wenn Sie von C++ oder Java zur Front-End-Entwicklung wechseln, müssen Sie die Generika in TS neu verstehen.

2. Generika

Ich denke, es gibt in TS hauptsächlich drei Hauptverwendungszwecke für Generika:

  • Deklarieren Sie einen generischen Container oder eine generische Komponente. Beispielsweise: verschiedene Containerklassen wie Map , Array , Set usw.; verschiedene Komponenten wie React.Component .
  • Beschränken Sie den Typ. Beispiel: Verwenden Sie extends , um die eingehenden Parameter so einzuschränken, dass sie einer bestimmten Struktur entsprechen.
  • Neue Typen generieren

Was den zweiten und dritten Punkt betrifft, werde ich sie hier nicht wiederholen, da sie im vorherigen Artikel deutlich erwähnt wurden. Zum ersten Punkt möchte ich Ihnen zwei Beispiele nennen:

Das erste Beispiel handelt von generischen Containern. Angenommen, ich möchte eine einfache generische verknüpfte Liste implementieren. Der Code lautet wie folgt:

Klasse LinkedList<T> { // Generischer Klassenwert: T;
  next?: LinkedList<T>; // Sie können sich selbst zur Typdeklaration verwenden constructor(value: T, next?: LinkedList<T>) {
    dieser.Wert = Wert;
    dies.nächstes = nächstes;
  }
  Protokoll() {
    wenn (dies.nächstes) {
      dies.nächstes.log();
    }
    konsole.log(dieser.Wert);
  }
}
let list: LinkedList<number>; // Generische Spezialisierung ist Nummer
[1, 2, 3].fürJeden(Wert => {
  Liste = neue LinkedList(Wert, Liste);
});
liste.log(); // 1 2 3

Die zweite ist eine generische Komponente. Wenn ich eine allgemeine Formularkomponente implementieren möchte, kann ich sie wie folgt schreiben:

Funktion Form<T erweitert { [Schlüssel: Zeichenfolge]: beliebig }>({ Daten }: { Daten: T }) {
  zurückkehren (
    <form>
      {data.map((Wert, Schlüssel) => <Eingabename={Schlüssel} Wert={Wert} />)}
    </form>
  )
}


Dieses Beispiel demonstriert nicht nur generische Komponenten, sondern auch, wie Erweiterungen zum Definieren generischer Einschränkungen verwendet werden. Die generische Formularkomponente kann in Wirklichkeit komplizierter sein, das Obige dient nur zur Veranschaulichung der Idee.

Damit sind wir mit der Besprechung der TS-Generika fertig! Aber dieser Artikel ist noch nicht zu Ende. Sehen wir uns einige erweiterte Verwendungstechniken für Generika an.

3. Generische Rekursion

Einfach ausgedrückt ist Rekursion eine Möglichkeit zur Problemlösung, bei der die Ausgabe einer Funktion weiterhin als Eingabe für logische Berechnungen verwendet werden kann. Nehmen wir ein einfaches Beispiel. Wenn wir beispielsweise eine Addition berechnen möchten, definieren wir eine add , die nur die Summe zweier Zahlen berechnen kann. Jetzt haben wir jedoch drei Zahlen, 1, 2 und 3, die berechnet werden müssen. Wie können wir dieses Problem mit vorhandenen Tools lösen? Die Antwort ist einfach. Zuerst ergibt add(1, 2) 3, und dann ergibt add(3, 3) 6. Dies ist die Idee der Rekursion.

Rekursion kommt im wirklichen Leben so häufig vor, dass wir ihre Existenz oft übersehen. Das Gleiche gilt in der Welt der Programmierung. Hier ist ein Beispiel, das veranschaulicht, wie Rekursion in TS implementiert wird. Beispielsweise habe ich jetzt einen generischen Typ ReturnType<T>, der den Rückgabetyp einer Funktion zurückgeben kann. Aber jetzt habe ich eine Funktion mit einer sehr tiefen Aufrufhierarchie und ich weiß nicht, wie tief sie ist. Was soll ich tun?

Idee 1:

Typ DeepReturnType<T erweitert (...Argumente: beliebig) => beliebig> = ReturnType<T> erweitert (
  ...Argumente: beliebig
) => beliebig
  ? DeepReturnType<ReturnType<T>> // Referenzieren Sie sich hier: ReturnType<T>;

Erklärung des obigen Codes: Hier wird ein generischer Typ DeepReturnType definiert, und die Typbeschränkung ist eine Funktion, die beliebige Parameter akzeptiert und beliebige Typen zurückgibt. Wenn sein Rückgabetyp eine Funktion ist, ruft es sich weiterhin mit dem Rückgabetyp auf, andernfalls gibt es den Rückgabetyp der Funktion zurück.

Hinter jeder intuitiven und einfachen Lösung steckt ein Aber. Dieses lässt sich allerdings nicht kompilieren. Der Hauptgrund ist, dass TS derzeit nicht unterstützt wird. Ich weiß nicht, ob es in Zukunft unterstützt wird, aber der offizielle Grund ist sehr klar:

  • Diese zirkuläre Absicht macht es unmöglich, ein Objektdiagramm zu bilden, es sei denn, Sie verschieben es auf irgendeine Weise (durch Faulheit oder Zustand).
  • Es gibt wirklich keine Möglichkeit herauszufinden, ob die Typableitung abgeschlossen ist.
  • Wir können im Compiler endliche Rekursionstypen verwenden, die Frage ist jedoch nicht, ob der Typ terminiert, sondern wie rechenintensiv und rechtmäßig die Speicherzuweisung ist.
  • Eine Metafrage: Wollen wir, dass die Leute solchen Code schreiben? Solche Anwendungsfälle gibt es, aber die auf diese Weise implementierten Typen sind möglicherweise nicht für die Verbraucher der Bibliothek geeignet.
  • Fazit: Auf so etwas sind wir nicht vorbereitet.

Wie also erreichen wir diese Art von Nachfrage? Es gibt eine Methode, die der offiziellen Idee entspricht, dass wir eine begrenzte Anzahl von Rekursionen verwenden können. Hier sind meine Ideen:

// Zweischichtiger generischer Typ Typ ReturnType1<T extends (...args: any) => any> = ReturnType<T> extends (
  ...Argumente: beliebig
) => beliebig
  ? Rückgabetyp<Rückgabetyp<T>>
  : Rückgabetyp<T>;
// Dreischichtiger generischer Typ Typ ReturnType2<T extends (...args: any) => any> = ReturnType<T> extends (
  ...Argumente: beliebig
) => beliebig
  ? Rückgabetyp1<Rückgabetyp<T>>
  : Rückgabetyp<T>;
// Vier Schichten generischer Typen, die die meisten Fälle abdecken können. Typ DeepReturnType<T extends (...args: any) => any> = ReturnType<T> extends (
  ...Argumente: beliebig
) => beliebig
  ? Rückgabetyp2<Rückgabetyp<T>>
  : Rückgabetyp<T>;
  
// Test const deep3Fn = () => () => () => () => "flag is win" als const; // Vierschichtige Funktion Typ Returned = DeepReturnType<typeof deep3Fn>; // Typ Returned = "flag is win"
const deep1Fn = () => "flag is win" als const; // Einschichtfunktionstyp Zurückgegeben = DeepReturnType<typeof deep1Fn>; // Typ Zurückgegeben = "flag is win"

Diese Technik kann erweitert werden, um tiefe Strukturen wie Exclude , Optional oder Required zu definieren.

4. Standardmäßige generische Parameter

Manchmal mögen wir Generika sehr, aber manchmal möchten wir nicht, dass die Verbraucher von Klassen oder Funktionen jedes Mal den generischen Typ angeben. In diesem Fall können wir standardmäßige generische Parameter verwenden. Dies wird häufig in vielen Bibliotheken von Drittanbietern verwendet, beispielsweise:

// Generische Komponente, die die PSC-Klasse Component<P,S,C> { empfängt
  Requisiten: P;
  Zustand: S;
  Kontext: C
  ....
}
// Muss die Klasse MyComponent extends Component<{}, {}, {}>{} verwenden
​
// Aber was ist, wenn meine Komponente eine reine Komponente ist, die keine Eigenschaften, keinen Status und keinen Kontext benötigt? // Ich kann die Klasse Component<P = {}, S = {}, C = {}> folgendermaßen definieren:
  Requisiten: P;
  Zustand: S;
  Kontext: C
  ....
}
// Dann können Sie class MyComponent extends Component {} verwenden.

Ich finde dieses Feature sehr praktisch, es implementiert partial instantiation in C++-Vorlagen auf sehr natürliche Weise in JS.

5. Generische Überladung

Generische Überladung wurde in der offiziellen Dokumentation mehrfach erwähnt. Diese Art der Überladung hängt von einigen Mechanismen der Funktionsüberladung ab. Schauen wir uns daher zunächst die Funktionsüberladung in TS an. Hier verwende ich die map -Funktion in lodash als Beispiel. Der zweite Parameter der Map-Funktion kann einen string oder function annehmen, wie das Beispiel auf der offiziellen Website:

const Quadrat = (n) => n * n;
​
// Karte der Empfangsfunktionen
Karte({ 'a': 4, 'b': 8 }, Quadrat);
// => [16, 64] (Iterationsreihenfolge ist nicht garantiert)
 
const Benutzer = [
  { 'Benutzer': 'Barney' },
  { 'Benutzer': 'Fred' }
];
​
// Erhalte die Karte des Strings
Karte (Benutzer, "Benutzer");
// => ['Barney', 'Fred']

Wie also drückt man eine solche Typdeklaration in TS aus? Ich kann Funktionsüberladung wie folgt verwenden:

// Dies dient nur zur Demonstration, die Richtigkeit wird nicht garantiert. In realen Szenarien muss hier der richtige Typ eingetragen werden, nicht irgendein
Schnittstelle MapFn {
  (Obj: any, Prop: String): any; // Beim Empfangen eines Strings, Szenario 1 (Obj: any, fn: (Value: any) => any): any; // Beim Empfangen einer Funktion, Szenario 2 }
const map: MapFn = () => ({});
​
map(users, 'user'); // Überlastungsszenario 1 map({ 'a': 4, 'b': 8 }, square); // Überlastungsszenario 2

Der obige Code verwendet einen ziemlich eigenartigen Mechanismus in TS, d. h. die Definition von Funktionen, neuen und anderen Klassenfunktionen kann in interface geschrieben werden. Diese Funktion dient hauptsächlich der Unterstützung aufrufbarer Objekte in js. In jQuery können wir beispielsweise $("#banner-message"), direkt ausführen oder dessen Methode $.ajax() aufrufen.

Natürlich können Sie auch einen anderen, traditionelleren Ansatz verwenden, wie zum Beispiel den folgenden:

Funktion map(Objekt: beliebig, Eigenschaft: Zeichenfolge): beliebig;
Funktion map(Objekt: beliebig, Funktion: (Wert: beliebig) => beliebig): beliebig;
Funktion map(obj, sekundär): beliebig {}

Hier wird die Funktionsüberladung grundsätzlich erklärt. Auf Generika verallgemeinert ist es im Grunde dasselbe. Hier ist ein Beispiel für eine Frage, die ein Freund gestellt hat. Ich werde hier nicht näher auf diese Frage eingehen. Die Lösung ist wahrscheinlich diese:

Schnittstelle FN {
  (Objekt: {Wert: Zeichenfolge; bei Änderung: () => {} }): ungültig;
  <T erweitert {[P in keyof T]: nie}>(Objekt: T): ungültig;
  // Für Objekte vom Typ T werden niemals andere Schlüssel empfangen.
}
​
const fn: FN = () => {};
​
fn({}); // OK fn({ value: "Hi" }); // Falsch fn({ onChange: () => {} }); // Falsch fn({ value: "Hi", onChange: () => ({}) }); // OK

Für das React-Ökosystem gibt es hier ein lesenswertes Beispiel für generische Überladung: die connect -Funktion. Sie können zum Quellcode wechseln, um mehr zu erfahren.

Insgesamt hat mir der Artikel nicht besonders gefallen. Der Grund hierfür ist, dass Generika in TS weit verbreitet sind, aufgrund ihres ursprünglichen Designs jedoch eine schlechte Spielbarkeit aufweisen. Aber ich unterstütze dieses Designkonzept. Erstens kann es unsere Anforderungen an die Definition von Typen erfüllen. Zweitens ist es einfacher und leichter zu verwenden als C++-Vorlagen.

Dies ist das Ende dieses Artikels über die C++ TypeScript-Reihe zu Generika. Weitere verwandte Inhalte zu TypeScript-Generika finden Sie in den vorherigen Artikeln von 123WORDPRESS.COM oder in den folgenden verwandten Artikeln. Ich hoffe, dass jeder 123WORDPRESS.COM in Zukunft unterstützen wird!

Das könnte Sie auch interessieren:
  • Detaillierte Erklärung der LFU-Unterstützung für Generika in C++
  • Detaillierte Erläuterung der Grundkonzepte der generischen C++-Programmierung
  • C++-Algorithmen und generische Algorithmen (Algorithmus, numerisch)
  • C++ Generische Programmierung erklärt
  • Implementierungscode der benutzerdefinierten generischen Klasse Troop<T> (C++, Java und C#)
  • Gemeinsame Nutzung der in C++ implementierten generischen List-Klasse
  • Einige Zusammenfassungen generischer C++-Algorithmen
  • Aufblähungsproblem durch Verwendung von Generika in C++

<<:  Detaillierte Erklärung der HTML-Formularelemente (Teil 1)

>>:  MySQL-Abfrage-Cache und Pufferpool

Artikel empfehlen

Warum wird in React nicht empfohlen, einen Index als Schlüssel zu verwenden?

1. Vergleichen Sie den alten virtuellen DOM mit d...

Vue implementiert Beispielcode zur Formulardatenvalidierung

Fügen Sie dem el-form-Formular Regeln hinzu: Defi...

Ausführliche Erklärung des Sperrmechanismus in MySQL InnoDB

Vorne geschrieben Eine Datenbank ist im Wesentlic...

Zusammenfassung der Anwendungsbereiche von Kubernetes

Kubernetes ist aufgrund seiner Anwendungsportabil...

HTML-Code, der den Internet Explorer zum Einfrieren bringen kann

Wir müssen lediglich einen beliebigen Texteditor ö...