Microsoft MVP성태의 닷넷 이야기
.NET Framework: 1113. C# 10 - (12) 문자열 보간 성능 개선 [링크 복사], [링크+제목 복사],
조회: 19714
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 2개 있습니다.)
(시리즈 글이 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

Optimizing memory usage with modern .NET features
; https://mijailovic.net/2025/04/10/memory-optimizations/
정성태

... [136]  137  138  139  140  141  142  143  144  145  146  147  148  149  150  ...
NoWriterDateCnt.TitleFile(s)
1655정성태3/19/201423226Windows: 92. Thumbs.db 파일이 삭제 안 되는 문제
1654정성태3/19/201425312개발 환경 구성: 219. SOS.dll 확장 모듈을 버전 별로 구하는 방법 [4]
1653정성태3/13/201420146.NET Framework: 428. .NET Reflection으로 다차원/Jagged 배열을 구분하는 방법
1652정성태3/12/201421213VC++: 76. Direct Show를 사용하는 다른 프로그램의 필터 그래프를 graphedt.exe에서 확인하는 방법파일 다운로드1
1651정성태3/11/201424872.NET Framework: 427. C# 컴파일러는 변수를 초기화시키지 않을까요?
1650정성태3/6/201425639VC++: 75. Visual C++ 컴파일 오류 - Cannot use __try in functions that require object unwinding [1]파일 다운로드1
1649정성태3/5/201420314기타: 44. BTN 스토어 앱 개인정보 보호 정책 안내
1648정성태3/5/201420674개발 환경 구성: 218. 스토어 앱 인증 실패 - no privacy statement
1647정성태3/3/201421950오류 유형: 224. 스카이드라이브 비정상 종료 - Error 0x80040A41: No error description available
1646정성태3/3/201431182오류 유형: 223. Microsoft-Windows-DistributedCOM 10016 이벤트 로그 에러 [1]
1645정성태3/1/201420925기타: 43. 마이크로소프트 MVP들이 모여 전국 세미나를 엽니다.
1644정성태2/26/201427875.NET Framework: 426. m3u8 스트리밍 파일을 윈도우 8.1 Store App에서 재생하는 방법파일 다운로드1
1643정성태2/25/201423719오류 유형: 222. 윈도우 8 Store App - APPX1204 SignTool Error: An unexpected internal error has occurred [1]
1642정성태2/25/201428311Windows: 91. 한글이 포함된 사용자 프로파일 경로 변경 [2]
1641정성태2/24/201425134기타: 42. 클래스 설명 [5]
1640정성태2/24/201446087.NET Framework: 425. C# - VLC(ActiveX) 컨트롤을 레지스트리 등록 없이 사용하는 방법 [15]
1639정성태2/23/201421811기타: 41. BBS 스토어 앱 개인정보 보호 정책 안내
1638정성태2/18/201444479Windows: 90. 실행 파일로부터 관리자 요구 권한을 제거하는 방법(부제: 크랙 버전을 보다 안전하게 실행하는 방법) [8]
1637정성태2/14/201425646Windows: 89. 컴퓨터를 껐는데도 어느 순간 자동으로 켜진다면? - 두 번째 이야기
1636정성태2/14/201421502Windows: 88. Hyper-V가 설치된 컴퓨터의 윈도우 백업 설정
1635정성태2/14/201422471오류 유형: 221. SharePoint - System.InvalidOperationException: The farm is unavailable.
1634정성태2/14/201422643.NET Framework: 424. C# - CSharpCodeProvider로 컴파일한 메서드의 실행이 일반 메서드보다 더 빠르다? [1]파일 다운로드1
1633정성태2/13/201425562오류 유형: 220. 2014년 2월 13일 이후로 Visual Studio 2010 Macro가 동작하지 않는다면? [3]
1632정성태2/12/201443488.NET Framework: 423. C#에서 DirectShow를 이용한 미디어 재생 [2]파일 다운로드1
1631정성태2/11/201422501개발 환경 구성: 217. Realtek 사운드 장치에서 재생되는 오디오를 GraphEditor로 녹음하는 방법
1630정성태2/5/201422818개발 환경 구성: 216. Hyper-V에 올려진 윈도우 XP VM에서 24bit 컬러 및 ClearType 활성화하는 방법
... [136]  137  138  139  140  141  142  143  144  145  146  147  148  149  150  ...