본문 바로가기
공부/C#

이것이 C#이다 Chapter 05 코드의 흐름 제어하기

by bokob 2024. 2. 25.

5.1 분기문

분기문(Branching Statement)은 프로그램의 흐름을 조건에 따라 여러 갈래로 나누는 흐름 제어 구문

C# 에서는 if, swith 두 가지 분기문 제공

 

1. if, else, else if

// if 문
if(조건식)
    참인_경우에_실행할_코드;
    
if(조건식)
{
    // 참인 경우에
    // 실행할
    // 코드
}

// if-else 문
if(조건식)
    참인_경우에_실행할_코드;
else
    거짓인_경우에_실행할_코드;
    
// if - else if - else 문 
if(조건식1)
    조건식1이_참인_경우에_실행할_코드;
else if(조건식2)
    조건식1이_거짓이고_조건식2가_참인_경우에_실행할_코드;
else
    위의_조건식들이_거짓인_경우에_실행할_코드;

 

2. if 문 중첩해서 사용하기

분기문이나 반복문 같은 흐름 제어문은 또 다른 흐름 제어문을 중첩해서 사용할 수 있다.

보통은 다름 종류의 흐름 제어문을 중첩하는 편인데, 유독 if문과 for문 등은 같은 종류의 흐름 제어문을 자주 중첩해서 사용하는 편이다.

if(number > 0)
{
    if(number % 2 == 0)
        Console.WriteLine("0보다 큰 짝수");
    else
        Console.WriteLine("0보다 큰 홀수");
}
else
{
    Console.WriteLine("0보다 작거나 같은 수");
}

'중첩'을 남발하면 가독성이 떨어진다. 내가 아닌 다른 누구라도 쉽게 읽을 수 있는 코드가 가장 좋은 코드다.

이를 위해서는 단순하고 명료하게 작성하도록 노력해야 한다.

컴퓨터가 이해할 수 있는 코드는 어느 바보나 다 짤 수 있다.
좋은 프로그래머는 사람이 이해할 수 있는 코드를 짠다.
- 마틴 파울러

 

3. switch 문

switch(조건문)
{                       // case부터 break 까지를 swith절 이라고 한다.
    case 패턴1:         // switch 레이블
        // 실행할 코드
        break;
    case 패턴1:
        // 실행할 코드
        break;
    case 패턴N:
        // 실행할 코드
        break;
    default:
        // 실행할 코드
        break;
}

 

switch 레이블(case로 시작되고 :로 끝나는 부분)의 패턴과 일치하는지를 검사하고, 그 레이블 아래에 있는 코드를 실행한다. 필요에 따라 break문 대신 goto나 return, throw 같은 점프문을 사용하는 것이 문법적으로 허용된다.

 

switch 문과 패턴 매칭

switch 레이블의 패턴에는 상수와 타입을 비롯해서 관계, 논리 등 C#이 지원하는 다양한 패턴을 적용할 수 있다.

using System;

namespace PracticeCSharp
{
    class Program
    {
        static void Main(string[] args)
        {
            object obj = null;

            string s = Console.ReadLine();
            if (int.TryParse(s, out int out_i))
                obj = out_i;
            else if (float.TryParse(s, out float out_f))
                obj = out_f;
            else
                obj = s;

            switch(obj)
            {
                case int:
                    Console.WriteLine($"{(int)obj}는 int 형식");
                    break;
                case float:
                    Console.WriteLine($"{(float)obj}는 float 형식");
                    break;
                default:
                    Console.WriteLine($"{obj}(은)는 int 형식");
                    break;
            }
        }
    }
}

 

TryParse() vs Parse()

C#에서 제공하는 기본 숫자 타입은 문자열을 숫자로 변환하는 Parse() 메소드를 제공한다.

숫자 타입은 Parse() 뿐만 아니라 TryParse() 메소드도 제공한다. 같은 기능을 한다.

차이는 Parse() 메소드는 변환이 실패하면 예외(Exception)을 던진다. 예외가 던져지면 프로그램은 현재 코드의 실행을 멈추고 흐름을 다른 곳으로 이동하게 된다.

TryParse() 메소드는 변환의 성공 여부를 반환하기 때문에 현재의 코드 흐름을 유지할 수 있다. TryParse()가 변환한 데이터는 두 번째 매개변수에 저장된다.

 

