# 14에 대한 3번째 순서에서는 변수와 array를 통한 data다루기와 함께, 의사결정/반복수행과 같은 실제 program의 동작방식을 살펴보고, 서로 다른 type간 형변환과 예외처리를 포함하여 application작성에 관한 전반적인 사항을 알아보고자 합니다.
1. 연산자
연산자라함은 실제값 또는 피연산자를 대상으로 사칙연산과 같은 동작을 수행하도록 하는 것을 말합니다. 이때 연산자는 연산결과에 대한 값을 반환할 수 있고 이 값은 다시 다른 변수에 할당할 수 있습니다.
1) 산술연산자
(1) 이항(binary) 연산자
가장 보편적인 연산자이며 2개의 피연산자를 필요로 합니다.
| var resultVariable = firstVariable [연산자] secodVariable; |
아래 예제는 이항연산자의 일반적인 사용방법을 나타내고 있습니다.
int x = 10;
int y = 20;
int resultAdd = x + y;
int resultMultiple = x * y;
가장 일반적인 이항연산자는 +, -, *, / 정도로 볼 수 있고 다른 하나는 % 인데 이는 나머지로 생각할 수 있습니다.
int i = 10;
int j = 3;
Console.WriteLine($"{i % j}"); //결과 1
(2) 단항(unary) 연산자
단항 연산자는 하나의 피연산자를 필요로 하는 것으로 피연산자의 앞이나 뒤에서 연산을 수행할 수 있습니다. 단항 연산자를 사용하는 가장 대표적인 사례로 증감/감소연산자를 예로 들 수 있습니다.
int i = 10;
int a = ++i;
int b = i++;
또한 typeof나 nameof, sizeof도 단한 연산자에 해당할 수 있습니다.
Type resultType = typeof(int);
string resultName = nameof(i);
int resultByteSize = sizeof(int);
++와 --에서 ++는 1씩 증가시키는 증감연산자, --를 1씩 감소시키는 감소연산자라고 합니다.
int i = 10;
int j = i++;
Console.WriteLine($"{i} / {j}");
위 예제는 증감연산자를 사용하여 변수 i의 값을 1만큼 증기시키고 있습니다. 위 예제를 실행하면 결과값으로 '11 / 10'을 나타내게 됩니다.
++는 1만큼 증가시킨다고 했는데 왜 결과에서 j의 값이 10이라고 할까? 그 이유는 ++연산자가 i의 값을 j에 할당하고 난 뒤에 실행되었기 때문입니다. 이런것을 후위연산자라고 하며 만약 i변수의 값이 j에 할당되기 전에 먼저 ++가 실행되어야 한다면 이를 '++i'와 같이 변수앞에 단항연산자를 붙여줘야 하고 이를 선행연산자라고 합니다.
int i = 10;
int j = ++i;
Console.WriteLine($"{i} / {j}");
위 예제는 ++를 선행연산자로 적용한 것이며 예제를 실행하면 '11 / 11'이라는 결과를 표시할 것입니다.
증감, 감소연산자를 할당연산자와 결합하여 사용할때 자칫 code를 읽는데서 잘못된 예상결과의 판단을 불러올 수 있습니다. 이러한 우려 때문에 Swift언어의 경우 version 3부터 단항연산자를 지원하지 않기도 합니다. 따라서 증감, 감소연산자를 사용하더라도 되도록이면 할당연산자와 결합하는 것을 피하는 것이 좋습니다.
(3) 삼항(ternary) 연산자
삼항 연산자는 아래와 같이 3개의 피연산자를 필요로 합니다.
| var result = first ? second : third |
위 예시에서는 삼항 연산자의 가장 흔한 것으로 if문과 유사한 '?:'를 사용하는 조건부 연산자를 나타내고 있습니다. 여기서 first는 boolean값을 표현하는 피연산자가 와야하며 second는 first가 true인 경우 반환할 값을, third는 first가 false인 경우 반환할 값을 나타냅니다.
int i = 10;
string result = i > 10 ? "i는 10보다 큽니다." : "i는 10과 같거나 작습니다.";
Console.WriteLine(result);
상기 예제는 실제 삼항연산자를 사용해 조건부 연산을 수행한 것으로 이것을 if문으로 바꿔보면 다음과 같이 구현할 수 있습니다.
if (i > 10)
{
result = "i는 10보다 큽니다.";
}
else
{
result = "i는 10과 같거나 작습니다.";
}
실제 많은 C#개발자가 가능한 경우 if문을 사용하기 보다는 삼항연산자를 사용하는 경우가 많습니다. Code를 더욱 간소화할 수 있고 읽기에도 익숙해진다면 더 명확한 code가 될 수 있기 때문입니다.
(4) 할당 연산자
지금까지의 예제를 통해 사용되어온 '='문자가 바로 할당연산자입니다. 할당연산자는 다른연산자와 결합이 가능하므로 아래와 같이 code를 간소화하여 사용하는 경우도 있습니다.
int i = 1;
i += 1 // i = i + 1;
i -= 1 // i = i - 1;
i *= 1 // i = i * 1;
i /= 1 // i = i / 1;
2) NULL 연산자
할당계열의 연산자로서 기본적으로 값을 활당하지만 만약 값이 null인 경우 이를 대체하는 다른 값으로 대신 할당하도록 하는 연산자입니다.
NULL 병합 연산자는 아래와 같이 3가지를 사용할 수 있습니다.
| 연산자 | 명칭 | 역활 |
| ?. | Null 조건부 연산자 (Null-conditional) | 개체가 null이라면 null을 반환하며 그렇지 않으면 개체를 통해 지정한 해당 속성의 값을 대신 반환합니다. |
| ?? | Null 병합 연산자 (Null-coalescing) | 왼쪽 표현식이 null이라면 오른쪽 값을 반환하지만 그렇지 않으면 아무것도 동작도 수행하지 않습니다. |
| ??= | Null 병합 할당 연산자 (Null-coalescing assignment) | 왼쪽 표현식이 null이라면 오른쪽 값을 반환하지만 그렇지 않으면 아무것도 동작도 수행하지 않습니다. 단순히 표현식을 평가하는 ??과는 달리 '??='연산자는 변수가 null인 경우 즉각 값을 할당합니다. |
아래 예제에서는 위 연산자에 대한 사용방식을 간단히 표현하고 있습니다.
int age = Employee?.Age ?? 0;
EmployeeName ??= "unknown";
예제에서 첫번째 구문의 '?.'연산자는 Age속성의 값을 할당하기 전에 Employee가 null인지를 우선 확안하게 됩니다. 만약 Employee가 null이라면 null자체를 반환하게 되고 그 상태에서 ?? 연산자를 통해 null이 확인되어 0을 할당하게 됩니다. 만약 Employee가 null이 아니라면 그때는 Employee?.Age값을 할당할 것입니다.
두번째 구문은 EmployeeName이 null이라면 '??='연산자에 의해 여기에 "unknown"이라는 문자열값을 할당합니다.
위와 같은 null연산자는 null이 발생하는 경우 null에 대한 예외를 일으키는 대신 대안으로 제시한 값을 대신 할당하게 함으로서 code를 간소하고 효휼적으로 작성할 수 있도록 해줍니다. 만약 위와 같은 null 연산자를 사용하지 않는다면 우리는 아래와 같이 null에 대한 예외사항을 직접 작성해줘야 합니다.
int age;
if (Employee is not null)
{
age = Employee.Age;
}
else
{
age = 0;
}
(1) NULL 조건부 할당 연산자 (Null-conditional assignment operator)
C# 14 부터 Null 조건부 할당 연산자는 instance를 포함하는 경우에만 속성이나 field에 값을 할당할 수 있습니다. 예를 들어 Employee라는 class와 함께
public class Employee
{
public string Name { get; set; } = string.Empty;
public int Age { get; set; }
}
Employee의 instance를 변경하는 method를 정의해야 한다면
public static void UpdateAge(Employee? emp, int age)
{
if (emp is not null)
emp.Age = age;
}
Null 조건 할당 연산자를 사용해 위와 동일한 구현을 아래와 같이 간소하게 표현할 수 있습니다.
public static void UpdateAge(Employee? emp, int age)
{
emp?.Age = age;
}
즉, emp가 null이 아니라면 emp의 Age속성은 age값으로 변경되지만 그렇지 않다면 아무런 후속동작도 수행되지 않습니다.
Null 조건부 할당 연산자에 관해서는 아래 link를 참고해 더 자세히 알아볼 수 있습니다.
https://github.com/dotnet/csharplang/discussions/8676
Null-conditional assignment · dotnet csharplang · Discussion #8676
Summary Permits assignment to occur conditionally within a a?.b or a?[b] expression. using System; class C { public object obj; } void M(C? c) { c?.obj = new object(); } using System; class C { pub...
github.com
3) 논리 연산자(Logical operators)
논리연산자는 대게 boolean값을 평가하게 되므로 그 결과로 true나 false를 반환합니다.
논리연산자중 '이진 논리 연산자(binary logical operator)'는 2개 이상의 boolean값을 평가하는 것으로 연산방식에 따라 AND, OR, XOR(exclusive OR)로 나눌 수 있으며 아래와 같이 사용됩니다.
bool a = true;
bool b = false;
Console.WriteLine($"AND a : {a & a}");
Console.WriteLine($"AND b : {b & b}");
Console.WriteLine($"AND a/b : {a & b}");
Console.WriteLine();
Console.WriteLine($"OR a : {a | a}");
Console.WriteLine($"OR b : {b | b}");
Console.WriteLine($"OR a/b : {a | b}");
Console.WriteLine();
Console.WriteLine($"XOR a : {a ^ a}");
Console.WriteLine($"XOR b : {b ^ b}");
Console.WriteLine($"XOR a/b : {a ^ b}");
예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

예제에서 사용된 '&'는 AND 논리연산자로, 피연산자 모두 true여야만 결과로 true를 표시합니다. '|'문자는 OR 연산자로 이 경우에는 피연산자중 단 하나만 true여도 그 결과로 true를 반환하며, '^'문자는 XOR 연산자로 피연산자중 하나면 true여도 그 결과로 true를 반환하지만 피연산자 모두 true여서는 안됩니다.
(1) 조건부 논리 연산자(Conditional logical operators)
조건부 논리 연산자는 위에서 소개한 논리연산자를 이중으로 표현(&&, ||)한 것을 말하는 것으로 조건에 따라 연산처리의 수행여부를 결정합니다. 예제를 위해 우선 아래와 같은 간단한 함수 하나를 추가합니다.
static bool DoSomething()
{
Console.WriteLine("함수 실행됨.");
return true;
}
위 예제는 top-level program 기능을 사용하는 경우라면 Program.cs의 어디에 만들어도 문제가 되지 않지만 가급적 가장 아래에 배치하는 것이 좋습니다.
위 함수는 호출되면 간단한 message를 표시한 뒤 그 결과로 true값을 반환합니다. 함수나 method의 작성에 관한 자세한 사항은 추후에 자세히 다룰 것이므로 지금은 예제의 함수가 어떤동작을 하고 어떤 것을 반환하는지만 알아두면 충분합니다.
함수를 만들고 나면 그 위에 아래의 구문을 추가합니다.
bool a = true;
bool b = false;
Console.WriteLine($"a & DoSomething() : {a & DoSomething()}");
Console.WriteLine($"b & DoSomething() : {b & DoSomething()}");
그리고 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

