C# 코딩의 기술 기본편

Table of Content

저자: 가와마타 아키라 (김완섭 역)
출판사: 길벗
출간일: 2015. 9. 23. 전자책 출간
관련 사이트: 리디북스

C# 기본서 뗀 후 읽어본 책. 책에 나오는 소스코드는 익숙하지 않은 면도 있었고 이 소스코드를 과연 써먹을 만한 곳이 있을까 싶은 의심도 들어서 소스코드에 집중하기 보다는 코딩을 어떻게 해야 효율적인지에 집중해서 읽었다.

 

래머 군, 악마 그리고 천사의 다이얼로그

악마: 무슨 속임수를 쓴 거지?
천사: 속임수 같은 거 쓰지 않았어!

래머군: 누구랑 데이트 하는데?
천사: 윈도 업데이트
악마: ……

이 책에선 현직 프로그래머인 래머 군, 그리고 악마와 천사가 등장한다. 이들은 서로 대화를 주고받으며 문제를 해결해나간다. 래머 군이 도움을 요청하면 악마는 땜빵식 코드를 짜주고 천사는 그 코드를 다듬어 주며 설명하는 방식. 지루하지 않게 만들어 주지만 개그가 꽤나 유치하다. (……)

C# 뉴비라서 책 내용이 좋은 건지 나쁜 건지 평가할 순 없지만 어떻게 코딩을 해야 좋은지 참고가 되었다.

 

주요 내용 (스포일러 주의!!)

[클릭(터치)하여 보기]

var 사용에 대한 고민

  • 수치로 처리되는지 문자열로 처리되는지에 대한 애매모호함이 생길 수 있음
    • 예: 1+2=3, “1”+”2”=“12″
  • 형 추론이 가능한 긴 코드에서 var를 사용하면 코드가 간결해져 가독성이 좋아짐
    • List<string> list = new List<string>(); <- 보다
    • var list = new List<string>();  <- 을 사용하면 코드가 간결해지고 형(List<string>) 추론이 가능함

if와 switch에 관한 오해

  • C언어에서 switch문의 단점
    • 수치 형만 사용 가능함
    • break 키워드를 사용하지 않으면 제어 이동(fall-through)1)이 발생하여 버그가 발생할 수 있음
  • C#에서는 이런 switch문의 단점을 보완
    • bool, char, string, 수치, 열거형 및 이들의 nullable 형식까지 사용 가능함
    • 또한 break 키워드를 사용하지 않으면 컴파일 에러가 발생하므로 제어 이동 때문에 버그가 발생하지 않음
    • 식 하나로 값을 분류하기 때문에 버그가 발생할 수 있는 여지도 줄어듦.

1) 제어 이동(fall-through): 앞에 있는 조건을 만족했지만, 다른 조건에 있는 처리도 계속해서 실행하는 것

for와 foreach

  • for문은 임의 접근이 가능하지만 인덱스 연산이 수행되기 때문에 foreach보다 느림
  • foreach문은 순차 접근 기반이므로 순차 접근 시엔 foreach를 사용하는 것이 성능면에서 이득

루프할 필요가 없는 루프

  • 단순한 데이터를 처리하는 경우 이미 구현된 메소드가 있는 경우가 대부분.
  • 직접 루프문을 작성하지 말고 테스트를 거친 라이브러리를 사용하면 코드가 간결해지며 신뢰성이 높아짐
    • 예: Enumerable의 First(), FirstOrDefault(), Last(), LastOrDefault()
namespace MyFirstConsoleApp
{
    class Program
    {
        public static void Main(string[] args)
        {
            int[] array = { -1, 1, -2, 2, 3 };

            /* 배열에서 가장 먼저 등장하는 0보다 작은 값 출력 */
            Console.WriteLine(array.FirstOrDefault(c => c < 0));

            /* 배열에서 두 번째로 등장하는 0보다 작은 값 출력 */
            Console.WriteLine(array.Where(c => c < 0).ElementAtOrDefault(1));
        }
    }
}

해제되지 않는 참조

  • 객체를 전역변수로 사용해야 하는 경우 객체 사용이 끝나면 null을 참조하게 하여 가비지 컬렉션이 객체를 회수하도록 해야 함
  • 위의 제약조건이 없다면 지역변수로 선언하여 사용하는 것이 Out of memory 예외 발생 등을 막을 수 있음
  • 거대한 배열은 메모리를 엄청나게 잡아먹지만 열거(Enumerable) 객체는 데이터 자체를 저장하는 게 아닌 필요한 데이터를 반복해서 가져오므로 메모리를 압박하지 않음

형변환 처리 팁

  • 특정 형식의 클래스 객체만을 골라 처리할 때 OfType<T>() 메소드를 활용하면 코드가 간결해짐
namespace MyFirstConsoleApp
{
    class Base { }

    class Extended:Base
    {
        public void SayHello() { Console.WriteLine("안녕요?ㅎ"); }
    }

    class Program
    {
        public static void Main(string[] args)
        {
            Base[] array = { new Base(), new Extended(), new Base() };

            /* as 연산자와 if문을 사용했으나 코드가 길어짐 */
            foreach(var item in array)
            {
                var extended = item as Extended;
                if (extended != null)
                    extended.SayHello();
            }

            /* 형변환 시 괄호가 많이 들어가 코드의 가독성이 떨어짐 */
            foreach (var item in array)
            {
                if (item is Extended)
                    ((Extended)item).SayHello();
            }

            /* OfType<>(): LINQ 메소드로 지정된 형의 데이터만 추출하여 해당 형으로 반환 
             * 위의 코드보다 간결해짐*/
            foreach (var item in array.OfType<Extended>())
                item.SayHello();
        }
    }
}

