Effective C# 3판 스터디, 그 첫 번째 포스트

📚 목차

  • 🔸 ITEM 1: 지역변수를 사용할 때는 var를 사용하라
  • 🔸 ITEM 2: const보다는 readonly가 좋다
  • 🔸 ITEM 3: 캐스트보다는 is, as가 좋다.
  • 🔸 ITEM 4: string.Format()을 보간 문자열로 대체하라
  • 🔸 ITEM 5: 화권 별로 다른 문자열을 생성하려면 FormattableString을 사용하라
  • 🔸 ITEM 6: nameof() 연산자를 적극 활용하라
  • 🔸 ITEM 7: 델리게이트를 이용하여 콜백을 표현하라
  • 🔸 ITEM 8: 이벤트 호출 시에는 null 조건 연산자를 사용하라
  • 🔸 ITEM 9: 박싱과 언박싱을 최소화하라
  • 🔸 ITEM 10: 베이스 클래스가 업그레이드된 경우에만 new 한정자를 사용하라




🔸 ITEM 1: 지역변수를 사용할 때는 var를 사용하라

타입을 명시적으로 드러내지 않는 경우라면 var를 사용하는 것이 좋다.

IEnumerable<string> q = 
    from c in db.Customers
    select c.ContactName;
 
var q2 = q.Where(s => s.StartsWith(start));
 
return q2;

q를 var로 받지 않을경우, IQueryable<string>을 반환해야 하지만

상위객체인 IEnumerable로 반환하게 되어 Where 구문에서 성능이 저하되게 된다.

var q = from c in db.Customers
        select c.ContactName;
 
var q2 = q.Where(s => s.StartsWith(start));
 
return q2;

반대로 q를 var로 받았을 경우에는 IQueryable를 반환하게 되어 성능저하가 발생하지 않게 된다.

📌 내장 숫자 타입의 경우,

내장 숫자 타입 (int, float, double 등)을 선언할 때는 명시적으로 타입을 선언하는것이 좋다.

예를들어, 다음 코드는 GetMagicNumber()의 리턴 타입에 따라 total에 저장되는 값이 달라진다.

var total = 100 * GetMagicNumber() / 6;




🔸 ITEM 2: const보다는 readonly가 좋다

🔹 Const

Const는 컴파일 상수이다.

const로 선언한 변수는, 컴파일 타임에 변수가 값으로 변환된다.

즉, 아래와 같은 코드가 있을 때,

public const int Millennium = 2000;
 
if (myDateTime.Year == Millennium)

이는 컴파일 시 다음과 같아진다.

if (myDateTime.Year == 2000)

타입을 명시적으로 드러내지 않는 경우라면 var를 사용하는 것이 좋다.

const는 내장된 숫자형, enum, string, null에 대해서만 사용 가능하다.

const의 값 치환 과정은 컴파일에서 적용되므로,

컴파일 할때 사용되는 상숫값을 정의할땐 반드시 const를 사용해야 한다.

readonly보다 성능이 빠르긴 하지만 미미하고, 유연성이 낮다.

🔹 Readonly

Readonly는 런타임 상수이다.

런타임에 값이 평가된다.

생성자에서 초기화 가능, 어떤 타입도 함께 사용할 수 있다.

const는 값이 바뀌면 응용프로그램 전체를 재빌드를 해야하지만,

readonly는 바뀐파일만 배포할 수 있다.




🔸 ITEM 3: 캐스트보다는 is, as가 좋다

🔹 Cast

형변환 연산자를 적용할 수 있다.

public class TestType
{
    private InnerType _value;

    // 사용자 정의 형변환
    // TestType을 MyType으로 캐스팅한다.
    public static implicit operator MyType(TestType t)
    {
        return t._value;
    }
}

사용자가 상황에 맞게 형변환 연산자를 정의 해줘야한다.

🔹 is,as 연산

사용자 정의 형변환은 수행불가

형변환 과정에서 새로운 객체를 생성하지 않음

value 타입은 as를 사용할 수 없다.

is,as 를 사용하는 것이 가독성이 더 뛰어나다.

