Microsoft MVP성태의 닷넷 이야기
.NET Framework: 1113. C# 10 - (12) 문자열 보간 성능 개선 [링크 복사], [링크+제목 복사]
조회: 10362
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)
(시리즈 글이 15개 있습니다.)
.NET Framework: 1094. C# 10 - (1) 구조체를 생성하는 record struct
; https://www.sysnet.pe.kr/2/0/12790

.NET Framework: 1096. C# 10 - (2) 전역 네임스페이스 선언
; https://www.sysnet.pe.kr/2/0/12792

.NET Framework: 1097. C# 10 - (3) 개선된 변수 초기화 판정
; https://www.sysnet.pe.kr/2/0/12793

.NET Framework: 1099. C# 10 - (4) 상수 문자열에 포맷 식 사용 가능
; https://www.sysnet.pe.kr/2/0/12796

.NET Framework: 1100. C# 10 - (5) 속성 패턴의 개선
; https://www.sysnet.pe.kr/2/0/12799

.NET Framework: 1101. C# 10 - (6) record class 타입의 ToString 메서드를 sealed 처리 허용
; https://www.sysnet.pe.kr/2/0/12801

.NET Framework: 1103. C# 10 - (7) Source Generator V2 APIs
; https://www.sysnet.pe.kr/2/0/12804

.NET Framework: 1104. C# 10 - (8) 분해 구문에서 기존 변수의 재사용 가능
; https://www.sysnet.pe.kr/2/0/12805

.NET Framework: 1105. C# 10 - (9) 비동기 메서드가 사용할 AsyncMethodBuilder 선택 가능
; https://www.sysnet.pe.kr/2/0/12807

.NET Framework: 1108. C# 10 - (10) 개선된 #line 지시자
; https://www.sysnet.pe.kr/2/0/12812

.NET Framework: 1109. C# 10 - (11) Lambda 개선
; https://www.sysnet.pe.kr/2/0/12813

.NET Framework: 1113. C# 10 - (12) 문자열 보간 성능 개선
; https://www.sysnet.pe.kr/2/0/12826

.NET Framework: 1114. C# 10 - (13) 단일 파일 내에 적용되는 namespace 선언
; https://www.sysnet.pe.kr/2/0/12828

.NET Framework: 1115. C# 10 - (14) 구조체 타입에 기본 생성자 정의 가능
; https://www.sysnet.pe.kr/2/0/12829

.NET Framework: 1116. C# 10 - (15) CallerArgumentExpression 특성 추가
; https://www.sysnet.pe.kr/2/0/12835




C# 10 - (12) 문자열 보간 성능 개선

휴~~~ 이번에도 역시, 이를 설명하기 위해 지난 2개의 글을 먼저 설명할 필요가 있었습니다. ^^;

C# - FormattableString 타입
; https://www.sysnet.pe.kr/2/0/12819

C# - .NET 6부터 공개된 ISpanFormattable 사용법
; https://www.sysnet.pe.kr/2/0/12821

그리고, 이번 문법에 대해서는 spec 문서보다는 다음의 블로그 글을 읽는 것이 더 이해가 잘 될 것입니다.

String Interpolation in C# 10 and .NET 6
; https://devblogs.microsoft.com/dotnet/string-interpolation-in-c-10-and-net-6/

사실 이번 글은 위의 내용을 정리한 것에 불과합니다.




자, 그럼 왜 문자열 보간에 대한 개선을 해야만 했는지, 기존 문자열 보간이 가진 문제점 - 즉, string.Format에 대한 문제점들을 다음과 같이 나열할 수 있습니다.

1) string.Format은 꽤나 무거운 작업을 동반합니다. 즉, 서식이나 정렬에 관계되고 심지어 자체적인 문자열 파싱도 해야 합니다. 그런데, 여기서 재미있는 것은 C# 컴파일러의 경우 string interpolation 표현이 있을 때 이미 한번 파싱을 해 string.Format으로 변경하는데요, 이것을 다시 런타임에 string.Format 메서드 내에서 다시 파싱을 하고 있었다는 점입니다.

2) string.Format은 object 인자를 받는데, 이로 인해 값 타입의 인스턴스인 경우 반드시 박싱 연산을 필요로 하게 됩니다. 이와 함께, 그동안 값 타입의 성능을 높이기 위한 모든 다양한 방법들, 가령 Span<char> 등의 인스턴스는 string.Format의 Object 타입 제한으로 인해 사용할 수 없는 문제점이 있습니다.

