# 멤버 초기화 구문이 좋은 이유 

 

. 생성자가 여러 개일 때도 멤버 변수 초기화를 누락하지 않도록 미연에 방지할 수 있다.

  (다만, 모든 생성자가 동일한 방법으로 멤버 변수를 초기화하는 경우에 한해서 사용해야 한다. )

 

. 가독성이 좋고, 유지보수도 용이하다. 

 

 

 

 

# 초기화 구문의 특성

 

. 컴파일러는 모든 생성자(생성자가 없는 경우에 생성하는 기본생성자 포함)의 시작 부분에 멤버 초기화 구문을 포함시키기 때문에 새로운 생성자를 추가하더라도 항상 멤버 초기화 구문이 포함된다.

 

. 또한, 생성하려는 타입이 클래스를 상속하고 있는 경우 베이스 클래스의 생성자가 호출되기 전에 멤버에 대한 초기화가 이루어진다.

 

. 멤버 변수의 초기화 순서는 변수의 선언 순서이다.

 

 

 

 

# 사용 방법

 

  변수를 선언하는 곳에 변수를 초기화 하도록 작성하기만하면 된다.

public class MyClass
{
    //컬렉션 선언과 동시에 초기화
    private List<string> labels = new List<string>();
}

 

 

 

 

# 주의해야할 경우

 

1. 객체를 0이나 null로 초기화하는 경우.

 

  기본 시스템 초기화 루틴은 코드를 실행하기 전에 모든 값을 0으로 설정한다.

그런데 객체를 0이나 null로 초기화 하게되면, C#컴파일러는 개의치 않고 코드를 생성하기 때문에 괜한 일을 추가적으로 하는 꼴이 된다.

public struct MyValType
{
    // 생략
}

MyValType myVal1; // 0으로 초기화
MyValtype myVal2 = new MyValType(); // 반복해서 0으로 초기화

  myVal2같은 경우 initobj라는 IL 명령을 사용하는데 박싱/언박싱 된 myVal2 변수 모두에 대해서 0으로 초기화 하는 과정이 수행된다.

이 과정으로 인해 약간의 추가 시간이 소요된다.

 

 

2. 동일한 객체를 반복해서 초기화하는 경우.

public class MyClass2
{
    private List<string> labels = new List<string>();

    MyClass2() {    }

    MyClass2(int size) 
    {
        labels = new List<string>(size);
    }
}

  위와 같이 List 객체를 생성하는 방식이 다양하게 혼재하는 경우 멤버 초기화 구문을 사용하지 않는 것이 좋다.

MyClass2를 생성할 때 컬렉션의 크기를 지정하게 되면 실제로 2개의 List<> 객체가 생성되며 그 중 하나는 즉각 가비지가 된다.

  멤버 초기화 구문은 생성자의 본문보다 앞서 수행되므로 생성자 본문에서 생성한 객체만 살아남는다.

 

 

3. 예외처리가 반드시 필요한 경우.

 

  멤버 초기화 구문은 try로 감쌀 수 없기 때문에 초기화 과정에서 예외가 발생하면 예외가 외부로 전파된다. 따라서 클래스 내부에서 복구를 시도할 수가 없다.

  예외처리가 필요하다면 반드시 생성자 내부에서 처리해야만 한다.

 

 

# 가비지 수집기

 

  가비지 수집기는 관리되는 메모리를 관장하며 네이티브 환경과는 다르게 메모리 누수, 댕글링 포인터, 초기화되지 않는 포인터, 여타의 메모리 관리 문제를 개발자들이 직접 다루지 않도록 자동화해줍니다.

 

  개발자가 직접 메모리를 관리하는 경우에 발생할 수 있는 해제 순서의 선후 관계에 대한 문제나 순환 참조의 문제, 여러 객체들 간의 복잡한 연관 관계의 문제를 가비지 수집기는 알아서 처리해줍니다.

 

  가비지 수집기는 COM처럼 개별 객체가 스스로 자신의 참조 여부나 횟수 등을 관리하도록 하지 않고, 응용프로그램의  최상위 객체로부터 개별 객체까지의 도달 가능 여부를 확인하도록 설계되어 있습니다. 응용프로그램 내의 최상위 객체로부터 참조 트리를 구성하여 도달 가능한 객체를 살아 있는 객체로 판단하고 도달 불가능한 객체를 가비지로 간주합니다.

 

 

 

 

# 리소스 관리에 대한 이해의 필요성

 

1. 가비지 수집기가 관리해주지 않는 비관리 리소스는 개발자가 직접 관리해야 합니다. 

...더보기

관리 리소스 - CLR에 있는 자원들.

