프로그래밍/C#

CLR via C# | 4장) 타입의 기초

소복 2022. 4. 18. 02:04

목차

     

    4장에서 알 수 있는 내용

    1. 타입의 공통분모 object
    2. 타입 안정성
    3. 캐스팅
    4. 네임스페이스
    5. 런타임에서의 (타입 + 스레드 + 스택 + 힙)의 형성 방식

    System.Object

    • public 메서드
      • Equals
        • 비교대상과 동일한 값인지 확인
        • (책에는 안 써있지만, 해시테이블 콜렉션에서 Key로 사용하려면 재정의 필수)
          • hashCode는 int로, 중복발생 가능성이 존재
          • hashCode가 같고 Equals도 true 여야 동등객체
      • GetHashCode
        • 해시 테이블 콜렉션에서 Key로 사용하려면 재정의 필수
        • 객체 식별의 유일성이 충족되어야 함
      • ToString
        • 일반적인 용도: 객체 현재 상태 설명, 디버깅
      • GetType
        • Type 타입 클래스에서 파생된 객체 인스턴스 반환 (타입에 대한 정보가 담긴 객체)
        • 비가상 메서드(재정의 불가)이기 때문에, 파생 클래스는 이 메서드를 재정의 할 수 없음
    • protected 메서드
      • MemberwiseClone
        • 비가상 메서드
        • 다음과 같은 절차로, 인스턴스 복사본을 반환 (얕은 복사)
          1. 현재 인스턴스와 동일한 타입의 새로운 인스턴스를 생성
          2. 현재 객체의 모든 필드를 새 객체로 복사
          3. 새 객체의 참조를 반환
      • Finalize
        • 기본적으로 다음 상황에 호출
          1. GC에서 현재 객체가 사용되지 않는 것을 파악해서,
          2. 객체에 대한 메모리 회수 요청이 들어올 때
        • 별도로 정리 작업이 필요한 타입은 반드시 재정의 필요

    new 연산자의 동작 순서

    class에만 해당하는 내용 (struct는 해당하지 않음)

    1. 메모리에 객체를 생성하기 위한 바이트 수 계산
      • 여기에는 [모든 부모 클래스의 필드, 타입 객체 포인터, 동기화 블록 인덱스] 가 포함됨
      • 뒤의 두개는 CLR에서 객체를 관리하기 위해 추가하는 항목들
    2. 힙 메모리에 계산한 바이트만큼 할당 + 0(null)로 초기화
      • (타입 객체 포인터의 경우, JIT를 통해 만들어진 타입 객체를 참조함)
      • (메모리 입장에선 null도 0)
    3. 생성자(매개변수);를 호출
    4. 힙에 할당된 객체의 참조 반환

    C#이 타입 안정성을 지키는 법

    • GetType 메서드로 런타임에 객체의 타입을 알 수 있음
      • 비가상 메서드라 거짓말을 치도록 수정할 수 없음
    • 따라서 다음과 같은 [컴파일 통과 및 런타임 에러] 상황을 만들어 줌
      • object today = new DateTime(2022, 3, 2);
      • String x = (String) today;
      • // 컴파일 상에서는 today가 object 타입이기에 문제가 없음
        • 자식 = (자식) 부모타입의 자식인스턴스; (O)
        • 자식 = (자식) 부모타입의 부모인스턴스; (런타임 에러)

    안전한 캐스팅 연산자

    캐스트 식

    is 연산자

     if (o is string)
     {
       string str = (string) o;
     }

    문제점

    • 타입 검사를 두 번이나 한다!
      • 타입 검사: 변수 o의 모든 상위 타입들을 확인하는 작업
      • 하지만 이는 C# 7.0 부터 다음처럼 쓸 수 있게 개선됨

    as 연산자

     string str = o as string;
     if (str != null)
     { ... }

    타입 검사 한 번으로 캐스팅이 가능하다!

    Namespace

    • 컴파일러가 소스코드 내 혹은 참조하는 어셈블리들에 없는 타입이름을 만나면
      • using에 추가한 네임스페이스를 다 붙여서 존재하는지 확인 함
      • (따라서 모호한 참조인지를 판별할 수 있는 것 -> 컴파일 타임에 검사하기 때문에 성능 저하 X)

    모호한 참조 해결 방법

     using Microsoft;
     using Wintellect;
     using WintellectWidjet = Wintellect.Widget;
     ​
     Widget           w1 = new Widget();           // Microsoft.Widget
     WintellectWidjet w2 = new WintellectWidjet(); // Wintellect.Widjet

    런타임에서의 [타입 + 스레드 + 스택 + 힙]

    스레드 생성시

    • 스택 1MB 할당
      • 크기는 실행 전에 옵션으로 정할 수 있음
      • 런타임에는 변경 불가능
      • 초과시 StackOverflow 발생
    • 스택에 저장되는 정보
      • 매개변수로 받은 것
        • 이때 '매개변수의 전달순서 및 해제방식'이 궁금하다면 콜링컨벤션을 찾아보기
      • 돌아갈 호출자 메서드의 주소
      • 지역변수

    return을 만나면

    • CPU의 명령 포인터 주소를 스택에 있는 "돌아갈 호출자 메서드의 중간 코드 주소"로 설정

    메서드 실행시 JIT로 (IL -> CPU 명령어) 변환

    • 동시에 메서드에서 참조하는 타입들을 파악
    • CLR은 로드한 어셈블리 메타데이터를 이용해 Type 객체를 생성 (단, 이미 생성된 Type 객체는 skip)
      • Type 객체의 필드
        • 타입객체포인터
          • Type 타입 객체의 타입객체포인터를 가르킴
        • 동기화 블록 인덱스
        • 정적필드도 여기에 생성
        • 메서드 테이블
          • 파생클래스의 경우 재정의한 메서드만 포함

    CLR이 모든 지역변수를 null/0으로 초기화해줌

    • 하지만 C# 컴파일러는 초기화되지 않은 지역변수 사용시 오류 발생

    정적 메서드 호출시..

    1. JIT가 타입 객체를 찾음
    2. 타입 객체의 메서드 테이블 에서 호출하려는 메서드 찾음
    3. (아직 JIT 컴파일안된 메서드라면 JIT 컴파일 수행)
    4. 컴파일된 메서드 호출

    비가상 메서드 호출시..

    1. JIT가 선언 타입의 타입 객체를 찾음
    2. 만약 선언타입이 해당 메서드를 정의하지 않으면, 상속(부모)관계쪽에 정의됐는지 확인
    3. 메서드 테이블 에서 호출하려는 메서드 찾음
    4. (아직 JIT 컴파일안된 메서드라면 JIT 컴파일 수행)
    5. 컴파일된 메서드 호출

    가상 메서드 호출시

    • 호출할 때 마다 JIT가 메서드안에 다음 내용의 코드를 넣음
      1. 호출에 사용된 변수 확인
      2. 객체를 찾아, 실제 객체의 타입객체포인터 검사하여 타입 객체 찾음
      3. 메서드 테이블 에서 호출하려는 메서드 찾음
      4. (아직 JIT 컴파일안된 메서드라면 JIT 컴파일 수행)
      5. 컴파일된 메서드 호출

    세 종류 호출의 차이점

    • 타입객체를 찾는 방식
      • 정적 : 호출에 사용된 타입으로 바로 타입객체를 찾음
      • 비가상 : 호출에 사용된 변수의 선언타입의 타입객체를 찾음 (+ 메서드 정의가 없으면 부모쪽 정의도 확인)
      • 가상 : 호출에 사용된 변수의 실제 객체 타입으로 타입객체를 찾음

     

    [출처: 제프리 리처의 CLR via C# 4판]

      •