티스토리 뷰

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.....)


이외에도 여러 기능이 있지만, 우선적으로 생각나는 것 들을 적어 보았습니다.

도움이 되셨다면 좋겠네요!

 

 

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
글 보관함