3) string.Format은 3개까지 인자를 받는 오버로드를 제공하지만, 그 이후부터는 params Object[]로 처리하기 때문에 인자 수가 4개 이상인 경우부터 무조건 배열을 위한 힙 메모리 할당이 발생합니다.

4) 인자로 넘어온 값은 문자열 변환을 위해 반드시 ToString 호출을 필요로 하며, 당연히 이때 임시 문자열이 생성됩니다.

이런 유의 문제점들을 C# 10에서 해결했다는데, 사실 어떻게 저런 것들을 개선할 수 있었을지 잘 상상이 안 됩니다. 한번 볼까요? ^^




C# 컴파일러의 경우, foreach에 배열을 전달해 열거를 하게 되면,

int[] array = ...;
foreach (int i in array)
{
    Use(i);
}

원래의 IEnumerator/IEnumerable을 활용한 코드로 번역하지 않고,

int[] array = ...;
using (IEnumerator<int> e = array.GetEnumerator())
{
    while (e.MoveNext())
    {
        Use(e.Current);
    }
}

좀 더 빠른 성능을 내도록 일부러 indexer를 활용하는 식으로 바꿔서 번역한다고 합니다.

int[] array = ...;
for (int i = 0; i < array.Length; i++)
{
    Use(array[i]);
}

그리고, 이런 식의 전처리를 컴파일러 쪽의 용어로 "Lowering"이라고 일컫는다는데,,, ^^ Lowering을 한글로 뭐라고 표현하면 좋을까요? ^^ 의미상으로 보면 "구문 변환"이면서 다소 최적화를 하므로 "최적 구문 변환" 정도로 하면 될까요? ^^; (혹시 이에 해당하는 번역을 아시는 분은 덧글 부탁드립니다.)

그러니까, C# 10 컴파일러는 문자열 보간에 대해서도 단순히 이전처럼 string.Concat나 string.Format으로 번역하지 않고, 좀 더 성능이 좋은 코드로 바꾸게 되었는데요, 바로 그 역할을 위해 .NET 6에 추가된 타입이 System.Runtime.CompilerServices.DefaultInterpolatedStringHandler ref struct입니다.

생각했던 것보다 의외로 해결책은 간단합니다. DefaultInterpolatedStringHandler는 문자열 처리를 StringBuilder처럼 하는데, Append 시킬 문자열에 대해 제네릭 인자로 처리하기 때문에 박싱 문제에서 자유롭게 된 것입니다.

namespace System.Runtime.CompilerServices
{
    [InterpolatedStringHandler]
    public ref struct DefaultInterpolatedStringHandler
    {
        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount);
        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, System.IFormatProvider? provider);
        public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, System.IFormatProvider? provider, System.Span<char> initialBuffer);

        public void AppendLiteral(string value);

        public void AppendFormatted<T>(T value);
        public void AppendFormatted<T>(T value, string? format);
        public void AppendFormatted<T>(T value, int alignment);
        public void AppendFormatted<T>(T value, int alignment, string? format);

        public void AppendFormatted(ReadOnlySpan<char> value);
        public void AppendFormatted(ReadOnlySpan<char> value, int alignment = 0, string? format = null);

        public void AppendFormatted(string? value);
        public void AppendFormatted(string? value, int alignment = 0, string? format = null);
        public void AppendFormatted(object? value, int alignment = 0, string? format = null);

        public string ToStringAndClear();
    }
}

따라서, C# 9 이전에는 다음과 같은 식의 문자열 보간을 하면,

public static string FormatVersion(int major, int minor, int build, int revision) =>
    $"{major}.{minor}.{build}.{revision}";

이렇게 번역을 했지만,

public static string FormatVersion(int major, int minor, int build, int revision)
{
    var array = new object[4];
    array[0] = major; // 박싱 발생
    array[1] = minor; // 박싱 발생
    array[2] = build; // 박싱 발생
    array[3] = revision; // 박싱 발생
    return string.Format("{0}.{1}.{2}.{3}", array); // 내부에서 각각 ToString을 호출해 문자열 힙 할당 발생
}

C# 10부터는 이렇게 바뀌게 됩니다.

