옆히

시작하세요! C# 12 프로그래밍 - 2부 본문

개인공부용1/cs

시작하세요! C# 12 프로그래밍 - 2부

옆집히드라 2024. 7. 14. 02:20
시작하세요! C# 12 프로그래밍 - 10점
정성태 지음/위키북스

 

개발 시간 단축을 위한 Code Snippet 알아보기 (tistory.com)


C# 2.0

#제네릭

제네릭을 사용하면 CLR이 JIT 컴파일 시에 클래스가 타입에 따라 정의될 때마다 T에 대응되는 타입을 대체해서 확장시킴(박싱/언박싱으로 인한 성능 문제 해결)

타입에 따라 자동으로 확장되는 제네릭

 

제네릭이 클래스 수준에서 지정된 것을 '제네릭 클래스(Generic class)'라고 한다.

class 클래스_명<형식매개변수[, ......]>
{
    //형식 매개변수를 멤버의 타입 위치에 지정
}
//설명: 1개 이상의 형식 매개변수를 <>안에 지정할 수 있다. 이때 사용되는 형식 매개변수의 이름은 임의로 지정 가능하다.

public class GenericSample<Type> //형식 매개변수의 이름은 임의로 줄 수 있다.
{
    Type _item;
    
    public GenericSample(Type value)
    {
        _item = value;
    }
}

public class TwoGeneric<K,V> //2개 이상 지정하는 것도 가능하다.
{
    K _key;
    V _value;
    
    public void Set(K key, V value)
    {
        _key = key;
        _value = value;
    }
}

 

제네릭은 클래스뿐 아니라 메서드에도 직접 지정할 수 있다. 이를 가리켜 '제네릭 메서드(Generic method)'라고 하며, 형식 매개변수가 클래스 수준이 아닌 메서드 수준에서 부여되는 것이 특징이다.

class 클래스_명
{
    [접근제한자]반환타입 메서드명<형식매개변수[, ...]>([타입명][매개변수명], ...)
    {
        //지역 변수
    }
}
//메서드명 다음에 형식 매개변수를 지정할 수 있으며, 이때 지정된 형식 매개변수는
//반환 타입, 메서드의 매개변수 타입, 메서드의 지역 변수 타입에 사용할 수 있다.

class PJH
{
    public void IsHeFree<T>(T num)
    {
        Console.WriteLine($"전역까지 {num}");
    }
}

 

 

#where 예약어

class Program
{
    //T에 입력될 수 있는 타입 조건을 where 예약어를 통해 명시함
    public static T Max<T>(T item1, T item2) where T : IComparable
    {
        if(item1.CompareTo(item2) >= 0)
            return item1;
        return item2;
    }
    static void Main(string[] args)
    {
        Console.WriteLine(Program.Max(5, 6));
        Console.WriteLine(Program.Max("abc", "def"));
    }
}

 

where 예약어 다음에 형식 매개변수를 지정하고 콜론(:)을 구분자로 써서 제약 조건을 걸 수 있다. 컴파일러는 T 타입으로 지정된 item1과 item2는 당연히 IComparable 인터페이스를 상속받은 타입의 인스턴스라고 가정하게 되고 코드에서 IComparable.CompareTo 메서드를 호출하는 것을 허용함

 

where 형식매개변수 : 제약조건[, ......]
//제네릭 구문이 사용된 메서드와 클래스에 대해 모두 where 예약어를 사용해 형식 매개변수가 따라야 할
//제약 조건을 1개 이상 지정할 수 있고, 형식 매개변수의 수만큼 where 조건을 지정할 수 있다.

public class Dict<K, V> where K: ICollection, IComparable where V: IComparable {}

 

#제네릭 형식 매개변수에 대한 특별한 제약 조건

제약 조건 설명
where T: struct T 형식 매개변수는 반드시 값 형식만 가능하다.
where T: class T 형식 매개변수는 반드시 참조 형식만 가능하다.
where T: new() T 형식 매개변수의 타입에는 반드시 매개변수가 없는 공용 생성자가 포함돼 있어야 한다. 즉, 기본 생성자가 정의돼 있어야 한다.
where T: U T 형식 매개변수는 반드시 'U 형식 인수'에 해당하는 타입이거나 그것으로부터 상속받은 클래스만 가능하다.

 

//형식 매개변수 2개를 사용해 제약 조건을 설정
public class BaseClass { }
public class DerivedClass : BaseClass { }

public class Utility
{
    public static T Allocate<T, V>() where V : T, new()
    {
        return new V();
    }
}

public class Program
{
    public static void Main()
    {
        BaseClass dInst = Utility.Allocate<BaseClass, DerivedClass>();
    }
}

 

#BCL에 적용된 제네릭

하위 호환성을 지키기 위해 기존의 컬렉션 타입은 그대로 유지하고, 각각에 대응되는 제네릭 타입을 새롭게 만들어 System.Collections.Generic 네임스페이스에 추가함

.NET 1.0 컬렉션 대응되는 제네릭 버전의 컬렉션
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>

 

#??연산자(null 병합 연산자)

피연산자1 ?? 피연산자2
//참조 형식의 피연산자1이 null이 아니라면 그 값을 그대로 반환하고, null이라면 피연산자2의 값을 반환한다.

string txt = null;

if(txt == null)
{
    Console.WriteLine("(null)");
}
else
{
    Console.WriteLine (txt);
}

//위의 코드를 다음과 같이 처리 가능
string txt = null;

Console.WriteLine(txt ?? "(null)");

 

#default 예약어

변수를 초기화하지 않은 경우 값 형식은 0, 참조 형식은 null로 초기화된다. 타입을 알고 있다면 타입을 기준으로 초깃값을 결정할 수 있지만, 제네릭의 형식 매개변수로 전달된 경우에는 코드에서 미리 타입을 알 수 없기 때문에 그에 대응되는 초깃값도 결정할 수 없다. -> default 예약어 사용

default(T);
//기본 디폴트 값
//int: 0
//bool: false
//char: '\0' (null 문자)
//struct: 모든 필드가 기본값으로 초기화된 구조체
//참조형(class, interface 등)은 null

 

#yield return/break

yield return과 yield break 예약어를 이용하면 기존의 IEnumerable, IEnumerator 인터페이스를 이용해 구현했던 열거 기능을 쉽게 구현할 수 있다.

//IEnumerable 인터페이스를 이용한 자연수 표현
using System;
using System.Collections;

public class NaturalNumber : IEnumerable<int>
{
    public IEnumerator<int> GetEnumerator()
    {
        return new NaturalNumberEnumerator();
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return new NaturalNumberEnumerator();
    }
    public class NaturalNumberEnumerator : IEnumerator<int>
    {
        int _current;

        public int Current
        {
            get { return _current; }
        }

        object IEnumerator.Current 
        { 
            get { return _current; } 
        }

        public void Dispose() { }

        public bool MoveNext()
        {
            _current++;
            return true;
        }

        public void Reset()
        {
            _current = 0;
        }
    }
}

public class Program
{
    public static void Main()
    {
        NaturalNumber number = new NaturalNumber(); 

        foreach (int i in number)
        {
            Console.WriteLine(i);
        }
    }
}

//yield return을 이용한 자연수 표현
class YieldNaturalNumber
{
    public static IEnumerable<int> Next(int max)
    {
        int _start = 0;

        while (true)
        {
            _start++;
            if(max < _start)
            {
                yield break;
            }
            yield return _start;
        }
    }
}

public class Program
{
    public static void Main()
    {

        foreach (int i in YieldNaturalNumber.Next(32))
        {
            Console.WriteLine(i);
        }
    }
}

 

yield 구문은 IEnumerable/IEnumerator로 구현한 코드에 대한 syntactic sugar에 지나지 않는다.

 

#부분(partial) 클래스

partial class A
{
    int part1;
}

partial class A
{
    int part2
}

 

partial 예약어를 클래스에 적용하면 클래스의 소스코드를 2개 이상으로 나눌 수 있다. 클래스 정의가 나뉜 코드는 한 파일에 있어도 되고 다른 파일로 나누는 것도 가능하지만 반드시 같은 프로젝트에서 컴파일해야 한다. C# 컴파일러는 빌드 시에 같은 프로젝트에 있는 partial 클래스를 하나로 모아 단일 클래스로 빌드한다.

 

#nullable 형식

참조 형식에서는 '미정'이라는 상태를 null로 표현할 수 있지만 값 형식에서는 미정 옵션이 없다. Nullable<T> 타입은 일반적인 값 형식에 대해 null 표현이 가능하게 하는 역할을 한다.

Nullable<T> name == T? name

 

Nullable<T> 타입은 HasValue, Value 라는 두 가지 속성을 제공하는데, 값이 할당됐는지 여부를 HasValue 불린 값으로 반환하고, 값이 있다면 원래의 T 타입에 해당하는 값을 Value 속성으로 반환한다.

 

#익명 메서드(anonymous method)

익명 메서드란 단어 그대로 이름이 없는 메서드로서 델리게이트에 전달되는 메서드가 일회성으로만 필요할 때 편의상 사용된다.

public class Program
{
    delegate int? MyDivide(int a, int b);
    public static void Main()
    {
        //내부적으로는 익명 메서드 역시 간편 표기법에 불과하다
        MyDivide myFunc = delegate (int a, int b)
        {
            if (b == 0)
            {
                return null;
            }
            return a / b;
        }
    }
}

 

아래에 람다식과 비교해서 타입 추론을 지원 안하고, 블록문만을 지원하는 등 람다식보다 불편한 면이 있다

 

#정적 클래스

C# 2.0부터 클래스의 정의에도 static 예약어를 사용할 수 있게 됐다. 이렇게 정의된 정적 클래스(static class)는 오직 정적 멤버만을 내부에 포함할 수 있다.


C# 3.0

#var 예약어

C# 3.0 컴파일러부터는 타입 추론(type inference) 기능이 추가되면서 메서드의 지역 변수 선언을 타입에 관계없이 var 예약어로 쓸 수 있게 됐다. var 예약어를 남발하면 코드의 가독성이 낮아지므로 그다지 권장되지 않지만, 복잡한 타입의 경우 var 코드를 사용하면 코드가 간결해지는 장점이 있다.

foreach(KeyValuePair<string, List<int>> elem in dict) {}

foreach(var elem in dict) {}

#자동 구현 속성(auto-implemented properties)

class Person
{
    public string Name { get; set; }
}
//C# 컴파일러는 내부 코드 없이 get/set만 지정된 속성을 빌드 시 자동으로 아래와 같은 식으로 확장해서 컴파일 한다.

class Person
{
    private string ...[Name 자동 속성에 대응되는 고유 식별자.]...;
    public string Name;
    {
        get { return ...고유식별자...; }
        set { ...고유식별자... = value; }
    }
}

//++
class Person
{
    public string Name { get; protected set; }
}
//이렇게 정의된 속성 Name은 외부에서는 오직 읽기만 가능하고, 내부에서는 읽기/쓰기 모두 가능

 

#개체 초기화(Object initializers)

생성자를 이용해 초기화하는 방식은 내부 상태 변수의 수가 늘어남에 따라 작성해야 할 코드의 양이 많아진다는 단점이 있다. 번거로운 초기화 문제를 해결하기 위해 C#에서는 public 접근자가 명시된 멤버 변수를 new 구문에서 이름과 값을 지정하는 형태로 초기화하는 구문을 지원한다.

class Person
{
    public string _name;
    int _age;

    public Person() { }
    public Person(int age)
    {
        _age = age;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Person p1 = new Person();
        Person p2 = new Person(10) { _name = "manaka rara" }; //생성자 + 객체 초기화
        //new 구문에 중괄호를 사용해 공용 속성(property)의 이름과 값을 지정해 초기화 가능
        //또한 생성자와 함께 객체 초기화(object initailizer)를 함께 사용도 가능하다.
    }
}

 

#컬렉션 초기화

객체 초기화와 함께 컬렉션의 초깃값을 지정하는 방법도 C# 3.0에서 개선했다.

List<int> numbers = new List<int>();
numbers.Add(0); numbers.Add(1); numbers.Add(2);

List<int> numbers = new List<int> { 1, 2, 3 };
//C# 컴파일러가 대신 Add 메서드를 호출하는 코드를 넣어주기 때문에 컬렉션 초기화 구문이 적용되려면
//해당 타입이 반드시 ICollection<T> 인터페이스를 구현해야함

List<Person> list = new List<Person>
{
    new Person { Name = "Mirei", Age = 14 },
    new Person { Name = "Rara", Age = 11 },
};
//이처럼 컬렉션 초기화에 객체 초기화 구문도 함께 적용할 수 있다.

 

#익명 타입

C# 2.0에서 익명 메서드가 지원됐고, C# 3.0부터는 타입에도 이름을 지정하지 않는 방식을 지원한다.

var p = new { Count = 10, Title = "Pripara" };
//객체의 타입명이 없기 때문에 지역 변수를 선언할 때 타입명이 아닌 var 예약어를 사용함
//이와 같은 코드를 빌드하면 C# 컴파일러가 컴파일 시점에 임의의 고유 문자열을 생성해 타입명으로 사용하고
//new 중괄호 내의 속성을 가진 클래스를 생성해 어셈블리에 포함시킨다.

//익명 타입 구문으로 C# 컴파일러에 의해 생성된 클래스
internal sealed class AnonymousType0<T1, T2>
{
    private readonly T1 V1Field;
    private readonly T2 V2Field;
    
    public AnonymousType0(T1 Count, T2 Title)
    {
        V1Field = Count;
        V2Field = Title;
    }
    
    public T1 Count { get { return V1Field; } }
    public T2 Count { get { return V2Field; } }
}

class Program
{
    static void Main(string[] args)
    {
        //익명 클래스 구문은 C# 컴파일러가 생성한 타입을 사용하는 구문으로 확장됨
        var p = new AnonymousType0{ Count = 10, Title = "Pripara" };
    }
}

 

#부분 메서드

C# 2.0에서는 클래스에 대해서만 지원되던 partial 예약어가 메서드까지 확장된 것이 '부분 메서드(partial method)'다. 부분 메서드는 코드 분할은 하지 않고 단지 메서드의 선언과 구현부를 분리할 수 있게만 허용한다. (메서드의 시그니처를 정의한 파일과 실행될 코드가 담긴 구현부를 별도의 파일로 분리할 수 있다.)

 

#부분 메서드의 제약 사항(C# 9.0에서 제약 사항을 없앤 부분 메서드를 지원함)

  • 부분 메서드는 반환값을 가질 수 없다
  • 부분 메서드에 ref 매개변수는 사용할 수 있지만 out 매개변수는 사용할 수 없다.
  • 부분 메서드에는 private 접근자만 허용된다.

 

#확장 메서드(extension method)

기존 클래스를 확장하는 방법으로 상속이 많이 쓰이는데 이 방법은 1)sealed로 봉인된 클래스는 확장할 수 없다. 2)클래스를 상속받아 확장하면 기존 소스코드를 새롭게 상속받은 클래스명으로 바꿔야 한다.와 같은 상황에서 좋은 선택이 아니다. -> 확장 메서드 사용

//확장 메서드는 static 클래스에 정의돼야 함
static class ExtentionMethodSample
{
    //확장 메서드는 반드시 static이어야 하고,
    //확장하려는 타입의 매개변수를 this 예약어와 함께 명시
    public static int GetWordCount(this string txt)
    {
        return txt.Split(' ').Length;
    }
}

class Program
{
    static void Main(string[] args)
    {
        string text = "Hello, World!";

        //마치 string 타입의 인스턴스 메서드를 호출하듯이 확장 메서드를 사용
        Console.WriteLine("Count: " + text.GetWordCount());
        //->Console.WriteLine("Count: " + ExtensionMethodSample.GetWordCount(text);와 같이 빌드됨
    }
}

 

static 메서드 호출을 인스턴스 메서드를 호출하듯이 문법적인 지원을 해주는 것이 확장 메서드의 실체다. 따라서 클래스 상속에서는 가능했던 부모 클래스의 protected 멤버 호출이나 메서드 재정의(override)가 불가능하다. 확장 클래스를 넴임 스페이스하에 정의한다면 확장 메서드를 사용하는 소스코드에서는 반드시 해당 네임스페이스를 using문으로 선언해야 한다. 

 

visual studio의 인텔리센스는 확장 메서드를 인식해 목록에 포함시켜줌

 

#컬렉션과 람다 메서드

//List<T>에 정의된 ForEach
public void ForEach(Action<T> action);

//Array에 정의된 ForEach
public static void ForEach<T>(T[] array, Action<T> action);

//FindAll 메서드
public List<T> FindAll(Predicate<T> match);

//기존의 컬렉션 크기만을 단순하게 반환하던 Count 속성 -> Enumerable 타입 확장 메서드
public static int Count<TSource>(this IEnumerable<TSource> source);
public static int Count<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);

//Enumberable 타입에 추가된 Where 확장 메서드(FindAll의 lazy evaluation 버전)
public static IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);

//IEnumerable<T> 타입에 정의된 Select 확장 메서드(ConvertAll의 lazy evaluation 버전) 
//예시
IEnumerable<double> doubleList = list.Select((elem) => (double)elem);

var itemList = list.Select(
	(elem) => new { TypeNo = elem, CreatedDate = DateTime.Now.Ticks } );
Array.ForEach(itemList.ToArray(), (elem) => { Console.WriteLine(elem.TypeNo); });

 

#람다 식(Lamda expression)

  1. 코드로서의 람다 식: 익명 메서드의 간편 표기 용도로 사용됨
  2. 데이터로서의 람다 식: 람다 식 자체가 데이터가 되어 구문 분석의 대상이 된다. 이 람다 식은 별도로 컴파일할 수 있으며, 그렇게 되면 메서드로도 실행할 수 있다.
class Program
{
    delegate int? MyDivide(int x, int y);
    static void Main(string[] args)
    {
        //익명 메서드를 람다 구문의 메서드로 대체
        MyDivide myFunc = (a, b) => { if (b == 0) return null; return a / b; };
        
        //내부 코드가 expression으로 평가 가능하면 약식 표현 가능
        MyDivide _myFunc = (a, b) => a / b;
    }
}

 