결과를 보면 각각의 연산작업에 2번씩 실행되었음을 알 수 있습니다. 여기서 어떤 분들은 위 결과에 대해 a는 납득이 되지만 b는 그렇지 않다라는 생각을 가질 수도 있습니다. 왜냐하면 '&'는 AND연산으로 피연산자 모두가 true여야만 그 결과가 true가 되는데 b는 처음부터 false로 시작하기 때문에 굳이 함수를 실행하지 않아도, 즉, 함수의 실행여부와는 관계없이 그 결과가 false가 되기 때문입니다. 다시 말해 b를 피연산자로 하는 경우 함수를 실행할 필요가 없다는 결론에 이르게 되는 것입니다. 하지만 단순한 논리연산자는 이러한 경우를 감안할 수 없으므로 무엇인가 다른방식을 적용해야 하고 그 결과로 조건부 논리 연산자가 등장하게 되었습니다.
이번에는 위 예제를 아래와 같이 바꿔보겠습니다.
Console.WriteLine($"a && DoSomething() : {a && DoSomething()}");
Console.WriteLine($"b && DoSomething() : {b && DoSomething()}");
예제를 변경하고 다시 실행하면 이번에는 다른 결과를 표시할 것입니다.

이번에는 a변수에 대해서만 함수가 두번 실행되었고 b에서는 한번만 실행되었음을 알 수 있습니다. b는 false이므로 그 뒤 어떤 값이 오더라도 결과는 false가 될것이고 따라서 함수를 실행하는게 무의합니다. '&&'연산자는 바로 이러한 조건의 논리를 그대로 적용하기 때문에 위와 같은 결과를 만들어낼 수 있습니다.
위에서 설명한 조건부 논리 연산자의 특징 때문에 이를 단락(Short-circuiting)으로 표현하기도 합니다. 조건부 논리 연산자는 적절히 사용하면 Application을 더욱 효휼적으로 작성할 수 있지만 자칫 예기치 않은 bug를 만들어낼 가능성도 있습니다. 특히 함수가 항상 실행되어야 한다거나 내지는 실행된다라고 예상하는 경우 Application의 흐름을 전혀 다른 방향으로 이끌 수 있으므로 함수와의 결합하여 사용할때는 특히 주의해야 합니다.
4) Bit 연산자 및 이진 shift 연산자
Bit연산자는 2개 이상의 bit로 표현된 값에서 동일한 column의 각 bit를 비교하는 연산자이며, 이진 shift 연산자는 bit값을 이동시키는 연산자로서 이러한 특징을 이용하면 일반적인 산술연산자보다 더 빠른 속도로 산술연산을 수행할 수 있습니다.
해당 연산자의 예제를 만들어보기 위해 우선 아래와 같이 2개의 int형 변수를 선언합니다.
int i = 10;
int j = 4;
Console.WriteLine($"i : {i, 3} | {i:B8}");
Console.WriteLine($"j : {j, 3} | {j:B8}");
예제에서 숫자 3은 3개 column에 대한 오른쪽 정렬을, B8은 8자리 binary표시를 의미하므로 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

위 상태에서 bit연산자를 사용한 아래 예제를 추가합니다.
Console.WriteLine();
Console.WriteLine($"i & j : {i & j, 3} | {i & j:B8}");
Console.WriteLine($"i | j : {i | j, 3} | {i | j:B8}");
Console.WriteLine($"i ^ j : {i ^ j, 3} | {i ^ j:B8}");
Bit연산자는 &, |, ^ 를 사용할 수 있으며 연산방식은 이전에 설명한 논리연산자와 동일합니다. 즉, 각 값의 bit를 비교해 &인 경우 AND이므로 동일한 값에 1을, |는 OR이므로 둘중 하나라도 1이면 1을, ^는 XOR이므로 둘중 하나만 1인 경우 1을 표시합니다.
&, | 연산자를 산술값에 적용하면 bit연산자가 되지만 boolean값에 적용하면 논리연산자가 됩니다.

이제 다음으로 Shift 연산자를 사용한 아래의 구문을 추가합니다.
Console.WriteLine();
Console.WriteLine($"i << 3 : {i << 3} | {i << 3:B8}");
Console.WriteLine($"i * 8 : {i * 8} | {i * 8:B8}");
Console.WriteLine($"j >> 1 : {j >> 1} | {j >> 1:B8}");
Console.WriteLine($"j - 2 : {j - 2} | {j - 2:B8}");
예제를 실행하면 다음과 같은 결과를 표시합니다.

예제에서 'i << 3'은 왼쪽 i값의 bit를 왼쪽으로 3column 이동하도록 합니다. 이동하면서 왼쪽으로 밀려난 값은 버려지며 오른쪽은 0으로 채워집니다. 그런데 예제의 결과에서 나타난것 처럼 이러한 동작은 값에 곱하기 8(* 8)을 한것과 동일한 결과를 만들어냅니다. 왼쪽부터 첫번째 1값은 64이고 두번째 1값은 16이기 때문에 결과는 80이 되기 때문입니다. 변수 j에 대한것도 마찬가지인데 'j << 1'은 오른쪽으로 1column이동하도록 하며 그 결과는 산술연산에서 -2와 동일한 결과를 나타냅니다. shift연산자는 산술연산과 동일한 결과를 만들어내면서도 더 빠른 동작을 수행할 수 있다는 특징이 있습니다.
5) 기타 연산자
nameof은 변수나 type, member에 대해 약식이름을 반환하는 연산자이며 sizeof는 type에서 필요로 하는 memory size를 byte단위로 반환하는 연산자입니다. 기술적으로 sizeof는 unsafe code block이 필요하지만 type을 지정할때 int나 double와 같은 C# 별칭을 사용하면 unsafe를 사용하지 않아도 됩니다.
int i = 10;
Console.WriteLine($"변수 {nameof(i)}는 약 {sizeof(int)} byte의 memory를 필요로 합니다.");
이 밖에도 type의 member를 가져오기 위해 사용하는 dot(.)도 'member 접근 연산자'라고 하는 연산자이며 method의 뒤에 붙이는 괄호(())역시 호출 연산자(Invocation operator)라고 하는 연산자에 해당합니다.
int i = 10;
string iname = i.ToString();
2. 선택문(분기문)
선택문은 서로 다른 code간의 분기를 위한 구문을 말합니다. C#에서 사용가능한 선택문으로는 if와 switch문이 있는데 이 둘은 '선택'이라는 측면에서 비슷하지만 swicth는 일부 일반적인 상황에서 if보다 더 code를 단순화시키면서도 효휼적인 선택처리가 가능합니다. 곧 보게 되겠지만 switch는 특히, 하나의 변수에 대해 각기 다른 처리를 필요로 하는 다양한 값을 가질 수 있는 경우에 유용하게 사용할 수 있습니다.
1) if를 사용한 분기
if문은 boolean 형식의 문을 평가한 뒤 그 결과를 토대로 분기를 결정하는 것으로서 만약 평가 결과가 true라면 내부의 block문을 실행하게 됩니다. else문은 평가 결과가 false인 경우 실행하는데 이는 선택적이라 필요한 경우에만 추가할 수 있습니다. 또한 if문은 충첩될 수 있습니다.
if문은 아래와 같은 형식으로 구현할 수 있습니다.
if (조건식1)
{
//조건1이 true인 경우 실행
}
else if (조건식2)
{
//상기 조건1이 false이고 조건2가 true인 경우 실행
}
else
{
//상기 모든 조건이 false인 경우 실행
}
각 if문의 조건식은 독립적으로 구현할 수 있으며 switch와는 달리 단일값을 사용할 필요는 없습니다. 또한 else if와 else는 선택적으로 필요한 경우에만 사용합니다.
아래 예제는 if문을 사용해 비밀번호를 학인하고 그에 따른 처리를 수행하는 방법을 나타내고 있습니다.
string password = "abc123";
string userInput = Console.ReadLine();
if (userInput == password)
{
Console.WriteLine("비밀번호가 일치합니다.");
}
else
{
Console.WriteLine("비밀번호가 일치하지 않습니다.");
}
예제를 실행하면 다음과 같은 결과를 표시할 것입니다. 결과는 입력한 값에 따라 달라질 수 있습니다.

(1) 괄호사용에 관해
Block은 기본적으로 괄호를 통해 표현되며 C#명세에서는 block을 복합문(compound statement)으로 언급하고 있습니다. 본래 block안의 실행문은 단일문이 와야하지만 이를 중괄호를 사용해 여러 구문으로 묶어 놓을 수 있다는 것을 나타내는 것입니다.
if, else, while 및 for와 같은 C#의 모든 제어문에서는 괄호를 사용하지 않고 단일문을 사용하되 그 이상의 문이 필요하다면 괄호를 사용해 하나의 덩어리로 묶어놓아야 합니다. 이러한 것을 삽입문(embedded statement)이라고 하는데 block이나 복합문은 이 삽입문중 하나이며 괄호를 사용하지 않는 단일문 역시 또 다른 style의 삽입문에 해당합니다.
띠라서 위 예제를 단일구문으로 변경하는 경우 괄호를 제거하고 아래와 같이 구현할 수 있습니다.
string password = "abc123";
string userInput = Console.ReadLine();
if (userInput == password)
Console.WriteLine("비밀번호가 일치합니다.");
else
Console.WriteLine("비밀번호가 일치하지 않습니다.");
보기에는 언뜻 code가 깔끔해 보일 수 있지만 사실 위와 같은 coding style은 중대한 결함을 만들 수 있기에 피하는 것이 좋습니다. 단지 예제에서의 if문뿐만 아니라 단일문을 허용하는 모든 제어문에서 괄호를 사용하지 않는 단일문의 사용은 권장되지 않습니다.
괄호를 사용하지 않아도 된다고 해서 꼭 그렇게 해야하는 것은 아닙니다. 괄호를 사용하지 않음으로인해 code가 효휼적으로 변하는 것도 아닙니다. 오히려 code를 읽기 어렵게 하고 관리성도 떨어지며 잠재적으로 위험한 code를 만들어낼 수도 있습니다. 예제에서는 공간의 활용을 위해 괄호를 생략할 수 있지만 실제 그러한 code를 작성하지 않도록 해야 합니다.
(2) if문에서의 조기반환 또는 guard절(Early return or guard clause)
'조기반환 또는 guard절'은 값을 반환하는 method안에서 if문을 사용하는 경우 상황에 따라 else를 사용하기 보다 if문의 조건이 만족하면(또는 만족하지 않으면) 곧장 값을 반환함으로서 code의 중첩이 깊어지는 문제를 해결하고자 하는 방법입니다.
예를 들어 아래와 같은 Method가 있다면
public bool IsSchoolZoneSpeeding(int speed)
{
if (speed > 30)
{
return true;
}
else
{
return false;
}
}
위의 예제를 조기반환 또는 guard절로 변경하게 되면 아래와 같이 구현할 수 있습니다.
public bool IsSchoolZoneSpeeding(int speed)
{
if (speed > 30)
{
return true;
}
return false;
}
위와 같은 방식은 가급적 중첩을 줄임으로서 code의 가독성을 향상시킬 수 있고 잘못된경우 혹은 경계조건에서 조기에 단락을 실행함으로서 code실행의 효휼성을 가져올 수 있습니다. 또한 code상에서 가장 우선적이거나 중요한점을 부각할 수 있다는 이점도 있습니다.
특히 이러한 방식은 ASP.NET Core와 같은 framework에서 조기에 입력값이나 상태의 유효성을 검증하고 사전에 값을 반환함으로서 code의 흐름을 간소화하고자 하는 방법으로 아래와 같이 광범위하게 사용되고 있습니다.
public IActionResult ProcessOrder(Order order)
{
if (order == null)
{
return BadRequest();
}
if (!order.IsValid())
{
return UnprocessableEntity();
}
// Normal processing
_orderService.Save(order);
return Ok();
}
위의 code에서 볼 수 있는 것처럼 else를 사용하지 않으면서 각각 잘못된 경우에 도달했을때 이를 처리하여 즉각적으로 method를 빠져나오도록 code를 설계하고 있습니다.
(3) 조건의 분리
if문에서 조건문을 사용할때 조건문 자체가 너무 길어지는 경우 이를 전부 if문안에 넣기 보다 따로 분리하여 지정하는 편이 좋습니다. 예를 들어
string password = Console.ReadLine();
if (password.Length >= 8 &&
char.IsLetterOrDigit(password[0]) == false &&
password.Any(char.IsDigit))
{
//
}
위와 같은 if문에서는 입력된 비밀번호가 8자 이상이면서 첫글자가 특수문자이고 숫자가 포함되어 있는지 여부를 확인하고 있습니다. 이렇게 복작한 조건의 경우는 아래와 같이 분리하면 code의 가독성을 훨씬 향상시킬 수 있습니다.
string password = Console.ReadLine();
bool isPasswordVaild = password.Length >= 8 && //8자이상
char.IsLetterOrDigit(password[0]) == false && //첫글자의 특수문자여부
password.Any(char.IsDigit); //숫자포함여부
if (isPasswordVaild) //비밀번호의 유효성 검증
{
///
}
(4) if문에서의 pattern matching
Pattern matching은 C#7에서 도입된 기능으로 if문에서는 is keyword를 사용해 변수의 선언과 결합함으로서 code를 더 안전하게 만들 수 있습니다.
object s = "2";
int i = 1;
if (s is int j)
{
Console.WriteLine($"{i + j}");
}
else
{
Console.WriteLine("변수 s는 int type이 아닙니다.");
}
위 예제에서는 if문의 조건식에서 's is int j'를 통해 is keyword를 사용하고 있습니다. 이렇게 하면 변수 s의 type을 확인하고 만약 type이 int라면 s의 값을 변수 j에 할당하게 됩니다. 그리고 이렇게 할당된 j는 if문 내부에서 자유롭게 사용할 수 있습니다. 여기서 중요한점은 이러한 방식으로 지역변수 j를 사용하면 최소한 j가 int형이라는 점을 보장할 수 있기 때문에 int에서 수행할 수 있는 작업을 더 안전하게 수행할 수 있다는 것입니다.
예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

