{ }codelper

学ぶ › TypeScript

言語ごとの特徴 · 38

インターフェースとジェネリクス

インターフェースで「このメソッドがある」という約束を決め、ジェネリクスでひとつの関数をいろいろな型に使います。

この単元は TypeScript · Java · C# 向けです。
Python・JavaScript はダックタイピングなので、area() さえあればどんなオブジェクトでも使え、インターフェースのキーワードは要りません。
C++ はテンプレート(template <typename T>)と C++20 のコンセプト(std::totally_ordered<T>)を使い、C と Craykon にはどちらもありません。

インターフェースは「これらのメソッドを必ず持つ」という約束で、ジェネリクスは型を引数として受け取るしくみです。
下の例は Circle と Rect を同じ Shape のリストに入れて面積を求め、ジェネリック関数 max 1つでいちばん大きい面積と辞書順でいちばん後ろの名前を探します。

import { readFileSync } from "node:fs"; interface Shape {  area(): number;  name(): string;} class Circle implements Shape {  r: number;  constructor(r: number) { this.r = r; }  area(): number { return Math.PI * this.r * this.r; }  name(): string { return "circle"; }} class Rect implements Shape {  w: number;  h: number;  constructor(w: number, h: number) { this.w = w; this.h = h; }  area(): number { return this.w * this.h; }  name(): string { return "rect"; }} function max<T extends number | string>(items: T[]): T {  let best = items[0];  for (const x of items) if (x > best) best = x;  return best;} const lines: string[] = readFileSync(0, "utf8").trim().split("\n");const shapes: Shape[] = [];for (const line of lines.slice(1)) {  const [kind, ...nums] = line.trim().split(" ");  const v = nums.map(Number);  shapes.push(kind === "circle" ? new Circle(v[0]) : new Rect(v[0], v[1]));} for (const s of shapes) console.log(`${s.name()} ${s.area().toFixed(2)}`);console.log(`largest area: ${max(shapes.map((s) => s.area())).toFixed(2)}`);console.log(`last name: ${max(shapes.map((s) => s.name()))}`);
stdin
3
circle 1
rect 3 4
circle 2
stdout
circle 3.14
rect 12.00
circle 12.57
largest area: 12.57
last name: rect

ポイント

  • インターフェース Shape は area() と name() だけを決めます。
    その約束を守るクラスはどれも Shape として扱えます。
  • s.area() を呼んだとき、どのクラスの area() が動くかは実行時にそのオブジェクトを見て決まります。
  • インターフェースのメソッドは自動的に public です。
    実装する側で public を外すとコンパイルエラーです。
  • ジェネリック関数は一度書けばいろいろな型に使えます。
    例の max は小数のリストにも文字列のリストにも使えます。
  • どんな型でも大小を比べられるわけではないので、制約を付けます:「T は比べられる型」。
  • ジェネリクスの型が実行時にも残るかは言語によって違います。
    残らなければ、実行中に T が何かはわかりません。

言語ごとの書き方

TypeScriptinterface Shape { area(): number; }、class Circle implements Shape。
制約は function max<T extends number | string>(items: T[]): T。
名前ではなく形で型を見るので、implements がなくてもメソッドがあれば Shape として通ります(implements は「抜けがないか検査して」というお願いに近い)。
型は実行時にすべて消えます
Javainterface Shape { double area(); }、class Circle implements Shape。
制約は static <T extends Comparable<T>> T max(List<T> items) — インターフェースでも extends と書きます。
public を外すと attempting to assign weaker access privileges。
ジェネリクスの情報はコンパイル後に消されます(型消去)
C#慣習で名前の頭に I: interface IShape、class Circle : IShape。
制約は static T Max<T>(List<T> items) where T : IComparable<T>。
public を外すと CS0737。
ジェネリクスが実行時にも残るので List<double> は数を包まずにそのまま持ち、new T[n]・typeof(T) も使えます

よくある間違い

インターフェースで実行時に検査したTypeScript

const s = JSON.parse(text) as Shape;
if (s instanceof Shape) { }

→ instanceof Shape は 'Shape' only refers to a type エラーです。as Shape を付けても、本当に area() があるかは誰も確かめません

インターフェースは実行時に存在しないので、入ってきた値は typeof s.area === "function" のように自分で確かめます。

ジェネリクスに int・double を入れたJava

List<double> areas = new ArrayList<>();
T[] arr = new T[n];

→ どちらもコンパイルエラー。型引数にはオブジェクトの型しか入らず、消された T では配列を作れません(generic array creation)

List<Double>・List<Integer> と書きます。
実行中に T が何かを調べることはできません。

Integer 2つを == で比べたJava

Integer a = 128, b = 128;
System.out.println(a == b);

→ false — 数がオブジェクトに包まれる(ボクシング)ので、== は同じオブジェクトかどうかを見ます。127 までは true になることがあり、よけいに紛らわしいです

包まれた数は equals で比べます。

言語ごとの違い
インターフェースとジェネリクスはコンパイル時に間違いを見つける道具です。
TypeScript と Java は実行時に型の情報を消すので、外から来たデータ(入力、JSON)は値を自分で確かめてはじめて安全になります。

練習問題