.NET/C#

[C# 11 과 .NET 7] 3. 흐름제어, Type 변환, 예외 처리

클리엘 2023. 5. 20. 15:56
728x90

이번에는 변수를 사용한 간단한 동작과 분기, pattern matching 수행, 구문 혹은 block의 반복과 여러 값을 저장하기 위한 array, 특정 type에서 다른 type으로의 변수나 표현식에 대한 변환, 예외 처리 그리고 숫자형 변수에 대한 overflow를 확인하기 위한 방법 등에 관해서 알아볼 것입니다.

 

1. 변수 연산

 

연산자는 변수나 literal값과 같은 피연산자에서 덧셈이나 곱셈과 같은 계산을 수행하는 것을 말합니다. 보통은 연산결과에 대한 새로운 값을 반환하며 이를 다른 변수에 할당하는 과정이 있을 수 있습니다.

 

대부분의 연산자는 2진연산자로서 아래 예제와 같이 2개의 피연산자를 필요로 합니다.

var result = firstOperand operator secondOperand;

이와 같은 2진연산자는 대표적으로 덧셈이나 곱셈등을 들 수 있습니다.

int x = 5;
int y = 3;
int resultOfAdding = x + y;
int resultOfMultiplying = x * y;

다른 연산자로 단항연산자가 있으며 단일 연산자를 필요로 하고 연산자 앞이나 뒤에 적용합니다.

var resultOperationAfter = onlyOperand operator;
var resultOperationBefore = operator onlyOperand;

위와 같은 방식으로 단항연산자는 변수의 증감이나 감산에 많이 사용되며 type을 확인하는 typeof, byte size를 확인하는 sizeof와 같은 연산자도 사용할 수 있습니다.

int x = 5;
int postfixIncrement = x++; //단항연산자의 후위적용
int prefixIncrement = ++x; //단항연산자의 전위적용

Type theTypeOfAnInteger = typeof(int); //type 가져오기
string nameOfVariable = nameof(x); //name 가져오기
int howManyBytesInAnInteger = sizeof(int); //size 가져오기

3항 연산자는 3개의 피연산자를 필요로 하며 아래와 같이 사용됩니다.

var result = firstOperand firstOperator secondOperand secondOperator thirdOperand;

(1) 단항 연산자

 

단항 연산자로 사용되는 가장 대표적인 사례로 1 증감을 나타내는 ++, 1 감소를 나타내는 --연산자가 있습니다. 예제를 작성해 보면서 실제 해당 연산자가 어떻게 작동하는지 확인해 보도록 하겠습니다.

 

Visual Studio에서 'csStudy03' solution을 생성하고 여기에 'Operators'라는 이름의 project를 추가합니다. 그리고 .csproj project file을 열어 <PropertyGroup> section다음에 <ItemGroup> section을 아래와 같이 추가하여 System.Console에 대한 정적 import를 적용합니다.

<ItemGroup>
	<Using Include="System.Console" Static="true" />
</ItemGroup>

Program.cs에서 기존의 구문을 모두 제거하고 a와 b이름으로 2개의 integer 변수를 선언합니다. 그리고 a변수에 3을 설정하고 다시 b변수에 a에 대한 증감을 할당합니다. 그리고 해당 변수의 값을 확인할 수 있도록 아래와 같이 code를 작성합니다.

int a = 3;
int b = a++;

WriteLine($"a is {a}, b is {b}");

console app을 동작시키기 전에, b의 값이 어떻게 될것인지를 추측해 보시기 바랍니다. 그리고 project를 실행하여 실제 나온 값과 예상한 값을 비교해 보시기 바랍니다.

실행결과 변수 b는 3의 값을 가지게 됩니다. 왜냐하면 ++연산자는 a변수의 후위에서 증감이 적용되었기 때문이며 이를 후위 연산자라고 합니다. 따라서 만약 b변수로 할당 전에 증감이 이루어져야 한다면 변수 앞에 적용되는 전위연산자를 사용해야 합니다.

 

위 예제 뒤에 아래의 code를 추가합니다. 해당 예제에서는 ++를 전위로 적용한 것입니다.

int c = 3;
int d = ++c;

WriteLine($"c is {c}, d is {d}");

code를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

변수의 할당과정에서 값을 증감하거나 감산하는 전위, 후위사이의 혼란스러움 때문에 Swift 언어 설계자는 해당 연산자의 지원을 version 3에서 제외하기로 하였습니다. C#에서 사용할때도 되도록이면 ++와 --연산자를 할당연산자인 =과 함께 사용하는 것을 피하고 이들 구문을 분리하여 연산을 수행하시기 바랍니다.

 

(2) 산술연산을 위한 2진연산자

 

증감과 감소는 단항 산술 연산자입니다. 다른 산술연산자로는 대게 2진 연산자를 의미하며 간단하게는 2개의 수에 대한 산술을 수행할 수 있습니다.

 

e와 f라는 2개의 int변수를 선언하고 각각의 변수에 값을 할당하는 구문을 추가합니다. 그리고 두 수에 대한 다섯 가지 산술연산을 수행하는 2진 연산자를 아래와 같이 적용합니다.

int e = 11;
int f = 3;

WriteLine($"e is {e}, f is {f}");
WriteLine($"e + f = {e + f}");
WriteLine($"e - f = {e - f}");
WriteLine($"e * f = {e * f}");
WriteLine($"e / f = {e / f}");
WriteLine($"e % f = {e % f}");

이미 알고 있는 것처럼 나머지(%)는 값을 나눈 후 남은 수를 의미합니다. 11개의 사탕과 3명의 친구가 있다면 각각 3개씩 나눠줬을 때 2개가 남게 되는 것입니다. 만약 사탕이 12개라면 이때는 각각 4개씩 나눠줄 수 있고 남는 사탕은 없게 됩니다.

 

이번에는 아래와 같이 g라는 이름의 double변수를 선언하고 값을 할당합니다. 이를 통해 정수와 실수의 나눗셈에 대한 차이를 보고자 합니다.

double g = 11.0;
WriteLine($"g is {g:N1}, f is {f}");
WriteLine($"g / f = {g / f}");

project를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

처음 피연산자가 11.0 값을 가진 g처럼 부동소수점 수라면 나눗셈의 결과로 정수대신 3.6666666666665과 같은 부동소수점을 반환하게 됩니다.

 

(3) 할당 연산자

 

지금까지 예제를 작성해 오면서 이미 가장 기본적인 할당연산자인 =를 사용해 왔습니다.

 

또한 할당연산자인 =문자를 사용하면 산술연산자와 같은 다른 연산자와의 결합을 통해서 아래와 같이 code를 더욱 간소화시킬 수 있습니다.

int p = 6;
p += 3;
p -= 3;
p *= 3;
p /= 3;

(4) 논리 연산자

 

논리 연산자는 Boolean값을 판단하므로 true 혹은 false 중 하나를 반환하게 됩니다. 예제를 통해 2개의 Boolean값을 판단하는 2진 논리연산자를 사용해 보도록 하겠습니다.

 

csStudy03 solution에서 'BooleanOperators'이름의 project를 추가한 뒤 해당 project의 .csproj file에서 System.Console namespace의 정적 import를 설정합니다.

 

Program.cs에서 기존의 구문을 모두 삭제하고 true와 false값을 가진 2개의 Boolean변수를 선언하는 구문을 추가합니다. 그리고 이들에 대해 AND, OR, XOR(배타적:exclusive OR) 논리 연산자를 적용한 결과를 표시할 수 있도록 수정합니다.

bool a = true;
bool b = false;

WriteLine($"AND | a     | b ");
WriteLine($"a   | {a & a,-5} | {a & b,-5}");
WriteLine($"b   | {b & a,-5} | {b & b,-5}");
WriteLine();
WriteLine($"OR  | a     | b ");
WriteLine($"a   | {a | a,-5} | {a | b,-5}");
WriteLine($"b   | {b | a,-5} | {b | b,-5}");
WriteLine();
WriteLine($"XOR | a     | b ");
WriteLine($"a   | {a ^ a,-5} | {a ^ b,-5}");
WriteLine($"b   | {b ^ a,-5} | {b ^ b,-5}");

project를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

AND(&) 논리연산자에서는 피연산자 모두가 true가 되어야만 결과는 true가 됩니다. OR(|) 논리연산자에서는 둘 중 하나만 이라도 true가 되면 결과는 true가 됩니다. XOR(^) 논리연산자에서는 피연산자가 true여야만 결과가 true가 되지만 모든 피연산자가 true여서는 안 됩니다.

 

(5) 조건부 논리 연산자

 

조건부 논리 연산자는 논리 연산자와 비슷하지만 & 대신 &&나 | 대신 ||로 하나 대신 2개의 기호문자를 사용하며 다른 말로 단락 부울 연산자라고도 합니다.

 

아래 예제는 Program.cs의 뒤에 추가한 것으로 하나의 값을 반환하는 method이며 값은 Boolean 연산에 사용되는 true Boolean값입니다.

static bool bDo()
{
    WriteLine("...somthing...");
    return true;
}

그다음 a와 b변수에 대해 And연산을 수행하는 다음 구문을 이어서 추가합니다.

WriteLine();
WriteLine($"a & bDo() = {a & bDo()}");
WriteLine($"b & bDo() = {b & bDo()}");

project를 실행하면 a에서 한번 b에서 한번 이렇게 method는 2번 호출되어 다음과 같은 결과를 표시하게 됩니다.

이번에는 위 구문을 그대로 복사하여 & 대신 &&로 변경합니다.

WriteLine();
WriteLine($"a & bDo() = {a && bDo()}");
WriteLine($"b & bDo() = {b && bDo()}");

다시 project를 실행하면 method는 a변수에 대해서만 동작한 결과를 표시합니다. b 변수는 처음부터 false이기 때문에 method는 동작하지 않으며 동작유무와는 상관없이 항상 결과가 false가 되므로 method의 식이 평가될 필요가 없는 것입니다.

이로서 조건부논리연산자가 어떻게 단락 되는 것으로 설명되는지를 알 수 있을 것입니다. 이를 통해 app을 좀 더 효휼적으로 만들 수 있지만 예제에서 처럼 method가 항상 호출된다는 가정이 세워지면 예상치 못한 bug를 유발할 수도 있습니다. 특정 method와의 결합이 이루어져야 한다면 그에 따른 부작용에 주의해야 합니다.

 

(6) bit 및 이진 shift 연산자

 

bit 연산자는 숫자의 bit에 영향을 주며 이진 shift 연산자는 일반적인 연산자보다 더 빠르게 일반적인 산술계산을 수행할 수 있습니다.

 

csStudy03 solution에서 BitwiseAndShiftOperators project를 추가합니다. Program.cs에서 기존의 구문을 모두 삭제한 뒤 아래와 같이 2개의 integer변수를 선언하고 각각 10과 6의 값을 할당합니다. 그리고 이에 대한 AND, OR, XOR 비트 연산자를 적용한 결과를 표시합니다.

int a = 10;
int b = 6;

Console.WriteLine($"a = {a}");
Console.WriteLine($"b = {b}");
Console.WriteLine($"a & b = {a & b}");
Console.WriteLine($"a | b = {a | b}");
Console.WriteLine($"a ^ b = {a ^ b}");

project를 실행하면 다음과 같은 결과를 표시할 것입니다.

이어서 Program.cs에 변수 a bit를 3 column 이동시키는 left-shift 연산자와 8 곱하기 그리고 변수 b의 bit를 1 column이동시키는 right-shift 연산자를 적용하고 그 결과를 표시하도록 합니다.

// 01010000 bit를 왼쪽으로 3 column 이동
Console.WriteLine($"a << 3 = {a << 3}");
// 곱하기 8
Console.WriteLine($"a * 8 = {a * 8}");
// 00000011 bit를 오른쪽으로 1 column 이동
Console.WriteLine($"b >> 1 = {b >> 1}");

project를 실행하고 결과를 확인합니다.

80은 변수의 bit가 왼쪽으로 3 column이동되면서 나타난 결과입니다. 1은 64와 16bit column으로 이동되었으므로 64+16=80이 되는 것입니다. 또한 이것은 8 곱하기 결과와 동일하지만 CPU는 bit-shift를 더 빠르게 수행할 수 있습니다. 3 결과는 b변수의 bit가 1 column이동된 것이며 2와 1 bit column에 1이 위치하므로 결과는 2+1=3이 됩니다.

integer 값을 연산할 때 &와 |는 bit연산자가 되며 true와 false같은 Boolean값을 연산할때 &와 |는 논리 연산자가 됩니다.

위와 같은 계산처리는 integer값을 0과 1로 이루어진 binary 문자열로 변환하면 쉽게 확인해 볼 수 있습니다.

 

Program.cs에서 아래와 같이 integer값을 binary(2진수) 문자열로 변환하는 method를 추가합니다. 이때 최대 8개의 0과 1의 2진수로 변환하도록 설정합니다.

static string ToBinaryString(int value)
{
    return Convert.ToString(value, toBase: 2).PadLeft(8, '0');
}

그리고 method위에 a와 b변수에 대한 여러 bit연산에 대한 결과를 표시하도록 합니다.

Console.WriteLine();
Console.WriteLine($"a = {ToBinaryString(a)}");
Console.WriteLine($"b = {ToBinaryString(b)}");
Console.WriteLine($"a & b = {ToBinaryString(a & b)}");
Console.WriteLine($"a | b = {ToBinaryString(a | b)}");
Console.WriteLine($"a ^ b = {ToBinaryString(a ^ b)}");

project를 실행하면 다음과 같은 결과를 표시할 것입니다.

(7) 기타 연산자

 

nameof와 sizeof는 type에 대한 작업 시 편리하게 사용할 수 있는 연산자입니다.

  • nameof : 변수, type, member 등에 대한 name을 반환합니다. 이때 name에서 namespace는 생략되며 문자열로서 값을 반환합니다.
  • sizeof : 해당 type에 필요한 byte의 size를 반환하며 이를 통해 data를 저장하기에 어떤 것이 효율적인지는 판단할 수 있습니다.
int age = 50;
WriteLine($"The {nameof(age)} variable uses {sizeof(int)} bytes of memory.");

기타 그 외 다양한 연산자들이 존재하는데 예를 들어 변수와 변수의 member사이에 점(.)은 member 접근 연산자라고 하며 method의 이름 끝에서 사용되는 괄호(())는 호출 연산자라고 합니다.

int age = 50;
char firstDigit = age.ToString()[0];

또한 =은 '할당 연산자', 대괄호는 'indexer 접근 연산자'라고 합니다.

 

2. 선택 문

 

대부분의 application은 필요한 처리의 흐름을 선택하고 분기하는 과정을 수없이 반복합니다. C#에서 사용되는 2가지 선택문으로는 if와 switch가 있으며 if는 code의 거의 모든 곳에서 흔하게 사용되는 반면 switch는 단일 변수에서 저장된 값에 따라 각각 다른 처리가 필요한 경우처럼 일반적인 scenario에서 code를 간소화시키는데 많이 사용됩니다.

 

(1) if 문을 통한 분기

 

if 문은 Boolean 식을 평가함으로써 흐름을 분기할지를 결정합니다. 만약 평가결과가 true라면 내부의 block이 실행됩니다. else block은 선택적이며 평가결과가 false인 경우 실행됩니다. 또한 if 구문은 중첩될 수 있습니다.

 

if 구문은 else if문으로 아래와 같이 다른 if 문과 결합될 수 있습니다.

if (expression1)
{
	// true
}
else if (expression2)
{
	// expression1 is false and expression2 if true
}
else if (expression3)
{
	// if expression1 and expression2 are false
	// and expression3 is true
}
else
{
	// if all expressions are false
}

각 if 문의 Boolean 식은 switch 문과는 달리 다른 것과 독립적이며 단일 값의 참조를 필요로 하지 않습니다.

 

csStudy03 solution에서 SelectionStatements project를 생성하고 Program.cs의 모든 구문을 삭제한 다음 password가 최소 6자인지를 확인하는 if 문을 아래와 같이 작성합니다.

string password = "abcde";

if (password.Length < 8)
{
    Console.WriteLine("Your password is too short. Use at least 8 characters.");
}
else
{
    Console.WriteLine("Your password is strong.");
}

project를 실행하면 다음과 같은 결과를 표시할 것입니다.

● if 문에서 괄호를 사용해야 하는 이유

 

각 block안에서는 단일 식만이 존재하기 때문에 위 예제에서는 굳이 괄호가 없어도 code가 아래와 같이 작성될 수 있습니다.

string password = "abcde";

if (password.Length < 8)
    Console.WriteLine("Your password is too short. Use at least 8 characters.");
else
    Console.WriteLine("Your password is strong.");

그러나 if 구문에서 위와 같은 style은 웬만하면 피해야 합니다. code를 읽기 어렵게 만들 수 있고 이 때문에 심각한 bug를 갖게 될 수 있기 때문입니다. 이에 대한 예로 Apple iPhone의 iOS 운영체제에서 발생한 악명 높은 #gotofail bug를 들 수 있습니다.

 

Apple의 iOS 6가 2012년 9월에 releas 된 후 18개월 동안 Secure Sockets Layer (SSL) 암호화 code에 bug가 있었습니다. 해당 bug는 Safari라는 전용 browser를 사용하는 모든 사용자가 영향을 받은 것으로 은행과 같은 보안이 필요한 website의 접속을 시도할 때 적절한 보안이 이루어지지 않았던 것으로 중요한 확인과정이 의도치 않게 지나치게 되면서 발생한 것입니다.

 

괄호를 사용하지 않아도 된다고 해서 그렇게 해야 하는 것은 아닙니다. 괄호를 사용하지 않으면 code가 더 간결해 보일 수는 있으나 code를 유지관리하기 어렵고 잠재적으로 더 위험할 수 있습니다.

 

(2) if 구문에서 Pattern matching 사용

 

C# 7.0 이후에서 도입된 기능으로 pattern matching이 있으며 if 구문에서는 지역변수 선언과 함께 is keyword를 결합하여 pattern matching을 구현함으로써 code를 더 안전하게 만들 수 있습니다. 

 

아래 예제는 o변수에 저장된 값이 int type이라면 값을 i지역변수에 할당합니다. 값이 할당된 i변수는 이후 if구문 안에서 사용됩니다. 이러한 방식은 i변수가 int type이고 다른 type의 값이 할당될 수 없음을 보장할 수 있기 때문에 o변수를 직접적으로 사용하는 것보다 code를 더 안전하게 유지시킬 수 있습니다.

object o = "3";
int j = 4;

if (o is int i)
{
    Console.WriteLine($"{i} x {j} = {i * j}");
}
else
{
    Console.WriteLine("o is not an int so it cannot multiply!");
}

예제를 동작시키면 다음과 같은 결과를 표시합니다.

이번에는 "3"주위에 "를 삭제하여 o변수에 string type대신 값이 int type으로 저장될 수 있도록 수정합니다. 그리고 다시 code를 실행하면 다음과 같은 결과를 표시할 것입니다.

(3) switch 구문에서 분기하기

 

switch 구문은 단일 표현식을 여러 가능한 case구문의 목록과 비교하기 때문에 if 문과는 차이가 있습니다. 모든 case 문은 단일 표현식과 연결되며 모든 case 문은 아래 중 하나로 종료되어야 합니다.

  • break keyword (아래 예제에서 1)
  • goto case keyword (아래 예제에서 2)
  • 문의 생략 (아래 예제에서 3)
  • label 이름을 참조하는 goto keyword (아래 예제에서 5)
  • 현재 처리를 벗어나기 위한 return keyword

아래 예제는 switch 문의 사용예를 표현하고 있습니다. 끝에서 두 번째 문은 건너뛰어 올 수 있는 label문이며 첫 번째 문은 1과 6 사이(code에서 숫자 7은 배타적 상한선입니다.)에 임의의 숫자를 생성하는 문입니다. switch 문의 분기는 이 임의의 숫자를 기반으로 합니다.

int number = Random.Shared.Next(1, 7);
Console.WriteLine($"random number : {number}");

switch (number)
{
    case 1:
        Console.WriteLine("One");
        break;
    case 2:
        Console.WriteLine("Two");
        goto case 1;
    case 3:
    case 4:
        Console.WriteLine("Three or four");
        goto case 1;
    case 5:
        goto A_label;
    default:
        Console.WriteLine("Default");
        break;
}

Console.WriteLine("After end of switch");

A_label:
Console.WriteLine($"After A_label");
goto keyword는 다른 case나 label로 건너뛸 수 있도록 사용할 수 있습니다. 대다수의 개발자는 goto keyword를 별로 좋아하지 않지만 일부 scenario에서는 좋은 해결책이 될 수 있습니다. 하지만 너무 남발해서는 안됩니다.

위 예제를 실행하면 아마도 아래와 같거나 비슷한 결과를 보게 될 것입니다. 다양한 case문에 대한 동작을 확인하려면 예제를 여러 번 실행해 보는 것이 좋습니다.

임의의 숫자를 생성하기 위해 사용한 Random class는 Next method를 가지고 있으며 이를 통해 포함되어야 할 하한선과 제외되어야 할 상한선을 지정하여 무작위 숫자를 생성할 수 있습니다. thread에 안전하지 않은 Random의 새로운 instance를 생성하는 대신 .NET 6부터는 thread에 안전한 Shared instance를 사용할 수 있으므로 모든 thread에서 동시적으로 사용할 수 있습니다.

 

(4) switch 구문에서의 Pattern matching

 

if 문과 마찬가지로 switch 문에서도 C# 7.0 이후부터 pattern matching을 지원합니다. case의 비교값은 더이상 literal값일 필요가 없이 pattern이 될 수 있습니다.

 

C# 7.0이후부터 code는 class의 subtype을 기반으로 더욱 간결하게 분기할 수 있으며 이를 안전하게 사용하기 위해 지역변수를 선언하고 할당할 수 있습니다. 추가적으로 case문은 when keyword를 포함하여 더 상세한 pattern matching을 수행할 수 있습니다.

 

지금 만들어볼 예제는 다른 속성을 가진 animal사용자 정의 class 계층 구조를 사용하여 switch문에서 pattern matching을 구현하는 사례입니다.

 

SelectionStatements project에서 Animals.cs라는 이름의 file을 추가하고 아래와 같이 3개의 class를 정의합니다. 이들은  하나의 base class와 2개의 파생 class로 이루어져 있습니다.

class Animal
{
    public string? Name;
    public DateTime Born;
    public byte Gender;
}

class Cat : Animal
{
    public bool IsMunchkin;
}

class Dog : Animal
{
    public bool IsSigorjabjong;
}

Program.cs에서는 animal에 대한 null가능한 array를 선언하고 각 animal이 가진 type과 attribute에 기반하여 message를 표시할 수 있는 구문을 추가합니다.

Animal?[] animals = new Animal?[] {
    new Cat { Name = "Mong", Born = new(year: 2022, month: 8, day: 23), Gender = 1, IsMunchkin = true },
    null,
    new Cat { Name = "kety", Born = new(year: 1994, month: 6, day: 12) },
    new Dog { Name = "Shiba", Born = DateTime.Today, IsSigorjabjong = false },
    new Dog { Name = "Gangaji", Born = DateTime.Today }
};

foreach (Animal? animal in animals)
{
    string message;

    switch (animal)
    {
        case Cat mongCat when mongCat.Gender == 1:
            message = $"mongCat - name : {mongCat.Name} and mele.";
            break;
        case Cat ketyCat when ketyCat.IsMunchkin == false:
            message = $"ketyCat - name {ketyCat.Name} and not munchkin.";
            break;
        case Cat cat:
            message = $"The cat is named {cat.Name}.";
            break;
        default:
            message = $"The animal named {animal.Name} is a {animal.GetType().
            Name}.";
            break;
        case Dog shibaDog when shibaDog.IsSigorjabjong == false:
            message = $"The {shibaDog.Name} dog is shiba.";
            break;
        case null:
            message = "The animal is null.";
            break;
    }

    Console.WriteLine($"switch statement: {message}");
}