케이스 가드

케이스 가드(Case Guard)는 switch 문의 case 절의 패턴을 더 구체적으로 만들어주는 추가적인 조건 검사라고 할 수 있다.

case 절 뒤에 when 절을 붙여 사용한다. when 절은 if문처럼 true/false로 결과가 나오는 식을 입력받는다.

switch(obj)
{
    case int:
        Console.WriteLine($"{(int)obj}는 int 형식");
        break;
    case float f when f >= 0:
        Console.WriteLine($"{f}는 양의 float 형식");
        break;
    case float:
        Console.WriteLine($"{(float)obj}는 음의 float 형식");
        break;
    default:
        Console.Write($"{obj}(은)는 모르는 형식");
        break;
}

 

4. switch 식

switch 식(Expression) 문(Statement)과 식은 다르다. 식(Expression)은 결과값을 만들어낼 수 있는 연산자와 연산자의 조합이다. 즉, 식은 계산을 통해 결과를 내놓는다. 이에 비해 문(Statement)은 결과를 내든 내지 않든 주어진 일을 할 뿐이다.

어떤 작업에 분기가 필요할 때는 switch 문을 사용하되, 분기를 거쳐 값을 내야 하는 경우에는 switch 식을 사용하면 가독성이 좋아진다.

 

swtich 문에서 조건식을 switch 키워드 앞으로 옮긴다.

case 키워드와 : 을 '=>' 로 바꾼다. break;는 불필요하다. 각 케이스는 콤마(,)로 구분

default 키워드는 '_'로 바꾼다. 이를 무시 패턴이라고 부른다.

int input = Convert.ToInt32(Console.Readline());

// 1의 자리를 버림 ex) 92->90, 87->80
int score = (int)(Math.Truncate(input/10.0) * 10);

string grade = score switch
{
    90 => "A",
    80 => "B",
    70 => "C",
    60 => "D",
    _ => "F"
};

 

switch 식에서도 케이스 가드를 이용할 수 있다.

bool repeated = true;

string grade = score switch
{
    90 when repeated == true => "B+", // score가 90이어도 repeated가 true이면 B+
    90 => "A",
    80 => "B",
    70 => "C",
    60 => "D",
    _ => "F"
};

 

5.2 반복문

반복문(Loop Statement)은 특정 조건을 만족하는 동안 코드 또는 코드 블록을 반복해서 실행하도록 하는 문장

C#은 모두 다음 네 가지의 반복문을 제공한다.

  • while
  • do while
  • for
  • foreach

1. while

C#의 while 문은 조건식이 참인 동안 코드를 반복 실행한다.

while(조건식)
    반복_실행할_코드

 

2. do while

while  문이 조건식을 평가한 후 그 결과가 참이면 코드를 실행하지만, do while 문은 조건식을 평가하기 전에 무조건 처음 한 번은 코드를 실행한다.

do
{
    반복_실행할_코드_블록 // 이 코드 블록은 최초 한 번은 무조건 실행된다.
}while(조건식);          // ';'가 있어야 한다.

do while 문 끝에 세미콜론(;)을 반드시 붙여줘야 한다. 그렇지 않으면 컴파일러가 해당 구문을 인식하지 못하고 에러 메시지를 출력한다.

 

3. for

while 문처럼 조건식이 참인 동안 코드를 반복 실행하지만, whlie 문보다 반복을 더 정교하게 제어할 수 있다.

for(초기화식; 조건식; 반복식;)
    반복_실행할_코드;

 

1) 초기화식

반복을 실행하기 전에 가장 먼저, 딱 한 번만 실행되는 코드. for 문에서 사용할 변수 등을 이곳에서 초기화한다.

 

2) 조건식

반복을 계속 수행할지를 결정하는 식. 이 조건식의 결과가 false가 되면 반복을 중단한다.

 

3) 반복식

반복이 끝날 때마다 실행된다. 주로 여기서 조건식에서 사용하는 변수의 값을 조정한다. 반복식이 실행된 후에는 조건식이 실행되어 반복을 계속 진행할지를 판단한다.

 

4. 중첩 for 

