C++ TpeScriptシリーズのジェネリックについて

C++ TpeScriptシリーズのジェネリックについて

序文:

面接のとき、私はたいてい応募者に奇妙な質問をするのが好きです。たとえば、ライブラリの作成者の場合、特定の機能をどのように実装しますか?一般的に、このタイプの質問には正解はありません。主な目的は、候補者がこのライブラリについてより深く理解しているかどうかをテストすることです。2 番目の目的は、それが楽しいということです。楽しむのは楽しいですが、真剣になるときには真剣にならなければなりません。以前、 TypeScriptを使っていた同級生にインタビューしたのですが、目から鱗が落ちる思いでした(私の経験上、中国の大企業ではたまに使っていますが、中小企業では基本的に使っていません)。そこで私は尋ねました、「ジェネリック医薬品をどう理解しますか?」尋ねた後、私も答えがわからなかったので後悔しました。しかし、私は後でその答えを後悔しませんでした。なぜなら、候補者は私に「ジェネリックが何なのかわかりません…」と答えたからです。

この事件が受験生に与えた影響は大きいか小さいかは別として、私にとっては大きな衝撃でした。それがジェネリック医薬品についての記事を書くきっかけとなりました。しかし、この種を植えて以来、私は後悔し始めました。 TS のジェネリックについて詳しく知れば知るほど、このトピックについて書くことはあまりないと感じます。まず第一に、TS におけるジェネリックは空気のようなもので、よく使われますが、説明するのは難しいものです。第二に、範囲が広すぎてすべてを網羅するのは困難です。

今日の投稿は、このシリーズのこれまでの投稿とは異なります。この記事では、C++ テンプレートが解決する必要がある問題から始めて、TS ジェネリックが解決する必要がある問題を紹介し、少し高度な使用シナリオを簡単に紹介します。

1. テンプレート

ジェネリックについて言えば、ジェネリックの創始者であるテンプレートについて言及する必要があります。 C++ のテンプレートは面倒でありながら強力であることで知られており、長年にわたってさまざまな主要な教科書で取り上げられてきました。現時点では、Java、.NET、または TS のジェネリックは、C++ テンプレートのサブセットを実装するものと考えられます。私はサブセットの記述に同意しません。目的の点では、TS テンプレートと C++ テンプレートは完全に異なるためです。

C++ テンプレートは、型安全な汎用コンテナを作成するために作成されました。まず、一般的なコンテナについてお話ししましょう。たとえば、リンク リストや配列を記述する場合、このデータ構造は、その中の特定のデータのタイプをあまり気にせず、対応する操作を実装できます。しかし、js 自体は型やサイズを気にしないので、js 内の配列はもともとユニバーサル コンテナーです。 TS の場合、ジェネリック医薬品の出現によりこの問題を解決できます。比較する価値のあるもう 1 つの点は生成です。C++ テンプレートは最終的に対応するクラスまたは関数を生成しますが、TS の場合、TS は何も生成できません。学生の中には、TS は最終的に JS コードを生成しないのかと疑問に思う人もいるかもしれません。これは少し不正確です。TS は最終的に元のロジックに何もせずに JS コードを分離するからです。

C++ テンプレートのもう 1 つの目的はメタプログラミングです。このメタプログラミングは非常に強力であり、主にコンパイル時のプログラミング構造を通じてプログラムの実行を最適化します。 TS に関する限り、現在のところ、同様の最適化は 1 つだけ行われ、つまり、const enum を実行場所でインライン化できるだけです。このタイプの最適化に関しては、前回の記事の最後でも型推論に基づく最適化について触れましたが、現時点では TS にはこの機能はありません。これらの単純な最適化がサポートされていない場合、より複雑なメタプログラミングはさらに不可能になります (メタプログラミングでは、ジェネリック パラメーターの論理的な推論と、最終的にそれらが使用される場所にインライン化する必要があります)。

C++ テンプレートについて私が言いたいことはこれですべてです。結局のところ、これはテンプレート メタプログラミングに関する記事ではなく、私は専門家ではありません。テンプレートについてさらに質問がある場合は、Lunzi 兄弟に尋ねてください。たくさんのテンプレートについて話した後、私が主に言いたいのは、TS のジェネリックとテンプレートは非常に異なるということです。 C++またはJavaからフロントエンド開発に切り替える場合でも、TS のジェネリックを再理解する必要があります。

2. ジェネリック

