{ }codelper

배우기 › C#

언어마다 다른 것 · 38

인터페이스와 제네릭

인터페이스로 "이런 메서드가 있다"는 약속을 정하고, 제네릭으로 한 함수를 여러 타입에 써요.

이 단원은 TypeScript · Java · C#에서 배워요.
Python·JavaScript는 덕 타이핑이라 area()만 있으면 어떤 객체든 쓸 수 있어서 인터페이스 키워드가 필요 없어요.
C++는 템플릿(template <typename T>)과 C++20 콘셉트(std::totally_ordered<T>)를 쓰고, C와 크레이콘에는 둘 다 없어요.

인터페이스는 "이 메서드들을 꼭 가진다"는 약속이고, 제네릭은 타입을 매개변수로 받는 거예요.
아래 예제는 Circle과 Rect를 Shape 목록에 함께 담아 넓이를 구하고, 제네릭 함수 max 하나로 가장 큰 넓이와 사전 순으로 가장 뒤인 이름을 찾아요.

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)}");    }}
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)도 돼요

자주 하는 실수

제네릭에 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)는 직접 값을 검사해야 안전해요.

연습 문제