박싱

값 타입(Value Type)이나 참조 타입(Reference Type)을 System.Object로 변환하는 작업.

 

언박싱

박싱한 System.Object 타입을 박싱 이전의 타입으로 변환하는 작업.

 

그중 오늘은 값 타입의 박싱 연산, 언박싱 연산에 대해 포스팅을 하겠다.

 

값 타입과 참조 타입에 대해서는 이전 포스팅을 참고하길 바란다.

computervision-is-fun.tistory.com/21

 

값 타입, 참조 타입

1. 값 타입 (struct, enum)  - new 예약어를 통해 할당시킨 값 타입은 스레드 스택 메모리에 저장되므로, 스레드가 종료될 시 스택의 순서대로  삭제 된다. (GC의 오버헤드가 없다)  - = 연산시 필드 값

computervision-is-fun.tistory.com

 

값 타입의 박싱

값 타입은 생성 시 스레드의 스택 메모리에 저장되고, 참조 값이 아닌 값 자체를 다룬다.

참조 타입과 여러모로 대조가 된다.

이런 설명을 전재로 아래와 같이 값 타입을 ArrayList에 저장시키면 어떤 동작이 일어나까??

struct Point
{
    int nPtX;
    int nPtY;
    
    public Point(int nInputPtX, int nInputPtY)
    {
    	nPtX = nInputPtX;
        nPtY = nInputPtY;
    }
}

public static void Main()
{
    Point pt = new Point(1,2);
    ArrayList arrList = new ArrayList();
    
    arrList.Add(pt);
}

ArrayList의 Add함수를 살펴보자

public virtual Int32 Add(Object Value);

이처럼 Add함수는 Object를 매개변수로 동작한다.

그리고 위 코드는 참조 타입 매개변수에 값 타입을 넣어도 동작이 되었다.

왜 그럴까??

이유는 값 형식의 Point를 참조 형식의 Object로 박싱 명령을 내부적으로 진행했기 때문이다.

박싱은 내부적으로 어떤 동작을 할까??

어떻게 스레드 스택 메모리에 저장되어있는 데이터가 관리되는 힙 메모리에 저장되는 것일까?

순서는 이렇다.

 

1. 관리되는 힙 메모리에 Point의 필드 멤버를 고려하여 메모리가 할당된다. 이때 참조 형식이므로 타입 객체 포인터와 동기화 블록 인덱스라는 필드를 포함하여 할당한다.

2. 값 타입인 Point의 필드 멤버 변수들을 관리되는 힙 메모리에 복사시킨다.

3. 새로 생성된 참조 타입의 Point의 포인터를 반환한다.

 

이제 실제 위의 동작을 하는지 ILDASM.EME(역어 셈)으로 실행해보겠다.

위의 코드를 역어셈, IL_0011~IL0012가 박싱 동작

아쉽게도 더욱 자세한 코드는 보기 힘들 것 같다. 하지만 위와 같이 box라는 부수적인 명령어가 붙으면서 우리는 박싱 동작을 짐작할 수 있다. 박싱 동작을 더욱 자세히 확인할 수 있는 방법은 더 찾아봐야겠다.

 

추가로 박싱 된 참조 타입의 Point는 기존의 값 타입의 Point보다 생명주기가 길다.

이유는 관리되는 힙 메모리가 저장되었기 때문에 다음 GC의 호출을 기다리기 때문이다.

 

값 타입의 언박싱

Object에 저장되어있던 참조 형식의 Point가 값 형식의 Point로 변환하는 작업을 말한다.

CLR은 언박싱을 수행하면 아래와 같은 동작을 한다.

 

1. 관리되는 힙 메모리에서 참조 형식의 Point의 필드 값들의 주소를 가져온다.

  (ex nPtX, nPtY)

2. 이 값들은 스레드 스택 메모리의 Point에 복사시킨다.

 

위의 과정과 아래의 코드를 역어 셈으로 보겠다.

static void Main(string[] args)
{
    Point pt = new Point(1, 2);
    Object o = pt;
    pt = (Point)o;
}

 

IL_0012가 언박싱 동작

IL_0012 명령 줄을 보면 unbox라 하고 Object를 Point의 값 타입으로 복사시킨 것을 알 수 있다.

 

결론

값 타입의 박싱과 언박싱 모두 의도하지 않게 복사 동작을 진행한다. 따라서 우리는 값 타입의 박싱과 언박싱을 최소화해야 한다. 

 

 

 

 

 

 

 

'Program > C#' 카테고리의 다른 글

const 상수  (1) 2020.12.20
값 타입, 참조 타입  (0) 2020.12.13
네임스페이스와 어셈블리  (1) 2020.12.05
타입에 대하여2  (0) 2020.12.02
타입에 대하여1  (1) 2020.11.29