public static string FormatVersion(int major, int minor, int build, int revision)
{
    var handler = new DefaultInterpolatedStringHandler(literalLength: 3, formattedCount: 4);
    handler.AppendFormatted(major); // 박싱 및 문자열 힙 할당 없음
    handler.AppendLiteral(".");
    handler.AppendFormatted(minor); // 박싱 및 문자열 힙 할당 없음
    handler.AppendLiteral(".");
    handler.AppendFormatted(build); // 박싱 및 문자열 힙 할당 없음
    handler.AppendLiteral(".");
    handler.AppendFormatted(revision); // 박싱 및 문자열 힙 할당 없음
    return handler.ToStringAndClear();
}

박싱은 물론이고 개별 힙 할당도 없어지고 마지막의 ToStringAndClear에서 최종 문자열을 만들어 단 한 번의 힙 할당만 하고 있습니다.

또한, StringBuilder와는 달리 DefaultInterpolatedStringHandler는 AppendFormatted를 public으로 공개했기 때문에, 사용자 정의 구조체 타입에도 ISpanFormattable만 구현되어 있다면 그대로 사용할 수 있습니다. 일례로 아래의 예제 코드는 실행해 보면 Console.WriteLine에서 Person 타입의 object.ToString 메서드가 실행되지 않고 TryFormat 메서드가 실행되는 것을 확인할 수 있습니다.

using System.Diagnostics;
using System.Runtime.InteropServices;

Console.WriteLine(Format.FormatVersion(1, 5, 1, 2<span style='color: blue; font-weight: bold'>,
                new Person { Age = 25 }</span>));

public class Format
{
    public static string FormatVersion(int major, int minor, int build, int revision, Person person) =>
        $"{major}.{minor}.{build}.{revision}.{person}";
}

// "C# - .NET 6부터 공개된 ISpanFormattable 사용법" 글의 예제 코드
public struct Person : ISpanFormattable
{
    public int Age;

    public override string ToString()
    {
        return ToString(null, null);
    }

    // ...[생략]...

    public unsafe bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
    {
        return TryFormatInt32(this.Age, -1, format, provider, destination, out charsWritten);
    }

    // ...[생략]...
}




자, 그런데 과거 FormattableString이 나온 이유를 상기시켜 보면, 이제 저렇게 바뀐 문자열 보간 처리 방식도 여전히 Localization에 대한 문제가 남아 있다는 것을 알 수 있습니다. 역시나 마이크로소프트는 이 문제도 함께 해결해야만 했을 것이고, 이를 위해 문자열 보간 처리를 위임하는 DefaultInterpolatedStringHandler에게 사용자가 넘겨주는 Localization 정보를 활용할 수 있는 메서드를 제공하는 방식으로 우회하고 있습니다.

즉, 위의 예제 코드에서 만약 사용자가 Localization을 제공해야 한다면 우선 다음과 같은 식의 메서드를 하나 정의합니다.

public static string Create(IFormatProvider provider, 
    [InterpolatedStringHandlerArgument("provider")] ref DefaultInterpolatedStringHandler handler) =>
        handler.ToStringAndClear();

해당 메서드에는 InterpolatedStringHandlerArgument 특성이 DefaultInterpolatedStringHandler 매개 변수에 적용되었고, 그 인자로 "provider"라는 문자열로 IFormatProvider를 전달할 매개 변수 이름을 지정했습니다. 그리고 이렇게 만든 메서드를 이전의 FormatVersion 메서드에 다음과 같이 적용시켜 주면,

public class Format
{
    public static string FormatVersion(int major, int minor, int build, int revision) =>
        Create(CultureInfo.InvariantCulture, $"{major}.{minor}.{build}.{revision}");

    public static string Create(IFormatProvider provider, 
        [InterpolatedStringHandlerArgument("provider")] ref DefaultInterpolatedStringHandler handler) =>
            handler.ToStringAndClear();
}

C# 10 컴파일러는 이를 인지하고 FormatVersion 메서드의 내부 코드를 다음과 같이 생성해 줍니다.