다른 반복문들도 중첩이 가능하지만, for문이 다른 반복문보다 반복 제어를 위한 장치가 더 잘 갖춰져 있기 때문에 가장 적합하다고 할 수 있다.

for(int i=0; i<5; i++)
    for(int j=0; j<10; j++)
        Console.WriteLine("Hello");

 

5. foreach

foreach 문은 배열(또는 컬렉션)을 순회하며 각 데이터 요소에 차례대로 접근하도록 해준다.

편리하게 배열(또는 컬렉션)의 끝에 도달하면 자동으로 반복이 종료된다.

foreach(데이터_타입 변수명 in 배열_또는_컬렉션)
    코드_또는_코드_블록

foreach 문은 in 키워드와 함께 사용한다. foreach 문이 한 번 반복을 수행할 때마다 배열(또는 컬렉션)의 요소를 차례대로 순회하면서 in 키워드 앞에 있는 변수에 담아준다.

int[] arr = new int[]{0, 1, 2, 3, 4};

foreach (int a in arr)
{
    Console.WriteLine(a);
}

 

6. for 또는 while을 이용한 무한 반복 코드

for( ; ;)
    // 반복 실행할 코드 블록

 

for 문으로만 무한 반복(Infinite Loop) 코드를 만들 수 있는 것은 아니다. while  문이나 do while 문으로도 무한 반복 코드를 만들 수 있다. 조건식이 항상 참이되게 하면 된다.

while(true)
    // 반복 실행할 코드 블록

 

무한 반복 프로글매은 Ctrl + C 키를 누르거나 작업 관리자에서 프로세스 이름을 찾아 강제 종료시키면 된다.

 

5.3 점프문

점프문(Jump Statement)은 흐름을 끊고 프로그램의 실행 위치를 원하는 곳으로 도약시킬 수 있다.

C#이 제공하는 점프문에는 다음과 같이 다섯 가지가 있다.

  • break
  • continue
  • goto
  • return (다른 장에서 다룸)
  • throw (다른 장에서 다룸)

1. break 

break 문은 이름처럼 현재 실행 중인 반복문이나 switch 문의 실행을 중단하고자 할 때 사용

반복문이나 switch 문의 중단시키려는 지점에 break; 를 입력해두면 된다.

int i=0;
while(i>=0)
{
    if(i==10)
        break;
        
    Console.WriteLine(i++);
}
Console.WriteLine("Prison Break"); // break 문을 통해 탈출하면 탈출한 코드 블록 다음으로 이동한다.

 

2. continue

continue 문은 한 회 건너 뛰어 반복을 계속 수행하게 하는 기능을 한다.

for(int i=0; i<5; i++)
{
    if(i==3)
        continue; // 밑의 코드는 실행하지 않고 건너띄게 된다.
        
    Console.WriteLine(i);
}

continue 문이 사용된 코드는 보면 그 아랫줄은 더 읽어보지 않아도 해당 코드 블록의 실행이 취소됨을 알 수 있다.

 

3. goto

goto 레이블;

레이블: // 레이블을 선언할 때는 콜론(:)을 붙인다.
    // 이어지는 코드

 

레이블(Label)은 변수하고는 좀 다른데, 코드 안의 위치를 나타내는 표지판 같은 존재다.

goto 문은 레이블이 가리키는 곳으로 바로 뛰어 넘어간다.

Console.WirteLine(" 1 ");

goto JUMP;

Console.WriteLine(" 2 ");
Console.WriteLine(" 3 ");

JUMP:
Console.WriteLine(" 4 ");

 

컴퓨터 과학계에서는 'goto의 해악'을 주제로 하는 논문이 여러 편 발표되었다. 하지만 유요한 경우가 있는데, 중첩된 반복문을 단번에 뚫고 나오기 위해 사용하는 것이다.

for(int i=0; i<100; i++)
{
    for(int j=0; j<200; j++)
    {
        for(int k=0; k<5-; k++)
            if(i == 0 && k==3)
                goto EXIT_FOR;
    }
}

EXIT_FOR:
    Console.WriteLine("Exit");

 

5.4 패턴 매칭

패턴 매칭(Pattern Matching)은 하스켈(Haskell)이나 엘릭서(Exlir) 같은 함수형 언어에서 사랑받는 기능 중 하나다.

많은 C# 프로그래머가 C#에 도입할 것을 무수히 요청한 기능이기도 하다.