비관리 리소스 - 닷넷에 관리 자원이 아닌 자원. OS 자원.

(데이터베이스 연결, GDI+ 객체, COM 객체, 시스템 객체 등)

 

2. 이벤트 핸들러나 델리게이트 등도 잘못 사용하면 불필요하게 메모리에 오래 남게 됩니다.

 

때문에, 관리되는 리소스를 이해하고 올바른 해제 작업을 수행한다면 더욱 효과적인 가비지 수집이 가능해집니다.

 

 

 

 

# 비관리 리소스 해제

 

  비관리 리소스의 생명주기에 대해 개발자가 더 손쉽게 관리할 수 있도록 finalizer와 IDisposable 인터페이스라는 두 가지 메커니즘을 제공합니다. 하지만, finalizer는 단점이 많기 때문에 IDisposable 인터페이스를 통해 리소스 관리할 것을 더욱 추천합니다. 

 

- finalizer

  비관리 리소스에 대한 해제 작업이 반드시 수행되도록 도와주는 방어적인 매커니즘입니다.

finalizer() 메서드는 직접 오버라이드하여 사용할 수 없고, 소멸자를 만듬으로써 오버라이드 됩니다. 소멸자가 만들어 지면 메모리 해제 작업이 필요하다는 것을 명시하게 됩니다. 개발자가 직접 관리하다보면 메모리 해제 작업을 까먹는 경우도 발생할 수 있는데, 이러한 경우에도 최소한 소멸자에서 호출할 수 있도록 하고 있습니다.

 

 

 

 

# finalizer가 성능에 미치는 영향

 

1. finalizer를 포함하고 있는 객체를 사용하면 가비지 수집 과정이 길어져, 비교적 오랫동안 관리되는 힙에 남아 있게 됩니다.

 

  finalizer는 소멸자를 통해서 호출된다고 앞서 설명하였습니다.

아래와 같이 C++에서 대중적으로 사용되는 리소스 해제 구문이 C#에서는 동일한 방식으로 동작하지 않습니다.

class CriticalSection
{
    public CriticalSection()
        {
            EnterCriticalSection();
        }
        
        //finalizer 오버라이드
        ~CriticalSection()
        {
            ExitCriticalSection();
        }
}

가비지 수집기가 finalizer를 포함하는 객체를 가비지로 판단한 경우, 그 즉시 객체가 점유하고 있는 메모리 공간을 해제하지 못합니다. finalizer를 호출해야 하는데 가비지 수집을 수행하는 스레드를 통해 직접 finalizer를 호출할 수 없습니다.

때문에, 가비지 수집기는 이 객체에 대한 참조를 다른 큐에 삽입하여 나중에 finalizer가 호출될 수 있도록 사전 준비만을 수행합니다.

 

이 과정에서

- finalizer가 없는 객체는 메모리로부터 즉각 제거되지만,

- finalizer가 있는 객체는 이후 다시 가지비 수집 절차가 진행될 때 다른 큐에 삽입해 두었던 객체의 참조를 꺼내어 finalizer를 순차적으로 호출하여 메모리를 반납합니다.

 

이러한 이유로 finalizer를 포함한 객체들은 가비지 수집 과정이 더욱 길어집니다.

 

가비지 수집과정에 세대 개념에 따라 finalizer의 비용에 대해 생각해보면,

...더보기

[세대 개념]

0세대 : 가비지 수집 이후 임의의 생성된 객체.

1세대 : 다시 가비지 수집 절차가 수행되고 이후에도 살아있는 0세대 객체가 1세대 객체가 된다.

2세대 : 두 번 혹은 그 이상의 가비지 수집 절차가 수행됐음에도 여전히 사용되고 있는 객체.

 

[ 가비지 수집 과정 ]

가비지 수집 절차가 시작되면

1. 우선 0세대 객체에 대해서만 가비지 수집을 수행합니다.

2. 대략 10번에 한 번 꼴로 추가적으로 1세대 객체에 대해서 가비지 수집을 수행합니다.

3. 그리고 약 100번에 한번 꼴2세대 객체를 포함한 모든 세대의 객체를 대상으로 가비지 수집을 수행합니다.

 

  finalizer를 가진 객체는 즉각 제거되지 못하므로 1세대 객체가 되고, 이 경우 9번의 가비지 수집 절차가 추가적으로 수행된 이후에 비로소 메모리에서 제거될 가능성이 있습니다. 만약 이 과정에서도 정리되지 못하고 2세대 객체가 되면 약 100번의 가비지 수집 절차가 추가로 수행된 후 삭제될 것입니다.

 

 

