[C# 11 과 .NET 7] 2. C#
이번 글에서는 C# programming 언어에 대한 기본적인 사항을 살펴볼 것입니다. 전반적으로 알아야 할 용어와 C#에 대한 기본적인 문법에 대한 것들입니다.
1. C# 언어
C#을 통해 application에 대한 source code를 작성하려면 그에 필요한 문법과 용어를 알고 있어야 할 것입니다. 다행스럽게도 programming 언어자체는 사람이 사용하는 언어(대게는 영어)와 비슷한 면을 가지고 있습니다. 다만 programming에서는 자신만의 단어를 만들어낸다는 차이만 있을 뿐입니다.
(1) 언어 version과 기능
● C# 1
2002년 02월에 발표되었으며 객체지향언어에 대한 모든 중요한 요소를 포함하였습니다.
● C# 1.2
foreach 구문의 끝에서 자동적인 disposal과 같은 약간의 향상이 있으며 Visual Studio .NET 2003과 함께 발표되었습니다.
● C# 2
2005년에 release되었으며 code의 성능향상과 type error를 줄이기 위한 generic을 도입하였습니다. 이로서 강력한 data type사용이 가능해졌습니다.
● C# 3
2007년에 release되었으며 LINQ(Language INtegrated Queries)및 익명 type, lambda 식과 관련된 기능이 도입되었으며 이로서 선언적 coding이 가능해졌습니다.
● C# 4
2010년에 release되었으며 F#및 Python과 같은 동적언어와의 상호운영성이 향상되었습니다.
● C# 5
2012년에 release되었으며 비동기 구문에 대한 단순화가 가능해졌습니다.
● C# 6
2015년에 release되었으며 언어에 대한 일부 개선이 이루어졌습니다.
● C# 7.0
2017년 3월에 release되었으며 언어에 대한 일부 개선과 함께 tuple이나 pattern matching과 같은 언어기능이 추가되었습니다.
● C# 7.1
2017년 8월에 release되었으며 언어에 대한 일부 개선이 이루어졌습니다.
● C# 7.2
2017년 11월에 release되었으며 언어에 대한 일부 개선이 이루어졌습니다.
● C# 7.3
2018년 5월에 release되었으며 ref 변수, pointer, stackalloc 등 성능중심의 safe code에 대한 성능개선에 중점을 둔 version입니다.
● C# 8
2019년 9월에 release되었으며 null처리와 관련된 언어의 주요 변화가 이루어졌습니다.
● C# 9
2020년 11월에 release되었으며 record type, pattern matching 개선, minimal-code등에 중점을 둔 version입니다.
● C# 10
2021년 11월에 release되었으며 일반적인 scenario에서 필요한 공통적인 code의 양을 최소 하는 기능에 중점을 둔 version입니다.
● C# 11
2022년 11월에 release되었으며 기본적인 code를 단순화하는데 중점을 둔 version입니다.
(2) C# 표준
몇해에 걸쳐 Microsoft는 몇몇 C# version을 표준기관에 제출하였습니다.
C# version | ECMA 표준 | ISO/IEC 표준 |
1.0 | ECMA-334:2003 | ISO/IEC 23270:2003 |
2.0 | ECMA-334:2006 | ISO/IEC 23270:2006 |
5.0 | ECMA-334:2017 | ISO/IEC 23270:2018 |
C# 6에 대한 ECMA표준은 여전히 초안이며 C# 7에 대한 기능을 추가하는 작업이 진행 중입니다. Microsoft는 C#을 2014년에 open-source화 하였으며 C# ECMA 표준문서를 아래 주소에서 확인할 수 있습니다.
ECMA-334 - Ecma International (ecma-international.org)
ECMA 표준보다 더 현실적으로 더 도움이 되는 것은 C#과 관련 기술에 대한 작업을 공개하고 있는 public GitHub repository라 할 수 있습니다.
C# language design | GitHub - dotnet/csharplang: The official repo for the design of the C# programming language |
Compiler implementation | GitHub - dotnet/roslyn: The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs. |
Standard to describe the language | GitHub - dotnet/csharpstandard: Working space for ECMA-TC49-TG2, the C# standard committee. |
(3) C# compiler version 맞추기
Roslyn으로 알려진 C#과 VB.NET에 대한 .NET 언어 compiler는 F# compiler와는 별개이며 .NET SDK의 일부로 배포되었습니다. 특정 C# version을 사용하기 위해서는 아래 표의 기준에 부합하는 최소한의 .NET SDK가 설치되어 있어야 합니다.
.NET SDK | Roslyn version | C# version |
1.0.4 | 2.0 ~ 2.2 | 7.0 |
1.1.4 | 2.3 ~ 2.4 | 7.1 |
2.1.2 | 2.6 ~ 2.7 | 7.2 |
2.1.200 | 2.8 ~ 2.10 | 7.3 |
3.0 | 3.0 ~ 3.4 | 8.0 |
5.0 | 3.8 | 9.0 |
6.0 | 4.0 | 10.0 |
7.0 | 4.4 | 11.0 |
class library를 생성할때는 최신의 .NET version뿐 아니라 .NET Standard를 대상으로 선택할 수 있지만 사용할 수 있는 C# version은 선택한 .NET Standard에 의해 정해지게 됩니다.
.NET Standard | C# |
2.0 | 7.3 |
2.1 | 8.0 |
비록 특정 compiler version에 access하려면 그에 맞는 최소한의 .NET SDK가 설치되어 있어야 하지만 생성한 project자체는 이전 version의 .NET을 대상으로 하면서 최신 version의 compiler를 사용할 수 있습니다. 예를 들어 .NET 7 SDK이후 version이 설치되어 있다면 .NET Core 3.0을 대상으로 하는 console app에서 C# 11 언어기능을 사용할 수 있습니다.
● SDK version 확인하기
현재 system에서 가능한 C# 언어 compiler version과 .NET SDK가 무엇인지 확인하려면 Windows Terminal을 열고 아래 명령을 내려줍니다.
dotnet --version |
위 명령의 실행결과로는 다음과 같이 나올 수 있습니다.
7.0.202 |
예제에서 표시된 version으로 현재 SDK는 새로운 기능이 없으며 약간의 bug수정이 포함된 2 version의 SDK임을 알 수 있습니다.
● 언어 version compiler 지정하기
Visual Studio와 dotnet command-line interface와 같은 개발자 도구는 기본적으로 가장 최신의 C#언어 compiler를 사용합니다. C# 8.0이 release 되기 이전에 C# 7.0이 기본적으로 사용되는 가장 최신 version이었는데 7.1, 7.2, 7.3과 같은 개선된 version을 사용하기 위해서는 project file의 <LangVersion> 요소를 통해 아래와 같이 지정해 줘야 했습니다.
<LangVersion>7.3</LangVersion>
.NET 7과 C# 11 release이후도 마찬가지로 Microsoft가 C# 11.1 compiler를 release 했다면 그리고 해당 version의 사용을 통해 새로운 언어의 기능을 사용하고자 한다면 역시 아래와 같은 설정이 필요합니다.
<LangVersion>11.1</LangVersion>
<LangVersion>에서 잠재적으로 설정할 수 있는 값은 아래 표를 참고하시기 바랍니다.
7, 7.1, 7.2, 7.3, 8, 9, 10, 11 | 설치된 특정 version의 compiler를 사용하도록 합니다. |
latestmajor | major version이 가장 높은 것을 사용합니다. |
latest | 가장 높은 major와 minor version을 사용하도록 합니다. 만약 현재의 최신 version이 11.2라면 11.2를 사용하게 됩니다. |
preview | 가능한 가장 높은 preview version을 사용합니다. |
새로운 project를 생성한 후에는 .csproj project file의 <LangVersion> 요소를 추가하거나 변경함으로써 사용하고자 하는 version을 바꿔줄 수 있습니다.
<TargetFramework>net7.0</TargetFramework>
<LangVersion>preview</LangVersion>
2. C# 문법과 용어
아주 간단하게는 .NET Interactive Notebooks를 통해 특정 application을 생성할 필요 없이 간단한 구문을 실행해 볼 수 있습니다. 특정한 C#기능에 관해서는 project를 생성해야 할 필요가 있는데 가장 간단한 유형의 application으로 console app을 생성할 수 있습니다.
(1) Compiler version 확인하기
Visual Studio 2022를 통해 'myStudy02'라는 solution을 생성하고 그 하위에 Console App 유형으로 'Vocabulary' 이름의 project를 생성합니다.
project가 생성되면 해당 project의 Program.cs file을 열고 첫 번째 줄 아래에 아래와 같이 C# version을 보여주는 구문을 추가합니다. 그리고 Debug / Start Without Debugging을 선택해 project를 실행합니다.
그러면 다음과 같이 compiler version과 언어 version이 compiler error message 번호 CS8304로 나타나게 됩니다.
version을 확인하였으면 error를 발생시키는 해당 구문을 주석처리하여 error message가 더 이상 나타나지 않도록 합니다.
(2) C# 문법
C#의 구문은 문장과 block으로 이루어져 있습니다. 또한 code에 대한 설명을 위해 주석을 사용할 수 있습니다.
주석만이 code를 설명하기 위한 유일한 수단은 아닙니다. 변수와 함수등에 대한 적절한 명칭부여와 단위 test작성, 실제 문서작성과정을 통해서도 code를 설명하기 위한 다른 수단으로 활용될 수 있습니다.
(3) 구문
C#에서 문장의 끝은 semicolon(;)으로 나타냅니다. 또한 문장은 여러 변수나 표현식으로 구성될 수 있습니다. 예를 들어 아래 구문에서 totalPrice는 변수이며 subtotal + salesTax는 식입니다.
var totalPrice = subtotal + salesTax;
예제에서 표현식은 subtotal이라는 피연산자와 + 연산자 그리고 salesTax라는 또 다른 연산자로 구성됩니다. 이때 피연산자와 연산자의 순서에 주의해야 합니다.
(4) 주석
주석은 code를 설명하는 주요 수단으로서 다른 개발자가 code를 읽을 때 해당 code가 어떻게 작동하는지에 대한 이해를 높여줄 수 있으며 심지어 시간이 흐른 후 자신의 code를 읽는 경우에도 많은 도움이 될 수 있습니다.
주석은 '//'로 2개의 slash를 사용해 아래와 같이 표현할 수 있습니다. compiler는 주석을 만나면 해당 주석은 무시하고 처리합니다.
// sales tax must be added to the subtotal
var totalPrice = subtotal + salesTax;
여러 줄의 구문을 작성하려면 /* 와 */ 사이에 주석을 추가하면 됩니다.
/*
안녕하세요.
반갑습니다.
*/
주석은 필요하다면 아래와 같이 중간에 삽입될 수도 있습니다.
var totalPrice = subtotal /* for this item */ + salesTax;
잘 명명된 매개변수와 이름을 가진 함수, 그리고 class encapsulation을 포함한 잘 설계된 code는 그것 자체로 code를 설명할 수도 있습니다. 특별한 경우가 아니면 너무 긴 주석은 오히려 code를 읽는데 방해가 될 수 있으므로 적절히 요약된 주석과 Naming규칙에 맞는 변수, method, class명을 같이 사용하는 것이 좋습니다.
(5) Block
C#에서 code의 block은 {로 시작하고 }로 끝납니다. 이를 통해 namespace, class, method나 foreach 같은 곳에서 해당 구문이 속한 시작과 끝을 나타낼 수 있고 하나의 code단위로서 취급될 수 있습니다.
(6) 문장과 Block의 예시
아래 예제는 top-level program 기능을 사용하지 않은 기본적 언 console app예제이며 몇몇 주석과 문장으로 이루어져 있습니다.
using System; // semicolon은 문장의 끝을 나타냅니다.
namespace Basics
{ // block의 시작
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
} // block의 끝
(7) C# 어휘
C#에서는 keyword, 기호문자, type등의 용어가 사용됩니다. 몇몇 단어는 예약된 keyword로서 사전정의되었으며 해당 단어는 식별자의 이름으로 사용될 수 없습니다.(using, namespace, class, static, int, string, double, bool, if, switch, break, while, do, for, foreach, and, or, not, record, init 등) 그러나 비록 권장하지 않지만 예약어를 포함한 식별자는 사용할 수 있습니다.(ints 등과 같은)
기호문자의 경우는 ", ', +, -, *, /, %, @, $와 같은 것이 있습니다.
이외에 특정한 문맥안에서만 특별한 뜻으로 사용되는 상황별 keyword도 존재합니다.
C# keyword는 모두 소문자입니다. 비록 자기 자신만의 식별자도 소문자를 사용할 수 있지만 되도록이면 그렇지 하지 않는 것 좋으며 C# 11 이후 compiler에서도 소문자로만 이루어진 식별자를 사용하는 경우 경고를 표시합니다.
(8) C# 구문을 위한 color scheme 바꾸기
기본적으로 Visual Studio 2022에서 C# keyword는 파란색으로 표시하여 이들을 쉽게 다른 code와 구별될 수 있도록 하고 있습니다. 하지만 이러한 색상적용이 마음에 들지 않는다면 'Tools / Options / environment / Fonts and Colors' menu를 통해 직접 표시가능한 색상을 조정할 수 있습니다.
(9) 정확한 code작성하기
메모장과 같은 일반 text editor에서는 정확한 단어(특히 영어)입력을 도울 수 있는 기능이 없습니다. 마찬가지로 정확한 C# code작성 역시 도울 수 없습니다.
하지만 Microsoft Word의 경우 잘못된 단어가 입력될때 붉은색 밑줄로 강조처리함으로써 문서작성을 도울 수 있습니다. 예를 들면 'icecream'인 경우 'ice-cream'혹은 'ice cream'이 되어야 한다는 것을 알려주는 식입니다. 파란색 밑줄을 통해서는 문법적 오류를 표시하는데 예를 들면 특정 문장의 경우 대문자로 시작되어야 함을 알려줍니다.
비슷하게 Visual Studio 2022에서도 C# code의 작성을 도울 수 있습니다. 예를 들어 대문자 L을 사용해 method의 이름이 WriteLine 되어야 함을 알리거나 문장이 semicolon으로 끝나야 함을 알려주는 문법적 error의 경우처럼 typing 하는 과정을 지속적으로 지켜보면서 붉은색 밑줄을 통해 문제점을 강조하여 개발자가 쉽게 잘못된 부분을 구분할 수 있도록 합니다.
(10) namespace importing
System은 namespace에 해당하며 해당 type의 주소와 같습니다. 누군가의 위치를 정확하게 알려주기 위해서 '서울시.서대문구.신촌역로.홍길동'과 같이 지정하면 사람들은 서울시에 있는 서대문구의 신촌역로길에 있는 '홍길동'이라는 사람을 떠올릴 수 있는 것과 같은 개념입니다.
이와 비슷하게 System.Console.WriteLine라고 한다면 이는 compiler에게 System namespace에 있는 Console이름의 type안에서 WriteLine method를 찾도록 알려주는 것입니다.
.NET 6 이전의 모든 version에서 Console App project template에서는 code를 간소화하기 위해 code file상단에 아래와 같은 구문을 추가함으로써 compiler가 namespace가 붙어있지 않은 type들에 대해서 항상 System namespace를 바라볼 수 있도록 하였습니다.
using System;
위와 같은 처리를 namespace import라고 하며 namespace를 import함으로서 해당 namespace에서 가능한 모든 type은 program안에서 자신들의 namespace를 붙여줄 필요 없이 사용할 수 있게 됩니다. 또한 IntelliSense에 반영됨으로써 편리하게 code를 입력할 수 있습니다.
.NET Interactive Notebooks는 대부분의 namespace가 자동적으로 import 되어 있습니다.
(11) Namespace의 암시적 import와 global import
본래 namespace import가 필요한 모든 .cs file은 namespace의 import를 위해 using구문으로 시작해야 합니다. System이나 System.Linq와 같은 namespace는 사실상 거의 모든 .cs file에서 필요하므로 첫 몇 line은 아래와 같이 using구문으로 시작하곤 합니다.
using System;
using System.Linq;
using System.Collections.Generic;
ASP.NET Core를 사용해 website나 service를 생성할때는 각 file에서 import 해야 할 namespace가 10개 이상이 되는 경우도 있었습니다.
C# 10에 들어서 도입된 새로운 keyword와 .NET SDK 6의 새로운 project의 결합을 통해 일반적인 namespace의 importing을 간소화할 수 있게 되었습니다. 이에 대한 결과로 global using keyword를 사용할 수 있는데 이 조합은 하나의 .cs file에서만 import 할 수 있음을 의미하며 이러한 import의 효과는 project전역의 모든 .cs file에 영향을 주게 됩니다. global using 구문은 Program.cs file에서도 가능하지만 GlobalUsing.cs와 같은 이름의 file로 global using 구문을 사용하는 전용의 file을 아래와 같이 분리할 것을 권장합니다.
global using System;
global using System.Linq;
global using System.Collections.Generic;
개발자가 이러한 새로운 C#기능에 익숙해지면 이러한 file에 대한 하나의 명명규칙이 표준이 될 수 있습니다. 그러면서 .NET SDK와 관련된 기능 역시 비슷한 명명규칙을 따를 것입니다.
.NET 6.0 이후를 target으로 한 모든 project는 C# 10 이후의 compiler를 사용하므로 System과 같은 몇몇 일반적인 namespace를 대상으로 전역적인 import를 수행하기 위해 obj/Debug/net7.0 folder안에서 [Project명].GlobalUsings.g.cs file을 생성하게 됩니다. 이때 암시적으로 import 되는 namespace의 list는 target으로 하는 SDK에 의존합니다.
SDK | import namespace |
Microsoft.NET.Sdk | System System.Collections.Generic System.IO System.Linq System.Net.Http System.Threading System.Threading.Tasks |
Microsoft.NET.Sdk.Web | Microsoft.NET.Sdk의 목록과 함께 System.Net.Http.Json Microsoft.AspNetCore.Builder Microsoft.AspNetCore.Hosting Microsoft.AspNetCore.Http Microsoft.AspNetCore.Routing Microsoft.Extensions.Configuration Microsoft.Extensions.DependencyInjection Microsoft.Extensions.Hosting Microsoft.Extensions.Logging |
Microsoft.NET.Sdk.Worker | Microsoft.NET.Sdk의 목록과 함께 Microsoft.Extensions.Configuration Microsoft.Extensions.DependencyInjection Microsoft.Extensions.Hosting Microsoft.Extensions.Logging |
이전에 생성한 project를 통해 현재 자동생성된 암시적 import file을 확인해 보려면 Visual Studio 2022의 Solution Explorer에서 Vocabulary project를 선택하고 Show All Files button을 click합니다. 그러면 compiler에 의해 생성된 bin과 obj folder를 볼 수 있습니다.
obj/Debug/net7.0 folder를 순서대로 확장한뒤 Vocabulary.GlobalUsings.g.cs file을 open 합니다.
해당 file의 명명규칙은 '[ProjectName]. GlobalUsings.g.cs'입니다. 여기서 generated에 해당하는 g를 통해 개발자가 작성한 code file과 구분하고 있습니다.
해당 file은 .NET 6.0을 target으로 하는 project를 위해 compiler가 자동으로 생성하는 file이며 System.Threading을 포함해 일반적으로 사용되는 몇몇 namespace를 아래와 같이 import 하고 있습니다.
// <auto-generated/>
global using global::System;
global using global::System.Collections.Generic;
global using global::System.IO;
global using global::System.Linq;
global using global::System.Net.Http;
global using global::System.Threading;
global using global::System.Threading.Tasks;
이번에는 Solution Explorer에서 project를 double click 한 뒤 암시적으로 import 할 namespace를 아래와 같은 방법으로 추가합니다.
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Using Remove="System.Threading" />
<Using Include="System.Numerics" />
</ItemGroup>
</Project>
<ItemGroup>은 <ImportGroup>과 구별됩니다. 따라서 정확하게 사용되어야 합니다. 다만, project group 또는 item group의 순서는 관계없습니다. 예를 들어 <Nullable>은 <ImplicitUsings>전/후 어디든 올 수 있습니다.
위와 같이 작성한 후 project file을 저장하고 다시 obj/Debug/net7.0 foler의 Vocabulary.GlobalUsings.g.cs file을 열어보면 위에서 지정한 System.Threading이 제거되어 있고 대신 System.Numerics가 추가되어 있음을 확인할 수 있습니다.
원한다면 모든 SDK에서 암시적으로 import 되는 namespace기능은 project file에서 <ImplicitUsings>요소를 삭제하거나 해당 요소의 값을 disable 함으로써 끌 수 있습니다.
<ImplicitUsings>disable</ImplicitUsings>
(12) 동사와 method
동사는 뛰다. 걷다와 같은 어떤 행동이나 동작에 관한 단어를 말합니다. C#에서 동사와 동일한 의미를 갖는 것을 method라고 하며 C#에서 method는 수십만 가지가 존재합니다. 또한 일반적인 언어로서 동사는 동작이 발생하는 시기와 경우에 따라서 쓰임이 달라지며 C#의 method도 마찬가지로 호출되는 방식에 따라 동작이 달라질 수 있습니다.
예를 들어 WriteLine와 같은 method 역시 호출할 때 지정된 인수에 따라 호출되는 방식과 실행방식이 달라질 수 있습니다. 이러한 형태의 호출방식을 overloading이라고 하는데 하나의 method가 아래와 같이 여러 가지 형식으로 호출하는 경우를 말합니다.
// WriteLine method만 호출되면 carriage return과 line feed가 됩니다.
Console.WriteLine();
// 인사말과 line종료 문자를 출력합니다.
Console.WriteLine("Hello C#");
// 형식화된 숫자및 날짜와 line종료 문자를 출력합니다.
Console.WriteLine("Temperature on {0:D} is {1}°C.", DateTime.Today, 23.4);
동사와의 또 다른 차이점은 단어의 철자는 같지만 문맥에 따라 다른 뜻을 가질 수도 있다는 점입니다.
(13) type, 변수, field 그리고 속성
'명사'라 함은 어떠한 것을 가리키는 이름이 될 수 있습니다. 예를 들어 '밀란'은 강아지의 이름입니다. 여기서 '강아지'는 '밀란'이 무엇인지를 우리에게 알려주는 말입니다. 따라서 '밀란'에게 공을 가져오라고 지시하기 위해 우리는 강아지의 이름을 사용할 수 있는 것입니다.
C#에서도 type, 변수, field와 속성은 동일합니다. 예를 들어 '동물'과 '자동차'는 type이며 어떤 종류를 분류하기 위한 명사입니다. '강아지'와 'Engine'은 field나 속성이 될 수 있으며 '동물'과 '자동차'에 속하는 명사입니다. '밀란'과 'Spark'는 변수이며 특정한 개체를 가리키 위한 명사가 될 수 있습니다.
'C#에서 가능한 type은 수만 가지입니다.' 라고 하는 말하는 것은 'C#에는 수만가지 type이 있습니다.'라고 말한 것과는 다릅니다. 이는 미묘한 차이지만 중요합니다. 엄격히 말하면 C#에서 type은 단지 string이나 int와 같은 몇 가지 keyword만 존재하며 어떠한 type도 정의하지 않습니다. type처럼 보이는 string과 같은 keyword는 별칭이며 C#이 실행되는 platform에 의해 제공되는 type을 표현하는 것입니다.
이는 C#이 독립적으로 존재할 수 없다는 것을 의미하는 것으로 .NET을 기반으로 실행되는 언어에 해당하는 것입니다. 이론적으로 누군가는 다른 기본 유형을 통해 다른 platform에서 실행되는 C#을 위한 compiler를 작성할 수도 있지만 C#의 platform은 System.Xml.Linq.XDocument와 같이 다수의 복잡한 type뿐만 아니라 C#에서 int라는 별칭과 연결되어 있는 System.Int32와 같은 type을 포함해 수십만 가지 type을 C#에 제공하는 사실상 .NET이라고 할 수 있습니다.
type이 종종 class와 혼동된다는 점에 주목해야 합니다. type은 class만을 의미하는 것이 아닙니다. C#에서 모든 type은 class, struct, enum, interface 또는 delegate로 분류될 수 있습니다. 예를 들어 string이라는 keyword는 class이지만 int는 struct입니다. 따라서 이 둘을 모두 표현하기 위한 것으로 type이라는 용어를 사용하는 것입니다.
(14) C#용어의 범위 표시하기
'C#에는 100개 이상의 keyword가 있는데 type은 얼마나 많이 존재할까?' 이에 대한 답을 간단한 예제 code를 작성해 봄으로서 C#에서 가능한 type과 이들에 대한 method가 얼마나 존재하는지를 확인해 보고자 합니다.
가볍게 진행하는 것으로 작성하고자 하는 code가 어떻게 동작하는지에 대한 설명은 생략할 것입니다. 그저 reflection이라는 것을 사용한다는 것 정도만 알고 있으면 충분합니다.
이전에 만든 예제에서 Program.cs의 내용을 모두 삭제하고 해당 file을 아래와 같이 변경합니다.
using System.Reflection;
Assembly? myApp = Assembly.GetEntryAssembly();
if (myApp == null)
return;
foreach (AssemblyName name in myApp.GetReferencedAssemblies())
{
Assembly a = Assembly.Load(name);
int methodCount = 0;
foreach (TypeInfo t in a.DefinedTypes)
methodCount += t.GetMethods().Count();
Console.WriteLine("{0:N0} types with {1:N0} methods in {2} assembly.", arg0: a.DefinedTypes.Count(), arg1: methodCount, arg2: name.Name);
}
현재 예제 project에서는 모든 .cs file을 위해 암시적 import와 global using기능을 사용해 필요한 namespace를 import 하고 있습니다. 그러나 단 하나의 file에서만 필요한 경우라면 해당 file에서 필요한 namespace를 import 하는 것이 좋습니다.
위 예제는 접근가능한 모든 type에 대한 loop를 수행하면서 각각이 가진 method의 수와 해당 assembly의 이름을 나열할 것입니다.
N0은 대문자 N다음에 숫자 0입니다. 즉, 대문자 O가 아님에 주의해야 하며 이는 숫자 N을 0 소수점자리로 형식화함을 의미합니다.
project를 실행하면 동작중인 현재 OS의 application에서 가능한 type과 method 수를 확인할 수 있습니다. 즉, 표시된 type의 수와 method는 사용하는 OS에 따라 달라질 수 있습니다.
위의 결과를 보면 System.Runtime assembly는 type이 0 임을 알 수 있습니다. 이것은 해당 assembly가 실제 type이 아닌 type-forwarder만을 포함고 있기 때문입니다. Type forwarding은 본래 assembly를 사용한 application을 compile 하지 않아도 type을 다른 assembly로 이동할 수 있도록 합니다.
이제 file의 상단에 (namespace import 다음에) 아래와 같이 file을 변경합니다.
using System.Reflection;
System.Data.DataSet ds;
HttpClient client;
Assembly? myApp = Assembly.GetEntryAssembly();
다른 assembly를 사용하는 변수를 선언함으로써 해당 assembly가 application에서 load 되어 이들에 대한 모든 type과 method를 확인할 수 있게 되었습니다. compiler는 변수가 사용되지 않았음을 알리는 경고를 표시할 수 있지만 이를 무시하도록 합니다.
project를 실행하면 다음과 같은 결과를 확인할 수 있습니다.
얼핏 보면 C#을 배우기 위해 방대한 type과 method의 사용법을 배워야 할 것처럼 보일 수 있지만 이들을 모두 외울 필요가 없으며 지금도 다른 누군가가 새로운 type과 memebr를 정의하고 있을 것이기에 이 모든 것을 다 안다는 것은 불가능합니다. C#의 구현방식은 C#을 공부하면서 자연스럽게 익히면 되고 특정 type을 사용해야 하는 경우라면 그때 가서 필요한 type과 해당 type이 가진 method를 확인하고 적절히 사용해 주기만 하면 됩니다.
3. 변수 사용
모든 application은 data를 처리합니다. data가 들어오고 처리되고 다시 나가는 식입니다.
Data는 일반적으로 file이나 database 또는 사용자의 입력을 통해 program으로 들어오고 동작중인 program의 memory에 저장될 변수로 값이 들어와 저장된 뒤 목적에 맞게 사용됩니다. 처리된 결과는 file이나 database 혹은 단순히 화면에 표시될 수 있으며 필요한 처리가 이루어지고 program이 종료되면 비로소 memory의 data도 같이 소거됩니다. 변수를 사용할 때는 우선 해당 변수가 memory에서 얼마나 많은 공간을 차지할지 그리고 얼마나 빠르게 처리될 수 있는지를 생각해야 합니다.
이에 대한 답은 적절한 type을 선택해야 한다는 것입니다. int와 double과 같은 type의 경우에는 단순히 저장되는 크기가 다르다고 생각할 수 있습니다. 더 작은 크기의 type은 당연히 더 작은 memory를 차지하지만 그렇다고 더 빨라지는 것은 아닙니다. 예를 들어 64bit system에서 16bit 숫자를 더하는 경우는 64bit 숫자를 더하는 것만큼이나 빨리 처리된다고 볼 수는 없는 것입니다. 몇몇 type은 stack으로 쌓일 수 있고 몇몇은 더 멀리 heap에 던져질 수도 있습니다.
(1) 명명규칙과 값의 할당
변수나 field에 이름을 짓는 명명규칙이 존재합니다. 물론 이러한 규칙이 강제는 아니어서 선택하기 나름이지만 되도록이면 규칙에 따라 이름을 부여하는 것이 좋습니다.
명명 규칙 | 예 | 적용 구분 |
Camel case (첫글자를 소문자로, 이후 단어사이의 구분은 대문자로) | cost, orderDetail, dateOfBirth | 지역변수, private field |
Title case (Pascal case, 첫글자를 대문자로, 이후 단어사이의 구분은 대문자로) | String, Int32, Cost, DateOfBirth, Run | Type, non-private field, 그외 method와 같은 다른 member |
어떤 경우는 private field의 이름에 _(under score) 문자를 접두사로 붙이는 경우도 있습니다. 예를 들어 dateOfBirth를 _dateOfBirth로 하는 식입니다. 모든 종류의 private member 이름은 class의 외부에서는 노출되지 않기 때문에 정의된 형식이 없습니다. 따라서 이와 같은 방법도 유효합니다.
일관적인 이름을 사용하면 다른 개발자가 code를 읽을 때 이해하기 쉽도록 도울 수 있습니다.(향후 자신의 code를 읽을 때도 마찬가지로)
아래 예제는 지역변수의 선언과 =문자를 통해 값을 할당하는 예를 보여주고 있습니다. 또한 C# 6.0에서 도입된 nameof keyword를 사용해 변수의 이름을 표시하도록 하고 있습니다.
double heightInMetres = 1.88;
Console.WriteLine($"The variable {nameof(heightInMetres)} has the value {heightInMetres}.");
(2) Literal
변수에 값을 할당하는 경우 종종 literal 값을 사용하곤 합니다. literal은 고정된 값을 표현하는 것으로 Date type의 경우에는 다른 표기법을 가지고 있는데 C#에 대한 학습과정에서 변수에 값을 할당할 때 literal 표기법을 사용하는 예를 볼 수 있을 것입니다.
(3) 문자열
'A'와 같은 단일 문자의 경우에는 char type으로 저장됩니다.
실제로는 조금 더 복잡할 수 있습니다. Egyptian Hieroglyph A002 (U+13001)는 이를 나타내기 위해 \uD80C와 \uDC01라는 2개의 System.Char값을 필요로 합니다. 따라서 항상 하나의 char가 하나의 문자와 일치한다는 추정을 해서는 안됩니다. 그렇지 않으면 잡아내기 어려운 bug를 만들 수 있습니다.
char에서 literal값을 할당하려면 홀따옴표(')를 사용해 감싸서 표현해야 합니다. 그 외 method나 기타 다른 방법으로 할당하는 경우 type에 맞으면 그대로 할당할 수 있습니다.
char letter = 'A';
char digit = '1';
char symbol = '$';
char userChoice = GetSomeKeystroke();
문자가 'abc'와 같이 하나 이상이라면 string type으로 저장해야 하며 literal인 경우 큰따옴표(")를 사용해야 합니다. 그 외에는 동일하게 할당할 수 있습니다.
string firstName = "abc";
string phoneNumber = "555-4256";
string address = GetAddressFromDatabase(id: 563);
string grinningEmoji = char.ConvertFromUtf32(0x1F600);
실제 아래와 같이 code를 만든 후
Console.OutputEncoding = System.Text.Encoding.UTF8;
string grinningEmoji = char.ConvertFromUtf32(0x1F600);
Console.WriteLine(grinningEmoji);
Windows Termial에서 실행하면 emoji가 출력됨을 알 수 있습니다.
● 확장 문자열
string 변수에 문자열을 저장할 때 필요하다면 tab이나 new line과 같은 특별한 문자열의 표현을 위해 backslash를 사용한 확장문자열을 포함시킬 수 있습니다.
string statusWithTabSeparator = "status\twaiting";
그러나 만약 Windows상에서 특정한 경로를 지정해야 하고 folder의 이름이 t로 시작할 때 아래와 같이 지정하면
string filePath = "C:\Users\testuser\tmp.txt";
compiler는 이를 tab으로 변환하기 때문에 오류를 일으킬 수 있습니다. 따라서 @문자를 접두사로 아래와 같이 붙여줘야 합니다.(혹은 backslash를 연속으로 사용할 수도 있습니다.)
string filePath = @"C:\Users\testuser\tmp.txt";
● Raw string literal
C# 11에서 도입된 것으로 raw string literal은 content를 escape 할 필요 없이 임의의 문자열을 편리하게 입력할 수 있도록 합니다. 이를 통해 XML이나 HTML, JSON 등 다른 언어를 포함하는 literal을 쉽게 정의할 수 있습니다.
Raw string literal은 3개 혹은 그 이상의 큰따옴표로 시작하고 끝나는 아래와 같은 형식으로 사용할 수 있습니다.
string xml = """
<person age="50">
<first_name>Mark</first_name>
</person>
""";
그렇다면 왜 꼭 3개나 그 이상의 큰따옴표어야 할까? 그것은 content자체가 세 개의 큰따옴표를 필요로 하는 경우가 있기 때문입니다. 실제 이런 경우라면 content의 시작과 끝을 나타내기 위해 4개의 쌍따옴표를 사용해야 합니다. 만약 content가 4개의 쌍따옴표를 사용 한다면 우리는 다시 해당 문장열의 시작과 끝을 위해 5개의 쌍따옴표를 사용해야 합니다.
위 예제에서 XML은 일부 들어 쓰기가 되어 있는데 compiler는 마지막 3개 또는 그 이상의 큰따옴표에 대한 들어 쓰기를 확인하고 자동적으로 raw string literal 내부의 모든 content에서 들여 쓰기 수준을 제거합니다. 따라서 해당 예제는 정의된 code안에서 들여쓰기되지 않고 대신 왼쪽여백으로 다음과 같이 정렬됩니다.
● 원시 보간 문자열 literal
필요한 경우 중괄호를 사용한 보간문자열을 raw string literal과 혼합하여 사용할 수 있습니다. 이는 literal을 시작할 때 $기호문자를 바뀌어야 할 표현식을 나타내는 괄호수만큼 지정함으로써 이 보다 적은 괄호를 raw content로서 취급할 수 있습니다.
예를 들어 아래와 같은 JSON문자열에서 하나의 괄호는 일반적인 괄호로 취급되지만 2개의 dollar기호로 compiler에게 내부의 모든 2개의 중괄호는 표현식값으로 변경되어야 한다는 것을 말해주게 됩니다.
var person = new { FirstName = "Alice", Age = 56 };
string json = $$"""
{
"first_name": "{{person.FirstName}}",
"age": {{person.Age}},
"calculation", "{{{1 + 2}}}"
}
""";
Console.WriteLine(json);
따라서 위의 예제는 다음과 같은 결과를 나타냅니다.
즉, dollar의 수는 compiler에게 보간표현식으로 인식하는데 필요한 괄호의 수를 알려줍니다.
● 요약정리
- Literal 문자열 : 큰따옴표로 문자열을 감싸는 것으로 표현되며 tab을 위한 '\t'처럼 확장문자열을 사용할 수 있습니다. backslash자체를 표현하려면 두 개의 backslash를 사용합니다.(\\)
- raw string listeral : 세 개 혹은 그 이상의 큰따옴표로 문자열을 감싸서 표현합니다.
- Verbatim string : literal 문자열에서 확장문자열을 무효화하기 위해 @문자를 접두사로 사용한 것을 말합니다. 이로서 \문자는 있는 그대로 \문자가 됩니다. 또한 공백문자를 compiler에서의 명령어 대신 공백자체로 처리함으로써 문자열값이 여러 줄에 걸쳐 있을 수 있도록 합니다.
- interpolated string : $문자를 접두사로 사용한 literal string으로서 변수의 값을 변수자체로 포함할 수 있도록 합니다.
(4) 숫자
숫자는 산술적, 예를 들어 곱셈과 같은 계산을 위해 사용되는 data자체를 말합니다. 그런 관점에서 전화번호는 숫자가 아닙니다. 변수를 숫자로서 저장하는 데 사용할지 여부를 결정하려면 해당 숫자가 산술적 계산을 수행할 필요가 있는지, 혹은 숫자형식에서 숫자가 아닌 문자(괄호나 -문자 같은)가 섞일 수 있는지를 고려해서 결정해야 합니다. 따라서 123-4567-8901과 같은 전화번호가 있다면 이는 숫자가 아닌 문자열로서 다뤄져야 합니다.
숫자는 42와 같은 자연수이며 셀 수 있는 정수이기도 합니다. 경우에 따라서는 음수(-)가 될 수 있고 3.9와 같은 소수도 될 수 있으며 이를 computing에서는 단일 또는 배정밀도 부동소수점으로 표현되기도 합니다.
Solution에 Numbers라는 이름의 project를 추가하고 Program.cs에서 기존의 내용을 모두 삭제한 뒤 아래와 같이 여러 data type을 사용한 해당 숫자형식의 변수를 선언합니다.
uint naturalNumber = 23;
int integerNumber = -23;
float realNumber = 2.3F;
double anotherRealNumber = 2.3;
uint는 부호 없는 정수를 담는 변수로 양수 또는 0에 해당합니다. int는 음수와 양수 그리고 0에 해당합니다. float는 단정밀도 부동소수점 형식이며 literal값에 F접미사를 붙여 해당 값이 float임을 나타낼 수 있습니다. double은 배정밀도 부동소수점 형식이며 숫자가 소수가 있는 경우의 기본 data type에 해당합니다.
● 정수
computer는 모든 걸 bit로 저장하며 0 아니면 1의 값을 갖습니다. 이런 것을 우리는 binary(2진수)라고 하는데 사람은 보통 10진수를 사용합니다.
Base 10이라고 알려진 10진수는 10을 기반으로 하는 것으로서 0부터 9까지 10개의 숫자를 사용합니다. 이러한 10진법은 대부분이 사람들이 사용하고 있는 숫자체계지만 과학이나 공학, computing에서 잘 사용되는 다른 숫자기반 역시 많이 사용되고 있으며 그중 binary는 Base 2 기반으로 0과 1이라는 2개의 digit값만 갖는 숫자체계입니다.
아래표를 통해서는 computer가 10진수에서 숫자 10이라는 값을 어떤 형태로 저장하는지를 보여주고 있습니다. 표에서 8과 2에 1 값이 있음을 주목하시기 바랍니다.
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
이 표를 통해 8+2는 10이 되고 10진수 10은 binary로 00001010이 됨을 알 수 있습니다.
● 숫자 분리자를 사용한 가독성 향상
C# 7.0부터는 _(underscore) 문자를 숫자 분리자로 사용할 수 있으며 이를 binary literal에도 적용할 수 있습니다. 즉, _ 문자는 decimal형식을 포함해 binary나 hexadecimal 표기법을 포함해 숫자 어디든 삽입하여 해당 숫자값의 가독성을 향상하는 데 사용됩니다.
예를 들어 10진수 표기법을 사용해 1백만 숫자를 쓸 때 1_000_000처럼 작성하여 천 단위 구분을 명확하게 할 수 있습니다. 뿐만 아니라 이미 언급한 바와 같이 어디든 _문자를 사용할 수 있으므로 인도에서 사용되는 2/3 group에 대한 표기도 10_00_000와 같이 표시할 수 있습니다.
● binary 또는 hexadecimal 표기법 사용
2진수인 binary 표기법은 오로지 0과 1만이 사용되며 literal로는 0b로 시작합니다. hexadecimal은 16진수로 0부터 9와 A부터 F까지 사용되고 literal로는 0x로 시작합니다.
아래 예제는 숫자형식의 변수를 선언한 것으로 해당 변수의 값을 _문자를 사용해 대입하였습니다.
//10진수
int decimalNotation = 2_000_000;
//2진수
int binaryNotation = 0b_0001_1110_1000_0100_1000_0000;
//16진수
int hexadecimalNotation = 0x_001E_8480;
Console.WriteLine($"{decimalNotation == binaryNotation}");
Console.WriteLine($"{decimalNotation == hexadecimalNotation}");
Program.cs file을 위와 같이 변경하고 실행하면 2개의 True값이 나오게 되는데 이를 통해 위 3개의 변숫값이 모두 같다는 것을 알 수 있습니다. int type이나 long, short과 같은 비슷한 type은 정확하게 해당 값을 표현할 수 있습니다.
(5) 실수
정수는 정확한 표현이 가능하지만 decimal과 같은 non integer형식의 실수는 그렇지 않으며 정확도에 차이가 있을 수 있습니다. float과 double type은 단정 와 배정도 부동 소수점을 사용해 실수를 저장합니다.
대부분의 Programming언어는 부동소수점 연산을 위해 IEEE표준을 구현하며 IEEE 754는 1985년 IEEE(Institute of Electrical and Electronics Engineers)에 의해 제정된 부동 소수점 연산에 대한 기술적 표준입니다.
아래 표는 computer가 binary표기법을 통해 숫자 12.75를 어떤 방식으로 표현하는지를 단순하게 나타내고 있습니다. 아래 표에서 8, 4, 1/2, 1/4에 1이 표시된 것에 주목하시기 바랍니다.
128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 | . | 1/2 | 1/4 | 1/8 |
0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | . | 1 | 1 | 0 |
8+4+1/2+1/4는 12¾이고 따라서 값은 12.75가 되며 10진수의 12.75는 binary로 00001100.1100이 됩니다. 해당 예를 통해 12.75가 각 bit를 통해 정확하게 표현된다는 것을 볼 수 있으나 모든 실수가 이렇게 표현될 수 있는 것은 아닙니다.
● 숫자 type의 범위 확인
C#에서는 sizeof() 연산자를 통해 각 type이 사용하는 memory의 크기를 byte수로 확인할 수 있습니다. 몇몇 type은 MinValue와 MaxValue라는 member를 갖고 있으므로 이를 통해 해당 type의 변수가 저장할 수 있는 최솟값과 최댓값을 확인할 수도 있습니다. 따라서 이러한 기능을 통해 간단한 code를 작성하여 type의 범위를 확인해 볼 것입니다.
project의 Program.cs file을 아래와 같이 작성합니다. 아래 code는 3개의 data type에 대한 size를 표시합니다.
Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine($"int는 {sizeof(int)} byte를 사용하며 { int.MinValue:N0} 부터 {int.MaxValue:N0} 까지 표현할 수 있습니다.");
Console.WriteLine($"double은 {sizeof(double)} byte를 사용하며 { double.MinValue:N0} 부터 {double.MaxValue:N0} 까지 표현할 수 있습니다.");
Console.WriteLine($"decimal은 {sizeof(decimal)} byte를 사용하며 { decimal.MinValue:N0} 부터 {decimal.MaxValue:N0} 까지 표현할 수 있습니다.");
해당 예제를 실행하면 다음과 같은 결과를 확인할 수 있습니다.
int는 변수는 memory에서 4byte를 사용하며 20만 숫자의 음수와 양수값을 저장할 수 있습니다. double은 memory에서 8byte를 사용하며 int보다 훨씬 더 큰 값을 저장할 수 있습니다. decimal은 memory에서 16byte를 사용하며 역시 큰 값을 저장할 수는 있지만 double만큼 크지는 않습니다.
그렇다면 double은 더 적은 memory를 사용함에도 어떻게 decimal보다 더 큰 값을 저장할 수 있을까?
● double과 decimal type의 비교
아래 예제는 double과 decimal값을 비교하기 위한 것으로 code자체보다는 값에 대한 이해를 중심으로 봐야 합니다.
Console.WriteLine("Using doubles:");
double a = 0.1;
double b = 0.2;
if (a + b == 0.3)
{
Console.WriteLine($"{a} + {b}와 {0.3}은 같음");
}
else
{
Console.WriteLine($"{a} + {b}와 {0.3}은 같지 않음");
}
예제에서는 2개의 double변수를 선언하고 해당 변수의 값을 더한 그 결과를 비교하고 있습니다.
소수에서의 분리자로 , (comma)를 사용하는 지역의 경우에는 다음과 같이 약간 다르게 표시될 수 있습니다.
0,1 + 0,2와 0,3은 같지 않음 |
double type은 0.1과 같은 수를 문자 그대로 부동소수점값으로 표현될 수 없기 때문에 정확성을 보장하지 않습니다.
정확성을 대략적으로 가늠할 때, 특히 두 수의 동일성 비교가 그렇게 중요하지 않은 경우, 예를 들어 사람의 키를 측정하여 더 크거나 더 작은지만 판단할 뿐 정확하게 일치하는지의 여부를 판단할 필요가 없는 경우에만 double을 사용해야 합니다.
위 예제의 문제점은 computer가 숫자 0.1 또는 그 배수를 저장하는 방식으로 설명됩니다. binary에서 0.1을 표현하기 위해 computer는 아래 표와 같이 1/16, 1/32, 1/256, 1/512등을 1로 저장하게 되며
4 | 2 | 1 | . | 1/2 | 1/4 | 1/8 | 1/16 | 1/32 | 1/64 | 1/128 | 1/256 | 1/512 | 1/1024 | 1/2048 |
0 | 0 | 0 | . | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0 |
따라서 소수에서 숫자 0.1은 binary로 0.00011001100110011...이 됩니다.
double값을 절대로 ==을 사용해 비교하지 마십시오. 실제 Gulf전에서 Patriot missile은 계산을 위해 double값을 사용하였습니다. double의 비 정확성으로 인해 공격해 오는 Iraqi의 Scud missile을 추적하고 요격하지 못해 미군 측 아군 28명이 사망하는 일이 있었습니다. 자세한 내용은 아래 글을 참고하시기 바랍니다.
The Patriot Missile Failure (umn.edu)
이제 다시 예제를 수정하여 기존의 double형식의 변수를 decimal로 바꿔주고 할당값과 비교값에도 M접미사를 붙여 줍니다.
Console.WriteLine("Using doubles:");
decimal a = 0.1M;
decimal b = 0.2M;
if (a + b == 0.3m)
{
Console.WriteLine($"{a} + {b}와 {0.3}은 같음");
}
else
{
Console.WriteLine($"{a} + {b}와 {0.3}은 같지 않음");
}
위와 같이 변경한 후 다시 예제를 실행시키면 이번에는 다음과 같은 결과를 표시하게 됩니다.
decimal의 경우에는 큰 정수부로서 숫자로 저장하고 소수점을 이동시키는 방법으로 정확성을 보장합니다. 예를 들어 0.1은 1로 저장되고 소수점을 왼쪽으로 한자리 이동시켜 표현합니다. 12.75의 경우에도 1275로 저장하고 소수점을 왼쪽으로 2자리 이동시켜 12.75를 표현하는 것입니다.
자연수만을 다루는 경우라면 int를 사용하고 다른 값과의 동일성을 위해 비교되는 않는 실수라면 double을 사용하며 더 작거나 더 큰 값을 비교하는데만 사용합니다. decimal은 실수의 정확성이 중요한 통화, CAD 설계 및 일반적인 공학계산등에 사용합니다.
float과 double type은 몇 가지 유용한 특수한 값을 가지고 있습니다. NaN은 예를 들어 0으로 나누는 경우와 같이 not-a-number를 표현하며 Epsilon은 float 혹은 double에서 저장가능한 가장 작은 양수값을, PositiveInfinity와 NegativeInfinity는 각각 무한한 큰 정수와 음수값을 표현합니다. 또한 IsInfinity와 IsNan과 같이 이들 특별한 값을 확인할 수 있는 method를 갖고 있습니다.
(6) Boolean
Boolean은 true와 false라는 2개의 literal값 중 하나만을 포함할 수 있습니다.
bool bt = true;
bool bf = false;
이러한 Boolean형식은 분기와 loop에 가장 많이 사용되고 있습니다.
(7) object
object라고 하는 이 특별한 type은 모든 data type을 저장할 수 있는 유연성을 갖췄지만 지저분한 code와 성능저하를 초래할 수 있기 때문에 가능한 한 사용을 피해야 합니다. object type에 대한 간단한 사용방법을 알고 보기 위해 solution에 variables라는 project를 추가하고 program.cs를 아래와 같이 작성합니다.
object height = 1.88; // object에 double형 저장
object name = "kim"; // object에 string형 저장
Console.WriteLine($"{name} is {height} metres tall.");
int length1 = name.Length; // compile error
int length2 = ((string)name).Length; // compiler에게 string형식임을 알려줌
Console.WriteLine($"{name} has {length2} characters.");
project를 실행하면 다음과 같이 해당 구문에 오류가 발생함을 알 수 있습니다. 이는 name이라는 변수의 data type을 compiler가 알 수 없기 때문입니다.
오류가 나는 해당 구문의 시작점에 \\문자를 추가하여 주석처리하고 project를 다시 실행합니다. 두 번째 name변수에서는 (string)이라는 형변환을 사용함으로써 명시적으로 compiler에게 object변수가 string형식임을 알려주고 있습니다. 이로서 length라는 속성으로 compiler가 접근할 수 있기 때문에 아무런 문제 없이 project가 실행됩니다.
object type은 C#의 첫 번째 version부터 존재하던 것이지만 앞서 설명한 문제점 때문에 이를 대체하기 위해 C# 2.0부터는 generics라는 대안이 마련되어 있습니다. 이것은 object만큼의 유연성을 제공하면서도 성능면에서 부하를 주지 않습니다.
(8) dynamic type
dynamic은 object와 마찬가지로 모든 data type을 저장할 수 있지만 역시 성능적인 문제가 생길 수 있습니다. dynamic keyword는 C# 4.0에서 도입되었으며 object와는 달리 변수에 저장된 값은 명시적인 cast 없이도 member에 접근할 수 있습니다.
Program.cs에서 기존의 code를 모두 삭제하고 아래와 같이 변경합니다.
dynamic something = "kim";
Console.WriteLine($"Length is {something.Length}");
예제에서는 something라는 dynamic 개체에 string형식의 값을 저장하고 있습니다. string은 Length라는 속성을 가지고 있습니다.
실제 위 예제를 실행하면 정상적으로 compile 되고 원하는 결과까지 출력함을 알 수 있습니다.
다시 예제를 아래와 같이 변경하고 project를 실행하면 이번에는 예외를 발생하게 될 것입니다.
dynamic something = 12;
Console.WriteLine($"Length is {something.Length}");
이는 int가 Length라는 속성을 가지고 있지 않기 때문이며 해당 예외는 compile이 아닌 runtime에서 발생한 것입니다. 왜냐하면 compile까지는 dynamic에 저장된 값의 type을 확인하지 않지만 runtime에 와서 CRL이 member를 확인하고 해당 member가 없는 경우 예외를 던지기 때문입니다. 또한 이러한 이유로 dynamic에서는 IntelliSense가 지원되지 않습니다.
예외는 runtime에서 무엇인가 잘못되었음을 나타내기 위한 방법으로서 추후에 자세히 알아볼 것입니다.
(9) 지역변수 선언
지역변수는 method내부에서 선언되며 method가 실행되는 동안에만 존재할 수 있습니다. 따라서 method의 실행이 종료되면 지역변수에 할당된 memory는 소거됩니다.
좀 더 정확하게 말하면 값 type의 경우에는 memory에서 소거되지만 참조 type인 경우에는 garbage collction에서 대신 소거하는 절차를 거치게 됩니다. 값 type과 참조 type에 관해서는 추후에 자세히 알아볼 것입니다.
● 지역변수의 type 명시하기
아래 예제는 특정 type으로 지역변수를 선언하고 해당 변수에 값을 할당한 예를 표현하고 있습니다.
int population = 67_000_000;
double weight = 1.88;
decimal price = 4.99M;
string fruit = "Apples";
char letter = 'Z';
bool happy = true;
참고로 위 예제를 실제 작성하면 Visual Studio나 기타 다른 code editor에 따라 변수에 값이 할당되었으나 사용되지 않았다는 경고를 나타낼 수 있습니다.
● 지역변수의 type 추론
C# 3 이후부터는 지역변수를 선언할 때 var keyword를 사용할 수 있습니다. compiler는 할당연산자(=)를 통해 할당된 값을 통해서 type을 추론하게 됩니다.
소수점이 없는 literal 수의 경우는 기본적으로 int변수로 추론되는데 이때 literal 수에 특정한 접미사를 붙여주면 해당 접미사에 따라 특정 type으로의 추론을 유도할 수 있습니다.
- L : long
- UL : ulong
- M :decimal
- D : double
- F : float
소수점이 존재하는 literal 숫자는 접미사로 M을 붙여주지 않으면 기본적으로 double로 추론되며 M이 있으면 decimal로 F가 있으면 float 변수로 추론됩니다.
큰따옴표(")는 string임을 나타내며 홀따옴표는(')는 단일문자형식인 char를 나타냅니다. 또한 true와 false는 bool type을 나타냅니다.
이러한 방식으로 이전 예제에 var를 사용하면 아래와 같이 변경될 수 있습니다.
var population = 67_000_000; //int
var weight = 1.88; // double
var price = 4.99M; // decimal
var fruit = "Apples"; // strings
var letter = 'Z'; // chars
var happy = true; // bool
위와 같이 code를 작성한 뒤 Visual Studio에서 특정 변수명에 mouse를 올리면 tooltip을 통해 해당 변수가 어떠한 형식으로 추론되는지를 표시해 줍니다.
이번에는 Program.cs에서 아래와 같이 XML로의 작업을 위한 namespace를 import 합니다. 다음에 선언할 몇몇 변수는 해당 namespace에 있는 type을 사용할 것이기 때문입니다.
.NET Interactive Notebooks를 사용한다면 주요 code를 작성한 cell위로 새로운 cell을 분리하여 using문을 추가하고 Execute Cell을 click 하여 namespace가 import 되도록 해야 합니다. 그러면 다음 cell에서 해당 namespace를 사용할 수 있습니다.
이전 구문아래에 아래와 같이 XML개체를 생성하는 구문을 추가합니다.
var xml1 = new XmlDocument(); //C# 3이후에서만 가능
XmlDocument xml2 = new XmlDocument(); //모든 C#에서 가능
var file1 = File.CreateText("something1.txt");
StreamWriter file2 = File.CreateText("something2.txt");
위 예제에서와 같이 개체에서도 var keyword는 변수의 할당 부분에서 type이 명시되므로 xml2처럼 type의 명시를 반복하지 않아도 됩니다. 따라서 file1에서와 같은 형태로 var를 사용하는 것은 좋은 방법이 아닙니다. 해당 구문에서는 어디에도 type이 명시되지 않기 때문이며 이런 경우에는 file2에서 처럼 var가 아닌 필요한 type을 명시해 주는 것이 좋습니다.
var keyword가 어떤 경우에서는 유용하게 사용될 수 있지만 몇몇 개발자들 사이에서는 사용된 type을 명시하지 않음으로 인해 code자체를 읽기 어렵게 만들 수 있다는 이유로 var의 사용을 최소화하기도 합니다. 만약 이런 걱정이 있다면 type이 명확한 경우에만 사용할 수도 있을 것입니다. 예를 들어 위의 예제에서 xml1은 할당과정에서 type이 xml2만큼 명확하면서도 더 짧은 구문을 가지게 있습니다. 그러나 file1에서는 해당 변수의 type이 명확하지 보이지 않기 때문에 file2와 같이 type을 드러내도록 하는 방법이 더 선호될 수 있습니다.
● 개체의 instance를 위한 대상으로 형식화된 new (target-typed new) 식
C#9에서 Microsoft는 target-typed new로 알려진 개체의 instance를 위한 새로운 구문을 도입했습니다. 이를 통해 개체를 instance화 할 때 변수 앞에서 type을 지정하고 해당 type이 반복 없이 new keyword를 사용하는 것입니다.
XmlDocument xml3 = new();
field나 속성을 통해 설정이 필요한 type인 있는 경우 아래와 같이 type을 추론할 수 있습니다.
// In Program.cs
Person kim = new();
kim.BirthDate = new(1967, 12, 26); // 기존방식 : new DateTime(1967, 12, 26)
class Person
{
public DateTime BirthDate;
}
개체를 instance 하기 위한 이러한 방법은 특히 같은 type에 대해 여러 개체를 가질 수 있는 array나 collection에서 유용하게 사용될 수 있습니다.
List<Person> people = new()
{
new() { FirstName = "kim" },
new() { FirstName = "hong" },
new() { FirstName = "choi" }
};
C# compiler 9 version의 이전 version을 사용하는 경우가 아니라면 개체의 instance 할 때 target-typed new 식의 사용을 권장합니다.
(10) type에 대한 기본값의 설정과 가져오기
string을 제외한 대부분의 원시 type은 값 type이며 이는 반드시 값을 가져야 한다는 것을 의미합니다. 이를 위해 default() 연산자를 사용하여 기본값을 지정할 수 있고 매개변수로서 type을 전달할 수도 있습니다.
string type은 참조 type이며 string 변수자체는 값에 대한 memory주소를 가지게 됩니다. '변수=값'이 성립하지 않는 것입니다. 또한 참조 type은 값으로 null을 가질 수 있는데 null은 변수가 어떠한 것도 참조하지 있지 않다는 것을 나타내는 literal에 해당하며 null은 모든 참조 type의 기본상태입니다.
아래 예제는 각각의 값 type에 대한 기본값을 default() 연산자로 확인하고 있습니다.
Console.WriteLine($"default(int) = {default(int)}");
Console.WriteLine($"default(bool) = {default(bool)}");
Console.WriteLine($"default(DateTime) = {default(DateTime)}");
Console.WriteLine($"default(string) = {default(string)}");
예제를 실행하면 다음과 같은 결과를 표시할 것입니다.
실행하는 지역에 따라 날짜와 시간은 다른 형식으로 표시될 수 있으며 string에서 null은 공백으로서 출력됩니다. 이번에는 예제를 아래와 같이 수정하고
int number = 13;
Console.WriteLine($"set to : {number}");
number = default;
Console.WriteLine($"default reset : {number}");
위 예제를 실행하여 실제 설정된 값과 기본값으로 변경된 결과를 확인합니다.
4. Console App
Console app은 text기반의 명령줄 실행 program입니다. 위에서 예제를 생성하고 실행하는 데 사용한 것이 바로 console app이며 이들에 관해서 좀 더 알아보고자 합니다. Console app은 단순하지만 꽤 많은 역할을 수행하고 있으며 compile이나 간단한 script작업에서도 많이 사용됩니다.
console app에서도 method처럼 필요한 인수를 전달하여 app의 동작을 제어할 수 있습니다. 이에 대한 예로 아래 명령은 현재 folder명 대신 특정 이름을 지정하여 F#언어를 사용한 새로운 console app을 생성하도록 합니다.
dotnet new console -lang "F#" --name "ExploringConsole" |
(1) 사용자에게 output 표시
console app이 수행하는 가장 일반적인 작업 2가지는 data를 읽고 쓰는 것입니다. 위 예제에서 우리는 이미 WriteLine method를 사용해 화면에 결과를 출력해 보았습니다. 만약 line의 끝에서 carriage return을 사용하지 않고 싶다면 Write method를 사용할 수도 있습니다.
● 번호가 매겨진 위치 인수를 사용한 서식화
서식화된 문자열을 생성하는 한 가지 방법으로 번호가 매겨진 위치 인수(numbered positional argument)를 사용하는 것이 있습니다.
이 기능은 Write나 WriteLine와 같은 method에서 지원하고 있으며 해당 기능을 지원하는 않는 method라면 문자열의 Format method를 사용해 문자열 매개변수를 서식화할 수 있습니다.
solution에서 Formatting이름의 Console app project를 생성하고 Program.cs의 기존내용을 모두 삭제한 아래와 같이 변수를 선언하 뒤 해당 변수의 값을 Console로 출력하고 string으로 서식화 하도록 변경합니다.
int numberOfApples = 12;
decimal pricePerApple = 0.35M;
Console.WriteLine(format: "{0} apples cost {1:C}", arg0: numberOfApples, arg1: pricePerApple * numberOfApples);
string formatted = string.Format(format: "{0} apples cost {1:C}", arg0: numberOfApples, arg1: pricePerApple * numberOfApples);
Write, WriteLine, Format method는 arg0, arg1, arg2, arg3이름의 총 4개에 대한 번호가 매겨진 인수를 사용할 수 있습니다. 만약 그 이상의 값을 전달해야 한다면 이때는 예제의 formatted와 같이 인수이름을 사용할 수 없습니다.
문자열 서식화에 익숙해진다면 arg0, arg1처럼 인수이름을 사용할 필요가 없습니다. 위 예제에서도 0과 1이 어디에서 왔는지를 보여주기 위해 formatted에 비표준 style을 사용하였습니다.
● 보간 문자열을 사용한 서식화
C# 6 이후부터는 보간문자열이라는 이름의 편의기능이 추가되었습니다. $문자의 접두사를 가진 문자열에서는 아래와 같이 변수의 이름을 중괄호로 감싸서 표현함으로써 변수의 문자열 위치와 현재값을 표시할 수 있습니다.
int numberOfApples = 12;
decimal pricePerApple = 0.35M;
Console.WriteLine($"{numberOfApples} apples cost {pricePerApple * numberOfApples:C}");
위와 같이 문자열에서 서식화를 위해 보간문자열을 사용하면 사람이 code를 읽기가 훨씬 편해집니다. 또한 보간 문자열을 사용하면 보간문자열 내부에서 여러 line에 걸쳐 code를 작성할 수 있습니다. 이는 C# 11부터 도입된 line break를 통해 지원되는 방식입니다.
Console.WriteLine($"{numberOfApples} applescost {pricePerApple
* numberOfApples:C}");
이러한 방식을 통해 문자열 변수에서 문자열을 지정할 때도 편리하게 사용할 수 있는데 C# 10 이전에 문자열은 각기 다른 변수의 문자열연결을 위해 + 연산자를 사용해야 했지만
const string firstname = "hong";
const string lastname = "sang su";
const string fullname = firstname + " " + lastname;
C# 10부터는 보간문자열을 바로 사용하여 문자열끼리 결합할 수 있습니다.
const string firstname = "hong";
const string lastname = "sang su";
const string fullname = $"{firstname} {lastname}";
이러한 방식은 const형식의 문자열값을 결합하는데만 사용될 수 있으며 runtime에서 data type을 형변환 해야 하는 숫자와 같은 다른 type에서는 사용할 수 없습니다.
● 서식 문자열
변수나 표현식에서는 comma(,)나 colon(:) 다음에 서식 문자열을 사용함으로써 서식화될 수 있습니다.
'N0' 서식 문자열은 천 단위로 분리되며 소수가 없는 숫자를 의미합니다. 또한 'C' 서식 문자열은 통화를 의미하는데 이 형식은 현재 thread에 의해 서식이 결정될 수 있습니다.
예를 들어 숫자나 통화형식의 숫자를 미국으로 지역화된 PC에서 실행한다면 천단위 분리자로서의 comma와 함께 pounds sterling을 사용하게 되지만 독일로 지역화된 PC라면 천단위 분리자로서 점(.)과 함께 euro를 사용하게 될 것입니다.
형식항목의 구문형식은 아래와 같습니다.
{ index [, alignment ] [ : formatString ] } |
각각의 형식항목에서는 정렬구분을 가질 수 있는데 이는 table에서 값을 출력할 때 지정한 문자넓이 안에서 왼쪽 혹은 오른쪽 정렬이 필요한 경우 사용됩니다. 정렬값은 정수로 설정될 수 있는데 양수인 경우 오른쪽 정렬을. 음수의 경우 왼쪽정렬을 의미합니다.
예를 들어 과일과 해당 과일이 몇 개가 있는지를 table로 표시하려는 경우 과일의 이름을 10자 정도의 크기로 왼쪽정렬을 하고 개수는 소수자리가 0인 숫자로 서식화여 6자 크기로 오른쪽 정렬을 하는 경우라면 서식문자열을 아래와 같이 작성할 수 있습니다.
string applesText = "Apples";
int applesCount = 1234;
string bananasText = "Bananas";
int bananasCount = 56789;
Console.WriteLine(format: "{0,-10} {1,6}", arg0: "Name", arg1: "Count");
Console.WriteLine(format: "{0,-10} {1,6:N0}", arg0: applesText, arg1: applesCount);
Console.WriteLine(format: "{0,-10} {1,6:N0}", arg0: bananasText, arg1: bananasCount);
위 예제를 실행하면 아래와 같이 정렬된 결과를 확인할 수 있습니다.
(2) 사용자로부터의 text 입력
사용자로 부터 특정 입력을 받기 위한 것으로 ReadLine method를 사용할 수 있습니다. 해당 method를 호출하면 사용자로 부터의 입력을 위해 대기하고 사용자가 필요한 내용을 입력 후 enter key를 누르게 되면 method는 사용자가 입력한 값을 그대로 반환합니다.
.NET Interactive Notebook에서는 Console.ReadLine()와 같은 입력 method를 사용할 수 없습니다. 따라서 string? name = "kim"; 과 같이 literal값을 사용해야 합니다.
사용자로부터의 입력기능을 확인해 보기 위해 Program.cs를 아래와 같이 작성합니다. 아래 예제는 사용자에게 이름과 나이의 입력을 요청하고 입력한 값을 그대로 출력하도록 합니다.
Console.Write("your name : ");
string? firstName = Console.ReadLine();
Console.Write("your age : ");
string? age = Console.ReadLine();
Console.WriteLine($"Hello {firstName}, you look good for {age}.");
기본적으로 .NET 6 이후에서는 null가능여부를 확인하고 있으므로 firstName과 age변수에?를 사용하지 않으면 ReadLine method는 string대신 null을 반환할 수 있으므로 사용자에게 경고를 표시할 수 있습니다.
따라서 각 변수에서 string다음에?를 사용하면 이는 compiler에게 변수가 null값을 다룰 수 있음을 알려주게 되고 그럼 경고를 표시하지 않게 됩니다. 이후 WriteLine method에서 변수가 null인 경우 공백을 대신 표시하게 되므로 application은 정상적으로 실행됩니다. 다만 null인 변수의 member로의 접근이 필요한 경우라면 null에 대한 별도의 처리가 필요합니다.
또는 아래와 같이 문의 끝(semi-colon전)에! 를 사용할 수도 있습니다.
string age = Console.ReadLine()!;
이것은 null-forgiving 연산자를 사용한 것으로 compiler에게 ReadLine method는 null을 반환하지 않는다는 것을 명시적으로 알려주는 역할을 합니다. 이로서 경고는 표시되지 않을 것입니다. 다만 이렇게 했음에도 불구하고 null이 들어오는 경우에 대한 책임은 개발자가 직접 져야 합니다. 물론 예제에서 사용된 Console의 ReadLine method는 빈 string값이라도 반드시 string을 반환하기 때문에 null에 대한 별도의 처리는 필요하지 않습니다.
위에서는 compiler의 null가능 경고에 대한 2가지 일반적인 처리방식을 알아보았습니다. 추후에는 이와 관련하여 저 자세한 사항을 알아볼 것입니다.
위 예제를 실행하고 이름과 나이를 입력하면 다음과 같은 결과를 확인할 수 있습니다.
(3) Console 사용의 간소화
C# 6.0 이후부터 using구문은 namespace를 import 하는 것뿐만 아니라 더 나아가 static class를 import 함으로써 code를 더욱 간소화시킬 수 있게 되었습니다. 이에 대한 예로 지금 다루고 있는 Console app에서 Console type 이름을 code전역에서 일일이 입력해주지 않고 있습니다.
● 단일 file에서 static type Importing
해당 기능을 확인해 보기 위해 Program.cs의 상단에 System.Console class를 정적으로 import 하기 위한 구문을 추가합니다.
using static System.Console;
이것으로 이제 나머지 code에서 사용된 모든 'Console.'부분을 제거할 수 있게 되었습니다. 따라서 기존의 code를 아래와 같이 변경합니다.
using static System.Console;
Write("your name : ");
string? firstName = ReadLine();
Write("your age : ");
string age = ReadLine()!;
WriteLine($"Hello {firstName}, you look good for {age}.");
예제를 실행하면 이전과 동작이 완전히 동일하다는 것을 알 수 있습니다.
● project의 모든 file을 위한 static type Importing
단지 하나의 file에서만을 위한 정적 importing대신 project의 모든 file에서 적용될 수 있는 importing을 구현할 수도 있습니다.
Program.cs에서 기존 System.Console의 정적 import를 제거합니다. Formatting project의 csproj file을 열어 <PropertyGroup> section다음에 아래와 같이 <ItemGroup> section을 추가합니다. 여기서 NET SDK 기능을 암시적으로 사용하는 System.Console의 정적 import가 이루어집니다.
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Using Include="System.Console" Static="true" />
</ItemGroup>
이 상태에서 그대로 project를 실행하면 예외 없이 이전과 동일한 동작이 수행될 것입니다.
(4) 사용자로 부터의 key입력받기
ReadKey method를 사용하면 사용자로부터 key입력을 받을 수 있습니다. 이때 method는 사용자가 key 혹은 다른 key와의 조합을 이루는 key의 입력을 대기하게 되고 실제 입력이 발생하면 그 결과를 ConsoleKeyInfo값으로 반환합니다.
.NET Interactive Notebook에서는 ReadKey method의 호출을 실행할 수 없습니다.
Program.cs file을 아래와 같이 변경합니다. 해당 예제는 사용자에게 단일 key나 또는 조합된 key의 입력을 요청하고 입력받은 key의 정보를 표시하도록 합니다.
Write("Press any key combination: ");
ConsoleKeyInfo key = ReadKey();
WriteLine();
WriteLine("Key: {0}, Char: {1}, Modifiers: {2}", arg0: key.Key, arg1: key.KeyChar, arg2: key.Modifiers);
project를 실행하고 k key를 눌러 다음과 같은 결과를 확인합니다.
이번에는 shift key를 누른 상태에서 k key를 다시 눌러봅니다.
만약 해당 project를 Visual Studio Code안의 terminal에서 실행하는 경우라면 일부 key의 조합이 Visual Studio Code에서 포착되어 실행 중인 console app까지 도달하지 못할 수 있습니다. 예를 들어 Ctrl + Shift + X key는 Visual Stduio Code에서 왼쪽 side bar의 Extensions view를 표시하는 기능을 실행하게 됩니다. 예제를 다양하게 test 해 보려면 외부에서 Windows Terminal 등을 실행시키고 Console app을 실행해야 합니다.
(5) Console app으로 인수전달하기
Console app을 실행할 때 특정한 값의 인수를 전달하여 필요한 동작을 수행시킬 수 있습니다. 예를 들면 dotnet 명령어에서 생성하고자 하는 project의 이름을 아래와 같이 전달할 수 있습니다.
dotnet new console dotnet new mvc |
C# 6.0 이전 version에서 console app project template은 아래와 같은 code를 기본적으로 생성합니다.
using System;
namespace Arguments
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
위 예제와 같이 string[] args 인수가 선언되어 있고 여기에 program class의 Main method로 값이 전달됩니다. 즉 args는 console app으로 인수를 전달하는 데 사용되는 array에 해당하는 것입니다. 하지만 .NET 6 이후의 console app project template에서는 top-level program이 사용되는데 여기서는 program class와 Main method가 감춰지고 args array 역시 보이지 않게 됩니다. 하지만 이러한 것들은 보이지 않을 뿐 여전히 존재하고 있으므로 이전 version과 동일하게 args인수를 그대로 사용할 수 있습니다.
명령줄 인수는 공백에 의해 구분되며 hypen이나 colon과 같은 다른 문자는 인수값의 일부로서 처리됩니다.
만약 인수값에 공백을 포함시키고자 한다면 인수값 전체를 홀따옴표(')나 큰따옴표(")로 감싸야합니다.
명령줄에서 terminal window는 필요에 따라 크기와 전경색, 배경색등을 임의로 설정할 수 있습니다. 실제 이러한 동작을 수행하는 console app을 만들어야 한다고 가정했을 때 사용자가 입력한 color색상명과 크기지정에 필요한 숫자를 console app의 entry point에 해당하는 Main method로 전달하여 args array로부터 해당 값을 읽을 수 있을 것입니다.
csStudy02 solution에 Arguments 이름의 project를 새롭게 추가하고 Arguments.csproj project file을 열어 <PropertyGroup> section 다음에 project의 전체 C#에 적용할 System.Console의 정적 import를 위해 <ItemGroup> section을 아래와 같이 추가합니다.
<ItemGroup>
<Using Include="System.Console" Static="true" />
</ItemGroup>
향후 console app project를 생성할 때는 위와 같은 System.Console의 정적 import를 위한 설정이 기본적으로 적용되었다는 것을 가정하고 진행합니다. 이 과정은 향후 다시 반복하지 않을 것입니다.
Program.cs에서는 기존의 구문을 모두 삭제하고 application으로 전달된 인수의 수를 표시하도록 하는 아래 구문을 추가합니다.
WriteLine($"There are {args.Length} arguments.");
이 상태에서 project를 실행하면 아마도 다음과 같은 결과를 표시하게 될 것입니다.
이제 Visual Studio의 Project의 속성으로 들어가 Debug tab을 선택하고 'Open debug launch profiles UI'를 click 합니다. 그리고 'Command line arguments'안에 인수값으로 'firstarg second-arg third:arg "fourth arg"'를 다음과 같이 입력합니다.
'Launch Profiles'를 닫고 project를 다시 실행하면 다음과 같은 결과를 표시하게 됩니다.
Program.cs file을 다시 열어 전달받은 4개의 인수값을 열거하도록 하는 구문을 array의 크기를 반환하는 이전 예제의 구문다음에 아래와 같이 작성합니다.
WriteLine($"There are {args.Length} arguments.");
foreach (string arg in args)
{
WriteLine(arg);
}
project를 다시 실행하면 이번에는 모든 인수의 값을 다음과 같이 표시할 것입니다.
(6) 인수를 통한 option 설정
위와 같은 방식을 통해 실제 사용자가 terminal window의 전경색, 배경색 그리고 cursor의 size를 직접 설정할 수 있도록 구현할 수 있습니다. cursor의 size는 cursor cell 아래의 선을 의미하는 1부터 cursor cell 높이의 백분율을 의미하는 100까지 의 값이 올 수 있습니다.
이전 예제에서 이미 System.Console class에 대한 적정 import를 적용했고 여기에는 ForegroundColor와 BackgroundColor 그리고 CursorSize와 같은 속성을 가지고 있으므로 Console. 에 해당하는 접두사 없이도 이들에 대한 이름만을 사용해 필요한 값을 설정할 수 있습니다.
System namespace는 이미 import 되었으므로 compiler는 ConsoleColor와 Enum type이 어떤 것인지를 알 수 있습니다. Program.cs에서 사용자가 이들 3개 인수에 대한 값을 입력하지 않은 경우에 경고를 표시할 수 있도록 하는 구문과 3개의 인수를 사용해 색상과 CursorSize를 설정하는 구문을 아래와 같이 기존 예제에서 추가한 구문 다음에 추가합니다.
if (args.Length < 3)
{
WriteLine("You must specify two colors and cursor size, e.g.");
WriteLine("dotnet run red yellow 50");
return; // stop running
}
ForegroundColor = (ConsoleColor)Enum.Parse(enumType: typeof(ConsoleColor), value: args[0], ignoreCase: true);
BackgroundColor = (ConsoleColor)Enum.Parse(enumType: typeof(ConsoleColor), value: args[1], ignoreCase: true);
CursorSize = int.Parse(args[2]);
위와 같은 code를 작성할 때 compiler는 CursorSize는 오직 Windows에서만 지원되는 설정임을 나타내는 경고를 표시할 수 있습니다.
Visual Studio의 project 속성에서 이전에 했던 것과 마찬가지 방법으로 인수값을 'red yellow 50'으로 변경하고 project를 다시 실행하면 절반 크기의 cursor와 함께 다음과 같이 색상이 변경됨을 확인할 수 있습니다.
compiler가 비록 error나 경고를 주지 않았더라도 runtime에서 일부 API호출은 다른 platform에서 실패할 수 있습니다. Windows에서 동작하는 console app이 cursur의 size를 바꿀 수 있다고 하더라도 macOS에서는 지원되지 않으며 이것을 시도한다면 예외를 일으킬 수 있습니다.
(7) 특정 API를 지원하지 않는 platform에 대처하기
따라서 만약 위와 같은 문제가 발생할 수 있다면 이러한 경우는 exception handler를 사용해 해결해야 합니다. 이때는 try ~ catch 구문을 사용할 수 있는데 예외처리에 대한 좀 더 자세한 사항은 추후에 알아볼 것이지만 일단은 아래와 같이 CursorSize구문 자체를 try ~ catch로 감싸도록만 처리합니다.
try {
CursorSize = int.Parse(args[2]);
} catch (PlatformNotSupportedException) {
WriteLine("platform does not support - CursorSize");
}
만약 위 예제를 macOS에서 동작시키는 경우라면 catch 된 예외를 보게 될 것이며 WiteLine에서 지정한 message를 사용자에게 표시할 것입니다.
다양한 OS환경에 따른 세부적인 처리는 System namespace에 있는 OperatingSystem class를 사용해 아래와 같이 처리할 수 있습니다.
if (OperatingSystem.IsWindows())
{
// Windows only
}
else if (OperatingSystem.IsWindowsVersionAtLeast(major: 10))
{
// Windows 10 or later
}
else if (OperatingSystem.IsIOSVersionAtLeast(major: 14, minor: 5))
{
// iOS 14.5 or later
}
else if (OperatingSystem.IsBrowser())
{
// Browser only (Blazor)
}
OperatingSystem class는 Android, iOS, Linux, macOS 그리고 Blazor web component를 위한 browser 같은 다른 일반적인 OS를 위해 사용할 수 있는 동일한 method를 갖고 있습니다.
다른 platform에 따른 또 다른 처리 방법으로는 조건부 compile를 사용하는 것입니다. 이와 관련하여 사용할 수 있는 조건부 compile에 대한 4개의 전처리 지시자가 존재하는데 #if, #elif, #else, #endif 등이 그것입니다. 또한 #define을 사용해 아래와 같이 기호문자를 정의할 수도 있습니다.
#define MYSYMBOL
물론 기본적으로 사용가능한 기호문자 역시 존재하며 이들은 자동적으로 정의됩니다.
Framework | Symbol |
.NET Standard | NETSTANDARD2_0, NETSTANDARD2_1 등 |
.NET | NET7_0, NET7_0_ANDROID, NET7_0_IOS, NET7_0_WINDOWS 등 |
따라서 위와 같은 기능을 통해 아래와 같이 특정한 platform에서만 compile 되는 구문을 작성할 수 있습니다.
#if NET7_0_ANDROID
// andriod에서만 compile될 구문
#elif NET7_0_IOS
// iOS에서만 compile될 구문
#else
// 그외
#endif
5. async와 await
C# 5에서는 쉽게 multithreading이 기능하도록 하는 Task type을 위해 2개의 C# keyword를 도입했으며 이 keyword는 하나의 쌍으로 아래와 같은 경우에 유용하게 사용될 수 있습니다.
- GUI(Graphical User Interface)를 위한 multitasking구현
- Web Application과 Web Service를 위한 확장성 구현
- filesystem, database, remote service 등 처리를 완료하기까지 다소 긴 시간이 요구될 수 있는 것과의 상호작용에서 호출이 차단되는 것을 방지하는 구현
async와 await keyword는 위에서 언급한 것 외에 생각보다 많은 다양한 곳에서 활용될 수 있는데 우선은 console app에서부터 어떤 방식으로 사용될 수 있는지에 대한 간단한 예부터 살펴보고 점차 이를 응용해 가도록 할 것입니다.
(1) console app의 응답성 향상
console app에서의 한 가지 제한사항으로는 async로 수식된 method안에서 await keyword만 사용할 수 있다는 것입니다. C#7 이전에는 Main method가 async로 수식되는 것을 허용하지 않았지만 C# 7.1에 들어서면서 드디어 Main에서도 async를 사용하기 위한 새로운 기능이 도입하게 됩니다.
csStudy02 solution에서 AsyncConsole이름의 새로운 project를 추가합니다. 그리고 AsyncConsole.csproj에서도 역시 System.Console의 정적 import를 위한 ItemGroup section을 추가합니다.
Program.cs에서 기존의 구문을 모두 삭제하고 HttpClient instance를 생성하기 위한 구문을 추가합니다. 그리고 naver home page를 호출하도록 한 뒤 해당 main page가 얼마나 많은 byte 수를 가지고 있는지를 표시하도록 합니다.
HttpClient client = new();
HttpResponseMessage response = await client.GetAsync("https://www.naver.com/");
WriteLine("Naver home page has {0:N0} bytes.", response.Content.Headers.ContentLength);
.NET 5 이전에는 아래와 같은 error message를 볼 수 있습니다.
Program.cs(14,9): error CS4033: The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'. [/Users/markjprice/Code/ Chapter02/ AsyncConsole/AsyncConsole.csproj] |
이런 경우 Main method에 직접 async keyword를 추가해야 하며 void type의 return에서 Task로 변경해야 합니다. 그러나 .NET 6 이후부터는 console app project template은 Main method의 비동기구현과 함께 Program class를 자동적으로 정의하는 top-level program 기능을 사용하게 되었습니다.
위와 같이 작성한 후 project를 실행하면 아래와 같은 결과를 확인할 수 있습니다. 이때 byte는 naver main home page가 주기적으로 바뀌면서 표시되는 숫자 역시 달라질 수 있습니다.