学习 › TypeScript
各语言特有 · 38
接口与泛型
用接口约定“有这些方法”,用泛型让一个函数适用于多种类型。
本单元适用于 TypeScript · Java · C#。
Python 和 JavaScript 是鸭子类型,只要有 area() 的对象都能用,不需要接口关键字。
C++ 使用模板(template <typename T>)和 C++20 的概念(std::totally_ordered<T>),C 和 Craykon 两者都没有。
接口是“一定有这些方法”的约定,泛型则是把类型当作参数来接收。
下面的示例把 Circle 和 Rect 放进同一个 Shape 列表求面积,再用一个泛型函数 max 找出最大的面积和按字母顺序排在最后的名字。
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) 也可以用 |
常见错误
在运行时用接口做检查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、doubleJava
List<double> areas = new ArrayList<>();
T[] arr = new T[n];→ 两行都是编译错误。类型参数只能是对象类型,被擦除的 T 也不能用来创建数组(generic array creation)
写成 List<Double>、List<Integer>。
运行时无法得知 T 是什么。
用 == 比较两个 IntegerJava
Integer a = 128, b = 128;
System.out.println(a == b);→ false —— 数字被包装成对象(装箱),== 看的是是不是同一个对象。到 127 为止可能得到 true,所以更容易混淆
包装后的数字用 equals 比较。
各语言的差异
接口和泛型是在编译时发现错误的工具。
TypeScript 和 Java 在运行时会擦除类型信息,所以来自外部的数据(输入、JSON)要自己检查值才安全。
接口和泛型是在编译时发现错误的工具。
TypeScript 和 Java 在运行时会擦除类型信息,所以来自外部的数据(输入、JSON)要自己检查值才安全。