2. finalizer 메서드를 호출하는 스레드가 finalzier 스레드이기 때문에 특정 쓰레드에 선호도(affinity)를 갖는 객체일 경우 문제를 발생할 수 있다.

 

  예를 들어, finalizer 메서드 내에서 잠금을 해제하려는 시도는 매우 위험합니다. 잠금을 잠근 스레드는 어플리케이션의 쓰레드이지만 잠금을 해제하려는 쓰레드는 finalizer 쓰레드이기 때문입니다. 

 

 

3. finalizer 메서드가 호출되는 순서를 보장할 수 없습니다. 

 

아래의 코드는 ObjectDisposedException 예외를 유발합니다. 

class DangerousType
{
    private StreamWriter _stream;
    public DangerousType()
    {
        _stream = new StreamWriter("Test.txt");
    }
    ~DangerousType()
    {
        _stream.Close();    // 오류가 발생할 수도 있다!
    }
    public void DoSomething()
    {
        //...... 생략 ......
    }
}

DangerousType 객체는 멤버로 StreamWriter 객체를 참조하고 있고, StreamWriter 클래스 또한 finalzier 메서드를 정의하고 있다고 가정한다. 그러면 DangerousType 객체가 가비지 컬렉션의 대상이 되는 경우 두 객체가 모두 finalizer 큐에 삽입될 것입니다. 그리고 finalizer 쓰레드가 StreamWriter 객체에 대해 먼저 Finalizer 메서드를 호출한다면 StreamWriter 객체는 메모리를 반환하고, 그 이후 DangerousType의 Finalizer 메서드를 호출하면 이미 닫히 파일에 대해 또 다시 Close() 메서드가 호출되기 때문에 예외가 발생하게 됩니다.

 

 

리소스를 해제하는 가장 좋은 방법으로는, finalizer 사용을 지양하고 IDisposable 인터페이스와 표준 Dispose 패턴을 활용하는 것입니다.

 

 

해당 메모리 사용에 관한 내용은 이후 Item 12, Item 13, Item 14, Item 15, Item 16, Item 17에서 더 다루고 있습니다.

 

 

참  고 :  http://www.simpleisbest.net/post/2011/05/12/Finalizer-Usage-Pattern.aspx

베이스 클래스에서 virtual로 선언하지 않은 멤버를 재정의하려는 경우 new 한정자를 사용 할 수 있습니다. 

하지만 사용할 수 있다고 해서 new 한정자는 비가상 메서드를 가상 메서드로 만드는 것이 아니라 클래스이 명명 범위 내에 새로운 메서드를 추가하는 역활을 수행합니다.

 

# override와 new 한정자의 차이

ovrerride로 정의된 가상 메서드를 사용하는 경우는 다음과 같은 결과를 가집니다.

public class BaseClass
{
    public virtual void Func()
    {
        Console.WriteLine("MyClass");
    }
}
public class DerivedClass : BaseClass
{
    public override void Func()
    {
        Console.WriteLine("DerivedClass");
    }
}

// override를 사용한 가상 메서드

BaseClass cl = new BaseClass();
BaseClass cl2 = new DerivedClass();

cl.Func();  // MyClass 출력
cl2.Func(); // DerivedClass 출력

new 한정자를 이용하는 경우 다음과 같은 결과를 가집니다

public class BaseClass
{
    public void Func()
    {
        Console.WriteLine("MyClass");
    }
}
public class DerivedClass : BaseClass
{
    new public void Func()
    {
        Console.WriteLine("DerivedClass");
    }
}

// new 한정자를 사용한 비가상 메서드

BaseClass cl = new BaseClass();
BaseClass cl2 = new DerivedClass();

cl.Func();  // MyClass 출력
cl2.Func(); // DerivedClass 출력

DerivedClass dcl = cl2 as DerivedClass;

dcl.Func(); // DerivedClass 출력

 

비가상 메서드는 정적으로 바인딩되므로 런타임에서 파생 클래스에서 새롭게 정의하고 있는 메서드가 있는지 찾지 않습니다

반면, 가상 메서드는 동적으로 바인딩 되므로 런타임에 객체의 타입이 무엇이냐에 따라 그에 부합하는 메서드를 호출합니다.

 

가상메서드와 비가상 메서드를 재정의하는 new 한정자의 기능을 제대로 파악하고 사용해야 하며, new 한정자를 사용하지 않기 위해 모든 메서드를 가상메서드로 변경하는 것은 좋지 않습니다.

 

