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

 

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

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

 

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

 

 

 

 

# 초기화 구문의 특성

 

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

 

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

 

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

 

 

 

 

# 사용 방법

 

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

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# 

문화권별로 다른 문자열을 생성하려면 FormattableString을 사용하라.



[ Content ]

# FormattableString
    . C# 6.0에서 추가된 클래스입니다.

    . 문자열 보간 기능의 결과로 생성되는 반환값으로 문자열일 수도 있지만, FormattableString을 상속한 타입일 수도 있습니다.

    . FormattableString은 문자열의 조립을 돕는 기능이 있어, 문화권과 언어를 지정하여 문자열을 생성하는데 활용할 수 있습니다.


1
2
3
4
5
6
7
 
String s = $ "It's the {DateTime.Now.Day} of the {DateTime.Now.Month} month";
 
var vs = $"It's the {DateTime.Now.Day} of the {DateTime.Now.Month} month"
 
FormattableString fs = $"It's the {DateTime.Now.Day} of the {DateTime.Now.Month} month";
 
cs


    . var로 선언하면 위 변수 vs는 string 객체가 될 수도 있겠지만, FormattableString을 상속한 타입의 객체가 될 수도 있습니다.


# 활용


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;
using System.Globalization;
 
namespace GlobalString
{
    class Program
    {
        static void Main(string[] args)
        {
            FormattableString fs = $"{Math.PI}";
            Console.WriteLine(fs);
            Console.WriteLine(ToGerman(fs));
        }
 
        public static string ToGerman(FormattableString src)
        {
            return string.Format(
                CultureInfo.CreateSpecificCulture("de-De"),
                src.Format,
                src.GetArgument(0));
        }
    }
}
cs


    : 위 예제의 결과는 다음과 같이 출력됩니다. 


3.1415

3,1415


     미국식  표기법인 '.'이 변환되어 ','로 출력되는 것을 볼 수 있습니다.



[ Digression ]

 

  사실 큰 필요성을 느끼진 못했습니다. 실제로는 로컬라이징을 위해 문자를 그대로 사용하기보다는 키 값을 이용하여 국가에 맞게 대응하는 데이터를 뿌려주도록 작업합니다. 그러나 대응 영역은 어디까지나 문자에 대한 대응이고, 위의 예제와 같이 기호에 대한 언어 문화가 적용될 필요가 있다면 유용할 것 같습니다. 




참 고 : Effective C# 

    https://ryo511.info/archives/3981

String.Format()을 보간 문자열로 대체하라.



[ Content ]

# String.Format()
    . 매서드가 포맷 문자열과 인자 리스트를 분리하여 전달하기 때문에 가독성이 떨어집니다.

    . 전달하고자 하는 인자가 많아 질수록.... 

      포맷 문자열에 나타낸 인자의 개수와 실제로 전달되는 인자의 개수가 정확히 일치하는지 확인이 불편합니다.

누락 시에는 런타임 예외가 발생합니다.



보간 문자열
    . C# 6.0부터 지원해주고 있는 기능입니다.

    . 사용법으로는 문자열 앞에는 $를 붙이고, 문자열로 변경할 표현식은 { }내에 두어 사용합니다.

    . 어느 부분이 어떻게 대체될 지 바로 알 수 있어 예측하기 쉽습니다.

    . 인자를 잘못 전달하거나, 출력 위치를 잘못 지정할 일이 없습니다.




# 보간 문자열을 사용하는 것이 좋은 이유!

    1. 코드의 가독성이 대폭 향상된다.


    2. 정적 타입 검사를 수행할 수 있어 개발자의 실수를 미연에 방지한다.


    3. 기존 문자열 포매팅 방식에 비해 문자열을 생성하기 위한 표현식이 더 풍성하다.



# 주의 사항


: 값 형식의 인자를 전달할 때, 문자열로 변경하여 사용하면 좋다!


 . 문자열 보간 기능을 사용하더라도 실제 C# 컴파일러는 Param을 이용하여 Object 배열을 전달합니다. 

  예를 들어, 아래에서 보면 Math.PI를 인자로 전달하고 있습니다.


 Math.PI는 double 형으로 컴파일러가 인자를 전달하게 되면,

 double -----> object 으로의 박싱을 수행하게 됩니다.

 

 이러한 약간의 코드가 성능에 큰 영향을 미치진 않겠지만, 

 너무 자주 사용하거나 많은 루프 내에 사용한다면 좋지 않은 영향을 미칠 수 있습니다.


 이런 문제는 ToString( )을 사용하여 전달할 인자를 사전에 문자열로 변경하면 컴파일러의 값 박싱을 방지할 수 있습니다.




    : 보간 문자열에서 삼항연산자를 사용할 수 있다, 다만 괄호로 묶어주자!


    . ' : '(조건 표현식)을 문자열 보간 기능과 같이 사용하게 되면 약간의 충돌이 발생할 수 있는데, 