// 캐스트 연산
object o = Factory.GetObject();
try
{
    MyType t;
    t = (MyType)o;
}
catch(InvalidCastException)
{
}
object o = Factiory.GetObject();
MyType t = o as MyType;
 
if( t != null)
{
}
else 
{
}




🔸 ITEM 4: string.Format()을 보간 문자열로 대체하라

보간 문자열이란 C# 6.0부터 도입된 문자열 포멧팅 방법이다.

var str_1 = "안녕하세요 저는 " + name + " 입니다. 나이는 " + age + "세 입니다.";
var str_2 = string.Format("안녕하세요 저는 {0} 입니다. 나이는 {1}세 입니다.", name, age);
var str_3 = $"안녕하세요 저는 {name}입니다. 나이는 {age}세 입니다.";

🔹 보간 문자열의 장점

  • 코드 가독성이 대폭 향상
  • 정적 타입 검사를 수행하기 때문에 개발자의 실수를 방지
  • 문자열을 생성하기 위한 표현식이 더 풍부함


// 앞에 $를 붙이고 표현식을 {}안에 둔다.
Console.WriteLine($"The value of pi is {Math.PI}");
 
// 원하는 포매팅을 위한 추가적 인자 전달
Console.WriteLine($"The value of pi is {Math.PI.ToString("F2")}");
 
// :을 이용해도 가능
Console.WriteLine($"The value of pi is {Math.PI:F2}");
 
// @를 넣고 ()를 사용해 ':' 가 조건연산자임을 알려줄 수 있다..
Console.WriteLine($@"THe value of pi is {(round ?
        Math.PI.ToString() : Math.PI.ToString("F2"))}");
 
// ?. 연산자와 ?? 연산자도 사용가능!
Console.WriteLine($"The customer's name is {c?.Name ?? "Name is missing"}");




🔸 ITEM 5: 문화권 별로 다른 문자열을 생성하려면 FormattableString 을 사용하라

문자열 보간을 사용하여 문자열을 만들면 반환값이 문자열일수도,

FormattableString을 상속한 타입일 수도 있다.

FormattableString을 사용하면 현재 컴퓨터에 지정된 문화권을 고려하여 문자열을 생성할 수 있다.

문화권 코드는 이 블로그를 통해 참고하면 된다.

예를들면 한국어는 ko-KR, 프랑스어는 fr-FR이다.

// 프랑스어 문화권으로 변경하는 방법
public static string ToGerman(FormattableString src)
{
        return string.Format(
        System.Globalization.CultureInfo.CreateSpecificCulture("fr-FR"),
        src.Format, src.GetArguments());
}




🔸 ITEM 6: nameof() 연산자를 적극 활용하라

항상 로컬 이름을 문자열로 반환하는 역할을 수행한다.

심볼의 이름을 평가하며, 타입, 변수, 인터페이스, 네임스페이스에 대하여 사용할 수 있다.

심볼의 이름을 바꾸거나 수정할때도 손쉽게 변경 사항을 반영할 수 있다.

예를 들면, 예외 타입에 하드코딩대신 nameof()를 사용하면 rename을 해도 바로 적용가능하다.

public static void ExceptionMessage(object thisCantBeNull)
{
        if(thisCantBeNull == null)
            throw new ArgumentNullException(nameof(thisCantBeNull),
                    "We told you this cant be null!");
}




🔸 ITEM 7: 델리게이트를 이용하여 콜백을 표현하라

델리게이트를 이용하면 타입 안정적인 콜백을 정의할 수 있다.

.NET Framwork 라이브러리는 Predicate<T>, Action<>, Func<>과 같은 형태로

자주 사용되는 델리게이트를 정의해두고 있다.

Multicast가 가능한 콜백을 만들 수 있다.




🔸 ITEM 8: 이벤트 호출 시에는 null 조건 연산자를 사용하라

C# 6.0에 새롭게 추가된 null 조건 연산자를 고전적인 null check방식 대신 사용하자.

🔹 고전적인 방식

