- C# 6.0에 대응되는 .NET Framework는 4.6 버전이다. 하지만 더 이상 .NET Framework에 종속적이지 않게 바뀌어서 독자적인 컴파일러 업그레이드가 가능하게 바뀌었으므로, C# 6.0 이상의 컴파일러를 .NET Framework 4.6 이하 환경에 설치해 실습하는 것이 가능하다.
- 주요한 변경점은 "간편 표기법"을 제공하는 정도이다.
[ 1. C# 3.0에 구현된 자동 구현 속성(Auto-implemented Properties)의 초기화 구문 추가 ]
- 자동 구현 속성을 사용한 경우 초기값을 부여하려면 별도로 생성자 등의 메서드를 이용해 코드를 추가해야만 했다. 이렇게 구현할 경우, 설정자(set) 메서드를 반드시 구현해야하는 강제성을 수반하므로, 이를 원치 않는 경우에는 다시 예전의 "필드 + 속성" 조합으로 구현해야만 했다.
using System;
namespace ConsoleApp1
{
class Person
{
public string Name { get; set; }
Person()
{
this.Name = "Jane";
}
}
}
- C# 6.0에[서는 새롭게 제공되는 "자동 구현 속성 초기화(Initializers for auto-properties)" 구문을 사용하면 아래와 같이 속성 정의 구문에서 직접 기본값을 지정할 수 있다. 아래와 같이 기술하면, 자동적으로 안겹치는 이름의 readonly 변수를 만드는 식으로 컴파일 된다.
using System;
namespace ConsoleApp1
{
class Person
{
public string Name { get; } = "Jane";
}
}
using System;
namespace ConsoleApp1
{
class Person
{
public string Name { get; } = "Jane";
public Person()
{
// set은 없지만 readonly 필드의 특성에 따라 생성자에서만 가능
Name = "John";
}
static void Main(string[] args)
{
Person p = new Person();
Console.WriteLine(p.Name); // John
}
}
}
[ 2. 람다 식을 이용한 메서드 속성 및 인덱서 정의 ]
- 메서드가 단일 식(expression)으로 이뤄진 경우 간략하게 람다 식을 이용해 정의할 수 있다. 예를 들어, 다음과 같이 클래스를 정의했을 때,
using System;
namespace ConsoleApp1
{
public class Vector
{
double x;
double y;
public Vector(double x, double y)
{
this.x = x;
this.y = y;
}
public Vector Move(double dx, double dy)
{
return new Vector(x + dx, y + dy);
}
public void PrintIt()
{
Console.WriteLine(this);
}
public override string ToString()
{
return string.Format("x = {0}, y = {1}", x, y);
}
}
}
- 위의 Move, PrintIt, ToString 메서드 내의 코드는 모두 단일 식(expression)으로만 이뤄졌기 때문에, 람다 식을 이용해 아래와 같이 바꿀 수 있다. 물론, 컴파일 될 때는 변경 이전의 코드와 동일한 결과물을 생성한다.
using System;
namespace ConsoleApp1
{
public class Vector
{
double x;
double y;
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 override string ToString() => string.Format("x = {0}, y = {1}", x, y);
}
}
- 속성도 내부적으로는 메서드로 구현되기 때문에, 람다식을 속성 정의에도 사용하는 것이 가능하다.
using System;
namespace ConsoleApp1
{
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;
static double RadianToDegree(double angle) => angle * (180.0 / Math.PI);
public Vector(double x, double y)
{
this.x = x;
this.y = y;
}
}
}
- 참고로, 생성자/소멸자, 이벤트의 add/remove 접근자의 경우 메서드이긴 하지만 예외적으로 람다 식을 이용한 구현을 할 수 없다. (단, C# 7.0에서는 할 수 있도록 바뀌었다.)
[ 3. using static 구문을 이용한 타입명 생략 ]
- 기존에는 static 멤버를 사용하는 경우 반드시 타입명과 함께 써야만 했다. (ex. Console.WriteLine("");) C# 6.0부터는 자주 사용하는 타입의 FQDN을 using static으로 선언해 해당 소스코드 파일 범위 내에서는 그 타입의 정적 멤버를 타입명 없이 바로 호출할 수 있게 되었다.
using System;
using static System.Console;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("test1");
WriteLine("test2");
}
}
}
- 정적 멤버에 적용된다는 기준으로 인해, enum 타입의 멤버와 const 상수 멤버에 대해서도 동일하게 타입명을 생략할 수 있다. 왜냐하면, 해당 멤버들은 컴파일하면 내부적으로 모두 static 유형으로 다뤄지기 때문이다.
using System;
using static ConsoleApp1.MyDay;
using static ConsoleApp1.BitMode;
using static System.Console;
namespace ConsoleApp1
{
public enum MyDay
{
Saturday, Sunday, // enum 필드의 내부 구현은 static 속성을 갖는다.
}
public class BitMode
{
// const 필드의 내부 구현은 static 속성을 갖는다.
public const int ON = 1;
public const int OFF = 0;
}
class Program
{
static void Main(string[] args)
{
MyDay day = Saturday;
int bit = ON;
WriteLine(day);
WriteLine(bit);
}
}
}
- 하지만 이 규칙의 적용에 예외가 있는데, C# 3.0에 도입된 확장 메서드의 경우 내부적으로 static 메서드로 표현되지만, 문법적인 모호성 문제로 인해 using static 적용을 받지 않는다. (컴파일 오류)
using System;
using System.Net;
using System.Text;
using static UriExtension;
static class UriExtension
{
public static string TextFromUrl(this Uri uri)
{
return "UriExtension: " + uri.ToString();
}
}
class Program
{
public static string TextFromUrl(Uri uri)
{
return "Program: " + uri.ToString();
}
static void Main(string[] args)
{
Uri uri = new Uri("http://www.naver.com");
string txt = TextFromUrl(uri); // 모호함 발생!
// 이 호출은 UriExtension의 정적 메서드 호출인지
// 아니면 Program 타입에 정의된 메서드 호출인지? (O)
Console.WriteLine(txt);
}
}
- 어차피, 확장 메서드는 인스턴스 메서드 처럼 호출되는 것이 일반적이므로, 마이크로소프트는 이런 모호함을 피하기 위해, 확장 메서드를 본연의 정적 메서드처럼 호출하려고 할 때는 타입명을 함께 써야만 하는 것으로 결졍했다.
[ 4. null 조건 연산자 ]
- 참조 변수의 값이 null이라면 그대로 null을 반환하고 null이 아니라면 지정된 멤버를 호출하는 "null" 조건 연산자가 추가되었다.
using System;
using System.Collections.Generic;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
List<int> list = new List<int> { 1,2,3,4,5 };
if (list != null) Console.WriteLine(list.Count); // 기존 방식
Console.WriteLine(list?.Count); // list == null이면 null 반환,
// list != null이면 list.Count 반환
for(int i = 0; i < list?.Count; i++)
{
Console.WriteLine(list[i]);
}
// null 조건 연산자는 단독으로 사용할 수 없고,
// 반드시 해당 참조형 변수의 멤버를 접근하거나 배열 인덱스와 같은
// 부가적인 접근을 필요로 한다.
string[] lines = new string[2] { "first", "wow" };
string firstElement = lines?[0]; // lines != null이면 lines[0] 반환
Console.WriteLine(firstElement);
// 주의할 점은 null 조건 연산자의 결과값이 null을 포함할 수 있기 때문에
// 이를 저장하기 위해서는 반드시 null 값을 처리할 수 있는 타입을 사용해야 한다.
// int count = list?.Count; // 잘못된 표현
int? count = list?.Count; // C# 2.0의 nullable 형식 사용
// 또한 ?? 연산자와 함꼐 사용해 null이면 값 형식으로 반환하는 방법도 있다.
int count2 = list?.Count ?? 0; // list?가 null을 반환하면 ??으로 0 반환
// 반환값이 없어도, 해당 멤버에 접근하는 것은 허용된다.
List<int> tempList = null;
tempList?.Add(5); // null이므로 호출 안함
// 아래와 같이 null 조건 연산자를 하나의 함수에 대해서 하나의 참조 변수에 대해
// 다중으로 사용한다면 그다지 효율적인 성능의 코드가 나오지 않는다.
// 쓸데없이 null 조건을 중복으로 확인한다.
List<int> myList = null;
list?.Add(5); // if(list != null) list.Add(5)
list?.Add(6); // if(list != null) list.Add(6)
list?.Add(7); // if(list != null) list.Add(7)
list?.Add(8); // if(list != null) list.Add(8)
}
}
}
[ 5. 문자열 내에 식(expression)을 포함 ]
- 아래의 코드에서 ToString 메서드의 구현을 보자.
using System;
namespace ConsoleApp1
{
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public override string ToString()
{
return string.Format("이름 : {0}, 나이 : {1}");
}
}
}
- 성능을 조금이라도 생각한다면, System.String 타입이 불변이라는 점을 감안해 아래 같은 코드보다는 string.Format을 대신 사용하게 된다.
"이름 : " + Name + "나이 : " + Age
- C# 6.0 부터는 string.Format 의 사용이 빈번하다는 이유로, 이에 대한 약식 표현이 추가되었으며, 이를 문자열 보간(String interpolation)이라 한다. $ 접두사를 붙인 문자열 내에 중괄호{}를 사용해 코드를 넣으면 된다. 진짜 중괄호를 넣으려면 중괄호를 두 번 입력한다.
using System;
namespace ConsoleApp1
{
class Person
{
public string Name { get; set; }
public int Age { get; set; }
public override string ToString()
{
return $"이름: {Name}, 나이 : {Age}";
}
public string ToString2()
{
return $"{{이름: {Name}, 나이 : {Age}}}";
}
public string ToString3()
{
return $"이름 : {Name.ToUpper()}, 나이 : {(Age > 19 ? "성년" : "미성년")}";
}
public string ToString4()
{
return $"이름 : {Name,-10}, 나이 : {Age,5:X}";
}
}
class Program
{
static void Main(string[] args)
{
Person p = new Person();
p.Name = "Lee";
p.Age = 30;
Console.WriteLine(p.ToString4());
}
}
}
[ 6. nameof 연산자 ]
- C# 코드에 사용된 식별자를 이름 그대로 출력하고 싶은 경우가 있다. 이 때, nameof를 사용하게 된다.
using System;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
OutputPerson("Lee", 30);
Console.WriteLine(nameof(System.Console)); // 마지막 이름만 반환된다.
}
static void OutputPerson(string name, int age)
{
Console.WriteLine($"{nameof(name)} == {name}");
Console.WriteLine($"{nameof(age)} == {age}");
}
}
}
- 다음 코드는 OutputPerson의 첫 번째 인자에 대한 이름을 리플렉션을 통해 구하는 방법을 보여준다.
리플렉션은 코드가 실행되어야 이름이 구해지지만 nameof는 C# 6.0 컴파일러가 컴파일 시점에 문자열로 직접 치환해 주기 때문에, 실행 시점에 부하가 전혀 없다.
using System;
using System.Diagnostics;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
OutputPerson(name: "Lee", age: 30);
}
static void OutputPerson(string name, int age)
{
StackFrame sf = new StackFrame();
System.Reflection.ParameterInfo[] parameters = sf.GetMethod().GetParameters();
string nameId = parameters[0].Name;
Console.WriteLine(nameId + ": " + name);
}
}
}
[ 7. Dictionary 타입의 인덱스 초기화 ]
- 아래와 같이, C# 3.0에서 Dictionary 타입의 초기화도 이미 다음과 같이 지원하고 있었다.
var weekEnds = new Dictionary<int, string>
{
{0, "Sunday"},
{6, "Saturday"}
};
- 추가적으로 C# 6.0에서는 키/값 개념에 좀 더 어울리는 직관적인 초기화 구문을 지원한다.
var weekEnds = new Dictionary<int, string>
{
[0] = "Sunday",
[6] = "Saturday"
};
// after compile
// weekends[0] = "Sunday";
// weekends[6] = "Saturday";
- 위의 두 구문은 컴파일 시 동일한 코드를 생성하지는 않는다. 기존 컬렉션 초기화를 사용하면 .Add 메서드로 변경되지만, 새로운 인덱스 초기화 구문의 코드는 컴파일 후 인덱서 방식의 코드로 변경된다. 참고로 초기화 구문이 배열의 인덱스가 아니라는 점을 기억해 두자. Dictionary의 첫 번째 형식 인자인 TKey 타입에 해당하는 인덱서 구문이기 때문에 그에 맞는 값을 초기화 구문에 사용해야 한다.
var people = new Dictionary<string, int>
{
["Anders"] = 7,
["Sam"] = 10
};
[ 8. 예외 필터 ]
- 예외 필터(Exception Filter) 기능이 추가 되었다.
- 아래 예시에서, ReadAllText 메서드에 지정한 파일이 없는 경우 무조건 FileNotFoundException을 반환하는 것이 아니라, 파일의 경로가 "temp"를 포함하는 경우에만 예외 처리를 할 수 있다.
using System;
using System.IO;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
string filePath = @"C:\temp\test.txt";
try
{
string txt = File.ReadAllText(filePath);
}
catch (FileNotFoundException e) when (filePath.IndexOf("temp") != -1)
{
Console.WriteLine(e.ToString());
}
}
}
}
- 메서드로 분리하는 것도 가능하다.
using System;
using System.IO;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
string filePath = @"C:\temp\test.txt";
try
{
string txt = File.ReadAllText(filePath);
}
catch (FileNotFoundException e) when (ShallowWhenTempFile(filePath))
{
Console.WriteLine(e.ToString());
}
}
static bool ShallowWhenTempFile(string filePath)
{
return filePath.IndexOf("temp") != -1;
}
}
}
- 해당 예외 필터의 조건식이 실행되는 시점은, 아직 예외 처리 핸들러가 실행되는 시점이 아니기 때문에, 예외가 발생한 시점의 호출 스택이 그대로 보존되어 있다. 그래서 기존 예외 처리 구조에 영향을 주지 않고도 부가적인 작업을 할 수 있다. 예를 들어, 다음과 같이 Log 메서드에서는 무조건 false를 반환하면서 원하는 작업을 예외에 대해 작성하는 것이 가능하다.
using System;
using System.IO;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
try
{
// 코드
}
catch (Exception e) when (Log(e))
{
// 이 에외 핸들러는 절대로 선택되지 않는다.
}
}
private static bool Log(Exception e)
{
Console.WriteLine(e.ToString()); // 발생한 예외 인스턴스를 다룰 수 있다.
return false;
}
}
}
- 동일한 타입의 catch 구문도 예외 필터와 함께 사용하면 허용이 된다. when 조건문은 여러번 실행되는 것이 가능하지만, 선택되는 catch 핸들러는 오직 하나뿐이다.
using System;
using System.IO;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
string filePath = @"C:\temp\test.txt";
try
{
string txt = File.ReadAllText(filePath);
}
catch (FileNotFoundException e) when (Log(e))
{
Console.WriteLine("1");
}
catch (FileNotFoundException e) when (Log(e)) // 동일한 예외 필터도 가능
{
Console.WriteLine("2");
}
catch(FileNotFoundException)
{
Console.WriteLine("3");
}
}
private static bool Log(Exception e)
{
return false;
}
}
}
- 예외 필터를 사용한 코드 역시 C# 6.0에서 컴파일만 되면 닷넷 프레임워크 2.0에서도 실행할 수 있다.
[ 9. catch/finally 블록 내에서 await 사용 가능 ]
- C# 6.0부터 catch/finally의 예외 처리 블록 내에서 비동기 호출(await)을 할 수 있게 변경되었다.
using System;
using System.IO;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
ProcessFileAsync();
Console.ReadLine();
}
private static async void ProcessFileAsync()
{
FileStream fs = null;
try
{
fs = new FileStream("test.txt", FileMode.Open, FileAccess.Read);
byte[] contents = new byte[1024];
await fs.ReadAsync(contents, 0, contents.Length);
Console.WriteLine("ReadAsync Called!");
}
catch(Exception e)
{
await LogAsync(e); // C# 5.0에서는 불가능했던 호출
}
finally
{
await CloseAsync(fs); // C# 5.0에서는 불가능했던 호출
}
}
static Task LogAsync(Exception e)
{
return Task.Factory.StartNew(
() =>
{
Console.WriteLine("LogAsync Called!");
});
}
static Task CloseAsync(FileStream fs)
{
return Task.Factory.StartNew(()
=>
{
Console.WriteLine("Close Async Called!");
fs?.Close();
});
}
// 1. 출력 결과(예외가 발생하지 않은 경우)
// ReadAsync Called!
// Close Async Called!
// 2. 출력 결과(예외가 발생한 경우)
// LogAsync Called!
// Close Async Called!
}
}
[ 10. 컬렉션 초기화 구문에 확장 메서드로 정의한 Add 지원 ]
- '컬렉션 초기화'에서 설명한 구문이 컴파일 되려면, 반드시 해당 타입이 ICollection<T> 인터페이스를 구현하고 있어야 한다. 예를 들어, 다음의 NaturalNumber 타입은 ICollection<T> 인터페이스를 구현하지 않았으므로 컬렉션 초기화 구문을 사용할 수 없어 컴파일 시에 오류가 발생한다.
using System;
using System.Collections;
using System.Collections.Generic;
namespace ConsoleApp1
{
public class NaturalNumber : IEnumerable
{
List<int> numbers = new List<int>();
public List<int> Numbers { get { return numbers; } }
IEnumerator IEnumerable.GetEnumerator()
{
return numbers.GetEnumerator();
}
}
class Program
{
static void Main(string[] args)
{
NaturalNumber numbers = new NaturalNumber() { 0, 1, 2, 3, 4 }; // 컴파일 오류
// C# 5.0 컴파일 오류
// CS1061 'NaturalNumber'에는 'Add'에 대한 정의가 포함되어 있지 않습니다.
foreach(var item in numbers)
{
Console.WriteLine(item);
}
}
}
}
- 이런 경우 NaturalNumber 타입을 수정할 수 있다면 ICollection<T> 인터페이스를 추가함으로써 손쉽게 컬렉션 초기화 구문을 지원할 수 있도록 만들겠지만 그렇지 않은 경우에는 상속 등의 우회적인 방법을 써야 한다.
- C# 6.0에는 Add 메서드를 ICollection<T> 인터페이스가 없다면 확장 메서드로도 구현되어 있는지 다시 한번 더 찾는 기능을 추가했다. 따라서, 다음과 같이 확장 메서드만 추가하면 위의 코드가 정상적으로 컴파일된다.
public static class NaturalNumberExtension
{
public static void Add(this NaturalNumber instance, int number)
{
instance.Numbers.Add(number);
}
}
- 결과적으로 C# 컴파일러는 빌드시 컬렉션 초기화 구문 부분을 내부적으로 확장 메서드로 구현된 Add를 사용하는 것으로 변경해 컴파일을 성공시킨다.
[ 11. #pragma의 "CS" 접두사 지원 ]
- 예를들어, 다음의 코드를 컴파일 하면 "CS0168" 경고가 발생한다.
using System;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
int i; // 경고 CS0168 : 'i'변수가 선언되었지만 사용되지 않았습니다.
}
}
}
- 기존에는 이 경고를 끄려면 #pragma에 "CS"를 뺀 0168 숫자만 넣어야 했지만, C# 6.0부터는 그대로 "CS0168"을 쓸 수 있게 되었다.
#pragma warning disable 0168
// C# 6.0 부터 아래 2가지 구문 모두 지원
#pragma warning disable CS0168
#pragma warning disable 0168
- 참고로 Visual Studio의 경우, 프로젝트 속성 창의 "빌드" 탭에 제공되는 "오류 및 경고" / "경고 표시 안함(Suppress warnings)" 값에도 동일하게 이 규칙이 적용된다.
[ 12. 재정의된 메서드의 선택 정확도를 향상 ]
- 예를들어, nullable 타입을 인자로 받는 메서드들이 중복 정의(overload)된 경우 기존 C# 5.0 컴파일러는 아래 상황에서 어떤 메서드를 선택해야 할지 몰라 컴파일 오류를 발생시킨다.
#pragma warning disable 0168
using System;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
WriteLine(null); // C# 5.0에서는 컴파일 오류 발생
}
static void WriteLine(uint? arg)
{
Console.WriteLine("uint? == " + arg);
}
static void WriteLine(int? arg)
{
Console.WriteLine("int? == " + arg); // 선택됨
}
}
}
- nullable을 빼고 해도 int arg를 파라미터로 받는 함수를 선택한다.
- 이외에도, delegate를 인자로 받는 중복 정의 메서드에 대한 선택에 대해서도 정확도가 높아졌다.
using System;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Task.Run(NullTask); // C# 5.0에서는 오류 발생
// 아래 두개 중에 모호하다. C# 6.0에서는 첫번째 선택
// public static Task Run(Func<Task> function);
// public static Task Run(Action action);
}
static Task NullTask()
{
Console.WriteLine("NullTask");
return null;
}
}
}
'Programming > C#' 카테고리의 다른 글
C# 5.0 변경점 - 호출자 정보, 비동기 호출 (1) | 2019.04.24 |
---|---|
C# 4.0 변경점 - 선택적 매개변수/명명된 인자, dynamic 예약어 (0) | 2019.04.15 |
C# 3.0 변경점(3) - LINQ(Language Integrated Query) (0) | 2019.04.09 |
C# 3.0 변경점(2) - 람다 식 (0) | 2019.04.04 |
C# 3.0 변경점(1) - var, 자동 구현 속성, 객체/컬렉션 초기화, 익명 타입, 확장 메서드 (0) | 2019.04.03 |