TS におけるジェネリックの主な用途は 3 つあると思います。

  • 汎用コンテナまたはコンポーネントを宣言します。たとえば、 MapArray 、 Set などのさまざまなコンテナ クラス、 React.Componentなどのさまざまなコンポーネント。
  • タイプを制限します。たとえば、受信パラメータを特定の構造に準拠するように制限するには、 extends使用します。
  • 新しいタイプを生成する

2 点目と 3 点目については、前回の記事で明確に述べましたので、ここでは繰り返しません。最初の点に関して、2つの例を挙げてみましょう。

最初の例は、汎用コンテナに関するものです。単純な汎用リンク リストを実装するとします。コードは次のとおりです。

class LinkedList<T> { // ジェネリッククラス value: T;
  next?: LinkedList<T>; // 型宣言には自身を使用できます。constructor(value: T, next?: LinkedList<T>) {
    this.value = 値;
    this.next = 次へ;
  }
  ログ(){
    if (this.next) {
      this.next.log();
    }
    console.log(この値);
  }
}
let list: LinkedList<number>; // ジェネリック特殊化は number です
[1, 2, 3].forEach(値 => {
  リスト = 新しい LinkedList(値、リスト);
});
リスト.log(); // 1 2 3

2 つ目は汎用コンポーネントです。汎用フォーム コンポーネントを実装する場合は、次のように記述できます。

関数 Form<T は { [key: string]: any }>({ data }: { data: T }) を拡張します {
  戻る (
    <フォーム>
      {data.map((値, キー) => <入力名={キー} 値={値} />)}
    </フォーム>
  )
}


この例では、汎用コンポーネントを示すだけでなく、 extends を使用して汎用制約を定義する方法も示します。実際の汎用フォーム コンポーネントはこれよりも複雑になる可能性がありますが、上記は単にアイデアを示すためのものです。

ここまでで、TSジェネリックについての説明は終わりです。しかし、この記事はまだ終わりではありません。ジェネリックの高度な使用テクニックをいくつか見てみましょう。

3. ジェネリック再帰

簡単に言えば、再帰とは、関数の出力を論理計算の入力として継続的に使用できる問題を解決する方法です。簡単な例を見てみましょう。たとえば、加算を計算したい場合、2 つの数値の合計のみを計算できるadd関数を定義します。しかし、計算する必要がある数値は 1、2、3 の 3 つです。この問題を既存のツールでどのように解決できるでしょうか。答えは簡単です。まず、add(1, 2) は 3 で、add(3, 3) は 6 です。これが再帰の考え方です。

再帰は現実の生活では非常に一般的なので、その存在を見落としてしまうことがよくあります。プログラミングの世界でも同じことが言えます。 TS で再帰がどのように実装されているかを示す例を次に示します。たとえば、関数の戻り値の型を返すことができるジェネリック型 ReturnType<T> ができました。しかし、呼び出し階層が非常に深い関数があり、その深さがわかりません。どうすればよいでしょうか?

アイデア1:

型 DeepReturnType<T extends (...args: any) => any> = ReturnType<T> extends (
  ...引数: 任意
) => 任意
  ? DeepReturnType<ReturnType<T>> // ここで自身を参照します: ReturnType<T>;

上記コードの説明: ここではジェネリック型DeepReturnTypeが定義されており、型制約は任意のパラメータを受け入れて任意の型を返す関数です。戻り値の型が関数である場合は、戻り値の型を使用して自分自身を呼び出し続け、そうでない場合は関数の戻り値の型を返します。

直感的でシンプルな解決策の背後には、必ず「しかし」があります。ただし、これをコンパイルすることはできません。主な理由は、TS が現在サポートされていないことです。将来的にサポートされるかどうかはわかりませんが、公式の理由は非常に明確です。

  • この循環的な意図により、何らかの方法 (遅延または状態を通じて) で延期しない限り、オブジェクト グラフを形成することが不可能になります。
  • 型推論が終了したかどうかを知る方法は実際にはありません。
  • コンパイラーでは有限型の再帰を使用できますが、問題は型が終了するかどうかではなく、それがどれだけ計算集約的でメモリ割り当てが合法であるかです。
  • メタ質問: 私たちは人々にこのようなコードを書いてもらいたいのでしょうか?このような使用例は存在しますが、このように実装された型はライブラリの消費者には適さない可能性があります。
  • 結論:私たちはこのような事態にまだ備えができていません。

では、このような需要にどう応えればよいのでしょうか?公式のアイデアのように、再帰の回数を制限して使用する方法があります。私のアイデアは次のとおりです:

// 2層のジェネリック型 type ReturnType1<T extends (...args: any) => any> = ReturnType<T> extends (
  ...引数: 任意
) => 任意
  ? 戻り値の型<戻り値の型<T>>
  : 戻り値の型<T>;
// 3層ジェネリック型 type ReturnType2<T extends (...args: any) => any> = ReturnType<T> extends (
  ...引数: 任意
) => 任意
  ? 戻り値1<戻り値<T>>
  : 戻り値の型<T>;
// ほとんどのケースに対応できる 4 層のジェネリック型 type DeepReturnType<T extends (...args: any) => any> = ReturnType<T> extends (
  ...引数: 任意
) => 任意
  ? 戻り値2<戻り値<T>>
  : 戻り値の型<T>;
  
// テスト const deep3Fn = () => () => () => () => "flag is win" as const; // 4 層関数 type Returned = DeepReturnType<typeof deep3Fn>; // type Returned = "flag is win"
const deep1Fn = () => "flag is win" as const; // 1 層関数 type Returned = DeepReturnType<typeof deep1Fn>; // type Returned = "flag is win"

この手法を拡張して、 ExcludeOptionalRequiredなどの深い構造を定義することもできます。

4. デフォルトのジェネリックパラメータ

ジェネリックが大好きな場合もありますが、クラスや関数の消費者が毎回ジェネ​​リック型を指定することを望まない場合もあります。このような場合は、デフォルトのジェネリック パラメータを使用できます。これは、次のような多くのサードパーティ ライブラリで広く使用されています。

// PSCを受け取る汎用コンポーネント class Component<P,S,C> {
  小道具: P;
  状態: S;
  コンテキスト:C
  ....
}
// MyComponent は Component<{}, {}, {}>{} を拡張するクラスを使用する必要があります
​
// しかし、コンポーネントが props、state、context を必要としない純粋なコンポーネントの場合はどうなるでしょうか? // 次のように class Component<P = {}, S = {}, C = {}> を定義できます。
  小道具: P;
  状態: S;
  コンテキスト:C
  ....
}
// 次に、クラス MyComponent を Component {} に拡張して使用できます。

この機能は非常に実用的だと思います。C++ テンプレートのpartial instantiationを JS で非常に自然な方法で実装します。

5. ジェネリックオーバーロード

ジェネリックオーバーロードについては、公式ドキュメントで何度も言及されています。この種のオーバーロードは、関数オーバーロードのいくつかのメカニズムに依存します。したがって、まずは TS での関数オーバーロードについて見てみましょう。ここでは、 lodashmap関数を例として使用します。 map 関数の 2 番目のパラメータは、公式 Web サイトの例のように、 stringまたはfunctionを受け入れることができます。

定数平方 = (n) => n * n;
​
// 受信関数のマップ
マップ({ 'a': 4, 'b': 8 }, 正方形);
// => [16, 64] (反復順序は保証されません)
 
const ユーザー = [
  { 'ユーザー': 'バーニー' },
  { 'ユーザー': 'フレッド' }
];
​
// 文字列のマップを受け取る
ユーザーをマップします。'user';
// => ['バーニー', 'フレッド']

では、TS でこのような型宣言をどのように表現するのでしょうか?次のように関数オーバーロードを使用できます。