C# 표준 위원회는 프로그래머의 요청을 받아들여 C# 7.0에 패턴 매칭을 도입했다. 지금까지도 꾸준히 업데이트하고 있다.

 

패턴 매칭은 어떤 식(Expression)이 특정 패턴(Pattern)과 일치(Matching)하는지를 검사한다.

패턴 매칭을 이용하면 장황하고 거추장스러운 분기문을 간결하고 읽기 쉬운 코드로 대체할 수 있다.

패턴 매칭은 식을 입력받고 일치 여부를 반환한다.

 

패턴 매칭은 switch 문/식 만을 위한 것은 아니다.

패턴 매칭은 switch 문과 switch 식과 관련이 깊지만, is 연산자를 이용하면 다른 문장이나 식에서도 사용할 수 있다.

 

식(Expression)이란, 코드에서 단일 결과값을 만들어낼 수 있는 연산자와 연산자의 조합을 말한다.

예를 들어, 1+2는 단일 결과값 3을 만들어내는 식이고, 10도 단일 결과값 10을 만든느 식이다.

다시 말해 리터럴과 상수, 변수는 연산자 없이 식이 될 수 있다.

a = 123;         // 123과 a=123은 식
b = int;         // int는 식이 아님. 해당 문(statement)도 유효하지 않음
c = typeof(int); // c와 c=typeof(int)는 식
d = a + 456;     // a + 456와 d = a + 456은 식

 

'식의 패턴'이라는 말은 '식 결과의 패턴'이라는 말과 같다.

C#이 지원하는 패턴의 종류에는 여러가지가 있다.

  • 선언 패턴
  • 형식 패턴
  • 상수 패턴
  • 프로퍼티 패턴
  • 관계 패턴
  • 논리 패턴
  • 괄호 패턴
  • 위치 패턴
  • var 패턴
  • 무시 패턴
  • 목록 패턴

1. 선언 패턴

주어진 식이 특정 타입(ex: int, string 등)과 일치하는지를 평가한다. 만약 주어진 식과 타입이 일치한다면, 선언 패턴(Declaration Pattern)은 식을 해당 타입으로 변환한다. 다음 두 가지를 수행한다.

 

1) 식이 주어진 타입과 일치하는지 테스트

2) 테스트가 성공하면 식을 주어진 타입으로 변환

 

is  연산자는 왼쪽에 있는 식이 오른쪽에 있는 패턴과 일치하는지를 테스트한다.

object foo = 23;
if (foo is int bar) // 1. foo가 int인 경우 2. foo를 int 형식ㅇ로 반환하여 bar에 할당한다.
{
    Console.WriteLine(bar);
}

foo is int가 true를 반환하는 경우에 bar 변수가 if 블록 안에 생성되고 23이 출력되지만, false를 반환하는 경우에는 생성되지 않는다. bar가 출력되는 일은 없다.

 

2. 타입 패턴

타입 패턴(Type Pattern)은 선언 패턴과 거의 같은 방식으로 동작하지만, 변수 생성 없이 타입 일치 여부만 테스트한다.

C# 9.0에서 더 간략한 타입 패턴 매칭을 지원하기 위해 도입되었다.

object foo = 23;
if (foo is int)
{
    Console.WriteLine(foo);
}

 

3. 상수 패턴

상수 패턴(Constant Pattern)은 식이 특정 상수와 일치하는지를 검사하며, 가장 많이 사용하는 패턴이다.

정수 리터럴과 문자열 리터럴뿐 아니라 null과 enum 등 모든 상수와 매칭할 수 있다.

var GetCountryCode = (string nation) => nation switch
{
    "KR" => 82,
    "US" => 1,
    "UK" => 44,
    _ => throw new ArgumentException("Not supported Code")
};

Console.WriteLine(GetCountryCode("KR"));
Console.WriteLine(GetCountryCode("US"));
Console.WriteLine(GetCountryCode("UK"));

 

4. 프로퍼티 패턴

프로퍼티 패턴(Property Pattern) 매칭은 식의 속성이나 필드가  패턴과 일치하는지를 검사한다.

입력된 식이 int, double 같은 기본 데이터 타입이 아닌 경우에 특히 유용하게 사용할 수 있다.