설계자가 특정 메서드를 가상으로 선언한다는 것은 파생 클래스에서 이 가상 메서드의 구현부를 변경할 것임을 예상하고 있으며, 파생 클래스에서 가상 메서드의 동작 방식을 변경할지라도 아무런 문제 없이 수행될 것임을 보장하는 것입니다. 따라서 어떤 메서드나 속성이 무엇인지를 우선 생각해보고 반드시 다형성이 필요한 경우에만 가상 메서드를 사용해야 합니다.

 

# new 한정자를 활용하는 경우

new 한정자를 활용해도 좋은 경우는 베이스 클래스에서 이미 사용하고 있는 메서드를 재정의하여 완전히 새로운 베이스 클래스를 만들어야 하는 경우 정도입니다.

이미 널리 사용되고 있는 메서드가 있어서 이를 사용하는 코드를 일일이 찾아서 수정하기 어렵거나, 외부 어셈블리에서 이 메서드를 사용하고 있어서 코드를 수정할 수 없는 경우라면 new 한정자를 사용해 볼 만합니다.

public class MyWidget : BaseWidget
{
    public void NormalizeValues()
    {
        // 세부 내용
    }
}

예를 들어, BaseWidget이라는 클래스를 상속하여 다음과 같이 MyWidget을 정의한 경우가 있고, 많은 사람들이 MyWidget 클래스를 사용 중인 상황에서 BaseWidget에 새로운 버전이 출시되었습니다.

이 때, BaseWidget에 NormalizeValues()라는 새로운 메서드가 추가된다면 빌드가 실패되는 결과가 발생할 것입니다.

 

이 문제를 해결하기 위해서 두가지 방법이 있다. MyWidget의 메서드 이름을 변경하거나, new 한정자를 사용하는 방법이 있습니다.

 

앞에서와 같은 상황에서 new 한정자를 이용하면 기존 메서드명을 그대로 사용할 수 있습니다. 하지만, new 한정자를 사용할 때는 각별히 주의해야 합니다.

코드를 모두 수정할 수 있다면 장기적으로 봤을 때는 메서드의 이름을 변경하는 것이 좋습니다. 왜냐하면, BaseWidget의 NormalizeValues()메서드와 같은 이름으로 다르게 동작하는 메서드 존재로 혼동이 올 수도 있기 때문입니다.

여러 상황을 고려하여 신중하게 new 한정자를 사용하는 것을 권합니다.

 

참 고 : Effective C#  

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

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

 

박싱과 언박싱은 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# 


# 델리게이트(Delegate)

 해석하면 '대리인'이라는 뜻의 델리게이트는 C++의 포인터처럼 메서드를 안전하게 캡슐화하는 형식입니다.
즉, 메소드를 대신해서 호출하는 역활을 하는 메소드를 참조하는 변수입니다.
특정 메소드를 처리할 때 그 메소드를 직접 호출해서 실행시켜야 했지만, 델리게이트를 사용하면 그 메소드를 대신하여 호출할 수 있습니다.

# 사용방법
(1) 델리게이트 타입 선언.
    메소드 타입은 매개변수와 반환타입에 의해 결정됩니다.
    델리게이트 타입도 그 메소드와 동일한 매개변수, 반환 타입으로 선언해주면 됩니다. 
(2) 델리게이트 인스턴스화하여 메서드 전달.

(3) 델리게이트 호출.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace DelegateTest
{
    class Program
    {
        delegate void Del(string message);
        public static void DelegateMethod(string message)
        {
            System.Console.WriteLine(message);
        }
        static void Main(string[] args)
        {
            // 명명된 메서드 전달.
            Del handler = DelegateMethod;
            // 무명 메서드 전달.
            Del handler2 = delegate (string message)
                            { System.Console.WriteLine(message); };
            // 람다식 활용.
            Del handler3 = (message) => System.Console.WriteLine(message);
            handler("Hello World");
            handler2("Bye World");
        }
    }
}
cs

# 콜백 메서드
인스턴스화 된 델리게이트는 매개 변수로 전달하거나 속성에 할당할 수 있습니다.
따라서, 메서드가 델리게이트를 매개 변수로 허용하여 매개 변수로 받은 델리게이트를 나중에 호출할 수 있습니다. 

이러한 방식을 비동기 콜백이라고 부르며, 콜백 메서드를 구현할 때, 델리게이트의 가치를 볼 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace Calculater
{
    delegate int Calc(int a, int b);
 
    class MainApp
    {
        public static void Calculator(int a, int b, Calc cb)
        {
            Console.WriteLine(cb(a, b));
        }
 
        public static int Plus(int a, int b) { return a + b; }
        public static int Minus(int a, int b) { return a - b; }
 
        static void Main(string[] args)
        {
            Calc plusCalc = new Calc(Plus);
            Calc MinusCalc = new Calc(Minus);
 
            Calculator(1122, plusCalc);  // 33 출력
            Calculator(3322, MinusCalc); // 11 출력
        }
    }
}
cs



