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 사용을 권장