class Car
{
    public string Model {get; set;}
    public DateTime ProducedAt {get; set;}
}

static string GetNickname(Car car)
{
    var GenerateMessage = (Car car, string nickname) =>
        $"{car.Model} produced in {car.ProducedAt.Year} is {nickname}";
        
    if (car is Car {Model:"Mustang", ProducedAt.Year: 1967})
        return GenerateMessage(car, "Fastback");
    else if (car is Car {Model:"Mustang", ProducedAt.Year: 1976})
        return GenerateMessage(car, "Cobra II");
    else
        return GenerateMessage(car, "Unknown");
}

static void Main(string[] args)
{
    Console.WriteLine(
        GetNickname(
            new Car() {Model = "Mustang", ProducedAt = new DateTime(1967, 11, 23)}));
    
    Console.WriteLine(
        GetNickname(
            new Car() {Model = "Mustang", ProducedAt = new DateTime(1976, 6, 7)}));
    
    Console.WriteLine(
        GetNickname(
            new Car() {Model = "Mustang", ProducedAt = new DateTime(2099, 12, 25)}));
}

 

5. 관계 패턴

관계 패턴(Relational Pattern) 매칭은 >, >=, ==, !=, <, <= 와 같은 관계 연산자를 이용하여 입력받은 식을 상수와 비교한다.

static bool IsPassed(double score) => score switch
{
    < 60 => false,
    _    => true,
};

 

관계 패턴과 논리 패턴의 and를 함께 이용하면 입력받은 식이 특정 범위에 들어오는지를 테스트하는 것도 가능하다.

static string GetGrade(double score) => score switch
{
    < 60 => "F",
    >= 60 and < 70 => "D",
    >= 70 and < 80 => "C",
    >= 80 and < 90 => "B",
    _ => "A",
};

 

6. 논리 패턴

패턴과 패턴을 패턴 논리 연산자(and(결합), or, not)을 조합해서 하나의 논리 패턴(Logical Pattern)으로 만들 수 있다.

class OrderItem
{
    public int Amount {get; set;}
    public int Price {get; set;}
}

static double GetPrice(OrderItem orderItem) => orderItem switch
{
    OrderItem {Amount: 0} or OrderItem {Price: 0}
        => 0.0,
    OrderItem {Amount: >= 100} and OrderItem {Price: >= 10_000}
        => orderItem.Amount * orderItem.Price * 0.8,
    not OrderItem {Amount: < 100}
        => orderItem.Amount * orderItem.Price * 0.9,
    _ => orderItem.Amount * orderItem.Price,
};

static void Main(string[] args)
{
    Console.WriteLine(GetPrice(new OrderItem() {Amount = 0, Price = 10_000}));
    Console.WriteLine(GetPrice(new OrderItem() {Amount = 100, Price = 10_000}));
    Console.WriteLine(GetPrice(new OrderItem() {Amount = 100, Price = 9_000}));
    Console.WriteLine(GetPrice(new OrderItem() {Amount = 1, Price = 1_000}));
}

 

7. 괄호 패턴

괄호 패턴(Parenthesized Pattern)은 소괄호 ()로 패턴을 감싼다. 보통 논리 패턴으로 여러 패턴을 조합한 뒤 이를 새로운 패턴으로 만드는 경우에 사용한다.

object age = 30;

if (age is (int and > 19))
    Console.WriteLine("Major");

 

8. 위치 패턴

위치 패턴(Positional Pattern)은 식의 결과를 분해(Deconstruct)하고, 분해된 값들이 내장된 복수의 패턴과 일치하는지를 검사한다. 위치 패턴 안에 내장되는 패턴에는 어떤 패턴이든 올 수 있다.

단, 분해된 값들과 내장된 패턴의 개수, 순서가 일치해야한다.

Tuple<string, int> itemPrice = new Tuple<string, int>("espresso", 3000);

if(itemPrice is ("espresso", < 5000))
{
    Console.WriteLine("The coffe is affordable.");
}

이 예제에서는 itemPrice는 string과 int 요소로 이루어진 튜플이다. 이 튜플을 상수 패턴("espresso")과 관계 패턴(< 5000)으로 이루어진 위치 패턴으로 매칭하고 있다.

 

struct Audience
{
    public bool IsCitizen {get; init;}
    public int Age {get; init;}
    
