言語ごとの特徴 · 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()))}`);import java.util.*; interface Shape { double area(); String name();} class Circle implements Shape { private final double r; Circle(double r) { this.r = r; } public double area() { return Math.PI * r * r; } public String name() { return "circle"; }} class Rect implements Shape { private final double w, h; Rect(double w, double h) { this.w = w; this.h = h; } public double area() { return w * h; } public String name() { return "rect"; }} public class Main { static <T extends Comparable<T>> T max(List<T> items) { T best = items.get(0); for (T x : items) if (x.compareTo(best) > 0) best = x; return best; } public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(); List<Shape> shapes = new ArrayList<>(); for (int i = 0; i < n; i++) { String kind = sc.next(); if (kind.equals("circle")) shapes.add(new Circle(sc.nextDouble())); else shapes.add(new Rect(sc.nextDouble(), sc.nextDouble())); } List<Double> areas = new ArrayList<>(); // List<double>은 안 돼요 List<String> names = new ArrayList<>(); for (Shape s : shapes) { System.out.println(s.name() + " " + String.format("%.2f", s.area())); areas.add(s.area()); names.add(s.name()); } System.out.println("largest area: " + String.format("%.2f", max(areas))); System.out.println("last name: " + max(names)); }}using System;using System.Collections.Generic; interface IShape{ double Area(); string Name();} class Circle : IShape{ private readonly double r; public Circle(double r) { this.r = r; } public double Area() => Math.PI * r * r; public string Name() => "circle";} class Rect : IShape{ private readonly double w, h; public Rect(double w, double h) { this.w = w; this.h = h; } public double Area() => w * h; public string Name() => "rect";} class Program{ static T Max<T>(List<T> items) where T : IComparable<T> { T best = items[0]; foreach (T x in items) if (x.CompareTo(best) > 0) best = x; return best; } static void Main() { int n = int.Parse(Console.ReadLine()); var shapes = new List<IShape>(); for (int i = 0; i < n; i++) { string[] p = Console.ReadLine().Split(' '); if (p[0] == "circle") shapes.Add(new Circle(double.Parse(p[1]))); else shapes.Add(new Rect(double.Parse(p[1]), double.Parse(p[2]))); } var areas = new List<double>(); var names = new List<string>(); foreach (var s in shapes) { Console.WriteLine($"{s.Name()} {s.Area():F2}"); areas.Add(s.Area()); names.Add(s.Name()); } Console.WriteLine($"largest area: {Max(areas):F2}"); Console.WriteLine($"last name: {Max(names)}"); }}3 circle 1 rect 3 4 circle 2
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が何かはわかりません。
言語ごとの書き方
| TypeScript | interface Shape { area(): number; }、class Circle implements Shape。制約は function max<T extends number | string>(items: T[]): T。名前ではなく形で型を見るので、 implements がなくてもメソッドがあれば Shape として通ります(implements は「抜けがないか検査して」というお願いに近い)。型は実行時にすべて消えます |
|---|---|
| Java | interface 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) も使えます |
よくある間違い
ジェネリクスに 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
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" のように自分で確かめます。
インターフェースとジェネリクスはコンパイル時に間違いを見つける道具です。
TypeScript と Java は実行時に型の情報を消すので、外から来たデータ(入力、JSON)は値を自分で確かめてはじめて安全になります。