animals라는 이름의 array는 Animal? type으로 선언되었으며 Cat이나 Dog와 같은 Animal의 하위 type과 함께 null을 포함하고 있습니다. 예제에서는 다른 속성을 가진 다른 type으로 4개의 Animal instance를 생성하고 있으며 다른 하나는 null입니다. 따라서 project를 실행하면 각 animal을 서술하는 다섯 가지의 message를 볼 수 있을 것입니다.

(5) switch 표현식을 통한 switch문의 간소화

 

C# 8.0 이후부터는 switch 표현식을 통해 switch 문을 간소화할 수 있습니다.

 

대부분의 switch문은 단순하기는 하지만 꽤 많은 typing을 필요로 합니다. 이에 반해 switch 표현식은 type 해야 하는 code를 간소화하는 동시에 모든 case가 단일 변수를 설정할 수 있도록 값을 반환할 수 있도록 설계되었습니다. 이를 위해 switch 표현식에서는 값을 반환함을 나타내는 => lambda를 사용합니다.

 

아래 예제는 switch문을 사용하는 이전 code에서 switch 표현식을 사용하도록 변경한 것이며 이를 통해 switch의 2가지 style을 비교해 볼 수 있습니다.

 

Program.cs아래 foreach loop문에서 animal이 가지고 있는 type과 attribute에 기반해 message를 설정하는 문을 switch 표현식을 사용해 아래와 같이 추가합니다.

