프로그래밍 언어론 7-2-2강. 타입 호환성(Type Compatibility)

[프로그래밍 언어론]

2019. 12. 13. 22:40

 

[타입 호환성]

: 대부분의 언어에서 매 맥락마다 타입 동등성을 요구하지 않는다.

: 대신, 맥락이 타입 호환성이 유지되는 정도면 충분하다고 말한다.

 

# 타입 호환성 유지 확인 케이스

  1) Assignment statement

    : 오른쪽의 타입이 왼쪽의 타입과 호환 되어야 한다.  ex) int a = b;     // b는 int 타입인 a와 호환되는 타입여부

  2) operands of '+' 타입

    : '+', 즉 더하기를 지원하는 일반적인 타입들이여야 사용 가능하다.    ex) a + b;   // a, b는 더하기 연산 가능한 타입

  3) subroutine 호출

    : subroutine에 전달되는 타입들은 선언시 사용한 파라미터 타입들과 호환 되어야 한다.

 

 

 

[언어별 타입 호환성]

: 언어마다 타입 호환성의 정의 범위가 다르다.

 

# 언어별 타입 호환성 예시

int a = 3.2'

2 + 3.2;

: 이와 같은 선언은 언어마다 호환성 범위에 따라 허용 범위가 달라진다.

: JAVA와 Ada에서는 이와 같이 사용 할 수 없다. (Ada : 타입 S와 T가 동등성이고, 서로 subtype인등 같은 종류여야지만 가능)

 

 

 

[자동 형 변환(Coercion)]

: 한 값의 타입을 다른 타입으로 자동으로, 암시적으로 전환(conversion)해주는 특성이다.

: 실행 시점에 수행되는 동적 시맨틱 확인 코드 필요

: 실행 시점에 수행되는 low-level representation 간 변환 코드 필요

 

# C에서의 자동 형 변환

  : 비교적 weak 타입 시스템이라, 약간의 자동형변환만 수행한다.

short in s;   // 16비트
unsigned long int l;   // 32비트
char c;     // 8비트
float f;    // 32비트
double d;   // 64비트

// 1) l의 아래 bit들이 signed number로 변환
s = l;

// 2) 추가된 위 bit를 0으로 채우고, 기존의 signed bit(맨위 부호 bit)를 숫자로 취급한다
l = s;

// 3) short <- char
s = c;

// 포맷 변경. 숫자 손실 발생가능 (하지만 동적 시맨틱 오류 발생은 X)
f = d;

  : 결론적으로 C언어는 큰 범위의 타입에서 작은 범위의 타입으로 형변환 중 일어나는 포맷손실에 대해 따로 작업 하지 않는다 ( = 동적 시맨틱 오류 검사 X )

  : 설계자는 어디까지 형 변환 허용해줄지 판단 하는 것이 중요 하다.

 

 

 

[Universal Reference Types]

: 어떤 타입이든 저장 할 수 있는 보편적 목적의 컨테이너 타입이 존재 한다.

 

# 각 언어에서의 universal reference type

1) C/C++ : void* (void 포인터)

2) Java, C# : Object

: 그렇다고 이 타입으로 할당될 수는 없지만 타입의 안전성을 고려하지 않은 것이다.

: 객체의 타입이 universal reference에 의해 참조됨을 알수 없기에, 컴파일러는 해당 객체에 대해 어떤 연산자도 수행되는 걸 허용하지 않는다.

: 특정 참조 타입의 객체에 재할당되는 것은 좀 까다로워, 타입 safety가 필요하다.

 

# 특정 참조 타입의 객체에 재할당

float f = value;

void* p = &f;

int* p = p      // 오류 발생(객체의 비트해석 부정확히 수행)

: 객체지향 언어에서는, 위와 같은 문제되는 상황들에 대해 유효성을 확인해준다.

 

# 객체지향 언어에서 왼쪽의 객체가 오른쪽의 객체의 할당을 지원 유효성 검사

Object o = new B();

A a = (A)o;

: 위와 같이 객체가 self-descriptive(자기-서술)하게 만들어, 포함될 타입의 tag를 기술해 확인할 수 있게 해준다.

: Java와 C#과 같은 객체지향 언어에서는 universal이 특정 할당되려면 type cast가 필요하고(위),

  해당 universal가 cast된 타입에 참조될 수 없으면 exception을 발생 시킨다. ( ClassCastException )

 

# Java에서 ClassCastException 발생

A a = new A();

Object o = new String("hello");

a = (A) o;   // ClassCastException 발생

 

# C++에서의 dynamic_cast 사용

int main() {

  A* a; 

  A* b = new B;               // B는 A의 상속관계, C는 독립관계지만 우연히 같은 메소드 보유

  C* c = new C;

  a = dynamic_cast<A*>(c);  // 형변환. (A*)(c)와 다른 개념 ( = static_cast(강제))

  print(a);                         // null, 잘못된 형변환일때 포인터에 null 값들이 들어가기에 잘못된 형변환 확인 가능

  a = dynamic_cast<A*>(b);

  print(a);

  delete c;

  delete b;

}

: 결론은 tagging 방식이용해서 dynamic_cast 가능

 

# universal reference 클래스 정리와 Generic**

: 초기 버전의 Java와 C#에서, 프로그래머는 최상위 클래스의 객체를 위한 컨테이너 클래스인 Object or object를 가진다.

: 하지만 이러한 Object를 이용하는 방법(위에서 쭉 본 방법)은 넣을땐 상관 업지만, 뺄때 반드시 type cast 가 필요하다.

: 심지어 이런 type cast를 해줘도 잘못된 cast 발생시 exception 처리 까지 해줘야 한다.

: 이런 문제는 넣을때 어떤 종류든 해당 공간에 넣을 수 있기에 발생하는 문제이다 ( safety 문제 )

: 그래서 오늘날에 Generic 이라는 개념이 존재하고 사용되어, 기존 obejct 방식은 사용이 감소하였다.

: 이 Generic 방법은 애초에 넣을때 기술된 타입의 클래스만 넣을 수 있게 하여 보다 안정적인 방법이다.

// 기존 object 방법

ArrayList a = new ArrayList();

a.add(value)                            // 어떤 종류의 타입이든 value에 들어갈 수 있다.

s = (String) a.pop()                  // 타입 cast 필요.. 실행 시점에 string 아닌 타입들에대해선 exception 발생

 

// Generic 방법

ArrayList<String> a = new ArrayList<String>();      // 애초에 String 타입만 해당 배열에 들어갈 수 있게 허용

a.add(value)

s = a.pop()

 

: 특히 타입 tag 없는 언어에선(cast), universal reference 타입의 객체가 실행 시점까지 그 타입들을 식별 할 수 없다.