티스토리 뷰
2019년도 벌써 며칠 남지 않았네요. 유니티 2020 알파가 발표된지도 벌써 오랜 시간이 지났습니다.
유니티2017에서 C#6 (Experimental), 2018.3에서 C#7.3까지 지원이 가능해져, 대체 언제까지 C#4를 써야하는걸까....라고 생각하던 때가 엊그제 같은데 지금은 하루하루 개발이 즐겁습니다.
C#7.3이 사용가능하게 되면서, 여러모로 편해지게 되었는데요, 이번에는 각 버전별로 자주 사용하게 되는 문법을 살펴보고자 합니다.
별의 갯수는 현업에서의 개인적인 사용빈도입니다.
C#5
-
Async/Await ★★★★★ (따로 포스팅하겠습니다)
C#6
https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-6
What's New in C# 6 - C# Guide
Learn the new features in C# Version 6
docs.microsoft.com
-
String Interpolation ★★★★★
var first = "Unity";
var second = "2017";
string.Format("Version:{0}{1}", first, second);
↓
$"Version:{first}{second}";
var one = 1;
var two = 2;
string.Format("{0}", one + two);
↓
$"{one + two}";
-
IReadOnlyList<T>, IReadOnlyCollection<T>, IReadOnlyDictionary<TKey, TValue> ★★★★★
C#4.0까지는 읽기전용 콜렉션(배열, 리스트, 등등...)을 표현하기 위해서는, ReadOnlyCollection<T>를 쓰거나 IEnumerable<T>(열거형)으로 선언 할 수 밖에 없었습니다.
private readonly ReadOnlyCollection<int> _someReadOnlyCollection;
var someList = new List<int>{ 1, 2, 3, 4, 5 };
_someReadOnlyCollection = new ReadOnlyCollection<int>(someList); //할당
private readonly IEnumerable<T> _someEnumerable;
//만약에 이 열거형이 엄청나게 무거운 지연평가 퀘리를 가진 열거형이라면?
_someEnumerable = Enumerable.Range(0, 10).Select(x => SomeSuperHeavyWork(x));
private int SomeSuperHeavyWork(int input)
{
// some super heavy work
return input;
}
var allAreLessThanTen = _someEnumerable.All(x => x < 10); //SomeSuperHeavyWork를 10번 처리
var eventCount = _someEnumerable.Count(x => x % 2 == 0); //SomeSuperHeavyWorks를 다시 10번 처리
...
열거형은 LINQ와 합쳐지면 지연평가를 표현하게 되기 때문에, 사용자 입장에서는 이 IEnumerable<T>가 지연평가 되어있는지를 항상 고민하면서 써야만 했기때문에, 대부분의 경우는 ReadOnlyCollection을 새로 선언하여 쓸 수 밖에 없었죠.
C# 6.0부터는, 대부분의 System.Collections.Generic; NameSpace의 자료구조에 ReadOnly형 인터페이스가 추가되었습니다.
- 배열 [] : IReadOnlyList<T>, IReadOnlyCollection<T>
- List<T> : IReadOnlyList<T>, IReadOnlyCollection<T>
- Dictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>, IReadOnlyCollection<KeyValuePair<TKey, TValue>>
- HashSet<T> : IReadOnlyCollection<T>
이로써, 안전하게 ReadOnlyCollection을 사용할 수 있게 되었습니다.
private readonly IReadOnlyCollection<int> _someReadOnlyCollection;
var someList = new List<int> { 1, 2, 3, 4, 5 };
_someReadOnlyCollection = someList; //List도 그대로 OK
var someArray = new int[] { 1, 2, 3, 4, 5 };
_someReadOnlyCollection = someArray; //Array도 그대로 OK
다만, 어디까지나 완전한 불변이 된 것이 아닌 어디까지나 Add, Remove등의 메서드가 제공이 되지 않는 것 뿐임을 주의하세요.
var someList = new List<int> { 1, 2, 3, 4, 5 };
IReadOnlyCollection<int> someReadonlyCollection = someList;
someList.Add(6);
foreach (var element in someReadOnlyCollection)
{
Console.WriteLine(element);
}
//Result :
1
2
3
4
5
6
정말로 불변(Immutable)을 표현하고 싶으시다면, ImmutableList/ImmutableCollection등을 사용합니다. (내장되어있지 않고, nuget에서 끌어와야합니다.)
유니티 개발에서는 대부분의 경우에는 여기까지 불변을 고집할 일이 생각보다 없는 편입니다.
다만 멀티스레드 환경의 서버등에서는 자주 쓰이는 편입니다.
-
ReadOnly auto property ★★★★★
public class SomeClass
{
public int Id { get; }
public SomeClass(int id)
{
Id = id;
}
public void SetId(int id)
{
// Compile error CS0200: Property or indexer cannot be assigned to -- it is read only
Id = id;
}
}
-
Expression-bodied function member ★★★
public int SomeMethod()
{
return 0;
}
↓
public int SomeMethod => 0;
-
Lazy<T> (늦은 초기화) ★★
public class SomeClass
{
public int Id => _lazyId.Value;
private readonly Lazy<int> _lazyId;
public SomeClass()
{
_lazyId = new Lazy<int>(() => SomeHeavyWork());
}
private int SomeHeavyWork()
{
//some heavy work...
return 5;
}
}
SomeClass.Id에 대한 접근이 이루어지는 시점에서 SomeHeavyWork가 동작합니다.
-
Null conditional operator ★
public class SomeClass
{
public void Call()
{
Console.WriteLine("Call");
}
}
// Case1
var someClassInstance = new SomeClass();
someClassInstance?.Call();
// Result
Call
// Case2
SomeClass someClassInstance = null;
someClassInstance?.Call();
// Result
UnityEngine.Object에는 쓰시지 않는 것을 추천합니다.
Unity의 Object체크는 Equality operator만이 오버로드 되어있기 때문에, Null conditional operator를 쓰게 되면
MissingReferenceException이 발생하게 됩니다.
var go = new GameObject("Cube");
// Case 1
Debug.Log(go?.name);
// Result
Cube
// Case 2
Destroy(go);
Debug.Log(go?.name);
// Result
MissingReferenceException: The object of type 'GameObject' has been destroyed but you are still trying to access it.
// Case 3
go = null;
Debug.Log(go?.name);
// Result
멀티스레드 환경에서는 thread-safe이기 때문에 애용되기도 합니다. (물론 락을 걸거나 하면 해결되지만요)
// Thread 1
var someClassInstance = new SomeClass();
if (someClassInstance != null)
{
someClassInstance.Call(); // Not thread-safe
}
// Thread 2
someClassInstance = null;
↓
// Thread 1
var someClassInstance = new SomeClass();
someClassInstance?.Call(); // Thread-safe
// Thread 2
someClassInstance = null;
요즘 제 트렌드는 null자체를 쓰지 않는 추세이기 때문에, 개인적으로서 사용 빈도는 크지 않은 편입니다만,
편리한 기능이라고 생각합니다.
var result = someClassA?.someClassB?.SomeMethod();
(대체 어디서부터 어디까지 실행 되었고 결과값은 대체 어떤 값인 걸까......)
-
Auto property initializer ★
public class SomeClass
{
public int Id { get; } = 0;
}
C# 7.0
https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7
What's New in C# 7.0 - C# Guide
Get an overview of the new features in version 7.0 of the C# language.
docs.microsoft.com
-
ValueTuple ★★★★★
지금까지 데이터의 흐름을 표현할때에는 크게 두 가지의 패턴이 있었습니다.
- 직접 클래스나 구조체를 선언
- 무명형 new { A = 1, B = 2 }; (클래스)
C#6에서 System.Tuple이 추가되었습니다. (클래스)
private Tuple<int, int> SomeTupleMethod()
{
return Tuple.Create(1, 2);
}
var someTuple = SomeTupleMethod();
Console.WriteLine(someTuple.Item1);
Console.WriteLine(someTuple.Item2);
//Result
1
2
보통 Tuple을 사용할 때에는, 데이터의 흐름을 표현할 때 입니다. LINQ등에서 많이 사용하고는 하는데요, 클래스를 할당하게 되면 당연하게도 힙 메모리를 확보하게 됩니다. 작지만 GC의 먹이가 되는 것이죠. 열거형과 합쳐지게 되면 아무리 작은 힙 메모리라고 하더라도, 신경이 쓰이는 양이 되고는 합니다.
C# 7.0부터는 ValueTuple이 추가되었습니다. (구조체)
주목해야 할 부분은, 구조체 라는 점입니다. 데이터의 흐름을 표현하는데에는 클래스를 필요로 하지 않거니와, 내부 메소드의 정의도 필요로 하지 않습니다.
private ValueTuple<int, int> SomeValueTupleMethod()
{
return ValueTuple.Create(1, 2);
}
var someValueTuple = SomeValueTupleMethod();
Console.WriteLine(someValueTuple.Item1);
Console.WriteLine(someValueTuple.Item2);
//Result
1
2
거기에 더불여 무명형의 장점외에 여러 편한 문법까지 지원해 줍니다.
//OK
private (int, int) SomeValueTupleMethod()
{
return (1, 2);
}
var someValueTuple = SomeValueTupleMethod();
Console.WriteLine(someValueTuple.Item1);
Console.WriteLine(someValueTuple.Item2);
//Result
1
2
↓
//OK
private (int Id, int Age) SomeValueTupleMethod()
{
return (1, 2);
}
//Ver1
var someValueTuple = SomeValueTupleMethod();
Console.WriteLine(someValueTuple.Id);
Console.WriteLine(someValueTuple.Age);
//Ver2
var (id, age) = SomeValueTupleMethod(); //Deconstructor
Console.WriteLine(id);
Console.WriteLine(age);
//Ver3
var someValueTuple = (Id: 1, Age: 2);
Console.WriteLine(someValueTuple.Id);
Console.WriteLine(someValueTuple.Age);
-
out var ★★★
var someDictionary = new Dictionary<int, string> { { 1, "철수" }, { 2, "영희" } };
var key = 0;
someDictionary.TryGetValue(out key);
↓
someDictionary.TryGetValue(out var key);
C# 7.1
https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7-1
What's new in C# 7.1
An overview of new features in C# 7.1.
docs.microsoft.com
-
Inferred tuple element name ★★★★
var one = 1;
var two = 2;
var x = (one: one, two: two);
Console.WriteLine(x.one);
Console.WriteLine(x.two);
↓
var one = 1;
var two = 2;
var x = (one, two);
Console.WriteLine(x.one);
Console.WriteLine(x.two);
-
Default literal expression ★★
int a = default(int);
string b = default(string);
↓
int a = default;
string b = default;
C# 7.2
https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7-2
What's new in C# 7.2
An overview of new features in C# 7.2.
docs.microsoft.com
-
readonly struct ★★★★★
public readonly struct SomeReadOnlyStruct
{
public int Id { get; }
public SomeReadOnlyStruct(int id) => Id = id;
}
데이터를 표현할때는 항상 고정적으로 쓰고 있습니다.
불변을 명시적으로 표현함으로서, 가독성은 물론이고 구조체의 방어적 복사를 방지하여 성능저하를 막아줍니다.
C# 7.3
-
Enum ★
public enum SomeEnum
{
A,
B,
C
}
public class SomeGenericClass<T> where T: struct, IConvertible
{
}
var someGenericClass = new SomeGenericClass<SomeEnum>();
↓
public class SomeGenericClass<T> where T: Enum
{
}
var someGenericClass = new SomeGenericClass<SomeEnum>();
Enum.TryGetValue를 쓰기위해서는 where constraints에 struct도 추가해 줘야 합니다. (낡은 API.....)
이외에도 여러 기능이 있지만, 우선적으로 생각나는 것 들을 적어 보았습니다.
도움이 되셨다면 좋겠네요!