-
공통 중간 언어 (Common Intermediate Language)
- 자바와 마찬가지로 C# 소스 코드를 컴파일하면 기계어로 바로 번역되는 것이 아니라 중간 언어(Intermediate Language)로 먼저 번역되고, 닷넷 런타임이 IL을 기계어로 번역해 준다. (자바의 바이트코드 같은 개념)
- IL은 구조상 어셈블리어와 비슷하게 생겼으며, 정식 명칭은 CIL(Common Intermediate Language)라고 함
- ILASM.EXE 컴파일러를 이용해 IL 코드를 컴파일하면 동일한 결과물을 얻을 수 있다.
- C#, VB.NET, C++/CLI, ASP.NET, F# 외에도 COBOL, Lisp, Python, PHP, Ruby 등의 언어도 IL 언어로 번역할 수 있으므로 닷넷 런타임은 언어에 독립적이다.
- 하지만 C#은 닷넷에 의한, 닷넷을 위한, 닷넷의 언어이므로 닷넷 기능을 최대한 활용할 수 있다.
-
공용 타입 시스템 (Common Type System)
- 닷넷 호환 언어가 지켜야 할 타입의 표준 규격
- CTS 규약의 범위를 벗어난 언어는 IL을 생성할 수 없다. (가령, 다중 상속)
- 그러나 CTS 규약을 100% 구현할 필요는 없다.
-
공용 언어 사양 (Common Language Specification)
- 닷넷 호환 언어가 지켜야 할 최소한의 언어 사양
- 예를 들어 C#, VB.NET 등 닷넷 호환 언어로 작성된 프로그램이 다른 언어로 작성된 프로그램과 객체를 공유할 수 있는 이유는 CLS를 준수하기 때문이다.
-
메타데이터
- C# 언어로 컴파일된 실행 파일에는 메타데이터가 있어서, 소스가 없어도 Reflection 기술을 이용해 어떤 클래스, 메서드가 제공되는지 확인 가능하다.
-
어셈블리, 모듈, 매니페스트
- 어셈블리: 닷넷에서 생성한 결과물(EXE, DLL)을 의미함 (그 어셈블리가 아니다!)
- 어셈블리 안에는 1개 이상의 모듈이 있을 수 있다.
- 모듈 목록을 관리하는 매니페스트라는 것이 있다.
- 즉, 하나의 어셈블리 안에는 매니페스트를 포함하는 모듈이 무조건 있으며, 그 외의 모듈들이 있을 수 있다. (각 모듈은 메타데이터를 갖고 있음)
-
공용 언어 기반구조 (Common Language Infrastructure)
- 마이크로소프트에서 ECMA 표준으로 제출한 공개 규약: CTS 명세, IL에 대한 코드 정의, 메타데이터와 이진 파일의 구조 등
- 마치 여러 회사에서 만든 JVM이 있듯이, 공개된 CLI를 이용해 닷넷 런타임을 만들 수 있다.
-
공용 언어 런타임 (Common Language Runtime)
- CLR의 역할: (1) IL을 JIT 컴파일러를 이용해 기계어로 번역하고, (2) GC를 제공해 동적 메모리 할당 및 회수를 지원함
- CLR 자체를 관리 환경(Managed Environment)라고 함
- 과거 닷넷 프레임워크가 구현한 CLR은 CLI 사양을 따르는 대표적인 VM이었다.
- 이후 닷넷 코어가 나오면서 다중 플랫폼에 맞게 재구현한 CoreCLR이 나왔다.
-
닷넷 프레임워크, 닷넷 코어, 그리고 닷넷
- 닷넷 = CoreCLR + 부가 구성 요소
- 부가 구성요소로는 BCL(Base Class Library)와 기타 파일들이 있다.
- 히스토리: "닷넷 프레임워크"가 처음에 등장, 다중 플랫폼 지원을 위해 "닷넷 코어"가 등장했다가 "닷넷"으로 통합됨
- 닷넷 프레임워크는 4.8까지 나왔고, 닷넷 코어는 3.1까지 나왔으며, 닷넷은 5, 8, 7, 8 순서로 나왔다. (2024년 상반기)
- 리눅스, Mac OS X, Android, iOS 등을 지원하며 모바일(Xamarin / .NET MAUI 프레임워크, Unity 게임 엔진)도 지원한다.
| 설명 | .NET Framework | .NET Core |
|---|---|---|
| 플랫폼 | Windows | Linux, Mac OS X 등 |
| 앱 유형 | WPF, ASP.NET, Windows Forms | UWP, ASP.NET Core |
| 기반 라이브러리 | Base Class Library | Core Library |
- C# 언어는 닷넷의 버전 업그레이드와 함께 꾸준히 발전해 왔고 이를 간단히 정리하면 다음과 같다.
- C# 언어의 향후 로드맵은 여기서 확인할 수 있다.
| 닷넷 런타임 | C# 언어 | 주요 기능 |
|---|---|---|
| 1.0, 1.1 | C# 1.0 | - |
| 2.0, 3.0 | C# 2.0 - 기존 문법 보완 | 제네릭, 익명 함수, 널(Null) 타입 |
| 3.5 | C# 3.0 - 함수형 언어의 장점 흡수 | 람다 표현식, 확장 메서드, LINQ, 익명 타입 |
| 4.0 | C# 4.0 - 동적 언어의 장점 흡수 | 지연 바인딩, 선택적 파라미터, 명명된 인자, COM 지원 확장 |
| 4.5 | C# 5.0 - 비동기 호출 추가 | async/await 비동기 예약어 |
| 4.6 | C# 6.0 - 간편 표기 문법 보강 | 프레임워크와 컴파일러의 분리, 다수의 간편 표기 문법 |
| 4.7 ~ 4.7.2, 4.8 | C# 7.0 ~ 7.1 - 패턴 매칭, C# 7.2 - 구조체 성능 향상, C# 7.3 - 문법 보강 | 패턴 매칭, 튜플, Span<T> |
| .NET Core 3.0 | C# 8.0 - 문법 보강 | 비동기 스트림, 인덱스/범위 연산자, 기본 인터페이스 메서드, 다수의 간편 표기 문법 |
| .NET 5 | C# 9.0 - 문법 보강 | 레코드, 함수 포인터, 모듈 이니셜라이저, 최상위 문 |
| .NET 6 | C# 10 - 문법 보강 | record의 구조체 지원, 다수의 세부 문법 개선 |
| .NET 7 | C# 11 - 문법 보강 | 패턴 매칭 추가, 원시 문자열 및 utf8 문자열 지원, 다수의 세부 문법 개선 |
| .NET 8 | C# 12 - 문법 보강 | 기본 생성자, 기본 람다 파라미터, 인라인 배열, 컬렉션 식, 기타 문법 개선 |
- 기본 자료형
- 정수형: sbyte, byte (8비트), short, ushort (16비트), int, uint (32비트), long, ulong (64비트)
- 이것들은 닷넷 기본 타입의 별칭이다. (System.Sbyte, System.Byte, System.Int16, System.UInt16, System.Int32, System.UInt32, System.Int64, System.UInt64)
- 실수형: float (4바이트, System.Single), double (8바이트, System.Double), decimal (16바이트, System.Decimal)
- 문자형: char (유니코드 16비트, System.Char), string (유니코드 문자열, System.String)
- 불리언: bool (System.Boolean)
- 정수형: sbyte, byte (8비트), short, ushort (16비트), int, uint (32비트), long, ulong (64비트)
- 형 변환 (암시적, 명시적), 예약어 및 키워드, 식별자, 리터럴, 변수 (스택, 힙), 상수, 연산자, 배열 (다차원, 가변 배열도 있음), 제어문 (조건문, 반복문), 구조체 (struct), 열거형 (enum)
-
클래스 (멤버, 메서드, 생성자, 종료자), 네임스페이스 (using, namespace), 캡슐화 (private, protected, public, internal, internal protected), 프로퍼티 (get, set), 읽기 전용 (readonly)
-
상속, 형 변환 (as, is), 부모 클래스 (base), 다형성 (오버라이드, 오버로드)
-
클래스 확장: 중첩 클래스, 추상 클래스, 델리게이트 (delegate), 이벤트, 인터페이스 (interface), 인덱서
-
깊은 복사와 얕은 복사 (ref [in, out 개념], out): 포인터 대용으로 생각하면 된다.
-
전처리기 지시문: 특정 소스코드를 상황에 따라 컴파일 과정에서 추가/삭제할 때 사용한다.
- #define, #undef: 소스코드 맨 위에 있어야 함
- #if, #elif, #else, #endif: 적용되는 부분에 기록함
- 그 외에도 #warning, #error, #line, #region, #endregion, #pragma 지시문이 있다.
-
지역 변수의 유효 범위: 지역 변수가 정의되면 변수를 포함하는 블록에서만 유효하다.
-
일반적인 애트리뷰트: 소스 외의 정보이며 메타데이터 정보에 포함됨, 프로그램에 영향을 끼치지 않으나 개발자가 정보를 남길 수 있음
- System.Attribute로부터 상속 받은 클래스이다.
- 애트리뷰트: 주석과 달리 컴파일러에게 코드에 대한 정보를 제공하기 위한 기능이다.
- 형태는 다음과 같다: [AuthorAttribute], [Author], [Author()], [Author("Anders")], [Author("Anders", Version = 1)], ...
- 생성자, 프로퍼티를 이용해 값을 지정할 수도 있다.
- Reflection 기술과 함께 응용하여 사용될 수 있다.
-
Flags 애트리뷰트
- System.AttributeUsageAttribute는 다른 애트리뷰트와 달리 적용할 수 있는 대상이 한정되어 있다.
- AttributeTargets 값의 종류의 각 타깃은 다음과 같다. (생략하면 다음 표에 따라 자동으로 선택됨)
- 다만 타깃을 명시적으로 지정해야 하는 경우도 있다. (예를 들어, BCL의 MarshalAs 애트리뷰트는 타깃이 Field, Paramter, ReturnValue가 있으므로 직접 명시해야 함)
| AttributeTargets 값 | [target: ...] | 설명 |
|---|---|---|
| Assembly | assembly | 어셈블리 |
| Module | module | 모듈 |
| Class | type | class |
| Struct | type | struct |
| Enum | type | enum |
| Constructor | method | 타입의 생성자 |
| Method | method | 타입의 메서드 |
| Property | property | 타입의 프로퍼티 |
| Field | field | 타입의 필드(멤버) |
| Event | event | 타입의 이벤트 |
| Interface | type | interface |
| Parameter | param | 메서드의 파라미터 |
| Delegate | type | delegate |
| ReturnValue | return | 메서드의 리턴값 |
| GenericParameter | typevar | C# 2.0에 추가된 제네릭 파라미터에 지정됨 |
| All | 기본값 | AttributeTargets에 정의된 모든 것 |
-
연산자
- 시프트 연산자: 비트 단위로 데이터를 제어할 때 사용함 (<<, >>)
- 비트 논리 연산자: 조건 논리 연산자(&&, ||, ^, !) 외에도 비트 논리 연산자(&, |, ^, ~)가 존재한다.
-
예약어
- 연산 범위 확인: checked, unchecked (이 키워드 다음에 {}로 감싸면, 오버플로에 대한 예외를 발생 혹은 발생시키지 않음. 단, 언더플로는 체크 못함)
- 가변 파라미터: params (함수 인자로 params 키워드 다음에 가변 배열 변수를 입력 받으면 임의의 개수를 인자로 받을 수 있다. C++의 va_list 개념)
- Win32 API 호출: extern (C#에서 C/C++ 언어로 만들어진 비관리 코드의 기능을 사용할 때 platform invocation을 사용함)
- 예를 들어, User32.dll 파일의 BOOL MessageBeep([in] UINT uTYPE) 함수를 호출한다고 해보자.
[DllImport("user32.dll")] static extern int MessageBeep(uint uType);
- Win32 API에 대한 PInvoke 구문은 www.pinvoke.net 사이트에서 검색해 볼 수 있다.
- 예를 들어, User32.dll 파일의 BOOL MessageBeep([in] UINT uTYPE) 함수를 호출한다고 해보자.
- 안전하지 않은 컨텍스트: unsafe (스택에 할당된 변수에 대해 C/C++ 언어의 포인터를 사용할 때 이 키워드를 맨앞에 붙임)
- 참조 형식의 멤버에 대한 포인터: fixed (힙에 할당된 인스턴스에 대해 C/C++ 언어의 포인터를 사용할 때 이 키워드를 맨앞에 붙임)
- 고정 크기 버퍼: fixed (위와 기능이 다름, extern으로 참조한 C++ DLL 함수에 인자를 넘길 때 메모리 배열 길이로 인한 오류를 막기 위해 unsafe 문맥에서 변수 앞에 사용함)
- 스택을 이용한 값 형식 배열: stackalloc (값 형식의 배열을 힙이 아닌 스택에 강제로 할당하게 만듦, 스택은 용량이 적지만 빠름)
int* pArray = stackalloc int[1024]; // int 4byte * 1024 == 4KB 용량을 스택에 할당
-
솔루션 (.sln)
- 1개 이상의 프로젝트를 포함하고 있다.
- 솔루션이 오피스 제품이라면, 프로젝트는 워드, 엑셀, 파워포인트, 아웃룩이라고 비유할 수 있다.
-
프로젝트 (.csproj)
- 비주얼 스튜디오의 소스코드 관리를 위해 도입된 개념
- 한 프로젝트 당 여러 개의 소스코드를 담을 수 있으며, 하나의 프로젝트는 하나의 EXE 또는 DLL 파일이 만들어진다. (내부적으로 XML 형식으로 기록되어 있음)
-
라이브러리 재참조
- 본인이 만든 라이브러리, 닷넷이 제공하는 라이브러리, NuGet 패키지를 통해 받은 라이브러리를 활용할 수 있다.
-
디버그 빌드, 릴리스 빌드
- 릴리스 빌드: 사용자에게 출시할 때에는 릴리스 빌드를 활용한다. (최적화 포함, 전처리 상수 TRACE 사용함)
- 디버그 빌드: 개발 중에는 디버그 빌드를 활용한다. (최적화 없음, 전처리 상수 DEBUG/TRACE 사용함)
- 전처리 상수 대신 [Conditional("DEBUG")] 애트리뷰트 등을 사용해도 됨
- 그 외에도 System.Diagnostics 네임스페이스에서 Debug, Trace 타입의 WriteLine 함수를 이용해 Debug, Trace 메시지를 따로 출력할 수 있다. (DebugView 프로그램 활용 가능)
-
플랫폼 선택
- 닷넷 런타임은 인텔/AMD CPU, ARM CPU를 지원하며, 그 외에도 모든 CPU를 지원한다. (x86/x64, ARM32/ARM64, AnyCPU)
-
예외
- 대표적인 예외로는 0으로 나누거나, 배열 범위를 벗어나거나, null을 참조하는 등이 있다.
- System.Exception 타입이 제공하는 멤버는 다음과 같다.
- Message (프로퍼티): 예외를 설명하는 메시지를 리턴한다.
- Source (프로퍼티): 예외를 발생시킨 애플리케이션의 이름을 리턴한다.
- StackTrace (프로퍼티): 예외가 발생한 메서드의 호출 스택을 리턴한다.
- ToString (메서드): Message, StackTrace 내용을 포함하는 문자열을 리턴한다.
- 개발자는 System.Exception에서 상속해서 예외를 직접 만들 수도 있다.
-
예외 처리 및 발생
- try/catch/finally 구문을 이용해서 예외가 발생해도 프로그램이 중단되지 않도록 할 수 있다.
- throw 구문을 이용해서 예외를 발생시킬 수도 있다.
- 다만 예외 처리는 무겁기 때문에 가능하면 다른 방법을 사용해서 회피하는 것이 좋다.
- 참고: using 키워드는 try/finally/Dispose에 대한 간편 표기법으로 사용된다. (네임스페이스를 선언하는 using과 다름)
- 가비지 수집기 (Garbage Collector)
- 가비지 수집기 작동 원리
- CLR에서 처음 할당되는 객체는 모두 0세대에 속한다.
- 0세대 객체의 용량이 일정 크기를 넘어가면 GC는 가비지 수집을 한다. --> 사용되지 않는 0세대 객체는 없애고, 사용되는 객체는 1세대로 승격된다.
- 1세대 객체의 총 용량이 일정 크기를 넘어가면 GC는 가비지 수집을 한다. --> 사용되지 않는 객체는 없애고, 1세대의 객체가 그 시점에도 사용되고 있으면 2세대로 승격된다. (2세대가 끝)
- 2세대 객체의 총 용량이 일정 크기를 넘어가면 GC는 가비지 수집을 한다.
- 가비지 수집을 하면 객체를 가리키는 주소값이 변경될 수 있다.
- 단, 일정 크기의 객체는 가비지 수집마다 이동하면 성능이 크게 손실되므로 별도의 대용량 객체 힙(Oarge Object Heap)에 생성된다.
- 주소는 바뀌지 않는 대신 메모리 파편화 현상이 발생한다.
- 대용량 객체는 처음부터 2세대 객체로 분류한다.
- 가비지 수집기 작동 원리
- 여기서 소개할 BCL 타입들은 일부에 불과하다. (자세한 것은 .NET API browser 참고할 것)
- System.DateTime
- System.TimeSpan
- System.Diagnostics.Stopwatch: 코드의 특정 구간에 대한 성능을 측정할 때 사용할 것
- System.String
- System.Text.StringBuilder: 문자열을 연결하는 작업이 많을 때 사용할 것
- System.Text.Encoding: ASCII, Default, Unicode (UTF-16을 의미함), UTF32, UTF8 등이 있음
- System.Text.RegularExpressions.Regex
- System.BitConverter
- System.IO.MemoryStream
- System.IO.StreamWriter / System.IO.StreamReader: 스트림에 문자열 데이터를 읽고 쓰는 데 유용함
- System.IO.BinaryWriter / System.IO.BinaryReader: 스트림에 이진 데이터를 읽고 쓰는 데 유용함
- System.Xml.Serialization.XmlSerializer
- System.Text.Json.JsonSerializer
- System.Collections.ArrayList
- System.Collections.Hashtable
- System.Collections.SortedList
- System.Collections.Stack
- System.Collections.Queue
- System.IO.FileStream
- System.IO.File / System.IO.FileInfo
- System.IO.Directory / System.IO.DirectoryInfo
- System.IO.Path
- System.Threading.Thread
- System.Threading.Monitor
- System.Threading.Interlocked
- System.Threading.ThreadPool
- System.Threading.EventWaitHandle
- 비동기 호출: I/O 연산이 끝날 때까지 차단되지 않으므로 non-blocking 호출이라고도 함
- System.Delegate의 비동기 호출: 일반 메서드에 대해서도 비동기 호출을 할 수 있는 수단 (C# 5.0의 async/await 구문이 제공되면서 거의 사용하지 않게 됨)
- System.Net.IPAddress
- 포트
- System.Net.IPEndPoint
- System.Net.Dns
- System.Net.Sockets.Socket
- System.Net.Http.HttpClient
- 마이크로소프트 SQL 서버
- ADO.NET 데이터 제공자
- 데이터 컨테이너
- 데이터베이스 트랜잭션: using 키워드를 이용해 트랜잭션을 begin하고 마지막에 commit 해야 함
- AppDomain과 Assembly
- Type과 리플렉션
- 리플렉션을 이용한 확장 모듈 구현
- 윈도우 레지스트리
- BigInteger
- IntPtr
-
예를 들어, int와 같은 프리미티브 타입인 객체를 ArrayList와 같은 컬렉션 객체에 담을 경우 박싱/언박싱으로 인해 성능 저하가 발생한다.
- 박싱/언박싱 문제: int 타입 <-> object 타입 식으로 잦은 형 변환이 발생하는 문제
- 제네릭은 C++의 template과 유사한 개념이다.
ArrayList를 보완한List<T>타입이 도입됨: 여기서 T는 타입으로 대체할 수 있다.- 구체적인 프리미티브 타입을 지정함으로써 박싱/언박싱 문제가 해결된다.
- 제네릭 클래스, 제네릭 메서드가 있다.
-
제네릭 메서드의 경우, 형식 파라미터에 제약 조건을 걸 수 있다.
-
public class MyClass<T> where T: ICollection public class MyType<T> where T: ICollection, IConvertible public class Dict<K, V> where K: ICollection where V: IComparable - where 키워드를 사용해 형식 파라미터가 따라야 할 제약 조건을 지정할 수 있다.
- 위의 경우 외에도 struct (값 형식), class (참조 형식), new() (기본 생성자 필수 포함), U (U 타입 인수) 키워드 등을 이용해 특별한 제약 조건을 걸 수 있음 (where T: [키워드])
-
-
BCL에 적용된 제네릭은 다음과 같다. (가능하면 제네릭 버전 컬렉션을 대신 사용하는 것을 권장함)
ArrayList --> List<T>Hashtable --> Dictionary<TKey, TValue>SortedList --> SortedDictionary<TKey, TValue>Stack --> Stack<T>Queue --> Queue<T>
-
제네릭 버전 인터페이스는 다음과 같다. (가능하면 제네릭 버전 인터페이스를 대신 사용하는 것을 권장함)
IComparable --> IComparable<T>IComparer --> IComparer<T>IEnumerable --> IEnumerable<T>IEnumerator --> IEnumerator<T>ICollection --> ICollection<T>
- 피연산자1 ?? 피연산자2
- 참조 형식의 피연산자1이 null이 아니라면 그 값을 그대로 반환하고, null이라면 피연산자2의 값을 반환한다.
-
일반적인 변수의 경우, 값 형식은 0으로 참조 형식은 null로 초기화된다.
-
그러나 제네릭 형식의 파라밑로 전달된 경우에는 미리 타입을 알 수 없으므로 초기값을 결정할 수 없다.
- default 예약어를 사용하면 컴파일러가 T 형식에 따라 자동으로 초기값을 결정할 수 있도록 해준다.
- default 예약어는 타입을 인자로 받기 때문에 임의로 타입을 지정하는 방식으로도 사용할 수 있다.
- yield 문법은 IEnumerable/IEnumerator로 구현한 코드에 대한 간편 표기법이라고 볼 수 있다.
- yield return: 리턴할 때 위치를 기억해 뒀다가 다음에 호출될 때 마지막 yield return 문이 호출됐던 코드 다음 줄부터 실행이 재개된다.
- yield break: 열거문이 제대로 작동하도록 루프 수행 후 열거를 중지하는 역할을 한다.
- yield 문법이 적용된 코드는 그대로 실행되는 것이 아니라 C# 컴파일러가 내부적으로 yield 문이 사용된 메서드를 컴파일 시에 IEnumerable이 적용된 것과 같은 코드로 치환한다.
- 이것은 필수 문법이 아니며, 알아두면 코드를 좀 더 간결하게 작성하는 데 도움이 된다.
- partial class 키워드를 사용하면 클래스 정의를 나누어서 할 수 있다.
- 클래스 정의가 나뉜 코드는 한 파일에 있어도 되고 다른 파일로 나누는 것도 가능하지만 반드시 같은 프로젝트에서 컴파일해야 한다.
- C# 컴파일러가 빌드 시에 같은 프로젝트에 있는 partial 클래스를 하나로 모아 단일 클래스로 빌드한다.
- 이 기법은 윈도우 응용 프로그램, 웹 응용 프로그램 개발 시에 주로 사용한다고 한다.
- nullable 타입:
System.Nullable<T>구조체- 일반적인 값 형식에 대해 null 표현이 가능하게 하는 역할을 한다.
- double? == Nullable
- hasValue 속성: 값이 할당되어 있으면 True, 할당되어 있지 않으면 False
- Value: hasValue가 true일 경우 유효한 값이 들어 있다.
- 이름이 없는 메서드로서 델리게이트에 전달되는 메서드가 1회용일 때 유용하게 사용된다.
- 함수 이름 대신 delegate 키워드를 사용하여 함수 정의를 넣어서 인자로 넘겨준다.
- 이 문법은 간편 표기법으로 C# 컴파일러가 내부적으로 빌드 시점에 중복되지 않을 특별한 문자열을 하나 생성해 메서드의 이름으로 사용하고 delegate 키워드 다음의 코드를 분리해 해당 메서드의 본체로 넣는다.
- 클래스의 정의에 static 키워드를 사용하여 정의된 정적 클래스는 오직 정적 멤버만을 내부에 포함할 수 있다.
- BCL 라이브러리에 포함된 System.Math 타입이 대표적이다.
- Math 타입 클래스는 인스턴스를 여러 개 만들 필요가 없기 때문이다.
- C# 컴파일러에 타입 추론(type inference) 기능이 추가되면서 생긴 문법
- 자바 스크립트의 동적 타입과 같은 기능
- 코드가 간결해지는 장점이 있으나 엄격한 타입을 정하지 않을 경우 코드 가독성 저하, 디버깅의 어려움 등이 있을 수 있으므로 권장하지 않음
- 자동으로 get, set 함수를 만들어 주는 기능
public string Name { get; get; }: 이렇게 하면 자동으로 get, set 함수를 만들어 준다.public string Name { get; protected set; }: get, set에 서로 다른 접근 제한자를 지정할 수도 있다.
- 내부 상태 변수의 개수가 많아질수록 생성자를 여러 개 만들어야 한다.
- C# 3.0에서는 public 접근자가 명시된 멤버 변수에 한해 new 구문에서 이름과 값을 지정하여 초기화하는 기능을 제공한다.
-
class Person { string _name; int _age; public string Name { get { return _name; } set { _name = value; } public int Age { get { return _age; } set { _age = value; } } } class Program { static void Main(string[] args) { // 아래의 생성자들은 클래스 선언에 따로 정의되어 있지 않아도 작동한다. Person p1 = new Person(); Person p2 = new Person { Name = "Anders" }; Person p3 = new Person { Age = 10 }; Person p4 = new Person { Name = "Anders", Age = 10 }; // get, set 함수가 없으면 이렇게라도 할 수 있다. Person p5 = new Person { _name = "Anders", _age = 10 }; } }
- 배열 변수처럼 컬렉션 객체를 초기화할 수 있는 문법이 추가되었다.
List<int> numbers = new List List<int> { 0, 1, 2, 3, 4 }; // Add 메서드를 사용하지 않아도 됨- 단, 해당 컬렉션 타입이 반드시
ICollection<T>인터페이스를 구현해야 한다.
- C# 2.0에서 익명 메서드가 지원되고, C# 3.0에서는 익명 타입도 지원된다.
var p = new { Count = 10, Title = "Anders" };- 단, 객체의 타입명이 없기 때문에 지역 변수를 선언할 때 var 예약어를 사용해야 한다.
- C# 2.0에서 부분 클래스가 지원되고, C# 3.0에서는 부분 메서드도 지원된다.
- 단, 코드 분할을 하는 것이 아니라 메서드의 선언부/구현부를 분리할 수 있게만 허용한다.
- 구현부가 정의되어 있지 않아도 컴파일은 정상적으로 된다.
- 제약 사항: (1) 리턴 값을 가질 수 없다. (2) ref 파라미터는 사용할 수 있지만 out 파라미터는 사용할 수 없다. (3) private 접근자만 허용된다.
- 제약 사항이 심하고 부분 클래스도 많이 사용하지 않기 때문에 별로 활용되지는 않는다.
-
일반적으로 상속을 통해 클래스를 확장하나 2가지 문제점이 있다.
- sealed로 봉인된 클래스는 확장할 수 없다. (예: BCL의 System.String 타입)
- 클래스를 상속 받아 확장하면 기존 소스코드를 새롭게 상속 받은 클래스 이름으로 바꿔야 한다.
-
확장 메서드 기능을 사용하면 클래스의 내부 구조를 전혀 바꾸지 않고 마치 새로운 인스턴스 메서드를 정의하는 것처럼 추가할 수 있다.
- 확장 메서드는 static 클래스에 정의되어야 한다.
- 확장 메서드는 반드시 static이어야 하고, 확장하려는 타입의 파라미터를 this 키워드와 함께 명시해야 한다.
-
그러나 클래스 상속과 달리 다음 한계점이 있다.
- 부모 클래스의 protected 멤버 호출이 불가능하다.
- 부모 클래스의 메서드 재정의(오버라이드)가 불가능하다.
- C#의 경우 람다 식은 다음과 같이 구별되어 사용된다.
- 코드로서의 람다 식: 익명 메서드의 간편 표기 용도로 사용됨
- 자바 스크립트의 화살표 연산자(=>)의 문법과 같다.
- 다만, 자바 스크립트와 달리 델리게이트로 정의해서 자주 사용하는 람다 식을 델리게이트로 정의해두고 쓸 수 있다. (Action, Func)
- 데이터로서의 람다 식: 람다 식 자체가 데이터가 되어 구문 분석의 대상이 됨 (메서드로 실행될 수 있음)
- 중괄호가 없는 람다 식의 경우, 그 자체로 "식을 표현한 데이터"로 사용할 수 있다.
- 람다 식이 코드가 아니라 System.Linq.Expressions.Expression 타입의 인스턴스 데이터 역할을 하게 된다.
Expression<Func<int, int, int>> exp = (a, b) => a + b; // 이렇게 하면 람다 식으로 Expression 객체로 다룰 수 있음- Expression 타입과 대응되는 팩터리 메서드를 이용하면, 일반적인 메서드 내부의 C# 코드를 Expression 조합만으로도 프로그램 실행 시점에 만들어 낼 수 있다.
- 코드로서의 람다 식: 익명 메서드의 간편 표기 용도로 사용됨
- LINQ (Language Integrated Query)
- C# 및 VB.NET 컴파일러에서 자주 사용되는 정보의 선택/열거 작업을 일관된 방법으로 다루기 위해 기존 문법을 확장시킨 것이다.
- 쿼리 문법은 SQL 쿼리의 SELECT 구문과 유사하다.
- 쿼리 문은
IEnumerable<T>타입으로 저장된다. - 이것도 간편 표기법에 지나지 않으며, C# 컴파일러에 의해 빌드 시에 원래의 확장 메서드를 사용하는 코드로 변경되어 컴파일된다.
-
선택적 파라미터: 인자를 전달하지 않아도 미리 지정된 기본값을 사용하는 것을 의미한다. (C++의 디폴트 파라미터 개념과 동일함)
- ref, out 예약어와 함께 사용할 수 없다.
- params 타입의 파라미터는 선택적 파라미터가 될 수 없다.
- 선택적 파라미터의 기본값은 반드시 상수 표현식이어야 한다.
-
명명된 인수: 인자의 값을 받는 인자의 이름을 표기하는 것을 의미하며 코드의 가독성을 높인다. (파이썬의 함수에서 사용하는 기법)
p.Output(age: 5, name: "Tom", address: "Tibet");
-
마이크로소프트는 기존의 C#, VB.NET, C++/CLI와 같은 정적 언어 말고도 동적 언어까지도 닷넷과 호환되도록 CLR을 바탕으로 한 DLR(Dynamic Language Runtime) 라이브러리를 내놓았다.
- 문제는 동적 언어로 만들어진 프로그램의 타입 시스템을 C#과 같은 정적 언어에서 연동하는 방법이 없다는 점이다.
- 이런 문제를 해결하기 위해 C# 4.0에서 dynamic 예약어를 추가했다.
- C# 3.0에서의 동적 타입인 var 예약어와 비슷해 보이나 차이가 있다.
- var 예약어는 빌드 시점에 초기값과 대응되는 타입으로 치환되는 반명, dynamic 변수는 컴파일 시점에 타입을 결정하지 않고 해당 프로그램이 실행되는 시점에 타입을 결정한다.
- dynamic 예약어는 내부적으로는 CallSite.Target 메서드를 사용하는 간편 표기법에 불과하다.
-
dynamic 예약어가 도입됨에 따라 다음 장점을 갖추게 되었다.
- 리플렉션 개선
- 덕 타이핑: 동적 언어에서 제공하는 기능으로 서로 다른 타입의 동일한 프로퍼티/메서드를 호출할 수 있게 해준다.
- 동적 언어와의 타입 연동
- 다중 스레드에서 컬렉션에 접근할 때 동기화를 적용하지 않으면 오류가 발생한다.
- 일일이 동기화 코드를 추가하는 것은 매우 번거로운 일이다.
- 그래서 BCL에서는 다중 스레드에서 쉽게 이용할 수 있는 전용 컬렉션을 System.Collections.Concurrent에서 제공하고 있다.
BlockingCollection<T>: Producer/Consumer 패턴에서 사용하기 좋은 컬렉션ConcurrentBag<T>:List<T>의 순서가 지켜지지 않는 동시성 버전ConcurrentDictionary<TKey, TValue>:Dictionary<TKey, TValue>의 동시성 버전ConcurrentQueue<T>:Queue<T>의 동시성 버전ConcurrentStack<T>:Stack<T>의 동시성 버전
-
C#은 C/C++과 달리 매크로 상수를 제공하지 않는다.
- 매크로 상수에 대한 요구사항을 반영하여 호출자 정보로 구현되었다.
- 매크로 상수와 같지는 않지만 C#의 특성을 살려 애트리뷰트, 선택적 파라미터의 조합으로 구현되었다.
-
호출자 정보: 호출하는 측의 정보를 메서드의 인자로 전달하는 것을 의미한다. 다음은 C# 5.0에서 제공하는 호출자 정보이다.
- CallerMemberName: 호출자 정보가 명시된 메서드를 호출한 측의 메서드 이름
- CallerFilePath: 호출자 정보가 명시된 메서드를 호출한 측의 소스코드 파일 경로
- CallerLineNumber: 호출자 정보가 명시된 메서드를 호출한 측의 소스코드 라인 번호
- async, await 예약어가 새롭게 추가되었다. (자바 스크립트에서도 지원하는 기능)
- 이 예약어를 이용하면 비동기 호출을 마치 동기 방식처럼 호출하는 코드를 작성할 수 있다.
- async를 메서드 앞에 붙여 주어야, 메서드 내부에서 await 키워드를 인지한다.
- 비동기 호출은 파일 입출력, 네트워크 통신 등 CPU 연산에 비해 시간이 오래 걸리는 작업을 할 때 유용하며 동기 호출과 달리 대기 시간이 발생하지 않는다.
- C# 3.0의 "자동 구현 속성"을 사용한 경우, 초기값을 부여하려면 별도로 생성자 등의 메서드를 구현해야만 했다.
- C# 6.0에서는 자동 구현 속성 초기화 구문을 제공하여 속성 정의 구문에서 직접 기본값을 지정할 수 있게 하였다.
-
class Person { public string Name { get; set; } = "Jane"; // set을 제거하면 읽기 전용 속성이 된다. }
-
메서드가 식(expression)으로 이루어진 경우 간략하게 표현식으로 구현할 수 있다.
public Vector Move(double dx, double dy) => new Vector(x + dx, y + dy);public void PrintIt() => Console.WriteLine(this);public override string ToString() => string.Format("x = {0}, y = {1}", x, y);
-
속성(프로퍼티) 정의에도 표현식을 적용할 수 있다.
public double Angle => Math.Atan2(y, x);- 단, 속성 정의에 대해 람다 식을 구현하면 읽기 전용 필드가 된다.
-
인덱서 구문에도 표현식을 적용할 수 있다.
static double RadianToDegree(double angle) => angle * (180.0 / Math.PI);- 속성(프로퍼티)과 마찬가지로 읽기 전용 인덱서로만 동작하게 된다.
-
생성자/종료자, 이벤트의 add/remove 접근자의 경우 메서드이기는 하지만 표현식을 이용한 구현은 불가능하다.
-
기존에는 static 멤버를 사용할 때 반드시 타입명과 함께 써야 했다.
Console.WriteLine("문자열 출력");
-
그러나 C# 6.0부터는 자주 사용하는 타입의 전체 이름(FQDN)을 using static으로 선언해 두면, 타입명을 생략할 수 있게 되었다.
-
using static System.Console; ... WriteLine("문자열 출력");
-
-
단, C# 3.0에 도입된 "확장 메서드" 기능의 경우에도 static 키워드를 사용하기 때문에 문법적 모호성 문제로 using static 적용을 받지 않으므로 타입명을 생략하면 오류가 발생한다.
- null 조건 연산자: 참조 변수의 값이 null이라면 그대로 null을 리턴하고, null이 아니라면 지정된 멤버를 호출한다.
- 이것을 잘 활용하면 null 값을 확인하는 if 문 사용을 대폭 줄일 수 있다.
list?.Count: list가 null이 아닐 경우 list.Count 멤버 변수의 값을 리턴하고, null이면 null을 리턴한다.
-
기존에는 문자열과 변수의 값을 조합해서 출력하려면 다음 방법을 사용해야 했다.
return "이름: " + Name + ", 나이: " + Age;return string.Format("이름: {0}, 나이: {1}", Name, Age); // string.Format을 이용한 방법
-
C# 6.0에서는 string.Format을 사용하지 않고도 다음과 같이 표현할 수 있다.
return $"이름: {Name}, 나이: {Age}";- 만약 중괄호를 출력하고 싶으면 두 번 연이어 입력해야 한다. (
return $"{{이름: {Name}, 나이: {Age}}}";)
- C# 코드에 사용된 식별자를 이름 그대로 출력하고 싶을 때 다음과 같이 하면 된다.
$"{nameof(name)} == {name}"- 코드 내에서 식별자 이름을 하드코딩할 필요가 없게 되었다.
- 리플렉션은 코드가 실행되어야 이름을 구할 수 있지만, nameof는 C# 6.0 컴파일러가 컴파일 시점에 문자열로 직접 치환해 주므로 실행 시점에 부하가 없다.
-
컬렉션 Dictionary 타입의 기존 초기화 문법은 다음과 같다.
-
var weekends = new Dictionary<int, string> { { 0, "Sunday" }, { 6, "Saturday" }, };
-
-
C# 6.0의 컬렉션 Dictionary 타입의 초기화 문법은 키/값 개념에 맞게 직관적으로 바뀌었다.
-
var weekends = new Dictionary<int, string> { [0] = "Sunday", [6] = "Saturday", }; - 참고로 키 값이 문자열이면
["Key"] = ...식으로 입력하면 된다.
-
- 비주얼 베이직, F# 언어에서 지원하던 예외 필터를 사용할 수 있게 되었다.
-
try { // ...[코드]... } catch (예외_타입 e) when (조건식) { // ...[코드]... } - catch에 지정된 예외_타입에 속하는 예외가 try 블록 내에서 발생했을 때, 조건식이 true로 평가된 경우에만 해당 예외 처리기가 선택된다.
- 조건식에는 메서드도 들어갈 수 있다.
- 기존 예외 처리 구문의 경우, 동일한 예외 타입의 catch 구문을 여러 개 둘 수 없었지만, 예외 필터를 사용하면 이것이 허용된다.
-
- "컬렉션 초기화"에서 설명한 구문이 컴파일되려면
ICollection<T>인터페이스를 구현해야 한다.- C# 6.0에서는
ICollection<T>인터페이스가 없다면 Add 메서드가 확장 메서드로도 구현되어 있는지 다시 한 번 더 찾는 기능을 추가했다.
- C# 6.0에서는
- C# 5.0에서 C# 6.0으로 바뀌면서 개선된 기능은 다음과 같다.
- catch/finally 블록 내에서 await 사용 가능
- #pragma의 'CS' 접두사 지원
- 재정의된 메서드의 선택 정확도를 향상
-
C# 7.0 이전에는 out 파라미터가 정의된 메서드를 사용하려면 반드시 인자로 전달될 인스턴스를 미리 선언해야 했다.
-
int result; int.TryParse("5", out result);
-
-
C# 7.0부터는 변수 선언 없이 변수의 타입과 함께 out 키워드를 쓸 수 있다.
int.TryParse("5", out int result);- 타입 추론을 컴파일러에게 맡기는 var 키워드도 사용할 수 있다. (out var result)
- 심지어 값을 무시하는 구문(discard)까지 추가되어 값이 필요하지 않은 상황에 대해서는 변수명까지 생략할 수 있게 되었다. (
int.TryParse("5", out _);)
-
C# 7.0 이전까지는 ref 키워드를 오직 메서드의 파라미터로만 사용할 수 있었다.
-
C# 7.0부터는 로컬 변수, 리턴값에 대해서도 참조 관계를 맺을 수 있게 개선되었다.
- 로컬 변수의 경우
int a = 5; ref int b = ref a; // 이렇게 하면 a와 b가 동일한 메모리를 공유하게 된다. - 리턴값의 경우 (총 네 곳에서 ref 키워드를 명시해야 함)
ref int item = ref intList.GetFirstItem(); ... public ref int GetFirstItem() { return ref list[0]; } - 제약 사항은 다음과 같다.
- 지역 변수를 return ref로 리턴해서는 안 된다.
- ref 키워드를 지정한 지역 변수는 다시 다른 변수를 가리키도록 변경할 수 없다. (C# 7.3에서 이 제약 사항이 제거됨)
- 로컬 변수의 경우
-
튜플: 유한 개의 원소로 이루어진 정렬된 리스트
- 이것을 이용하면 메서드에서 인자를 받거나 값을 리턴할 때 여러 개의 값을 한 번에 전달할 수 있다. (리턴 값, 인자 모두 튜플 타입이 가능함)
- 괄호를 이용해 다중 값을 처리할 수 있는 구문을 지원하게 되었다.
-
(bool, int) result = pg.ParseInteger("50"); ... (bool, int) ParseInteger(string text) { ... return (result, number); } - 튜플을 리턴하는 메서드가 지정한 튜플의 이름들을 원하지 않거나 이름이 지정되지 않은 튜플인 경우 호츨하는 측에서 강제로 이름을 지정하는 것도 가능하다. (
(bool success, int n) result = pg.ParseInteger("50");) - 심지어 튜플로 받지 않고 개별 필드를 분해해서 받는 구문도 지원한다. (
(var success, var number) = pg.ParseInteger("50");) - 또한 out 파라미터 처리에서 지원했던 생략 기호도 튜플의 리턴값을 분해하는 구문에 사용할 수 있다. (
(var _, var n) = pg.ParseInteger("70");) - 2개의 변수 값을 교환하는 Swap 함수 같은 기능도 튜플을 사용하면 간단하게 구현할 수 있다. (
(a, b) = (b, a);)
-
메서드의 인자와 리턴에 사용한 모든 튜플 구문은 C# 7.0 컴파일러가 소스코드를 컴파일하는 시점에 System.ValueTuple 제네릭 타입으로 변경해서 처리한다.
- 기존의 System.Tuple은 참조 형식이지만 System.ValueTuple은 값 형식이다.
- 커스텀 타입에 Deconstruct 메서드를 1개 이상 정의하여 튜플의 리턴 값을 분해하는 구문을 구현할 수 있다.
- 분해가 되는 개별 값을 out 파라미터를 이용해 처리하면 된다.
- 문법: 접근_제한자 void Deconstruct(out T1 x1, ..., out Tn xn) { ... }
- out 파라미터이므로 반드시 값을 채워서 리턴해야 함
-
class Rectangle { ... public void Deconstruct(out int x, out int y, out int width, out int height) { ... } } ... (var _, var _, int width, int height) = rect;
-
C# 6.0의 "표현식(람다식)을 이용한 메서드, 속성 및 인덱서 정의"에서 람다 식으로 메서드의 정의가 가능했던 유형은 다음과 같다.
- 일반 메서드
- 프로퍼티의 get 접근자 (읽기 전용)
- 인덱서의 get 접근자 (읽기 전용)
-
C# 7.0부터는 다음의 메서드 정의까지 람다 식으로 정의가 가능하다.
- 생성자
- 종료자
- 이벤트의 add/remove
- 프로퍼티의 set 접근자
- 인덱서의 set 접근자
- 로컬 함수 문법: 메서드 안에서만 호출 가능한 메서드를 1개 이상 정의할 수 있다.
- 문법은 메서드 정의와 완전히 같고 단지 다른 메서드의 내부에서 정의한다는 차이점만 있다.
-
일반적으로 async 키워드가 붙은 메서드는 리턴 타입이 반드시
void,Task,Task<T>중 하나로 알려져 있다. -
C# 7.0에서는 비동기 메서드에서 그 외의 타입도 리턴할 수 있게 되었다. (
ValueTask<T>타입)- 기존
Task리턴 타입의 async 메서드는 모든 경우에Task객체를 생성해 리턴하였다. ValueTask는 async 메서드 내에서 await를 호출하지 않은 경우라면 불필요한Task객체 생성을 하지 않음으로써 성능을 높인다.
- 기존
- throw 구문은 제어문과 마찬가지로 식(expression)이 아닌 문(statement)에 해당한다.
- 이전에는 표현식에서 throw를 사용하는 것에 제한이 있었다.
- C# 7.0부터는 throw를 표현식에서도 사용할 수 있게 되었다.
- 그러나 throw가 표현식으로 완전히 바뀐 것은 아니기 때문에 표현식이 허용되는 모든 곳에서 사용할 수는 없다.
- 숫자가 길어지면 가독성이 길어진다.
- C# 7.0부터 숫자 내 임의의 위치에 밑줄을 추가할 수 있도록 허용하고 있다.
int number = 10_000_000; // 이것은 10진수, 16진수, 2진수에도 적용 가능함- 값에는 영향을 미치지 않으며 순수하게 가독성을 높이기 위한 기능이다.
-
C# 7.0부터 추가된 패턴 매칭 유형은 다음과 같다.
- 상수 패턴
- 타입 패턴
- var 패턴
-
is 연산자의 패턴 매칭
- is 연산자는 기존 기능애서 패턴 매칭을 지원하기 위해 as 연산자 기능을 흡수했다.
- 기존의 is 연산자는 변환 여부만 알 수 있었고 as 연산자는 변환 결과를 포함하고 있었는데, 이제는 is 연산자가 as 연산자와 동일한 역할을 수행할 수 있다.
- 물론 기존의 is 연산자처럼 타입만 비교하는 것도 여전히 가능하다.
-
switch/case 문의 패턴 매칭
-
C# 7.1부터는 Main 메서드에도 async를 허용하였다.
- 람다 식으로도 Main 메서드를 구현할 수 있다.
-
가능한 Main 메서드 정의는 다음과 같다.
static async Task Main()static async Task Main(string[])static async Task<int> Main()static async Task<int> Main(string[])
- C# 7.1부터는 default 예약어가 타입 추론이 가능해져서 리터럴 형식으로 쓸 수 있게 바뀐다.
- C# 컴파일러 입장에서는 default 대상이 되는 타입을 추론할 수 있으므로 굳이 타입을 지정할 필요가 없다.
- 제네릭 인자에도 default 리터럴을 쓸 수 있다.
- C# 7.1부터는 튜플의 변수명에 대해서도 타입 추론을 활용해 사용의 편의성을 높였다.
- C# 7.0에서 C# 7.1으로 바뀌면서 개선된 기능은 다음과 같다.
- 패턴 매칭 - 제네릭 추가
- 참조 전용 어셈블리 (Ref Assemblies)
- 메서드의 파라미터로 ref, out 키워드가 있었는데 C# 7.2부터는 in 키워드가 추가되었다.
- ref 키워드를 사용하면 값 복사 부하는 없어지지만, 원본 값도 같이 변경되는 부작용이 있다.
- 그래서 ref와 readonly 의미를 모두 갖는 in 키워드를 추가했다.
- 기존의 readonly 프로퍼티의 경우, 직접 변경하는 것은 불가하나 메서드 호출을 통해 간접적인 변경은 허용한다는 문제점이 있었다.
- 그러나 메서드 호출을 통해 변경할 때 오류가 발생하지도 않고, 실제로 값은 변경되지 않는 현상이 발생한다.
- C# 컴파일러의 방어 복사본(defensive copy) 처리 메커니즘은 개발자가 스스로 인지하기 어려워서 버그를 야기할 수 있다.
- readonly struct는 구조체 자체를 읽기 전용으로 만들어 해결하였다.
- 읽기 전용 구조체는 타입 내의 멤버 변수들을 모두 읽기 전용으로 강제한다. (모든 멤버는 readonly 키워드를 붙여야 함)
- C# 컴파일러가 방어 복사본(defensive copy) 처리를 하지 않으므로 성능이 개선되었다.
- C# 7.2부터는 ref 키워드가 메서드 인자뿐만 아니라 리턴값, 로컬 변수에도 사용 가능하게 되었다.
-
값 형식의 struct는 스택을 사용하지만, 그 struct가 class 안에 정의되어 있는 경우 힙에 데이터가 위치하게 된다.
-
ref struct는 값 형식을 오직 스택에만 생성할 수 있도록 강제하는 방법이다.
- 스택에만 생성할 수 있으므로 로컬 변수, 메서드의 리턴 및 인자로 전달하는 것은 가능하다.
- 다른 ref struct 타입의 필드로 정의할 수도 있다.
- 그 외에 주요한 제약 사항으로 인터페이스를 구현할 수 없다.
- 또 using 문의 대상으로 사용할 수 없다.
-
이러한 특수 타입이 나온 주된 이유는 C# 응용 프로그램의 성능을 최대한 높일 수 있도록 새롭게 도입한
Span<T>를 위해 꼭 필요했기 때문이다.Span<T>타입은 내부적으로 관리 포인터를 사용한다.- 관리 포인터는 절대 관리 힙에 놓일 수 없는 제약이 있기 때문에
Span<T>타입을 구현하면서 관리 포인터를 담을 수 있는 특수 구조체로 ref struct가 도입되었다. Span<T>와 더불어 가비지 컬렉터의 사용을 줄여 성능 향상을 가져온다.
Span<T>: 제네릭 관리 포인터를 가진 readonly ref struct- 배열에 대한 참조 뷰를 제공하는 타입이라고 볼 수 있다.
- 기본적으로 C#에서 만드는 모든 배열을
Span<T>타입으로 가리킬 수 있다. - 무분별한 힙의 사용을 최소화할 수 있고 이로 인해 가비지 컬렉터의 사용을 줄여 자연스럽게 성능 향상을 가져온다.
- 예외 처리기에서도 힙의 경우 catch/finally 절이 실행되지 않지만,
Span<T>타입으로 감싼 경우 예외 처리가 가능하다는 장점이 있다.
- C# 7.2부터는 로컬 변수의 ref 키워드의 사용을 3항 연산자에도 적용할 수 있게 되었다.
- 접근 제한자가 5개(public, internal, protected, internal protected, private)였는데, C# 7.2부터 private protected가 추가되었다.
- private protected는 private 접근자의 적용과는 아무런 상관이 없다.
- 오히려 internal protected와 비슷하다.
- internal protected: 동일 어셈블리 내에서 정의된 클래스이거나 다른 어셈블리라면 파생 클래스인 경우에 한해 접근을 허용한다. 즉 internal 또는 protected 조건이다.
- private protected: 동일 어셈블리 내에서 정의된 파생 클래스인 경우에 한해 접근을 허용한다. 즉 internal 그리고 protected 조건이다.
- C# 7.0의 "리터럴에 대한 표현 방법 개선"에서 숫자형 리터럴에 밑줄을 쓰는 것이 가능했지만, 리터럴 접두사(0x, 0b)가 있는 경우 밑줄이 바로 나올 수 없다는 제약이 풀렸다.
- C# 4.0에 추가된 "명명된 인수"는 일단 파라미터의 이름을 명시하면 이후의 인자들은 모두 이름을 명시해야 하는 제약이 있다.
- C# 7.2부터는 이렇게 뒤에 오지 않는 명명된 인수를 정상적으로 컴파일한다.
p.Output(name: "Tom", 16, address: "Tibet");
-
제네릭의 제약 조건으로 타입을 사용하려면 다음 조건을 만족해야 한다.
- 클래스 타입이어야 한다.
- sealed 타입이 아니어야 한다.
- System.Array, System.Delegate, System.Enum은 허용하지 않는다.
- System.ValueType은 허용하지 않지만 특별히 struct 제약을 대신 사용할 수 있다. 또한 System.Object도 허용하지 않지만 어차피 모든 타입의 기반이므로 제약 조건으로써의 의미가 없다.
-
C# 7.3에서 System.Delegate, System.Enum은 제약 조건에서 풀리게 된다.
-
마지막 제약 조건으로 기존 strcut 제약의 좀 더 특수한 사례에 속한 unmanaged가 있다.
- 이를 이용하면 struct 제약 중에서 대상 타입이 참조 형식을 필드로 갖지 않는다는 보장을 하나 더 해준다.
- 따라서 기존에는 불가능했던 형식 파라미터에 대한 포인터 연산을 할 수 있다.
- fixed 키워드를 그 대상이 기본형이거나 그것의 배열 또는 string으로 제한되었다.
- 사용자가 만든 타입은 fixed의 대상이 될 수 없다.
- C# 7.3부터 사용자 타입이 GetPinnableReference라는 이름으로 관리 포인터를 리턴하는 메서드를 포함하고 있는 경우라면 fixed 구문에 자연스럽게 사용할 수 있도록 통합되었다.
Span<T>타입은 GetPinnableReference를 구현하고 있으므로Span<T>타입에 fixed 키워드를 사용할 수 있다.
- C# 7.3부터는 힙에 할당된 경우일지라도 일관성 있게 fixed 없이 고정 크기 배열에 대한 인덱싱이 가능하다.
-
C# 7.2까지는 다음 4가지 유형에서 변수를 허용하지 않았다.
- 필드 초기화 식
- 프로퍼티 초기화 식
- 생성자 초기화 식
- LINQ 쿼리 식
-
원칙적으로 변수 선언은 문(statement)에 해당하기 때문에 식(expression)을 요구하는 코드에 사용할 수 없다.
- 그러나 out 키워드를 갖는 메서드 호출이나 패턴 매칭에서의 변수명 선언이 발생하는 코드에서도 오류가 발생하므로 표현의 제약을 가져오는 문제로 부각되었다.
- 그래서 위 4가지 유형에 대해 변수를 허용하게 되었다.
- 애트리뷰트는 "자동 구현 속성"에 대해 지정하는 것이 불가능했지만 C# 7.3부터는 가능해졌다.
- 튜플 구문에 대해서도 ==, != 연산자가 지원된다.
- "리턴값 및 로컬 변수에 ref 기능 추가"에서 ref 로컬 변수인 경우 기존에는 다른 변수를 재할당하는 것이 불가능했으나 C# 7.3부터는 가능해졌다.
- "스택을 이용한 값 형식 배열: stackalloc"에서 다룬 스택 배열에 대한 초기화 구문을 C# 7.3부터 지원한다.
-
닷넷 프로그램을 개발하면 종종 접하게 되는 예외가 바로 System.NullReferenceException이다.
- 이 신규 문법에서는 컴파일러 수준에서 null 참조 예외가 없도록 보장하는 것이 목표이다.
-
참조 타입의 사용 유형은 2가지가 있으며, C# 컴파일러는 이렇게 대응한다.
- 해당 인스턴스가 null일 필요가 없는 참조 형식 (Non-nullable reference type) --> 참조 타입을 정의할 때 null 값을 담는 멤버가 없도록 보장함
- 해당 인스턴스가 null일 수 있는 참조 형식 (Nullable reference type) --> 참조 타입의 인스턴스를 사용할 때 반드시 null 체크를 하도록 보장함
-
C# 컴파일러에서 (런타임이 아닌) 컴파일 타임에 null 참조 예외를 어떻게 막을 수 있을까?
- #nullable 지시자를 이용해 해당 소스코드 파일에 null 값일 수 있는 타입이 정의되지 않도록 보장한다.
- #nullable [옵션]: enable이면 null 가능성이 있을 경우 경고를 발생시키고, disable이면 null 가능성 체크를 하지 않는다. (닷넷 7부터는 enable이 기본값임)
- null일 수 있다면 해당 인스턴스를 null 가능한 타입이라고 명시한다.
참조_타입?: 참조_타입에 물음표를 붙여 인스턴스가 null일 수도 있음을 명시한다.- 이렇게 하면 컴파일러가 해당 멤버가 사용된 코드를 검사해 null 가능성이 있다고 경고를 발생시킨다.
- 개발자가 명시적으로 null 체크를 위한 메서드를 만들었다면, C# 컴파일러에 의해 null 체크에 해당하는 코드라고 인식하도록 NotNullWhen 애트리뷰트를 적용할 수 있다.
static bool IsNull([NotNullWhen(false)] string? value): 이 경우 IsNull 메서드가 false를 리턴하면 null이라고 C# 컴파일러가 인지함- NotNullWhen 애트리뷰트 외에도 null 처리 관련 힌트를 부여하는 애트리뷰트가 다수 존재한다: AllowNull, DisableNull, DoesNotReturn, DoesNotReturnIf, MaybeNull, MaybeNullWhen, NotNullIfNotNull
- #nullable 지시자를 이용해 해당 소스코드 파일에 null 값일 수 있는 타입이 정의되지 않도록 보장한다.
-
null 체크 코드를 추가하지 않고 null 자체를 개발자가 감수하기로 했다면 null 포기 연산자를 사용하여 #nullable enable 경고를 무시할 수도 있다.
return person.Name!.Length; // 이 경우 person.Name의 값이 nullable이어도 경고하지 않음
-
nullable 문맥 제어
- 주석 문맥 (annotation context)
- enable
- 모든 참조 타입은 null이 가능하지 않은 참조 타입으로 취급하고 따라서 null 예외 없이 안전하게 접근 가능
- null 가능한 참조 타입(예: string?)을 사용할 수 있고, 해당 변수에 접근할 때 정적 분석기에 의해 null일 수 있다면 경고 발생
- disable
- null 가능한 참조 타입을 사용할 수 없음(참조 타입에 ?를 붙이면 경고 발생)
- 모든 참조 변수는 null일 수 있음
- 참조 변수를 접근해도 경고가 발생하지 않음
- enable
- 경고 문맥 (warning context)
- enable
- null 접근으로 분석된 경우 경고를 발생시킨다. 주석 문맥의 활성화와 상관 없이 정적 분석기의 판정에 따른다.
- disable
- 경고를 발생시키지 않는다.
- enable
- 주석 문맥 (annotation context)
-
각각의 문맥 제어는 #nullable과 #pragma warning 지시자를 이용해 켜고 끌 수 있지만 매번 소스코드에 지정하는 것이 불편하므로 프로젝트 파일의 Nullable 노드를 통해 전역적으로 설정하는 것이 가능하다.
-
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net7.0</TargetFramework> <Nullable>enable</Nullable> </PropertyGroup> </Project> -
주석 경고 지시자 프로젝트 설정 enable enable #nullable enable <Nullable>enable</Nullable>enable disable (불가능) <Nullable>annotations</Nullable>dsiable enable (불가능) <Nullable>warnings</Nullable>disable disable #nullable disable <Nullable>disable</Nullable>
-
- 여기서 비동기 스트림은 IEnumerable/IEnumerator의 비동기 버전이다.
- 메서드가 async를 지원하더라도 동기 버전 IEnumerable/IEnumerator를 사용하면 성능 저하가 발생한다.
- 따라서 비동기 버전 IEnumerable/IEnumerator인 IAsyncEnumerable/IAsyncEnumerator가 추가되었다.
- C# 8.0에 새로 추가된 2개의 연산자가 있다.
-
연산자 문법 의미 ^ ^n 인덱스 연산자로서 뒤에서부터 n번째 위치를 의미함 (주의: 마지막 위치가 1에서 시작함) .. n1..n2 범위 연산자로서 n1은 포함하고 n2는 포함하지 않는 범위를 지정한다. 수학적으로 표현하면 [n1, n2)와 같다. n1, n2은 생략 가능하다.
-
- IDisposable 인터페이스를 구현한 타입의 Dispose 메서드를 finally 구문에서 호출하도록 변경해 주는 using 문은 사용하기 편리하지만 블록이 추가되어 들여쓰기 구간이 발생한다.
- C# 8.0에서는 들여쓰기를 생략할 수 있도록 개선된 문법을 제공한다.
- 이 경우 using 블록은 using에 사용된 변수 선언을 기준으로 가장 가까운 바깥 블록까지 적용된다.
- "스택에만 생성할 수 있는 값 타입 지원 - ref struct"에서 ref struct 특성으로 인해 인터페이스를 구현할 수 없고 이로 인해 using 문에 사용할 수 없었다. (C# 7.2)
- 그래서 Dispose가 필요한 작업인데도 IDisposable을 구현할 수 없어 using 문에 사용하는 것이 불가능하다.
- C# 8.0에서는 특별히 ref struct 타입에 한해서만 public void Dispose() 메서드를 포함한 경우 using 문에서 사용할 수 있도록 허용한다.
- "로컬 함수"는 static 함수를 구현할 수 없었지만 C# 8.0에서 제약이 풀렸다.
- 패턴 매칭에 대한 지원이 추가되었다.
- switch 식
- 프로퍼티 패턴
- 튜플 패턴
- 위치 패턴
- 재귀 패턴
- C# 8.0에서는 인터페이스의 메서드에 구현 코드를 추가할 수 있게 되었다.
- 유의할 점: 상속 받은 클래스에서 기본 인터페이스 메서드를 구현하지 않았다면, 그 메서드는 반드시 인터페이스로 형 변환해서 호출해야만 한다.
- 다중 상속에서 발생하는 다이아몬드 문제에 대한 모호한 호출 문제를 해결할 수 있다.
변수 ??= 기본값: 참조 객체인 변수의 값이 null이면 기본값을 변수에 대입한다.- 이것은 ??와 = 연산자의 복합 연산자에 불과하다. (
txt = txt ?? "test";이것은txt ??= "test";와 같다)
- 이것은 ??와 = 연산자의 복합 연산자에 불과하다. (
- C# 6.0에 추가된 $ 접두사는 문자열 내에 식을 사용하도록 허용한다.
- 여기서 $@는 보간식에서 여러 행을 표현하고 싶을 때 쓰는 기호이다.
- 여기서 @ 접두사와 혼용하는 경우 반드시 $@ 순서로 작성해야 한다.
- C# 8.0부터는 제약이 풀려서 순서에 상관없이 정상적으로 컴파일된다.
- "스택을 이용한 값 형식 배열: stackalloc"에서 본 stackalloc 키워드는 지역 변수를 초기화하는 구문에만 한정되어 있었다.
- C# 7.2의
Span<T>타입이 나오면서 좀 더 자유롭게 사용할 수 있어야 한다는 요구사항이 반영되어 stackalloc은 식(expression)이 되었다.
- C# 7.2의
- 제네릭 구조체의 경우 내부 필드가 참조 객체를 포함하는지 여부와 상관없이 포인터 관련 연산을 지원하지 않았다.
- C# 8.0부터는 제네릭 구조체에도 포인터 관련 연산이 가능해졌다.
- 비관리 메모리로 할당 받은 경우 GC에 부하를 주지 않는다는 장점이 있다.
- "읽기 전용(readonly) 구조체"에서는 방어 복사본 문제로 인한 성능 손실을 없애기 위해 구조체 내의 모든 필드를 readonly로 바꾸도록 강제했다.
- 그러나 원하는 메서드에 대해서만 상태 변경을 하지 않는다고 보장하면 구조체 자체의 정의를 readonly struct로 만들지 않아도 된다.
- C# 8.0에서는 읽기 전용 메서드를 추가하여 구조체 전체를 읽기 전용으로 강제하지 않아도 되게끔 조건을 완화하였다.
-
C# 9.0에서는 값을 담는 용도의 구조체에 대해 record 키워드를 넣으면 기본 메서드 구현을 모두 제공하는 클래스로 바꿔준다.
public record Point-->public class Point로 바꿔주며 Equals, GetHashCode, ==, != 등의 메서드가 자동으로 추가된다.
-
새로 추가된 문법 2가지가 있다. (개인적으로 캡슐화, 정보 은닉의 특성에 위배되는 것이 아닌가 싶다)
- init 설정자 추가
- readonly struct와 달리 클래스는 불변 타입을 개발자가 직접 구현해야 했다. (해당 필드를 외부에서 접근할 수 없으므로 생성자 구현이 필요함)
- init 설정자를 사용하면 private 멤버라도 외부에서 초기화 구문으로 초기화를 할 수 있다.
- 덕분에 개발자는 필드 초기화를 위한 생성자, 튜플과의 연동을 위한 Deconstruct 메서드도 만들 필요가 없다.
- with 초기화 구문 추가
- with 키워드를 이용하면 private 멤버의 값을 변경하는 별도의 메서드가 없어도 자유롭게 불변 개체의 값을 변경하는 인스턴스를 만들 수 있다.
- init 설정자 추가
- 변수의 타입에 따라 new 연산자의 대상 타입을 생략하도록 허용한다.
- var 키워드처럼 new 키워드 다음에 타입을 지정하지 않아도 자동으로 타입을 결정한다.
- 조건 연산자(?:)의 식 평가에도 타입 추론 기능이 관여되어 개선이 이루어졌다.
- 형 변환 없이도 컴파일이 가능해졌다.
-
"로컬 함수"에도 애트리뷰트를 부여할 수 있게 되었다.
-
또한 로컬 함수로 extern 유형을 정의할 수 있게 되었다.
-
정적 익명 함수
- 익명 메서드 및 람다 메서드를 정적으로 정의하는 것이 가능해졌다.
-
익명 함수의 파라미터 무시
- 원래 밑줄만으로 구성한 변수명이나 메서드 이름을 짓는 것이 가능하다.
- 단, 메서드의 파라미터가 2개 이상 밑줄을 사용하면 중복 이름으로 평가되어 컴파일 오류가 발생했으나 C# 9.0부터는 밑줄을 식별자가 아닌 무시 구문으로 다루게 되어 정상적으로 컴파일이 된다.
-
C# 9.0 이상에서 가능한 메서드별 구현 차이점
메서드 종류 파라미터 무시 static 애트리뷰트 일반 메서드 X O O 익명 메서드(C# 2.0) O (C# 9.0) O (C# 9.0) X 람다 메서드(C# 3.0) O (C# 9.0) O (C# 9.0) O (C# 10) 로컬 함수(C# 7.0) X O (C# 8.0) O (C# 9.0)
- C# 9.0부터 도입된 최상위 문의 기능으로 코드를 단 한 줄로 요약하는 것이 가능해졌다.
- 예:
System.Console.WriteLine("Hello World!"); - 별도의 타입 및 메서드를 정의하지 않고도 컴파일이 된다. (C# 컴파일러가 자동으로 임시 타입과 Main 메서드를 생성함)
- 예:
- 패턴 일치 문법에 다음 3가지가 추가되었다.
- 패턴에 타입명만 지정 가능
- 프리미티브 타입에 한해 상수 값에 대한
<,<=,>,>=관계 연산 가능 - 논리 연산자(not, and, or)를 이용해 패턴 조합 가능
-
C# 언어 수준에서 제공하지 않은 닷넷 런타임 기능으로 이라는 특별한 타입이 있다.
- CLR이 닷넷 모듈을 로드한 후 거기에 정의된 정적 생성자를 호출하면, 사용자 코드에서 호출하지 않아도 자동으로 실행되는 코드를 정의할 수 있다.
- C# 9.0부터는 ModuleInitializer라는 애트리뷰트를 정적 메서드에 부여하면 C# 컴파일러가 해당 메서드를 타입의 정적 생성자에서 호출하는 코드를 넣어 컴파일한다.
-
ModuleInitializer 애트리뷰트의 대상이 될 메서드에는 다음과 같은 제약 사항이 있다.
- 반드시 static 메서드이어야 한다.
- 리턴 타입은 void, 파라미터는 없어야 한다.
- 제네릭 타입은 허용하지 않는다.
- 타입에서 호출이 가능해야 하므로 internal 또는 public 접근 제한자만 허용한다.
-
유의사항: 어떤 순서로 호출될지 제어할 수 없으므로 실행 순서에 의존하는 코드를 작성해서는 안 된다.
- C# 언어에서는 메서드를 이름, 파라미터 타입으로 구분하므로 리턴 타입이 달라도 중복 메서드로 인지하여 컴파일 오류가 발생한다.
- 그러나 C# 9.0부터는 리턴 타입이 상속 관계의 하위 타입인 경우에 한해 사용할 수 있게 되었다.
- 외부 개발자가 해당 타입의 소스코드를 변경하지 않고 단지 GetEnumerator 확장 메서드를 제공하면 foreach에 사용할 수 있도록 만들 수 있다.
-
C# 3.0의 부분 메서드는 다음 3가지 주요 제약을 가지고 있었는데 C# 9.0에서 모두 풀렸다.
- 리턴 타입 허용
- out 파라미터 허용
- (암묵적으로 private이지만) 명시적으로 private을 포함한 접근 제한자 허용
-
대신 구현부를 더 이상 생략할 수 없는 조건이 추가되었다.
- C# 컴파일러는 기본적으로 모든 로컬 변수의 공간을 사용 여부에 상관없이 0 값으로 초기화하도록 내부 코드를 생성한다.
- 하지만 0으로 초기화하는 것이 꼭 필요한 작업은 아니다.
- C# 컴파일러는 개발자가 명시적으로 초기화하지 않은 변수를 사용할 때 컴파일 오류를 발생시킨다.
- 어차피 개발자가 변수에 값을 할당해야 하기 때문에 변수를 초기화하는 작업을 2번 하지 않는 게 낫다.
- 로컬 변수의 초기화를 생략하고 싶은 메서드에 SkipLocalsInitAttribute 애트리뷰트를 unsafe와 함께 적용하면 성능을 높일 수 있다.
- 32비트 환경에서는 4바이트로, 64비트 환경에서는 8바이트로 동작하는 nint, nuint 정수 타입이 새로 추가되었다.
-
C# 8.0까지는 함수 포인터의 역할을 델리게이트가 담당했다.
- 델리게이트를 경유하기 때문에 성능이 떨어진다.
- C# 9.0부터 대상 메서드를 바로 호출해 성능을 높일 수 있는 새로운 함수 포인터 구문을 unsafe 문맥으로 제공한다.
- 델리게이트를 함수 포인터로 바꾸면 성능은 개선되지만 안정성을 포기해야 한다.
-
함수 포인터는 비관리 모듈과의 연동에도 큰 변화를 가져온다.
- 비관리 함수 포인터 지원
- 비관리 함수를 통한 콜백 지원
- 기본적으로 nullable 형식임을 의미하는 물음표 표시는 참조 형식이 아닌 값 형식에만 허용된다.
- C# 8.0의 nullable 문맥은 참조 형식에 대해서도 ? 표시를 허용하게 만들었다.
- 그러나 범용 형식 파라미터(T)에 ?를 사용하면 해당 타입이 값 형식일 수도, 참조 형식일 수도 있으므로 컴파일 오류가 발생했다.
- 그래서 반드시 값 형식인지, 참조 형식인지 명시하는 제약이 필요하다.
- C# 9.0부터는 제약을 지정하지 않으면 "where T class"를 생략한 것으로 간주하기로 했다.
- 자세한 내용은 생략한다.
- C# 10에서 record 문법이 2가지 개선되었다.
- 레코드 구조체
- 참조 형식(class) 유형으로만 record 타입을 정의할 수 있었으나 이제는 값 형식의 타입 생성을 지시하는 record struct가 추가되었다.
- 따라서 기존의 참조 형식 record 정의를 구분하기 위해 record class 타입도 명시적으로 추가되었다.
- class 타입의 record에 ToString 메서드의 sealed 지원
- record를 정의하면 C# 컴파일러는 자동으로 System.Object 클래스의 멤버 중 Equals, GetHashCode, ToString 메서드에 대한 기본 코드를 제공하며, 원한다면 ToString과 GetHashCode 메서드에 한해 사용자 측에서 재정의할 수 있다.
- C# 10부터는 상속 클래스에서의 재정의를 막는 sealed 키워드를 ToString 메서드에 적용하는 것이 가능해졌다. (단 record struct는 상속이 불가능하므로 사용할 수 없음)
- 레코드 구조체
-
구조체에 다음 2가지를 지원하게 되었다.
- 기본 생성자 지원
- 필드 초기화 지원
-
이 기능들은 레코드 구조체(record struct)를 지원하기 위해 추가된 것이다.
-
전역 using 지시문
- 자주 사용하는 네임스페이스들을 파일마다 선언해야 하는 번거로움 때문에 추가된 기능이다.
- C# 프로젝트에 포함되는 하나의 소스코드 파일에 global 키워들을 이용한 네임스페이스 선언을 포함하면 된다.
- C# 10부터는 프로젝트 파일(.csproj) 안에 전역 네임스페이스 선언을 자동으로 추가할 수 있는 ImplicitUsings라는 설정이 추가되었고, 이것을 enable 시키면 전역 네임스페이스 선언을 담고 있는 C# 소스코드 파일을 자동으로 생성해 프로젝트와 함께 빌드한다.
-
파일 범위 네임스페이스
- C# 소스코드에서 네임스페이스를 정의하면 그 영역을 중괄호를 이용한 블록으로 표시한다.
- C# 10부터는 중괄호를 생략하면 기본적으로 해당 파일에 정의한 전체 타입에 적용된다.
- "문자열 내에 식(expression) 포함"에서 보간식을 사용하면 string.Format으로 치환된다.
- 부간식을 사용한 경우 프로그램 실행 시점에 문자열이 결정되기 때문에 상수식이 될 수 없다.
- 그러나 C# 10부터는 보간식에 사용한 코드가 오직 상수 문자열인 경우, 컴파일 시간에 문자열을 결정할 수 있게 되었다.
- 이러한 개선과 함께 상수 문자열을 리턴하는 nameof에 대한 문자열 보간식 내에서의 사용이 자유로워졌다.
- 그러나 이 기능은 오직 상수 문자열만 허용하며 그 외의 상수(숫자형 등)는 지원하지 않는다.
- 타입이 중첩된 경우 내부 인스턴스가 가진 필드에 대한 패턴 매칭을 좀 더 간편하게 지정할 수 있는 문법이 추가됐다.
- 기존의 경우 필드에 대한 패턴 지정은 중괄호를 이용해야만 했다.
- C# 10부터는 도트(.) 연산자를 이용해 멤버를 접근하던 것과 동일한 방식으로 중괄호의 사용을 줄일 수 있다.
- 또한 null 체크까지도 가능하므로 코드의 양을 줄일 수 있게 되었다.
-
람다 사용법에 대한 3가지 개선이 이루어졌다.
- 람다 식에 애트리뷰트 허용
- 단, 애트리뷰트가 지정된 경우에는 파라미터가 하나만 있는 람다 식에서는 반드시 해당 파라미터에 괄호를 함께 사용해야 한다.
- 리턴 타입 지정 허용
- 이제 람다 식의 맅너 값을 명시적으로 지정할 수 있다.
- 단, 리턴 타입을 명시하는 경우에는 파라미터가 하나라도 반드시 괄호와 함께 지정해야 한다.
- 람다 식에 대한 var 추론 향상
- C# 9.0까지는 람다 식에 대해서는 var로 정의한 변수의 타입 추론을 지원하지 않았다.
- 그래도 타입 정보가 없는 타입에서는 여전히 오류가 발생한다. (타입 정보가 있어야 추론이 가능함, 적어도 인자 값의 타입이라도...)
- 람다 식에 애트리뷰트 허용
-
이로써 람다 식은 일반 메서드와 다름 없는 수준이 되었다.
-
C# 5.0에 "호출자 정보"로 다음과 같은 애트리뷰트가 제공되었다.
- CallerMemberName
- CallerFilePath
- CallerLineNumber
-
C# 10에서는 호출자 인수를 식으로 처리할 수 있는 애트리뷰트가 추가되었다.
- CallerArgumentExpression
- 이것을 사용하면, 인자로 넘겨준 표현식을 C# 컴파일러가 자동으로 문자열로 넘겨주기 때문에 단위 테스트, 실행 로그를 남기는 데 유용하다.
- C# 10에서 개선된 기능은 다음과 같다.
- 한정된 할당 분석 개선 (Improved Definite Assignment Analysis)
- AsyncMethodBuilder 재정의
- 보간된 문자열 개선
- 박싱/언박싱으로 인해 GC가 작동하여 성능이 저하했던 이전 코드가 내부적으로 GC 힙 메모리 사용을 줄이는 코드로 바뀌어서 컴파일된다.
- 분해 구문에서 기존 변수의 재사용 가능
- Deconstrcut 메서드를 사용한 분해 구문 사용시 값이 대입될 변수를 괄호 안에 모두 선언하든지, 외부에서 모두 선언해야 했는데 이제는 내/외부 변수를 섞어서 받을 수 있게 되었다.
- Source Generator V2 API
- 향상된 #line 지시문
-
"기본 인터페이스 메서드"에서 인터페이스 내에 정적 멤버까지도 정의하는 것이 가능해졌다.
- C# 11부터는 정적 멤버 중 메서드에 대해서 하위 클래스에서 구현을 강제할 수 있는 abstract 구문을 추가했다.
- 가령 Math.Sum 메서드를 구현할 때 기존에는 제네릭 메서드가 불가능했지만 C# 11에서는 가능하다.
- 이외에도 닷넷 7부터 숫자 형식의 타입은 다음과 같은 인터페이스를 구현하고 있으므로 관련 연산이 필요할 때 제네릭 제약 조건을 활용하면 공통 코드를 쉽게 만들 수 있다.
-
숫자형 타입에 추가된 정적 추상 메서드를 담은 인터페이스
| 인터페이스 이름 | 재정의 가능한 기능 |
|---|---|
| IParseable | Parse(string, IFormatProvider) |
| ISpanParseable | Parse(ReadOnlySpan<char> IFormatProvider) |
| IAdditionOperators | x + y |
| IBitwiseOperators | `x & y, x |
| IComparisonOperators | x < y, x > y, x < y, x >= y |
| IDecrementOperators | --x, x-- |
| IDevisionOperators | x / y |
| IEqualityOperators | x == y, x != y |
| IIncrementOperators | ++x, x++ |
| IModulusOperators | x % y |
| IMultiplyOperators | x * y |
| IShiftOperators | x << y, x >> y |
| ISubtractionOperators | x - y |
| IUnaryNegationOperators | -x |
| IUnaryPlusOperators | +x |
| IAdditiveIdentity | (x + T.AdditiveIdentity) == x |
| IMinMaxValue | T.MinValue, T.MaxValue |
| IMultiplicativeIdentity | (x * T.MultiplicativeIdentity) == x |
| IBinaryFloatingPoint | 2진 부동 소수점 형식의 공통 멤버 |
| IBinaryInteger | 2진 정수형의 공통 멤버 |
| IBinaryNumber | 2진 숫자 형식의 공통 멤버 |
| IFloatingPoint | 부동 소수점 형식의 공통 멤버 |
| INumber | 숫자 형식의 공통 멤버 |
| ISignedNumber | 부호 있는 숫자 형식의 공통 멤버 |
| IUnsignedNumber | 부호 없는 숫자 형식의 공통 멤버 |
- C# 1.0부터 지원하던 애트리뷰트가 C# 11부터 제네릭을 허용하게 되었다.
- IoC 컨테이너, TDD 프레임워크 등의 리플렉션을 활용하던 분야에서는 제네릭 특성의 도입으로 인해 이전보다 더 효율적으로 공통 코드를 적용하는 것이 가능해졌다.
-
"checked, unchecked"는 이미 만들어진 정수형 타입에 한해서만 동작했다.
-
C# 11에서 개발자는 ++, --, 음수 부호(-), 사칙연산(+, -, *, /) 연산자 오버로드 메서드를 각각 2개씩 다음 유형으로 구현할 수 있다.
- checked 키워드를 추가한 유형: C# 컴파일러가 checked 문맥인 경우 호출하는 메서드
- 기존 문법과 동일한 유형: C# 컴파일러가 unchecked 문맥인 경우 호출하는 메서드 (만약 checked 유형의 메서드를 정의하지 않으면 이 메서드가 checked 문맥에서도 호출됨)
-
shift 연산자 재정의에 대한 제약 완화
- "<<" 연산자를 재정의할 때 2번째 인자는 int만 사용할 수 있었다.
- C# 11부터는 이러한 제약이 풀려서 2번째 인자를 Int3 구조체 타입으로 재정의하는 것이 가능하다.
-
새로운 연산자 ">>>" (부호 없는 오른쪽 시프트 연산자)
- ">>" 연산자는 최상위 비트를 그대로 유지하면서 밀어낸다.
- 그래서 부호 없는 오른쪽 시프트 연산을 원할 경우 별도의 메서드를 구현해야 했다.
- 그러나 C# 11부터는 ">>>" 연산자를 사용하면 된다.
-
IntPtr/UIntPtr은 포인터 연산을 위한 용도로, nint/nuint는 플랫폼(32비트/64비트)에 따라 바뀌는 정수 타입이라는 용도로 분리해 사용했다.
-
C# 11에서 이 두 가지가 통합되었다.
- nint/nuint가 기존의 IntPtr/UIntPtr을 흡수했다.
- nint/nuint는 사칙 연산이 가능한 정수 타입이기에 닷넷 7의 BCL부터 IntPtr/UIntPtr에 대해 사칙 연산자에 해당하는 메서드 재정의가 추가되었다.
- 원시 문자열 리터럴
- 기존에는 큰따옴표 자체를 문자열에 포함하기 위해서는 이스케이프 문자를 이용해야 했다.
- C# 11부터는 큰따옴표 3개로 시작과 끝을 감싸면 내부에 있는 문자열을 그대로 표현할 수 있다.
- 만약 3개 이상의 큰따옴표를 표시하고 싶으면, 문자열을 묶는 큰따옴표의 수를 그보다 1개 이상 많이 사용하면 된다.
- 문자열 보간 개선
- 보간식 내에 개행 허용
- 중괄호 내의 보간식에 새로운 라인을 허용하게 되었다.
- 원시 문자열의 보간식에 사용할 중괄호의 이스케이프 처리 개선
- 원시 문자열 내에 보간식을 사용하기 위한 중괄호 개수를 여러 개 허용하게 되었다.
- 허용하고 싶은 중괄호 개수만큼 맨앞에 달러 기호 개수를 넣으면 된다.
- 보간식 내에 개행 허용
- UTF-8 문자열 리터럴 지원
- C# 언어는 소스코드에 사용한 문자열의 기본 인코딩을 UTF-16으로 한다.
- 반면, 다른 프로그래밍 언어에서 사용하는 각종 통신 방식의 기본 인코딩 방식은 UTF-8을 채택하고 있으므로 C# 문자열을 통신으로 보낼 때 UTF-8로 변환해야 한다. (
byte[] buffer = Encoding.UTF8.GetBytes(text);) - C# 11부터는 UTF-8 문자열 리터럴을 사용할 수 있게 u8 접미사를 지원해 해결하고 있다. (
ReadOnlySpan<byte> buffer = "Hello"u8;) - 단, UTF-8 문자열 맅럴은 컴파일 시에 값이 정해지는 상수가 아니라는 점을 유의할 것
- 지금까지 추가된 패턴 매칭은 버전별로 다음과 같으며, C# 11에는 목록 패턴 매칭이 추가되었다.
- C# 7: 기본 패턴 매칭
- C# 8: switch 식, 프로퍼티 패턴, 튜플 패턴, 위치 패턴
- C# 9: 타입 패턴, 관계 연산자 지원, 논리 연산자 지원, 괄호 연산자 지원
- C# 10: 프로퍼티 패턴 개선
- C# 11: 목록 패턴 (.. 슬라이스 패턴, "_" 문자인 무시(discard) 패턴 추가)
-
ref 필드는 GC 힙에 할당되는 참조형 타입 내에 정의하는 것이 불가능하다.
- 가비지 컬렉션의 부하가 심해지므로 막은 것이다.
- 오직 ref struct에 한해 ref 필드를 가질 수 있도록 제한을 풀었다.
-
scoped 키워드가 추가되었다.
- ref로 받은 파라미터를 절대로 다른 ref 필드에 보관하지 않겠다고 명시하는 효과를 갖는다.
- 이렇게 해야 ref 필드를 허용함에 따라 발생할 수 있는 문제를 차단할 수 있다. (메서드 내에서만 유효한 인스턴스가 메서드가 리턴된 이후에도 접근 가능하면 안 됨)
- 중첩 유형을 제외하고 타입 자체에 허용되는 접근 제한자는 internal, public 뿐이다.
- 그 외의 접근 제한자를 타입 정의에 사용하면 CS1527 컴파일 오류가 발생한다.
- C# 11에서는 타입에만 적용할 수 있는 새로운 접근 제한자로 file 키워드가 추가되었다.
- 파일 내부에서만 유효한 탓에 몇 가지 제약이 따른다.
- "nameof 연산자"를 이용하면 코드에서 사용한 식별자에 한해 하드 코딩을 없앨 수 있다.
- 단, 메서드의 파라미터는 내부 코드에서만 nameof를 적용할 수 있었기 때문에 메서드 또는 다른 파라미터의 문맥으로부터 접근하는 것은 컴파일 오류가 발생했다.
- C# 11부터는 이 사례들에 대해 모두 허용하게 되었다.
-
타입에 생성자를 정의하면 그 타입의 인스턴스를 만들 때 어떤 값을 넘겨야 하는지 강제할 수 있는 효과를 얻을 수 있다.
- 하지만 값을 단순히 저장하는 것이 목적이라면 생성자를 매번 정의하는 것이 번거롭다.
- C# 11부터는 속성 및 필드에 적용할 수 있는 required 키워드를 통해 개체 초기화 구문에서 반드시 값을 제공하도록 강제하는 방법을 추가했다.
-
이와 함께 생성자에만 적용할 수 있는 SetsRequiredMembers 애트리뷰트가 함께 추가되었다.
- 이 애트리뷰트가 적용된 생성자는 required 필드를 무시할 수 있다.
- 단, SetsRequiredMembers가 적용된 생성자를 정의한 클래스를 상속 받는 경우, 해당 생성자와 연계하는 자식 클래스의 생성자를 정의한다면 반드시 SetsRequiredMembers 애트리뷰트를 함께 적용해야만 한다.
-
required 제약 사항
- required 멤버는 class, struct, record에서만 허용되고, interface에는 정의할 수 없다.
- fixed, ref readonly, ref, const, static 및 인덱서 구문에도 required를 조합할 수 없다.
-
C# 10까지는 구조체의 경우 필드 초기화와 생성자 내에서의 코드를 합쳐서 모든 필드가 초기화되는 것을 강제했다.
-
C# 11부터는 struct 역시 class와 마찬가지로 초기화되지 않은 필드에 대해 자동으로 기본값을 설정하도록 바뀌었다.
- 정적 메서드를 델리게이트 타입의 로컬 변수에 할당한 후 호출하는 코드의 경우, 호출할 때마다 GC 힙을 사용하는 코드가 실행된다.
- GC 힙 사용을 줄이려면 변수를 재활용하는 처리를 개발자가 직접 만들어야 한다.
- C# 11부터는 컴파일러가 자동으로 처리해주므로 기존 코드에 대해 GC 힙 사용을 줄여 성능이 개선되었다.
-
C# 3.0에 처음 선보인 람다 문법은 이후 지속적인 문법 개선이 있어왔다.
- C# 6.0: 람다 표현식을 이용해 일반 메서드, 속성과 인덱서의 get 접근자에 대한 정의
- C# 7.0: C# 6.0에 구현된 범위를 확장 - 생성자, 종료자, 이벤트의 add/remove, 속성과 인덱서의 set 접근자
- C# 9.0: 파라미터 무시, static 지원
- C# 10: 애트리뷰트 허용, 리턴 타입 지정, var 추론
-
C# 12부터는 람다 문법에 파라미터 기본값까지 설정할 수 있게 되었다.
- 단, 기본값을 지정한 경우에는 반드시 그에 맞는 delegate 타입을 사용해야 한다. (그렇지 않으면 컴파일 경고/오류가 발생한다)
- 생성자 메서드는 대부분 내부 상태를 설정하는 단순한 용도로 쓰는 경우가 많다.
- C# 9.0의 record 문법을 활용하면 단 1줄의 코드로 줄일 수 있다.
- 편리한 record의 생성자 메서드 정의 방식을 C# 12부터 일반 타입에 도입한 것이 기본 생성자이다.
- record와 다른 점은 class 정의 시 지정한 파라미터는 생성자 메서드의 파라미터로만 사용할 뿐 그에 대응하는 내부 멤버 필드 및 초기화 코드까지 자동 생성하지는 않는다는 점이다.
- 즉, 생성자 메서드 정의만 생략할 수 있다.
- 나머지 다른 초기화 코드들은 기본 생성자의 파라미터를 활용해 직접 구현해야 한다.
- 유의사항: 다른 생성자와 함께 ㅈㅇ의할 경우 반드시 기본 생성자를 경유하는 this 호출 코드를 넣어야 한다.
-
using을 이용하여 선언한 네임스페이스에 별칭을 부여할 수도 있다.
-
C# 12부터는 네임스페이스와 타입에 대해서만 별칭을 부여하던 범위를 넘어 다음 항목에 대한 별칭도 만들 수 있게 지원한다.
- 이름이 없는 타입
- 포인터 타입
- Nullable 타입
- C# 언어는 이미 고정 크기에 대한 배열을 fixed 구문을 통해 지원하고 있다.
- 다만 이 구문은 unsafe 문맥을 필요로 하며, 프로젝트 빌드 시에 AllowUnsafeBlocks 옵션을 true로 명시해야 한다.
- C# 12부터 InlineArray 애트리뷰트를 부여한 타입으로 해결하고 있다.
- 더 이상 unsafe를 필요로 하지 않는다.
- 다음의 타입들에 대해 컬렉션을 초기화하는 구문으로 기존의 중괄호와 함께 대괄호를 사용할 수 있게 되었다.
- 배열
- Span/ReadOnlySpan
- C# 3.0의 컬렉션 초기화 구문을 지원하는 타입
- IEnumerable 타입 중 C# 6.0의 Add 확장 메서드가 정의된 타입
- C# 12에서는 ref readonly 변경자가 추가되었다.
- C# 7.2에 추가된 in 변경자는 의미적으로 ref + readonly에 해당한다.
- in 변경자와 reaf readonly는 표현만 다를 뿐, 같은 구문에 불과하다.
- 다만 특정 상황에서 컴파일러가 경고를 다르게 한다. (ref readonly는 값을 직접 전달하면 경고를 발생시키지만, in 변경자는 경고를 발생시키지 않는다)
- 특정 소스코드의 메서드 호출을 다른 메서드로 가로챌 수 있는 방법을 제공한다.
- 정식 버전으로 포함된 것이 아니며 약간의 사전 설정이 필요하다.
- C# 10에서 언급했던 소스 생성기에서 사용할 의도로 도입된 것이다.
- 실험적으로 추가한 기능임을 인지시키는 Experimental 애트리뷰트가 추가되었다. (닷넷 8 BCL)
- C# 12 컴파일러는 이를 인지해 오류를 발생시키도록 연동되었다.
- 만약 사용자가 컴파일 오류를 무시하고 기능을 사용하고 싶다면 #pragma 지시자를 사용해야 한다.