타입 매개변수가 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

생성자 내에서는 절대로 가상 함수를 호출하지 말라

 

객체가 완전히 생성되었다고 판단하기 전까지는 절대로 가상 함수를 호출하는 코드는 피해야 한다.

예측할 수 없는 함수가 수행되기 때문이다. 예시 코드를 보여주겠다.

Class Baseclass
{
    protected Baseclass()
    {
    	Func();
    }
    
    protected virtual void Func()
    {
        Console.WriteLine("Base class Func");
    }
}

class Derived : Baseclass
{
	private readyonly string msg = "Before Initializer"
    
    public  Derived(string msg)
    {
        this.msg = msg;
    }
    
    protected override void Func()
    {
    	Console.WriteLine(msg);
    }
    
    public static void Main()
    {
        var d = new Derived("Constructed in main");
    }
}

구조는 이렇다

Baseclass는 가상 함수 Func를 가지고 있다.

Derived는 Baseclass의 파생 클래스이다.

Derived는 Baseclass의 가상 함수 Func를 override 했다.

 

콘솔 창에는 어떤 결과가 출력될까??

1. "Base class Func"

2. "Before Initializer"

3. "Constructed in main"

 

자 우리가 이전에 포스팅했던 객체의 생성 순서를 생각해보자

1. 파생 클래스의 멤버 변수에 대한 초기화 구문 수행

2. 베이스 클래스의 멤버변수에 대한 초기화 구문 수행

3. 베이스 클래스의 생성자.

4. 파생 클래스의 생성자

 

그렇다면 "Base class Func" 이 출력되겠구나!!

 

하지만 정답은 "Before Initializer"이다.

 

이유를 살펴보자. 

1. 베이스 클래스의 생성자를 살펴보면 자기 클래스 내에 정의된 가상 함수를 호출하고 있다.

2. 파생 클래스가 베이스클래스의 가상함수를 재정의 했기 때문에 파생 클래스의 재정의 함수가 호출된다. (런타임에 객체가 파생클래스 이기 때문)

3. msg는 "Before Initializer"인 상태로 Func() 수행

4. msg에 "Constructed in main" 값 저장

 

가상 함수 호출에 대한 방법은 추 후에 좀 더 자세히 살펴보겠다.

위의 코드를 좀더 괜찮은 코드로 바꿔보겠다.

abstract class Baseclass
{
    protected Baseclass()
    {
        Func();
    }
    
    protected abstract void Func();
}

class Derived : Baseclass
{
    private readonly string msg = "before Initializer";
    
    public Derived(string msg)
    {
    	this.msg = msg;
    }
    
    protected override void Func()
    {
    	Console.WriteLine(msg);
    }
    
    public static void Main()
    {
    	var d = new Derived("Constructed in main");
    }
}

위의 코드와 결과는 같지만 코드의 가독성을 높였다.

멤버 변수 msg의 상태는 초기에  "before Initializer"에서 출력 후 "Constructed in main"으로 변했다.

 

이처럼 베이스 클래스의 생성자 내에서 가상 함수를 호출하면 파생 클래스가 가상 함수를 어떻게 구현했는지에 따라 매우 민감하게 동작한다. 파생 클래스는 Client 개발자들이 실제 어떻게 개발할지 모르는 노릇이다. 일반적으로 파생 클래스의 생성자 함수는 2가지로 보는데 첫번째는 매개변수를 이용하여 파생클래스를 초기화 하는 방식 두번째는 매개변수 초기화 구문을 이용해서 초기화를 시키는데 예시와 같은 Baseclass를 작성하면 파생클래스의 제약사항이 많아진다.

 

결론

1. 객체의 생성자 안에서 가상 함수를 호출하지 말자.

2. 호출해도 되는 경우는 매개 변수가 없는 생성자인 상황뿐이다.

3. 그냥 생성자에 가상 함수 쓰지 말자!!!!!! 🤦‍♂️

 

 

 

 

 

 

 

 

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

Effective C# ITEM 18  (1) 2020.11.15
Effective C# ITEM 17  (0) 2020.11.08
Effective C# ITEM15  (0) 2020.11.03
Effective C# ITEM14  (0) 2020.10.28
Effective C# ITEM13  (0) 2020.10.28

불필요한 객체를 만들지 말라

 

이전에 포스팅한 가비지 컬렉터에 대해 로드를 줄이는 방법을 포스팅하겠다.

 

1. 자주 호출되는 함수에 생성하는 변수는 멤버 변수를 고려해보자

 