람다 메서드는 delegate에 대응된다. 람다 메서드는 기존 메서드와는 달리 일회성으로 간단한 코드를 만들 때 사용되는데 이럴 때 델리게이트를 일일이 정의해야 하는 불편함을 덜기 위해 자주 사용되는 delegate 형식을 제네릭의 도움으로 일반화해서 BCL에 Action과 Func로 포함시켰다.

 

public delegate void Action<T>(T obj);
-> 반환값이 없는 델리게이트로서 T 형식 매개변수는 입력될 인자 1개의 타입을 지정
public delegate TResult Func<TResult>();
-> 반환값이 있는 델리게이트로서 TResult 형식 매개변수는 반환될 타입을 지정

이를 이용하면 별도로 delegate를 정의할 필요 없이 Action<T>, Func<T>를 사용하면 다음과 같이 간단하게 람다 메서드를 사용할 수 있다.

 

class Program
{
    static void Main(string[] args)
    {
        Action<string> logOut =
            (txt) =>
            {
                Console.WriteLine(DateTime.Now + ": " + txt);
            };
        logOut("This is my world!");

        Func<double> pi = () => 3.141592;

        Console.WriteLine(pi());
    }
}

 

람다 메서드의 구현은 중괄호를 가질 수 있는 문(statement)과 그렇지 않는 식(expression)으로 구현되고 후자의 경우 '람다 식'이라고 부른다. 람다식의 또다른 장점은 그것을 CPU에 의해 실행되는 코드가 아닌, 그 자체로 '식을 표현한 데이터'로도 사용할 수 있다는 점이다. 이처럼 데이터 구조로 표현된 것을 '식 트리(expression tree)'라고 한다.

 

식 트리로 담긴 람다 식은 익명 메서드의 대체물이 아니기 때문에 델리게이트 타입으로 전달되는 것이 아니라 식에 대한 구문 분석을 할 수 있는 Systme.Linq.Expressions.Expression 타입의 인스턴스가 된다. 즉, 람다 식이 코드가 아니라 Expression 객체의 인스턴스 데이터의 역할을 하는 것이다.

 

Expression<Func<int, int, int>> exp = (a, b) => a + b;

람다 식의 트리 표현

 

Expression<T> 타입의 형식 매개변수는 람다 식이 표현하는 델리게이트 타입이 되고, exp 변수는 코드를 담지 않고 람다 식을 데이터로서 담고 있다. 

 

System.Linq.Expressions 네임스페이스에 정의된 타입 및 대응되는 팩터리 메서드를 이용하면 일반적인 메서드 내부의 C# 코드를 Expression의 조합만으로도 프로그램 실행 시점에 만들어 내는 것이 가능하다.

자세한 내용 pp.619-623 참고

 

#LINQ

프로그래밍 작업 가운데 '데이터 열거'는 꽤나 빈번하게 나타나는 작업 유형 중 하나다. C# 및 VB.NET 컴파일러는 이처럼 자주 사용되는 정보의 선택/열거 작업을 일관된 방법으로 다루기 위해 기존 문법을 확장시켰는데, 이를 LINQ(Language Integrated Query)라고 한다. 이름에서 의미하듯 LINQ 구문은 기존 언어 체계에 쿼리가 통합됐다는 것인데, 실제로 그 쿼리 문법은 SQL 쿼리의 SELECT 구문과 유사하다.

 

엄밀히 따지면 LINQ 쿼리도 '간편 표기법'임. LINQ의 select 예약어는 사실상 Select 확장 메서드를 호출하는 또 다른 문법에 지나지 않고, 그런 의미에서 아래 세 가지 코드는 완전히 동일한 역할을 한다

//LINQ 표현
from person in people
select person;

//확장 메서드 표현
people.Select( (elem) => elem );

//일반 메서드 표현
IEnumerable<Person> SelectFunc(List<Person> people)
{
    foreach (var item in people)
    {
        yield return item;
    }
}

 

#where, orderby, group by, join

SELECT * FROM people GROUP BY ......//group by를 사용하면 select 못함

내부 조인(Inner Join) vs 외부 조인(Outer Join) -> 레코드를 누락시키지 않고 포함

 

#LINQ 쿼리와 확장 메서드의 관계

LINQ IEnumerable<T> 확장 메서드
Select Select
Where Where
orderby [ascending] OrderBy
orderby [descending] OrderByDescending
group ... by GroupBy
join ... in ... on ... equals Join
join ... in ... on ... equals ... into GroupJoin

 

 

#표준 쿼리 연산자(standard query operators)

LINQ 쿼리의 대상은 IEnumerable<T> 타입이거나 그것을 상속한 객체여야 한다. 그와 같은 객체에 LINQ 쿼리를 사용하면 C# 컴파일러는 내부적으로 IEnumerable<T> 확장 메서드로 변경해 소스 코드를 빌드한다. 이 때문에 IEnumerable<T>에 정의된 확장 메서드는 표준 쿼리 연산자(standard query operators)라고 한다. pp.634-637

종류  메소드 이름  설명  C# 쿼리식 문법
정렬  OrderBy  오름차순으로 값을 정렬  orderby
 OrderBy
       Descending
 내림차순으로 값을 정렬  orderby ... descending
 ThenBy  오름차순으로 2차 정렬 수행  orderby ..., ... 
 ThenBy
     Descending
 내림차순으로 2차 정렬 수행  orderby ...,
  ... descending
 Reverse  컬렉션 요소의 순서를 거꾸로 뒤집는다.  
집합  Distinct  중복 값을 제거한다.  
 Except  두 컬렉션 사이의 차집합을 반환한다.
 임의의 한 컬렉션(a,b,c,e)에 존재하는데 
다른 컬렉션(a,d,f)에는  존재하지 않는 요소들(b,e)을 반환한다.
 
 Intersect  두 컬렉션 사이의 교집합을 반환한다.
 양쪽 컬렉션 양쪽에 존재하는 요소들만 반환한다.
 
 Union  두 컬렉션 사이의 합집합을 반환한다.
 한쪽 컬렉션이 a,b,c,d요소를 갖고 있고, 
다른 컬렉션이 a,b,c,d,e  요소를 갖고 있다면 이 두 컬렉션 사이의 합집합은 a,b,c,d,e 이다.
 
필터링  OfType  메소드의 형식 매개 변수로 형식 변환이 가능한 값들만 추출한다.  
 Where  필터링할 조건을 평가하는 함수를 통과하는 값들만 추출한다.  where
수량 연산  All  모든 요소가 임의의 조건을 모두 만족시키는지를 평가한다. 
 결과는 true 이거나 false 둘 중 하나이다.
 
 Any  모든 요소 중 단 하나의 요소라도 임의의 조건을 만족시키는지 
 평가한다. 결과는 true이거나 false 둘 중 하나이다.
 
 Contains  명시적 요소가 포함되어 있는지 평가한다.
 결과는 true이거나 false 둘 중 하나이다.
 
데이터 추출  Select  값을 추출하여 시퀀스를 만든다.  select
 SelectMany  여러 개의 데이터 원본으로부터 값을 추출하여 하나의 시퀀스를 만든다. 여러개의 from 절을 사용한다.  
데이터 분할  Skip  시퀀스에서 지정한 위치까지 요소들을 건너뛴다.  
 SkipWhile  입력된 조건 함수를 만족시키는 요소들을 건너뛴다.  
 Take  시퀀스에서 지정한 요소까지 요소들을 취한다.  
 TakeWhile  입력된 조건 함수를 만족시키는 요소들을 취한다.  
데이터 결합  Join  공통 특성을 가진 서로 다른 두 개의 데이터 소스의 객체를 
연결한다. 공통 특성을 키(Key)로 삼아, 
키가 일치하는 두 객체의 쌍을 추출한다.
 Join ... in ... on ... equals ...
 GroupJoin  기본적으로 Join 연산자와 같은 일을 하되, 
조인 결과를 그룹으로 만들어 넣는다.
 Join ... in ... on ... equals ... into ...
데이터 그룹화  GroupBy  공통된 특성을 공유하는 요소들을 각 그룹으로 묶는다. 
각 그룹은 IGrouping<TKey, TElement>객체로 표현된다.
 group ... by 또는 group ... by ... into
 ToLookup  키(Key) 선택 함수를 이용하여 골라낸 요소들을 
Lookup<TKey, TElement> 형식의 객체에 삽입한다.( 이 형식은 하나의 키에 여러 개의 객체를 대응시킬 때 사용하는 컬렉션이다 )
 
생성  DefaultIfEmpty  빈 컬렉션을 기본값이 할당된 싱글턴 컬렉션으로 바꾼다.   
 Empty  비어 있는 컬렉션을 반환한다.  
 Range  일정 범위의 숫자 시퀀스를 담고 있는 컬렉션을 생성한다.  
 Repeat  같은 값이 반복되는 컬렉션을 생성한다.  
동등 여부 평가  SequenceEqual  두 시퀀스가 서로 일치하는지를 평가한다.  
요소 접근  ElementAt  컬렉션으로부터 임의의 인덱스에 존재하는 요소를 반환한다.  
 ElementAt
         OrDefault
 컬렉션으로부터 임의의 인덱스에 존재하는 요소를 반환하되, 
인덱스가 컬렉션의 범위를 벗어날 때 기본값을 반환한다.
 
 First  컬렉션의 첫 번째 요소를 반환한다. 조건식이 매개 변수로 
입력되는 경우 이 조건을 만족시키는 첫 번째 요소를 반환한다.
 
 FirstOrDefault  First 연산자와 같은 기능을 하되, 반환할 값이 없는 경우 
기본값을 반환한다.
 
 Last  컬렉션의 마지막 요소를 반환한다. 조건식이 매개 변수로 
입력되는 경우 이 조건을 만족시키는 마지막 요소를 반환한다.
 
 LastOrDefault  Last 연산자와 같은 기능을 하되, 반환할 값이 없는 경우 
기본값을 반환한다.
 
 Single  컬렉션의 유일한 요소를 반환한다. 조건식이 매개 변수로 
입력되는 경우 이 조건을 만족시키는 유일한 요소를 반환한다.
 
 SingleOrDefault  Single 연산자와 같은 기능을 하되, 반환할 값이 없거나 
유일한 값이 아닌 경우 주어진 기본값을 반환한다.
 
형식 변환  AsEnumerable  매개 변수를 IEnumerable<T>로 형식 변환하여 반환한다.  
 AsQueryable  (일반화) IEnumerable 객체를 
(일반화)IQueryable 형식으로 반환한다.
 
 Cast  컬렉션의 요소들을 특정 형식으로 변환한다.  범위 변수를 선언할 때 명시적으로 형식을 지정하면 된다.
 OfType  특정 형식으로 형식 변환할 수 있는 값만을 걸러낸다.  
 ToArray  컬렉션을 배열로 변환한다. 이 메소드는 강제로 쿼리를 실행한다.  
 TODictinoary  키 선택 함수에 근거해서 컬렉션의 요소를 
Dictionary<TKey, TValue>에 삽입한다. 
이 메소드는 강제로 쿼리를 실행한다.
 
 ToList  컬렉션을 List<T> 형식으로 변환한다. 
이 메소드는 강제로 쿼리를 실행한다.
 
 ToLookup  키 선택 함수에 근거해서 컬렉션의 요소를 
Lookup<TKey, TElement>에 삽입한다. 
이 메소드는 강제로 쿼리를 실행한다.
 
연결  Concat  두 시퀀스를 하나의 시퀀스로 연결한다. Console
집계  Aggregate  컬렉션의 각 값에 대해 사용자가 정의한 집계 연산을 수행한다.  
 Average  컬렉션의 각 값에 대한 평균을 계산한다  
 Count  컬렉션에서 조건에 부합하는 요소의 개수를 센다  
 LongCount  Count와 동일한 기능을 하지만, 매우 큰 컬렉션을 대상으로 한다는 점이 다르다.  
 Max  컬렉션에서 가장 큰 값을 반환한다  
 Min  컬렉션에서 가장 작은 값을 반환한다  
 Sum  컬렉션 내의 값의 합을 계산한다.  

 

표준 쿼리 연산자 가운데 IEnumerable<T>, IOrderedEnumerable<TElement>를 반환하는 메서드를 제외한 다른 모든 것들은 LINQ 식이 평가되면서 곧바로 실행된다. LINQ 쿼리라고 해서 모두 지연된 연산(lazy evaluation) 또는 지연 실행(deferred excution) 방식으로 동작하는 것은 아니다.

 

#일관된 데이터 조회

LINQ 기술이 의미 있는 이유는 갖가지 다양한 데이터 원본에 대한 접근법을 단일화했다는 것이다. 기존의 데이터 접근 방식에서는 데이터 원본마다 제공되는 별도의 API를 익혀서 사용해야 하는 불편함이 있었지만, LINQ의 도입으로 해당 데이터 원본마다 'LINQ 제공자'만 구현돼 있다면 데이터를 조회할 때 LINQ 쿼리를 일관되게 사용할 수 있다.

 

특정 자료형에 대해 LINQ를 사용할 수 있게 별도로 타입을 정의해둔 것을 LINQ 제공자(provider)라고 하고 지금까지 배운 것은 LINQ to Objects라는 이름으로 구분됨

 

LINQ 관계도

 


C# 4.0

 

#선택적 매개변수와 명명된 인수

선택적 매개변수(optional parameter)란 메서드의 매개변수 가운데 전달되지 않은 인자가 있는 경우 미리 지정된 기본값을 사용하는 것을 의미한다. (++메서드 오버로드)

 

선택적 매개변수를 이용하면 단 하나의 메서드만 정의하는 것으로도 메서드 오버로드의 효과를 이룰 수 있다.

class Person
{
    //pulic void Output(string name) {...}
    //pulic void Output(string name, int age) {...}
    //pulic void Output(string name, int age, string address) {...} 기존 메서드 오버로드 방식
    //메서드의 매개변수를 정의할 때 기본값을 함께 명시하는 것으로 간단하게 해결 가능
    public void Output(string name, int age = 0, string address = "korea")
    {
        Console.WriteLine(string.Format("{0}: {1} in {2}", name, age, address));
    }
}

 

#규칙

  • 선택적 매개변수는 ref, out 예약어와 함께 사용할 수 없다.
  • 선택적 매개변수가 시작되면 그 사이에 필수 매개변수를 사용할 수 없다.
  • 선택적 매개변수가 시작돼도 마지막에는 params 유형의 매개변수를 정의할 수 있다.
  • params 유형의 매개변수는 선택적 매개변수가 될 수 없다. 즉, 기본값을 지정할 수 없다.
  • 선택적 매개변수의 기본값은 반드시 상수 표현식이어야 한다.
  • 선택적 매개변수에 전달되는 인자는 차례대로 대응되며, 중간에 생략돼 전달될 수 없다.
//맨 마지막 규칙은 명명된 인수(named argument)를 사용해서 해결 가능
p.Output("Sophie", address: "france");
//명명된 인수는 선택적 매개변수에만 적용할 수 있는 것은 아니며, 순서를 지킬 필요도 없다.
p.Output(age: 5, name: "Tom", address: "Tibet");

 

#dynamic 예약어

마이크로소프트는 기존의 C#, VB.NET, C++/CLI와 같은 정적 언어 말고도 동적 언어까지도 닷넷과 호환되도록 CLR을 바탕으로 한 DLR(Dynamic Language Runtime) 라이브러리를 내놓았다.

 

dynamic 예악어 -> C#이 동적 언어와 연동을 쉽게 할 수 있게 해줌

 

#var vs dynamic

//Func 메서드는 정의되지 않음
dynamic a = 5;
a = "TEST"; //형식이 결정되지 않아서 다시 문자열로 초기화 가능
a.Func(); //실행시 에러 (컴파일 오류가 아님)

var b = 5;
b = "TEST"; //컴파일 에러 (컴파일시 형식이 결정됨)
b.Func(); //컴파일 에러

var 예약어는 C# 컴파일러가 빌드 시점에 초깃값과 대응되는 타입으로 치환하는 반면, dynamic 변수는 컴파일 시점에 타입을 결정하지 않고, 해당 프로그램이 실행되는 시점에 타입을 결정한다.

 

실제 컴파일러는 dynamic 예약어를 위해 다음과 같이 코드를 생성함

using System.Runtime.CompilerServices;
using Microsoft.CSharp.RuntimeBinder;

class Program
{
    //dynamic d = 5;
    //d.CallTest();는 C# 컴파일러가 dynamic 예약어를 위해 아래와 같은 코드를 자동으로 생성함

    public static CallSite<Action<CallSite, object>> p__Site1;

    static void Main(string[] args)
    {
        object d = 5;

        if(p__Site1 == null)
        {
        //System.Int32 타입의 인스턴스에는 CallTest 메서드는 존재하지 않아서 실행중 예외가 발생함
            p__Site1 = CallSite<Action<CallSite, object>>.Create(
                Binder.InvokeMember(CSharpBinderFlags.ResultDiscarded, "CallTest", null, typeof(Program),
                new CSharpArgumentInfo[]
                {
                    CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)
                }));
        }
        p__Site1.Target(p__Site1, d);
    }
}

 

원래의 변수 d는 object 타입으로 바뀌었고, 그것이 사용될 때마다 CallSite.Target 메서드를 통해 실행됨(dynamic 예약어도 간편 표기의 한 분류이다.)

 

#리플렉션 개선

string txt = "Test Func";
bool result = txt.Contains("Test");

//위코드의 Contains 메서드를 리플렉션을 통해 호출할 경우
Type type = txt.GetType();
//System에 Contains가 오버로드 되어 있으므로 매개변수 명시해야함
MethodInfo containsMehotdInfo = type.GetMethod("Contains", new Type[] { typeof(string) });
if(containsMehotdInfo != null)
{
    object returnValue = containsMehotdInfo.Invoke(txt, new object[] { "Test" });
    bool callResult = (bool)returnValue;
}

//리플렉션 기술이 dynamic의 근간을 이루고 있기 때문에 dynamic 예약어를 리플렉션의 간편 표기법 정도로 여기고 사용해도 무방함
dynamic txt = "TestFunc";
bool result = txt.Contains(txt);
//이러한 특성은 확장 모듈(Plug-in)을 사용하기 쉽게 만들어준다. 기존에는 Assembly.LoadFrom 등으로 직접 로드한 어셈블리 안에
//있는 타입의 메서드를 호출하려면 리플렉션을 이용해야만 했다. 하지만 dynamic을 사용하면 확장 모듈로부터 생성된 객체를
//dynamic 변수에 담아 사전에 정의된 메서드의 이름으로 호출하기만 하면 된다.

 