message = animal switch
{
    Cat mongCat when mongCat.Gender == 1 => $"mongCat - name : {mongCat.Name} and mele.",
    Cat ketyCat when ketyCat.IsMunchkin == false => $"ketyCat - name {ketyCat.Name} and not munchkin.",
    Cat cat => $"The cat is named {cat.Name}.",
    Dog shibaDog when shibaDog.IsSigorjabjong == false => $"The {shibaDog.Name} dog is shiba.",
    null => "The animal is null.",
    _ => $"The animal named {animal.Name} is a {animal.GetType().Name}."
};

Console.WriteLine($"switch expression: {message}");

이전예제와의 주요 차이점은 case와 break keyword가 제거되었다는 것입니다. _(underscore) 문자는 default를 나타내는 것으로 'discard'라고도 하며 아래 link를 통해 더 자세한 사항을 확인할 수 있습니다.

 

Discards - unassigned discardable variables | Microsoft Learn

 

Discards - unassigned discardable variables

Describes C#'s support for discards, which are unassigned, discardable variables, and the ways in which discards can be used.

learn.microsoft.com

project를 실행하면 다음과 같이 이전 실행결과와 동일함을 알 수 있습니다.

3. 반복문

 

반복문은 while과 for문에서의 조건이 true일 때나 foreach문에서 collection의 각 item에 대한 문의 block을 반복하는 것을 말합니다. 이 중 어느 문을 사용할지에 대한 선택은 문제가 되는 logic을 해결하기 쉬운 것과 개인선호도에 따라 결정됩니다.

 

(1) while 문

 

while문은 boolean 식을 평가하고 결과가 true라면 loop의 반복을 수행합니다. csStudy03 solution에서 IterationStatements라는 이름의 project를 추가합니다. 그리고 Program.cs에서 아래와 같이 기존의 구문을 모두 삭제하고 integer변숫값이 10보다 작으면 반복을 수행하는 while문을 정의합니다.

int x = 0;
while (x < 10)
{
    Console.WriteLine(x);
    x++;
}

project를 실행하면 다음과 같이 0부터 9까지 숫자가 표시될 것입니다.

(2) do 문

 

do 문은 while문과 비슷하지만 boolean식이 block의 위가 아닌 아래에서 평가됩니다. 즉, block은 반드시 한 번은 실행되는 구조를 갖습니다.

 

Program.cs에서 do loop를 정의하는 아래 구문을 작성하고

string? password;
do
{
    Console.Write("Enter your password: ");
    password = Console.ReadLine();
}
while (password != "abcd");

Console.WriteLine("Correct!");