object s에 들어가는 값이 처음부터 "2"와 같은 방법을 통해 문자열로 저장하고 있으므로 s는 문자열 type이 되고 따라서 위와 같은 결과를 표시하게 됩니다.
위 예제를 "2" 대신 아래와 같이 변경하여 s변수에 2의 값이 int으로 저장되도록 하고
object s = 2;
예제를 다시 실행하면 이번에는 다른 결과를 표시하게 될 것입니다.

is 연산자에 대한 더 자세한 내용은 아래 link를 참고하시기 바랍니다.
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/is
The `is` operator - Match an expression against a type or constant pattern - C# reference
Learn about the C# `is` operator that matches an expression against a pattern. The `is` operator returns true when the expression matches the pattern.
learn.microsoft.com
2) Switch
Switch문은 하나의 단일값을 여러개의 가능한 다른 값으로 비교하고 해당 값의 case문으로 처리를 분기하는 분기문입니다. 이때 각 case문은 아래의 방법중 하나로 끝을 맺어야 합니다.
- break keyword 사용
- goto case keyword 사용
- 어떠한 구문도 없음
- 다른 label을 참조하는 goto keyword 사용
- 현재 처리를 벗어나는 return keyword 사용
아래는 일반적인 switch의 사용법을 나타내는 예제입니다. 각 case문에서 위와 같은 규칙이 적용되어 있음에 주목하시기 바랍니다.
int i = 2;
switch(i)
{
case 1:
Console.WriteLine("1");
break;
case 2:
Console.WriteLine("2");
goto case 1;
case 3:
case 4:
goto Jump;
default:
Console.WriteLine("etc...");
break;
}
Jump:
Console.WriteLine("jumped!");
예제에서 goto keyword는 다른 case나 지정한 label로 code의 흐름을 이동시키기서 사용됩니다. 그런데 대부분의 개발자들사이에서 goto문은 소위 'spaghetti code'를 만들 수 있다는 인식때문에 피해야할 대상으로 여겨지고 있습니다. 하지만 일부 scenario에서는 최선의 해결책이 될 수도 있습니다. 실제 Microsoft에서도 .NET Class library에 goto문을 사용하곤 합니다.
GitHub · Change is constant. GitHub keeps you ahead.
Join the world's most widely adopted, AI-powered developer platform where millions of developers, businesses, and the largest open source community build software that advances humanity.
github.com
goto는 편리한 만큼 code를 복잡하게 만들 수 있다는 양면성을 가지고 있으므로 남발하지 말고 꼭 필요한 곳에만 쓰도록 해야 합니다.
예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

상기 결과는 i변수에 설정되는 값에 따라 달라질 수 있습니다.
'Jump:' Label문은 goto문의 의해 이동되어 실행될 뿐만 아니라 위에서 아래로 흐르는 code의 흐름을 통해 실행될 수도 있습니다.
(1) Switch문에서의 pattern matching
if문과 마찬가지로 switch문에서도 pattern matching을 지원합니다. 따라서 case문에서는 기존 literal값대신 pattern을 대신 사용할 수 있습니다.
C#7 부터 class의 하위 type에 기반하여 code를 더 간결하게 분기할 수 있게 되었으며 지역변수의 선언과 할당을 통해 이를 더 안전하게 사용할 수 있습니다. 또한 case에서는 when keyword를 도입함으로서 더 세부적인 pattern matching을 수행할 수 있습니다.
우선 switch에서의 pattern matching을 사용해 보기 위해 아래와 같은 class를 정의합니다.
class에 관한 자세한 사항은 추후에 자세히 알아볼 것입니다.
class Car
{
public string ModelName;
public int MaxSpeed;
public DateTime Release;
}
class Sedan : Car
{
public bool IsGasoline;
}
class Truck : Car
{
public bool IsDiesel;
}
예제에서 Sedan과 Truck은 Car의 하위 class(sub type)이며 Car는 최상위 부모 class에 해당합니다.
이제 아래와 같이 Cars 배열을 선언하고 각 type에 따라 분기하여 type과 속성명을 표시하도록 합니다.
var cars = new Car?[] {
new Sedan {
ModelName = "Sonta", MaxSpeed = 200, Release = new( year: 2019, month: 12, day: 04 )
},
null,
new Sedan {
ModelName = "Avan", MaxSpeed = 180, Release = new ( year: 2022, month: 05, day: 15 ), IsGasoline = false
},
new Truck {
ModelName = "Manbo", MaxSpeed = 150, Release = new ( year: 2012, month: 03, day: 29 )
}
};
foreach (Car? car in cars)
{
switch (car) {
case Sedan speedCar when speedCar.MaxSpeed == 200:
Console.WriteLine($"{speedCar.ModelName}이 가장 빠른 차량임");
break;
case Sedan gCar when gCar.IsGasoline = false:
Console.WriteLine($"{gCar.ModelName}차량은 휘발유를 사용하지 않음");
break;
default:
Console.WriteLine("그 외");
break;
case null:
Console.WriteLine("개체가 null상태임");
break;
case Truck truck:
Console.WriteLine($"{truck.ModelName}은 {truck.Release.Year}년에 출시된 truck임.");
break;
}
};
예제를 보면 default를 switch가운데 정의하였는데 이는 이런것도 가능하다는 것을 보여주기 위해 고의적으로 배치한 것이므로 실제 switch문을 작성할때는 default를 항상 마지막에 정의하여야 합니다.
'case Sedan speedCar when speedCar.MaxSpeed == 200;' 해당 구문은 속성 pattern matching을 통해 아래와 같이 더 간소하게 구현할 수 있습니다.
'case Sedan { MaxSpeed : 200 } speedCar:'
예제에서는 Car? type의 cars array를 정의하였으므로 Car type을 상속받은 모든 하위 type이 올 수 있습니다. 또한 '?'문자를 통해 null가능한 type이 되었으므로 null역시 수용할 수 있습니다. 따라서 위에서 정의한 array의 순회하면서 각각의 조건에 맞는 type에 따라 아래와 같은 결과를 표시할 것입니다.

(2) switch 표현식
switch 표현식은 C#8부터 사용가능한 것으로 일반적인 switch문을 더욱 간소화할 수 있습니다.
위 예제와 같은 switch문이 그리 복잡한 것은 아니지만 지금 소개할 switch 표현식은 각 case문에서 값을 반환하는 기존 방식과 달리 lambda(=>)를 사용하여 반환값을 나타냄으로서 구문자체를 간소화하기 때문입니다.
아래 예제는 이전 switch 예제에서 switch 표현식을 적용한 결과를 나타내고 있습니다.
string msg;
foreach (Car? car in cars)
{
msg = car switch
{
Sedan speedCar when speedCar.MaxSpeed == 200 => $"{speedCar.ModelName}이 가장 빠른 차량임",
Sedan gCar when gCar.IsGasoline == false => $"{gCar.ModelName}차량은 휘발유를 사용하지 않음",
Truck truck => $"{truck.ModelName}은 {truck.Release.Year}년에 출시된 truck임.",
null => "개체가 null상태임",
_ => "그 외"
};
Console.WriteLine(msg);
};
상기 예제와 이전의 swicth와의 가장 큰 차이는 case와 break가 존재하지 않는다는 것입니다. 또한 예제에서 underscore(_)문자는 이전 case에서의 default에 해당하며 이를 'discard'라고 합니다. 관련된 더 자세한 사항은 아래 link를 참고하시기 바랍니다.
Discards - unassigned discardable variables - C# | Microsoft Learn
Discards - unassigned discardable variables - C#
Describes C#'s support for discards, which are unassigned, discardable variables, and the ways in which discards can be used.
learn.microsoft.com
예제를 실행하면 이전과 동일한 결과를 표시할 것입니다.
3. 반복문
반복문은 구문의 block을 반복하는 구문으로서 while이나 for문에서 조건이 true면 반복을 실행하며, foreach문에서는 collecction안의 각 item을 순회하는 목적으로 사용됩니다. for, while 또는 foreach를 선택하는 기준은 현재 문제를 해결하기에 가장 적합한 것과 개인적인 선호도에 따라 달라질 수 있으며 딱히 정재진 규칙은 없습니다.
1) while
while문은 boolean식을 평가한 뒤 그 결과가 true면 계속해서 해당 block을 반복하는 구문입니다.
아래 예제는 while을 사용하는 간단한 예제를 나타낸 것입니다. 주어진 조건(i < 10)이 false를 반환할때까지 내부 {}사이의 code를 계속 반복하게 되며 결과적으로 0부터 9까지 숫자를 표시하고 동작을 종료할 것입니다.
int i = 0;
while (i < 10)
{
Console.WriteLine(i);
++i;
}
예제를 실행하면 다음과 같은 결과를 표시합니다.

2) do
위에서 언급한 while문은 시작할때부터 boolean조건을 평가하지만 do는 일단 한번 실행한 뒤에 그 다음 boolean조건을 평가한다는 차이가 있습니다. 이것 외에는 동작방식이 거의 while과 일치합니다.
아래 예제는 do를 사용하는 간단한 예제를 나타낸 것입니다.
int right = 5;
int answer = 0;
do
{
Console.WriteLine("2 + 3 = ?");
answer = int.Parse(Console.ReadLine());
}
while(right != answer);
do문은 일단 내부의 구문을 최소 한번은 실행하게 됩니다. 따라서 boolean식 조건도 위가 아닌 하단에 위치하게 됩니다.
예제를 실행하면 다음과 같은 결과를 표시할 것입니다. Application은 사용자에게 질문의 답을 요청하게 되고 그 정답이 입력될때까지 같은 질문을 반복하게 됩니다.