public static string FormatVersion(int major, int minor, int build, int revision)
{
    IFormatProvider invariantCulture = CultureInfo.InvariantCulture;
    IFormatProvider provider = invariantCulture;
    DefaultInterpolatedStringHandler defaultInterpolatedStringHandler;
    defaultInterpolatedStringHandler..ctor(3, 4, invariantCulture);
    defaultInterpolatedStringHandler.AppendFormatted<int>(major);
    defaultInterpolatedStringHandler.AppendLiteral(".");
    defaultInterpolatedStringHandler.AppendFormatted<int>(minor);
    defaultInterpolatedStringHandler.AppendLiteral(".");
    defaultInterpolatedStringHandler.AppendFormatted<int>(build);
    defaultInterpolatedStringHandler.AppendLiteral(".");
    defaultInterpolatedStringHandler.AppendFormatted<int>(revision);
    return Format.Create(provider, ref defaultInterpolatedStringHandler);
}

이뿐만이 아닙니다. 사실 위의 코드를 자세히 들여다보면 결국 AppendFormatted로 들어갈 문자열들이 어딘가에는 버퍼로 있어야 한다는 것을 알 수 있으며 결국 그 버퍼들이 생성되는 문제가 있다는 것도 짐작할 수 있습니다. 마이크로소프트는 일단 이 문제를 ArrayPool을,

C# - ArrayPool<T> 소개
; https://www.sysnet.pe.kr/2/0/12478

C# - ArrayPool<T>와 MemoryPool<T> 소개
; https://www.sysnet.pe.kr/2/0/12480

이용하는 방식으로 기본 구현을 제공합니다. 하지만, 이것조차도 사용하고 싶지 않을 때, 즉 사용자 측에서 TryFormat으로 인해 필요한 버퍼의 크기를 미리 알 수 있다면 직접 stackalloc 등의 버퍼로 대체할 수 있도록 또 다른 인자를 제공합니다. 아래는 그렇게 해서 바뀐 Create 메서드와 그것을 사용해 FormatVersion2 메서드에 반영한 예제를 보여줍니다.

public static string Create(IFormatProvider provider, Span<char> myBuffer,
    [InterpolatedStringHandlerArgument("provider", "myBuffer")] ref DefaultInterpolatedStringHandler handler) =>
        handler.ToStringAndClear();

public static string FormatVersion2(int major, int minor, int build, int revision) =>
    Create(CultureInfo.InvariantCulture, stackalloc char[64], $"{major}.{minor}.{build}.{revision}");

그럼, C# 10 컴파일러는 ArrayPool로부터 버퍼를 받아오지 않고 사용자가 넘겨준 버퍼를 활용하도록 다음과 같은 코드를 생성합니다.

public unsafe static string FormatVersion2(int major, int minor, int build, int revision)
{
    IFormatProvider invariantCulture = CultureInfo.InvariantCulture;
    IFormatProvider formatProvider = invariantCulture;
    Span<char> span = new Span<char>(stackalloc byte[(UIntPtr)128], 64);
    Span<char> span2 = span;
    IFormatProvider provider = formatProvider;
    Span<char> myBuffer = span2;
    DefaultInterpolatedStringHandler defaultInterpolatedStringHandler;
    defaultInterpolatedStringHandler..ctor(3, 4, invariantCulture, span2);
    defaultInterpolatedStringHandler.AppendFormatted<int>(major);
    defaultInterpolatedStringHandler.AppendLiteral(".");
    defaultInterpolatedStringHandler.AppendFormatted<int>(minor);
    defaultInterpolatedStringHandler.AppendLiteral(".");
    defaultInterpolatedStringHandler.AppendFormatted<int>(build);
    defaultInterpolatedStringHandler.AppendLiteral(".");
    defaultInterpolatedStringHandler.AppendFormatted<int>(revision);
    return Format.Create(provider, myBuffer, ref defaultInterpolatedStringHandler);
}

그리고, 사용자의 편의를 위해 Create 메서드는 고정적으로 사용할 수 있는 유형인 까닭에 미리 string 타입에 포함시켰으므로 최종적으로는 위의 코드를 다음과 같이 간단하게 변경할 수 있습니다.

public class Format
{
    public static string FormatVersion(int major, int minor, int build, int revision) =>
        string.Create(CultureInfo.InvariantCulture, $"{major}.{minor}.{build}.{revision}");

    public static string FormatVersion2(int major, int minor, int build, int revision) =>
        string.Create(CultureInfo.InvariantCulture, stackalloc char[64], $"{major}.{minor}.{build}.{revision}");
}

