언어마다 다른 것 · 38
인터페이스와 제네릭
인터페이스로 "이런 메서드가 있다"는 약속을 정하고, 제네릭으로 한 함수를 여러 타입에 써요.
이 단원은 TypeScript · Java · C#에서 배워요.
Python·JavaScript는 덕 타이핑이라 area()만 있으면 어떤 객체든 쓸 수 있어서 인터페이스 키워드가 필요 없어요.
C++는 템플릿(template <typename T>)과 C++20 콘셉트(std::totally_ordered<T>)를 쓰고, C와 크레이콘에는 둘 다 없어요.
인터페이스는 "이 메서드들을 꼭 가진다"는 약속이고, 제네릭은 타입을 매개변수로 받는 거예요.
아래 예제는 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)도 돼요 |
자주 하는 실수
제네릭에 int·double을 넣었어요Java
List<double> areas = new ArrayList<>();
T[] arr = new T[n];→ 둘 다 컴파일 오류. 타입 매개변수에는 객체 타입만 들어가고, 지워진 T로는 배열을 못 만들어요(generic array creation)
List<Double>·List<Integer>로 써요.
실행 중에 T가 무엇인지는 알아낼 수 없어요.
Integer 두 개를 ==로 비교했어요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)는 직접 값을 검사해야 안전해요.