#덕 타이핑

둘 이상의 타입에서 '동일한 이름'으로 제공되는 속성 또는 메서드가 있다고 했을 때 여기에 접근해야 하는 코드를 작성하고 싶다면 보통은 인터페이스나 부모클래스를 상속하고 상속 관계를 이용해 호출할 수 있게 만든다.

하지만 모든 타입이 이런 식의 구조적인 호출 관계를 따르지는 않는다. 가령, string 타입과 List<T> 타입은 동일하게 IndexOf 메서드를 제공하지만 두 타입은 IndexOf 메서드를 위한 기반 타입을 상속받은 것이 아니기 때문에 공통 타입이 없다. 이러한 경우 dynamic을 사용하면 아무 문제 없이 처리할 수 있다.

    static int DuckTypingCall(dynamic target, dynamic item)
    {
        return target.IndexOf(item);
    }
    static void Main(string[] args)
    {

        List<int> list = new List<int>() { 1, 2, 3, 4, 5 };

        string txt = "test func";
        Console.WriteLine(DuckTypingCall(txt, "func")); // 5
        Console.WriteLine(DuckTypingCall(list, 3)); // 2
    }

 

덕 타이핑(duck typing)의 타이핑(typing)은 '형식 적용'을 의미한다. 일반적으로 객체 지향 언어에서는 강력한 형식(strong type)이 적용돼 있어 특정 속성이나 메서드를 호출하고 싶다면 반드시 그 형식을 기반으로 동작하게 된다. 하지만 동적 언어에서 널리 사용된느 덕 타이핑은 강력한 형식을 기반으로 하지 않고 단지 같은 이름의 속성이나 메서드가 공통으로 제공되면 그것을 기능적인 관점에서 동일한 객체라고 본다. 위에서 List<int>와 string은 객체지향 관점에서 보면 완전히 별개의 객체지만 IndexOf 메서드의 기능이 동일하게 제공된다는 관점에서 보면 같은 객체라고 볼 수 있는 것이다.

 

dynamic 예약어는 동적 언어에서나 가능하던 덕 타이핑을 정적 언어인 C#에서 가능하게 만든다.

 

#동적 언어와의 타입 연동

파이썬을 CLR로 포팅한 버전 IronPython 이용

using IronPython.Hosting;

class Program
{
    static void Main(string[] args)
    {
        var scriptEngine = Python.CreateEngine();
        var scriptScope = scriptEngine.CreateScope();

        //파이썬에서 AddFunc 함수를 정의
        string code = @"
def AddFunc(a, b):
    print('AddFunc called')
    return (a + b)
";
        //파이썬 엔진에서 해석된 AddFunc 참조를 dynamic 변수로 받고,
        scriptEngine.Execute(code, scriptScope);

        dynamic addFunc = scriptScope.GetVariable("AddFunc");

        //dynamic 변수를 마치 델리게이트 타입인 것처럼 메서드를 호출
        int nResult = addFunc(3, 5);

        Console.WriteLine(nResult);
    }
}

 

반대로 파이썬에서 C# 코드를 호출하는 것도 가능하다.

 

#동시성 컬렉션(Concurrent Collections)

C# 언어의 문법과 관련은 없고 단지 C# 4.0과 함께 배포한 닷넷 BCL에 추가된 타입 중 스레드와 연관해 알아 둘 필요가 있는 내용이 정리됨

ThreadPool.QueueUserWorkItem(new WaitCallback(CallbackMethod));

ThreadPool.QueueUserWorkItem((arg) => 
        {
            // 작업 내용
            ChangeList();
        });

 

#다중 스레드 접근을 위한 동기화 코드를 추가하는 것을 편하게 하기 위해 마이크로소프트가 제공하는 전용 컬렉션을 닷넷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>의 동시성 버전

위의 컬렉션들은 모두 스레드에 안전(thread-safe)하므로 다중 스레드 환경에서 개발자가 별도의 동기화 코드를 작성할 필요가 없다.


C# 5.0

#호출자 정보

매크로(macro) 상수가 실제로 디버깅에 매우 유용하게 사용됐기에 C#에서도 이에 대한 요구사항을 수용해 호출자 정보(caller information)로 구현됐다. 하지만 매크로를 통해 구현된 것은 아니고 C#의 특징을 살려 특성(attribute)과 선택적 매개변수의 조합으로 구현됐다.

class Program
{
    static void LogMessage(string txt,
        [CallerMemberName] string memberName = "",
        [CallerFilePath] string filePath = "",
        [CallerLineNumber] int lineNumber = 0)
    {
        Console.WriteLine("text: " + txt);
        Console.WriteLine("method name: " + filePath);
        Console.WriteLine("line number: " + lineNumber);
    }
    static void Main(string[] args)
    {
        LogMessage("테스트 로그");
    }
}

 

 

'호출자 정보'란 단어 그대로 '호출하는 측의 정보'를 메서드의 인자로 전달하는 것을 말한다. 이렇게 제공되는 호출자 정보는 현재 세 가지만 허용된다. (아래 표)

 

#C# 5.0에서 제공되는 호출자 정보

특성 설명
CallerMemberName 호출자 정보가 명시된 메서드를 호출한 측의 메서드 이름
CallerFilePath 호출자 정보가 명시된 메서드를 호출한 측의 소스코드 파일 경로
CallerLineNumber 호출자 정보가 명시된 메서드를 호출한 측의 소스코드 라인 번호

 

호출자 정보 특성이 명시된 매개변수는 반드시 선택적 매개변수 형식이어야 한다.

 

#비동기 호출

C# 5.0에 추가된 async, await 예약어를 사용하면 비동기 호출을 마치 동기 방식처럼 호출하는 코드를 작성할 수 있다.

동기식을 비동기로 바꾸는 요령

 

위 그림의 작업을 컴파일러가 알아서 해주는 목적으로 async/await 예약어가 탄생함

using System.Text;
class Program
{
    static void Main(string[] args)
    {
        AwaitRead();
    }
    private static async void AwaitRead()
    {
        using (FileStream fs = new FileStream(@"C:\windows\system32\drivers\etc\services",
            FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, true))
        {
            byte[] buf = new byte[fs.Length];
            await fs.ReadAsync(buf, 0, buf.Length);
            //아래의 두 라인은 C# 컴파일러가 분리해 ReadAsync 비동기 호출이 완료된 후 호출
            string txt = Encoding.UTF8.GetString(buf);
            Console.WriteLine(txt);
        }

    }
}

 

Async 류의 비동기 호출에 await 예약어가 함께 쓰이면 C# 컴파일러는 이를 인지하고 그 이후의 코드를 묶어서 ReadAsync의 비동기 호출이 끝난 후에 실행되도록 코드를 변경해서 컴파일 한다. -> 비동기 호출을 동기 호출처럼 작성

 

await를 적용한 후의 스레드 실행 관계

 

async/await는 문맥 예약어로 async 예약어를 지정하면 C# 컴파일러가 await를 예약어로 취급함

 

#닷넷 BCL에 추가된 Async 메서드

async/await의 도입으로 비동기 호출 코드가 매우 간결해졌다. 이러한 편리함을 누릴 수 있게 마이크로소프트는 기존의 BCL 라이브러리에 제공되던 복잡한 비동기 처리에 async/await 호출이 가능한 메서드를 추가했다.

 

#Task, Task<TResult> 타입

await로 대기할 수 있는 Async 메서드의 반환값이 모두 Task 또는 Task<TResult> 유형이다. Task 타입은 반환값이 없는 경우 사용되고, Task<TResult> 타입은 TResult 형식 매개변수로 지정된 반환값이 있는 경우로 구분되는데, await 비동기 처리와는 별도로 원래부터 닷넷에 추가된 병렬 처리 라이브러리(TPL: Task Parallel Library)에 속한 타입이다. 

따라서 await 없이 Task 타입을 단독으로 사용하는 것도 가능하다.

using System.Text;
class Program
{
    static void Main(string[] args)
    {
        //기존
        ThreadPool.QueueUserWorkItem(
            (obj) =>
            {
                Console.WriteLine("process workitem");
            }, null);

        //Task 타입의 생성자는 Action 타입의 델리게이트 인자를 받는다.
        Task task1 = new Task(
            () =>
            {
                Console.WriteLine("process taskitem");
            });
        //start 메서드가 호출되면 내부적으로 ThreadPool의 자유 스레드를 이용해 Action 델리게이트로 전달된 코드를 수행한다.
        task1.Start();

        Task task2 = new Task(
            (obj) =>
            {
                Console.WriteLine("process taskitem(obj)");
            }, null);

        task2.Start();

        Console.ReadLine();
    }
}

 

Task 타입이 ThreadPool의 QueueUserWorkItem과 차별화된 점은 좀 더 세밀하게 제어할 수 있다는 점이다.

 

Task<TResult> 타입을 사용하면 자유 스레드에 던져 실행이 끝날 때 반환값까지 처리 가능 task.Result();(대기 기능도 포함)

 

#async 예약어가 적용된 메서드의 반환 타입

async 예약어가 지정되는 메서드에는 void, Task, Task<T>만 반환이 가능하다는 제약이 있다.

이때 async void 유형은 해당 메서드 내에서 예외가 발생했을 때 그것이 처리되지 않은 경우 프로세스가 비정상적으로 종료되므로 권장되지 않음

 

#Async 메서드가 아닌 경우의 비동기 처리

닷넷 BCL에 Async 메서드로 제공되지 않았던 모든 동기 방식의 메서드를 반환이 Task, Task<T>인 비동기 버전 메서드로 만들면 비동기로 변환할 수 있음

//이 메서드에 await를 사용하면 ReadAllText에 비동기 호출 적용 가능
static Task<string> ReadAllTextAsync(string filePath)
{
    return Task.Factory.StartNew(() =>
    {
        return File.ReadAllText(filePath);
    });
}

 

#비동기 호출의 병렬 처리

using System.Text;
class Program
{
    static void Main(string[] args)
    {
        //await울 이용해 병렬로 비동기 호출: 5초 소요
        DoAsyncTask();

        Console.ReadLine();
    }

    private static async Task DoAsyncTask()
    {
        var task3 = Method3Async();
        var task5 = Method5Async();

        await Task.WhenAll(task3, task5);

        Console.WriteLine(task3.Result + task5.Result);
    }

    private static Task<int> Method3Async()
    {
        return Task.Factory.StartNew(() =>
        {
            Thread.Sleep(3000);
            return 3;
        });
    }
    private static Task<int> Method5Async()
    {
        return Task.Factory.StartNew(() =>
        {
            Thread.Sleep(5000);
            return 5;
        });
    }
}

 

Task.WhenAll과 await의 조합으로 Main, DoAsyncTask 메서드를 숳ㅇ하는 스레드가 task3, task5의 작업이 완료될 때까지 대기하지 않고 곧바로 다음 작업을 계속해서 수행함


C# 6.0

2.0-제네릭, 3.0-LINQ, 4.0-dynamic, 5.0-비동기 호출과 같은 중요한 기능을 배웠었는데 6.0은 단순 코드량을 줄여주는 간편 표기법 문법이 추가됨

 

#자동 구현 속성 초기화(Initailizers for auto-properties) 구문 추가

class Person
{
    public string Name { get; set; } = "Jane";
}

 

숨겨진 필드에 readonly 예약어가 부여됨

 

#표현식을 이용한 메서드, 속성 및 인덱서 정의

public class Vector
{
    double x;
    double y;
    
    //속성
    //public double Angle => Math.Atan2(y, x); get만 자동 정의되고 set 기능은 제공되지 않는다.
    
    //인덱서
    //public double this[string angleType] =>
    //    angleType == "radian" ? this.Angle :
    //    angleType == "degree" ? RadianToDegree(this.Angle) : double.NaN;
    
    public Vector(double x, double y)
    {
        this.x = x;
        this.y = y;
    }
    
    //메서드
    public Vector Move(double dx, double dy) => new Vector(x + dx, y + dy);
    
    public void PrintIt() => Console.WriteLine(this);
    
    public overrid string ToString() => string.Format("X = {0}, y = {1}", x, y);
}

 

 

속성 정의에서 설정자(set) 메서드에 대한 표현식 정의는 C# 7.0에서부터 지원

 

일반적으로 식(expression)이라고 하면 0개 이상의 연산자(operator)에 의해 결합되어 단일 값으로 계산할 수 있는 경우로 여기에는 메서드 호출 및 리터럴 자체도 포함함

식 중에서도 특히 컴파일 시점에 정할 수 있는 경우(constant expression)이라고 한다.

 

반면 문(statement)으로는 선택/반복/점프 및 변수의 선언문이 있으며, 이와 함께 식의 경우에도 대입, 호출, 증가/감소, await과 new에 한해 문으로 사용될 수 있다.

 

#using static 구문을 이용한 타입명 생략

확장 메서드의 경우 내부적으로 static 메서드로 표현되지만 문법적인 모호성 문제로 인해 using static 적용을 받지 않는다.

 

#null 조건 연산자

Console.WriteLine(list?.Count); //list == null -> null 반환
                                //list != null -> list.Count 멤버 변수의 값 반환
//아래와 같이 자동 변경해서 컴파일 됨
Console.Write(list != null ? new int?(list.Count) : null);

for(int i = 0; list != null && i < list.Count; i++) { ... }

//아래처럼 가능
for(int? i = 0; i <list?.Count; i++) { ... }

 

null 조건 연산자의 결괏값이 null을 포함할 수 있기 때문에 이를 저장하기 위해서는 반드시 null 값을 처리할 수 있는 타입을 사용해야 한다.

 

int count = list?.Count; //컴파일 에러 int 에는 null 대입 불가
int? count = list?.Count;
//또는 ?? 연산자를 통해null시 0 반환해서 사용
int count = list?.Count ?? 0;

List<int> list = null;
list?.Add(5); //반환 값 없이도 널 연산자 사용 가능

 

#문자열 내에 식(expression)을 포함

System.String 타입은 불변해서 문자열을 직접 연결하는 코드보다는 string.Format 메서드가 권장되는데, 문자열 보간(String interpolation)이라는 약식 표현이 추가됨

 

return $"이름: {name}, 나이: {age}";
//컴파일시 아래처럼 됨
return string.Format("이름: {0}, 나이: {1}", name, age);

//형식 문자열도 지원
return $"{{ 이름: {name,-10}, 나이: {age,5:X} }}"; //{ 입력시 {{ 두 번 사용

 

#nameof 연산자

nameof의 반환값은 식별자 이름의 마지막 이름만 반환됨

string txt = nameof(System.Console);
    //txt == "Console"

 

nameof 연산자의 등장으로써 코드 내에 이름을 하드코딩하는 사례는 C# 6.0부터 볼 수 없게 됐다.

//다음 코드는 OutputPerson의 첫 번째 인자에 대한 이름을 리플렉션을 통해 구함
static void OutputPerson(string name)
{
    //StackFrame 클래스는 호출 스택의 프레임을 나타내며, 
    //프로그램이 실행될 때 메서드 호출 스택의 특정 프레임에 대한 정보를 제공함
    StackFrame sf = new StackFrame();
    System.Reflection.ParameterInfo [] parameters = sf.GetMethod().GetParameters();
    
    string nameId = parameters[0].Name;
    Console.WriteLine(nameId + ": " + name);
}
//리플렉션은 코드가 실행돼야 이름이 구해지는 반면 nameof는 C# 6.0 컴파일러가
//컴파일 시점에 문자열로 직접 치환해 주기 때문에 실행 시점에는 부하가 전혀 없다.

#Dictionary 타입의 인덱스 초기화

var weekends = new Dictionary<int, string>
{
    { 0, "Sunday" },
    { 6, "Saturday" },
    //컴파일시 weekend.Add(0, "Sunday");
    { 6, "Sunday" }, //실행시 예외
}

var weekends = new Dictionary<int, string>
{
    //[TKey] = TValue;
    //컴파일시 weekends[0] = "Sunday";
    [0] = "Sunday",
    [6] = "Saturday",
};

#예외 필터

비주얼 베이직과 F# 언어에서 지원하던 예외 필터(Exception filters)가 사용가능해짐

try
{
    //...[코드]...
} catch (예외_타입 e) when (조건식)
{
    //...[코드]...
}
//catch에 지정된 예외_타입에 속하는 예외가 try 블록 내에서 발생한 경우, 
//조건식이 true로 평가된 경우에만 해당 예외 처리기가 선택된다.

 

예외 처리 필터에는 한 가지 특이한 점이 있는데, 해당 예외 필터의 조건식이 실행되는 시점은 아직 예외 처리 핸들러가 실행되는 시점이 아니기 때문에 예외가 발생한 시점의 호출 스택(Call stack)이 그대로 보존돼 있다는 점이다. 그래서 기존 예외 처리 구조에 영향을 주지 않고도 부가적인 작업을 할 수 있다.

 

예외 필터는 닷넷의 IL(Intemediate Language) 수준에서 이미 지원하기 때문에 예외 필터의 IL 코드로 직접 변경된다.

 

예외 필터를 사용하면 기존에 불가능했던 동일한 예외 타입의 catch 구문 여러개 두기 가능함(when 조건문은 여러 번 실행되는 것이 가능하지만 선택되는 catch 예외 핸들러는 오직 하나뿐임)

#컬렉션 초기화 구문에 확장 메서드로 정의한 Add 지원

8.4절 '컬렉션 초기화'(pp.602)에서 설명한 구문이 컴파일되려면 반드시 해당 타입이 ICollection<T> 인터페이스를 구현하고 있어야 함

 

C# 6.0에는 Add 메서드를 ICollection<T> 인터페이스가 없다면 확장 메서드로도 구현돼 있는지 다시 한번 더 찾는 기능을 추가했다.

 

#기타 개선 사항

  • catch/finally 블록 내에서 await 사용 가능
  • #pragma의 'CS' 접두사 지원
  • 재정의된 메서드의 선택 정확도를 향상

 


C# 7.0

#더욱 편리해진 out 매개변수 사용

//기존
{
    int result; //미리 선언 필요
    int.TryParse("5", out result);
}