    public Audience(bool isCitizen, int age)
    {
        IsCitizen = isCitizen;
        Age = age;
    }
    
    public void Deconstruct(out bool isCitizen, out int age)
    {
        isCitizen = IsCitizen;
        age = Age;
    }
}

static void Main(string[] args)
{
    var CalculateFee = (Audience audience) => audience switch
    {
        (true, < 19) => 100,
        (true, _) => 200,
        (false, < 19) => 200,
        (false, _) => 400,
    };
    
    var a1 = new Audience(true, 10);
    Console.WriteLine(
        $"내국인: {a1.IsCitizen} 나이: {a1.Age} 요금: {CalculateFee(a1)}");
        
    var a2 = new Audience(false, 33);
    Console.WriteLine(
        $"내국인: {a2.IsCitizen} 나이: {a2.Age} 요금: {CalculateFee(a2)}");
}

 

9. var 패턴

var 패턴(var Pattern)은 null을 포함한 모든 식의 패턴 매칭을 성공시키고, 그 식의 결과를 변수에 할당한다.

var 패턴은 어떤 식의 결과를 임시 변수에 할당한 뒤 추가적인 연산을 수행하고자 할 때 유용하게 사용할 수 있다.

// 모든 과목이 60점이 넘고, 평균이 60점 이상인 경우에 Pass
var IsPassed =
    (int[] scores) => scores.Sum() / scores.Length is var average
    && Array.TrueForAll(score, (score) => score >= 60)
    && average >= 60;
    
int[] scores1 = {90, 80, 60, 80, 70};
Console.WriteLine($"{string.Join(",", scores1)}: Pass:{IsPassed(scores1)}");

int[] scores2 = {90, 80, 59, 80, 70};
Console.WriteLine($"{string.Join(",", scores2)}: Pass:{IsPassed(scores2)}");

/* 결과
90,80,60,80,70: Pass:True
90,80,59,80,70: Pass:False
*/

 

10. 무시 패턴

무시 패턴(Discard Pattern)도 var 패턴처럼 모든 식과의 패턴 일치 검사를 성공시킨다. 하지만 var 패턴과 다르게 is 식에서는 사용할 수 없고, switch 식에서만 사용 가능하다. '모든 식'을 매칭할 수 있기 때문에 switch 문의 default 케이스와 비슷한 용도로 사용하면 된다. 무시 패턴은 '_' 기호를 이용한다.

var GetCountryCode = (string nation) => nation switch
{
    "KR" => 82,
    "US" => 1,
    "UK" => 44,
    _ => throw new ArgumentException("Not supported Code") // 무시 패턴 매칭
};

Console.WriteLine(GetCountryCode("KR"));
Console.WriteLine(GetCountryCode("US"));
Console.WriteLine(GetCountryCode("UK"));

"KR", "US", "UK" 이 세 개 상수와 일치하지 않는 모든 식의 값을 _에 매칭시켜 ArgumentException을 던진다.

 

11. 목록 패턴

목록 패턴(List Pattern)은 배열이나 리스트(List)가 패턴의 시퀀스가 일치하는지를 검사한다. 패턴의 시퀀스는 대괄호([]) 사이에 패턴의 목록을 입력해서 만든다.

var match = (int[] array) => array is [int, >10, _];

Console.WriteLine(match(new int[] {1, 100, 3}));    // True
Console.WriteLine(match(new int[] {100, 10, 999})); // False

 

범위 패턴 '..'을 같이 사용하면 식으로 입력되는 배열이나 리스트의 길이에 관계없이 패턴 매칭을 할 수 있다.

예를 들어, [int, > 10, ..]은 첫 두 요소에 대해서만 패턴이 일치하는지를 검사하고 세 번째 이후의 요소에 대해서는 길이를 포함해서 검사를 수행하지 않는다.

var match = (int[] array) => array is [int, >10, ..];

Console.WriteLine(match(new int[] {1, 100, 101, 102, 103, 104}));    // True
Console.WriteLine(match(new int[] {100, 10, 999}));                  // False

 

목록 패턴은 다량의 데이터를 처리할 때 유용하다. 특히 파일이나 데이터베이스에서 레코드를 읽어 처리하는 문제에 적합하다.