project를 실행하면 다른 조건을 평가하지 않고 제일 먼저 password의 입력을 요구하는 화면이 표시됩니다. 이 상태에서 사용자가 제대로 된 password를 입력할 때까지 사용자의 입력값을 평가하여 password의 입력요구를 계속할지를 결정하게 됩니다.

(3) for 문

 

for문 역시 while문과 비슷하지만 아래 조건식의 결합으로 조건평가에서 더 간결함을 유지할 수 있습니다.

  • 선택적 초기화 표현식 : loop가 시작될 때 단 한번 실행됩니다.
  • 선택적 조건부 표현식 : loop가 시작될 때부터 매 반복마다 실행되며 다른 반복을 계속할지를 확인합니다. 표현식이 true를 반환하거나 표현식이 아예 없는 경우만 제반복을 실행합니다.
  • 선택적 반복자 표현식 : 구문의 끝에서 매 반복마다 실행됩니다. 대게는 변수의 값을 증가하는 데 사용합니다.

대부분의 for는 아래와 같이 정수의 counter에 사용됩니다. 아래 예제는 for문을 통해 1부터 10까지의 숫자를 표시합니다.

for (int i = 1; i <= 10; i++)
{
    Console.WriteLine(i);
}

(4) foreach 문

 

foreach문은 이전 3개의 반복문과는 약간 차이가 있습니다.

 

보통은 array나 collection과 같은 배열에서 각 item에 대한 문의 block을 수행하는 데 사용합니다. 이때 각 item은 대게 읽기 전용이 되며 배열의 구조가 반복과정에서 변경되면(item이 추가되거나 삭제되는 경우 등) 예외가 발생할 수 있습니다.

 

아래 예제는 string 변수에 대한 array를 생성하고 각 변숫값과 변숫값의 길이를 표시합니다.

string[] names = { "hong", "kim", "lee" };

foreach (string name in names)
{
    Console.WriteLine($"{name} has {name.Length} characters.");
}

● foreach문의 내부 작동 방식

 

array나 collection같이 여러 item을 나타내는 type의 작성자는 개발자가 item을 열거하기 위해 foreach구문을 사용할 수 있는지 확인해야 합니다.

 

기술적으로 foreach문은 아래 규칙을 따르는 모든 type에서 사용할 수 있습니다.

  • type은 개체를 반환하는 GetEnumerator이름의 method를 가져야 합니다.
  • 반환된 개체는 Current속성과 MoveNext라는 method를 가져야 합니다.
  • MoveNext method는 Current의 값을 바꾸고 열거할 item이 있다면 true를 없으면 false를 반환해야 합니다.

이러한 기능의 구현을 위해 IEnumerable과 IEnumerable<T> interface가 존재하며 공식적으로 이들 규칙을 정의하고 있지만 compiler는 이들 interface를 구현한 type을 강제하지 않습니다. 다만 해당 interface를 구현한 경우, 예를 들어 위 예제에서 foreach문은 compiler가 아래와 같은 code로 바꾸게 됩니다.

IEnumerator e = names.GetEnumerator();
while (e.MoveNext())
{
    string name = (string)e.Current;
    WriteLine($"{name} has {name.Length} characters.");
}

iterator와 읽기 전용 Current속성을 사용하기 때문에 foreach문에서 선언된 변수는 현재 item의 값을 변경하는 데 사용될 수 없습니다.

 

4. array

 

예를 들어 4명의 어떤 사람에 대한 이름과 같이 같은 Type에 대한 다수의 값을 저장할 때는 array를 사용할 수 있습니다. 앞서 예로 든 사람의 이름이라면 string array를 사용할 수 있을 것입니다.

 

(1) 1차원 array

 

예제로 만들어볼 code는 4개의 string값을 저장하는 array에 대한 memory할당 code입니다. 이때 array에서 string은 0부터 3까지의 index를 가지게 됩니다.(array는 대게 0부터 시작합니다. 따라서 마지막 item의 index는 array의 크기보다 1만큼 작습니다.)

 

실제 array에 이름을 할당한 것을 직접 나타낸다면 다음과 같이 표현할 수 있을 것입니다.

0 1 2 3
hong kim lee choi
모든 array가 0부터 시작한다고 단정해서는 안됩니다. .NET에서 가장 일반적인 array의 type은 szArray이며 1차원 0-index(single-demention zero-indexed) array를 의미합니다. 이런 경우 보통 [] 구문을 사용하지만 .NET에서는 또한 mdArray(다차원:multi-dementional array)를 사용할 수도 있고 이때는 0 하한선을 가지지 않습니다. 흔한 경우는 아니지만 이러한 것도 존재한다는 것을 알고 있어야 합니다.

마지막으로 array는 for문을 사용해 array의 각 item을 순회할 수 있습니다.

 

csStudy03 solution에서 'Arrays'이름의 project를 추가한 뒤 Program.cs에서 기존 구문을 모두 삭제합니다. 그리고 아래와 같이 string값에 대한 array를 선언하고 사용하는 code를 작성합니다.

string[] names = new string[4];

names[0] = "hong";
names[1] = "kim";
names[2] = "lee";
names[3] = "choi";

for (int i = 0; i < names.Length; i++)
{
	Console.WriteLine(names[i]);
}

위와 같은 project를 실행하면 다음과 같은 결과를 볼 수 있습니다.

Array는 memory에 할당될 때 항상 size가 고정되므로 Array에 대한 instance를 생성할 때 얼마나 많은 item을 저장할지를 결정해야 합니다.

 

위에서 array를 선언하고 값을 할당하기까지의 절차대신 array의 초기화 구문을 사용할 수도 있습니다. 위의 예에서 for문 전에 아래와 같이 array를 선언하고 memory에 할당하여 instance를 생성할 수 있는 구문을 추가합니다. 그리고 for문에서 기존 name대신 name2를 순회할 수 있도록 변경합니다.

string[] names2 = new[] { "hong", "kim", "lee", "choi" };

for (int i = 0; i < names2.Length; i++)
{
	Console.WriteLine(names[i]);
}

위의 code를 실행하면 이전의 실행결과와 동일한 결과가 표시됨을 알 수 있습니다.

 

array를 memory에 할당하기 위한 new[] 구문을 사용할 때는 중괄호에 최소 하나 이상의 item을 지정함으로써 compiler가 data의 type을 추론할 수 있게 해야 합니다.

 

(2) 다차원 array

 

여러 string(다른 type도 마찬가지) 값을 저장하기 위해 1차원 array를 사용하는 대신 cube나 심지어 더 높은 차원으로 값을 격자형태로 다뤄야 한다면 다차원 array를 사용해야 합니다.

 

예를 들어 2차원의 경우 저장된 string값을 표현하려면 grid형태로 다음과 같이 나타낼 수 있습니다.

  0 1 2 3
0 car bike airplane bus
1 hong kim lee choi
2 keyboard monitor CPU memory

Program.cs에서 아래와 같이 string값에 대한 2차원 배열을 선언하고 instance 화하는 구문을 작성합니다.

string[,] grid1 = new[,]
{
	{ "car", "bike", "airplan", "bus" },
	{ "hong", "kim", "lee", "choi" },
	{ "keyboard", "monitor", "CPU", "memory" }
};

위의 배열에서 우리는 해당 array에 대한 차원별 index의 상한 값과 하한값을 아래와 같이 method를 통해 확인할 수 있습니다.

Console.WriteLine($"first dimension - Lower bound: {grid1.GetLowerBound(0)}");
Console.WriteLine($"first dimension - Upper bound: {grid1.GetUpperBound(0)}");
Console.WriteLine($"second dimension - Lower bound: {grid1.GetLowerBound(1)}");
Console.WriteLine($"second dimension - Upper bound: {grid1.GetUpperBound(1)}");

따라서 이를 통해 string값을 순회하기 위한 for구문에서 해당 method를 대신 사용할 수 있습니다.

for (int row = 0; row <= grid1.GetUpperBound(0); row++)
{
	for (int col = 0; col <= grid1.GetUpperBound(1); col++)
	{
		Console.WriteLine($"Row {row}, Column {col} value : {grid1[row, col]}");
	}
}

project를 실행하면 다음과 같은 결과를 볼 수 있습니다.

다차원 배열도 마찬가지로 instance를 생성할 때 모든 row와 column에 대한 값을 할당해야 합니다. 그렇지 않으면 compiler error를 보게 됩니다. 만약 지정할 string값이 없다면 string.Empty를 대신 사용할 수도 있습니다. 또는 'string? []'을 통해 null가능한 array를 선언해야 하는 경우 지정할 string값이 없을 때는 'null'을 사용할 수 있습니다.

 

database나 file로부터 값을 읽어 들이는 경우와 같이 array에 대한 초기화 구문을 사용할 수 없는 경우에는 배열 차원의 선언과 값을 지정하고 memory에 할당하는 과정을 분리할 수 있습니다.

string[,] grid2 = new string[3, 4]; // allocate memory
grid2[0, 0] = "car"; // assign values
grid2[0, 1] = "bike";
grid2[2, 3] = "airplan";

차원에 대한 크기를 지정할 때는 배열 index의 상한 값이 아닌 배열의 크기를 지정해야 합니다. 따라서 'new string[3, 4]'와 같은 표현은 배열(array)이 첫번째 차원에서 index의 상한값 2까지 부여되는 3개의 item을 가지고 두번째 차원에서 index의 상한값 3까지 부여되는 4개의 item을 가질 수 있음을 의미하게 됩니다.

 

