포스트

(C#) 7. 코드의 흐름 제어 (while)

조건이 참인 동안 반복하는 while과, 최소 한 번은 실행하는 do-while의 차이. 입력 루프에서 ReadLine이 null을 돌려줄 때 무한 루프가 되는 문제까지 확인했다.

(C#) 7. 코드의 흐름 제어 (while)

같은 코드를 여러 번

6편의 가위바위보는 한 판 하고 프로그램이 끝난다. 다시 하려면 실행을 다시 해야 한다. 반복이 필요하다.

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
using System;

namespace WhileEX
{
    class Program
    {
        static void Main(string[] args)
        {
            // while 반복문
            int count = 5;
            string answer;

            while (count > 0)
            {
                Console.WriteLine("Hello World!");
                count--;
            }

            do
            {
                Console.WriteLine("(y/n) : ");
                answer = Console.ReadLine();
            } while (answer != "y");

            Console.WriteLine("end");
        }
    }
}

조건을 언제 검사하는가

whiledo-while의 차이는 조건을 먼저 보느냐 나중에 보느냐다.

1
2
3
4
5
int count = 0;

while (count > 0) { }      // 한 번도 실행되지 않는다

do { } while (count > 0);  // 한 번은 실행된다

이 차이가 어디서 의미가 있는지는 아래 입력 루프에서 바로 보인다. 사용자에게 물어보려면 일단 한 번은 물어봐야 하고, 그 답을 봐야 다시 물을지 정할 수 있다. while로 쓰면 조건에 넣을 answer가 아직 없어서 어색해진다.

1
2
3
4
5
6
7
// while 로 쓰면 이렇게 된다
string answer = "";
while (answer != "y")
{
    Console.WriteLine("(y/n) : ");
    answer = Console.ReadLine();
}

answer를 억지로 ""로 초기화해야 한다. 조건을 만족시키기 위한 가짜 초기값이라 의미가 없다. do-while이 이 상황에 맞는 문법이다.

조건을 바꾸는 코드를 빠뜨리면

1
2
3
4
5
while (count > 0)
{
    Console.WriteLine("Hello World!");
    count--;         // 이게 없으면 영원히 돈다
}

count--를 빼면 count가 계속 5라서 조건이 절대 거짓이 안 된다. 화면이 순식간에 채워지고 CPU 하나를 다 쓴다.

while을 쓸 때는 조건에 나오는 변수가 루프 안에서 바뀌는지를 확인하는 습관이 생겼다. 이걸 한 줄로 묶어주는 게 다음 편의 for다.

ReadLine 이 null 이면 빠져나오지 못한다

do-while 쪽에서 실제로 걸린 문제다.

Console.ReadLine()은 입력이 끝나면 null을 돌려준다. 콘솔에서 Ctrl+Z(리눅스는 Ctrl+D)를 누르거나, 파일을 파이프로 넣었는데 그 안에 y가 없으면 그렇게 된다.

null != "y"는 참이다. 그래서 조건이 계속 만족되고, 다시 ReadLine을 부르면 또 null이 온다. 화면에 (y/n) :만 무한히 찍히면서 멈추지 않는다.

1
2
3
4
5
6
7
string answer;
do
{
    Console.Write("(y/n) : ");
    answer = Console.ReadLine();
    if (answer == null) return;      // 입력이 끝났다
} while (answer != "y");

while 조건에서 끝내기 어려운 경우라 루프 안에서 빠져나가게 했다. 조건이 두 개 이상이 되면 while (true) + break 쪽이 오히려 읽기 편할 때가 많다.

1
2
3
4
5
6
7
8
9
10
11
while (true)
{
    Console.Write("(y/n) : ");
    string answer = Console.ReadLine();

    if (answer == null) return;
    if (answer == "y") break;
    if (answer == "n") { Console.WriteLine("종료합니다."); return; }

    Console.WriteLine("y 또는 n 을 입력하세요.");
}

조건을 한 줄에 욱여넣지 않고 각각의 경우를 나열하니 무엇을 처리하고 무엇을 안 하는지가 보인다.

대문자 Y 를 입력하면 안 끝난다

answer != "y"는 정확히 소문자 y만 통과시킨다. Y를 치면 계속 물어본다. 사용자 입장에서는 왜 안 되는지 알 수 없다.

1
if (string.Equals(answer, "y", StringComparison.OrdinalIgnoreCase)) break;

answer.ToLower() == "y"로도 되지만, 문화권에 따라 대소문자 규칙이 다른 경우가 있어서 비교 목적이면 OrdinalIgnoreCase가 안전하다는 걸 나중에 알았다. 앞뒤 공백도 흔하니 Trim()을 같이 걸어두면 덜 성가시다.

while(true) 를 게임 루프로 쓸 때

게임은 결국 “입력 받고, 갱신하고, 그리고”를 계속 반복하는 구조다.

1
2
3
4
5
6
while (true)
{
    ProcessInput();
    Update();
    Render();
}

이렇게만 두면 CPU 코어 하나를 100% 쓴다. 콘솔 프로그램에서는 Console.ReadLine()이 입력을 기다리며 막아주니 문제가 안 되는데, 입력을 기다리지 않는 루프면 직접 쉬어줘야 한다.

1
System.Threading.Thread.Sleep(16);   // 대략 60 FPS

정확한 프레임 제어는 이보다 복잡하지만, 처음에는 이 정도만 알아도 노트북 팬이 도는 걸 막을 수 있다.

정리하면

  • while은 조건을 먼저 보고, do-while은 한 번 실행한 뒤에 본다
  • 사용자에게 물어보고 답에 따라 반복할지 정하는 구조는 do-while이 자연스럽다
  • 조건에 쓰인 변수가 루프 안에서 바뀌는지 확인한다. 안 바뀌면 무한 루프다
  • Console.ReadLine()null을 돌려줄 수 있고, null != "y"는 참이라 루프를 빠져나오지 못한다
  • 종료 조건이 여러 개면 while (true) + break가 읽기 쉽다
  • 문자열 비교는 대소문자와 공백을 고려한다
  • 입력을 기다리지 않는 무한 루프는 CPU를 다 쓴다
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.