3) for
for문은 아래와 같은 조합으로 만들어집니다.
| 초기화 표현식 | loop를 시작할때 한번은 실행되는 부분으로 반복문의 초기값을 설정합니다. 별다른 초기화가 필요하지 않으면 생략할 수 있습니다. |
| 조건 표현식 | loop가 실행될때 마다 실행되는 부분으로 반복문의 실행여부를 확인하는 부분입니다. 만약 해당 표현식에서 true를 반환한다면 loop는 다시 재실행됩니다. 경우에 따라 생략이 가능한데 자칫 무한 loop가 될 수 있으므로 내부실행부분에서 loop를 빠져나가기 위한 처리를 추가해야 합니다. |
| 반복 표현식 | loop의 실행이 종료될때마다 실행되는 부분으로 대부분 값의 증가나 감소처리를 진행합니다. |
아래 예제는 for문을 사용하여 1부터 10까지 숫자를 나타내도록 하고 있습니다.
for (int i = 1; i <= 10; i++)
{
Console.WriteLine(i);
}
예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

예제에서 'int i = 1'이 초기화, 'i <= 10'이 조건, 'i++'이 반복 표현식에 해당합니다. 만약 이 모두를 생략하고 for문을 작성해야 한다면 다음과 같이 작성할 수 있습니다.
for (;;)
{
...
}
4) foreach
상기 for문은 값의 변화에 따른 반복을 수행하지만 foreach문은 array나 List와 같이 배열을 열거하는데 사용됩니다. 이때 열거대상인 배열은 읽기전용상태가 되므로 만약 기존 item을 삭제하거나 추가하려고 시도하면 예외를 발생시키게 됩니다.
아래 예제는 foreach문을 통해 문자열값의 List를 대상으로 열거하며, 각각의 item을 표시하고 있습니다.
List<string> fruits = ["Apple", "Banana", "Strawberry"];
foreach (string fruit in fruits)
{
Console.WriteLine(fruit);
}
위 예제를 실행하면 다음과 같이 표시할 것입니다.

List및 관련 내용에 관한 더 자세한 사항은 추후에 알아볼 것입니다.
(1) foreach문의 동작방식
만약 array나 collection과 같이 다수의 item을 표현하는 type을 정의하고자 한다면 해당 type이 foreach문을 통해 열거가능함을 알려줄 필요가 있고 따라서 아래와 같은 규칙이 준수되어야 합니다. 아무리 foreach라도 주어진 type이 열거가능함을 foreach스스로가 알 수 있는 방법이 없기 때문입니다.
ⓐ type은 object를 반환하는 GetEnumerator라는 이름의 method를 가지고 있어야 합니다.
ⓑ ⓐ에서 반환된 개체는 반드시 Current라는 속성과 MoveNext라는 method를 가지고 있어야 합니다.
ⓒ MoveNext method는 반드시 Current속성값을 변경해야 하며 열거 가능한 item이 존재하는 경우 true를 그렇지 않으면 false를 반환해야 합니다.
.NET에서는 위와 같은 규칙을 어떤 type이든 일관성있게 적용하기 위해 IEnumerable과 IEnumerable<T> interface를 제공하고 있으며 해당 interface를 통해 상기 규칙을 정의하고 있습니다.(다만 해당 interface의 규칙을 강제하지는 않습니다.)
사실 compiler는 이전 예제와 같은 code를 아래와 같은 형식으로 처리하고 있는데
IEnumerator e = fruits.GetEnumerator();
while (e.MoveNext())
{
string fruit = (string)e.Current;
Console.WriteLine(fruit);
}
보시는 바와 같이 iterator와 해당 읽기전용의 Current속성을 사용하기 때문에 foreach문안에 선언된 변수는 현재 item의 값을 변경하는데 사용할 수 없습니다.
또한 예제에서는 fruits의 변수에 List<string> type을 사용하였는데 만약 이 type이 아래와 같이 단순 배열(array)이라면
string[] fruits = ["Apple", "Banana", "Strawberry"];
Compiler는 배열이 IEnumerable<T> interface를 구현한다는 것을 무시하고 이를 사용하는 대신 조금 더 효휼적으로 array의 Length속성을 사용하는 for문을 작성하여 아래와 같이 구현합니다.
for (int len = 0; len < fruits.Length; len++)
{
Console.WriteLine(fruits[len]);
}
4. 배열(Array)
동일한 Type이면서 다수의 값을 한꺼번에 저장해야 한다면 array를 사용할 수 있습니다. 이전 예제에서도 문자열 type의 과일 이름을 다수로 저장하는데 array를 사용하였습니다.
1) 1차원 Array
다수의 값을 한꺼번에 저장하는 array는 보통 내부 요소를 저장할때 최초 0부터 시작하는 zero 하한선(lower bound of zero)을 사용하므로 각 요소는 0부터 3까지 index값을 가지고 저장됩니다. 따라서 마지막 item의 index는 array의 크기의 -1값을 됩니다.
다음 이미지는 1차원 array가 값을 저장할때 상태를 나타낸 것입니다.

Array가 item을 저장할때 항상 0부터 시작된다고 단정지어서는 안됩니다. .NET에서 array의 가장 일반적인 type은 []문법을 사용하는 szArray(Single-dimensional Zero-indexed array)이지만 다차원 array인 mdArray도 제공하고 있으며 이는 'zero 하한선'을 사용하지 않습니다. 물론 흔하게 사용되는건 아니지만 이런것도 있다는 것을 알려드리고자 합니다.
아래 예제는 1차원 array에 4개의 문자열값을 저장하고 이를 for문으로 순회하면서 해당 요소를 화면으로 표시하도록 하고 있습니다.
string[] fruits;
fruits = new string[4];
fruits[0] = "Apple";
fruits[1] = "Banana";
fruits[2] = "Watermelon";
fruits[3] = "Orange";
for (int i = 0; i < fruits.Length; i++)
{
Console.WriteLine($"{fruits[len]} of {i}");
}
위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

예제를 보면 array를 선언한 뒤에 아래와 같이 array의 instance를 생성하고 있습니다.
fruits = new string[4];
Array는 memory에 할당되는 시점에 항상 고정된 값의 크기를 지정해야 합니다. 따라서 array를 선언하고 할당하기 전에는 해당 array에 얼마나 많은 item을 담을 것인지 예상하고 그에 맞는 크기를 지정하는 것이 필요합니다. 또는 array를 선언하고 지정된 item을 아래와 같이 저장하면 자동으로 해당 item의 수만큼 array의 size를 확보하기 때문에 좀더 간결하게 array를 초기화 할 수 있습니다.
string[] fruits = { "Apple", "Banana", "Watermelon", "Orange"};
2) 다차원 Array
string이나 다른 data type을 하나의 줄로만 계속 연결해서 저장하려면 1차원 array만으로 충분하지만 array를 통해 table형태로 값을 저장하려면 다차원 array가 필요합니다.
예를 들어 array를 2차원으로 구성한다면 아래와 같은 그림으로 표현할 수 있습니다.

위 그림은 다차원중에서도 array를 2개 사용하는 2차원을 표현한 것인데 이를 code를 바꾸면 아래와 같이 구현할 수 있습니다.
string[,] twoArray =
{
{ "a", "b", "c", "d" },
{ "aa", "bb", "cc", "dd" },
{ "aaa", "bbbb", "ccc", "ddd" }
};
'string[,]'구문에서 comma가 사용되었는데 이를 통해 string type의 array를 2차원으로 선언한 것이 됩니다. 혹은 다음과 같은 방법이 사용되기도 하는데
string[,] twoArray = new string[3, 4];
twoArray[0, 0] = "a";
twoArray[0, 1] = "b";
twoArray[0, 2] = "c";
twoArray[0, 3] = "d";
twoArray[1, 0] = "aa";
twoArray[1, 1] = "bb";
twoArray[1, 2] = "cc";
twoArray[1, 3] = "dd";
twoArray[2, 0] = "aaa";
twoArray[2, 1] = "bbb";
twoArray[2, 2] = "ccc";
twoArray[2, 3] = "ddd";
위 방법은 array의 선언과 할당을 분리하는 것으로, 사실 array의 차원을 이해하는데는 오히려 위와 같은 code가 더 도움이 될 수 있습니다. 또한 database나 file과 같이 외부에서 runtime에 값을 설정해야 하는 경우라면 이러한 방법을 사용해야 합니다.
array를 선언할때 []안에서는 comma(,)를 기준으로 첫번째 숫자가 첫번째 차원의 길이를, 두번째 숫자가 두번째 차원의 길이를 나타냅니다. 요소를 가져올때 붙이는 최대상한선의 index를 의미하는 것이 아니므로 주의해야 합니다.
array에서 각 차원의 하한선과 상한선은 다음과 같이 해당 array의 GetLowerBound와 GetUpperBound라는 2개의 method를 통해 확인할 수 있습니다.
Console.WriteLine($"1 dimension lower bound : {twoArray.GetLowerBound(0)}");
Console.WriteLine($"1 dimension uppper bound : {twoArray.GetUpperBound(0)}");
Console.WriteLine($"2 dimension lower bound : {twoArray.GetLowerBound(1)}");
Console.WriteLine($"2 dimension uppper bound : {twoArray.GetUpperBound(1)}");
위 예제를 실행하면 다음과 같이 표시할 것입니다.

이제 위 array에 대한 값을 어떻게 확인할 수 있을지 생각해 봅시다. 1차원 array에서는 단 하나의 for문만 있으면 되지만 위 예제처럼 2차원이라면 for문을 하나 더 추가해야 하며, 이렇게 만들어진 2개의 중첩된 for문을 통해 array를 순회하여 값을 표시할 수 있습니다.
for (int row = 0; row <= twoArray.GetUpperBound(0); row++)
{
for (int col = 0; col <= twoArray.GetUpperBound(1); col++)
{
Console.WriteLine($"{row} Row, {col} Column : {twoArray[row, col]}")
}
}
위 예제에서 바깥쪽 for문은 첫번째 차원을, 그 다음 for문은 두번째 차원을 순회합니다.
array를 선언할때의 예제를 보면 array를 초기화 할때 각 row와 column에 들어갈 모든 값을 지정하고 있습니다. 이렇게 하지 않으면 오류가 발생하는데 만약 중간에 값을 비워둬야 한다면 string type의 경우 string.Empty로 이를 대신할 수 있습니다.
string[,] twoArray =
{
{ string.Empty, "b", "c", "d" },
{ "aa", "bb", string.Empty, "dd" },
{ "aaa", "bbbb", "ccc", string.Empty }
};
string.Empty는 말그대로 공백을 의미합니다. 또는 null처럼 공백이 아닌 아예 없는 것으로 만들어야 한다면 'string?[,]'과 같이 null을 허용하게끔 array를 선언하고 null keyword로 값을 비워두면 됩니다.
string?[,] twoArray =
{
{ null, "b", "c", "d" },
{ "aa", "bb", null, "dd" },
{ "aaa", "bbbb", "ccc", null }
};
3) 가변배열(jagged arrays)
가변배열은 다차원배열과 비슷하지만 각각 차원이 다른 요소를 저장할 수 있는 배열을 말합니다.
가변배열을 시각화하면 다음과 같이 나타낼 수 있으며