복잡하게 설명했지만, Localization이나 사용자 버퍼를 전달하고 싶다면 string.Create를 사용하면 되고, 그 외의 경우라면 그냥 일반적인 보간 문자열을 예전처럼 사용하면 됩니다. (어쩌면, 원래부터 DefaultInterpolatedStringHandler가 관여됐다면 애당초 FormattableString은 나오지 않았을 것입니다.)




재미있는 것은 C# 10 컴파일러에게 DefaultInterpolatedStringHandler와 같은 타입을 사용자 정의해 인식시킬 수 있다는 점입니다. 이를 위해 필요한 것은 일정한 포맷을 가진 AppendLiteral, AppendFormatted와 같은 메서드를 담고 있을 것과 InterpolatedStringHandlerAttribute 특성만 부여돼 있으면 됩니다.

일례로 다음과 같은 타입을 하나 임의로 만들 수 있고,

[InterpolatedStringHandler]
public ref struct MyInterpolatedStringHandler
{
    public MyInterpolatedStringHandler(int literalLength, int formattedCount)
    {
    }

    public MyInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider provider)
    {
    }

    public MyInterpolatedStringHandler(int literalLength, int formattedCount, IFormatProvider provider, Span<char> initialBuffer)
    {
    }

    public string MakeText()
    {
        return null;
    }

    public void AppendLiteral(string value)
    {
    }

    public void AppendFormatted<T>(T value)
    {
    }
}

이것을 사용하는 FormatVersion 메서드를 자유롭게 정의할 수 있습니다.

public class Format
{
    public static string FormatVersion(int major, int minor, int build, int revision) =>
        string.Create(CultureInfo.InvariantCulture, $"{major}.{minor}.{build}.{revision}");

    public static string FormatVersion2(int major, int minor, int build, int revision) =>
        string.Create(CultureInfo.InvariantCulture, stackalloc char[64], $"{major}.{minor}.{build}.{revision}");

    public static string FormatVersion3(int major, int minor, int build, int revision) =>
        Format.UseMyHandler($"{major}.{minor}.{build}.{revision}");

    public static string UseMyHandler([InterpolatedStringHandlerArgument()] ref MyInterpolatedStringHandler handler) =>
        handler.MakeText();
}

그럼 C# 10 컴파일러는 여러분들이 정의한 타입을 이용해 문자열 보간 구문을 처리하는 코드를 생성합니다.

// Format
public static string FormatVersion3(int major, int minor, int build, int revision)
{
    MyInterpolatedStringHandler myInterpolatedStringHandler = new MyInterpolatedStringHandler(3, 4);
    myInterpolatedStringHandler.AppendFormatted<int>(major);
    myInterpolatedStringHandler.AppendLiteral(".");
    myInterpolatedStringHandler.AppendFormatted<int>(minor);
    myInterpolatedStringHandler.AppendLiteral(".");
    myInterpolatedStringHandler.AppendFormatted<int>(build);
    myInterpolatedStringHandler.AppendLiteral(".");
    myInterpolatedStringHandler.AppendFormatted<int>(revision);
    return Format.UseMyHandler(ref myInterpolatedStringHandler);
}




이와 관련한 변화들을 좀 볼까요? ^^

새로운 DefaultInterpolatedStringHandler도 사실 ref struct 타입은 처리할 수 없습니다. 왜냐하면, 위와 같은 처리 자체가 대상 값 타입이 ISpanFormattable을 구현하고 있는 경우에 한해 힙 할당을 없애는 방향으로 동작하기 때문에 인터페이스조차 상속할 수 없는 ref struct는 그에 대한 혜택을 받지 못합니다. 하지만, ref struct 유형 중 가장 많이 사용되는 ReadOnlySpan<char>에 대해서는 오버로드된 메서드를 가지고 있기 때문에,

public void AppendFormatted(ReadOnlySpan<char> value);
public void AppendFormatted(ReadOnlySpan<char> value, int alignment = 0, string? format = null);

문자열에 대한 처리에 한해서는 다음과 같이 직접 문자열 보간에 사용하는 것이 가능해졌습니다.

ReadOnlySpan<char> span = "Hello World!"[0..2].AsSpan();
string text = $"{span,4}"; // C# 9 이하에서는 컴파일 오류 - error CS0029: Cannot implicitly convert type 'System.ReadOnlySpan<char>' to 'object'

또한 StringBuilder의 경우 Append 메서드에 AppendInterpolatedStringHandler가 적용된 오버로드를 추가해,

