. 박싱     : 값 타입을 참조 타입으로 변경하며, 이 과정에서 새롭게 생성된 참조 타입의 객체는 힙에 생성된다.

. 언 박싱 : 박싱되어 있는 참조타입의 객체로부터 값 타입 객체의 복사본을 가저온다. 스택에 생성된다. 

 

박싱과 언박싱은 System.Object 타입이나 인터페이스 타입이 필요한 곳에서 값 타입을 사용하기 위해 반드시 필요한 메커니즘입니다. 하지만, 박싱과 언박싱이 수행되는 과정에서 생성되는 임시 객체로 예상치 못한 버그가 발생하기도 합니다. 

 

또한, MSDN에 나오는 박싱과 언박싱의 성능에 대한 설명은 아래와 같습니다. 

'값 형식이 박싱되면 완전히 새로운 개체가 생성되어야 합니다. 이 작업은 단순 참조 할당보다 20배나 오래 걸립니다. 언박싱 시 캐스팅 프로세스는 할당의 4배에 달하는 시간이 소요될 수 있습니다.'

박싱과 언박싱은 메모리와 속도 성능에도 좋지 않은 영향을 미치기 때문에 왠만하면 피하는 것이 좋습니다. 

 

int firstNumber = 10;
int SecondNumber = 15;
float ThirdNumber = 1.5;

Console.WriteLine($"A few numbers : {FirstNumber}, {SecondNumber}, {ThirdNumber}");

위의 단순한 코드에서 박싱의 예를 쉽게 찾아볼 수 있습니다. 

 

보간 문자열을 만드는 작업은 System.Object 객체에 대한 배열을 사용합니다. 때문에 FirstNumber가 값 타입이라면 System.Object 타입으로 변환하기 위해 아래와 같은 박싱 과정을 수행합니다.

int i = 25;
object o = i; // 박싱
o.ToString();

따라서, 값 타입의 객체를 직접 전달하지 말고 문자열 인스턴스를 전달하여 박싱을 피하는 것이 좋습니다.

 

 

. 컬렉션 사용을 피하고, 제네릭 컬렉션을 사용하라.

 

컬렉션은 C++의 STL처럼 C#에서 지원하는 자료구조입니다.

.NET 1.X의 컬렉션은 System.Object 타입의 객체에 대한 참조를 저장하도록 구현되어 있어 박싱/언박싱이 일어납니다.

이러한 문제에 대한 해결책으로 .NET 2.0에 제네릭 컬렉션이 나왔습니다. 박싱/언박싱이 일어나지 않도록 개체의 타입을 지정하여 사용하도록 하고 있습니다.

namespace Collection
{
    class MainClass
    {
        static void Main(string[] args)
        {
            // 비 제네릭 컬렉션
            ArrayList arrayList = new ArrayList();
            arrayList.Add(1);
            arrayList.Add(2);
            arrayList.Add(3);
            Console.WriteLine("비 제네릭(ArrayList)");
            foreach(var item in arrayList)
            {
                // var item == System.Object item

                int i = (int)item; // 언박싱
                Console.WriteLine(i);
            }
            // 제네릭 컬렉션
            List<int> list = new List<int>();
            list.Add(1);
            list.Add(2);
            list.Add(3);
            Console.WriteLine("제네릭(List)");
            foreach(var item in list)
            {
                // var item == Int32 item

                Console.WriteLine(item);
            }
        }
    }
}

 

 

참 고 : Effective C#  

        : https://076923.github.io/posts/C-9/

 

C# 강좌 : 제 9강 - 데이터 형식

C# Data Type

076923.github.io

      http://www.mkexdev.net/Article/Content.aspx?parentCategoryID=1&categoryID=5&ID=670

 

MKEX Dev .NET