C#은 ' : '을 표맷 문자열의 시작이라 판단하기 때문입니다.

=> 컴파일 오류가 발생하는 것을 볼 수 있습니다.


이러한 문제는 아래와 같이 ( )괄호로 묶어 사용하면 해결이 가능해집니다!



: 보간 문자열 중첩 사용도 가능하지만, 적당히 사용하자!


. 마지막으로 보간 문자열의 표현식은 중첩해서 사용도 가능합니다. 

 하지만 무엇이든 그렇듯, 과도하게 사용하면 이해하기 어려운 코드가 될 수 있습니다.



[ Digression ]

 

  공부하면서 String.Format( )과 문자 보간열에 대한 정보를 찾아본 결과, 

문자 보간열도 컴파일 타임에서 String.Format( )으로 바뀌어 성능상 큰 차이는 없는 것으로 보입니다. 

다만, 문자열을 합치는 경우에는 (String Builder, concat과 같은) 여러 방법 중 문자 보간열이 가장 느린 것으로 결과가 나왔습니다.





참 고 : Effective C# 




캐스트보다는 is, as가 좋다.



[ Content ]

cast (캐스트)
    . 프로그래밍 언어에서, 객체의 유형을 다른 유형으로 바꾸는 것을 말한다.
    . 아래는 C형태의 캐스트로 '(타입)피연산자' 형태로 사용된다.


1
2
3
int i = 42;
char* p = &buf;
*p = (char)i;

cs



- as 연산자

    . 특정 형식의 변환을 수행한다. 캐스트 작업과 비슷하지만, 변환할 수 없는 경우 예외를 발생시키지 않고 null을 반환한다.
    . 참조 : https://docs.microsoft.com/ko-kr/dotnet/csharp/language-reference/keywords/as


is 연산자

    . 지정된 형식으로 변환할 수 있는지, 런타임에 형식 호환성을 평가한다. 
      변환이 가능하면 true, 그렇지 않으면 false를 반환한다.
    . 참조 : https://docs.microsoft.com/ko-kr/dotnet/csharp/language-reference/keywords/is


- is, as 연산자를 사용하는 것이 좋은 이유!

    1. 코드 작성이 용이하다.


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
object o = Factory.GetObject();
 
//----------- as 연산자를 사용한 버전
 
MyType t = o as MyType;
 
if(t != null)
{
    // MyType 타입의 t 객체 사용.
}
else
{
    // 오류 보고.
}
 
//----------- 캐스트를 사용한 버전
 
try
{
    MyType t;
    t = (MyType)o;
    
    // MyType 타입의 t 객체 사용.
}
catch(InvalidCastException)
{
    // 오류 보고
}
cs


  캐스팅을 사용한 경우에는 예외처리 코드와 null 확인 코드 모두 필요합니다.
반면, as를 사용하면 try / catch 문을 사용할 필요가 없습니다. 때문에 성능과 가독성이 좋고, null 확인 코드만 있으면 되죠!



    2. 더 안전하고 런타임에 효율적으로 작동한다.

 

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
30
31
32
33
34
35
36
37
38
39
40
41
42
public class SecondType
{
    private MyType _value;
 
    // 중략
    // 형변환 연산자 SecondType -> MyType
    public static implicit operator MyType(SecondType t)
    {
        return t._value;
    }
}
 
SecondType o;
// obejct o = Factory.Getobject();
// o는 SecondType
 
//----- as 연산자 사용.
 
MyType t = o as MyType; // o가 MyType이 아니면 실패.
 