override void OnPaint(EvengArgs e)
{
    using(Font MyFont = new Font("Blue", 10.0f))
    {
    	e.Graphics.DrawString(MyFont...);
    }
    
    base.OnPaint(e);
}

 

GUI 프로그램을 해본 개발자들은 OnPaint 함수에 대해 알 것이다.

정말 자주 호출되는 함수이다. (마우스가 해당 UI 컨트롤 위에 한 픽셀 움직이기만 해도 호출된다.)

 

그럼 당연히 MyFont라는 객체는 OnPaint함수가 호출되는 만큼 지역적으로 할당했다가 가비지 객체로 변한다.

이 작업이 100번, 500번, 10000번 호출되면 가비지 객체가 늘어나고 결국 가비지 컬렉터가 3초만 기다려도 미치는 사용자들에게 피해를 줄 것이다.

해결 방법은 간단하다. MyFont를 멤버 변수로 쓱 올려주면 끝이다.

Font _myFont = new Font("Blue", 10.0f);

override void OnPaint(EvengArgs e)
{
    e.Graphics.DrawString(_myFont...); 
    base.OnPaint(e);
}

자 해결되었다. 하지만 문제가 있다. Font 타입은 IDisposable 인터페이스를 구현한 타입의 객체는 멤버변수로 변하면

우리 개발자들은 야근을 피하기 위해 Dispose패턴을 사용하여 Font객체를 제거해줘야 한다.  Dispose패턴은 ITEM 17에 포스팅할 것이니 알고 싶으면 ITEM17로 가길 바란다.

 

2. 지연 평가(Lazy evaluation) 알고리즘을 고려해보자

 

지연 평가 알고리즘 원리는 간단하다. 

호출 직전까지 할당을 지연시키는 것이다.

실제 .NET Framework 설계자는 지연 평가 알고리즘을 이용해 정적 속성인 Brush 객체를 설계했다.

 

    1. 호출 전까지 null형태로 메모리를 사용하지 않는다.

    2. 호출 시 null 체크 후 정적 속성으로 메모리를 잡는다.

    3. 이후 호출에는 같은 객체를 사용하여 추가 로드를 잡지 않는다.

 

private static Brush blackBrush;

public static Brush Black
{
    get
    {
        if(blackBrush == null)
            blackBrush = new SolidBrush(Color.Black);
            
        return blackBrush;
    }
}

 

위의 두 방법은 단점도 고려해봐야 한다.

    1. 경우에 따라 생성된 객체가 메모리상에 필요 이상으로 오랫동안 남아 있을 수 있다.

    2. Dispose() 메서드를 호출해야 할 시점을 결정할 수 없기 때문에 비관리 리소스를 삭제할 수 없다.

 

3. string 객체는 변경할 수 없는 타입이다.

자 무슨 말이냐 하면 우리가 평소에 사용하던 연산자를 이용한 문자열 처리가 쓸데없는 가비지를 만든다는 것이다.

예시 소스로 보여주겠다.

 

 

int value = 3;
string msg = "Hello";
msg += "My";
msg += value.toString();

 

 

위와 같은 방법은 아래 방법으로 작업이 이루어진다.

 

 

int value = 3;
string msg = "Hello";
string temp1 = new string(msg + "Hallo");
msg = temp1; //Hello 는 가비지가 된다.
string temp2 = new string(msg + value.toString());
msg = temp2; //Hello, Hallo 는 가비지가 된다
//문자열 "Hello", "Hallo", temp1, temp2 모두 가비지가 된다.

 

보이는가? 몇 개의 가비지 객체가 생성되었는가? 더 이상 볼 수가 없다. 다음의 두 방법으로 해결하자

 

    1. 문자열 보간법을 활용한 코드로 한 번에 문자열을 생성한다.

 

int value = 3;
string msg = $"Hello Hallo {value.toString()}";

 

    2. StringBuilder 클래스를 사용한다.

 

int value = 3;
StringBuilder msg = new StringBulider("Hello");
msg.Append("Hallo");
msg.Append(value.toString());
string finalmsg = msg.toString();

 

StringBuilder는 실제 수정 가능한 문자열을 나타내기 위한 타입으로, 새로운 문자열 생성, 수정, 변경 등의 작업을 수행할 수 있다.

단계적으로 변환 가능한 문자열을 처리하고 최종 적으로 변경 불가능한(immutable) 문자열을 처리할 때 사용하면 된다.

문자열 보간법과 StringBuilder에 대한 선택은 개발자 나름이지만 필자가 찾아본 결과