; https://learn.microsoft.com/en-us/dotnet/api/system.text.stringbuilder.appendinterpolatedstringhandler

문자열 보간으로 Append하는 경우,

StringBuilder sb = new StringBuilder();
int value = 5;
sb.Append($"{value}");

AppendInterpolatedStringHandler를 사용하는 코드로 C# 10 컴파일러는 번역을 합니다.

StringBuilder.AppendInterpolatedStringHandler appendInterpolatedStringHandler;
appendInterpolatedStringHandler..ctor(0, 1, stringBuilder);
appendInterpolatedStringHandler.AppendFormatted<int>(value);
stringBuilder2.Append(ref appendInterpolatedStringHandler);

위의 번역이 재미있는 것이, 만약 여러분들이 기존에 StrinBuilder와 함께 사용하던 문자열 보간 코드가 있다면 C# 10 컴파일러로 다시 빌드하는 것만으로도 성능 향상의 기회를 가질 수 있다는 점입니다. ^^

마지막으로 AssertInterpolatedStringHandler를 제공해,

Debug.AssertInterpolatedStringHandler Struct
; https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.debug.assertinterpolatedstringhandler

Debug.Assert에서의 문자열 보간 사용 시 힙 메모리 할당을 최적화할 수 있게 되었고, TryWriteInterpolatedStringHandler를 제공해,

MemoryExtensions.TryWriteInterpolatedStringHandler Struct
; https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.trywriteinterpolatedstringhandler

제공해야 할 문자열 크기에 비해 버퍼가 작다면 Append 관련 코드를 빠르게 벗어날 수 있는 InterpolatedStringHandler가 제공되고 있습니다. 아마도, 이런 식으로 사용자 정의된 handler는 점점 더 늘어날 것이고 이로 인해 BCL은 계속해서 힙 메모리 사용을 줄이는 쪽으로 가게 될 것입니다.

(첨부 파일은 이 글의 예제 코드를 포함합니다.)




C# 10 - (1) 구조체를 생성하는 record struct (공식 문서, Static Abstract Members In Interfaces C# 10 Preview)
; https://www.sysnet.pe.kr/2/0/12790

C# 10 - (2) 전역 네임스페이스 선언 (공식 문서, Global Using Directive)
; https://www.sysnet.pe.kr/2/0/12792

C# 10 - (3) 개선된 변수 초기화 판정 (공식 문서, Improved Definite Assignment)
; https://www.sysnet.pe.kr/2/0/12793

C# 10 - (4) 상수 문자열에 포맷 식 사용 가능 (공식 문서, Constant Interpolated Strings)
; https://www.sysnet.pe.kr/2/0/12796

C# 10 - (5) 속성 패턴의 개선 (공식 문서, Extended property patterns)
; https://www.sysnet.pe.kr/2/0/12799

C# 10 - (6) record class 타입의 ToString 메서드를 sealed 처리 허용 (공식 문서, Sealed record ToString)
; https://www.sysnet.pe.kr/2/0/12801

C# 10 - (7) Source Generator V2 APIs (Source Generator V2 APIs)
; https://www.sysnet.pe.kr/2/0/12804

C# 10 - (8) 분해 구문에서 기존 변수의 재사용 가능 (공식 문서, Mix declarations and variables in deconstruction)
; https://www.sysnet.pe.kr/2/0/12805

C# 10 - (9) 비동기 메서드가 사용할 AsyncMethodBuilder 선택 가능 (공식 문서, Async method builder override); 
; https://www.sysnet.pe.kr/2/0/12807

C# 10 - (10) 개선된 #line 지시자 (공식 문서, Enhanced #line directive)
; https://www.sysnet.pe.kr/2/0/12812

C# 10 - (11) Lambda 개선 (공식 문서 1, 공식 문서 2, Lambda improvements) 
; https://www.sysnet.pe.kr/2/0/12813

C# 10 - (12) 문자열 보간 성능 개선 (공식 문서, Interpolated string improvements)
; https://www.sysnet.pe.kr/2/0/12826

C# 10 - (13) 단일 파일 내에 적용되는 namespace 선언 (공식 문서, File-scoped namespace)
; https://www.sysnet.pe.kr/2/0/12828

C# 10 - (14) 구조체 타입에 기본 생성자 정의 가능 (공식 문서, Parameterless struct constructors)
; https://www.sysnet.pe.kr/2/0/12829