이를 code화 하면 아래와 같이 구현할 수 있습니다.
string[][] jaggedArray =
[
[ "a", "b", "c" ],
[ "aa", "bb", "cc", "dd" ],
[ "aaa", "bbb" ],
];
만약 C#11 이전의 version을 사용한다면 위의 대괄호를 사용할 수 없고 아래와 같이 중괄호와 함께 new keyword를 사용하여 instance화 해야 합니다.
string[][] jaggedArray =
{
new[] { "a", "b", "c" },
new[] { "aa", "bb", "cc", "dd" },
new[] { "aaa", "bbb" }
};
가변배열역시 GetLowerBound()와 GetUpperBound() method를 통해 하산선과 상한선값을 가져올 수 있습니다.
for (int arr = 0; arr <= jaggedArray.GetUpperBound(0); arr++)
{
Console.WriteLine($"Upper bound of array {arr} : {jaggedArray[arr].GetUpperBound(0)}");
}
위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

값을 순회하는 방식역시 for문과 같은 반복문을 사용하면 됩니다. 다만 가변배열은 각각의 row마다 새로운 array가 들어가 잇는 셈이므로 각 array에 대한 상한선을 구해 요소를 순회하여야 합니다.
for (int row = 0; row <= jaggedArray.GetUpperBound(0); row++)
{
for (int col = 0; col <= jaggedArray[row].GetUpperBound(0); col++)
{
Console.WriteLine($"{row} Row, {col} Column : {jaggedArray[row][col]}");
}
}
위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

4) Array의 List Pattern Matching
위 예제에서는 어떠한 개체에 대해서 pattern matching을 사용해 어떤식으로 개체의 속성과 type을 비교할 수 있는지 살펴봤었습니다. 이러한 pattern matching은 array나 기타 collection에서도 적용할 수 있습니다.
C# 11에서 도입된 pattern matching은 public으로 정의된 Length나 Count 속성과 함께 int 혹은 System.Index 매개변수를 사용하는 indexer를 가진 type이라면 어떤 것에도 pattern matching을 적용할 수 있습니다.
indexer에 관한 자세한 사항은 추후에 알아볼 것입니다.
어떤 switch문안에서 다수의 list pattern을 정의할때는 가장 상세한 조건이 가장 먼저올 수 있도록 해야 합니다. 자세하지 않은 일반적인 pattern이 우선으로 오면 해당 pattern이 사전에 먼저 조건에 부합될 수 있고 결국 다음순서에 존재하는 상세 pattern까지 도달할 수 없게 될 것이기 때문입니다.
아래 표는 int값을 갖는 list로 가정할 경우 사용할 수 있는 list pattern의 예제를 나열하고 있습니다.
| [] | 빈 array 또는 collection과 일치합니다. |
| [..] | Array나 collection의 항목갯수가 0개이상일때와 일치합니다. 만약 '[]'와 '[..]'모두에서 조건이 거쳐가도록 하려면 이론상 '[..]'이 '[]'이후에 와야 합니다. |
| [int item1] or [var item1] | 지정된 단일 항목을 가진 list와 일치합니다. 또한 반환표현식을 통해 일치하는 항목의 값을 반환하므로 item1을 참조해 값을 사용할 수 있습니다. |
| [1, 2] | 정확히 지정한 2개의 항목을 지정한 순서대로 가진 list와 일치합니다. |
| [_, _] | 2개의 item을 가진 모든 list와 일치합니다. underscore(_)는 단일값을 의미합니다. |
| [var item1, var item2] | 2개의 item을 가진 모든 list와 일치하며 반환표현식에 의해 반환되는 값을 item1과 item2를 참조하여 사용할 수 있습니다. |
| [_, _, _] | 3개의 item을 가진 모든 list와 일치합니다. |
| [var item1, ..] | 하나 이상의 item을 가진 모든 list와 일치하며 반환표현식에 의해 반환되는 첫번째 item값을 item1을 참조하여 사용할 수 있습니다. '..'은 범위값을 의미합니다. |
| [var firstItem, .., var lastItem] | 2개의 이상을 가진 모든 list와 일치하며 반환표현식에 의해 반환되는 첫번째 item과 마지막 item값을 firstItem과 lastItem을 참조해 사용할 수 있습니다. |
| [.., var lastItem] | 하나 이상의 item을 가진 모든 list와 일치하며 반환표현식에 의해 반환되는 마지막 item값을 lastItem을 참조하여 사용할 수 있습니다. |
예제를 위해 우선 아래와 같은 int형 type의 array를 선언하고
int[] _1Numbers = {};
int[] _2Numbers = { 1, 2, 5, 6 };
int[] _3Numbers = { 1, 2, 3, 4 };
int[] _4Numbers = { 1, 2, 8 };
int[] _5Numbers = { 1, 2 };
int[] _6Numbers = { 9, 10, 11, 12, 13 };
int[] _7Numbers = { 99 };
patterm matching을 사용할 switch 표현식을 작성합니다.
static string filtering(int[] numbers) => numbers switch
{
[] => "_1NNumbers",
[1, 2, _, 6] => "_2Numbers",
[1, .., 4] => "_3Numbers",
[1, 2, 8] => "_4Numbers",
[int item1, int item2] => $"_5Numbers with {item1}, {item2}",
[9, .. int[] nums] => $"_6Numbers with {nums.Length}",
[..] => "etc.. _7Numbers"
};
이제 다음과 같이 각 array에 대해 filtering을 호출합니다.
Console.WriteLine(filtering(_1Numbers));
Console.WriteLine(filtering(_2Numbers));
Console.WriteLine(filtering(_3Numbers));
Console.WriteLine(filtering(_4Numbers));
Console.WriteLine(filtering(_5Numbers));
Console.WriteLine(filtering(_6Numbers));
Console.WriteLine(filtering(_7Numbers));
위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

C#6 부터는 code안에서 함수를 정의할 수 있는 식본문member(Expression-bodied function members)를 지원하고 있으며 예제에서는 이를 사용해 filtering method를 구현하고 있습니다. 또한 '=>'문자를 사용하는 lambda를 통해 함수로 부터의 값반환을 표현하고 있습니다.
여기서는 Pattern matching에 대한 모든 것을 설명하지 않았습니다. 더 자세한 사항은 아래 link를 참고해 주시기 바랍니다.
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns#list-patterns
Patterns - Pattern matching using the is and switch expressions. - C# reference
Learn about the patterns supported by the `is` and `switch` expressions. Combine multiple patterns using the `and`, `or`, and `not` operators.
learn.microsoft.com
4) Array의 List Pattern Matching
위 예제에서는 pattern matching을 사용해 어떤식으로 개체의 속성과 type을 비교할 수 있는지 살펴봤었습니다. 이러한 pattern matching은 array나 기타 collection에서도 적용할 수 있습니다.
C# 11에서 도입된 pattern matching은 public으로 정의된 Length나 Count 속성과 함께 int 혹은 System.Index 매개변수를 사용하는 indexer를 가진 type이라면 어떤 것에도 pattern matching을 적용할 수 있습니다.
indexer에 관한 자세한 사항은 추후에 알아볼 것입니다.
어떤 switch문안에서 다수의 list pattern을 정의할때는 가장 상세한 조건이 가장 먼저올 수 있도록 해야 합니다. 상세적이지 않은 일반적인 pattern이 우선으로 오면 해당 pattern이 사전에 먼저 조건에 부합될 수 있고 결국 다음순서에 존재하는 상세 pattern까지는 도달할 수 없을지 모르기 때문입니다.
아래 표는 int값을 갖는 list로 가정할 경우 사용할 수 있는 list pattern의 예제를 나열하고 있습니다.
| [] | 빈 array 또는 collection과 일치합니다. |
| [..] | Array나 collection의 항목갯수가 0개이상일때와 일치합니다. 만약 '[]'와 '[..]'모두에서 조건이 거쳐가도록 하려면 이론상 '[..]'이 '[]'이후에 와야 합니다. |
| [int item1] or [var item1] | 지정된 단일 항목을 가진 list와 일치합니다. 또한 반환표현식을 통해 일치하는 항목의 값을 반환하므로 item1을 참조해 값을 사용할 수 있습니다. |
| [1, 2] | 정확히 지정한 2개의 항목을 지정한 순서대로 가진 list와 일치합니다. |
| [_, _] | 2개의 item을 가진 모든 list와 일치합니다. underscore(_)는 단일값을 의미합니다. |
| [var item1, var item2] | 2개의 item을 가진 모든 list와 일치하며 반환표현식에 의해 반환되는 값을 item1과 item2를 참조하여 사용할 수 있습니다. |
| [_, _, _] | 3개의 item을 가진 모든 list와 일치합니다. |
| [var item1, ..] | 하나 이상의 item을 가진 모든 list와 일치하며 반환표현식에 의해 반환되는 첫번째 item값을 item1을 참조하여 사용할 수 있습니다. '..'은 범위값을 의미합니다. |
| [var firstItem, .., var lastItem] | 2개의 이상을 가진 모든 list와 일치하며 반환표현식에 의해 반환되는 첫번째 item과 마지막 item값을 firstItem과 lastItem을 참조해 사용할 수 있습니다. |
| [.., var lastItem] | 하나 이상의 item을 가진 모든 list와 일치하며 반환표현식에 의해 반환되는 마지막 item값을 lastItem을 참조하여 사용할 수 있습니다. |
예제를 위해 우선 아래와 같은 int형 type의 array를 선언하고
int[] _1Numbers = {};
int[] _2Numbers = { 1, 2, 5, 6 };
int[] _3Numbers = { 1, 2, 3, 4 };
int[] _4Numbers = { 1, 2, 8 };
int[] _5Numbers = { 1, 2 };
int[] _6Numbers = { 9, 10, 11, 12, 13 };
int[] _7Numbers = { 99 };
patterm matching을 사용할 switch 표현식을 작성합니다.
static string filtering(int[] numbers) => numbers switch
{
[] => "_1NNumbers",
[1, 2, _, 6] => "_2Numbers",
[1, .., 4] => "_3Numbers",
[1, 2, 8] => "_4Numbers",
[int item1, int item2] => $"_5Numbers with {item1}, {item2}",
[9, .. int[] nums] => $"_6Numbers with {nums.Length}",
[..] => "etc.. _7Numbers",
};
이제 다음과 같이 각 array에 대해 filtering을 호출합니다.
Console.WriteLine(filtering(_1Numbers));
Console.WriteLine(filtering(_2Numbers));
Console.WriteLine(filtering(_3Numbers));
Console.WriteLine(filtering(_4Numbers));
Console.WriteLine(filtering(_5Numbers));
Console.WriteLine(filtering(_6Numbers));
Console.WriteLine(filtering(_7Numbers));
위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