// 구식적인 방법. Updated가 null이 아니여서 if 구문에 들어올때,
// 다른 스레드에서 이벤트 핸들러를 취소할경우 NullReferenceException이 발생.
public void RaiseUpdates()
{
        counter++;
        if(Updated != null)
            Updated(this, counter);
}

🔹 null 조건 연산자를 이용한 방식

// NEW!: null 조건 연산자를 이용하면 멀티스레딩 환경에도 안전하다.
public void RaiseUpdates()
{
        counter++;
        Updated?.Invoke(this, counter);
}




🔸 ITEM 9: 박싱과 언박싱을 최소화하라

int    i = 123;
object o = i;       // 박싱
int    j = (int)o;  // 언박싱

박싱과 언박싱은 성능에 좋지 않은 영향을 미친다.

수행하는 과정에서 임시객체가 생성되는데, 이로 인해 버그가 발생할 수도 있다.

박싱은 값 타입을 참조 타입으로 변경한다.




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

베이스 클래스에서 이미 사용하고 있는 메서드를 재정의하여

완전히 새로운 베이스 클래스를 만들어야 하는경우에 new한정자를 사용한다.

public class MyClass
{
        public void MagicMethod()
        {
        }
}
 
public class MyOhterClass : MyClass
{
    // MagicMethod를 재정의
    public new void MagicMethod()
    {
    }
}

🔹 overridenew의 차이

virtual method (가상 메서드)는 abstract method (추상 메서드) 와 달리, 자식 클래스에서 재정의 여부는 선택이다.

추상 메서드에서는 재정의는 필수이다.


이때, 가상 메서드가 포함된 클래스를 상속할 경우 자식 클래스에서 재정의 할 때에는

override, new 두 가지 한정자로 정의할 수 있습니다.

추상 메서드가 포함된 클래스를 상속할때에는 override 만 가능하다.


메서드 한정자
가상 메서드 override, new
추상 메서드 override



그럼 어떨때 overridenew를 사용해야 하는것일까?

이를 정확히 설명하기 위해서는 Up CastingDown Casting부터 설명해야 한다.


  • Up Casting : 자식 → 부모 형변환. (자동) (Cat 클래스를 Animal 클래스로 변환)
  • Down Casting : 부모 → 자식 형변환. (명시적) (Animal 클래스를 Dog 클래스로 변환)


// Up Casting
Animal animal = new Lion();

// Down Casing
Lion lion = (Lion)animal;


1️⃣ Case 1 . override를 사용 할 경우

class Program {
    static void Main(string[] args) {
        // Up Casting
        Animal animal = new Lion();
        animal.Sound();
    }
}
 
 
public class Animal {
    public virtual void Sound() {
        Console.WriteLine("부모");
    }
}
public class Lion : Animal {
    public override void Sound() {
        Console.WriteLine("어흥");
    }
}
  • override는 자신이 상속받은 부모 클래스의 메소드를 재정의 하는 것을 의미한다.
  • Animal.Sound 부모 클래스 메서드를 숨기고, 자식 클래스인 Lion.Sound 메서드를 재 정의함.
  • Up Casting 된 animal 객체에서 Sound를 호출하면, 재정의된 Lion.Sound 메서드가 호출이 되고 콘솔에는 어흥이 찍히게 된다.


2️⃣ Case 2 . new를 사용 할 경우

class Program {
    static void Main(string[] args) {
        // Up Casting
        Animal animal = new Lion();
        animal.Sound();
    }
}
 
 
public class Animal {
    public virtual void Sound() {
        Console.WriteLine("부모");
    }
}
public class Lion : Animal {
    public new void Sound() {
        Console.WriteLine("어흥");
    }
}
  • override 와 달리, new 키워드는 부모 클래스의 메서드를 완전히 무시하고 동일한 이름의 새로운 메서드를 만드는 것을 의미한다.
  • Lion.Sound 메서드Animal.Sound 메서드와 완전히 다른 메서드라고 생각하면 된다.
  • Up Casting 된 animal 객체에서 Sound를 호출하면, animal 객체의 타입은 Animal 이기 때문에 Animal.Sound 메서드가 호출이 되고 콘솔에는 부모가 찍히게 된다.