부트캠프

C# 제네릭과 var, dynamic

noyyo 2023. 8. 14. 21:12

코드를 작성하다보면 타입만 달라지고 기능은 똑같은 메소드나 클래스등을 만들 경우가 있다.

매번 오버로드하기엔 코드 중복이 너무 많이 되고 들어오는 모든 타입을 확실히 모를 경우도 있다.

그럴 때 일반적으로 쓰기 위한 제네릭 프로그래밍 C#에서는 어떻게 하는지 알아봤다.

 

●Generic

제네릭 Class 만들기

class GenericClass<T>() { }

이런 방식으로 선언한다. 인스턴스화는 new GenericClass<구체화 타입>()와 같이 사용한다.

형식 매개 변수라고 불리는 T는 런타임에 인스턴스화 할 때 주어진 타입으로 구체화된다.

 

클래스 전체에 제네릭이 필요한 것이 아니라면 특정 메소드만 제네릭으로 선언할 수도 있다.

void GenericMethod<T>(T arg) { }

위와 같이 선언하고 호출할때 GenericMethod<int>(10) 과 같이 타입을 구체화해서 호출해주면 된다.

 

이렇게 제네릭으로 구현하면 코드의 중복도 줄이고 여러 형식에 대해 같은 기능을 수행할 수 있다.

 

그런데 제네릭으로 클래스나 메소드를 사용할 때 어떤 클래스, 인터페이스에 있는 기능이 필요한 경우가 있다.

이럴 때 형식 매개 변수 T만 가지고 컴파일러는 이 T가 도대체 무슨 타입인지 알 방법이 없다.

 

이 때 T가 해당 클래스와 인터페이스 등을 가지고 있어야 한다는 제약조건을 걸 수 있다.

void Constraints<T>() where T : SomeClass, SomeInterface { }

where 키워드를 사용해서 형식 매개 변수에 제약조건을 설정할 수 있고 형식 매개 변수가 1개가 아니라 2개 3개가 넘어가도 각각 제약 조건을 설정할 수 있다.

void Constraints<T, U>() where T : struct where U : class { }

이 때 중간에 ,를 넣지 않는다는 점 주의하자.

그리고 where 뒤에 오는 제약조건에 struct가 올 경우 ValueType을 의미하고 class가 올 경우 ReferenceType을 의미한다.

where T : struct는 흔히 사용하는 int, float 과 같은 값타입이라는 말이고 where T : class는 참조타입이라는 말이다.

where T : new()

위와 같은 제약조건은 T타입이 생성자에 public으로 선언된 기본 생성자가 존재해야함을 의미한다.

 

기본적인 제네릭 사용법은 이게 끝이다.

그리고 이 제네릭을 통해 간단한 계산기를 만드려고 했는데 간단하게 연산자로 계산하려니 T타입에 연산자가 있다는 보장이 없다. 연산자 오버로딩이 있다는 제약조건도 설정할 수 없었다.

그래서 사용하게 된 것이 dynamic 키워드

 

●dynamic

dynamic 키워드는 하나의 정적 타입이지만 동적으로 타입을 결정하게 해준다.

뭔 말이냐면 int float처럼 선언할때 dynamic d1 같은 변수 선언이 가능하다. 컴파일러가 dynamic이라는 타입이 있다는 것을 알아먹는다.

그런데 런타임에 실제 데이터타입은 dynamic 타입이 아니라 해당 변수에 들어있는 내용물의 타입이 된다.

dynamic d1;
d1 = 10;

해당 코드에서 런타임에서 d1의 데이터타입은 dynamic이 아니라 int가 된다.

이렇게 런타임에 데이터 타입을 결정하게 만들어주는 것이  dynamic 타입이다. 중간에 값이 다른 데이터 타입으로 바뀔 수도 있고 타입 체크를 할 때 있는 데이터를 기준으로 타입이 결정된다.

.

엄청 편리해 보이고 실제로 그러하지만 주의해야할 점은 dynamic 타입에 대해서 컴파일러는 뭐? 런타임에 알아서 하겠다고? 그래 니 맘대로 해라 그럼 뭐 결정도 안났으니까 타입체크나 오류체크같은건 없다~하고 무시한다는 점이다.

실제로 dynamic 타입에 들어올 데이터타입이 가지고 있지 않은 메소드를 실행시키는 코드라고 하더라도 컴파일러는 뭐 알아서 되니까 쓰겠지 하고 문제없음! 을 외치지만 런타임에서는 해당 메소드가 없으므로 바로 오류를 뿜뿜하게 된다.

계산기를 구현할 때도 오퍼레이터를 사용하면 컴파일러는 있으니까 썼겠지~ 하고 통과시켜주고 오퍼레이터 오버로딩이 되어있는 클래스를 넣어서 사용하면 문제가 없지만 아니라면 바로 오류가 발생한다.

 

여기서 잘 실행이 되더라도 주의할 점은 해당 기능이 항상 올바르게 작동한다는 보장이 없다.

즉 + 연산자를 통해 Add를 구현했는데 엉뚱하게 String객체가 들어오게 되면 내가 원한건 sum이었는데 실제로 수행된 건 append일 가능성이 있다는 것.

이런 점을 조심해서 사용하자.

 

●var

dynamic이 동적으로 타입 체크를 한다면 var는 정적으로 타입 체크를 한다.

컴파일러가 알아서 형식을 추론해서 타입을 맞춰주게 되는 것이다.

var v1 = 10;
var v2 = "var";

여기서 똑같은 var 타입으로 선언한 v1, v2 는 컴파일러에 의해 자동으로 int와 string 타입으로 치환된다.

dynamic 과는 다르게 한 번 결정되면 중간에 다른 타입으로 바뀔 수 없다. int였다가 string일 수는 없다는 이야기.

int i = 10; 이 i = "int"; 하면 바로 오류나듯이 마찬가지다.

 

상당히 편리하지만 모든 상황에서 사용 가능한 것은 아니고 사용할수록 코드에서 타입을 확실히 알 수 없게 되다보니 필요한 상황에서만 사용하는게 낫다. 식의 값을 캐스팅해서 받아온다던가 API 간의 데이터를 넘기거나 받아올 때 사용한다던가 하는 상황에서 자주 쓰인다. 해당 상황에서는 타입을 알고 있거나 알 필요가 없기 때문이다.

'부트캠프' 카테고리의 다른 글

간단한 Snake게임 만들기2  (0) 2023.08.17
간단한 Snake 게임 만들기1  (0) 2023.08.16
미니프로젝트 마무리  (0) 2023.08.11
씬 전환하기  (0) 2023.08.10
GameObject 배치하기  (0) 2023.08.09