1. 값 타입 (struct, enum)

 - new 예약어를 통해 할당시킨 값 타입은 스레드 스택 메모리에 저장되므로, 스레드가 종료될 시 스택의 순서대로

   삭제 된다. (GC의 오버헤드가 없다)

 - = 연산시 필드 값이 모두 복사 동작을 한다. 일반적으로 16Byte 넘을 시 참조 타입(Class)으로 사용을 권한다. (Call by Value

 - System.ValueType으로 부터 항상 상속된다. (System.ValueType은 결국 System.Object로부터 상속되지만 Equals 메     서드를 재정의 함.)

 - 값 타입을 기본 타입으로 사용하여 새로운 값 타입이나 참조 타입을 정의 할 수 없다.

   (암묵적으로 seald라고 보면 된다.)

 - 런타임 중 NullReference Exception이 발생하지 않는다.

    (값 타입은 기본적으로 필드 멤버들은 모두 0으로 초기화 하기 때문 하지만 선언만 하고 정의를 안 할 시 컴파일러가

    오류 처리 해줌)

 

2. 참조 타입 (class)

 - new 예약어를 통해 할당시킨 참조 타입은 GC의 관리 메모리(힙 메모리)에 저장되며, 더 이상 사용을 안 할 시 GC가

   삭제한다.

 - = 연산 시 참조 타입의 포인터만 저장시킨다. (Call by Reference

 - System.Object로부터 상속된다.

 - 가상 함수, Base Class 등 상속시키거나 상속받을 수 있다.

 - 런타임 중 NullReference Exception이 발생한다.

 

코드 작성 시 struct를 쓰면 더 좋은 상황

 - 변경하지 않는 구조 일 경우 (Immutable Type)

 - 다른 타입으로부터 상속받을 필요가 없는 타입일 경우

 - 타른 타입의 기본 타입으로 쓰이게 하지 않을 경우

 - 인스턴스의 전체 크기가 약 16byte 미만일 경우

 - 함수의 매개변수나 반환 타입으로 사용할 일이 별로 없는 경우

 

 

 

'Program > C#' 카테고리의 다른 글

const 상수  (1) 2020.12.20
값타입의 박싱과 언박싱  (1) 2020.12.14
네임스페이스와 어셈블리  (1) 2020.12.05
타입에 대하여2  (0) 2020.12.02
타입에 대하여1  (1) 2020.11.29

이번 주제는 실제 코딩보다는 환경 설정 문제, 아키텍처 설계 문제에 직면할만한 문제 중 하나로 포스팅했다

 

네임스페이스 : 서로 다른 타입들의 특성과 성향을 논리적으로 그룹화하기 위한 수단이다.

어셈블리       : 네임스페이스에 그룹화 되어 있는 타입의 구현체 (ex. exe, dll)로 보면 된다.

 

using System.IO; // IO 기능을 위한 네임스페이스
using System.Text; //Text 기능을 위한 네임스페이스

FileStream fs = new FileStream(...);
StringBuilder sb = new StringBuilder();

c# 개발자들은 위와같은 using 지시자 구문으로 네임스페이스를 지정한다. 만약 네임스페이스 가없다면 아래와 같이 복잡하게 작성해야 한다.

System.IO.FileStream fs = new System.IO.FileStream(...);
System.Text.StringBuilder sb = new System.Text.StringBuilder();

CLR은 C# 코드 중 using 구문은 신경 쓰지 않는다. 왜냐하면 컴파일 단계에서 C# 컴파일러가 작성된 using 구문으로 메서드의 반환 값, 인수의 개수와 타입을 탐색하며 두 번째와 같은 코드로 변경시키기 때문이다.

실제로 컴파일러는 FileStream fs와 같은 문구를 만나면 using 지시자에 정의되어있는  System.IO, System.Text를 탐색해 FileStream 타입이 있는지 찾아낸다. 이런 컴파일러의 동작 덕분에 우리 개발자들은 조금 더 깨끗한 코드를 작성할 수 있다.

 

System.IO.FileStream 타입을 찾아낸 컴파일러는 그다음 무엇을 할까??

 

바로 어셈블리를 탐색 한다. 그리고 특정 타입에 대한 정보와 일치하는 어셈블리를 발견하면 어셈블리 정보와 타입 정보가 만들어지는 관리 모듈의 메타 데이터상에 기록시킨다. 그리고 CLR은 관리 모듈메타 데이터를 읽고 수행하는 것이다.

C# 컴파일러는 기본적으로 MSCORLIB.DLL어셈블리를 따로 지정하지 않더라도 자동으로 찾아볼 대상에 포함시키고 있다.

MSCORLIB.DLL에는 무엇이 있을까??

 

C# MSDN에서 기본 타입에 대해 조금만 찾아봐도 참조한 어셈블리에 포함되어있다.

docs.microsoft.com/ko-kr/dotnet/csharp/

 

C# 문서 - 시작, 자습서, 참조.

프로그래밍 C# 알아보기 - 초보 개발자, C#을 처음 사용하는 개발자, 숙련된 C# / .NET 개발자용

docs.microsoft.com

 

MSCORLIB.DLL은 기본적으로 Object, Int32, double, String 등 핵심적인 타입들은 모두 포함하는 FCL이 들어있다.

 

네임스페이스를 씀으로 써 일반적으로 겪을 수 있는 문제

회사 A와 회사 B가 제공하는 DLL을 동시에 쓴다고 할 때 A사의 Widget, B사의 Widget을 쓰는 상황

using A;
using B;

Widget w = new Widget(); //A.Widget? B.Widget?

위의 코드는  컴파일러가 A.Widget, B.Widget의 모호한 참조 오류가 발생한다.

그래서 위의 모호성을 없애기 위해서 1번 해결 방법은 using 지시자 구문을 쓰지 않는 것이다.

A.Widget w = new A.Widget(); //A.Widget

그리고 두 번째 해결 방법

using AWidth = A.Widget;
AWidth w = new AWidth(); //A.Widget

이렇게 우리는 참조의 모호성을 해결했다.

일반적으로 제3자가 사용하는 타입을 설계하는 경우, 반드시 이러한 타입들을 별도의 네임스페이스상에 정의하여 컴파일러가 쉽게 타입을 구분할 수 있도록 설계해야 한다. 위의 예시에서 안 좋게 나왔지만 사실 주요 방법은 앞에 회사명.으로 시작하는 네임스페이스로 지시하는 방법이 일반적이다. 실제 Microsoft도 Microsoft.CSharp, Microsoft.VisualBasic을 쓴다고 한다.

 

추가로 C#코드에서 네임스페이스 선언 방법이다

namepsace Name{
    public Class A{...} //Name.A
    
    namespace Donghee
    {
        public class B{...} //Name.Donghee.B
    }
}

네임스페이스와 어셈블리 사이의 연관성

네임스페이스와 어셈블리에 대해서는 필요 불충분의 관계이고, 이는 연관성이 별로 없다는 뜻이다. 무슨 말이냐 하면 System.IO.FileStream 클래스 타입은 mscrolib.dll에 System.IO.FileSystemWatcher 클래스 타입은 System.dll에 들어 있다. 즉 앞에 System.IO가 동일하다고 해서 동일한 어셈블리에 포함되어 있는 것이 아니다. 추가로 하나의 어셈블리에 2개 이상의 네임스페이스가 포함될 수도 있다. (ex. System.Int32, System.Text.StringBuilder는 mscrolib.dll에 같이 정의되어있음)

정리하자면

 

- 네임스페이스가 같다고 하나의 어셈블리에 포함되어 있지 않다.

- 하나의 어셈블리에 2개 이상의 네임스페이스가 포함될 수 있다.

 

예시 Class ResXFileRef 

네임스페이스 :  System.Resources

어셈블리      :  System.Windows.Forms.dll

 

docs.microsoft.com/ko-kr/dotnet/api/system.resources.resxfileref.-ctor?view=net-5.0

 

ResXFileRef 생성자 (System.Resources)

ResXFileRef 클래스의 새 인스턴스를 초기화합니다.Initializes a new instance of the ResXFileRef class.

docs.microsoft.com

 

'Program > C#' 카테고리의 다른 글

const 상수  (1) 2020.12.20
값타입의 박싱과 언박싱  (1) 2020.12.14
값 타입, 참조 타입  (0) 2020.12.13
타입에 대하여2  (0) 2020.12.02
타입에 대하여1  (1) 2020.11.29

타입간 캐스팅

코딩을 하다보면 타입간 캐스팅을 해야하는 상황이 빈번하게 발생한다. 캐스팅은 일반적으로 

Up Casting, Down Casting기능으로 쓰인다.

 

Up casting

- 파생된 객체가 베이스 클래스로 변환하는 캐스팅

Down casting  

- 베이스 클래스에서 파생 클래스로 캐스팅

 

//System.Object에서 파생 된 base
public class base
{
	
}

//base에서 파생 된 derived
public class derived : base
{
    
}

class Program
{
    static void Main(string[] args)
    {            
        object ob = new derived(); //upcasting
        Console.WriteLine(ob.GetType().ToString());
        //derived 출력
        
        Base b = (Base)ob;        //downcasting
        Console.WriteLine(b.GetType().ToString());
        //derived 출력
        
        derived d = (derived)b;   //downcasting
        Console.WriteLine(d.GetType().ToString());
        //derived 출력
    }

}

 

위의 코드는 캐스팅의 예시 이다. CLR기준으로 순서대로 설명을 해보자면

 

1. Main 함수 시작 후 CLR은 derived 인스턴스를 생성하여 인스턴스 포인터를 반환한다.

 

2. 저장할 인스턴스의 클래스 형태는 object이므로 CLR은 derived 객체가 object로 업캐스팅이 가능한지 검사 후 업캐스팅 하여 인스턴스 ob에 저장한다. 

 

3. 인스턴스 ob의 타입을 출력 한다. ob는 derived에서 이미 지정된 GetType을 가지고 있으므로 derived 출력

 

4. 저장할 인스턴스의 클래스 형태는 Base이므로 CLR은 derived 객체가 Base로 다운 캐스팅이 가능한지 검사 후 다운캐스팅 하여 인스턴스 b에 저장한다.

 

5. 인스턴스 b의 타입을 출력 한다. b는 derived에서 이미 지정된 GetType을 가지고 있으므로 derived 출력

 

6. 저장할 인스턴스의 클래스 형태는 derived이므로 CLR은 derived 객체가 derived로 다운 캐스팅이 가능한지 검사 후 다운캐스팅 하여 인스턴스 d에 저장한다.

 

7. 인스턴스 d의 타입을 출력 한다. d는 derived에서 이미 지정된 GetType을 가지고 있으므로 derived 출력

 

CLR은 매번 캐스팅 할때 인스턴스의 현재 타입과 변환 할 타입의 상속구조, 기본타입을 비교한다.

때문에 캐스팅은 변환 타입에 따라 로드가 걸릴 수 밖에 없다. 하지만 이는 프로그램의 무결성과 보안성을 보장하기 위해 필요한 작업고, Object의 GetType 메서드가 비가상 메서드인 이유와 동일하다. 

//System.Object에서 파생 된 base
public class base
{
	
}


class Program
{
    static void Main(string[] args)
    {            
        object ob = 3;
        Base b = (Base)ob;  // InvalidCastEception 런타임 Error 발생      
    }

}

위는 InvalidCastException 런타임 에러가 발생하는 구조이다.

이유는 CLR이 int형 타입이 들어간 ob가 연관성 없는 Base Class로 캐스팅을 진행했기 때문이다.

이런 런타임 에러는 프로그램이 죽는 결과를 보여주며 해결방법은 try/catch문으로 예외를 처리해주거나

개발자가 주의하는 방법 뿐이다.

 

is와 as 연산자를 이용한 캐스팅

위에서 설명했던 명시적 캐스팅은 개발자의 실수로 런타임 에러가 발생할 가능성이 존재한다.

그래서 C#은 is 와 as 연산자를 지원한다.

 

is : 확인 하고자 하는 인스턴스의 타입 변환 가능유무를 true, false로 반환 해준다.

as : 인스턴스를 변환 하고자 하는 타입으로 변환 시킨다 성공시 변환 타입을 반환 하고 실패시 null을 반환 한다.

 

is 연산자 예시 코드

if(o is Employee)
{
	Employee e = (Employee)o;  
}

 

as 연산자 예시 코드

Employee e = o as Employee;

if(e == null)
	return;

조금더 설명을 해보자면 is는 CLR이 변환 하고자 하는 타입과 검사를 수행하고 bool 형식으로만 반환 시킨다.

다시 말해 is 연산자 예시 코드

1. if(o is Employee)

2. Employee e = (Employee)o; 

총 2번 CLR이 타입에 대해 검사를 수행한다. 

그래서 성능상으로 바로 as 연산자를 이용해서 변환 후 null 체크만 진행 하는 코드가 더 빠르다.

 

결론 

1. 캐스팅은 CLR이 타입 간 상속 관계, 기본 타입 관계를 비교하여 변환 가능하면 변환, 불가능 하면 InvalidCastException 예외를 던진다.

2. is 보다는 as를 이용하여 null 체크 하는 것이 빠르다.

 

'Program > C#' 카테고리의 다른 글

const 상수  (1) 2020.12.20
값타입의 박싱과 언박싱  (1) 2020.12.14
값 타입, 참조 타입  (0) 2020.12.13
네임스페이스와 어셈블리  (1) 2020.12.05
타입에 대하여1  (1) 2020.11.29

- 모든 타입은 System.Object를 상속한다.

class Derived {
	...
}
class Derived : System.Object {
	...
}

위의 두 Class는 같은 코드이다. 이와 같은 약속으로 모든 타입은 System.Object의 public 메서드, protected 메서드를 그냥 사용할 수 있고, override 하여 사용할 수도 있다.

아래 표는 System.Object의 메서드 표이다.

Public 메서드 설명
Equals 인수 인스턴스와 동일한 값을 가지는 경우 true 아니면 false 를 반환 한다.
ToString 현재 타입의 전체 이름을 문자열로 반환한다.
override하여 객체의 상태  또는 테스트 코드 등으로 사용하는데 현재 스레드와 연결된 CaltureInfo의 설정에 따라 적절한 지역 설정을 반영하여 반환하는 것이 좋다고 한다.
GetHashCode 객체의 값으로부터 해시 코드를 만들어 반환한다. Dictionary와 같이 해시 테이블 컬렉션에서 해당 객체를 사용하기 위해서는 override하여 해시 키값의 유일성을 만족하는 함수를 작성 해야한다. 
GetType GetType 메서드를 호출하기 위하여 사용된 객체의 타입을 설명하는 Type이라는 타입으로 부터 파생된 객체의 인스턴스를 반환한다. 여기서 반환되는 Type객체는 객체의 타입의 관한 메타데이터를 가지고 있으며 이를 사용하기 위해 Reflection 클래스들과 함께 활용할 수 있다. 비가상 메서드이며 이는 타입에 대한 정보를 속이거나 잘못 전달하여 타입 안정성을 위반하지 않도록 예방한 것이다. 
Protected 메서드 설명
MemberwiseClone 비가상 메서드로 현재 인스턴스와 동일한 타입, 동일한 값을 모드 재할당하여 생성된 인스턴의 포인터를 반환한다.
그냥 Clone 기능이라 보면 편하다.
Finalize GC가 해당 객체는 더이상 어디에서도 참조 되지 않고 사용하지 않는다고 판단하고 이 객체에 대한 메모리 공간을 회수하는 요청이 들어왔을때 호출 된다. 비관리 리소스를 가지고 있는 객체일 경우 재정의 해야한다.   

- 값 타입을 제외한 참조 타입은 모드 new 연산자를 통해 생성한다.

Employee e = new Employee("Hong")

위와 같은 명령어가 발생하면 CLR은 아래와 같은 순서로 진행시킨다.

1. 할당하려는 타입(System.Object를 포함)하여 정의된 모든 필드들을 메모리에 할당하기 위한 바이트 수를 계산한다.

2. 별도의 타입 객체 포인터(Type Object Pointer)와 동기화 블록 인덱스(Sync Block Index)가 추가되며 이는 CLR에 의해 객체를 관리하기 위해 사용되며 추가된 멤버 값들은 실제 객체의 크기에 포함된다.

더보기

🎫계산된 Byte의 수

System.Object 필드

Emplyee 필드

Type Object Pointer

Sync Block Index

3. 계산된 Byte의 수만큼 관리되는 힙으로부터 객체를 위하여 메모리를 할당하며, 처음 할당할 때에는 모든 Byte를 0으로 초기화한다.

4. 타입 객체 포인터, 블록 인덱스 멤버를 초기화한다.

5. 생성자와 인수(Hong)가 Employee 생성자 중 일치하는 인수 형태를 지닌 생성자가 실행된다. 위와 같은 경우는 Emplyee(string strNmae)이 되겠다. 상속받은 객체가 있다면 상속받은 Super Class의 생성자부터 실행되겠다.

6. 특이하게도 생성자의 마지막은 System.Object의 생성자를 부르게 되며, 이 생성자는 하는 일 없이 반환된다.

7. 모든 작업이 끝나면 생성된 객체의 포인터를 반환한다.

 

C#의 객체를 사용하는 우리는 알게 모르게 객체의 포인터를 다룬 것이다. 

그래서 일반적인 함수에 인수가 객체일 경우 Call by Reference를 사용하게 됐다.

 

결론

- 모든 타입은 System.Object를 상속받는다.

- new 연산자는 모든 초기화를 완료한 후 객체의 포인터를 반환시킨다.

 

'Program > C#' 카테고리의 다른 글

const 상수  (1) 2020.12.20
값타입의 박싱과 언박싱  (1) 2020.12.14
값 타입, 참조 타입  (0) 2020.12.13
네임스페이스와 어셈블리  (1) 2020.12.05
타입에 대하여2  (0) 2020.12.02

타입 매개변수가 IDisposable을 구현한 경우를 대비하여 제네릭 클래스를 작성하라

 

제약조건은 2가지 역할을 한다.

1. 제약조건을 정의함으로써 런타임에서의 오류를 컴파일 타임에서의 오류로 대체할 수 있다.

2. 타입 매개변수로 사용할 수 있는 타입을 명확히 규정해준다. 

 

위의 2가지 역할은 사용자가 해야 할 행동을 제시해준다. 하지만 하지 말아야 하는 행동은 제시하지 않는다.

예를 들어 IDisposable 인터페이스를 가지고 있는 타입을 제네릭 타입에 사용할 경우이다. 만약 우리가 작성한 제네릭 클래스에 IDisposable 인터페이스에 대해 구현이 안되어있다면 메모리 릭이 생길 수 도 있다.

public interface IEngine
{
	void Dowork();
}

public class EngineDriverOne<T> where T : IEngine, new()
{
    public void GetThingsDone()
    {
    	T driver = new T();
        driver.Dowork();
        //만약 T가 IDisposable 인터페이스를 가지고 있다면...?
    }
}

간단한 예시 코드를 보자 EngineDriverOne은 IEngine 인터페이를 가지고 있는 객체에 대해서 내부적으로 생성하여 테스트를 시작해준다. 자 저기서 T가 IDisposeable 인터페이스를 가지고 있다면..??

 

해결방안 1. using, as 관용구를 이용한 방법

T driver = new T();
using(driver as IDisposable)
{
    driver.Dowork();
}

using, as 관용구를 이용해 IDisposable이 구현되어있다면 Dispose를 호출, 안되어있다면 null로 Dispose를 호출하지 않는다.

 

 

해결방안 2. 타입 매개변수를 멤버변수로 직접 생성하여 내부적으로 처리하는 방법

public sealed class EngineDriver<T> : IDisposable where T : IEngine, new()
{
    //driver라는 멤버변수는 사용하기 직전까지 생성하지 않는다.
    private Lazy<T> driver = new Lazy<T>(()=> new T());
    
    //사용자가 해당 클래스의 GetThingsDone()을 호출하면 driver 멤버변수를 생성하여 작업을 시작한다.
    public void GetThingsDone() => driver.Value.Dowork();
    
    //IDisposable 패턴
    public void Dispose()
    {
        //만약 driver를 한번이라도 사용했다면 호출 된다.
    	if(driver.IsValueCreated)
        {
            var resource = driver.value as IDisposable;
            resource?.Dispose();
        }
    }
}

 

seald 예약어로 EngineDriver를 super class로의 사용을 방지했다. 이유는 EngineDriver의 IDisposable유무를 모르는 개발자가 사용하여 파생 클래스의 Dispose를 호출하지 않을 경우를 방지한 것이다. 상속을 못한다는 단점이 있지만 추후에 생길 수 있는 문제를 방지한 부분이다.

 

 

해결방안 3.  타입 매개변수를 멤버변수로 가지고 있지만 참조 형태로만 가지고 있는 방법.

public sealed class EngineDriver<T> where T : IEngine
{
    private T driver;
    
    public EngineDriver(T driver)
    {
        this.driver = driver;
    }
    
    public void GetThinsDone()
    {
        driver.Dowork();
    }
}

EngineDriver의 멤버변수를 참조 형태로만 가지고 있다가 외부 개발자의 Dispose에 의존하는 방법이다.

위의 방법은 외부에서 생성된 인스턴스를 가져오기 때문에 new() 한정자도 필요 없다.

 

 

이렇게 제네릭 클래스에서 Dispose의 처리까지 살펴봤다.

Dispose 패턴에 대한 처리 방법은 다양하고 명확한 정답은 없다. 설계하기 나름이다.

 

결론

해결방안 1.  using, as 관용구를 이용한 방법

해결방안 2.  타입 매개변수를 멤버 변수로 직접 생성하여 Dispose패턴을 직접 구현하는 방법.

해결방안 3.  타입 매개변수를 멤버변수로 가지고 있지만 참조 형태로만 가지고 있는 방법.

 

 

'Program > Effective C#' 카테고리의 다른 글

Effective C# ITEM20  (0) 2020.11.25
Effective C# ITEM 19  (0) 2020.11.18
Effective C# ITEM 18  (1) 2020.11.15
Effective C# ITEM 17  (0) 2020.11.08
Effective C# ITEM16  (0) 2020.11.05

ICompareble<T>와 IComparer<T>르 이용하여 객체의 선후 관계를 정의하라

 

컬렉션을 원하는 조건으로 정렬하는 방법은 다양하다. 그중 C#은 ICompareble<T>와 IComparer<T> 인터페이스를 지원하고 이는 MS에서 최적화 해준 Sort()기능을 사용할 수 있다.

 

public class Customer : IComparable<Customer>, IComparable
{
        private readonly string strName;
        public Customer(string strName)
        {
            this.strName = strName;
        }

        public int CompareTo(Customer other) 
            => strName.CompareTo(other);

        int IComparable.CompareTo(object other)
        {
            if (!(other is Customer))
                throw new ArgumentException("Argument is not Customer", nameof(other));

            Customer otherCurtomer = other as Customer;

            return this.CompareTo(otherCurtomer);
        }
}

Customer Class의 strName 문자열 기준으로 정렬기능이 있는 코드 이다.

구지 IComparable을 쓸필요가 있을까 싶지만 .NET Framework 2.0 이전에 개발된 기능을 사용하고 싶다면 필요하다.

여기서 눈여겨 봐야하는 부분은 

int IComparable.CompareTo(object other)
{
            if (!(other is Customer))
                throw new ArgumentException("Argument is not Customer", nameof(other));

            Customer otherCurtomer = other as Customer;

            return this.CompareTo(otherCurtomer);
}

이 부분이다. 이 부분 덕분에 

Customer c1 = new Customer("Health Man");
Employee e1 = new Employee();

if(c1.CompareTo(e1) == 0)
	Console.WriteLine("Is Equal!");

아래와같은 이상한 동작을 피할 수 있다.

이유는 IComparable.CompareTo라는 강력한 타입으로 직접 지정을 했기 때문이다. 따라서 사용자는 구지 사용하고 싶다면 

Customer c1 = new Customer("Health Man");
Employee e1 = new Employee();

if(c1.CompareTo((Customer)e1) == 0)
	Console.WriteLine("Is Equal!");

과 같이 명시적으로 캐스팅을 진행해야 한다. 하지만 퍼포먼스에 신경을 쓰는 개발자 이거나 뭔가 이상한 낌새를 느끼는 개발자는 위와같은 코드를 피할 확률이 높아진다.

 

만약 IComparable.CompareTo를 CompareTo으로 코딩을 하면 어떻게 될까?

 

간단한 좌우를 판단하는 로직에서는 괜찮지만 평균적으로 nlog(n)의 시간복잡도를 가지고 있는 정렬 알고리즘은 nlog(n)만큼의 박싱과 언박싱이 이루어진다. 알다시피 박싱과 언박싱은 최소화 하는게 최선의 방법이라 했다.

 

그 다음은 Customer의 이름뿐만아니라 수요액에 따라 정렬하는 기능을 추가하고 한다.

 

우선 클래스의 기본적인 선후 관계의 연산자들을 정의한다.(테스트 결과 구지 안해도 된다)

public static bool operator <(Customer left, Customer right) => left.CompareTo(right) < 0;
public static bool operator >(Customer left, Customer right) => left.CompareTo(right) > 0;
public static bool operator <=(Customer left, Customer right) => left.CompareTo(right) <= 0;
public static bool operator >=(Customer left, Customer right) => left.CompareTo(right) >= 0;

 

우선 2가지 방법이 있다. 

1번은 .NET Framework에 제네릭 기능이 포함된 이후 개발된 API에 적용가능하다.

2번은 1번 이전에도 가능하다.

 

1번 Comparision 델리게이트를 이용한 방법

public static Comparison<Customer> CompareByMoney => (left, right) => left.dblMoney.CompareTo(right.dblMoney);

2번 IComparer 인터페이스로 추가적인 선후 관계 논리를 제공하는 경우를 위한 표준화된 방법

 private class MoneyComparer : IComparer<Customer>
{
            int IComparer<Customer>.Compare(Customer left, Customer right) => left.dblMoney.CompareTo(right.dblMoney);
}

private static Lazy<MoneyComparer> moneyCompare = 
	new Lazy<MoneyComparer>(()=>new MoneyComparer());

public static IComparer<Customer> MoneyCompare => moneyCompare.Value;

 

1,2번 코드 사용방법

List<Customer> customers = new List<Customer>();
customers.Add(new Customer("c", 25));
customers.Add(new Customer("b", 23));
customers.Add(new Customer("z", 200));
customers.Add(new Customer("d", 2));
customers.Add(new Customer("j", 2021));
customers.Add(new Customer("f", 1215));
customers.Add(new Customer("q", 12513));
customers.Add(new Customer("i", 4574));

//1번 : Comparsion 델리게이트 사용 방법
customers.Sort(Customer.CompareByMoney);

//2번 : Comparer 인터페이스 사용 방법
customers.Sort(Customer.MoneyCompare);

 

자 이렇게 ICompare, IComparer, Comparision을 이용하여 객체의 관계 및 컬렉션에서의 정렬방법을 포스팅했다.

 

결론. 

1. ICompare<T>는 평소와 같이 구현 ICompare는 이전버전을 강력한 타입 ICompare.CompareTo 으로 정의 및 구현

(박싱, 언박싱 최소화)

2. Comparision<T> 델리게이트를 이용하면 컬렉션 정렬 메서드를 제공하기 쉽다.

(MS에서 최적화 시킨 Sort기능 사용)

 

'Program > Effective C#' 카테고리의 다른 글

Effective C# ITEM21  (0) 2020.11.26
Effective C# ITEM 19  (0) 2020.11.18
Effective C# ITEM 18  (1) 2020.11.15
Effective C# ITEM 17  (0) 2020.11.08
Effective C# ITEM16  (0) 2020.11.05

런타임에 타입을 확인하여 최적의 알고리즘을 사용하라

 

제네릭 타입을 사용하면 개발자 입장에서 적은 코드양에 비해 많은 기능을 얻을 수 있다.

하지만 제네릭 타입을 사용하여 제한 조건을 추가하는 순간부터 런타임 중에 원치 않는 형 변환(Up Casting, Down Casting)을 시도하여 성능을 저하시킬 수도 있도 있고, 특정 최적화된 타입을 쓰지 못할 수도 있다. 따라서 최적의 성능을 원할 경우에는 제네릭 타입보다는 개발자가 직접 구현하는 것이 좋을 때도 있다. 아래 예시 코드로 시작하겠다.

 

public sealed class ReverseEnumerable<T> : IEnumerable<T>
{
    private class ReverseEnumerator : IEnumerator<T>
    {
        int currentindex;
        IList<T> collection;
        
        public ReverseEnumerator(IList<T> srcCollection)
        {
            collection = srcCollection;
            currentindex = collection.Count;
        }
        
        public T Current => collection[currentindex];
        
        public void Dispose()
        {
        	//IEnumerator<T>는 IDisposable을 상속받고 있다.
        }
        
        //IEnumerator<T>의 멤버들
        object System.Collections.IEnumerator.Current => this.Current;
        public bool MoveNext() => --currentIndex >= 0;
        public void Reset() => currentIndex = collection.Count;
    }
    
    IEnumerable<T> sourceSequence;
    IList<T> originalSequence;
    
    public ReverseEnumerable(IEnumerable<T> sequence)
    {
    	sourceSequence = sequence;
    }
    
    public IEnumerator<T> GetEnumerator()
    {
    	if(originalSequence == null)
        {
            originalSequence = new IList<T>();
            foreach(T item in sourceSequence)
                originalSequence.Add(item);
        }
        
        return new ReverseEnumerator(originalSequence);
    }
    
   	//IEnumerable<T>의 멤버
    System.Collections.IEnumerator
    	System.Collections.IEnumerable.GetEnumerator() =>
            this.GetEnumerator();
}

이 Class의 기능은 IEnumerable<T>를 상속받는 컬렉션을 매개변수로 입력 후 역순환 컬렉션을 반환하는 Class 이다.

자 우선 GetEnumerator()의 함수에서 originalSequence를 복사하는 동작이 거슬린다.

 

1. 생성자에서 originalSequence를 변환 시켜서 저장한다.

   - 만약 변환을 실패해도 기존의 null 처리가 되어있기 때문에 괜찮다.


    public ReverseEnumerable(IEnumerable<T> sequence)
    {
    	sourceSequence = sequence;
        
        //만약 sequence가 IList<T>가 구현되지 않았다면 기존의 복사동작이 이루어진다.
        //즉 성능 최적화 부분에 대해서 가능성을 열어 둔 부분이다.
        originalSequence = sequence as IList<T>;
    }

2. IList<T>자체를 받아오는 생성자를 오버로드한다.

  - 아시다시피 IList<T>는 IEnumerable<T>를 상속받기 때문에 초기 Downcasting으로 저장 시킨다. 

    public ReverseEnumerable(IEnumerable<T> sequence)
    {
    	sourceSequence = sequence;
        
        //만약 sequence가 IList<T>가 구현되지 않았다면 기존의 복사동작이 이루어진다.
        //즉 성능 최적화 부분에 대해서 가능성을 열어 둔 부분이다.
        originalSequence = sequence as IList<T>;
    }
    
     public ReverseEnumerable(IList<T> sequence)
    {
    	sourceSequence = sequence;
        originalSequence = sequence;
    }

3. sourceSequence에 IList<T> 타입이  들어 갈경우 ICollection<T> 성질을 이용하여 최적화를 추가 할 수 있다.

    - 컬렉션의 사이즈를 미리 지정하여 Add의 로드를 줄인다.

public IEnumerator<T> GetEnumerator()
{
    if (originalSequence == null)
    {
        //sourceSequence가 IList<T>를 받아온 경우 가능한 루프
        if (sourceSequence is ICollection<T>)
        {
            ICollection<T> source = sourceSequence as ICollection<T>;
            
            /**
            //컬렉션의 사이즈를 미리 지정한다.
            //기본적으로 가변형 컬렉션은 삽입, 삭제 시 사용하는 메모리를 가변 시키는 로드가 있으므로 
            //미리 할당 시켜놓고 사용하면 전자 보다 빠르다
            **/
            originalSequence = new List<T>(source.Count);
        }
        else
            originalSequence = new List<T>();


        foreach (T item in sourceSequence)
            originalSequence.Add(item);
    }

    return new ReverseEnumerator(originalSequence);
}

 

최종 코드

public sealed class ReverseEnumerable<T> : IEnumerable<T>
{
    private class ReverseEnumerator : IEnumerator<T>
    {
        int currrentIndex;
        IList<T> collection;

        public ReverseEnumerator(IList<T> srcCollection)
        {
            collection = srcCollection;
            currrentIndex = srcCollection.Count;
        }

        public T Current => collection[currrentIndex];

        public void Dispose()
        {
                
        }

        object IEnumerator.Current => this.Current;

        public bool MoveNext()
            => --currrentIndex >= 0;

        public void Reset()
            => currrentIndex = collection.Count;
    }

       
    IEnumerable<T> sourceSequence;
    IList<T> originalSequence;

    public ReverseEnumerable(IEnumerable<T> sequence)
    {
        sourceSequence = sequence;

        originalSequence = sequence as IList<T>;
    }

    public ReverseEnumerable(IList<T> sequence)
    {
        sourceSequence = sequence;

        originalSequence = sequence;
    }


    public IEnumerator<T> GetEnumerator()
    {
        if (originalSequence == null)
        {
            if (sourceSequence is ICollection<T>)
            {
                originalSequence = new List<T>(source.Count);
            }
            else
                originalSequence = new List<T>();


        foreach (T item in sourceSequence)
            originalSequence.Add(item);
    }

        return new ReverseEnumerator(originalSequence);
    }

    IEnumerator IEnumerable.GetEnumerator()
        => this.GetEnumerator();
}

위의 코드는 매개변수에 대한 테스트가 모두 런타임에 진행 된다. 즉 개발자가 유의깊게 잘 개발해야한다.

나중에 TDD 관련 공부도 해야겠다. 

 

결론

1. 제네릭의 제한조건에 너무 의존하지말자.

2. 런타임 최적화도 고려해보자

3. 런타임 최적화 방식은 런타임의 타입을 확인하여 처리하는 코드를 추가한다.

4. 제한 조건 vs 직접 코딩 상황에 따라 개발

 

'Program > Effective C#' 카테고리의 다른 글

Effective C# ITEM21  (0) 2020.11.26
Effective C# ITEM20  (0) 2020.11.25
Effective C# ITEM 18  (1) 2020.11.15
Effective C# ITEM 17  (0) 2020.11.08
Effective C# ITEM16  (0) 2020.11.05

반드시 필요한 제약 조건만 설정하라

 

제네릭 타입의 제약 조건은 클라이언트 개발자와 아키텍처 개발자와의 적절한 조건을 찾아서 설계되어야 한다.

C# 컴파일러는 타입 매개변수에 대한 제약조건이 없을 시 System.Object의 public 메서드만 제공한다. 왜냐하면 컴파일러는 타입 매개변수에 대한 어떠한 추측도 할 수 없고, C#의 특성상 모든 타입은 System.Object에서 파생된다는 규칙 때문이다.

 

클라이언트 개발자와 아키텍쳐 개발자와의 적절한 조건을 조절해주는 제네릭 타입2가지 방법.

   1. 런 타임에서 구분하기

 

public class Test<T>
{
	public bool AreEqual(T a, T b)
	{
		if(a is IComparable<T> &&  b is IComparable<T>)
		{
			IComparable<T> aVal = a as IComparable<T>;
			IComparable<T> bVal = b as IComparable<T>;
			return (aVal.CompareTo(b) == 0);
		}

		throw new ArgumentException("Type does not implement IComparable<T>");
	}
}

   - 개발자가 직접 오류가 발생하지 않게 처리를 진행했다.(잘못된 코드가 있어도 컴파일 완료)

 

   2. 컴파일 타임에서 구분하기

 

public class Test<T> where T : IComparable<T>
{
	public bool AreEqual(T a, T b)
		=> (a.CompareTo(b) == 0); 	
}

   - 컴파일러를 통해 오류가 발생하지 않게 했다(잘못된 코드가 있을 경우 컴파일 오류).

 

 

System.Object의 public 메서드만 제공

public static bool AreEqal<T>(T a, T b)
	=> a.Equals(b);

  위의 코드의 Eqauls는 기본적으로 Object의 Eqauls를 호출한다. 하지만 성능상의 문제가 발생한다.

  1. object타입이 아닌 다른 참조 타입일 수도 있기 때문이다. (다른 타입 -> object 박싱).

  2. T타입 a와 b가 object.Equals를 오버라이드 하는지 탐색. (런타임에서 탐색하는 비용)

 

public static bool AreEqal<T>(T a, T b)
	where T : IEquatable<T>
    	=> a.Equals(b);

  두번째코드는 IEquatable<T>의 Equals를 호출한다.  이는 위에서 설명했던 2가지의 약간의 성능 문제를 해결할 수 있다.  하지만 다른 개발자는 해당 메서드를 사용하기 위해 Class 설계 시 IEquatable<T> 인터페이스를 구현해야 한다.

뭐 지금은 하나쯤이야 구현하면 되지 하지만 정말 다양한 상황이 발생하는 이 세상은 IEquatable<T> 인터페이스뿐만 사용하게 둘까??

 

그래서 때때로 AreEqual 메서드를 오버로드 형태로 제공하면 제공하면 위와 같은 문제(다른 개발자의 편의를 생각하는 문제)를 풀어줄 수 있다. '1. 런 타임에서 구분하기 예시 코드'처럼 말이다. 아키텍처 설계자가 직접 처리하는 것이다.

 

때때로 new 대신 default()를 사용하면 new() 제약 사항이 필요 없을 수도 있다.

실제로 잘 작성된 제네릭 타입은 지정한 타입의 기본값을 가져오기 위해서 default()를 사용한다.

아래 두 코드를 비교해보자

 

1. 조건을 만족하는 첫 번째 객체를 찾아내고 그렇지 못하면 기본값을 반환하는 코드이다.

 

public static T FirstOrDefault<T>(this IEnumerable<T> sequence, Predicate<T> test)
{
    foreach(T val in sequence)
    {
    	if(test(val))
            return val;
    }
    
    return default(T);
}

 

 

2. T 타입의 객체를 생성하는 펙토리 메서드를 사용하는데, 이 팩토리 메서드가 null을 반환하면 기본 생성자를 호출한 후 그 값을 반환한다.

 

public delegate T FactoryFunc<T>();

public static T Factory<T>(FactoryFunc<T> makeANewT) where T : new()
{
    T rVal = makeANewT();
    
    if(rVal == null)
    	return new T();
    
    return rVal;
}

차이점 

1. 1번 코드는 default()를 사용하여 new() 제약조건을 사용하지 않았다.

2. 1번 코드의 T는 값 타입인 경우 JIT 컴파일러가 null 테스트 코드를 제거해준다.

3. 1번 코드는 값 타임과 참조 타입 모두 사용 가능하다.

4. 2번 코드는 new()를 사용하여 new() 제약 조건을 사용했다. 

5. 2번 코드는 new() 제약 조건을 통해 참조 타입이기 때문에 null 체크를 해야 한다.

6. 2번 코드는 new() 제약 조건에 의해 참조 타입만 사용 가능하다.

 

new(), struct, class,를 제약 조건으로 설정하는 경우에는 항상 주의해야 한다. 위의 코드와 같이 제약 사항을 추가하면 객체의 생성을 제한하고 이는 코드의 재사용성에 영향을 줄 수 있다. 그래서 이상적으로 new(), struct, class제약 사항은 가능한 피하는 것이 좋다. new T() 보다는 default(T)!!!!!!!

 

결론 

1. 제네릭 타입은 제약사항이 없을 시 System.Object로 시작한다.

2. 너무 많은 제약사항은 클라이언트 개발자에게 독이 된다.

3. System.Object의 public 메서드가 호출될 것 같으면 오버로드 형태로 내가 직접 코딩하여 제공한다.

4. new(), struct, class 제약사항은 가능하면 피하자

5. 펙토리 메서드 작성 시 new T() 보다는 default(T)를 사용하자

 

'Program > Effective C#' 카테고리의 다른 글

Effective C# ITEM20  (0) 2020.11.25
Effective C# ITEM 19  (0) 2020.11.18
Effective C# ITEM 17  (0) 2020.11.08
Effective C# ITEM16  (0) 2020.11.05
Effective C# ITEM15  (0) 2020.11.03

표준 Dispose 패턴을 구현하라

 

GC의 영역 밖인 비관리 리소스는 반드시 사용자가 직접 메모리 해제 코드 작성을 해야 한다.

해제 방법은 간단하다. 아래에서 설명한 Dispose와 finalizer를 작성하여 처리해야 한다.

 

Dispose 패턴에 필요한 작업

 

Super class

1. 리소스를 정리하기 위해서 IDisposable 인터페이스를 구현해야 한다.

2. 멤버 필드로 비관리 리소스를 포함하는 경우에 한해 방어적으로 동작할 수 있도록 finailzer를 추가해야 한다.

3. Dispose와 finailzer(존재하는 경우)는 실제 리소스 정리 작업을 수행하는 다른 가상 메서드에 작업을 위임하도록 작성돼야 한다. 파생 클래스가 고유의 리소스 정리 작업이 필요한 경우 이 가상 메서드를 재정의할 수 있도록 하기 위함이다.

 

Derived class

1. 파생 클래스가 고유의 리소스 정리 작업을 수행해야 한다면 베이스 클래스에서 정의한 가상 메서드를 재정의 한다.

2. 멤버 필드로 비관리 리소스를 포함하는 경우에만 finailzer를 추가해야 한다.

3. 베이스 클래스에서 정의하고 있는 가상 함수를 반드시 재호출해야 한다.

 

핵심은 비관리 리소스를 멤버 필드로 포함하고 있다면 반드시 finalizer를 구현해야 한다.

 

하지만 finalizer는 GC의 finalizer queue에 객체들의 참조값을 한번 추가된 후에 수행되기 때문에 자연스레 GC의 관리 힙에 로드가 생기게 된다.

우리는 여기서 finailzer에서 생기는 로드를 최대한으로 줄이는 방법을 찾아보겠다.

 

class Exampleclass : IDisposable
{
	bool disposed = false;
	
    public Exampleclass(){}
    
    protected virtual void Dispose(bool disposing)
    {
    	if(disposed)
        	return;
        
        if(disposing)
        {
        	//관리 객체 처리          
        }
                
        //비관리 객체 처리
                
        disposed = true;
    }
    
    public void Dispose()
    {
    	Dispose(true);
        GC.SupperssFinalize(this); // 해당 객체에 대해서 GC가 가비지 수집을 하지 않도록 예약 
    }
    
    public void ExampleFunc()
    {
    	if(disposed)
        	throw new ObjectDisposedException("Exampleclass","Called Example Method on Disposed object");
            
        ...//Func 구현
    }
    
    //finalizer는 GC가 호출 한다.
    //Client 개발자가 직접 Dispose를 호출했다면 finalizer는 호출되지 않는다.
    ~Exampleclass() => Dispose(false);	
}

public class Progream
{
    static public void Main()
    {
        Exampleclass ex = new Exampleclass(); // 할당...
        ...//사용 하는 코드
        ex.Dispose(); //사용자가 호출하는 코드 사용자가 호출하지 않았다면 GC에서 Dispose(false) 자동 호출
    }
}

코드 설명

1. IDisposable 인터페이스를 상속받은 Exampleclass. 

2. Dispose 처리 유무를 확인하기 위한 bool disposed 멤버 변수를 false로 초기화.

3. public void Dispose 메서드 구현(IDisposable 상속 받았기 때문)

     - Dispose(bool) 메서드 호출

     - GC의 가비지 수집을 스킵하기위한 GC.SuppressFinalize(this)

4. protected virtual void Dispose(bool disposing) 메서드 구현(코드 작성자가 직접 해제 작업을 처리 하기 위함)

     - 매게 변수 disposing을 확인

        true : 비관리, 관리 리소스 삭제

        false : 비관리 리소스 삭제

     - disposed를 true로 변환 (삭제 되었기 때문)

5. finalizer 구현(Client 개발자가 dispose를 직접 호출하지 않을 경우를 대비하여)

     - Dispose(false) 호출

 

Client개발자는 해당 객체를 사용하고 Dispose 메서드를 직접 호출해야한다.

Dispose메서드를 호출하지 않을 경우에는 finalizer가 호출되어 비관리 리소스가 처리 되지만 프로그램의 전체적인 속도만 느려진다.

Class 설계자는 Client개발자의 코드에서 메모리 릭을 안남기고 오로지 코드 구조에 따라 프로그램의 퍼포먼스만 영향을 준다.

 

다음은 해당 class에 대한 파생 클래스 코드이다.

public class DerivedExampleclass : Exampleclass
{
    private bool disposed = false;
    
    protected override void Dispose(bool disposing)
    {
        if(disposed)
            return;
            
        if(disposing)
        {
            //관리 객체 처리
            ...
        }
        
        //비관리 객체 처리
        ...
        
        disposed = true;
        
        //부모 class 객체 처리
        base.Dispose(disposing);
    }
        
    public void DerivedFunc()
    {
        if(dieposed)
            throw new ObjectDisposedException("Derivedclass","Called Derived Method on Disposed object");
    }
    
    //Client 개발자가 호출하지 않을시 GC에서 자동으로 호출 (파생클래스 -> 베이스 클래스 순서로 호출)
    ~DerivedExampleclass() => Dispose(false);
}

자 주의 할점은 base.Dispose(disposing) 이다.

그외에는 이전 Exampleclass와 같다.

 

코드 설명

1. DerivedExampleclass는 Exampleclass를 상속.

2. DerivedExampleclass는 Exampleclass의 protected virtual void Dispose(bool disposing)을 override하여 구현

     - 매게변수 disposing을 확인

          true : 비관리, 관리 리소스 삭제

          false : 비관리 리소스 삭제

     - base.Dispose(dispose) 호출

     - disposed를 true로 변환 (삭제 되었기 때문)

3. finalizer 구현(Client 개발자가 dispose를 직접 호출하지 않을 경우를 대비하여)

     - Dispose(false) 호출

     - base.Diespose(flase) 호출

 

파생 클래스도 슈퍼 클래스와 원리는 비슷하다. 다른점이 있다면 파생클래스는 Dispose하면서 슈퍼 클래스도 같이 호출되는 것이다. 이는 파생클래스만 Dispose 할 경우 슈퍼 클래스는 나중에 finalizer가 GC에 의해 호출되지만 퍼포먼스의 영향을 주기 때문에 함께 호출시킨 것이다.

파생 클래스도 개별적으로 disposed라는 플래그가 존재한다. 이는 파생 클래스에서 독자적으로 비관리 리소스를 가짐을 처리 하기 위해서다.

 

결론

    1. 비관리 리소스를 사용할 경우 반드시 Dispose패턴을 사용하자.

    2. 비관리 리소스를 사용할 경우 반드시 반드시 finalizer를 작성하자. (상식적으로 Dispose를 호출해야 하지만 호출하지 않는 Client 개발자를 탓할수는 없다)

    3. Dispose와 finalizer는 리소스 삭제만 하자 다른 기능도 추가로 가진 경우 야근을 부른다.

 

 

 

 

 

 

 

   

 

 

 

 

 

'Program > Effective C#' 카테고리의 다른 글

Effective C# ITEM 19  (0) 2020.11.18
Effective C# ITEM 18  (1) 2020.11.15
Effective C# ITEM16  (0) 2020.11.05
Effective C# ITEM15  (0) 2020.11.03
Effective C# ITEM14  (0) 2020.10.28

+ Recent posts