C#6 부터는 code안에서 함수를 정의할 수 있는 식본문member(Expression-bodied function members)를 지원하고 있으며 예제에서는 이를 사용해 filtering method를 구현하고 있습니다. 또한 '=>'문자를 사용하는 lambda를 통해 함수로 부터의 값반환을 표현하고 있습니다.
Pattern matching에 대한 보다 더 자세한 사항은 아래 link를 참고해 주시기 바랍니다.
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns#list-patterns
5) Trailing comma
위 예제를 다시 보면 마지막 '[..] => "etc.. _7Numbers"'끝에 comma가 붙어 있는데 이를 trailing comma라고 하며 comma뒤에 다른 요소가 더 나오지 않아도 compiler는 별다른 오류를 표시하지 않습니다.
C#과 같은 대부분의 언어에서는 이러한 방식의 comma를 허용하는데 위 예제처럼 다수의 item을 comma로 분리하는 경우(array나 collection초기화, enum 또는 switch표현식등)에 사용하며 향후에 comma를 추가하거나 제거하지 않아도 item순서를 쉽게 재정렬하거나 추가/삭제할 수 있습니다.
2008년 swicth표현식에서 trailing comma를 허용하는것에 대한 논의가 있었는데 관련 내용에 대해서는 아래 link를 참고 하시기 바랍니다.
https://github.com/dotnet/csharplang/issues/2098
Switch expressions currently disallow trailing comma · Issue #2098 · dotnet/csharplang
Update: LDM (1/9) decided to allow such trailing comma. @jcouv commented on Thu Dec 20 2018 Trailing commas are currently disallowed, which is inconvenient when copy/pasting a line. public class C ...
github.com
뿐만 아니라 JSON serializer도 이러한 방식을 사용할 수 있는 option을 가지고 있습니다.
https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions.allowtrailingcommas?view=net-10.0
JsonSerializerOptions.AllowTrailingCommas Property (System.Text.Json)
Get or sets a value that indicates whether an extra comma at the end of a list of JSON values in an object or array is allowed (and ignored) within the JSON payload being deserialized.
learn.microsoft.com
6) inline array
C#12에 도입된 inline array는 .NET Runtime team에서 성능향상을 위해 사용한 기능으로 아래와 같이 struct에 inline array특성을 지정하여 사용합니다.
[System.Runtime.CompilerServices.InlineArray(5)]
public struct InArr
{
private int _item;
}
위 예제를 실행하면 InlineArray를 통해 전달한 인수만큼 연속적인 메모리공간을 할당하여 배열을 생성하게 되며 일반적인 배열과 동일하게 다음과 같이 값을 설정할 수 있습니다.
InArr ia = new InArr();
for(int i = 0; i < 5; i++)
{
ia[i] = i;
}
배열에 들어간 값을 가져오는 방식도 크게 다르지 않지만 대신 InlineArray는 Length속성이 없기 때문에 자체적으로는 배열의 길이를 확인할 방법이 없습니다. 따라서 동적으로 배열의 길이를 확인해야 한다면 여러가지 방법이 있겠지만 아래와 같이 Span을 경유하는 방식이 가장 편리할 것으로 보입니다.
Span<int> si = ia;
for(int i = 0; i < si.Length; i++)
{
Console.WriteLine(si[i]);
}
inline array에 관한 좀더 자세한 사항은 아래 link를 참고 하시기 바랍니다.
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-12.0/inline-arrays
Inline arrays, or fixed sized buffers - C# feature specifications
Inline arrays provide a general-purpose and safe mechanism for declaring inline arrays within C# classes, structs, and interfaces.
learn.microsoft.com
7) Array와 collection
Array는 다수의 item을 임시적으로 저장하는데 유용하게 사용할 수 있지만 만약 item을 동적으로 추가하거나 삭제하는 경우가 있다면 array보다는 collection을 사용하는편이 효휼적일 수 있습니다. 그런데 반대로 item을 추가하거나 삭제할 일이 전혀 없다면 collection보다는 array를 사용하는 편이 훨씬 더 나은 선택입니다. Array는 내부의 item을 인접하게 저장함으로서 memory를 훨씬 더 효휼적으로 사용하며 따라서 collection보다 나은 성능을 낼 수 있습니다.
collection에 관한 자세한 내용은 추후에 알아볼 것입니다.
5. 형변환과 type변환
Programming에서 서로다른 type간 변수의 값을 바꾸는 경우는 매우 흔한 경우입니다. 예컨데 문자열형식으로 들어온 data는 처음에는 string type으로 저장되지만 곧 필요에 따라 숫자나 날짜등으로 얼마든지 변환하여 취급되는 것입니다.
숫자역시 비슷하지만 엄밀히 다른 type인 경우에도 변환이 필요합니다. int에서 double로 변환하는 경우처럼 계산하고자 하는 목적에 따라 그 data역시 달라져야 하는 것입니다.
형변환은 곧 type변환을 의미하기도 하며 결국 같은 의미로 통용됩니다. 그리고 이러한 변환은 그 변환의 방식에 따라 암시적(implicit)변환과 명시적(explicit)변환으로 나눌 수 있는데 암시적 변환은 자동적으로 발생하는 것으로서 비교적 안전하게 변환이 이루어지며 변환결과에 따른 data손실이나 변경이 발생하지 않습니다. 반대로 명시적 변환은 개발자가 의도적으로 변환하는 것으로서 특히 숫자에서 소수의 정밀도에 따라 data의 손실이 발생할 수 있는데 이는 compiler에게 이러한 손실이 발생할 수 있음을 알고있고 그걸 감수할 준비가 되었음을 알려주는 것입니다.
1) Number type 에서의 암시적 형변환과 명시적 형변환
int값을 double값으로 변환하는 암시적형변환은 어떠한 data의 손실이나 변형도 발생하지 않기 때문에 안전하게 이루어질 수 있습니다.
아래 예제가 int형에서 double형으로 암시적 변환이 수행되는 사례를 나타는 것인데, int형 변수 i를 double형 변수 d에 할당하는 순간 암시적인 형변환이 발생합니다.
int i = 10;
double d = i; //암시적 형변환
Console.WriteLine(i);
그럼 반대는 어떨까?
이번에는 double형 변수 d를 먼저 값을 할당하여 선언한 뒤 int형 변수 i로 암시적형변환이 이루어지도록 아래와 같이 code를 작성합니다.
double d = 10.99;
int i = d;
Console.WriteLine(i);
위 상태에서 예제를 실행하려 하면 아래와 같은 오류가 발생할 것입니다.
| error CS0266: Cannot implicitly convert type 'double' to 'int'. An explicit conversion exists (are you missing a cast?) |
결론만 얘기하면 double형에서 int형으로의 암시적형변환은 불가능합니다. int보다 double자체가 더 넓은 범위의 값을 가질 수 있을 뿐만 아니라 int는 점이하의 소수부분 정보를 담을 수 없기 때문에 형변환과정에서 data의 손실이 발생할 수 밖에 없기 때문입니다. 따라서 이런 경우에는 개발자가 형변환을 직접 지정하는 명시적형변환을 수행해야 합니다.
명시적형변환은 변환하고자하는 type을 괄호를 사용해 아래와 같이 직접 지정해 주는 방식입니다.
double d = 10.99;
int i = (int)d;
Console.WriteLine(i);
분명히 알아야할건 위와 같은 경우는 어떻게든 작동은 하겠지만 data의 손실과 그에 따른 결과를 개발자가 직접 책임져야 한다는 것입니다. 특히 큰값을 가진 type의 변수를 작은 값을 가지는 type의 변수로 변환할때는 전혀 예상치 못한 엉뚱한 값으로 변환이 수행될 수도 있음을 염두해 두어야 합니다.
long l = 100;
int i = (int)l;
Console.WriteLine(i);
위 예제는 long값에 100을 할당한 뒤 이를 int변수로 형변환을 수행하고 있습니다. 이런 경우에는 long type이라 하더다도 int에 충분히 수용할 수 있는 100의 값을 변환하는 것이기 때문에 아무런 문제가 되지 않지만
long l = long.MaxValue;
int i = (int)l;
Console.WriteLine(i);
위와 같이 int범위를 넘어서는 매우 큰값에 대해 형변환을 수행하면 아마도 아래와 유사한 결과를 표시할 것입니다.

또는 아래와 같이 overflow를 발생시켜 엉뚱한 값을 표시할 수도 있는데
long l = 5_000_000_000;
int i = (int)l;
Console.WriteLine(i);

이것은 int형 값을 binary로 표현하는 과정에서 발생하는 결과입니다.
(1) Binary에서 음수를 표현하는 방식
'long.MaxValue'를 사용한 예제의 결과에서 i의 값은 -1이 됩니다. 이렇게 되는 이유는 음수를 표현할때 binary에서 첫번째 bit를 부호를 나타내는데 사용하기 때문입니다. 첫번재 bit가 0이면 양수, 1이면 음수를 의미합니다.
아래 예제는 이를 확인하기 위한 것으로 i의 값을 양수5부터 음수5까지 순회하면서 각각의 값을 binary로 표시하고 있습니다.
for (int i = 5; i >= -5; i--)
{
Console.WriteLine($"{i} to bit => {i:B32}");
}
예제를 실행하면 다음과 같은 결과를 표시합니다.

예제에서 사용한 :B32는 32자리의 크기로 binary를 나타내기 위한 것입니다.
결과를 보면 모든 양수값에서 bit는 0으로 시작하며 모든 음수값에서 bit는 1로 시작함을 알 수 있습니다. 특히 -1값을 보면 모든 bit가 1임을 알 수 있는데 상기 예제에서 long의 Max값을 binary로 표현하면
long l = long.MaxValue;
Console.WriteLine($"{l} to bit => {l:B64}");
아래와 같이 되고

이를 int값으로 변환하게 되면 뒤의 1부분만 32bit로 들어가게 되므로 결국 예제의 i값은 -1이 됩니다. 즉, 큰값에서 작은값으로의 형변환시에 큰값의 bit가 버려지게 되는 것인데, 예를 들어 32bit int값을 16bit int값으로 형변환하게 되면 앞의 16bit에 해당하는 모든 bit가 버려지게 되고 나머지 16bit만 사용됩니다. 그리고 결국 여기에 해당하는 만큼의 값만이 남게 되는 것입니다.
5,000,000,000의 long값을 int로 변환하는 예제에서도 마찬가지입니다. 이러한 원리가 적용되면 결과적으로 707949866이라는 값을 표시하게 됩니다.
long l = 5_000_000_000;
int i = (int)l;
Console.WriteLine($"{l} to bit => {l:B64}");
Console.WriteLine($"{i:10} to bit => {i:B64}");

부호화된 수를 computer에서 표시하는 것과 관련해 더 자세한 사항을 알아보려면 아래 link를 참고하시기 바랍니다.
https://en.wikipedia.org/wiki/Signed_number_repr%EF%BB%BFesentations
2) System.Convert로 type변환하기
본래 type간 형변환은 비슷한류의 type안에서만 수행할 수 있습니다. 예를 들어 int는 byte나 long사이에서만 가능하며 class역시 동급이나 해당 class의 하위 class사이에만 가능합니다. 따라서 long을 문자열로 하거나 byte를 DateTime으로 변환하는 것은 암시적이든 명시적이든 불가능합니다.
이렇게 서로 다른 type간 변환을 수행하기 위해서는 조금 특별한 방법을 써야 하는데 그것이 바로 System.Convert를 사용하는 것입니다. System.Convert는 bool, DateTime, string등 뿐만 아니라 숫자관련 모든 type으로 부터 변환을 수행할 수 있습니다.
아래 예제는 System.Convert를 사용하여 변환을 수행한 것으로
long l = 1_000_000_000;
string s = System.Convert.ToString(l);
Console.WriteLine(s);
System.Convert의 ToString method를 통해 지정한 값을 문자열로 변환하고 있습니다. System.Convert는 이외 ToInt32나 ToBoolean등 다양한 변환 method를 가지고 있으므로 필요시에 적절한 method를 호출하여 사용할 수 있습니다.
변환을 말할때 흔히 Cast와 Convert가 많이 언급되는데 기본적인 개념은 동일하지만 Convert는 변환과정에서 overflow가 발생하면 이에 대한 예외를 발생시키지만 Cast는 예외대신 overflow를 허용한다는 차이가 있습니다.
(1) 반올림 규칙
실수를 정수형태로 형변환하는 경우는 소수점 이하 값을 어떻게 할지에 대한 결정이 필요한데, 이를 위해 적용되는 규칙이 존재합니다. 우리가 초등학생때 배운것을 생각해 보면 소수이하값이 .5이상이면 올림을, 그 보다 더 작은 값이면 내람을 하면 된다고 판단할 수도 있습니다. 물론 이치에 틀린말은 아니지만 이러한 방식은 경우에 따라 한쪽으로 올림이 몰리는 현상이 발생할 소지가 많습니다. 따라서 C#에서는 약간의 규칙을 적용하고 있습니다.
아래 예제는 C#에서 소수점이하값을 처리하기 위해 사용하는 규칙을 정확히 알아보기 위한 것으로서 몇개의 소수에 대한 변환을 수행하고 있습니다.
double[] values = { 2.49, 3.5, 4.51, -5.49, -6.5, -7.51 };
foreach(var item in values)
{
Console.WriteLine(Convert.ToInt32(item));
}
위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

