목차
4장에서 알 수 있는 내용
- 타입의 공통분모 object
- 타입 안정성
- 캐스팅
- 네임스페이스
- 런타임에서의 (타입 + 스레드 + 스택 + 힙)의 형성 방식
System.Object
- public 메서드
- Equals
- 비교대상과 동일한 값인지 확인
- (책에는 안 써있지만, 해시테이블 콜렉션에서 Key로 사용하려면 재정의 필수)
- hashCode는 int로, 중복발생 가능성이 존재
- hashCode가 같고 Equals도 true 여야 동등객체
- GetHashCode
- 해시 테이블 콜렉션에서 Key로 사용하려면 재정의 필수
- 객체 식별의 유일성이 충족되어야 함
- ToString
- 일반적인 용도: 객체 현재 상태 설명, 디버깅
- GetType
- Type 타입 클래스에서 파생된 객체 인스턴스 반환 (타입에 대한 정보가 담긴 객체)
- 비가상 메서드(재정의 불가)이기 때문에, 파생 클래스는 이 메서드를 재정의 할 수 없음
- Equals
- protected 메서드
- MemberwiseClone
- 비가상 메서드
- 다음과 같은 절차로, 인스턴스 복사본을 반환 (얕은 복사)
- 현재 인스턴스와 동일한 타입의 새로운 인스턴스를 생성
- 현재 객체의 모든 필드를 새 객체로 복사
- 새 객체의 참조를 반환
- Finalize
- 기본적으로 다음 상황에 호출
- GC에서 현재 객체가 사용되지 않는 것을 파악해서,
- 객체에 대한 메모리 회수 요청이 들어올 때
- 별도로 정리 작업이 필요한 타입은 반드시 재정의 필요
- 기본적으로 다음 상황에 호출
- MemberwiseClone
new 연산자의 동작 순서
class에만 해당하는 내용 (struct는 해당하지 않음)
- 메모리에 객체를 생성하기 위한 바이트 수 계산
- 여기에는 [모든 부모 클래스의 필드, 타입 객체 포인터, 동기화 블록 인덱스] 가 포함됨
- 뒤의 두개는 CLR에서 객체를 관리하기 위해 추가하는 항목들
- 힙 메모리에 계산한 바이트만큼 할당 + 0(null)로 초기화
- (타입 객체 포인터의 경우, JIT를 통해 만들어진 타입 객체를 참조함)
- (메모리 입장에선 null도 0)
- 생성자(매개변수);를 호출
- 힙에 할당된 객체의 참조 반환
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 부터 다음처럼 쓸 수 있게 개선됨
-
if (o is string str) { Console.WriteLine(str); }
- 형식 테스트 연산자 및 캐스트 식 - MSDN
-
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 타입 객체의 타입객체포인터를 가르킴
- 동기화 블록 인덱스
- 정적필드도 여기에 생성
- 메서드 테이블
- 파생클래스의 경우 재정의한 메서드만 포함
- 타입객체포인터
- Type 객체의 필드
CLR이 모든 지역변수를 null/0으로 초기화해줌
- 하지만 C# 컴파일러는 초기화되지 않은 지역변수 사용시 오류 발생
정적 메서드 호출시..
- JIT가 타입 객체를 찾음
- 타입 객체의 메서드 테이블 에서 호출하려는 메서드 찾음
- (아직 JIT 컴파일안된 메서드라면 JIT 컴파일 수행)
- 컴파일된 메서드 호출
비가상 메서드 호출시..
- JIT가 선언 타입의 타입 객체를 찾음
- 만약 선언타입이 해당 메서드를 정의하지 않으면, 상속(부모)관계쪽에 정의됐는지 확인
- 메서드 테이블 에서 호출하려는 메서드 찾음
- (아직 JIT 컴파일안된 메서드라면 JIT 컴파일 수행)
- 컴파일된 메서드 호출
가상 메서드 호출시
- 호출할 때 마다 JIT가 메서드안에 다음 내용의 코드를 넣음
- 호출에 사용된 변수 확인
- 객체를 찾아, 실제 객체의 타입객체포인터 검사하여 타입 객체 찾음
- 메서드 테이블 에서 호출하려는 메서드 찾음
- (아직 JIT 컴파일안된 메서드라면 JIT 컴파일 수행)
- 컴파일된 메서드 호출
세 종류 호출의 차이점
- 타입객체를 찾는 방식
- 정적 : 호출에 사용된 타입으로 바로 타입객체를 찾음
- 비가상 : 호출에 사용된 변수의 선언타입의 타입객체를 찾음 (+ 메서드 정의가 없으면 부모쪽 정의도 확인)
- 가상 : 호출에 사용된 변수의 실제 객체 타입으로 타입객체를 찾음
[출처: 제프리 리처의 CLR via C# 4판]