(3) 가변배열 (jagged array)

 

다차원의 배열이 필요한 상태에서 각 차원에 저장될 item의 다르다면 가변배열이라고 하는 배열에 대한 배열을 정의할 수 있습니다.

 

가변배열을 예로 들어 표로 표현하자면 다음과 같이 표현할 수 있을 것입니다.

0 0 1 2  
car bike airplan
1 0 1 2 3
hong kim lee choi
2 0 1  
keyboard monitor

위와 같은 형태로 가변배열을 직접 생성해 보도록 하겠습니다.

 

Program.cs에서 아래와 같이 가변배열을 선언하고 string값에 대한 instance를 생성하는 구문을 추가합니다.

string[][] jagged = new[]
{
	new[] { "car", "bike", "airplan" },
	new[] { "hong", "kim", "lee", "choi" },
	new[] { "keyboard", "monitor" }
};

위의 예제에서도 역시 가변배열에 각 index의 하한값과 상한 값을 확인할 수 있습니다.

Console.WriteLine("jagged array - Upper bound: {0}", jagged.GetUpperBound(0));

for (int array = 0; array <= jagged.GetUpperBound(0); array++)
{
	Console.WriteLine("array {0} - Upper bound: {1}", arg0: array, arg1: jagged[array].GetUpperBound(0));
}

위의 예제를 실행하면 다음과 같은 결과를 확인할 수 있는데

이전 예제와 동일하게 string값을 순회하기 위한 것으로 위의 결괏값을 응용해 for문을 중첩하여 사용할 수도 있습니다.

for (int row = 0; row <= jagged.GetUpperBound(0); row++)
{
	for (int col = 0; col <= jagged[row].GetUpperBound(0); col++)
	{
		Console.WriteLine($"Row {row}, Column {col}: {jagged[row][col]}");
	}
}

해당 code를 실행한 결과는 다음과 같습니다.

(4) array와 collection에서의 List pattern matching

 

이전에 우리는 이미 각각의 개별적인 개체에 대해 해당 개체의 type과 속성을 통해 어떤 식으로 pattern matching을 지원할 수 있는지를 예제를 통해 보았습니다. 이러한 pattern matching은 array와 collection에서도 사용될 수 있습니다.

 

List pattern matching은 public 한정자의 Length 또는 Count 속성과 int 또는 System.Index매개변수를 사용하는 indexer 가진 모든 type에서 사용할 수 있습니다. indexer에 관해서는 추후에 자세히 알아볼 기회가 있을 것입니다.

 

동일한 switch 구문에서 여러 list pattern을 정의할 때는 더 구체적인 것이 처음에 올 수 있도록 순서를 정렬해야 합니다. 그렇지 않으면 가장 일반적인 pattern은 더 구체적인 pattern과도 일치하게 되고 결국 구체적인 pattern이 도달할 수 없는 상황을 만들 수 있기 때문에 compiler는 오류를 낼 수 있습니다.

 

아래 표에서는 int값에 대한 list에서 list pattern matching을 나타내는 예를 표현하고 있습니다.

[] 빈 array 또는 collection과 일치합니다.
[..] 0을 포함해 값을 가진 item의 array 또는 collection과 일치합니다. 때문에 위의 경우를 포함해 둘다 switch적용이 필요하다면 [..]은 []다음에 와야 합니다.
[1, 2] 순서대로 2개 item의 list를 정확하게 해당 값과 일치시킵니다.
[_] list를 모든 단일 item과 일치시킵니다.
[int item1] 또는 [var item1] list를 모든 단일 item과 일치시키고 item1을 참조함으로서 반환식에서 반환된 값을 사용할 수 있습니다.
[_, _] list를 모든 2개의 item과 일치시킵니다.
[var item1, var item2] list를 모든 2개의 item과 일시키고 item1과 item2를 참조함으로서 반환식에서 반환된 값을 사용할 수 있습니다.
[_, _, _] list를 모든 3개의 item과 일시킵니다.
[var item1, ..] list를 하나 또는 그 이상의 item과 일치시킵니다. 또한 item1을 참조함으로서 반환식에서 반환된 첫 item의 값을 참조할 수 있습니다.
[var firstItem, .., var lastItem] list를 두개 또는 그 이상의 item과 일치시킵니다. 또한 firstItem과 lastItem을 참조함으로서 반환식에서 반환된 첫번째와 마지막 item의 값을 참조할 수 있습니다.
[.., var lastItem] list를 하나 또는 그 이상의 item과 일치시킵니다. 또한 lastitem을 참조함으로서 반환식에서 반환된 마지막 item의 값을 참조할 수 있습니다.

이제 작성해 볼 예제를 통해 몇몇 사례를 직접 확인해 보도록 하겠습니다.

 

Program.cs에서 int값에 대한 몇몇 arrary를 정의하고 가장 적합하게 일치되는 pattern에 따라 설명문을 반환하는 method에 해당 array를 전달하는 구문을 아래와 같이 추가합니다.

Console.OutputEncoding = System.Text.Encoding.UTF8;

int[] sequentialNumbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int[] oneTwoNumbers = new int[] { 1, 2 };
int[] oneTwoTenNumbers = new int[] { 1, 2, 10 };
int[] oneTwoThreeTenNumbers = new int[] { 1, 2, 3, 10 };
int[] primeNumbers = new int[] { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 };
int[] fibonacciNumbers = new int[] { 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 };
int[] emptyNumbers = new int[] { };
int[] threeNumbers = new int[] { 9, 7, 5 };
int[] sixNumbers = new int[] { 9, 7, 5, 4, 2, 10 };

Console.WriteLine($"{nameof(sequentialNumbers)}: {CheckSwitch(sequentialNumbers)}");
Console.WriteLine($"{nameof(oneTwoNumbers)}: {CheckSwitch(oneTwoNumbers)}");
Console.WriteLine($"{nameof(oneTwoTenNumbers)}: {CheckSwitch(oneTwoTenNumbers)}");
Console.WriteLine($"{nameof(oneTwoThreeTenNumbers)}: {CheckSwitch(oneTwoThreeTenNumbers)}");
Console.WriteLine($"{nameof(primeNumbers)}: {CheckSwitch(primeNumbers)}");
Console.WriteLine($"{nameof(fibonacciNumbers)}: {CheckSwitch(fibonacciNumbers)}");
Console.WriteLine($"{nameof(emptyNumbers)}: {CheckSwitch(emptyNumbers)}");
Console.WriteLine($"{nameof(threeNumbers)}: {CheckSwitch(threeNumbers)}");
Console.WriteLine($"{nameof(sixNumbers)}: {CheckSwitch(sixNumbers)}");

static string CheckSwitch(int[] values) => values switch
{
	[] => "빈 array",
	[1, 2, _, 10] => "array는 1, 2값과 단일값 하나 그리고 10값을 포함함",
	[1, 2, .., 10] => "array는 1, 2값과 공백을 포함한 모든 범위의 값 그리고 10값을 포함함",
	[1, 2] => " array는 1과 2만 포함함",
	[int item1, int item2, int item3] => $"array는 {item1}, {item2}, {item3}을 포함함",
	[0, _] => "array는 0으로 시작하고 그 외 다른 값 하나를 포함함",
	[0, ..] => "array는 0으로 시작하고 그 외 다수의 다른 수를 포함함",
	[2, .. int[] others] => $"array는 2로 시작하고 {others.Length} 이상의 수를 포함함",
	[..] => "그외 다른 모든 item에 해당하는 array",
};

위의 code를 실행하면 결과를 다음과 같을 것입니다.

list pattern에 대한 더 자세한 사항은 아래 link를 참고하시기 바랍니다.

Patterns - Pattern matching using the is and switch expressions. | Microsoft Learn

 

Patterns - Pattern matching using the is and switch expressions.

Learn about the patterns supported by the `is` and `switch` expressions. Combine multiple patterns using the `and`, `or`, and `not` operators.

learn.microsoft.com

(5) array 요약

 

위에서는 array의 다양한 유형을 선언하기 위해 아래 표와 같이 약간씩 다른 구문을 사용했습니다.

1차원 array string[]과 같은 datatype[]
2차원 array string[,]
3차원 array string[,,]
10차원 array string[,,,,,,,,,]
jagged array(가변배열) string[][]
jagged array에 대한 array string[][][]

array는 임시적으로 다수의 item을 저장하는데 유용하게 사용될 수 있습니다. 특히 collection은 동적으로 item을 추가하거나 삭제할 때 더욱 유연한 option들을 가지고 있습니다. collection에 관한 자세한 사항은 추후에 다시 알아볼 것입니다.

동적인 item의 추가나 삭제가 필요하지 않다면 Liat<T>와 같은 collection보다 array를 사용하는 것이 더 좋은 선택이 될 수 있습니다.
5. type 간 casting 및 convertting

 

data를 다룰 때는 종종 다른 type 간 변수의 값에 대한 변환이 필요한 경우가 있습니다. 예를 들어 data의 입력이 console에서 text로 이루어지는 경우에 초기에는 string type의 변수에 저장될 수 있지만 해당 data를 처리하는 방식에 따라 곧 시간/날짜나 숫자, 기타 다른 type으로 변환해야 할 경우가 있을 수 있는 것입니다.

 

때로는 같은 숫자 type이지만 해당 data의 계산을 수행하기 이전에 integer에서 float type으로의 변환이 필요한 경우도 있습니다.

 