if(t != null
{
    // MyType 으로 형변환된 t 사용.
}
else
{
    // 실패.
}
 
//----- 캐스트 사용.
 
try
{
    MyType t1;
    t1 = (MyType) o;
    
    //MyType 형변환 된 t1 사용.
}
catch(InvalidCastException)
{
    // 형변환 오류 보고.
}
cs


  위에 캐스트와 as 연산자를 이용한 예제가 있습니다. 결과를 먼저 말하자면 두 버전 모두 실패합니다..!!
캐스트로 형변환 하는 경우, 사용자 정의 형변환 연산자가 호출되어 캐스팅에 성공할 것 같지만 그렇지 않습니다.
그 이유는 컴파일러는 단순히 컴파일 타임에 객체가 어떤 타입으로 선언됐는지만 추적하기 때문입니다.
컴파일러는 객체 o가 런타임에 어떤 타입인지 알 수 없어 객체 o가 object 타입이라고 생각하고 object 타입을 MyType으로 형변환 할 수 있는 연산자가 정의되어 있는지를 확인합니다.


  as나 is는 런타임에 객체의 타입을 확인하지만, 필요에 따라 박싱을 수행하는 것을 제외하고는 어떠한 작업도 수행하지 않습니다. 따라서, as, is 형변환은 사용자 정의 형번은 수행되지 않습니다. as, is를 사용하여 형변환을 수행하려면 변환하고자 하는 객체는 지정한 타입이거나 지정한 타입을 상속한 타입이어야 합니다. 이 외에는 모두 실패하게 됩니다.


  다시 말해, t = st as MyType; 에서 st 가 MyType 이거나 MyType을 상속한 타입이 아니라면 컴파일 오류를 발생하게 될겁니다. 이처럼 as, is 연산자가 결과의 일관성이 높고 더 안전하며 런타임에 효율적입니다.


- 사용자 정의 형변환 : https://loveme-do.tistory.com/4?category=767223




[ Digression ]


  이미 C++을 공부하거나 C#을 공부하면서 C스타일의 형변환은 지양해 와서 as, is를 사용하는게 훨씬 익숙한 편인데, 캐스트가 컴파일 타임에 객체 타입을 확인해서 추적한다거나 사용자 정의 형변환은 이번에 알게 됐네요....!!
캐스트를 완전히 쓰진 않으니 써야할 경우 이에 좀 더 안전하게 대처할 수 있지 않을까 합니다.






참 고 : Effective C# 

const보다는 readonly가 좋다.



[ Content ]

- const
    . 컴파일 타임 상수.
    . 컴파일 타임에 변수가 값으로 대체되어, 메서드 내부에서도 선언이 가능.
    . 컴파일 타임에 상수를 리터럴로 대체해야 하기 때문에, 내장된 숫자형, enum, 문자열, null과 같은 내장 자료형만 사용 가능.

- readonly
    . 런 타임 상수.
    . 컴파일 타임에 값으로 대체되는 것이 아닌, 상수에 대한 '참조'로 컴파일되어 메서드 내에서는 선언 불가능.
    . 꼭 생성과 동시에 초기화하지 않아도 되며, 생성자에서 최초 1번 초기화 가능. 그 이후에는 수정 불가능.
    . 어떤 타입과도 사용될 수 있다.


 위 내용의 런타임 상수와 컴파일 상수의 차이 중, 중요한 차이는 값이 런타임에 평가된다는 점입니다. 
런타임 상수를 참조하는 코드를 컴파일하면 컴파일 타임 상수처럼 코드를 값으로 대체하지 않고, 참조 코드를 생성합니다.
이러한 차이가 유지 및 보수에 상당한 영향을 미칩니다.


1
2
3
4
5
public class UsefulValues
{
    public static readonly int StartValue = 5;
    public const int EndValue = 10;
}



 위와 같이 정의하여 값을 사용하다 아래와 같이 수정할 때,


1
2
3
4
5
public class UsefulValues
{
    public static readonly int StartValue = 105;
    public const int EndValue = 110;
}



 수정 후, 리빌드하지 않고 사용한다면, StartValue에 대한 값은 참조 코드로 인해 런타임에 105로 읽어드리지만, EndValue는 이전 컴파일 시점에 대체되었던 10으로 값이 유지됩니다.

 이처럼 컴파일 타임 상수의 값을 수정할 때는 신중해야 하며, 모든 코드를 재컴파일해야 합니다. 하지만, 런타임 상수는 값 변경만으로도 수정이 가능하며 이진 호환성도 그대로 유지됩니다.


 이와 반대로 재컴파일 전까지 기존 값을 유지하고 싶은 경우도 있을겁니다. 이러한 경우 컴파일 타임 상수를 활용하면 됩니다.
또한, 선택적 매개 변수에 대한 기본값, 특성의 매개 변수, switch/case문의 레이블, enum 정의에 사용되는 상수 등도 컴파일 시에 사용되어야 하므로 const를 통해 초기화 해야합니다.
        - 선택적 매개 변수 : https://loveme-do.tistory.com/2

 const가 가지는 장점은 성능이 빠르다는 것인데, 이를 통해 얻을 수 있는 성능 개선 효과가 크지 않아, 위와 같은 예외적인 상황을 제외한다면 readonly를 사용하는 것이 좋습니다.




[ Digression ]


 아직 많지 않은 경험으로 사용하면서 가장 크게 느낀 장점은 readonly가 모든 타입과 사용할 수 있다는 점입니다.
상수 사용에 내장 타입과도 많이 사용하지만 직접 정의한 타입을 사용하는 경우도 많아 자주 사용할 수 밖에 없습니다. 
 위의 내용처럼 유지 및 보수 시에 확실히 상수 값 변경에 부담을 가지지 않아도 된다는 점은 크게 느낄 수 있는 장점이라 생각합니다.






참 고 : Effective C# 

+ Recent posts