간단한 문자열 처리는 문자열 보간법

매우 많은 변화의 문자열 처리(50번 이상의 변화)는 StringBuilder가 더 좋다고 한다.

 

결론

1. 자주 할당하는 지역 객체에 대해서 멤버 변수로 옮길지 고려하자.

2. 사용 직전까지 할당을 미루다가 할당 후에는 한번 할당 한 객체를 계속 사용한다.

3. string을 변환시키기보다는 문자열 보간법, StringBuilder를 활용하자.

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

Effective C# ITEM 17  (0) 2020.11.08
Effective C# ITEM16  (0) 2020.11.05
Effective C# ITEM14  (0) 2020.10.28
Effective C# ITEM13  (0) 2020.10.28
Effective C# ITEM 12  (0) 2020.10.26

초기화 코드가 중복되는 것을 최소화하라

 

생성자를 작성하다보면 우리는 다양한 방법을 생각한다.

1. 매개 변수가 없는 생성자

2. 매개 변수가 있는 생성자

3. 오버로딩한 생성자들

4. 매개 변수가 기본값을 가지는 생성자

5. Client가 호출 불가능한 생성자(Singleton pattern)

 

오늘은 이 중 3, 4항목을 다루겠다.

 

3. 오버로딩한 생성자들

장점

a. 코드의 결합도가 낮다.

b. 코드 변경시 Client의 코드 수정을 덜 신경써도 된다.

단점

a. 코드량이 늘어 나고 4번 항목에 비해 제공하는 옵션이 상대적으로 적다.

public class MyClass
{
	private List<int> datas;
    
	private string name;
    
	public MyClass() : 
		this(0,"")
	{
    
	}
    
	public MyClass(int Length) :
		this(Length, "")
	{
	}
    
	public MyClass(int Length, string name)
	{
		datas = new List<int>(Length);
		this.name = name;
	}    
}

 

4. 매개 변수가 기본값을 가지는 생성자

장점

a. 상대적으로 적은 코드량으로 Client에게 더 많은 옵션을 제공한다.

b. 모든 매개변수에 대해서 기본값을 지정 시(제네릭 클래스 제외) new 예약어를 사용할 수 있다.

단점

a. 모든 매개변수에 대해서 기본값을 지정해도 제네식 클래스는 매개변수가 없는 생성자가 필요하다.

b. 같은 옵션영역에서의 생성자들의 코드 결합도가 높아진다.

 

public Class MyClass
{
	private List<int> datas;  
	private string name;
    
	public MyClass() :
		this(0,string.Empty)
	{
	}
    
	public MyClass(int initNumber = 0, string name = "")
	{
		datas = new List<int>();
		this.name = name;
	}
}

 

어떤 방법으로 생성자를 작성할지는 개발자의 자유이다.

유의할점이 있다면 생성자는 합리적인 매개변수를 받아야 하고 예외를 유발해서는 안 된다.

 

마치며 생성자의 호출 순서를 정리하고 끝내겠다.

1. 정적 변수의 저장 공간을 0으로 초기화

2. 정적 변수에 대한 초기화 구문 수행

3. 베이스 클래스의 정적 생성자 수행

4. 정적 생성자 수행

5. 인스턴스 변수의 저장 공간을 0으로 초기화

6. 인스턴스 변수에 대한 초기화 구문 수행

7. 베이스 클래스 생성자 수행

8. 인스턴스 생성자 수행

 

한번 생성이 완료된 시점 부터는 5번 부터 시행 된다. 

 

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

Effective C# ITEM16  (0) 2020.11.05
Effective C# ITEM15  (0) 2020.11.03
Effective C# ITEM13  (0) 2020.10.28
Effective C# ITEM 12  (0) 2020.10.26
Effective C# ITEM11  (0) 2020.10.25

정적 클래스 멤버를 올바르게 초기화하라

 

정적 멤버 변수를 포함하는 Class가 있다면 반드시 인스턴스 생성 전에 정적 멤버 변수를 먼저 초기화해야 한다.

하지만 정적 멤버 변수의 초기화 과정이 복잡하다면 C#에서 지원하는 정적 생성자를 사용하여 생성을 타이밍을 늦추는 방법도 있다.

 

정적 생성자는 해당 Class내의 멤버 변수를 최초로 접근하기 전에 자동으로 호출이 된다.

이 말인즉슨 정적 변수를 초기화하거나, 싱글톤 패턴을 적용하거나, 혹은 여타의 작업을 효율적으로 사용할 수 있다는 뜻인데 Lazy Initialization(늦은 초기화)를 진행하여 사용하기 전까지는 보류하고 있는 기능이다.

 