형변환(Converting)은 Casting으로도 알려져 있으며 암시적인 형변환과 명시적인 형변환이라는 2가지 형식으로 변환방식이 존재합니다. 암시적인 형변환은 대게 자동적으로 이루어지는 것으로서 비교적 안전하게 이루어지며 이 과정에서는 어떠한 정보의 손실도 발생하지 않습니다.

 

명시적인 형변환은 소수점에 대한 정밀도와 같이 일부 정보를 손실할 가능성이 있는 것으로 수동적으로 수행되는 경우이며 C# compiler에게 정확히 형변환을 의도하고 있고 이로 인한 손실을 감수하겠다는 것을 말해주는 것이기도 합니다.

 

(1) 숫자에 대한 명시적인 casting과 암시적인 casting

 

int변수에서 double변수로의 암시적인 casting은 casting과정에서 손실될 data가 없으므로 안전하게 casting 될 수 있습니다.

 

csStudy03 solution에 'CastingConverting'이름의 project를 생성하고 Program.cs의 기존 구문을 모두 삭제합니다. 그리고  아래와 같이 int와 double변수를 선언하고 값을 할당하는데, 예제에서 int변수의 값을 double변수에 할당할 때 바로 암시적인 cast가 이루어집니다.

int a = 10;
double b = a;
Console.WriteLine(b);

이번에는 double변수를 선언하고 literal값을 할당한 뒤 double변수의 값을 int로 변수로 할당하여 double->int간 암시적인 형변환을 시도합니다.

double c = 9.8;
int d = c;
Console.WriteLine(d);

이 상태에서 compile을 시도하면 다음과 같이 error message가 Visual Studio의 Error List에 표시될 것입니다.

Severity Code Description Project File Line Suppression State
Error CS0266 Cannot implicitly convert type 'double' to 'int'. An explicit conversion exists (are you missing a cast?) CastingConverting ...

double 변수에서 int 변수로의 형변환은 잠재적으로 안전하지 않으며 소수점 이후값과 같이 일부 data의 손실이 발생할 수 있기 때문에 형변환을 수행할 수 없다는 것을 말하고 있습니다. 따라서 이런 경우 type에서 괄호를 사용해 double변수에 대한 명시적인 형변환을 수행해야 합니다. 이때 괄호를 cast 연산자라고 하는데 이렇게 명시적인 형변환을 수행하는 경우에도 개발자가 직접 형변환을 시도한 것이고 이후에 발생할 모든 사항을 이해하고 있다는 것을 전제로 하므로 소수점이하의 data가 경고 없이 제거될 수 있음은 반드시 알고 있어야 합니다.

 

이번에는 변수 d로의 할당 부분을 아래와 같이 변경하고

double c = 9.8;
int d = (int)c;
Console.WriteLine(d);

compile을 시도하면 오류는 발생하지 않지만 실제 실행 결과를 보면 .8부분은 제거되었음을 알 수 있습니다.

또한 큰 수의 integer값에서 상대적으로 작은 integer값으로의 형변환인 경우에도 비슷한 방법을 적용해야 합니다. 이러한 경우에도 큰 값의 bit가 복사되고 이를 예상하지 못한 방법으로 해석될 수 있음을 인지해야 합니다.

 

아래와 같이 long(64bit) 변수와 int(32bit) 변수를 선언하고 각 변수에 값을 할당하도록 합니다. 예제에서는 둘 다 수용가능한 작은 값을 할당한 뒤 다시 int에서 수용이 불가능한 큰 값을 할당하고 있습니다.

long e = 10;
int f = (int)e;
Console.WriteLine($"e is {e:N0} and f is {f:N0}");

e = long.MaxValue;
f = (int)e;
Console.WriteLine($"e is {e:N0} and f is {f:N0}");

위 예제를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

다시 e의 값을 50억으로 할당한 뒤

e = 5_000_000_000;

다시 code를 실행하면 결과를 다음과 같을 것입니다.

(2) System.Convert를 사용한 type변환

 

예를 들어 byte, int, long와 같은 정수 type이나 class와 하위 class사이에서와 같은 비슷한 type 간에서는 형변환을 사용할 수 있지만 long에서 string이나 byte에서 DateTime형식과 같은 경우에는 위와 같은 형변환을 수행할 수 없습니다. 대신 System.Convert와 같은 type연산자를 사용해 형변환을 수행할 수 있으며 C#의 모든 숫자 type을 변환할 수 있습니다.

 

Program.cs에서 System.Convert class를 정적 import 합니다.

using static System.Convert;

그다음 double변수를 선언하고 값을 할당한 뒤 integer로의 현변환을 시도합니다. 이때 ToInt32 method는 System.Convert의 method입니다.

double g = 9.8;
int h = ToInt32(g);
Console.WriteLine($"g is {g} and h is {h}");

code를 실행하면 다음과 같은 결과를 볼 수 있습니다.

casting과 converting의 중요한 차이점은 double값의 9.8에서 소수점 이하를 버리는 대신 10으로 반올림을 한다는 것입니다.

 

(3) 반올림

 

이전 예제를 통해 실제 cast연산자가 실수에서 소수점 이하 부분을 잘라내는 결과를 확인해 본 바 있습니다. 그리고 좀 다르게 System.Convert method는 반올림 혹은 반내림을 수행하기도 합니다. 이때 반올림/반내림에는 일정한 규칙이 존재합니다.

 

● 기본 반올림 규칙

 

현재 초등학교 아이들은 소수점이하가 .5이거나 그 이상이라면 반올림하고 그 미만이라면 반내림할 수 있다고 배웁니다.  그럼 C#에서도 동일한 규칙이 적용될까요?

 

아래 에제는 double변수의 array를 선언하고 값을 할당하고 있으며 각 item에 대해 integer로의 형변환을 수행한 뒤 그 결과를 표시하도록 하고 있습니다.

using static System.Convert;

double[] doubles = new[] { 9.49, 9.5, 9.51, 10.49, 10.5, 10.51 };

foreach (double n in doubles)
{
	Console.WriteLine($"ToInt32({n}) is {ToInt32(n)}");
}

위의 예제를 실행하면 다음과 같은 결과를 나타냅니다.

위 결과를 보면 C#에서는 초등학교에서 가르치는 규칙과는 약간 다르다는 것을 알 수 있습니다. 우선 소수점 이하가 .5이하면 반내림을 수행하며 소수점 이하가 .5와 같거나 그 이상이라면 반올림을 수행합니다. 그런데 소수점 이하가 .5이고 정수 부분이 홀수라면 반올림을, 정수부분이 짝수라면 반내림을 수행합니다.

 

이러한 규칙은 은행가의 반올림(오사오입 : banker’s rounding)으로 알려져 있으며 반올림과 반내림이 교차로 진행되면서 한쪽으로 치우치게 되는 것을 줄여줄 수 있기 때문에 선호되는 방식입니다. 하지만 JavaScript와 같은 다른 많은 언어들은 위에서 언급한 초등학교의 규칙을 그대로 적용하고 있는 것도 많기 때문에 주의해야 합니다.

 

(4) 반올림 규칙 제어하기

 

위에서 언급한 C#의 반올림 규칙은 반드시 따라야 하는 것은 아니고 Match class의 Round method를 통해 원하는 방향으로 조정할 수 있습니다.

 

아래 예제는 위 예제에서 foreach문을 변경한 것으로 각 double값에 대한 반올림을 수행하지만 이때는 반올림위주인  AwayFromZero규칙(5에서 걸리면 0에서 멀어지는 쪽)을 적용하고 있습니다.

foreach (double n in doubles)
{
    Console.WriteLine(format: "Math.Round({0}, 0, MidpointRounding.AwayFromZero) is {1}", arg0: n, arg1: Math.Round(value: n, digits: 0, mode: MidpointRounding.AwayFromZero));
}

위 예제를 실행하면 다음과 같은 결과를 볼 수 있습니다.

다른 Programming언어를 사용할 때도 반올림규칙을 확인해 볼 것을 권장합니다. 때로는 예상한 대로 동작하지 않을 수 있기 때문입니다.

 

(5) string으로의 type변환

 

대부분의 type에서 사람이 읽을 수 있는 text로 변환하기 위해서는 string으로의 변환이 필요합니다. 이와 같은 이유로 C#의 모든 type은 System.Object class로부터 파생된 ToString이라는 method를 가지고 있습니다.

 

ToString method는 모든 변수의 현재값을 text로 변환하는데 그럴 수 없는 일부 type에서는 자신의 namespace와 type의 이름을 대신 반환합니다.

 

아래 예제에서는 몇몇 변수를 선언하고 이들을 문자열로 표현하도록 변환을 시도하고 있습니다.

int number = 12;
Console.WriteLine(number.ToString());

bool boolean = true;
Console.WriteLine(boolean.ToString());

DateTime now = DateTime.Now;
Console.WriteLine(now.ToString());

object me = new();
Console.WriteLine(me.ToString());

예제를 실행하면 다음과 같은 결과가 표시됩니다.

(6) binary 개체에서 string으로의 변환

 

image나 video 같은 binary data를 network protocol이나 저장된 binary개체를 읽는 다른 OS(Operating System)에 의해 전송될 때 때로는 bit원문을 그대로 전송할 수 없는 경우가 있습니다. 이런 경우 binary 개체를 string문자열로 변환하는 방법이 있는데 이러한 방식을 Base64 encoding이라고 합니다.

 

