(C#) 4. 코드의 흐름 제어 (if-else)
조건에 따라 갈라지는 코드를 처음 써보며 정리한 것. else if가 별도 문법이 아니라는 점, 중괄호를 생략하면 생기는 문제, 매직 넘버를 enum으로 바꿔야 하는 이유까지.
위에서 아래로만 흐르지 않게
2편에서 조건을 bool 값으로 만들었다. 이제 그 값에 따라 실행할 코드를 고른다.
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
using System;
namespace if_else
{
class Program
{
static void Main(string[] args)
{
int choice = 0; // 0:가위, 1:바위, 2:보, 3:치트키
if (choice == 0)
{
Console.WriteLine("가위입니다.");
}
else if (choice == 1)
{
Console.WriteLine("바위입니다.");
}
else if (choice == 2)
{
Console.WriteLine("보입니다.");
}
else
{
Console.WriteLine("치트키입니다.");
}
}
}
}
else if 는 하나의 문법이 아니다
처음엔 if, else if, else 세 가지 키워드가 있는 줄 알았다. 아니었다. else if는 else 뒤에 if 문 하나가 온 것뿐이다.
들여쓰기를 원래대로 풀면 이렇게 생겼다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (choice == 0)
{
// ...
}
else
{
if (choice == 1)
{
// ...
}
else
{
if (choice == 2) { }
else { }
}
}
C#에서는 else에 붙는 문장이 하나면 중괄호를 생략할 수 있고, if 문 전체가 문장 하나다. 그래서 한 줄로 이어 붙여 쓸 수 있고, 그 관례가 굳어져 else if처럼 보이게 됐다.
이걸 알고 나니 조건이 위에서부터 순서대로 검사된다는 게 당연해졌다. 앞 조건이 참이면 뒤는 아예 보지 않는다. 그래서 순서가 중요하다.
1
2
3
// 잘못된 순서 - 두 번째 조건에 절대 도달하지 않는다
if (hp > 0) { }
else if (hp > 50) { }
범위를 나눌 때는 좁은 조건을 먼저 둬야 한다.
마지막 else 가 받아내는 범위
원 코드에서 choice가 3이면 “치트키”가 나오는데, 4나 −1이어도 “치트키”가 나온다. else는 앞의 조건이 전부 거짓인 모든 경우를 받는다.
“치트키”라는 특정 값을 처리하려던 거라면 조건을 명시하고 진짜 예외는 따로 두는 게 맞다.
1
2
3
4
if (choice == 3)
Console.WriteLine("치트키입니다.");
else
Console.WriteLine("잘못된 입력입니다.");
의도하지 않은 값이 조용히 정상 경로로 흘러가면, 나중에 원인을 찾기 어렵다.
중괄호를 생략하면
문장이 하나면 중괄호를 뺄 수 있다.
1
2
if (isAlive)
Console.WriteLine("살아있음");
짧아서 좋아 보이는데, 나중에 줄을 하나 더 추가할 때 사고가 난다.
1
2
3
if (isAlive)
Console.WriteLine("살아있음");
Console.WriteLine("체력 회복"); // 조건과 무관하게 항상 실행된다
들여쓰기가 맞아 보여서 눈으로는 안 잡힌다. C#은 들여쓰기가 아니라 중괄호로 블록을 정하기 때문이다. 실제로 이런 실수 때문에 유명한 보안 취약점이 난 적도 있다.
그래서 한 줄이어도 중괄호를 붙이는 쪽으로 굳혔다. 예외는 if (x) return;처럼 같은 줄에 끝나는 경우 정도다.
조건에 bool 만 들어간다
2편에서 본 내용인데 여기서 다시 걸린다.
1
2
if (choice) // 컴파일 에러
if (choice = 0) // 컴파일 에러
C에서는 if (choice = 0)이 대입 후 0이 되어 조용히 거짓이 됐다. C#은 int를 bool로 안 바꿔주니 컴파일 단계에서 잡힌다. ==를 =로 잘못 치는 실수가 원천적으로 막히는 셈이다.
값 하나를 고르는 거면 삼항 연산자
if/else로 값만 정하는 경우가 자주 나온다.
1
2
3
4
5
string result;
if (hp > 0)
result = "생존";
else
result = "사망";
이건 한 줄로 쓸 수 있다.
1
string result = hp > 0 ? "생존" : "사망";
변수를 선언과 동시에 초기화할 수 있어서, 나중에 대입을 빠뜨리는 실수가 없어진다. readonly나 const로 만들 수도 있다.
중첩하면 급격히 읽기 어려워지므로 두 갈래까지만 쓰는 게 좋았다. 세 갈래 이상이면 switch 쪽이 낫다.
중첩이 깊어지면 먼저 빠져나간다
조건이 겹치면 들여쓰기가 계속 깊어진다.
1
2
3
4
5
6
7
8
9
10
if (player != null)
{
if (player.IsAlive)
{
if (player.Hp < 50)
{
Heal(player);
}
}
}
아닌 경우를 먼저 걸러내고 빠져나가면 평평해진다.
1
2
3
4
5
if (player == null) return;
if (!player.IsAlive) return;
if (player.Hp >= 50) return;
Heal(player);
읽을 때 “여기까지 왔으면 앞의 조건은 모두 통과한 것”이 보장되니 머릿속에 담을 조건이 줄어든다. 이걸 가드 절이라고 부른다는 건 나중에 알았다.
매직 넘버를 없애기
0, 1, 2, 3이 각각 무슨 뜻인지는 주석에만 적혀 있다. 주석은 코드와 따로 놀 수 있고, 실제로 값을 하나 추가하면 주석 고치는 걸 잊는다.
enum으로 만들면 이름이 코드에 들어온다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum Choice
{
Scissors, // 0
Rock, // 1
Paper, // 2
Cheat // 3
}
Choice choice = Choice.Scissors;
if (choice == Choice.Scissors)
Console.WriteLine("가위입니다.");
else if (choice == Choice.Rock)
Console.WriteLine("바위입니다.");
enum은 값을 안 적으면 0부터 순서대로 붙는다. 주석에 적어둔 번호와 정확히 같다.
타입이 생기니 if (choice == 5) 같은 게 컴파일 에러가 되고, 편집기가 Choice.까지 치면 후보를 보여준다. 이 상태에서는 다음 편의 switch가 훨씬 잘 어울린다.
정리하면
else if는 별도 문법이 아니라else뒤에 온if문이다. 그래서 위에서부터 순서대로 검사된다- 범위 조건은 좁은 것을 먼저 둔다. 순서가 틀리면 도달하지 못하는 가지가 생긴다
- 마지막
else는 나머지 전부를 받는다. 특정 값을 처리하려면 조건을 명시한다 - 중괄호를 생략하면 줄을 추가할 때 조건 밖으로 새어 나간다
- 값만 고르는 분기는 삼항 연산자, 중첩이 깊어지면 가드 절로 먼저 빠져나간다
- 의미 있는 정수 상수는
enum으로 바꾼다