//C# 7.0; 기존 처럼 컴파일 됨
{
    int.TryParse("5", out int result);
}

//discard 구문
//discard 식별자는 변환된 결과를 무시하고 사용하지 않겠다는 것을 의미함
//즉, 이 코드는 변환 결과를 저장하지 않고 단순히 변환이 성공했는지 여부만을 알고자 할 때 사용
{
    int.TryParse("5", out int _); //변수명 대신 underline로 생략
    int.TryParse("5", out var _); //타입 대신 var
    int.TryParse("5", out _); //타입도 생략 가능
}

 

#반환값 및 로컬 변수에 ref 기능 추가(ref returns and locals)

C# 7.0에서 out 예약어는 사용이 쉽게 개선된거라면 ref 예약어는 사용법이 확장됨 기존의 ref 예약어가 오직 메서드의 매개변수로만 사용가능했다면 C# 7.0부터는 로컬 변수와 반환값에 대해서도 참조 관계를 맺을 수 있게 개선되었다.

 

#로컬 변수

int a = 5;
ref int b = ref a; //a와 b는 동일한 메모리를 공유

 

#참조 return

{
    IntList intList = new IntList();
    ref int item = ref intList.GetFirstItem();
    item = 5; //참조값이므로 값을 변경하면 원래의 int[] 배열에도 반영
}

class IntList
{
    int[] list = new int[2] { 1, 2 };
    
    public ref int GetFirstItem()
    {
        return ref list[0];
    }
}

 

참조 return 덕분에 메서드에 값을 대입하는 구문이 가능해짐

 

