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 ソースコードのインストールと仮想ホストの設定に関する詳細なチュートリアル

推薦する

Dockerイメージが消える問題を解決する

1. 50と93では鏡像が消える [root@h50 /]# df -h ファイルシステムの使用済み...

Navicat による MySQL パーティショニングの実践

MySQLのパーティショニングは、非常に大きなテーブルを管理するのに役立ちます。MySQLのパーティ...

Html+CSS フローティング広告ストリップの実装

1.html部分コードをコピーコードは次のとおりです。 <!DOCTYPE html> ...

完全なMySQL学習ノート

目次MyISAM と InnoDBパフォーマンスの低下と SQL の速度低下の理由: MySQL 実...

MySQL ストアド プロシージャの作成と呼び出しの詳細な説明

目次序文ストアドプロシージャ: 1. ストアドプロシージャの作成と呼び出し1. ストアドプロシージャ...

MySQL 分離レベルの詳細な説明と例

目次MySQL の 4 つの分離レベルデータ テーブルを作成します。分離レベルの設定物事の分離レベル...

WeChatミニプログラムにナビゲーション機能を実装する方法

1. レンダリング2. 操作手順1. テンセントマップキーを申請する - 住所2. ミニプログラムの...

mysql IS NULL インデックスケースの説明を使用する

導入MySQL の SQL クエリ ステートメントで is null、is not null、!= ...

Docker で Kong API Gateway をインストールして使用する詳細なチュートリアル

1 はじめにKong は単純な製品ではありません。この記事で言及されている Kong は主に Kon...

MySQL でサーバーのインストールを開始できない場合の解決策について簡単に説明します。

コンピュータに初めて MySQL をインストールする場合、通常このエラー メッセージは表示されません...

Vueライフサイクルの違いの詳細な説明

ライフサイクル分類vue の各コンポーネントは独立しており、各コンポーネントには独自のライフサイクル...

HTML+CSS+JS でスタックカルーセル効果を実装するサンプルコード

効果:スライドショーが一方向に動く場合、各画像のサイズ、位置、透明度、レベルを変更する必要があります...

JavaScript 配列の include と Reduce の基本的な使用法

目次序文配列.プロトタイプ.includes文法パラメータ戻り値例配列プロトタイプの削減文法パラメー...

mysql 簡単な操作例を表示

この記事では、例を挙げて mysql show 操作について説明します。ご参考までに、詳細は以下の通...

Linux でファイルの権限 (所有権) を変更する

Linux と Unix はマルチユーザー オペレーティング システムであるため、ファイルの権限と所...