이를 지원하기 위해 Convert type은 ToBase64String과 FromBase64String이라는 method를 같이 가지고 있으며 해당 method가 실질적인 변환을 수행하는 method입니다.

 

아래 예제에서는 임의의 byte값으로 채워진 byte배열을 생성하고 각 byte값과 해당 값을 Base64로 변환시킨 결과를 확인하고 있습니다.

byte[] binaryObject = new byte[128];
Random.Shared.NextBytes(binaryObject);

Console.WriteLine("Binary Object as bytes:");
for (int index = 0; index < binaryObject.Length; index++)
{
    Console.Write($"{binaryObject[index]:X} ");
}
Console.WriteLine();

string encoded = Convert.ToBase64String(binaryObject);
Console.WriteLine($"Binary Object as Base64: {encoded}");

기본적으로 int값은 10진수 형식으로 표시되므로 해당 값을 16진수로 표시하기 위해서는 'X'와 같은 서식 code를 사용해 서식을 지정해야 합니다.

 

위 예제를 실행하면 다음과 같은 결과가 표시될 것입니다.

(7) 문자열에서 숫자 또는 날짜/시간형식으로 Parse 하기

 

가장 흔하기 이루어지는 형변환 중 다른 하나는 문자열에서 숫자나 날짜/시간값으로 변환하는 것입니다.

 

ToString의 정반대역할을 하는 method로 Parse가 있으며 숫자와 날짜/시간의 type인 DateTime을 포함해 몇몇 type에서만 Parse method를 갖고 있습니다.

 

아래 예제는 parse method를 사용해 문자열값으로 부터 integer와 DateTime값으로 parsing을 수행하고 그 결과를 표시하도록 하고 있습니다.

int age = int.Parse("42");
DateTime birthday = DateTime.Parse("1 December 1980");
Console.WriteLine($"I was born {age} years ago.");
Console.WriteLine($"My birthday is {birthday}.");
Console.WriteLine($"My birthday is {birthday:D}.");

기본적으로 날짜나 시간의 값은 짧은 형식으로 출력합니다. 이때 'D'와 같은 서식 code를 사용하여 긴 날짜 서식으로 날짜만을 표시할 수도 있습니다.

 

표준 날짜 및 시간 서식 지정자에 관한 사항은 아래 link를 참고하시기 바랍니다.

Standard date and time format strings | Microsoft Learn

 

Standard date and time format strings

Learn how to use a standard date and time format string to define the text representation of a date and time value in .NET.

learn.microsoft.com

● Parse 오류

 

Parse method의 한 가지 문제점은 string을 변환할 수 없는 경우 error를 발생시킨다는 것입니다.

 

아래 예제는 알파벳을 포함한 문자열에서 integer변수로의 parse를 시도하고 있습니다.

int count = int.Parse("abc");

하지만 위 예제를 실행하면 다음과 같은 오류를 마주하게 됩니다.

● TryParse method를 사용한 예외 처리하기

 

Parse의 에러상황을 피하기 위한 방법으로 TryParse method를 대신 사용할 수 있습니다. TryParse는 문자열로부터 변환을 시도하고 변환이 가능하면 true를 불가능하면 false를 반환합니다. 예외는 상대적으로 처리 비용이 비싼 편이어서 할 수 있다면 예외가 발생하는 경우를 되도록 피하는 것이 좋기 때문에 TryParse사용을 권장합니다.

 

out keyword는 TryParse method에서 변환을 실행할 때 count변수에 값을 설정하기 위해 필요한 부분입니다.

 

아래 예제는 사용자에게 egg 수에 관한 count값을 입력받고 이를 TryParse method를 사용해 변환하여 그 결과를 표시하도록 하고 있습니다.

Console.Write("How many eggs are there? ");
string? input = Console.ReadLine();

if (int.TryParse(input, out int count))
{
    Console.WriteLine($"There are {count} eggs.");
}
else
{
    Console.WriteLine("I could not parse the input.");
}

code를 실행하고 12 값을 입력하면 다음과 같은 결과를 표시하게 됩니다.

그러나 'twelve'처럼 문자열을 입력하면 Parse에서의 예외대신 다음과 같은 결과를 대신 표시합니다.

또한 string값에서 다른 type으로의 변환을 위해 System.Convert type의 method를 사용할 수 있습니다. 그러나 Parse method와 같이 변환을 수행할 수 없는 경우 예외가 발생되는 것은 동일합니다.

6. 예외 처리

위에서 형변환을 수행할 때 error가 발생할 수 있는 몇몇 scenario를 살펴봤는데 어떤 언어의 경우에는 잘못된 동작을 수행할때 error code를 반환하기도 합니다. .NET에서는 동작의 실패에 대한 상세한 보고를 위해서 설계된 더 풍부한 '예외(Exception)'를 사용합니다. 이러한 상황이 실제 발생한다면 우리는 이를 두고 'runtime 예외가 발생했다.'라고 말합니다.

 

다른 system의 경우에는 여러 가지로 사용할 수 있는 값을 반환하기도 하는데 예를 들어, 반환값이 양수라면 table의 row를 의미할 수 있고 음수의 경우라면 일부 오류를 의미할 수 있습니다.

 

예외가 발생하면 thread는 중단되며 호출 code에서 try-catch문을 정의했다면 이를 통해 예외를 처리할 수 있는 기회가 주어지게 됩니다. 만약 현재 method가 이를 처리하지 않는다면 호출 method에 기회가 주어지며 이곳에서도 처리하지 않으면 call stack전체를 차례로 거슬러 올라가게 됩니다.

 

지금까지 확인해 본 바와 같이 console app이나 .NET Interactive notebook의 기본동작은 stack trace(추적)을 포함해 예외에 대한 message를 출력하며 동시에 code이 동작을 중지하게 됩니다. application의 경우라면 실행이 종료될 텐데 이는 잠재적으로 손상된 상태로 code의 실행을 계속 진행시키는 것보다 훨씬 나은 방법입니다. 실행 중인 code는 예외가 발생하면 catch 되며 이를 통해 예외가 발생한 문제점을 이해하고 이를 적절히 수정하는 것으로 해당 예외를 처리할 수 있습니다.

구문을 확인하는 것으로 가능한 한 예외가 발생할 수 있는 code의 작성을 피할 수 있을지 모르지만 모든 것을 그렇게 진행하기는 어려운 것이므로 때로는 code를 호출하는 상위 수준의 component에서 예외가 catch 되도록 하는 것이 좋습니다.

(1) try block

 

만약 작성 중인 code가 잠재적으로 error를 발생시킬 가능성이 있다면 try block을 통해서 이를 감싸놓을 수 있습니다. 예를 들어 text를 숫자형식으로 parsing 할 때와 같은 code가 그럴 것입니다. catch block의 모든 구문은 try block의 구문에 의해 예외가 발생하는 경우 실행됩니다.

 

예제의 작성을 위해 csStudy03 solution에서 HandlingExceptions project를 생성합니다. 그리고 Program.cs의 기본 구문을 모두 삭제하고 사용자가 나이를 입력하고 입력된 나이를 다시 출력하는 구문을 아래와 같이 작성합니다.

Console.WriteLine("Before parsing");
Console.Write("What is your age? ");

string? input = Console.ReadLine();

try
{
	int age = int.Parse(input);
	Console.WriteLine($"You are {age} years old.");
}
catch
{
}

Console.WriteLine("After parsing");

위 예제에서는 catch block에서 어떠한 code도 포함하지 않고 있으며 compiler는 아래와 같은 경고를 표시하게 됩니다.

기본적으로 새로운 .NET 6 Project에서 Microsoft는 null가능 참조 type을 허용하므로 이와 같은 많은 compiler경고를 볼 수 있습니다. 실제 code에서는 null에 대한 확인 code를 추가해야 하며 적절하게 이를 처리하도록 해야 합니다. 위 예제에서는 그렇게 견고한 code를 만들 필요가 없고 예제마다 null확인이 들어간다면 무이하게 code가 난잡해질 수 있기 때문에 기본적으로 null확인을 포함하지 않고 있습니다.

 

향후에도 잠재적으로 null가능한 변수의 많은 예제를 보게 될 텐데 이들에 관한 경고는 예제에 한해서 무시할 것이며 오로지 실제 작성되는 code에서만 주의하면 됩니다.

 

위 예제의 경우 사용자는 ReadLine의 입력완료를 위해 Enter를 눌러야 하며 아무런 입력이 없는 경우 empty string을 반환할 것이므로 실제 input이 null 될 가능성은 없습니다. 경고를 지우기 위해서는 'input'을 'input!'으로 변경하면 되는데 표현식뒤에 느낌표(!)를 'null-forgiving'연산자라고 하며 null에 대한 compiler의 경고를 표시하지 않도록 합니다. null-forgiving 연산자는 runtime에는 아무런 영향도 끼치지 않습니다. 만약 해당 표현식이 runtime에서 null로 평가된다면 예외가 발생할 수 있는 다른 방법을 통해 할당한 경우가 됩니다.

 

예제는 pasing전과 이후로 2개의 message를 포함하고 있는데 이는 code의 흐름을 명확하게 알기 위해서 추가한 것입니다.

 

예제를 실행하고 42를 입력하면 다음과 같은 결과를 확인할 수 있습니다.

하지만 만약 입력을 forty-two로 하게 되면 이전과는 다른 결과가 표시됩니다.

