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

 

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

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

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

.Net 리소스 관리에 대한 이해

 

.Net의 리소스를 관리하려면 가비지 수집기(Garbage Collector, GC)의 동작 방식을 알아야 한다.

C#은 C++과 다르게 Heap 메모리에 적재시킨 인스턴스 영역을 해제 시킬 필요가 없다.

왜냐하면 가비지 수집기가 알아서 해제 시켜주기 때문이다.

C++을 먼저 접했던 나는 조금 꺼림찍한 내용이었다.

 

자 그럼 가비지 수집기의 동작방식을 알아보자.

 

가비지 수집기는 관리되는 메모리(managed memory)를 다룬다.

그래서 아래의 3가지와 같은 문제를 개발자는 부담을 줄일 수 있다.

1. 메모리 누수(leak)

2. 댕글링 포인터(dangling pointer)

3. 초기화되지 않는 포인터

 

그렇다면 개발자는 마음대로 객체를 할당하고 사용만 하면 되는 것인가??

반은 맞고 반은 틀리다.

 

이유를 들어보자

1. 가비지 수집기는 데이터베이스 연결, GDI+ 객체, COM 객체, 시스템 객체 등과 같은 비관리 리소스는 여전히 개발자가 관리해야 한다.

2. EventHandler나 delegate 등도 참조는 되어있지만 사용하지 않는 객체들이 불필요하게 메모리에 남을 수도 있다.

 

자 위와 같은 종류의 객체들은 어떻게 관리하고 사용해야 할까??

.Net framework는 IDispose 인터페이스를 제공하고 개발자는 표준 Dispose 패턴만 활용하면 문제를 해결할 수 있다.

IDispose패턴을 활용하는 방법은 ITEM17에 다시 포스팅하겠다..

 

자 다시 돌아와서 가비지 수집기는 어떤 원리로 메모리를 관리하는 것일까?

혹시 마크/콤팩트(Mark/Compact) 알고리즘이라고 들어봤는가?

알면 해당 포스팅은 그냥 넘기면 될 것 같다.

 

마크/콤팩트 알고리즘은 여러 객체 사이의 관계를 파악하고 사용하지 않는 객체는 자동으로 제거하는 알고리즘이다.

 

여러 객체사의 관계란?

Class A는 Class B를 상속한다.

Class A의 필드 변수에 Class C라는 인스턴스를 가지고 있다.

이런 부분을 관계라고 생각하면 될 것이다.

 

본인은 WPF에서 MVVM 패턴을 사용하므로 MainViewModel으로 예시를 들겠다

1. MainViewModel안에는 Test1Model, Test2Model을 가지고 있다.

Heap 메모리 상태

2. 사용자가 Test2Model에서 일련의 연산 수행시켜

  Class C라는 것을 할당하여 실행했다.

Class C를 할당한 Heap 메모리 상태

3. 실행이 끝난 후 Class C는 그대로 남아있다.

실행이 끝난 후 Heap 메모리 상태 Class C는 그대로 남아있다.

4. 가비지 수집기가 최상단 MainViewModel 객체의 관계를 탐색한다.

마크/콤팩트 알고리즘의 탐색 결과

5. Class C는 Test1Model에서 계속 사용하고 있는가?? -> No 

Class C를 제거한 Heap 메모리

6. Heap 메모리는 Compact 됐다. 

Compact 적용 Heap 메모리

그림이 많이 조잡하지만 이해해주길 바란다.

마크/콤팩트 알고리즘의 자세한 내용은 google에서 조금 더 탐색해보길 바란다...

 

가비지 수집기는 수집 과정을 최적화 하기 위해서 세대(generation)이라는 개념이 도입되어있다.

내용이 너무 많으므로 간단하게 설명하겠다.

세대는 가비지 수집기가 한번 수행할 때마다 증가하게 되어있다.

 

예를 들어 위의 상황을 들어보자

 

1. 프로그램 실행 시

MainViewModel : 0 세대

Test1Model : 0 세대

Test2Model : 0 세대

 

2. 사용자가 Test1ViewModel에서 연산을 시켜서 Class C를 추가했을 때

MainViewModel : 0 세대

Test1Model : 0 세대

Class C       : 0 세대

Test2Model : 0 세대

 

3. 가비지 수집 수행

MainViewModel : 1 세대

Test1Model : 1 세대

Class C       : 제거

Test2Model : 1 세대

 

4. 사용자가 Test1ViewModel에서 연산을 다시 실행

MainViewModel : 1 세대

Test1Model : 1 세대

Class C       : 0 세대

Test2Model : 1 세대

 

5. 가비지 수집 수행

MainViewModel : 2 세대

Test1Model : 2 세대

Class C       : 제거

Test2Model : 2 세대

 