C# 10 - (15) CallerArgumentExpression 특성 추가 (공식 문서, Caller expression attribute)
; https://www.sysnet.pe.kr/2/0/12835

Language Feature Status
; https://github.com/dotnet/roslyn/blob/main/docs/Language%20Feature%20Status.md




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 7/23/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2023-07-23 12시19분
String.Create is 🔥 for string concatenation
; https://twitter.com/Dave_DotNet/status/1682699744747958274/photo/1
정성태

1  2  3  4  5  6  7  8  9  10  [11]  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13344정성태5/9/20236306.NET Framework: 2116. C# - OpenAI API 사용 - 지원 모델 목록 [1]파일 다운로드1
13343정성태5/9/20234196디버깅 기술: 192. Windbg - Hyper-V VM으로 이더넷 원격 디버깅 연결하는 방법
13342정성태5/8/20234120.NET Framework: 2115. System.Text.Json의 역직렬화 시 필드/속성 주의
13341정성태5/8/20233904닷넷: 2114. C# 12 - 모든 형식의 별칭(Using aliases for any type)
13340정성태5/8/20233905오류 유형: 857. Microsoft.Data.SqlClient.SqlException - 0x80131904
13339정성태5/6/20234612닷넷: 2113. C# 12 - 기본 생성자(Primary Constructors)
13338정성태5/6/20234098닷넷: 2112. C# 12 - 기본 람다 매개 변수파일 다운로드1
13337정성태5/5/20234616Linux: 59. dockerfile - docker exec로 container에 접속 시 자동으로 실행되는 코드 적용
13336정성태5/4/20234373.NET Framework: 2111. C# - 바이너리 출력 디렉터리와 연관된 csproj 설정
13335정성태4/30/20234501.NET Framework: 2110. C# - FFmpeg.AutoGen 라이브러리를 이용한 기본 프로젝트 구성 - Windows Forms파일 다운로드1
13334정성태4/29/20234150Windows: 250. Win32 C/C++ - Modal 메시지 루프 내에서 SetWindowsHookEx를 이용한 Thread 메시지 처리 방법
13333정성태4/28/20233624Windows: 249. Win32 C/C++ - 대화창 템플릿을 런타임에 코딩해서 사용파일 다운로드1
13332정성태4/27/20233718Windows: 248. Win32 C/C++ - 대화창을 위한 메시지 루프 사용자 정의파일 다운로드1
13331정성태4/27/20233741오류 유형: 856. dockerfile - 구 버전의 .NET Core 이미지 사용 시 apt update 오류
13330정성태4/26/20233408Windows: 247. Win32 C/C++ - CS_GLOBALCLASS 설명
13329정성태4/24/20233621Windows: 246. Win32 C/C++ - 직접 띄운 대화창 템플릿을 위한 Modal 메시지 루프 생성파일 다운로드1
13328정성태4/19/20233254VS.NET IDE: 184. Visual Studio - Fine Code Coverage에서 동작하지 않는 Fake/Shim 테스트
13327정성태4/19/20233676VS.NET IDE: 183. C# - .NET Core/5+ 환경에서 Fakes를 이용한 단위 테스트 방법
13326정성태4/18/20235045.NET Framework: 2109. C# - 닷넷 응용 프로그램에서 SQLite 사용 (System.Data.SQLite) [1]파일 다운로드1
13325정성태4/18/20234392스크립트: 48. 파이썬 - PostgreSQL의 with 문을 사용한 경우 연결 개체 누수
13324정성태4/17/20234234.NET Framework: 2108. C# - Octave의 "save -binary ..."로 생성한 바이너리 파일 분석파일 다운로드1
13323정성태4/16/20234132개발 환경 구성: 677. Octave에서 Excel read/write를 위한 io 패키지 설치
13322정성태4/15/20234897VS.NET IDE: 182. Visual Studio - 32비트로만 빌드된 ActiveX와 작업해야 한다면?
13321정성태4/14/20233734개발 환경 구성: 676. WSL/Linux Octave - Python 스크립트 연동
13320정성태4/13/20233739개발 환경 구성: 675. Windows Octave 8.1.0 - Python 스크립트 연동
13319정성태4/12/20234171개발 환경 구성: 674. WSL 2 환경에서 GNU Octave 설치
1  2  3  4  5  6  7  8  9  10  [11]  12  13  14  15  ...