구조체를 활용할 기회는 많지 않다.

  • 구조체는 작은 크기의 멤버(데이터)가 적을 때 클래스의 인스턴스로 처리하는 것보다 빠름(멤버가 10개 정도 되어도 이미 많음)
  • 멤버가 많은 구조체를 인수로 사용하면 통째로 복사되기 때문에 호출을 반복하여 속도가 느려짐
  • 이런 경우 클래스를 사용하는 것이 유리함

static이 만능은 아니다.

  • static 메소드는 인스턴스 생성 없이 바로 사용하므로 편리하다고 느낄 수 있음
  • 그러나 여러 처리를 병렬로 실행할 때는 static 메소드를 사용하지 않는 것이 좋음
  • 한 곳에서만 사용하는 것이 확실하다면 static을 사용

const, readonly 그리고 enum

  • const는 반드시 선언 시 값을 할당해야 하며 한 번 값이 할당되면 이후 변경이 불가능함. static 키워드를 사용하지 않아도 자동으로 static 변수가 됨.
  • readonly는 선언 시 값을 할당하지 않아도 되며 한 번 값이 할당되어도 생성자를 통해 값을 변경할 수 있음. static을 사용하여 static 변수로 지정할 수 있음.
  • 이름을 잘못 사용하면 치명적일 수 있는 경우 상수보다는 열거형(enum)을 사용하는 것이 좋음.

메소드의 매개변수가 너무 많을 때

  • 메소드에 매개변수가 많은 경우 명명된 매개변수(Named Parameter)와 선택적 매개변수(optional parameters)를 사용하면 코드가 깔끔해짐
namespace MyFirstConsoleApp
{
    class Person { }

    class Program
    {
        /* 선택적 매개변수(Optional Parameters):
         * 매개변수의 기본값을 지정할 수 있음 */
        private static void DumpPerson1(Person p1, Person p2 = null,
            Person p3 = null, Person p4 = null, Person p5 = null,
            Person p6 = null, Person p7 = null, Person p8 = null)
        {
            // ...
        }

        public static void Main(string[] args)
        {
            Person p1 = new Person();
            Person p3 = new Person();

            /* 명명된 매개변수(Named Parameters):
             * 매개변수를 순서대로 입력하지 않을 수 있음 */
            DumpPerson1(p1, p3: p3);
        }
    }
}

예외는 가급적 발생하지 않도록

  • 예외 처리는 무겁기 때문에 가급적 예외를 발생시키지 않는 것이 좋음
    • try-catch문 대신 예외 발생 시 bool 형을 반환하는 메소드를 사용하는 것도 방법
    • 예: Parse() 메소드 대신 TryParse() 메소드 사용
  • 예외가 발생하지 않도록 설계하는 것보다 처음부터 무효한 데이터를 제외하는 것이 중요

쿼리가 너무 많을 때

  • 쿼리를 수만 번 이상 반복 실행하면 오버헤드가 과도하게 발생해 느려짐
  • 이런 경우 메모리를 더 사용하더라도 배열로 만들어서 처리하면 더 빠를 수 있음
  • 어느 쪽을 사용할 지는 데이터 양과 처리 성격에 따라 판단
namespace MyFirstConsoleApp
{
    class Program
    {
        public static void Main(string[] args)
        {
            /* ToArray() 메소드로 열거형을 배열로 만들어 전달 */
            var ar = Enumerable.Range(0, 100000).ToArray();

            var start = DateTime.Now;
            int sum = 0;

            for(int i=0; i<ar.Count(); i++)
                sum += ar.ElementAt(i);

            Console.WriteLine(sum);
            Console.WriteLine(DateTime.Now - start);
        }
    }
}

같은 기능을 두 번 구현하는 경우

  • 이미 고유성이 확보되었는데도 다시 고유성을 판정하면 불필요한 코드가 됨
    • 예: 예외를 2번 판정하는 경우
namespace MyFirstConsoleApp
{
    class Program
    {
        public static void Main(string[] args)
        {
            var dic = new Dictionary<int, string>();
            dic.Add(1, "One");
            dic.Add(2, "Two");

            /* Dictionary의 Add() 메소드는 키값이 이미 존재하면 예외를 발생시키므로
             * 아래 if문을 쓸 필요는 없음 */
            //if (dic.ContainsKey(1))
            //    throw new ApplicationException("Key is already used");
            dic.Add(1, "원"); // ArgumentException 발생
        }
    }
}

기타

  • 서로 연관되지 않은 정보들은 각각의 클래스로 분리하여 구현
  • 어떤 경우에도 자원이 반드시 해제되어야 한다면(예: 파일 닫기) IDisposable 인터페이스와 using문을 사용
  • 저장하려는 데이터 형식의 크기보다 더 큰 형식을 사용하는 형의 오용을 지양
    • char 대신 string을 사용하거나, 0과 1만 사용하지만 long을 사용하는 등
  • System.Collections 네임스페이스는 레거시로 남아있는 컬렉션. 사용하지 않는 것을 권장.
    • 필요하다면 System.Collections.Generic 사용을 권장

댓글 남기기