이해되었는가?

가비지 수집기로부터 살아남으면 세대가 증가하는 것이다.

그럼 Global 변수와 멤버 변수는?? 

자연히 세대가 높은 객체가 될 것이다.

 

자 그럼 세대가 큰 것과 작은 것의 차이는 무엇일까?

바로 가비지 수집기가 탐색/수집하는 대상 선정의 차이다.

 

1세대 객체는 가비지 수집 10번에 한번 탐색 대상

2세대 객체는 가비지 수집 100번에 한번 탐색 대상

이라는 뜻이다. 10번, 100번은 대략이다 자세한 내용은 스스로 찾아보길 바란다.

 

자 여기까지가 가비지 수집기의 간단한 작동 이론이다.

 

결론

1. 가비지 수집기 덕분에 .Net Framework 개발자들은 메모리 관리에 신경을 덜 써도 된다.

2. 가비지 수집기는 마크/콤팩트(Mark/Compact) 알고리즘, 세대(Generation) 최적화 기법을 사용하여 가비지 수집을 한다.

3. 데이터베이스 연결, GDI+ 객체, COM객체, 시스템 객체 등 과같은 비관리 리소스는 IDispose 패턴을 사용하여 메모리 해제 기능을 만든다.

4. eventhandler, delegate 등 참조는 되어있지만 사용하지 않는 객체들이 불필요하게 메모리에 남을 수도 있다.

 

 

 

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

Effective C# ITEM14  (0) 2020.10.28
Effective C# ITEM13  (0) 2020.10.28
Effective C# ITEM 12  (0) 2020.10.26
Effective C# ITEM10  (0) 2020.10.21
Effective C# ITEM9  (0) 2020.10.21

베이스 클래스가 업그레이드된 경우에만 new 한정자를 사용하라

 

비가상 메서드정적으로 바인딩된다. 이는 런타임에 파생 클래스에서 새롭게 정의하고 있는 메서드를 호출한다.

가상 메서드동적으로 바인딩된다. 이는 런타임에 객체 타입이 무엇이냐에 따라 파생 클래스에서 새롭게 정의하고 있는 메서드를 호출한다.

 

new 한정자는 비가상 메서드를 가상 메서드로 바꾸는 것이 아니라 명명 범위(naming scope) 내에 새로운 메서드를 추가하는 역할을 한다.

 

예시 소스부터 보자

 

public class MyClass
{
	public void MagicMethod(){...}
}

public class MyOtherClass : MyClass
{
	publie new void MagicMethod(){...}
}

 

 

object c = MakeObject();

MyClass cl = c as MyClass;
cl.MagicMethod();

MyOherClass cl2 = c as MyOtherClass;
cl2.MagicMethod();

 

두 메서드의 호출 결과는 각각 다르다. 이와 같은 코드를 보는 개발자들은 혼란스러울 뿐이다. 왜냐하면 동일한 객체를 이용하여 동일한 메서드를 호출했지만 결과는 다르기 때문이다. 내가 제일 중요시 여기는 협업하는 코딩에서는 쥐약인 형태이다. 

사실 이런 한정자는 C#에서 왜 제공하는지는 잘 모르지만 덕분에 개발의 자유도는 높아진 것 같다.

 

사용해야 하는 상황이 책에 나와있으니 예시를 올리겠다.

상황 : 헬스 쟁이 개발자는 Widget회사에서 제공하는 BaseWidget Class를 상속받아 휘황찬란한 Widget을 만들고 있었다. 뿐만 아니라 협업하는 개발자 A, B, C.... Z들도 쓰고 있었다.

 

public class MyWidget : BaseWidget
{
	public void NormalizeValues(){...}
}

 

그런데 어느 날 Widget회사에서 새로운 버전을 출시했다고 한다.

새로운 기능을 기대하고 있던 헬스 쟁이 개발자는 당장 구매하여 이전 BaseWidget Class를 갈아치웠다.

 

public class MyWidget : newBaseWidget
{
	public void NormalizeValues(){...}///빨간줄
}

 

큰일이다 자세히 보니 

 

public class newBaseWidget
{
	public void NormalizeValues(){...}
}

 

newBaseWidget에 같은 이름의 함수가 생겨난 것이다...

여기서 2가지 방법이 있다.

 

방법 1 (소요시간 약 일주일)

NormalizeValues 함수 명을 바꾼다.

그리고 NormalizeValues함수를 사용하는 모든 코드를 수정한다.

 

public class MyWidget : newBaseWidget
{
	public void NormalizeAllValues()
	{
		base.NormalizeValues();
	}
}

방법 2 (소요시간 약 10분)

new 한정자를 써서 탈출한다.

 