이전에 포스팅했던 생성자 관련해서도 일반적인 생성자처럼 

 

1. 정적 멤버 초기화

2. BaseClass 정적 생성자 호출

3. 정적 생성자 호출

순서대로 실행이 된다.

 

2가지 예시로 설명을 하겠다.

1. 생성자가 단순하여 정적 생성자를 생략한 싱글톤 패턴

 

public class MySingleton
{
	private static readonly MySingleton _mySingleton = new MySingleton();
    
	public static MySingleton TheOnly
	{
		get
		{
			return _mySingleton;
		}
	}
    
	private MySingleton(){ }
}

 

2. 생성자가 복잡하여(오버헤드가 큰 경우) 정적생성자를 사용한 싱글톤 패턴

 

public class MySingleton
{
	private static readonly MySingleton _mySingleton;
    
	static MySingleton()
	{
		_mySingleton = new MySingleton();
	}
    
	public MySingleton TheOnly
	{
		get
		{
			return _mySingleton;
		}    
	}
    
	private MySingleton(){}    
}

 

정적 생성자에 대해서 정리하자면

1. AppDomain내에서 CLR이 특정 타입에 접근해야 할 경우 정적 생성자를 우선적으로 호출한다.

2. 정적 생성자는 모든 타입에 대해서 하나만 가질 수 있고 어떠한 매개변수도 허용하지 않는다.

3. CLR에서 정적 생성자를 호출할때 예외가 발생한 경우 프로그램을 언로드 하지 않는 한 정적생성자는 다시 호출되지     않는다. 따라서 개발자는 정적 생성자를 코딩한 경우 신중하게 다뤄야 한다.

4. 정적생성자를 사용해야 하는 경우에는 try/catch문으로 신중하게 다뤄야 한다.

예시 코드

static MySingleton()
{
	try
	{
		_mySingleton = new MySingleton()
	}
	catch
	{
		//복구 시도
	}
}

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

Effective C# ITEM15  (0) 2020.11.03
Effective C# ITEM14  (0) 2020.10.28
Effective C# ITEM 12  (0) 2020.10.26
Effective C# ITEM11  (0) 2020.10.25
Effective C# ITEM10  (0) 2020.10.21

할당 구문보다 멤버 초기화 구문이 좋다

 

간단하게 결론부터 말하겠다.

 

1. Class 생성자에서 받아오는 매개변수로 값이 바뀌는 멤버 변수는 생성자에서 초기화

2. Class 생성자에서 받아오는 매개변수와 무관하게 값이 같은 멤버변수는 선언과 초기화를 동시에 하면 된다.

enum ESex
{
   Male = 0,
   Female,
}

Class Student
{
 	//선언과 동시에 초기화
 	List<string> _subject = new List<string>(){"Math","Science","Korean"};
    
 	//사용자가 설정해야 알 수 있는 값
 	uint _grade;
 	ESex _sex;
    
 	public Student(uint grade, ESex sex)
 	{
 	this._grade = grade;
 	this._sex = sex;
 	}
}

 

위의 예시가 끝이다.

위의 Class에 대해서 사용자가 Student라는 Class를 생성 했을때 Compile에서는

 

선언과 동시에 초기화한 멤버변수를 생성자 전에 먼저 실행된다.

_subject -> _grade -> _sex 순서대로 초기화가 진행된다는 뜻이다.

그리고 만약 어떤 SuperClass를 상속할 경우에는 SuperClass의 생성자가 호출되기 전에 _subject멤버 변수는 먼저 초기화가 진행된다.

 

유의할 점이 하나 더 있다.

int, float, double 등 (값 타입, struct 타입)은 0, Class 등 참조 타입은 Null로 저수준에서 직접 CPU 명령을 수행하여 메모리 블록을 0 또는 Null으로 설정하기 때문에 만약 0 또는 Null로 초기화하는 멤버 변수라면 굳이 다시 실행시키는 짓은 피하길 바란다. 

 

int value1 = 0; //CPU에서 0으로 메모리블록을 지정했지만 명령어에서 다시 0으로 지정 
int value2;     //CPU에서 0으로 메모리블록 지정

 

 

 

 

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

Effective C# ITEM14  (0) 2020.10.28
Effective C# ITEM13  (0) 2020.10.28
Effective C# ITEM11  (0) 2020.10.25
Effective C# ITEM10  (0) 2020.10.21
Effective C# ITEM9  (0) 2020.10.21

+ Recent posts