#반환 및 로컬 변수에 사용할 수 있는 ref 예약어 제약

  • 지역 변수를 return ref로 반환해서는 안 된다. 지역 변수의 유효 범위가 스택상에 있을 때로 한정되기 때문에 메서드의 실행이 끝나 호출 측으로 넘어가는 시점에 스택이 해제되어 return ref로 반환받은 인스턴스가 남아 있을 거라는 보장을 할 수 없기 때문이다.
  • ref 예약어를 지정한 지역 변수는 다시 다른 변수를 가리키도록 변경할 수 없다.(C# 7.3에서 재할당이 가능해짐)

 

#튜플

튜플(Tuple)이란 유한 개의 원소로 이뤄진 정렬된 리스트를 의미하는 자료구조로서 일반적으로 n-tuple 이라고 일컬을 때의 n은 요소의 수를 의미하며 가령 3개의 요소를 갖는 경우 3-tuple이라고 함

 

C#에 도입된 튜플은 메서드에서 인자를 받거나 값을 반환할 때 여러 개의 값을 한 번에 전달할 수 있는 약식 구문이라고 여기면 된다.

 

dynamic을 도입하면 정적 형식 검사를 할 수 없어 나중에 필드 이름 바뀌어도 컴파일 시 문제를 알아낼 수 없음

 

//튜플의 반환 타입
(반환타입 [필드명], 반환타입 [필드명], ...) 메서드명([타입명] [매개변수명], ...)
{
    //코드: 메서드의 본문
    return ([반환타입에 해당하는 표현식, ...]);
}

//튜플의 입력 타입
반환타입 메서드명(([타입명][매개변수명], [타입명][매개변수명], ...))
반환타입 메서드명([타입명][매개변수명], ([타입명][매개변수명], [타입명][매개변수명]),...)

괄호를 사용해 2개 이상의 타입 및 그 이름을 지정할 수 있고 반환타입뿐만 아니라 매개변수로도 전달할 수 있다.
튜플의 각 요소는 Item1부터 차례대로Item2, Item3, ...의 순으로 자동 명명되지만 원한다면 이름을 직접 지정하는 것도 가능하다.

 

튜플을 반환하는 메서드가 지정한 튜플의 이름들 호출하는 측에서 강제로 지정하는 것이 가능

{
    (bool success, int n) result = pg.ParseInteger("50");
    Console.WriteLine(result.success); //튜플의 첫 번째 인자에 success로 접근
    Console.WriteLine(result.n); //튜플의 두 번째 인자에 n으로 접근
 }
 {
    //튜플로 받지 않고 개별 필드를 분해해서 받는 구문도 지원함
    (var success, var number) result = pg.ParseInteger("50");
    Console.WriteLine(result.success); //튜플의 첫 번째 인자의 값을 담은 success 변수
    Console.WriteLine(result.number); //튜플의 두 번째 인자의 값을 담은 number 변수
}
{
    //out 매개변수 처리에서 지원했던 생략기호도 튜플의 반환값을 분해하는 구문에 사용 가능
    (var _, var _) = pg.ParseInteger("70"); //2개의 값을 모두 생략
    (var _, var n) = pg.ParseInteger("70"); //마지막 값만 n으로 받음
    Console.WriteLine(n);
}

//++튜플로 구현한 swap
int a = 5;
int b = 7;

(a, b) = (b, a);

 

메서드의 인자와 반환에 사용한 모든 튜플 구문은 C# 7.0 컴파일러가 소스코드를 컴파일하는 시점에 System.ValueTuple 제네릭 타입으로 변경해서 처리한다. 참고로 기존의 System.Tuple이 참조 형식인 반면, System.ValueType은 값 형식이다.

 

#Deconstruct 메서드

튜플의 반환값을 분해하는 구문을 원하면 Deconstruct라는 이름의 특별한 메서드를 1개 이상 정의해 구현 가능하다. 이때 분해가 되는 개별 값을 out 매개변수를 이용해 처리하면 된다. (pp.722~723)

접근_제한자 void Deconstruct(out T1 x1, ..., out Tn xn) {...}
//T1 ~ Tn은 분해될 값을 담을 타입. out 매개변수이므로 반드시 값을 채워서 반환해야 함

 

#람다 식을 이용한 메서드 정의 확대(Expression-bodied members)

C# 6.0의 람다식 접근 확장

  • 일반 메서드
  • 속성의 get 접근자(읽기 전용으로 처리됨)
  • 인덱서의 get 접근자(읽기 전용으로 처리됨)

C# 7.0에서 확장된 람다식 접근

  • 생성자(Constructor)
  • 종료자(Finalizer)
  • 이벤트 add/remove
  • 속성의 set 구문
  • 인덱서의 set 구문
set => _ = (index == 0) ? x = value : y = value; //_을 통해 반환 값은 무시

private EventHandler positioChanged;
public event EventHandler PositionChanged //이벤트의 add/remove 접근자 정의 가능
{
    add => this.positionChanged += value;
    remove => this.positionChanged -= value;
}

public Vector Move(double dx, double dy)
{
    x += dx;
    y += dy;
    
    positionChanged?.Invoke(this, EventArgs.Empty);
    
    return this;
}

 

#로컬 함수(Local functions)

void LocalFuncTest()
{
    //로컬함수로 사용하면 기존 delegate 방식보다 편하게 함수 정의
    (bool, int) func(int a, int b)
    {
        if(b == 0) return (false, 0)
        return (true, a / b);
    }
    //(bool, int) func(int a, int b) => (b == 0) ? (false, 0) : (true, a / b);
    
    Console.WriteLine(func(10, 5));
}

 

참고로 로컬 함수에 대해 C# 컴파일러는 소스코드를 internal 접근자를 가진 메서드로 정의해 타입내에 자동으로 추가한다. 단지 이때 메서드 이름이 컴파일러에 의해 임의로 만들어지기 때문에 다른 곳에서는 호출할 수 없을 뿐이다. 하지만 리플렉션을 이용해 다소 억지스럽지만 원한다면 호출이 가능하다.

#사용자 정의 Task 타입을 async 메서드의 반환 타입으로 사용 가능

기존에 async 예약어가 붙은 메서드는 반환 타입이 void, Task, Task<T> 중 하나로 알려졌는데 C# 7.0 부터 비동기 메서드에서 그 외의 타입도 반환이 가능해졌다 ex) ValueTask<T> (async 메서드 내에서 await을 호출하지 않은 경우라면 불필요한 Task 객체 생성을 하지 않음으로써 성능을 높임

 

사용자 정의 반환 타입 구현

 

#자유로워진 throw 사용

throw 구문은 식(expression)이 아닌 문(statement)에 해당해서 표현식에서의 사용이 제한됐었다. C# 7.0부터는 throw가 의미 있게 사용될 만한 식에서 허용되도록 바뀌었다.(완전히 식으로 바뀐게 아님)

 

public void Assert(bool result) =>
{
#if DEBUG
    _ = result == true ? result : throw new ApplicationException("ASSERT");
#else
    _ = result;
#endif

 

이 밖에도 null 병합 연산자(??)와 람다식을 사용할 수 있는 곳에서 throw를 사용할 수 있다.

 

//null 병합 연산자
public Person(string name) => Name = name ??
                              throw new ArgumentNullException(nameof(name))l;
//람다 식
public string GetLastName() => throw new NotImplementedException();

//단일 람다 식을 이용한 델리게이트 정의
Action action = () => throw new Exception();

 

#리터럴에 대한 표현 방법 개선

숫자 데이터가 길어질 때 밑줄을 추가해서 가독성 개선이 가능해짐

int number1 = 10000000;
int number2 = 10_000_000;

uint hex1 = 0xFFFFFFFF
uint hex2 = 0xFF_FF_FF_FF //1바이트마다 끊어서도 표현 가능

 

#패턴 매칭

C#에서의 패턴 매칭 -> 객체에 대한 매칭

 

#C# 7.0에서 추가된 매칭 유형

  • 상수 패턴(constant patterns)
  • 타입 패턴(Type patterns)
  • Var 패턴(Var patterns)

각 패턴은 '객체에 대해' 상수 값인지, 아니면 주어진 어떤 타입과 일치(match)하는지 테스트할 수 있다.

 

#is 연산자의 패턴 매칭

is 연산자는 기존 기능에서 패턴 매칭을 지원하기 위해 as 연산자의 기능을 흡수했다.

if (a is int b) //b 같이 변수명을 붙이면 as 연산자와 동일 역할 수행
{
    Console.WriteLine($"a is an integer with value {b}");
}
else
{
    Console.WriteLine("a is not an integer");
}

 

#switch/case 문의 패턴 매칭

switch(인스턴스)
{
    case 패턴_매칭_식1:
        구문;
        break;
    case 패턴_매칭_식n:
        구문;
        break;
    default:
        구문;
        break;
 }
 
 //설명: 인스턴스의 값과 컴파일 또는 실행 시에 결정되는 case의 패턴_매칭_식 결괏값이 일치하는 경우
 //해당 case에 속한 구문을 실행한다. 나열된 case의 패턴_매칭_식에 일치하는 값이 없다면 default에
 //지정된 구문의 코드를 실행한다. 인스턴스의 타입 유형에는 제약이 없다.

 

case 조건의 패턴 매칭 문법은 기본적으로 is 연산자와 같음

foreach(object item in objList)
{
    switch(item)
    {
        case 100: //상수 패턴
            break;
            
        case null: //상수 패턴
            break;
            
        case DateTime dt: //타입 패턴 (값 형식) - 필요 없다면 dt 변수명을 밑줄(_)로 생략 가능
            break;
            
        case ArrayList arr: //타입 패턴 (참조 형식) - 필요 없다면 arr 변수명을 밑줄(_)로 생략 가능
            break;
            
        case var elem: //Var 패턴 (이렇게 사용하면 default와 동일)
            break;
    }
}

//case 조건의 패턴 매칭에는 한 가지 혜택이 더 있는데 바로 When 예약어를 추가해
//조건을 한 번 더 검사할 수 있다는 것이다. ->  예외 필터에서 사용법과 같음

swtich(j)
{
    case int i when i > 300:
        break;
    default:
        break;
}

 

case에 when 조건을 사용해서 var 패턴을 사용자 정의 패턴 매칭 구현에 사용할 수 있다.

 

#case의 평가 순서

  • default 절은 그 위치에 상관없이 항상 마지막에 평가된다.
  • case의 순서가 중요해지는데 상위의 case 조건에서 이미 만족한다면 그 절의 코드가 실행되고 그 하위의 case 조건에 대한 평가는 무시된다.

 

++

C#에서 switch 문에서 break 문이 없으면, C와 C++과 같은 일부 언어에서와 달리, 다음 case로 넘어가는 "fall-through" 동작을 하지 않습니다. C#에서는 case 블록이 break 또는 return과 같은 제어문으로 끝나지 않으면 컴파일 오류가 발생합니다.


C# 7.1

#Main 메서드에 async 예약어 허용

C# 7.1부터 다음과 같은 새로운 유형의 Main 메서드 정의가 가능하다.

static async Task Main()
static async Task Main(string[])
static async Task<int> Main()
static async Task<int> Main(string[])

 

#default 리터럴 추가

default 예약어에 대해 타입 추론이 가능해져서 리터럴 형식으로 써도 사용이 가능해짐

default(Type) 형식이 아닌 default 로 사용 가능

 

#타입 추론을 통한 튜플의 변수명 자동 지정

C# 7.1부터는 튜플에 대입된 변수명을 타입 추론을 통해 알 수 있으므로 Item1 같은 이름을 대체하기 위해 명시적으로 이름을 지정할 필요가 없어졌다.

int age = 20;
int name = "rara";

var t = (age, name)
Console.WriteLine($"{t.age}, {t.name}");

 

#기타 개선 사항

  • 패턴 매칭 - 제네릭 추가
  • 참조 전용 어셈블리(Ref Assemblies)

C# 7.2

C# 7.2는 값 형식(struct)에서 발생하는 부하를 없앨 수 있는 중요한 문법을 포함하고 있으며, 이는 가비지 컬렉션의 부하를 최소화할 수 있어 고성능 응용 프로그램을 다루는 개발자들의 요구사항을 만족한다.

#메서드의 매개변수에 in 변경자 추가

in 예약어는 ref와 readonly 의미를 모두 가짐

class Program
{
    static void Main(string[] args)
    {
        Program pg = new Program();
        
        Vector v1 = new Vector();
        pg.StructParam(in v1); //v1 인스턴스의 주소만 복사
    }
    
    void StructParam(in Vector v) //ref + readonly 의미
    {
        //in 예약어를 사용하면 값 복사의 부하도 없애는 동시에
        //원치 않은 변경으로 인한 부작용을 방지할 수 있다.
        v.X = 5; //컴파일 오류: 읽기 전용 변수이므로 멤버 할당 불가능
    }
}

 

내부 구현의 관점에서 보면 out 예약어가 ref 방식의 변형인 것처럼 in 예약어 역시 ref를 기반으로 System.Runtime.CompilerSevices.IsReadOnlyAttrubute 특성을 메서드의 인자에 추가하는 방식으로 다뤄진다. 이 때문에 동일한 이름으로 ref, out과 in 예약어를 적용한 메서드를 각각 만들 수 는 없다.

#읽기 전용(readonly) 구조체

C# 컴파일러는 readonly가 적용된 구조체 인스턴스에 한해 그 상태를 유지할 수 있도록 모든 메서드 호출을 다음과 같이 자동으로 변경해서 컴파일한다.

 

#defensive copy

readonly Vector v1 = new Vector();
Vector temp = v1; //원본 v1의 값을 변경하지 않도록 방어 복사본(defensive copy) 처리
temp.Increment(); //값 복사된 temp 인스턴스에 대해 메서드 호출

 

방어 복사본 처리는 개발자 스스로 인지하기가 쉽지 않아 자칫 버그로 쉽게 이어질 수 있다.(IL 코드 수준에서 드러나는 방어 복사본 처리를 모른다면 그저 황당한 C# 버그 정도로 여길 수 밖에 없다.

 

이와 같은 문제를 해결하기 위해 C# 7.2에 신규 추가된 문법이 'readonly 구조체'다.

readonly struct 타입명
{
    //모든 필드는 readonly
}

설명: 멤버 필드가 모두 readonly여야 한다는 점을 제외하고는 기존 struct 타입과 동일하다.

 

struct 내에 메서드 정의가 가능하나 클래스와 결정적으로 차이가 나는 점은 struct는 값 형식이라는 점이다.

 

#메서드의 반환 값 및 로컬 변수에 ref readonly 추가

class Program
{
    readonly Vector v1 = new Vector();

    static void Main(string[] args)
    {
        Program pg = new Program();
        StructParam(in pg.GetVector()); //in 변경자를 적용
    }

    private ref readonly Vector GetVector() //메서드의 반환 값을 ref readonly로 설정
    {
        return ref v1 //v1 인스턴스의 값 복사가 발생하지 않도록 ref 반환
    }
}

 

#스택에만 생성할 수 있는 값 타입 지원 - ref struct

struct가 class 안에 정의된 경우 힙에 데이터가 위치하게 됨. 이때 값 형식을 오직 스택에만 생성할 수 있도록 강제할 수 있는 방법이 추가 됨

ref struct 타입명
{
    //멤버 정의
}

설명: 인스턴스가 힙에 생성될 수 없다는 점을 제외하고는 기존 struct 타입과 동일하다.

 

주요한 제약사항으로 인터페이스를 구현할 수 없다는 점이 있다. 인터페이스로의 형 변환은 스택 객체가 힙 객체로 변환돼야 하는 박싱 작업을 수반하는데 ref struct 타입은 힙에 생성할 수 없는 유형이기 때문이다.

 

Span 타입은 내부적으로 관리 포인터를 사용한다. 그리고 관리 포인터는 GC의 현재 구현상 절대 '관리 힙'에 놓일 수 없다는 제약을 갖는다. 그래서 마이크로소프트가 Span<T> 타입을 구현하면서 관리 포인터를 담을 수 있는 특수한 구조체가 필요하게 됐고 그것이 바로 'ref struct'다.

#신규 추가 타입: Span<T>

Span<T>를 기술적으로 정의하면 '제네릭 관리 포인터를 가진 readonly ref struct'라고 말할 수 있지만 좀 더 쉽게 기능적인 면으로 접근해 보면 단순히 '배열에 대한 참조 뷰(view)'를 제공하는 타입으로 설명할 수 있다. 따라서 기본적으로 C#에서 만드는 모든 배열을 Span<T> 타입으로 가리킬 수 있다.

 

참조 뷰라는 이름에 걸맞게 Span<T> 인스턴스는 원본을 관리 포인터로 가리키고 있다는 점에서 값 변경까지 허용한다.

{//힙 배열에 대한 일관된 뷰
    var arr = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7 };
    Span<byte> view = arr;

    Span<byte> viewLeft = view.Slice(0, 4);
    Span<byte> viewRight = view.Slice(4);
}

 

위 소스코드 예시에서 Span<T> 타입을 이용해서 순수하게 스택 영역의 값만으로  view 변수에 대한 일정 역역을 가리키는 역할을 한다. 따라서 무분별한 힙의 사용을 최소화할 수 있고 이로 인해 가비지 컬렉터의 사용을 줄여 자연스럽게 성능 향상을 가져온다.

 

Span<T>는 힙 배열, 스택 배열, 비관리 메모리의 배열에 대한 일관된 뷰를 제공한다는 점이다.

{
    Span<byte> bytes = stackalloc byte[10]; //스택 배열
    bytes[0] = 100;
    Print(bytes);
}

unsafe
{
    int size = 10;
    IntPtr ptr = Marshal.AllocCoTaskMem(size); //비관리 메모리의 배열
    
    try
    {
        Span<byte> bytes = new Span<byte>(ptr.ToPointer(), size);
        bytes[9] = 100;

        Print(bytes);
    }
    finally
    {
        Marshal.FreeCoTaskMem(ptr);
    }
}

static void Print(Span<byte> view)
{
    Console.WriteLine(string.Join(',', view.ToArray()));
    Console.WriteLine();
}

 

Print 메서드 입장에서는 전달된 인자의 배열이 스택, 힙, 비관리 메모리인지에 상관없이 단일한 코드로 일관되게 참조 뷰에 접근해 연산을 할 수 있다.

 

unsafe 예제에서 byte[10] = 1; 처럼 비관리 메모리에 잘못 접근하게 되면  Access Violation 오류가 발생해 프로그램은 비정상 종료하게 되므로 try 이후 catch/finally 절은 실행되지 않는다. 하지만 Span<T>로 감싼 경우라면 그 대상이 비관리 메모리일지라도 내부적인 관리 포인터의 혜택으로 예외처리가 가능해 try/catch 절이 정상 작동한다.

 

#3항 연산자에 ref 지원

3항 연산자에서 로컬 변수에 참조를 전달할 수 있는 지원이 추가되어 이런 문제가 해결됨

ref (조건식) ? ref 표현식1 : ref 표현식2;

설명: 조건식에 따라 표현식1과 표현식2의 참조를 반환

 

#private protected 접근자 추가

public: 모든 코드에서 접근 가능
internal: 동일한 어셈블리 내에서만 접근 가능
protected: 해당 클래스와 이를 상속받는 파생 클래스에서만 접근 가능
internal protected: 동일한 어셈블리 내에서 또는 다른 어셈블리의 파생 클래스에서 접근 가능(internal or protected)
private protected: 동일 어셈블리 내에서 정의된 파생 클래스인 경우에만 접근 가능(internal and protected)
private: 해당 클래스 내에서만 접근 가능

 

C# 7.2부터 private protected가 추가되는데, 사실 중간 언어(IL) 수준에서 private protected 접근자가 제공되고 있었지만 이를 C# 언어에서도 표현 가능하게 만든 것이다.

 

internal protected가 internal or protected라면 private protected는 internal 그리고 protected 조건이다.

 

 

#숫자 리터럴의 선행 밑줄(Digit separator after base specifier)

리터럴 접두사(0x, 0b)가 있는 경우에도 밑줄 사용가능 ex) 0x_1000

 

#뒤에 오지 않는 명명된 인수(Non-trailing named arguments)

class Program
{
    static void Main(string[] args)
    {
        Person p = new Person();

        //C# 7.1 이전에는 컴파일 오류
        //첫 번째 인자에 이름을 지정했으므로 두 번쨰 인자도 이름을 지정해야 했었다
        p.Output(name: "rara", 11, address: "pripara");
    }
    class Person
    {
        public void Output(string name, int age = 0, string address = "korea")
        {
            Console.WriteLine($"{name}: {age} in {address}");
        }
    }
}

 


C# 7.3

#신규 제네릭 제약 조건 - Delegate, Enum, unmanaged

제네릭의 제약 조건으로 타입을 사용하려면 다음 조건을 만족해야 함

  • 클래스 타입이어야 한다.
  • sealed 타입이 아니어야 한다.
  • System.Array, System.Delegate, System.Enum은 허용하지 않는다.
  • System.ValueType은 허용하지 않지만 특별히 struct 제약을 대신 사용할 수 있다. 또한 System.Object도 허용하지 않지만, 어차피 모든 타입의 기반이므로 제약 조건으로써의 의미가 없다.

이중 System.Delegate, System.Enum은 7.3에서 제약이 풀림

class EnumValueCache<TEnum> where TEnum : System.Enum
{
    Dictionary<TEnum, int> _enumKey = new Dictionary<TEnum, int>();

    public EnumValueCache()
    {
        //Enum.GetValues(typeof(TEnum))는 TEnum 열거형의 모든 값을 포함하는 배열을 반환
        int[] intValues = Enum.GetValues(typeof(TEnum)) as int[];
        TEnum[] enumValues = Enum.GetValues(typeof(TEnum)) as TEnum[];

        for(int i = 0; i < intValues.Length; i++)
        {
            _enumKey.Add(enumValues[i], intValues[i]);
        }
    }

    public int GetInteger(TEnum value)
    {
        return _enumKey[value];
    }
}

 

 

기존 struct 제약의 좀 더 특수한 사례에 속하는 제약 조건 unmanaged 를 이용하면 struct 제약 중에서 대상 타입이 참조 형식을 필드로 갖지 않는다는 보장을 하나 더 해준다. 따라서 기존에는 불가능했던 형식 매개변수에 대한 포인터 연산을 할 수 있다.

 

class UnsafeMethods
{
    [DllImport("kernel32.dll")]
    public static extern void RtlZeroMemory(IntPtr dst, int length);
}

class UnmanagedWrapper<T> : IDisposable where T : unmanaged
{
    IntPtr _pArray;
    int _maxElements;

    public unsafe UnmanagedWrapper(int n)
    {
        _maxElements = n;

        int size = sizeof(T) * n;
        _pArray = Marshal.AllocCoTaskMem(size);
        //Windows API에서 제공하는 함수로, 메모리 블록을 0으로 채우는 역할
        UnsafeMethods.RtlZeroMemory(_pArray, size);
    }

    public unsafe T this[int idx]
    {
        get
        {
            //unmanged 제약을 사용함으로써 포인터 연산이 가능한 타입만 받아들인다
            //따라서 기존에는 불가능했던 (T*)와 같은 포인터 연산을 사용할 수 있다.
            T* ptr = ((T*)_pArray.ToPointer() + idx);
            return *ptr;
        }
        set
        {
            T* ptr = ((T*)_pArray.ToPointer() + idx);
            *ptr = value;
        }
    }

    public void Dispose()
    {
        Marshal.FreeCoTaskMem(_pArray);
    }
}

 

UnmanagedWrapper 타입을 일반 배열을 다루듯이 사용할 수 있다.

 

#사용자 정의 타입에 fixed 적용 가능

//C# 7.3부터 사용자 타입이 GetPinnableReference라는 이름으로 관리 포인터를 반환하는 메서드를
//포함하고 있는 경우라면 fixed 구문에 자연스럽게 사용이 가능하도록 통합됨
public ref int GetPinnableReference() { ... }

 

이와 같은 문법 추가로 Span<T> 에 일관성 있는 fixed 구문을 제공됨

 

#힙에 할당된 고정 크기 배열의 인덱싱 개선

기본적으로 fixed를 이용한 고정 크기 배열을 정의한 구조체는 다음과 같이 사용 가능

unsafe struct CSharpStructType
{
    public fixed int fields[2];
    public fixed long dummy[3];
}

class Program
{
    unsafe static void Main(string[] args)
    {
        CSharpStructType item = new CSharpStructType();
        item.fields[0] = 5;
        int n = item.fields[2];
    }
}
//CSharpStructType은 값 형식이므로 item 인스턴스는 스택에 자리 잡게 되고 내부의 fixed 고정 크기
//배열에 대한 인덱싱은 직접 접근이 가능하다.

 

위와 같이 struct 값 형식으로 쓰이는 경우 말고 다른 클래스의 내부에서 정의된다면 값이 힙에 할당되기 때문에 indexing 하기 전에 fixed 해야 했는데, 7.3부터는 이런 경우에도 일관성 있게 fixed 없이 고정 크기 배열에 대한 인덱싱이 가능하다.

 

#초기화 식에서 변수 사용 가능

변수 선언은 문(statement)에 해당하기 때문에 식(expression)을 요구하는 코드에 사용할 수 없지만  out 변경자를 갖는 메서드의 호출이나 패턴 매칭에서의 변수명 선언이 발생하는 코드에서도 오류가 발생하여 표현의 제약을 가져올 수 있으므로 7.3부터 초기화식에 변수 선언이 가능해짐

//필드 초기화 식에서 변수 사용
private readonly bool _field = int.TryParse("5", out int result);

//속성 초기화 식에서 변수 사용
int Number { get; set; } = int.TryParse("5", out int result) ? 0 : -1;

//생성자의 초기화 식에서 변수 사용
public Derived(int i) : base(i, out var result) { ... }

//LINQ 쿼리 식에서 변수 사용
var query = from text in strings
            where int.TryParse(text, out int result)
            select text;

 

#자동 구현 속성의 특성 지원

//field 옵션을 제공해 자동 생성 필드의 특성을 지정할 수 있는 구문을 사용 가능함
[Serializable]
public class foo
{
    [field: NonSerialized] //자동 생성된 필드에 특성이 적용됨
    public string MySecret { get; set; }
}

//아래와 같이 컴파일 됨
[Serializable]
public class foo
{
    [NonSerialized]
    private string _mySecret; //자동 생성

    public string MySecret
    {
        get { return _mySecret; }
        set { _mySecret = value; }
    }
}

 

 

#튜플의 ==, != 연산자 지원

 

#ref 지역 변수의 재할당 가능

C# 7.0의 신규 문법인 12.2절 '반환값 및 로컬 변수에 ref 기능 추가(ref returns and locals)'에서 ref 로컬 변수인 경우 기존에는 다음과 같이 다른 변수를 재할당하는 것이 불가능했다.

class Program
{
    static void Main(string[] args)
    {
        int a = 5;
        ref int b = ref a; //a를 가리키는 ref 로컬 변수 b

        int c = 6;

        //C# 7.3부터 정상적으로 컴파일 됨
        b = ref c; //새롭게 변수 c에 대한 ref를 할당
    }
}

 

#stackalloc 배열의 초기화 구문 지원

스택을 이용한 값 형식 배열: stackalloc에서 다룬 스택 배열에 대한 초기화 구문을 지원함

class Program
{
    static unsafe void Main(string[] args)
    {
        int* pArray1 = stackalloc int[3] { 1, 2, 3 };
    }
}

//Span<T> 타입과 연동
Span<int> span1 = stackalloc int[3] { 1, 2, 3 };

 

 


C# 8.0

# #nullable 지시자와 nullable 참조 형식

++닷넷 7부터 새로 생성하는 프로젝트는 #enable의 기본 설정이 enable로 설정됨(null 가능성이 있는 경우 경고 발생)

 

닷넷 프로그램을 개발하면 종종 접하게 되는 예외인 System.NullReferenceException이다. 신규 문법의 목표가 컴파일러 수준에서 null 참조 예외가 없도록 보장시키는게 신규 문법의 목표다 (런타임이 아닌 컴파일 타임에서 처리)

 

#C# 컴파일러의 대응

  • Non-nullable reference의 경우, C# 컴파일러는 참조 타입을 정의할 때 null 값을 담는 멤버가 없도록 보장
  • Nullable reference type의 경우, C# 컴파일러는 참조 타입의 인스턴스를 사용할 때 반드시 null 체크를 하도록 보장

필드 값이 null을 허용해야 하는 경우 C# 컴파일러는 해당 인스턴스가 null일 수 있음을 알리는 'nullable reference type'을 정의 하는 방법을 새롭게 제공함

참조_타입?
설명: 참조_타입에 물음표(?)를 붙여 인스턴스가 null일 수도 있음을 명시한다.

 

널 가능 참조 타입을 선언하면 이제 컴파일러는 해당 멤버가 사용된 코드를 검사해 널 가능성이 있다면 컴파일 경고를 발생시킨다. 경고를 없애려면 null 체크 코드를 추가해야함

 

#NotNullWhen 특성

//NotNullWhen 특성의 생성자에 전달된 false에 따라 IsNull 메서드가
//false를 반환하면 null이라고 C# 컴파일러가 인지 컴파일 경고 발생 안시킴
static bool IsNull([NotNullWhen(false)] string? value)
{
    if(value == null)
    {
        return true;
    }

    return false;
}

 

#null 자체인 경우를 받아들여 부가적인 코드를 사용하기 싶지 않을 때 null 포기 연산자(null-forgiving operator) 를 사용할 수 도 있다. name!. (!. 붙임)

 

# #Nullable 활성화에 따라 C# 컴파일러는 모든 '참조 형식'을 다음의 두 가지 형식 중 하나로 취급함

  • Non-nullable reference type: 기본적으로 모든 참조 타입을 이것으로 취급하며 해당 타입의 인스턴스에는 null 초기화 및 null 대입을 할 수 없다.
  • Nullabe reference type: 예외적으로 null일 수 있는 인스턴스가 필요하다면 물음표(?)를 붙여 지정한다. 그렇게 되면 null을 대입할 수는 있지만 사용하기 전에 null 체크를 해야 하거나 명시적으로 null 접근임을 알고 있다는 표시로 null 포기 연산자(!.)를 쓴다

#컴파일러에 null 처리 관련 힌트를 부여하는 특성(pp791 - pp795)

  • [AllowNull]
  • [DisallowNull]
  • [DoesNotReturn]
  • [DoesNotReturnIf()]
  • [MaybeNull]
  • [MaybeNullWhen()]
  • [NotNullIfNotNull]

#널 가능(Nullable) 문맥 제어

nullable 관리는 주석 문맥(annotaition)과 경고 문맥(warning context)으로 나뉘어 적용됨

문맥 설정 설명
주석 문맥 enable 모든 참조 타입은 널이 가능하지 않은 참조 타입으로취급하고 따라서 null 예외 없이 안전하게 접근 가능
널 가능한 참조 타입(ex: string?)을 사용할 수 있고, 해당 변수에 접근할 때 정적 분석기에 의해 null일수 있다면 경고 발생
disable 널 가능한 참조 타입을 사용할 수 없음(참조 타입에 ?를 붙이면 경고 발생)
모든 참조 변수는 null일 수 있음
참조 변수를 접근해도 경고가 발생하지 않음(C#7.3 이전처럼)
경고문맥 enable null 접근으로 분서고딘 경우 경고를 발생시킨다. 주석 문맥의 활성화와 상관 없이 정적 분석기의 판정에 따른다
disable 경고를 발생시키지 않는다.

 

#비동기 스트림

IEnumerable/IEnumerator의 비동기 버전임

using System.Collections;

class Program
{
    static async Task Main(string[] args)
    {
        ObjectSequence seq = new ObjectSequence(10);

        //async를 지원하는 내부 코드에서 foreach를 쓰면
        //해당 열거에 한해 동기적으로 스레드를 점유함으로써 비동기 처리가 무색해짐
        foreach(object obj in seq)
        {
            Console.WriteLine(obj);
        }
    }
}

class ObjectSequence : IEnumerable
{
    int _count = 0;

    public ObjectSequence(int count)
    {
        _count = count;
    }

    public IEnumerator GetEnumerator()
    {
        return new ObjectSequenceEnumerator(_count);
    }

    class ObjectSequenceEnumerator : IEnumerator
    {
        int _i = 0;
        int _count = 0;

        public ObjectSequenceEnumerator(int count)
        {
            _count = count;
        }

        public object Current
        {
            get
            {
                Thread.Sleep(1000);
                return _i++;
            }
        }
        public bool MoveNext() => _i >= _count ? false : true;
        public void Reset() { }
    }
}

//비동기 스트림 적용
using System.Collections;

class Program
{
    static async Task Main(string[] args)
    {
        //foreach에 await 적용
        await foreach (var value in GenerateSequence(10))
        {
            Console.WriteLine($"{value} (tid: {Thread.CurrentThread.ManagedThreadId})");
        }

        Console.WriteLine($"Completed (tid: {Thread.CurrentThread.ManagedThreadId})");


        //아래와 같이 while문 이용도 가능
        var enumerator = GenerateSequence(10).GetAsyncEnumerator();

        try
        {
            while(await enumerator.MoveNextAsync())
            {
                int item = enumerator.Current;
                Console.WriteLine($"{item} (tid: {Thread.CurrentThread.ManagedThreadId})");
            }

            Console.WriteLine($"Completed (tid: {Thread.CurrentThread.ManagedThreadId})");
        }
        finally
        {
            await enumerator.DisposeAsync();
        }
    }

    //IEnumerable을 async로 바꿈
    public static async IAsyncEnumerable<int> GenerateSequence(int count)
    {
        for(int i = 0; i < count; i++)
        {
            //작업을 Task로 변경한 후 await를 호출
            await Task.Run(() => Thread.Sleep(100));
            yield return i;
        }
    }
}

 

#새로운 연산자 - 인덱스, 범위

# C# 8.0의 신규 연산자

연산자 문법 의미 닷넷 타입
^ ^n 인덱스 연산자로서 뒤에서부터 n번째 위치를 지정한다.(주의할 점은 일반적인 배열 인덱스가 0부터 시작하는 것과는 달리 인덱스 연산자는 마지막 위치를 1로 지정한다. System.Index
.. n1..n2 범위 연산자로서 시작 위치 n1은 포함하고 끝 위치 n2는 포함하지 않는 범위를 지정한다. 수학의 구간 기호로 표현하면 [n1, n2)와 같다.
n1 값이 생략되면 기본값 0
n2 값이 생략되면 기본값 ^0
System.Range

 

class Program
{
    static void Main(string[] args)
    {
        string txt = "hello";

        Console.WriteLine(txt[^1]); //o
        Console.WriteLine(txt[^3]); //l
        Console.WriteLine(txt[0..2]); //he [n1..n2)
        Console.WriteLine(txt[0..^0]); // == Range.All()
        Console.WriteLine(txt[..]); // == 0..0^

        int i = 5;
        System.Index firstWord = ^i;
        Console.WriteLine(txt[firstWord]); //h

        System.Index firstWord2 = new Index(0, false); //두 번째 인자 -> fromEnd
        Console.WriteLine(txt[firstWord2]); //h
    }
}

 

#간결해진 using 선언

class Program
{
    static void Main(string[] args)
    {
        //블럭 생략해서 사용가능
        using var file = new System.IO.StreamReader("test.txt");

        string txt = file.ReadToEnd();
        Console.WriteLine(txt);
    } //using에 사용된 변수 선을 기준으로 가장 가까운 바깥 블록이 간략화된 using 블럭의 끝이 됨
}

 

#Dispose 호출이 가능한 ref struct

C# 7.2  스택에만 생성할 수 있는 값 타입 지원 - ref struct는 특성으로 인해 인터페이스를 구현할 수 없고 이로 인해 using 문도 사용할 수 없었지만 C# 8.0에서는 특별히 ref struct 타입에 한해서만 public void Dispose() 메서드를 포함한 경우 using 문에서 사용할 수 있도록 허용한다.

더보기

Marshal 클래스의 이름은 "마샬링(marshaling)"이라는 개념에서 유래되었습니다. 마샬링은 컴퓨팅과 소프트웨어 개발에서 데이터 변환 및 전송 과정을 의미하는 용어입니다.

마샬링(Marshaling)이란?

마샬링은 주로 다음과 같은 두 가지 경우에 사용됩니다:

  1. 데이터 변환: 한 형태의 데이터 구조를 다른 형태로 변환하는 과정입니다. 예를 들어, 관리되는 환경에서 관리되지 않는 환경으로 데이터를 변환하거나 그 반대로 변환하는 경우가 있습니다.
  2. 데이터 전송: 프로세스 간 또는 네트워크를 통해 데이터를 전송하는 과정입니다. 예를 들어, 메모리 내의 구조체를 바이트 배열로 변환하여 네트워크를 통해 전송하고, 수신 측에서 다시 원래의 구조체로 변환하는 과정이 포함됩니다.

Marshal 클래스의 역할

.NET의 Marshal 클래스는 주로 관리되는 코드와 관리되지 않는 코드 간의 데이터 변환 및 전송을 위한 기능을 제공합니다. 이러한 기능에는 다음이 포함됩니다:

  • 관리되는 객체를 포인터로 변환하고 그 반대로 변환
  • 관리되지 않는 메모리를 할당하고 해제
  • 데이터 구조를 다른 형태로 변환
  • COM 인터페이스와의 상호 운용 지원
using System.Runtime.InteropServices;

class Program
{
    static void Main(string[] args)
    {
        //using 문 사용가능
        using (UnmanagedVector v1 = new UnmanagedVector(500.0f, 600.0f))
        {
            Console.WriteLine(v1.X);
            Console.WriteLine(v1.Y);
        }

    }

    ref struct UnmanagedVector
    {
        IntPtr _alloc;

        public UnmanagedVector(float x, float y)
        {
            _alloc = Marshal.AllocCoTaskMem(sizeof(float) * 2);

            this.X = x;
            this.Y = y;
        }

        public unsafe float X
        {
            get
            {
                return *((float*) _alloc.ToPointer());
            }
            set
            {
                *((float*)_alloc.ToPointer()) = value;
            }
        }

        public unsafe float Y
        {
            get
            {
                return *((float*)_alloc.ToPointer() + 1);
            }
            set
            {
                *((float*)_alloc.ToPointer() + 1) = value;
            }
        }
        public void Dispose()
        {
            if (_alloc == IntPtr.Zero) return;

            Marshal.FreeCoTaskMem(_alloc);
            _alloc = IntPtr.Zero;
        }
    }
}

 

#정적 로컬 함수

Local functions는 static 함수를 구현할 수 없었지만 C# 8.0부터 그 제약이 풀림 static으로 로컬 함수를 정의할 경우 외부 변수를 명시적으로 인자를 통해 받는 것으로 처리해야 함

 

#패턴 매칭 개선

#switch 식

(인스턴스) switch
{
    패턴_매칭_식1 => 식1, 
    패턴_매칭_식2 => 식2, 
    패턴_매칭_식n => 식n, 
    _ => 식,
};

설명: 실행 시 결정되는 인스턴스의 값과 패턴_매칭_식 결괏값이 일치하는 경우 해당 식을 실행한다.
나열된 패턴_매칭_식에 일치하는 값이 없다면 '_'에 지정한 식을 실행한다.(switch 문이 아닌 switch식임을 기억)

 

public static bool Even(int n)
{
    //swtich 문 구현
    //switch (n)
    //{
    //    case int i when (i % 2) == 0: return true;
    //    default: return false;
    //}

    //switch 식 구현
    return n switch
    {
        var x when (x % 2) == 1 => false,
        _ => true
    };
}

//식이라서 아래처럼 축약 가능
public static bool Even1(int n) => n switch { var x when (x % 2) == 1 => false, _ => true };
public static bool Even2(int n) => (n % 2) switch { 1 => false, _ => true };

 

#속성 패턴

class Program
{
    static void Main(string[] args)
    {
        point x = new point();
        x.x = 1; x.y = 0;

        Func<point, bool> example = x =>
        {
            switch (x)
            {
                //x > 10 or x is even -> true
                case var pt1 when x.x == 0:
                case var pt2 when x.y == 0:
                    return true;
            }
            return false;
        };

        //속성 패턴을 이용해서 간결하게 가능
        Func<point, bool> example1 = x =>
        {
            switch (x)
            {
                case { x: 0 }:
                case { y: 0 }:
                    return true;
            }
            return false;
        };

        Func<point, bool> example2 = x =>
            x switch
            {
                { x: 0 } => true,
                { y: 0 } => true,
                _ => false,
            };

        Console.WriteLine(example(x).ToString());
    }

    public class point
    {
        public int x { get; set; } = 0;
        public int y { get; set; } = 0;
    }
}

//속성 패턴을 사용해 is 연산자에 적용가능
//if(pt is point when pt.x == 500) { ... } 불가
if(x is point { x: 100 })
{
    Console.WriteLine(x.x.ToString());
}

 

#튜플 패턴

튜플도 자동 구현된 속성을 기반으로 하므로 속성 패턴을 사용해 유사하게 패턴 매칭을 할 수 있다.

Func<(int, int), bool> detectZeroOR = (arg) =>
    (arg) switch
    {
        { Item1: 0 } => true,
        { Item2: 0 } => true,
        _ => false,
    };
    
//C# 8.0 에서는 튜플을 위한 간편 패턴 매칭을 지원
Func<(int, int), bool> detectZeroOR = (arg) =>
    (arg) switch
    {
        (0, _) => true,
        (_, 0) => true,
        _ => false,
    };
    
Func<(int, int), bool> detectZeroOR = (arg) =>
    (arg) switch
    {
        (var X, var Y) when X == 0 || Y == 0 => true,
        _ => false,
    };
    
//튜플 패턴 역시 when 조건을 제외하고는 속성 패턴과 마찬가지로 is 연산자에서 사용 가능
Func<(int, int), bool> detectZeroOR12 = (arg) => arg is (0, _) || arg is (_, 0);

 

 

#위치 패턴

튜플이 아닌 타입도 원하는 속성으로 튜플을 임시로 구성하면 튜플 패턴으로 쉽게 다루는 것이 가능하다.

//즉석에서 튜플 생성
Func<(int, int), bool> detectZeroOR = (pt) =>
    (pt.x, pt.y) switch
    {
        (0, _) => true,
        (_, 0) => true,
        _ => false,
    };

//Deconstruct 메서드 구현 이용
public void Deconstruct(out int x, out int y) => (x, y) = (x, y);

//이 조건을 만족하는 타입이라면 C# 8.0 컴파일러는 튜플 패턴과 동일한 구문을 허용한다.
Func<(int, int), bool> detectZeroOR = (pt) =>
    pt switch
    {
        (0, _) => true,
        (_, 0) => true,
        _ => false,
    };
    
//위치 패턴도 마찬가지로 is 연산자에서 사용할 수 있다.
bool zeroDetected = pt is (0, _) || pt is (_, 0);

 

 

#재귀 패턴

사용자 정의 타입을 포함한 경우 속성/튜플/위치 패턴 매칭에 대해 재귀적으로 구성하는 것이 가능하다

//이렇게 길어진 코드를    
    static MatrixType GetMatrixType(Matrix2x2 mat)
    {
        switch (mat)
        {
            case Matrix2x2 m when m.V1.X == 0
                && m.V1.Y == 0
                && m.V2.X == 0
                && m.V2.Y == 0:
                return MatrixType.Zero;

            case Matrix2x2 m when m.V1.X == 0 && m.V1.Y == 0:
                return MatrixType.Row1Zero;

            default: return MatrixType.Any;
        }
    }

    //속성 패턴을 다음과 같이 재귀적으로 구현 가능
    static MatrixType GetMatrixType2(Matrix2x2 mat)
    {
        switch (mat)
        {
            case { V1: { X: 0, Y: 0 }, V2: { X: 0, Y: 0 } }:
                return MatrixType.Zero;

            case { V1: { X: 0, Y: 0 }, V2: _ }:
                return MatrixType.Row1Zero;

            default: return MatrixType.Any;
        }
    }
    
//재귀 패턴 역시 is 연산자에 동일하게 적용할 수 있다.
if(mat is ((0, 0), (0, 0))) { ... } //Matrix 타입에 Deconstruct 정의 필요

 

#기본 인터페이스 메서드

인터페이스의 메서드에 구현 코드를 추가하는 것이 가능해졌다. (자바에서는 인터페이스의 디폴트 메서드, 다른 언어들에서는 trait이라는 문법과 유사하다.)

 

프로퍼티, 인덱서, 이벤트의 get/set도 모두 내부적으로는 결국 메서드의 구현이므로 당연히 인터페이스에 정의 가능하고, 정적 멤버의 경우에는 메서드와 필드까지 포함 가능하다.

 

상속받은 클래스에서 기본 인터페이스 메서드를 구현하지 않았다면 그 메서드는 반드시 인터페이스로 형 변환해 호출해야만함 -> 다이아몬드 문제 해결

 

기존에는 계약을 정의하면서 메서드 구현이 필요한 경우 추상클래스를 만들어야 했지만 이제는 어느정도 기존 추상 클래스로 정의된 것들을 인터페이스로 바꾸는 것이 가능하다.

 

  다중 상속 상태 필드 정의 메서드 구현
추상 클래스 X O O
기본 인터페이스 메서드 O X O

 

상태 필드 정의( 객체의 상태를 나타내는 데이터 멤버(필드)는 불가, static 필드 가능

 

#??= (널 병합 할당 연산자)

#??= 연산자

변수 ??= 기본값
설명: 참조 객체인 변수의 값이 null이면 기본값을 변수에 대입한다.

txt = txt ?? "기본값"; //txt가 null이면 기본값 아니면 txt 반환
txt ??= "test"; //?? 연산자의 복합 대입 사용법과 동작이 같음

 

 

#문자열 @, $ 접두사 혼합 지원

@ 접두사와 혼용하는 경우  $@ @$(8.0)

string path = $@"{path}\file.log";

 

 

#기본 식(primary expression)으로 바뀐 stackalloc

stackalloc에서 살펴본 stackalloc 예약어는 초기화 구문을 지원하기까지 여전히 그것의 문법적인 지위는 선언문(declaration statement)의 하나로서 지역 변수를 초기화하는 구문에 한정돼 있었다. 객체 생성을 위한 new 구문과 유사하지만 스택 공간만 점유한다는 특성이 있다. ( 초기화 구문은 지역 변수의 초기화에 한정됩니다. 즉, 메서드나 블록 내에서만 사용할 수 있으며, 메서드가 끝나면 할당된 메모리는 자동으로 해제됩니다. )

C# 8.0부터는 stackalloc을 문법적으로 아예 식(expression)의 위치로 변경됐기 때문에 더 자유롭게 사용 가능

//아래와 같이도 사용 가능
int length = (stackalloc int [] { 1, 2, 3 }).Length;

if(stackalloc int[10] == stackalloc int[10]) { }

 

 

#제네릭 구조체의 unmanaged 지원

제네릭 구조체의 경우 내부 필드가 참조 객체를 포함하는지 여부와 상관없이 기존에는 포인터 관련 연산을 지원하지 않았다. C# 8.0부터는 제네릭 구조체 중에서도 unmanaged 제약의 형식 매개변수를 필드로 가진 경우 다른 참조 형식의 필드가 없다면 정상적으로 사용 가능해짐

 

Span 타입과 unmanaged 제약을 조합하면 관리 힙에 두담을 주지 않는 배열을 생성해 다룰 수 있다.

 

public unsafe ref struct NaiveMemory<T> where T : unmanaged
{
    int _size;
    IntPtr _ptr;

    public NaiveMemory(int size)
    {
        _size = size;
        _ptr = Marshal.AllocHGlobal(size * sizeof(T));
    }

    public Span<T> GetView()
    {
        return new Span<T>(_ptr.ToPointer(), _size);
    }

    public void Dispose()
    {
        if (_ptr == IntPtr.Zero)
        {
            return;
        }

        Marshal.FreeHGlobal(_ptr);
        _ptr = IntPtr.Zero;
    }
}

 

위의 코드는 메모리를 닷넷이 관리하는 GC 힙이 아닌, 운영체제로부터 직접 메모리를 할당받고 있어 GC에 아무런 부하를 주지 않는다. 또한 GetView 메서드에서 Span 뷰를 반환하고 있으므로 다음 코드와 같이 자유롭게 네이티브 메모리를 값 형식의 배열로 안전하게 다룰 수 있다.

 

//아래 코드에서 GC 발생 안함
while(true)
{
    //비-관리 메모리로부터 int[1024] 공간만큼 할당받아 사용
    using(NaiveMemory<int> buf = new NaiveMemory<int>(1024))
    {
        Span<int> viewBuf = buf.GetView();
        for(int i = 0; i < viewBuf.Length; i++)
        {
            viewBuf[i] = i;
        }
    }
}

 

극단적인 성능 요구 환경에서 사용 가능

byte[] buffer = ArrayPool<byte>.Shared.Rent(1024); 공유 풀을 이용한 재사용

 

#구조체의 읽기 전용 메서드

타입 자체를 readonly struct로 바꾸지 않아도 메서드에 readonly를 사용하는 것으로 방어 복사본 생성 안할 수 있음

//메서드에 readonly를 적용하면 방어 복사본을 생성하지 않음 프로퍼티나 람다식 정의 메서드에도 사용가능
public readonly (float x, float y) ToTuple()
{
    return (x, y);
}

 

 


C# 9.0

#레코드(Records)

Equals, GetHashCode, ==, !=, ToString() 같은 기본적인 메서드를 매 클래스마다 재정의하는 것은 번거로운 일이므로 이를 지원하는 문법이 추가됨

 

record -> class + '기본 생성 코드'이다. 그외 모든 기능은 class와 동일한 기준을 따름

 

#init 설정자 추가

값을 담는 타입에 불변(immutable) 처리를 struct 의 경우 readonly struct로 처리했다면 class의 경우에 init 설정자(setter)를 사용하는 식으로 구현 가능

//private set으로 구현된 필드를 외부에서 접근해 초기화할 수 없으므로 반드시 이를 위한 생성자를
//정의해야 하는 제약을 init을 사용하면 간결하게 구현 가능함
public class Point
{
    public int X { get; init; }
    public int Y { get; init; }
}

//별도로 생성자를 정의하지 않아도 프로퍼티에 값 설정이 가능하면서도 이후 불변 개체로써
//동작할 것을 C# 컴파일러에 의해 보장받게 된다.

//이 코드는 아래와 같이 컴파일 됨
public record Point(int X, int Y) { }

public class Point
{
    public int X { get; init; }
    public int Y { get; init; }

    public Point(int x, int y) => (X, Y) = (x, y);

    //Equals, GetHashCode, ==, !=, ToString 구현 코드 생략
}

/*
 * Point pt1 = new Point(5, 6); //생성자가 제공되므로
 * (int x, int y) = pt1; //Deconstruct가 제공되므로
 */

 

이처럼 개발자가 필드 초기화를 위한 생성자와 튜플과의 연동을 위한 Deconstruct 메서드도 만들 필요가 없어졌다. 게다가 원한다면 생성자 및 기타 메서드를 정의하는 것도 가능하다. init 메서드도 set 메서드의 역할을 하므로 블록을 사용해 원하는 코드를 추가할 수 있다.

 

#with 초기화 구문 추가

개별 속성만 변경할 때 편의성을 제공 record 타입의 인스턴스에 대해서만 with 예약어를 이용해 속성을 초기화할 수 있는 구문이 제공됨

Point pt1 = new Point(5, 10);

// record로 정의한 타입의 인스턴스인 경우에만 허용
// pt1 인스턴스의 기존 값에서 Y만 변경한 새로운 인스턴스를 반환
Point pt2 = pt1 with { Y = pt1.Y + 2 };

 

내부적으로 C# 컴파일러는 record의 with 초기화 구문을 위해 특별히 public 접근 제한자가 적용됐음에도 불구하고 오직 C# 컴파일러만 사용할 수 있는 Clone 메서드를 protected 생성자와 함께 제공함

 

#대상에 따라 new 식 추론(Target-typed new expressions)

//C# 2.0 이하 - 타입을 모두 지정
Point pt1 = new Point(5, 6);

//C# 3.0 이상 - new의 대상 타입을 추론해 var 결정
var pt2 = new Point(5, 6);

//C# 9.0 - 변수의 타입에 따라 new 연산자가 타입을 결정
Point pt3 = new(5, 6)

var dict = new Dictionary<Point, bool>()
{
    [new(5, 6)] = true;
    [new(7, 3)] = false;
    [new() { X = 3, Y =2 }] = false;
};

 

#달라진 조건식 평가

조건 연산자(?:)는 형식 안정성을 위해 2항과 3항의 타입 중 어느 하나는 다른 항에 암시적 형 변환이 가능해야 한다는 제약이 있다.(_ ? A : B; 에서 A와 B는 서로 암시적으로 형 변환이 가능해야 함) 하지만 C# 9.0부터 대상 타입으로의 추론이 적용되면서 형 변환 없이 컴파일이 가능해졌다.

 

//C# 9.0부터 string과 int의 대상 타입인 object로 암시적 형 변환이 가능하므로 허용
object retValue = (arg.Length == 0) ? "(empty)" : 1;

 

#로컬 함수에 특성 지정 가능(Attributes on local functions)

C# 7.0에 처음 도입된 로컬 함수에 이제 특성(attribute)도 부여할 수 있게 됐다.

 

using System.Runtime.InteropServices;

class Program
{
    static void Main(string[] args)
    {
        MessageBox(IntPtr.Zero, "message", "title", 0);

        //특성 부여가 가능해져 extern P/Invoke 정의를 로컬 함수로도 가능
        [DllImport("User32.dll", CharSet = CharSet.Unicode)]
        static extern int MessageBox(IntPtr h, string m, string c, int type);
    }
}

 

 

#익명 함수 개선

#정적 익명 함수(static anonymous functions)

기존의 익명 메서드 및 정적 메서드를 정적으로 정의하는 것이 가능해짐

 

class Program
{
    static void Main(string[] args)
    {
        string text1 = "rara";
        const string text2 = "rara";

        Func<string, string> func = static i =>
        {
            //return text1 + i.ToString(); 정적 메서드여서 text1 접근 불가
            return text2 + i.ToString(); //const 변수는 참조에 대한 부작용이 없어서 사용 가능
        };

        Console.WriteLine(func(" in pripara"));
    }
}

 

#익명 함수의 매개변수 무시

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.0)
로컬 함수(C# 7.0) X O (C# 8.0) O (C# 9.0)

 

#최상위 문(Top-level statements)

별도의 타입 및 메서드를 정의하지 않고 입력한 코드는 말 그대로 최상위 문의 위치에 놓여 컴파일이 가능하다. C# 컴파일러 측에서 자동으로 임시 타입과 Main 메서드를 만들어 최상위 문에 해당하는 코드를 넣어 처리하는 식으로 동작한다.

 

int argLen = args.Length; // 최상위 문에서도 동일하게 명령행 인자(Main(string[] args)에 접근가능

 

#패턴 매칭 개선(Pattern matching enhancements)

  • 패턴에 타입명만 지정 가능
  • 기본타입에 한해 상수 값에 대한 <, <=, >, >= 관계 연산 가능
  • 논리 연산자(not and or)를 이용해 패턴 조합 가능
//변수명 생략
switch(objValue)
{
    case int: break;
    case System.String: break;
}

//관계 연산
static bool GreaterThan10(int number) => number is > 10; //is 패턴

static bool GreaterThan10(int number) => //switch 패턴
    number switch
    {
        > 10 => true,
        _ => false
    };
    

static bool GreaterThanTarget(int number, int target) => //switch 패턴(상수 비교 x)
    number switch
    {
        //상수 제약에 걸려 불가능한 표현은 기존 when 사용
        int value when value > target => true,
        _ => false
    };
    
//C# 9.0의 not null 조건 테스트
if (objValue is not null) { ... }

static bool IsLetter(char c) =>
    c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');

 

#모듈 이니셜라이저(Module initializers)

C# 언어 수준에서 제공하지 않아 잘 알려져 있지 않은 닷넷 런타임의 기능으로 <Module>이라는 특별한 타입이 있는데, CLR은 닷넷 모듈을 로드한 후 그것에 저으이된 정적 생성자를 호출해 주는데, 이로 인해 사용자 코드에서 일부로 호출하지 않아도 자동으로 실행되는 코드를 개발자가 정의하는 것이 가능하다.

 

C# 9.0부터 이에 대한 제어가 추가됐는데, 개발자는 단지 ModuleInitializer라는 이름의 특성을 정적 메서드에 부여하면 C# 컴파일러는 해당 메서드를 <Module> 타입의 정적 생성자에서 호출하는 코드를 넣어 컴파일 한다.

 

#moduleInitializer 특성의 대상이 될 메서드에는 다음과 같은 제약사항이 있음

  • 반드시 static 메서드이고
  • 반환 타입은 void, 매개변수는 없어야 하며
  • 제네릭 유형은 허용되지 않고
  • <Module> 타입에서 호출이 가능해야 하므로 internal 또는 public 접근 제한자만 허용

또한 어떤 순서로 호출될지 제어할 수 없으므로 실행 순서에 의존하는 코드를 작성해서는 안됨

 

#공변 반환 형식(Convariant return types)

C# 언어에서는 메서드를 이름과 매개변수의 타입으로 구분한다. 즉, 반환 타입을 포함하지 않으므로 반환 타입만 다른 메서드에 대해 중복 정의가 불가능하다.

public class TestClass
{
    public short MyMethod(int count) { return 0; }
    public int   MyMethod(int count) { return 0; } //컴파일 에러
}

class Program
{
    static void Main(string[] args)
    {

    }

    public class Product
    {
        public virtual Product GetDevice() { return this; }
    }

    public class Headset : Product
    {
        //C# 9.0 + .NET 5 이상의 환경에서 컴파일 가능
        //C# 9.0부터 반환 타입이 상속 관계의 하위 타입인 경우에 한해 사용할 수 있게 했고,
        //이를 좀 더 기술적으로 'C# 메서드의 반환 타입에 공변을 허용한다'는 용어로 표현한다.
        public override Headset GetDevice()
        {
            return this;
        }
    }
}

 

.NET Framework: 408. 자바와 닷넷의 제네릭 차이점 - 중간 언어 및 공변/반공변 처리 (sysnet.pe.kr)

.NET Framework: 743. C# 언어의 공변성과 반공변성 (sysnet.pe.kr)

[C# 4.0] New Features in C# : 07. 공변성과 반공변성(Covariance and Contravariance) (tobegin.net)

 

상속받은 하위 타입에 virtual 메서드를 재정의(override)하면서 반환 타입이 정확히 일치하지 않아 C# 8.0까지 컴파일 불가능했다. C# 9.0부터 반환 타입이 상속 관계의 하위 타입인 경우에 한해 사용할 수 있게 되었다.

예제와 같은 상속 관계를 기준으로 Headset -> Product 타입으로의 변환을 허용하는 것을 공변(Covariance), 그 반대의 경우를 반공변(Contravariance)라고 한다. 이러한 변경 가능을 일컬어 가변(Variance)이라고 한다. (반대의 의미는 불변(Invariance))

 

# 공변성은 제네릭 타입의 매개변수가 상위 타입으로 변환될 수 있는 성질을 말합니다. 즉, 하위 타입의 제네릭 객체를 상위 타입의 제네릭 객체로 사용할 수 있는 것을 의미

 

namespace System.Collections.Generic
{
    //out 키워드가 적용된 제네릭 형식 매개변수는 메서드의 반환 타입에서만 사용될 수 있다.
    //입력 매개변수로 사용 불가함 -> 안전하지 않는 변환이 발생할 수 있기 때문이다.
    //out 키워드는 제네릭 형식 매개변수가 공변적임을 나타냄
    public interface IEnumerable<out T> : IEnumerable
    {
        IEnumerator<T> GetEnumerator();
    }
}

//in 키워드는 제네릭 형식 매개변수에 대해 반공변성을 나타냄
//상위 타입을 하위 타입으로 변환할 수 있도록 허용함
//반공변성은 주로 입력 매개변수로만 사용되는 제네릭 형식 매개변수에 적용된다.
public interface IComparer<in T>
{
    int Compare(T x, T y); //T를 상위 타입의 비교기를 하위 타입의 비교기로 사용할 수있음
}

public class Animal { }
public class Dog : Animal { }

public class AnimalComparer : IComparer<Animal>
{
    public int Compare(Animal x, Animal y)
    {
        return x.GetHashCode().CompareTo(y.GetHashCode());
    }
}

class Program
{
    static void Main()
    {
        IComparer<Dog> dogComparer = new AnimalComparer();

        Dog dog1 = new Dog();
        Dog dog2 = new Dog();

        int result = dogComparer.Compare(dog1, dog2);
        Console.WriteLine(result);
    }
}

 

공변성이 가능한 이유 -> 하위 타입에서 상위 타입으로의 암시적 변환이 안전하기 때문이다.

 

반공변성이 가능한 이유 -> 상위 타입을 받아들이는 메서드나 델리게이트를 하위 타입에서도 사용할 수 있음

 

모두 다형성에 기반함

 

[A ≼ B일 때 인자 타입에 대해]
    반공변이면,
        (B -> T) ≼ (A -> T)
    공변이면,
        (A -> T) ≼ (B -> T)

[A ≼ B일 때 반환 타입에 대해]
    반공변이면,
        (T -> B) ≼ (T -> A)
    공변이면,
        (T -> A) ≼ (T -> B)

 

[A ≼ B일 때 인자 타입에 대해 반공변]
        (B -> T) ≼ (A -> T)

[A ≼ B일 때 반환 타입에 대해 공변]
        (T -> A) ≼ (T -> B)

 

#foreach 루프에 대한 GetEnumerator 확장 메서드 지원(Extension GetEnumerator)

foreach 구문을 사용하기 위해 반드시 IEnumerable을 구현할 필요는 없다. 엄밀히 말하면 foreac에 열거 가능한 개체는 GetEnumerator 라는 이름의 메서드를 구현하기만 하면 되고, 그 메서드가 반환하는 타입은 Current { get; }, MoveNext 메서드만 가지고 있으면 된다. 

 

C# 9.0부터 외부 개발자가 해당 타입의 소스코드를 변경하지 않고 단지 GetEnumerator 확장 메서드를 제공하면 foreach에 사용하도록 만들 수 있다.

 

//기존 구현
public class Program
{
    static void Main(string[] args)
    {
        Pripara pripara = new Pripara();

        foreach(var member in pripara)
        {
            Console.WriteLine(member.Name);
        }
    }
}


public class Pripara
{
    List<Member> members;

    public Pripara()
    {
        members = new List<Member>
        {
            new Member() { Name = "rara" },
            new Member() { Name = "mirei" },
            new Member() { Name = "sophi" }
        };
    }

    public Member[] GetMembers() => members.ToArray();

    public MemberList GetEnumerator()
    {
        return new MemberList(this);
    }

    public class MemberList
    {
        Member[] _members;
        public int _current = -1;

        public MemberList(Pripara pripara)
        {
            _members = pripara.GetMembers();
        }

        public Member Current
        {
            get
            {
                return _members[_current];
            }
        }

        public bool MoveNext()
        {
            if(_current >= _members.Length - 1)
            {
                return false;
            }
            _current++;
            return true;
        }
    }
}

// C#9.0
Notebook notebook = new Notebook();

foreach(Device device in notebook) // 확장 메서드를 받아 들여 열거
{    
    System.Console.WriteLine(device.name);
}

public static class NotebookExtension
{
    // 외부 개발자가 GetEnumerator 확장 메서드를 제공
    public static IEnumerator<Device> GetEnumerator(this Notebook Instance)
    {
        foreach(Device device in instance.GetDevices())
        {
            yield return device;
        }
    }
}

 

#부분 메서드에 대한 새로운 기능(New features for partial methods)

C# 3.0 partial method가 가진 주요 제약 3가지가 모두 풀렸다.

  • 반환 타입 허용
  • out 매개변수 허용
  • (암시적으로 private이지만) 명시적으로 private을 포함한 접근 제한자 허용

자동 코드 생성

 

위와 같은 제약을 허용해서 사용하면 구현부를 생략할 수 없다 -> 자동 생성 코드를 제공하는 측에서 그 코드를 사용하는 측으로 하여금 특정 코드를 반드시 구현하도록 강제할 수 있는 효과를 갖는다.

 

#localsinit 플래그 내보내기 무시(Suppress emitting localsinit flag)

C# 컴파일러는 기본적으로 모든 로컬 변수의 공간을 사용 여부에 상관없이 0으로 초기화하도록 내부 코드를 생성한다. 변수의 초기화를 생략하고 싶은 메서드에 SkipLocalsInitAttribute 특성을 unsafe와 함께 적용하면 된다.

 

일반적으로 변수 몇 개의 초기화를 생략하는 것이 성능에 큰 영향을 주지는 않지만 stackalloc에 적용되면 요소의 수와 해당 메서드의 호출 횟수에 따라 성능을 높일 여지가 충분하다.

 

[SkipLocalsInitAttribute]
unsafe static void LocalsInitStackAlloc()
{
    var arr = stackalloc int[1000];

    for (int i = 0; i < 1000; i++)
    {
        // C# 8.0 이전에는 0을 출력하지만 9.0부터 값을 예측할 수 없음
        Console.WriteLine($"{arr[i]}, ");
    }
}

 

#원시 크기 정수(Naive ints)

C/C++ 호환성을 위해 제공됨

32비트 환경에서는 4바이트, 64비트 환경에서는 8바이트로 동작하는 nint, nuint 정수 타입이 새롭게 추가됨

 

#함수 포인터(Function pointers)

함수포인터를 대체하는 델리게이트는 원본 메서드의 실행에 앞서 델리게이트를 경유한다는 점에서 성능상 좋지 않다. C# 9.0부터 대상 메서드를 바로 호출해 성능을 높일 수 있는 새로운 함수 포인터 구문을 unsafe 문맥으로 제공한다.

 

public class Program
{
    unsafe static void Main(string[] args)
    {
        //관리 메서드의 경우 delegate* managed<T> ... managed 생략됨
        //비관리 메서드의 경우 unmanaged 붙여야 함
        delegate*<int, int, bool> equals = &Program.Equals;
        Console.WriteLine(equals(1, 2));
    }
    static bool Equals(int x, int y) => x == y;
}

 

델리게이트와 함수 포인터의 차이점은 형식 안전성에 있다. 델리게이트는 반드시 대상 메서드가 델리게이트의 타입이 맞는지 검사할 수 있지만 함수 포인터는 이에 대한 검증을 할 수 없는 유형을 제공하므로 개발자가 안정성을 책임져야 한다.

 

.NET Framework: 634. C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (1) - x86 환경에서의 __cdecl, __stdcall에 대한 Name mangling (sysnet.pe.kr)

.NET Framework: 637. C# 개발자를 위한 Win32 DLL export 함수의 호출 규약 (3) - x64 환경의 __fastcall과 Name mangling (sysnet.pe.kr)

 

#비관리 함수 포인터는 32비트 환경에서의 호출 규약에 맞춰 4가지 유형을 지원함(64비트 환경에서는 호출 규약이 fastcall 하나로 통일됨)

//System.Runtime.CompilerServices.CallConvStdcall
delegate* unmanaged[Stdcall]<void> ptr1 = null;

//System.Runtime.CompilerServices.CallConvCdecl
delegate* unmanaged[Cdecl]<void> ptr2 = null;

//System.Runtime.CompilerServices.CallConvFastcall
delegate* unmanaged[Fastcall]<void> ptr3 = null;

//System.Runtime.CompilerServices.CallConvThiscall
delegate* unmanaged[Thiscall]<void>ptr4 = null;

 

실제 호출시에는 stdcall, cdecl, thiscall만 가능하다. 닷넷 5 환경부터는 호출 규약을 생략할 수 있고, 그런 경우에는 실행 중인 런타임이 정해주는 기본 호출 규약을 따르게 된다.

 

비관리 메모리를 사용할 때 delegate를 사용하면 new 구문이 축약된 간편 표현식이여서 의도치 않은 GC 발생으로 프로그램이 종료될 수 있음 -> 함수포인터 용

 

#제약 조건이 없는 형식 매개변수 주석(Unconstrained type parameter annotations)

여기서 주석은 nullable 형식의 ? 표시를 의미함

 

#nullable enable

//제약을 지정하지 않은 경우 where T class 를 생략한 것으로 지원을 추가함
static void CreateArray<T>(int n) /* where T : class */
{
    // C# 9.0 - nullable 문맥이 아닌 경우 컴파일 경고만 발생
    var t = new T?[n];
}

static void CreateArray2<T>(int n) /* where T : class */
{
    // C# 9.0 - nullable 문맥인 경우 경고 없이 컴파일
    var t = new T?[n];
}

 

C# 8.0에서는 하위 클래스의 경우 "제약 조건이 없는 형식 매개변수 주석"을 허용했다 단 9.0과는 다르게 where T : struct로 연결되었다. C# 9.0에서는 하위호환성을 지키기 위해 하위 클래스의 제약 조건이 없는 형식 매개변수 주석은 where T : class가 아닌 where T : struct로 유지하고 결정했다. 하위 클래스에서 제약 조건이 없는 메서드를 재정의하고 싶다면 where T class를 대신하는 where T default 제약을 사용해야 한다.

 


C# 10.0

#레코드 개선

#레코드 구조체(Record structs)

record struct, record class

 

#class 타입의 record에 ToString 메서드의 sealed 지원

++)record를 정의하면 C# 컴파일러는 자동으로 System.Object 클래스의 멤버 중 Equals, GetHash-Code, ToString 메서드에 대한 기본 코드를 제공하며, 원한다면 그중에서 ToString과 GetHashCode 메서드에 한해 사용자 측에서 재정의할 수 있다. 그리고 C# 10부터는 상속 클래스에서의 재정의를 막는 sealed 예약어를 ToString 메서드에 적용하는 것이 가능해졌다.

#구조체 개선(record에 한해 사용하기를 권장)

  • 기본 생성자 지원
  • 필드 초기화 지원

구조체의 경우 클래스와 다른 점이 있다면 필드 초기화를 사용하기 위해 반드시 1개 이상의 생성자를 정의해야만 한다는 규칙이 있다.(필드 초기화가 생성자를 통해 이뤄짐)

 

#네임스페이스 개선

#전역 using 지시문(Global Using Directive)

//같은 프로젝트 내의 다른 소스코드 파일에서 Linq 관련 확장 메서드를
//별다른 네임스페이스 선언 없이도 사용 가능해짐
global using System.Linq;

 

#파일 범위 네임스페이스(File Scoped Namespaces)

네임스페이스의 중괄호를 생략하는 대신 기본적으로 해당 파일에 정의한 전체 타입에 적용됨

 

#보간된 상수 문자열(Constant Interpolated Strings)

const float PI =3.14f;
const string output = $"nameof{PI} == {PI}"; //컴파일 오류
//내부적으로 PI.ToString() 메서드를 호출하고, 그 결과가 프로그램 실행 시점의 스레드에
//설정된 System.Threading.Thread.CurrentThread.CurrentCulture 값에 따라 달라질 수 있으므로
//컴파일 시점에 구할 수 없는 명백한 제약이 있음

 

#확장 속성 패턴(Extended property patterns)

타입이 중첩된 경우 내부 인스턴스가 가진 필드에 대한 패턴 매칭을 좀 더 간편하게 지정할 수 있는 문법이 추가됨

Name n1 = new Name("manaka", "rara");
Name n2 = new Name("minami", "mirei");
Name n3 = new Name("hojo", "sophi");

Name[] soramiSmile = { n1, n2, n3 };

foreach (Name name in soramiSmile)
{
    if (name is { LastName: "rara" } Rara)
    {
        Console.WriteLine($"{nameof(Rara)} == {name.FirstName}");
    }
}

record class Name(string FirstName, string LastName);

Person p1 = new(n1, 12);
Person p2 = new(n2, 14);
Person p3 = new(n3, 15);

Person[] SoramiSmile = { p1, p2, p3 };

foreach (Person idol in SoramiSmile)
{
    //if(idol is { name: { LastName: "mirei" } } Mirei)
    //{
    //    Console.WriteLine(Mirei);
    //}

    //내부 클래스의 필드에 접근하는 경우라면 점(.) 연산자를 
    //이용해 멤버를 접근하던 것과 동일한 방식으로 중괄호의 사용을 줄일 수 있다.
    if(idol is { name.LastName: "mirei" } Mirei)
    {
        Console.WriteLine(Mirei);
    }
}

record class Person(Name name, int age);

 

 

해당 점 연산자는 null 체크까지도 겸비했다.

 

 

#람다 기능 향상(Lambda improvements)

  1. 람다 식에 특성 허용: 람다 식 자체와 매개변수, 반환 값 모두에 특성을 추가할 수 있게 됐다.
  2. 반환 타입 지정 허용
  3. 람다 식에 대한 추론 향상: var 추론

매개변수가 하나만 있는 람다식에서는 반드시 해당 매개변수에 괄호를 함께 사용해야 한다.

//컴파일 에러
list.ForEach([MyMethod()] elem => Console.WriteLine(elem));

//괄호 명시해야 함
list.ForEach([MyMethod()] (elem) => Console.WriteLine(elem));

 

람다 식에 Conditiional 특성 사용 불가

 

//반환 타입을 명시적으로 short로 지정, 매개변수가 하나라도 x를 괄호에 포함
Func<int, short> f1 = short (x) => 1;

//ref 반환도 지정 가능
MethodRefDelegate f2 = ref int (ref int x) =>
{
    x++;
    return ref x;
};

public delegate ref int MethodRefDelegate(ref int arg);

//람다 식의 var 추론
var f = int (int x) => x; //반환 타입 및 매개변수 타입 명시 var 추론 가능

 

 

#호출자 인수 식(CallerArgumentExpression)

using System.Runtime.CompilerServices;

public static class Program
{
    public static void Main(string[] args)
    {
        //컴파일러가 CallerArgumentExpression("cond") 특성으 인지하고,
        //자동으로 cond 매개변수에 전달한 식을 문자열로 변환해 msg 매개변수로 전달
        MyDebug.Assert(args.Length >= 1);
    }
}

public static class MyDebug
{
    public static void Assert(bool cond, [CallerArgumentExpression("cond")] string msg = null)
    {
        if (cond == false)
        {
            Console.WriteLine("Assert failed: " + msg);
        }
    }
}

//CallerArgumentExpression은 C/C++의 문자열화 연산자(Stringizing operator)인 #을 활용한 매크로처럼 동작한다.
#define LOG_COND(x) printf_s("%s, result == %d", #x, x)

void main(int argc, char *argv[] {
     LOG_COND(argc >= 5);
}
/* 출력 결과
argc >= 5, result == 0
*/

 

#기타 개선 사항

그외) Improved Definite Assignment Analysis, AsyncMehodBuilder 재정의

 

#보간된 문자열 개선(Improved Interpolated Strings)

 

#분해 구문에서 기존 변수의 재사용 가능(Mix Declarations and Variables in Deconstruction)

string firstName;
var person = ValueTuple.Create("manaka", "rara");

//다음 코드는 C# 9 이전까지 컴파일 오류 발생
(firstName, string lastName) = person;

 

#Source Generator V2 API

 

#향상된 #line 지시문(Enhanced #line directives)

 

 


C# 11.0

#인터페이스 내에 정적 추상 메서드 정의 가능

정적 멤버 중 메서드에 대해서는하위 클래스에서 구현을 강제할 수 있는 추상(abstract) 구문을 추가했다.

->제네릭 메서드나 클래스에서 특정 타입이 반드시 특정 정적 메서드를 제공하도록 요구할 수 있다.(abstract가 빠지면 그냥 인터페이스에서 제공하는 정적 메서드)

 

원래 고전적인 인터페이스는 인스턴스 멤버 구현을 강제하도록 한정되었었다.

 

#숫자형 타입에 추가된 정적 추상 메서드를 담은 인터페이스

인터페이스 이름 재정의 가능한 기능
IParseable Parse(string, IFormatProvider)
ISpanParseable Parse(ReadOnlySpan<char>, IFormatProvider)

 

 

 

#제네릭 타입의 특성 적용

 

#사용자 정의 checked 연산자

사용자 정의 타입에서 overflow에 따른 제어를 checked/unchecked 구문과 연동하기

//checked 문맥에서 호출
public static Int3 operator checked ++(Int3 lhs)
{
    if(lhs.value + 1 > 8388607)
    {
        throw new OverflowException((lhs.value + 1).ToString());
    }
    return new Int3(lhs.value + 1);
}

 

#shift 연산자 개선

시프트 연산자에서 이동할 비트를 정수로 지정해야하는 조건이 완화됨

 

#새로운 연산자 ">>>" (부호 없는 오른쪽 시프트 연산자)

최상위 비트를 부호 비트로 취급하지않고 단순히 비트 그대로 밀어낼고 싶을 때 사용

 

#IntPtr/UIntPtr과 nint/unint의 통합

System.Int32 타입의 C# 예약어가 int였던 것처럼 System.IntPtr과 System.UIntPtr에 대한 C# 예약어로 각각 nint, nuint가 됨

 

#문자열 개선

큰 따옴표 n개로(3부터) 묶으면 n - 1개까지 연이어 따옴표 사용이 가능함,추가로  문자열 들여쓰기도 개선됨(닫는 큰 땅모표의 칼럼 위치를 기준으로 내부 문자열의 공백을 무시, JSON 문자 표기시 편리함)

 

#문자열 보간 개선

  1. 보간식 내에 개행 허용
  2. 원시 문자열의 보간식에 사용할 중괄호의 이스케이프 처리 개선

$ 기호와 같은 수의 중괄호를 보간식에 사용함(적은 경우는 모두 일반 문자로 다룸)

//사용 예시
string text = $$"""
          {
                  "runtimeOptions": {
                      "tfm": "net6.0",
                      "framework": {
                          "name": "Microsoft.NETCore.App",
                          "version": "{{Environment.Version}}"
                      }
              }
          }
          """;

 

#UTF-8 문자열 리터럴 지원

UTF-8 문자열 리터럴은 컴파일 시에 값이 정해지는 상수가 아니라서 상수를 요구하는 문법(메서드의 매개변수에 기본값을 설정)에는 사용불가

 

#목록 및 ReadOnlySpan<char> 패턴 매칭

//슬라이스 패턴
int[] arr = { 1, 2, 3, 1, 4 };

if(arr is [1, .., 3, _, 4]) //1로 시작, 마지막은 3, (임의 정수), 4로 끝나는 패턴
{
    Console.WriteLine(string.Join(", ", arr));
}

static int Start1And3orHigher(int[] vaslues)
{
    const int three = 3;
    switch (values)
    {
        case [1, >= three, ..]: return 1; //1로 시작하고 2번째 요소가 3보다 크다면?
        case [>= 2, ..]: return 2; //첫 번째 요소가 2보다 크다면?
        default: return 0;
    };
}

 

목록 패턴은 Count(or Length) 속성 제공 && indexer 속성 제공을 만족하는 타입이면 사용 가능

NaturalNumber list = new NaturalNumber();

if (list is [1, .., Int32.MaxValue]) //목록 패턴 사용 가능
{
    Console.WriteLine("integer 범위");
}

public class NaturalNumber
{
    int _length = Int32.MaxValue;

    public int this[int index] //indexer 제공
    {
        get { return index + 1; }
    }
    public int Length => _length; //Length 속성 제공 public int Count => _length;도 가능
}

 

기존 타입 중에 대표적으로 Span<T>가 위의 조건을 만족시킴

Span<int> oneToFour = stackalloc int[] { 1, 2, 3, 4 };

if(oneToFour is [1, .. var remains, 4]) //조건에 해당하는 요소를 변수로 받는 것도 가능
{
    Console.WriteLine(string.Join(", ", remains.ToArray())); //출력: 2, 3
}

 

C# 11 컴파일러는 Span 제네릭 유형 중 유일하게 ReadOnlySpan<char> 타입에 대해 패턴 매칭 시 string 타입의 문자열과 직접 비교할 수 있는 연산을 지원한다.

ReadOnlySpan<char> text = "TEST".AsSpan()[0..2];

if(text is ['T', 'E']) Console.WriteLine("['T', 'E']"); //정석
if(text is "TE") Console.WriteLine("TE"); //컴파일러 지원

 

#ref struct 내에 ref 필드 지원

ref 필드는 GC 힙에 할당되는 참조형 타입 내에 정의하는 것이 불가능함 값 형식인 struct 타입도 class 내에 포함시키면 GC 힙에 할당 가능해서 불가함

 

ref struct에서 만든 타입은 절대로 GC 힙에 생성할 수 없으므로 ref 필드를 가질 수 있음

 

#새로운 예약어: scoped (922~924)

스택프레임 내에만 유효한 변수를 ref로 저장할 수 있어서 C# 컴파일러는 이에 대해서 컴파일 오류를 명시시킴. 만약 스택프레임 내 변수를 참조에 대한 연결 없이 순수하게 사용한다면 scoped 예약어를 사용하면 된다.

 

scoped 예약어의 사용은 ref로 받은 매개변수를 절대로 다른 ref 필드에 보관하지 않겠다고 명시하는 효과를 갖는다.

 

#파일 범위 내에서 유효한 타입 정의

중첩 유형을 제외하고 타입 자체에 대해 허용되는 접근 제한자는 internal(기본), public 단 2개임

 

(기본값인) internal은 같은 어셈블리 내에서만 사용할 수 있도록 제약할 수 있지만, C# 코드 파일 내에서만 사용 가능하도록 제약할 수는 없다. 바로 그런 필요성에 의해 오직 타입에만 적용할 수 있는 접근 제한자로 새롭게 file 예약어가 추가됐다.

//같은 파일 내에서만 타입을 사용
file class PrivateClass {}

 

file class 유형은 반드시 file class 타입에서만 상속 가능, file class 유형에서는 (file class를) 필드로 정의, 메서드 반환 타입으로 사용 가능

 

#메서드 매개 변수에 대한 nameof 지원 확장

using System.Runtime.CompilerServices;

CallTest("Test");

//C# 10 이전에는 컴파일 오류
[Arg(nameof(msg))]
static void CallTest(string msg)
{
    Console.WriteLine($"{nameof(msg)}: {msg}");
}

public class ArgAttribute : Attribute
{
    public ArgAttribute(string name)
    {
        Name = name;
    }
    public string Name { get; }
}

public static class MyDebug
{
    //C# 10 이전에는 컴파일 오류
    public static void Assert(bool cond, [CallerArgumentExpression(nameof(cond))] string? message = null)
    {
        if(cond == false)
        {
            Console.WriteLine("Assert failed: " + message);
        }
    }
}

 

#속성 및 필드에 지정할 수 있는 required 예약어 추가

required을 붙이면 속성에 필히 값을 설정해야 함

[SetRequiredMember] 특성을 붙인 생성자는 required 필드를 무시(필수 멤버가 설정 안되었다는 경고를 안띄움), 상속된 자식 클래스는 부모 클래스의 생성자가 이 특성이 붙여져있다면 자식 클래스의 생성자도 마찬가지로 적용해야 함

 

required 멤버는 class, struct, record에서만 허용되고 interface에는 저으이할 수 없다. 또한 fixed, ref readonly, ref, const, static 및 인덱서 구문에도 required를 조합할 수 없다.

 

#구조체 필드의 기본값 자동 초기화(auto-default structs)

struct가 class와 마찬가지로 초기화되지 않은 필드에 대해 자동으로 기본값을 설정하도록 바뀜

 

#정적 메서드에 대한 delegate 처리 시 캐시(cache) 적용

기존 delegate로 생성된 인스턴스를 재활용해주는 코드를 자동으로 추가함(GC 힙 사용을 줄임). 컴파일러가 자동으로 대행해 주는 서비스로 문법적 변화는 없다

 


C# 12.0

#기본 람다 매개 변수

람다 문법에 매개변수의 기본값까지 설정 가능해짐

var addWithDefault = (int addTo = 2) => addTo + 1;

//사용자가 소스코드에 지정한 기본값은 엄밀히 델리게이트 타입에 반영되는 식으로 처리됨
[CompilerGenerated]
internal delegate TResult AnonymousDelegate<T1, TResult>(T1 arg = 2);

 

#기본 생성자

record의 생성자 메서드 정의 방식을 C# 12부터 일반 타입에 도입한 것이 바로 기본 생성자(Primary constructor)다.(기존 기본 생성자(default constructor)와는 다름)

 

생성자 메서드 정의만 생략할 수 있는 구문을 제공하고 나머지 다른 초기화 코드들은 기본 생성장의 매개변수를 활용해 직접 제공해야 함

 

#모든 형식의 별칭

C# 12.0부터는 단순히 네임스페이스와 타입에 대해서만 별칭을 부혀하던 범위를 넘어서 1) 이름이 없는 타입(unnamed type), 2) 포인터 타입, 3) 널 가능한(Nullable) 타입을 지원함으로써 이제 모든 타입에 대한 별칭을 만들 수 있게 됐다.

using cio = System.Console;
using Person = (int Age, string Name); //이름이 없는 타입에 별칭을 부여(내부적으로는 튜플로 처리)
using unsafe BytePtr = byte*; //unsafe를 함께 사용해 별칭 지정
using NullableInteger = int?; //널 가능한 타입의 별칭 사용은 값 형식만 지원

//타입 별칭
cio.WriteLine();

//익명 타입 별칭
Person person = new Person { Age = 11, Name = "rara" }; //마치 타입처럼 정의해 사용
Person person2 = (14, "Mirei"); //내부적으로는 튜플이라서 이와 같이 사용도 가능

//ValueTuple<int, string> person2 = (14, "Mirei"); 가능

//최종적으로 C# 컴파일러가 아래와 같이 구문을 치환
//ValueTuple<int, string> person2 = new ValueTuple<int, string>(14, "Mirei");

//포인터 타입 별칭
unsafe
{
    byte[] buffer = new byte[] { 1, 2, 3 };
    fixed (BytePtr ptr = buffer)
    {
         cio.WriteLine(*ptr); // 1
    }
}

//널 가능한 타입 별칭
cio.WrtieLine(typeof(NullableInteger).FullName);

 

#인라인 배열

[System.Runtime.CompilerServices.InlineArray(5)]
public struct FieldBuffer
{
    private int _element0; //public 접근을 허용하지만 실용적이지 않음
                           //배열의 타입을 표현하는 필드 한 개만을 정의할 수 있음

    //만약 바이트 배열을 정의한다면,
    //private byte bufferElement;
}

public struct SafeStructType
{
    public int Count;
    public FieldBuffer Fields; //sizeof(int) * 5 크기만큼의 연속 공간을 할당
}

.Field[i] indexer 구문으로 인라인 배열의 개별 요소 능

고정 크기 배열에 대한 비관리(unmanaged) 문법을 순수 관리(managed) 문법으로 대체할 수 있게됨

 

#컬렉션 식과 스프레드 연산자(942-944)

다음에 타입들에 대해서 컬렉션을 초기화하는 구문으로 기존의 중괄호와 함께 대괄호를 사용할 수 있게 됨

  • 배열
  • Span/ReadOnlySpan
  • C# 3.0 컬렉션 초기화 구문을 지원하는 타입
  • IEnumerable 타입 중 C# 6.0의 Add 확장 메서드가 정의된 타입

컬렉션 식 초기화 구문은 식(expression)으로 평가받기 때문에 이에 준하는 모든 코드에 적용 가능

List<int> list = (Environment.OSVersion.Version.Major > 4) ? [1, 2] : [3, 4];

ListArray([1, 2, 3]);

static void ListArray(List<int> list) { }

Array.ForEach([3, 4, 5], (elem) => Console.Write($"{elem}, "));

public List<int> GetDefaults => [1, 2, 3];

 

컬렉션 식 내에서만 사용할 수 있는 스프레드 연산자도 새롭게 추가 되었다(..)

int[] arr1 = [1, 2, 3];
int[] arr2 = [4];
int[] arr3 = [5, 6];

//int[] -> int 스프레드 연산자 사용
int[] arr = [0, .. arr1, .. arr2, .. arr3];
Array.ForEach(arr, (elem) => Console.Write($"{elem} "));

 

#ref readonly 매개 변수

in 변경자와 같은 구문임. ref readonly는 'ref' 예약어의 특성이 부각돼 '값'을 직접 전달하는 경우 경고를 발생시킴

 

#Interceptor(컴파일 시점에 메서드 호출 재작성)

소스 생성기에 사용할 의도로 도입됨

 

#Experimental 특성 지원

[experimental("name")]

[obsolete]

 


C# 13.0

 

 

'개인공부용1 > cs' 카테고리의 다른 글

함수형 프로그래밍  (0) 2024.07.15
디자인 패턴의 아름다움  (0) 2024.07.14
정규표현식  (0) 2024.07.11
혼자 공부하는 컴퓨터 구조 + 운영체제  (1) 2024.06.18
Bresenham's line algorithm  (0) 2024.02.07