public class MyWidget : newBaseWidget
{
	public new void NormalizeValues()
	{
		base.NormalizeValues();
	}
}

 

 

자 문제는 해결했다. 하지만 장기적인 대안으로 봤을 때는 방법 1이 올바른 방법이다. 

간혹 누군가가 newBaseWidget이라는 Class를 다루면 조금씩 꼬이기 시작하기 때문이다.

 

당장 내가 3달 안에 퇴사를 한다면 방법 2가 좋을 수도 있다.

하지만 뼈를 묻는다면 무조건 방법 1이 좋다!!

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

Effective C# ITEM14  (0) 2020.10.28
Effective C# ITEM13  (0) 2020.10.28
Effective C# ITEM 12  (0) 2020.10.26
Effective C# ITEM11  (0) 2020.10.25
Effective C# ITEM9  (0) 2020.10.21

박싱과 언박싱을 최소화하라

 

.NetFramework 환경에서는 모든 타입을 참조타입인 System.Object로 정의한다.

위와 같은 정의방식으로 우리는 서로 다른 타입을 이어준다.

 

박싱    : 생성한 인스턴스를 상위 클래스나 인터페이스에 포함시키는 작업

언박싱 : 박싱으로 인해 상위 클래스 내부에서 포함되어있는 인스턴스의 복사본을 가져오는 작업

 

int i = 25;
object o = i;      //박싱
int z = o as int; //언박싱

 

정말 쉬운 예제이다. 

 

나같은 초보 개발자들은 코딩을 하다보면 해당 상황과 비슷한 상황에 직면하면

구현이 최우선인지라 신경쓰지 않고 코딩부터 한다.

 

최근에는 같은 기능을 하는 Class 2개에서 메서드 1개씩만 각각 가져와서 사용하는 상황이 있었다.

 

interface iMethod
{

  void Function();

}


class FunctionalClass1 : iMethod
{

  public void Function(){...}

}

class FunctionalClass2 : i Method

{

  public void Function(){...}

}

class HandlerClass
{

  public List<iMethod> Methods
  {
    get
    {

       return new List<iMethod>(){new FunctionClass1(), new FunctionClass2};

    }

  }
}

 

일단 잘됐다! 

하지만 위와같이 사용하면 FunctionClass1~2를 iMethod라는 인터페이스로 박싱을 시켜버려

성능을 하향시킨다...

그러면 어떻게 해야 할까?

지금 떠오르는 방법은 Strategy pattern이다!!

 

interface iMethod
{
  void Function();
}

class MethodBehavior1 : iMethod
{
   public void Function(){...}
}

class MethodBehavior2 : iMethod
{
   public void Function(){...}
}

class FunctionalClass1
{
  private iMethod _method = new MethodBehavior1();
  
  public iMethod Method
  {
     get
     {
     	return _method;
     }
  }
}

class FunctionalClass2
{
 private iMethod _method = new MethodBehavior2();
  
  public iMethod Method
  {
     get
     {
     	return _method;
     }
  }
  
  public FunctionalClass2(iMethod method)
  {
     this._method = method;
  } 
}

class HandlerClass
{
  FunctionalClass1 _functionclass1 = new FunctionalClass1();
  FunctionalClass2 _functionclass2 = new FunctionalClass2();
  
  public List<iMethod> Methods
  {
    get
    {

       return new List<iMethod>(){_functionclass1.Method, _functionclass2.Method};
    }

  }
}

 

막상 구현해보니 BehaviorClass는 결국 인터페이스로 박싱을 한다...

하지만 이전 HandlerClass에서 Methods를 Get을 여러번하면 여러번 박싱 했지만 

이제는 한번만 한다. 별로 만족스럽지 못한 결과다...

 

Effective C#에서 보여준 예제로 마지막을 깔끔하게 정리해야겠다..

 

int nFirstNumber = 1;
int nSecondNumber = 2;


//박싱을 무시한 예시
Console.WriteLine($@"Number1 {nFirstNumber}, Number2 {nSecondNumber}")

//위의 동작은
//int i = 1;
//object o = i;
//string output = o.toString();
//와 같은 원리로 동작한다고한다..


//박싱을 최소화한 예시
Console.WriteLine($@"Number1 {nFirstNumber.toString()}, Number2 {nSecondNumber.toString()}")
//위의 동작은 object로 변하는 박싱동작을 피하고 바로 string으로 넘겨준다고한다.
//string output = nFirstNumber.toString();

 

결론

System.Object 타입이나 인터페이스 타입으로 변경하는 코드는 가능한 작성하지 말아야한다.

하지만 코드의 유지보수성과 코웍이 가능한 코드도 중요하다!

CLR 만세!

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

Effective C# ITEM14  (0) 2020.10.28
Effective C# ITEM13  (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

+ Recent posts