결과를 보면 C#에서는 위에서 언급한 단순한 규칙을 따르고 있지 않다는 것을 알 수 있습니다. 예제에서 적용된 규칙을 살펴보면 우선 소수점값이 .5보다 낮으면 0으로 내림을 수행하고 .5보다 더 높으면 올림을 수행합니다. 여기까지는 별 다를바 없지만 만약 소수점이하값이 .5이고 정수부분이 음수라면 반올림을 수행하지만 정수부분이 양수라면 내림을 수행한다는 것을 알 수 있습니다. 이러한 규칙을 흔히 '은행가의 반올림'이라고 하며 반올림과 내림을 균등하게 교차시킬 수 있다는 이유로 선호되는 방식이기도 합니다. 다만 JavaScript와 같은 언어에서 처럼 '은행가의 반올림'규칙을 따르지 않는 것도 존재합니다.
(2) 반올림 규칙 조정하기
C#에서는 반드시 위와 같은 규칙만을 적용해야 하는 것은 아닙니다. 필요하다면 Math class의 Round method를 사용하여 이러한 규칙을 조정할 수 있습니다.
foreach(var item in values)
{
Console.WriteLine(Math.Round(item, 0, MidpointRounding.AwayFromZero));
}
아래 예제는 위 예제에서 Round method를 통해 MidpointRounding.AwayFromZero option으로 반올림규칙을 조정한 것입니다. 위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

결과에서 보듯 소수점이하 .5를 포함해 그 이상인 값은 모두 반올림되었음을 알 수 있습니다.
3) 특정 type에서 string으로 형변환 하기
어떤 type에서 string으로 변환하는 경우의 대부분은 읽을 수 있는 형태로 type을 나타내기 위한 것입니다. 이를 위해 모든 type은 System.Object class에서 파생된 ToString이라는 method를 갖고 있습니다.
변수에서 ToString은 해당 변수의 값을 문자열로 나타내지만 어떤 type의 경우에는 ToString으로 나타낼만한 정보가 없기 때문에 대신 자신이 속한 namespace와 이름을 표시하기도 합니다.
아래 예제는 몇가지 변수를 선언해두고 각각 자신의 ToString method를 호출하고 있습니다.
int i = 10;
bool b = true;
object o = new();
Console.WriteLine(i.ToString());
Console.WriteLine(b.ToString());
Console.WriteLine(o.ToString());
위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

그런데 위 예제는 사실 아래와 같이 ToString method를 생략할 수 있습니다.
Console.WriteLine(i);
Console.WriteLine(b);
Console.WriteLine(o);
특정 개체를 WriteLine method에 전달하면 암시적으로 string type의 형변환이 이루어지기 때문이며 따라서 명시적으로 ToString을 호출할 필요가 없습니다. 그러나 ToString method를 명시적으로 호출하면 string으로 발생하는 형변환의 boxing동작을 피할 수 있기 때문에 method를 호출하는 쪽이 성능면에서는 더 도움이 될 수 있습니다.
(1) Binary에서 string으로의 변환
Image나 동영상과 같은 media file은 그 자체가 binary로 이루어져 있는데 이러한 파일에 대한 전송과 저장은 아주 흔하게 발생합니다. 예를 들어 특정 image를 전송하고자 할때 binary자체의 bit를 그대로 보낼 수 있지만 bit를 있는 그대로 다룬다는건 생각보다 절차가 복잡하고 어려울 수 있습니다. 게다가 이기종간의 전송이라면 동일한 bit를 다르게 해석할 수 있다는 문제도 발생할 수 있습니다.
이러한 문제를 해결하기 위한 가장 안정적인 방식은 binary개체를 안정적인 문자열로 변환하여 처리하는 것이며 이때 가장 많이 사용되는 것이 바로 Base64 encoding입니다. Base64는 64개의 특정 문자집합을 사용해 임의의 bit data를 문자열로 변환하는 encoding scheme로서 data전송에 광범위하게 사용되는 방식입니다.
Base64를 사용하기 위해서는 Convert type을 사용해야 하며 Convert는 이를 위한 ToBase64String과 FromBase64String이라는 2개의 method를 갖고 있고 이 method를 통해 변환과 복원을 수행할 수 있습니다.
예제를 위해 우선 아래와 같이 byte배열 변수를 선언하고 이를 임의의 값으로 채워넣습니다.
byte [] b = new byte[128];
Random.Shared.NextBytes(b);
그런다음 각각의 개별값을 그대로 표시합니다.
for(int i = 0; i < b.Length; i++)
Console.Write(b[i] + " ");
여기까지 예제를 실행하면 아래와 같은 결과를 볼 수 있습니다. 결과는 채워진 값에 따라 다르게 보일 수 있습니다.

기본적으로 위 결과는 int값을 10진수로 표시합니다. 따라서 만약 이를 16진수로 표시하고자 한다면 :X2와 같은 서식 code를 사용해야 합니다.
만약 위와 같은 byte배열 data가 image binary data라고 가정한다면 다음의 방법을 통해 base64 문자열로 변환할 수 있습니다.
string s = Convert.ToBase64String(b);
Console.WriteLine(s);
위 예제는 다음과 같은 결과를 표시합니다.

(2) URL에서 Base64 사용하기
Base64는 위와 같은 상황에서는 매우 유용하게 사용될 수 있지만 특정한 상황에서는 문제를 일으킬 수 있습니다. 특히 query string을 갖고 있는 URL을 처리하는 경우에는 +나 -같은 문자가 문제가 될 수 있는데 Base64에서 이러한 문자는 조금 특수하게 취급되고 있기 때문입니다.
따라서 URL의 경우 Base64Url이라고 하는 전용 scheme를 사용해야 합니다. 기본적으로는 Base64와 비슷하지만 URL에서 문제될 수 있는 몇가지 문자를 약간 다르게 사용함으로서 URL을 좀더 안전하게 생성할 수 있습니다.
.NET9 에서는 새로운 Base64Url class를 도입하였고 여기에서 URL data를 encoding하고 decoding하기 위한 최적화된 method를 제공하고 있습니다.
아래 예제에서는 특정 URL을 통해 Base64Url을 사용하여 URL을 encode하고 다시 decode한뒤 그 결과를 확인하고 있습니다.
using System.Buffers.Text;
using System.Text;
string originalURL = "http://...url?find=name+age";
byte[] urlBytes = Encoding.UTF8.GetBytes(originalURL);
string encodedURL = Base64Url.EncodeToString(urlBytes);
Console.WriteLine("Encode : " + encodedURL);
byte[] urlEncodeBytes = Base64Url.DecodeFromChars(encodedURL);
string encodedUrl = Encoding.UTF8.GetString(urlEncodeBytes);
Console.WriteLine("Decode : " + encodedUrl);
위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

Base64와 관련된 더 자세한 사항은 아래 URL을 참고하시기 바랍니다.
https://base64.guru/standards/base64url
Base64URL | Base64 Standards | Base64
Base64URL Base64URL is a modification of the main Base64 standard, the purpose of which is the ability to use the encoding result as filename or URL address. The Base64URL is described in RFC 4648 § 5, where it is mentioned that the standard Base64 alphab
base64.guru
(3) 숫자나 날짜/시간 parsing하기위한 Parse 사용하기
문자열값으로 부터 숫자나 날짜 혹은 시간으로 변환하여 가져오는 경우는 아주 흔하게 발생하는 일입니다. 어떤 type에서 ToString method를 호출하는 것은 개체를 문자열로 나타내기 위한 것인데 이것과 반대되는 것이 바로 Parse입니다. 다만 Parse는 모든 type에 존재하지 않고 숫자나 DateTime과 같은 특정 type에만 존재합니다.
아래 예제에서는 문자열에서 각각의 Parse method를 통해 int값과 DateTime값을 가져오고 있습니다.
int i = int.Parse("100");
DateTime dt = DateTime.Parse("2026-02-02 10:30:20");
Console.WriteLine(i);
Console.WriteLine(dt);
위 예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

DateTime형식의 값을 사용할때는 표준 날짜/시간 서식지정자를 사용할 수 있습니다.
https://learn.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings#table-of-format-specifiers
(4) TryParse method를 통해 예외 처리하기
Parse method를 편리하기는 하지만 만약 지정한 문자열에서 원하는 값으로 변환할 수 없는 경우에는 예외를 발생시키게 됩니다.
예를 들어 아래와 같은 경우입니다. int의 Parse를 통해 문자열을 int형으로 변환하려 하고 있지만 지정한 문자열은 숫자가 안니 문자열을 갖고 있습니다.
int i = int.Parse("abc");
먄약 위의 예제를 실행하려 하면 다음과 같은 예외를 보게 될 것입니다.

위와 같이 Parse에서 발생할 수 있는 예외를 적절하게 처리하려면 try ~ catch문을 사용하거나 더 간단하게는 TryParse method를 대신 사용할 수 있습니다. TryParse는 주어진 문자열을 원한는 type으로 변환을 시도하고 변환이 가능하다면 true를, 그렇지 않으면 false를 반환합니다. 예외를 발생시키는 것은 비교적 비용이 많이 드는 동작이므로 되도록이면 이를 피하고자 TryParse를 사용하는 것입니다.
아래 예제는 TryParse를 사용해 문자열값으로부터 int type으로 변환을 수행하고 있습니다.
if (int.TryParse("abc", out int i))
{
Console.WriteLine(i);
}
else
{
Console.WriteLine("failed!");
}
예제에서 사용된 out keyword는 TryParse method가 정상적으로 변환하는 작업을 수행했을때 그 결과값을 담아둘 변수를 지정하기 위한 것입니다.
예제를 실행하면 다음과 같은 결과를 표시할 것입니다.