Microsoft. NET 을 시작하는 분들을 위한 강좌입니다. 주로 기초적인 내용과 때론 기본적인 내용을 다룹니다 [C# 기초강좌] 6. C# 자료형 작성자 : 박종명 최초 작성일 : 2010-07-02 (금요일) 최종 수정일 : 2010-07-02 (금요일) 조회 수 : 5993 회 “C#은 자료는 값 타입과 참조타입으로 나누어 집니다” 안녕하세요. 박종명입니다. 닷넷 여섯 번째 강좌를 진행하도록 하겠습니다 강좌가 좀 늦어졌네요. 개인적 사유로 조금

www.mkexdev.net

 

이벤트를 발생하는 작업은 그리 어려운 부분이 없어 보이지만, 이벤트를 호출하는 경우에도 다양한 문제가 발생합니다.  

namespace Delegate
{
    class EventSource
    {
        private EventHandler<int> Updated;
        public void RaiseUpdates()
        {
            Counter++;
            Updated(this, Counter);
        }
        private int Counter;
    }
}

우선, 위의 코드에서 Updated에 결합된 이벤트 핸들러가 없다면, NullReferenceException이 발생합니다. 이벤트 핸들러가 결합돼 있지 않다면 null 값을 가지기 때문입니다. 따라서 이벤트를 발생시키기 이전에 이벤트 핸들러가 유효한지 확인해야 합니다.

 

public void RaiseUpdates()
{
    Counter++;
    if (null != Updated)
        Updated(this, Counter);
}

이벤트 핸들러가 결합되어 있는지 확인 후, 호출하도록 위와 같이 수정하였습니다. 이렇게 수정하면 대부분의 경우 잘 동작하지만 여전히 문제는 있습니다.

if문을 호출하여 Updated 이벤트가 null이 아님을 확인했지만, 이벤트를 발생시키는 코드를 수행하기 직전에 다른 스레드가 이벤트 핸들러의 등록을 취소하게 된다면.... 다시 NullReferenceException 예외가 발생하게 됩니다.

 

흔히 나타나지 않는 문제이지만, 증상을 재현하기도 쉽지 않고 이런 버그는 분석도 어렵고 고치기도 까다롭습니다. 코드 상의 문제가 쉽게 찾아지지 않기 때문입니다.

 

public void RaiseUpdates()
{
    Counter++;
    var handler = Updated;
    if (null != handler)
        handler(this, Counter);
}

위 예제가 .NET과 C#을 이용하여 안전하게 이벤트를 발생시키는 권장 코드입니다.

 

이벤트 핸들러를 새로운 지역 변수에 할당하면, 이 지역변수는 원래 이벤트에서 이벤트 핸들러의 얕은 복사본을 생성합니다.

멀티캐스트 델리게이트를 포함할 수 있고, 내부적으로 원래 이벤트의 이벤트 핸들러 목록을 그대로 가지고 있습니다.

 

만약 다른 스레드가 이벤트에 대한 구독을 취소하더라도 기존 객체에 포함된 이벤트 필드의 내용은 수정되지만, 복사된 지역 변수의 내용은 변하지 않습니다.

 

 

위의 코드를 null 조건 연산자를 사용하면 보다 더 간단한 작성도 가능해집니다.

public void RaiseUpdates()
{
    Counter++;
    Updated?.Invoke(this, Counter);
}

null 조건 연산자(?.)는 왼쪽을 평가하여 이 값이 null이 아닌 경우에만 연산자 오른쪽의 표현식을 실행시킵니다. 만약 연산자 왼쪽이 null일 경우, 예외도 발생하지 않으면 아무 작업 없이 다음 단락으로 이동합니다. 

연산자의 왼쪽을 평가하고 메서드를 수행하는 과정이 원자적으로(한 번에) 수행된다는 것입니다.

 

위와 같이 이벤트 호출에 null 조건 연산자를 사용하면 멀티스레드 환경에서도 안전할 뿐 아니라 훨씬 간결하게 작성할 수 있다.

 

 

null 조건 연산자 : https://loveme-do.tistory.com/7

 

[C#] ?. 연산자 (null 조건부 연산자)

?. 연산자 (null 조건부 연산자) null 조건 연산자는 ? 앞에 있는 객체가 NULL인지 체크해서 1. NULL 이라면, NULL을 리턴. 2. NULL이 아니라면, ? 다음의 속성이나 메서드를 실행. 일반적으로 ?. 와 같이 표현되..

loveme-do.tistory.com

참 고 : Effective C#  

# delegate의 장점!

: 타입 안정적인 콜백 정의할 있다
: 여러 클래스가 상호 통신을 수행해야 클래스 간의 결합도를 낮추고 싶다면 인터페이스보다 델리게이트를 사용하는 것이 좋다
: 런타임에 통지 대상을 설정 있고, 다수의 클라이언트에게 통지 보낼 수도 있다
: 하나의 델리게이트는 여러 메서드에 대한 참조를 포함 있기 때문이다


# 자주 사용되는 delegate

.NET Framework 라이브러리는 Predicate<T>, Action<>, Func<> 같은 형태로, 자주 사용되는 델리게이트를 정의해두고 있다.
 
. Predicate<T>조건을 검사하여 bool값을 반환하는 델리게이트.
(Func<T, bool> == Predicate<T> 동일하다고 있다.)
. Func<> : 여러 개의 매개변수를 받아 단일의 결과값을 반환하는 델리게이트.
. Action<> : 여러 개의 매개변수를 받지만 반환 타입이 void인 델리게이트.


# 멀티캐스트

모든 델리게이트는 기본적으로 멀티캐스트가 가능하다.
일반적으로 동일한 타입의 매개변수를 취하더라도 반환 타입이 다른 경우 서로 다른 델리게이트 타입으로 간주하며,


컴파일러는 사이의 형변환을 허용하지 않는다.

멀티캐스트 델리게이트는 한 번만 호출하면, 델리게이트 객체에 추가된 모든 대상 함수가 호출된다.

. 주의 1 : 예외 안전성이 좋지 않다.

멀티캐스트 델리게이트의 내부 동작 방식은 대상 함수들을 연속적으로 호출하는 형태로 구현된다.
델리게이트는 어떤 예외도 잡지 않으며, 따라서 예외가 발생하면 함수 호출 과정이 중단된다. 

. 주의 2 : 마지막 호출 대상 함수의 반환값만이 델리게이트의 반환값이다.

1
2
3
4
5
6
7
8
9
10
11
12
List<ComplicatedClass> container = new List<ComplicatedClass>();
 
public void LengthyOperation(Func<bool> pred)
{
foreach(ComplicatedClass cl in container)
{
cl.DoLengthyOperation();
 
if (false == pred()) // 사용자가 임의로 중단을 요청했는지 확인
return;
}
}
 cs


위 메서드를 멀티캐스트 델리게이트로 사용하면 문제가 발생한다.


1
2
3
4
5
Func<bool> cp = () => CheckWithUser();
 
cp += () => CheckWithSystem();
 
c.LengthyOperation(cp);
cs

델리게이트의 반환값은 체인 마지막으로 호출된 함수의 반환값이 되며, 다른 반환값은 모두 무시된다.

따라서 위의 예제의 경우 CheckWithUser( )의 반환값은 무시된다. 


이러한, 문제를 해결하려면, 델리게이트에 포함된 호출 대상 콜백 함수를 직접 다뤄야한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void LengtyOperation2(Func<bool> pred)
{
    bool bContinue = true;
    
    foreach(ComplicatedClass cl in bContinue)
    {
        cl.DoLengthyOperation();
        foreach (Func<bool> pr in pred.GetInvocationList())
            bContinue &= pr();
        if (!bContinue)
            return;
    }
cs


위처럼 코드를 작성하면 델리게이트에 추가된 개별 메서드가 ture를 반환하는 경우에만 다음 메서드에 대한 호출을 이어갈 수 있다.



delegate 관련 글 : https://loveme-do.tistory.com/13

참 고 : Effective C# 

+ Recent posts