code가 실행될 때 error 예외가 catch 되고 입력결과 message가 표시되지 않지만 console app의 실행은 계속 진행되었습니다. 비정상적으로 app이 중지되는 기본적인 동작보다 훨씬 나아 보일 수 있지만 때로는 발생한 error의 type을 확인해 봐야 하는 경우가 있습니다.

실제 작성하는 code에서 위의 예제처럼 비어있는 catch문을 사용을 피해야 합니다. 이것은 보이지 않는 예외가 될 수 있으며 잠재적인 문제점을 숨기게 됩니다. 예외에 대한 적절한 처리를 할 수 없거나 그렇게 하기를 원하지 않는다면 최소한 예외에 대한 log를 남겨두는 쪽으로 만들어야 합니다. 또는 예외자체를 재발생(rethrow)시켜서 상위 수준의 code에서 대신 예외에 대한 처리를 결정할 수 있도록 해야 합니다.

● 모든 예외를 catch 하기

 

발생할 수 있는 모든 예외 type에 대한 정보를 가져오기 위해 catch block에서 System.Exception type의 변수를 선언할 수 있습니다.

 

아래 예제는 실제 catch block에서 예외변수를 선언한 것으로 console로 예외에 대한 정보를 출력하는 데 사용하고 있습니다.

try
{
	int age = int.Parse(input);
	Console.WriteLine($"You are {age} years old.");
}
catch (Exception ex)
{
	Console.WriteLine($"{ex.GetType()} - {ex.Message}");
}

이전 예제의 try~catch문을 위와 같이 변경하고 project를 실행한 뒤 이전과 동일하게 forty-two를 입력하면 다음과 같은 결과를 표시하게 됩니다.

● 특정 예외만 catch 하기

 

위 예제를 통해 우리는 예제에서 발생하는 예외의 type이 어떤 것인지를 알게 되었습니다. 이를 통해 해당 예외의 type만을 catch 하여 사용자에게 표시할 message를 좀 더 친숙하게 변경함으로써 예제의 동작을 더 개선시킬 수 있습니다.

 

아래 예제는 기본 catch문을 남겨둔 상태에서 'format exception type'을 위한 새로운 catch block을 추가한 것입니다.

try
{
	int age = int.Parse(input);
	Console.WriteLine($"You are {age} years old.");
}
catch (FormatException)
{
	Console.WriteLine("입력된 나이는 숫자형식이 아닙니다.");
}
catch (Exception ex)
{
	Console.WriteLine($"{ex.GetType()} - {ex.Message}");
}

예제를 실행한 뒤 동일하게 forty-two를 입력하면 이번에는 다음과 같은 결과를 표시하게 될 것입니다.

위 예제에서 기존의 범용적인 catch block을 밑에 남겨둔 이유는 다른 type의 예외가 발생할 수 있는 만약을 대비한 것입니다. 그에 대한 한 가지 예로 다시 project를 실행하고 이번에는 숫자 9999999999를 입력해 봅니다.

위의 결과를 통해 우리는 'System.OverflowException' type의 예외가 발생하였음을 알 수 있습니다. 만약 해당 type에 관한 별도의 처리를 수행해야 한다면 overflow exception에 관한 catch block를 동일하게 추가해 주면 됩니다.

catch (OverflowException)
{
	Console.WriteLine("입력된 값이 너무 큽니다.");
}
catch (FormatException)
{
	Console.WriteLine("입력된 나이는 숫자형식이 아닙니다.");
}
catch (Exception ex)
{
	Console.WriteLine($"{ex.GetType()} - {ex.Message}");
}

project를 실행한 뒤 다시 9999999999을 입력하면 다음의 결과를 표시합니다.

여러 catch exception을 사용할 때 순서는 예외 type에 대한 상속계층과 관련되어 있기 때문에 중요하게 작용합니다. 잘못된 순서로 예외가 발생한다면 compiler는 build error를 발생시킬 수 있습니다.

너무 많은 catch exception은 만들지 않는 것이 좋습니다. 때로는 처리되는 방식의 logic을 변경할 수 있는 상황에 대해 더 많은 정보가 알려진 수준에서 처리되기 위해 상위로 call stack을 전파하는 것이 좋은 방법이 될 수 있습니다.

● filter를 통한 catch

 

catch문에서는 또한 when keyword를 사용함으로써 아래 예제에서와 같이 filter를 적용할 수 있습니다.

catch (FormatException) when (input.Contains("forty"))
{
	Console.WriteLine("40과 같이 숫자를 입력하세요.");
}
catch (FormatException)
{
	Console.WriteLine("입력된 나이는 숫자형식이 아닙니다.");
}
7. overflow 확인하기

이전에 숫자 type을 casting 할 때 일부 data의 손실이 발생할 수 있음을 알 수 있었습니다. 예를 들어 long변수에서 int변수로 casting 할 때도 마찬가지인데 저장된 type의 값이 너무 크다면 이런 경우는 overflow가 됩니다.

 

(1) checked 구문을 통한 overflow 예외 throw 하기

 

 checked구문은 overflow가 발생했을때 .NET이 예외를 throw 하도록 합니다. 기본적으로는 성능적인 문제 때문에 이 과정을 조용히 처리하게 됩니다.

 

csStudy03 solution에서 'CheckingForOverflow' project를 생성하고 Program.cs의 기존 구문을 모두 삭제한 뒤 아래 예제를 작성합니다.

 

예제는 하나의 int변수를 선언하고 가능한 최댓값에서 1보다 작은 integer값을 할당하고 있습니다. 그리고 3번에 걸쳐 1의 값을 증가시키고 해당 값을 console을 통해 출력하도록 합니다.

int x = int.MaxValue - 1;

Console.WriteLine($"Initial value: {x}");
x++;
Console.WriteLine($"After incrementing: {x}");
x++;
Console.WriteLine($"After incrementing: {x}");
x++;
Console.WriteLine($"After incrementing: {x}");

해당 예제에서는 int변수의 값으로 최대값에서 -1 작은 초기값을 설정한 뒤 값을 1씩 증가시키면서 그때마다 해당 변수의 값을 표시하고 있습니다. 그리고 최댓값이상으로 도달하게 되면 변수 type의 최솟값으로 overflow가 되고 이때 값에서 부터 다시 증가를 계속하게 됩니다.

 

실제 위 code를 실행하여 언급한 대로 결과가 표시되는지 확인합니다.

결과를 보면 어떠한 이상징후도 없이 overflow가 조용히 처리되어 변수는 int의 최소값으로 바뀌게 되고 증가처리가 계속 진행되었음을 알 수 있습니다.

 

이제 위 구문을 변경하여 아래와 같아 checked구문을 추가합니다.

checked
{
	int x = int.MaxValue - 1;

	Console.WriteLine($"Initial value: {x}");
	x++;
	Console.WriteLine($"After incrementing: {x}");
	x++;
	Console.WriteLine($"After incrementing: {x}");
	x++;
	Console.WriteLine($"After incrementing: {x}");
}

code를 실행하면 overflow가 발생할 때 이를 감지하여 다음과 같은 예외를 발생시키게 됩니다.

이를 통해 다른 예외와 마찬가지로 try-catch block을 사용함으로써 사용자에게 좀 더 자연스러운 error message를 표시할 수 있을 것입니다.

Console.OutputEncoding = System.Text.Encoding.UTF8;

try
{
	checked
	{
		int x = int.MaxValue - 1;

		Console.WriteLine($"Initial value: {x}");
		x++;
		Console.WriteLine($"After incrementing: {x}");
		x++;
		Console.WriteLine($"After incrementing: {x}");
		x++;
		Console.WriteLine($"After incrementing: {x}");
	}
}
catch(OverflowException ex)
{
	Console.WriteLine("값이 허용범위를 초과하였습니다.");
}

(2) uncheck구문을 통한 compiler overflow check 무시하기

 

위에서 다뤄본 것은 runtime에서 overflow에 대한 기본적인 동작에 관한 것이며 이러한 동작을 바꾸기 위해 checked구문을 어떻게 사용할지에 대한 것이었습니다. 이번에는 compile time에서의 overflow동작에 관한 것과 해당 동작을 변경하기 위한 unchecked구문에 관해 알아보고자 합니다.

 

이와 관련된 keyword는 unchecked이며 code의 block안에서 compiler에 의해 수행되는 overflow check를 끄도록 합니다

 

아래 예제는 위 예제의 끝에서 계속 이어지는 구문으로 compiler는 해당 구문이 overflow 될 것이라 판단하고 compile 하지 않을 것입니다.

int y = int.MaxValue + 1;

위 구문에서 mouse pointer를 위에 갖다 대면 compile time에서 발생한 error message를 다음과 같이 표시할 것입니다.

위와 같은 compile time check를 하지 않으려면 해당 code block를 unchecked로 감싸놓으면 됩니다. 아래 예제는 y의 값을 console로 출력하고 1씩 감소시키는 동작을 반복해서 수행하도록 하고 있으며 해당 구문 전체를 uncheck문으로 감싸놓았습니다.

unchecked
{
	int y = int.MaxValue + 1;
	Console.WriteLine($"Initial value: {y}");
	y--;
	Console.WriteLine($"After decrementing: {y}");
	y--;
	Console.WriteLine($"After decrementing: {y}");
}

위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

물론 실제 위와 같이 명시적으로 overflow check를 하지 않도록 함으로써 overflow를 허용하게 되는 경우는 드문 일일 것입니다. 자주 사용할 것은 아니므로 그냥 이런 것이 있다는 정도만 알고 넘어가도 충분하리라 생각됩니다.

728x90