// これはデモンストレーションのみであり、正確性は保証されません。実際のシナリオでは、ここに正しいタイプを入力する必要があります。
インターフェース MapFn {
  (obj: any, prop: string): any; // 文字列を受け取る場合、シナリオ 1 (obj: any, fn: (value: any) => any): any; // 関数を受け取る場合、シナリオ 2 }
定数マップ: MapFn = () => ({});
​
map(users, 'user'); // オーバーロードシナリオ 1 map({ 'a': 4, 'b': 8 }, square); // オーバーロードシナリオ 2

上記のコードは、TS のかなり特殊なメカニズムを使用しています。つまり、関数の定義、新しいクラス関数、およびその他のクラス関数をinterfaceに記述できます。この機能は主に、js 内の呼び出し可能なオブジェクトをサポートするためのものです。たとえば、 jQueryでは、 $("#banner-message"),を直接実行したり、そのメソッド $.ajax() を呼び出したりすることができます。

もちろん、次のような、より伝統的なアプローチを使用することもできます。

関数 map(obj: any, prop: string): any;
関数 map(obj: any, fn: (value: any) => any): any;
関数 map(obj, secondary): 任意 {}

ここでは関数オーバーロードについて基本的に説明します。ジェネリックに一般化すると、基本的には同じです。これは友人が提起した質問の例です。ここではこの質問について詳しく説明しません。解決策はおそらくこれです:

インターフェースFN {
  (obj: { 値: 文字列; onChange: () => {} }): void;
  <T は {[P in keyof T]: never}> を拡張します (obj: T): void;
  // T 型の obj の場合、他のキーを受け取ることはありません。
}
​
定数fn: FN = () => {};
​
fn({}); // OK fn({ value: "Hi" }); // 間違い fn({ onChange: () => {} }); // 間違い fn({ value: "Hi", onChange: () => ({}) }); // OK

React エコシステムでは、ジェネリック オーバーロードの例として、読む価値のあるconnect関数があります。詳細については、ソース コードを参照してください。

全体的に、私はこの記事があまり好きではありませんでした。その理由は、TS ではジェネリックが広く使用されているものの、その独自の設計により、プレイアビリティが低いためです。しかし、私はこの設計コンセプトを支持します。まず、型を定義するための要件を満たすことができます。次に、C++ テンプレートよりもシンプルで使いやすいです。

ジェネリックに関する C++ TypeScript シリーズの記事はこれで終わりです。TypeScript ジェネリックに関するその他の関連コンテンツについては、123WORDPRESS.COM の以前の記事を検索するか、以下の関連記事を引き続き参照してください。今後とも 123WORDPRESS.COM をよろしくお願いいたします。

以下もご興味があるかもしれません:
  • C++ におけるジェネリックをサポートする LFU の詳細な説明
  • C++ジェネリックプログラミングの基本概念の詳細な説明
  • C++ アルゴリズムと汎用アルゴリズム (アルゴリズム、数値)
  • C++ ジェネリックプログラミングの説明
  • カスタム Troop<T> ジェネリック クラスの実装コード (C++、Java、C#)
  • C++ で実装された汎用 List クラスの共有
  • C++ 汎用アルゴリズムの概要
  • C++ でジェネリックを使用することで発生する肥大化の問題

<<:  MySQL クエリ キャッシュとバッファ プール

>>:  Apache ソースコードのインストールと仮想ホストの設定に関する詳細なチュートリアル

推薦する

CentOS 7.4 に MySQL 5.7 を手動でインストールする方法

MySQL データベースは、特に JAVA プログラマーの間で広く使用されています。クラウド データ...

「fsck」を使用して Linux のファイルシステムエラーを修正する方法

序文ファイル システムは、データの保存方法と復元方法を整理する役割を担います。 いずれにせよ、時間の...

JavaScriptにおけるこれの深い理解

Jsでのこれの深い理解JavaScriptスコープはstatic scopeスコープですが、 Jsの...

VMware WorkStation 14 pro インストール Ubuntu 17.04 チュートリアル

この記事では、VMware Workstation14 ProにUBuntu17.04をインストール...

UbuntuへのDocker CEのインストール

この記事は、Ubuntu 17.10 での Docker CE のインストールを記録するために使用さ...

JavaScript ベースのパスワード ボックス検証情報の実装

この記事では、パスワードボックスの検証情報を実装するためのJavaScriptの具体的なコードを例と...

VueのID認証管理とテナント管理の詳細な説明

目次概要ボタンレベルの権限アイデンティティ認証管理R/U 権限権限の更新テナント管理テナント切り替え...

Centos7 での mysql 8.0.15 のインストールと設定

この記事では、参考までにMySQL 8.0.15のインストールと設定のグラフィックチュートリアルを紹...

Win10にnginxをインストールする方法

会社から、負荷を実装するためにnginxをベースにFordプロジェクトのWebServiceサーバー...

Javascriptを使用して滑らかな曲線を生成する方法

目次序文ベジェ曲線の紹介二次ベジェ曲線3次ベジェ曲線ベジェ曲線計算機能フィッティングアルゴリズム付録...

React の国際化 react-intl の使用

React で国際化を実現するにはどうすればよいでしょうか? react-intlプラグインは、Re...

iframe を使用して Web ページに他の Web ページを埋め込む方法

iframe の使い方:コードをコピーコードは次のとおりです。 <DIV align=cent...

Mac でソースコードから MySQL 5.7.17 をコンパイルしてインストールするチュートリアル

1. ダウンロードして解凍します: /Users/xiechunping/Softwares/mys...

MySQLのROUND関数の丸め演算における落とし穴の分析

この記事では、MySQL の ROUND 関数を使用した丸め操作の落とし穴を例を使って説明します。ご...

VirtualBox でのホストオンリー + NAT モードのネットワーク構成

VirtualBoxのHost Only+NATモードのネットワーク構成は参考用です。具体的な内容は...