이외에도 문자열값을 다른 type으로 변환하기 위해 System.Convert method를 사용할 수 있지만 변환에 실패하는 경우 예외를 발생시킵니다.
모든 TryParse method는 변환의 성공과 실패를 나타내기 위해 bool type의 값을 반환하며, 변환에 성공하는 경우 값을 담기위한 out 매개변수를 가집니다. 이러한 규칙은 모든 try method에 동일하게 적용됩니다.
6. 예외처리
.NET Application에서는 어떤 예기치 않은 문제가 발생하면 '예외(Exception)'을 발생시킬 수 있습니다. 이때 발생한 예외를 적절히 처리하는 것을 예외처리라고 하며, 어떤 언어에서는 error가 발생하면 그에 해당하는 error code를 대신 반환하기도 합니다. .NET은 error가 발생한 경우에 예외라는걸 통해서 문제점을 좀더 잘 살펴볼 수 있도록 지원하고 있습니다.
Error code를 사용하는 경우 해당 값에 따라 여러가지로 판단할 수 있을 것입니다. 예를 들어 값이 음수인 경우 무엇인가 잘못되었음을 알리는 것이지만 양수값인 경우 정상처리되었음을 알려주는 의미가 될 수도 있습니다. .NET에서도 일부 third-party library의 경우 result type을 자체적으로 정의하여 이러한 값의 의미를 더 잘 이해할 수 있도록 해주는 경우도 있는데 예외를 사용하는 것 보다 이러한 방식을 더 선호하는 경우도 있습니다.
Application에서 예외가 발생하면 thread는 중지됩니다. 이때 예외를 발생시킨 code를 try~catch문안에서 구현했다면 자체적으로 예외를 처리할 수 있습니다. 만약 그렇지 않은경우 해당 code를 호출한 상위 stack으로 예외를 던지게 되며 그 상위에서도 적절한 예외처리가 구현되어 있지 않으면 또 다시 그 상위로 예외를 던지는 동작을 계속 진행하게 됩니다. 그러다가 더이상 거슬로 올라갈 수 있는 상위 stack이 존재하지 않으면 그때 사용자에게 stack trace정보를 포함해 예외 message를 표시하고 Application전체를 종료시키게 됩니다. 잠재적으로 잘못된 상태의 Application을 불안하게 유지하는 것보다 이러한 방식으로 처리하는 것이 더 좋은 선택일 수 있습니다.
if문과 같이 조건을 확인하는 방식을 통해 예외를 발생시킬 수 있는 code의 작성을 최소화하는 것이 좋습니다. 물론 문제가 되는 code를 호출하는 상위 component에서 예외를 처리하도록 하는 것이 더 나은 선택일 수 있으므로 상황에 따라 잘 판단해야 합니다.
.NET9 부터 예외는 NativeAOT 예외처리 model을 기반으로한 새로운 방식을 사용하고 있으며 이를 통해 이전보다 더 나은 성능을 그대할 수 있게 되었습니다.
1) try ~ catch
어떤 code가 error를 유발할 가능성이 있으면 이를 try~catch문으로 감싸둘 수 있습니다. 예를 들어 문자열에서 숫자값을 읽어들이는 위의 예제의 경우에도 Parse method를 사용하면서 try~catch문을 사용할 수 있습니다.
try~catch는 try와 catch 2개의 부분으로 구성되는데 try문안에 있는 code가 기본적으로 실행되면서 예외가 발생한 경우에만 catch문안에 있는 code를 실행하게 됩니다.
Console.WriteLine("start");
string? s = Console.ReadLine();
try
{
int i = int.Parse(s!);
Console.WriteLine($"input value : {i}");
}
catch (System.Exception)
{
}
Console.WriteLine("end");
위 예제는 문자열값을 int값으로 읽어들이는 시도를 하고 있는데 이 구문을 try~catch로 감싸고 있습니다.
또한 예제는 'warning CS8600: Converting null literal or possible null value to non-nullable type.'이나 'warning CS8604: Possible null reference argument for parameter 's' in 'int int.Parse(string s)'.'과 같은 경고 message를 표시할 수 있는데 이는 .NET6 부터 nullable 참조 type을 허용하고 있기 때문입니다. 물론 위 code가 실제 사용자에게 배포되는 application수준의 code라면 이러한 경고를 무시해선 안되고 null인 경우에 대한 적절한 처리를 추가해야 합니다. 그러나 예제는 사용자가 아무런 값을 입력하지 않는 경우에도 공백을 반환하므로 실제 null이 되지 않을 뿐더러 null을 확인하는 추가적인 code를 추가하면 code를 더 복잡하게 만들 수 있어 이를 생략하고 있습니다.
(1) null-forgiving 연산자를 통해 경고 message억제하기
위 예제에서의 compiler 경고를 억제하려면 s문자뒤에 느낌표(!)문자를 붙여주면 됩니다.
string? s = Console.ReadLine();
try
{
int i = int.Parse(s!);
Console.WriteLine($"input value : {i}");
}
catch (System.Exception)
{
}
이때 !를 'null-forgiving 연산자'라고 합니다. null-forgiving 연산자는 runtime에서 아무런 영향도 주지 않지만 만약 null이 발생한다면 예외를 발생시킬 것입니다.
예제를 실행하고 숫자를 입력하면 아래와 같은 결과를 볼 수 있습니다.

이번엔 다시 예제를 실행하고 숫자가 아닌 문자를 입력합니다. 그러면 결과를 약간 다르게 표시될 것입니다.

예제가 실행된 후 try안에 구문에서 예외가 발생하면 별다른 message를 표시하지 않고 application이 정상적으로 종료된다는 것을 알 수 있습니다. 물론 이러한 동작도 나쁘지 않지만 예외가 발생하면 어떤 문제가 발생했지는 사용자에게 알려주는 것이 더 유용할 수 있습니다. 실제 사용자에게 배포되는 application의 경우라면 예외를 조용히 처리하는건 잠재적인 문제점을 숨기는 것에 불과하므로 당장에 예외를 사용자에게 보여주길 원하지 않거나 적절한 처리를 하지 않을 것이라면 log를 남겨두는것도 방법이 될 수 있습니다. 또는 예외를 상위로 던져 상위수준에서 발생한 예외를 처리하도록 하는 것도 좋은 방법입니다.
(2) 모든 예외 처리하기
발생할 수 있는 모든 type의 예외에 대한 정보를 가져오기 위해서는 catch에서 System.Exception type의 변수를 선언해야 합니다.
try
{
int i = int.Parse(s!);
Console.WriteLine($"input value : {i}");
}
catch (System.Exception ex)
{
Console.WriteLine($"{ex.GetType()} - {ex.Message}");
}
위 예제는 예외가 발생한경우 해당 예외 type과 예외에 대한 message를 표시하도록 하고 있습니다.
예제를 실행한 후 abc와 같이 숫자가 아닌 문자를 입력하면 아래와 같은 예외 message를 볼 수 있습니다.

(3) 특정 예외만 처리하기
위 예제를 통해 우리는 숫자가 아닌 값을 입력하면 어떤 type의 예외가 발생할 수 있는지 알 수 있습니다. 발생가능한 예외의 type을 알면 해당 하는 예외가 발생했을때 사용자에게 좀더 도움이 될만한 정보를 제공해 줄 수 있을 것입니다.
위 예제에서 기존 catch (System.Exception ex)는 그대로 남겨두고 그 위에서 이전에 발생한 FormatException 예외를 아래와 같이 추가합니다. FormatException이 어떤 경우에 발생할 수 있는지 알고 있으므로 사용자에게 그에 맞는 안내 message를 다음과 같이 남겨 둘 수 있습니다.
try
{
int i = int.Parse(s!);
Console.WriteLine($"input value : {i}");
}
catch (System.FormatException)
{
Console.WriteLine("The you entered is not a valid number.");
}
catch (System.Exception ex)
{
Console.WriteLine($"{ex.GetType()} - {ex.Message}");
}
예제를 실행하고 이전 예제와 동일한 값을 입력하면 이번에는 다음과 같은 결과를 표시할 것입니다.

위 예제에서 System.Exception에 대한 catch를 제거하지 않고 남겨 두는 이유는 FormatException이외에 다른 예외가 발생하는 경우에 대한 대비입니다. 이때 위와 같이 여러 catch를 사용하고자 하는 경우에는 순서가 중요한데, 가장 구체적이며 발생할 가능성이 많은 예외를 위에 두고 System.Exception처럼 보편적인 예외는 가장마지막에 두어야 합니다. 그렇다고 이 순서를 크게 걱정할 필요는 없습니다. 어차피 순서가 잘못되면 compiler는 error를 표시하고 Application의 compile을 중단할 것이기 때문입니다.
모든 예외 type은 System.Exception으로 부터 상속되므로 System.Exception은 전체 예외 type에 대한 최상위 type이 됩니다.
다만 너무 많은 예외를 잡지 않도록 주의해야 합니다. 예외를 처리할 수 있는 더 많은 정보를 가진 상위의 call stack으로 예외를 전파시키는 것이 당장 예외를 처리하여 전파를 중단시키는 것 보다 더 나은 상황을 만들 수 있기 때문입니다.
(4) Catch에서 filter적용하기
catch부분에서는 when keyword를 사용하여 특정 조건에 대한 예외를 filter할 수 있습니다.
catch (System.FormatException) when (s.Contains("abc"))
{
Console.WriteLine("Please enter a number, not a letter.");
}
catch (System.FormatException)
{
Console.WriteLine("The you entered is not a valid number.");
}
위 예제에서는 동일한 FormatException에 대한 catch를 추가하면서도 when을 사용해 입력된 내용중에서 'abc'라는 문자열을 포함하는 경우로 한정하고 있습니다.
2) Overflow
이전에 형변환에 대해 언급할때 number type의 형변환시 예를 들면 long에서 int형으로 변환시에 본래 값에서 일부 data가 손실될 수 있음을 알 수 있었습니다. 그런데 변환대상이 되는 값이 저장될 type에 비해 너무 크다면 이때는 overflow가 발생하게 되고 예상치 못한 결과를 만들게 될 수 있습니다.
(1) checked
checked 구문을 사용하면 overflow 예외를 발생시킬 수 있습니다. 본래 overflow상황이 발생하면 성능적인 문제 때문에 일부 data가 손실되는 상황이 발생해도 결과값을 저장함으로서 별다른 오류없이 조용히 처리하지만 checked문은 overflow가 발생하면 이에 대한 예외를 던져주게 됩니다.
먼저 아래 에제를 보겠습니다. 예제는 int형변수에 int가 담을 수 있는 최대값을 저장한 후 +1을 하여 그 결과를 확인하고 있습니다.
int i = int.MaxValue;
Console.WriteLine(i);
++i;
Console.WriteLine(i);
예제를 실행하면 아래와 같은 결과를 표시할 것입니다.

실행결과를 보면 int최대값에서 +1을 하게 됨으로서 overflow가 발생하였고 그 결과 i의 값은 -값이 되었습니다.
아래 에제는 위 예제에서 checked문만 추가한 것입니다.
checked {
int i = int.MaxValue;
Console.WriteLine(i);
++i;
Console.WriteLine(i);
}
예제를 실행하면 이번에는 다음과 같은 결과를 표시할 것입니다.

결과를 보면 예외를 통해 overflow가 발생하였음을 알 수 있습니다.
당연하지만 overflow예외의 경우에도 catch를 통해 잡아낼 수 있습니다.
try
{
checked {
int i = int.MaxValue;
Console.WriteLine(i);
++i;
Console.WriteLine(i);
}
}
catch(Exception ex)
{
Console.WriteLine(ex.Message);
}

(2) unchecked
위에서 사용한 checked는 runtime에서 overflow가 발생한 경우 기본동작을 바꿔 예외를 발생시키도록 하기 위한 것입니다. 그런데 overflow는 compile단계에서 오류를 발생시키는 경우도 있습니다. 특히 VS2026과 같은 IDE에서 아래와 같이 overflow가 예상되는 구문을 작성했을때
int i = int.MaxValue + 1;
Console.WriteLine(i);
++i;
Console.WriteLine(i);
compiler는 compile을 시도하기 전에 다음과 같이 해당 구문에 문제가 있음을 알려주게 됩니다.

이러한 문제를 피하려면 unchecked라는 구문을 checked와 동일한 방식으로 다음과 같이 사용해야 합니다.
unchecked
{
int i = int.MaxValue + 1;
Console.WriteLine(i);
++i;
Console.WriteLine(i);
}
이렇게 하면 더이상 compiler는 overflow에 대한 오류를 발생시키지 않을것입니다. 하지만 이러한 방법은 오류를 억제할 뿐 overflow는 여전히 발생할 수 있다는 점을 기억해야 합니다.
'.NET > C#' 카테고리의 다른 글
| [C# 14 / .NET 10] C# 14 (0) | 2025.12.01 |
|---|---|
| [C# 14 / .NET 10] 2025년 11월 C# 14 탄생 (0) | 2025.11.06 |
| [C# 12와 .NET 8] 11. LINQ (0) | 2024.03.11 |
| [C# 12와 .NET 8] 10. Entity Framework Core (1) | 2024.03.07 |
| [C# 12와 .NET 8] 9. File, Streams, Serialization (0) | 2024.02.28 |