# 델리게이트 체인

델리게이트는 둘 이상의 메서드를 호출할 수 있습니다. 이러한 호출을 멀티캐스트라고 합니다.

델리게이트에 메서드 목록을 추가하려는 경우, 더하기 또는 더하기 대입 연산자('+' 또는 '+=')를 사용합니다. 

메서드 제거도 가능한데, 제거하는 경우엔 감소 또는 감소 대입 연산자('-' 또는 '-=')를 사용하면 됩니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
namespace Multicast
{
    delegate void PrintText();
 
    class MainApp
    {
        public static void First() { Console.Write("첫번째 "); }
        public static void Second() { Console.Write("두번째 "); }
        public static void Third() { Console.Write("세번째 "); }
 
        static void Main(string[] args)
        {
            PrintText MultiHandler;
 
            MultiHandler = new PrintText(First);
            MultiHandler += Second;
            MultiHandler += Third;
 
            Console.WriteLine("메서드 추가");
            MultiHandler();
 
            MultiHandler -= First;
            MultiHandler -= Third;
 
            Console.WriteLine("\n\n메서드 제거");
            MultiHandler();
        }
    }
}
cs



    [ 콘솔 출력 결과 ]


    메서드 추가

    첫번째 두번째 세번째


    메서드 제거

    두번째





delegate 관련 Effective C# : https://loveme-do.tistory.com/14


참   고 : https://mrw0119.tistory.com/19?category=585887

           https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/delegates/using-delegates

nullable 형식 한정자




nullable 형식은 기본 값 형식의 모든 값과 추가로 NULL 값을 나타내는 형식입니다. 


Nullable<T> 또는 T? 의 형태로 사용할 수 있으며, struct와 임의의 값 형식이 될 수 있지만 참조 형식은 될 수 없습니다.


1
2
3
4
5
6
7
int a = null;
// 컴파일 에러
// int는 null을 허용하지 않는 형식.
 
int? b = null;
 
int result = b ?? 0;
cs


??(null 병합 연산자)나 ?.(null 조건부 연사자)와 같이 C#에서 null 값을 체크해주는 연산자들이 있습니다. 


이런 연산자를 사용하다 보면 null 값을 허용하지 않는 형식에도 사용이 필요한 경우가 있습니다.


이 때, nullable 형식 한정자를 사용하게 되면 null 값과 함께 내부 형식 값도 모두 나타낼 수 있습니다.



. null 병합 연산자 : https://loveme-do.tistory.com/8

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






참   고 : https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/nullable-types/using-nullable-types

?? 연산자 (null 병합 연산자)



 
?? 연산자는 피연산자가


1. NULL일 경우, 오른쪽 피연산자를 반환.

2. NULL이 아닐 경우, 왼쪽 피연산자를 반환.


1
2
3
int? a = null;
 
int cnt = a ?? 10;
cs


위와 같을 때,

anull 이라면, cnt = 10;

anull 이 아니라면, cnt = a; 의 값이 대입되게 됩니다. 


객체가 null인 경우에 대한 디폴트 값을 설정해 주거나, 이러한 상황을 안전하게 처리할 수 있습니다. 





참  고 : https://docs.microsoft.com/ko-kr/dotnet/csharp/language-reference/operators/null-coalescing-operator

?. 연산자 (null 조건부 연산자)



 null 조건 연산자는 ? 앞에 있는 객체가 NULL인지 체크해서


1. NULL 이라면, NULL을 리턴.

2. NULL이 아니라면, ? 다음의 속성이나 메서드를 실행.


일반적으로 ?. 와 같이 표현되지만, 만약 인덱서 혹은 배열 요소에 적근할 경우는 ?[]와 같이 표현된다.


1
2
3
4
5
6
7
8
9

// customer 컬렉션이 null 이면 cnt는 null
// null이 아니라면, cnt는 customer의 갯수.
int? cnt = customer?.Count;
 
// customer가 null인지 체크하고
// 다시 customer[0]가 null인지 체크한다.
// customer이 null이거나 customer[0]이 null이라면 age는 null
// 그렇지 않다면, age는 customer[0].age
int? age = customer?[0]?.age;

cs



null 조건부 연산자를 사용하면 null 참조될 수 있는 객체에 안전하게 접근할 수 있으며,

null에 대한 예외 처리를 축약해서 개발자의 의도를 표현할 수 있다.






참  고: http://www.csharpstudy.com/CS6/CSharp-null-conditional-operator.aspx


+ Recent posts