C# 컴파일러에서 해석하는 기타 특성

Conditional, Obsolete, AttributeUsage, AsyncMethodBuilder, InterpolatedStringHandler, ModuleInitializer 특성은 코드의 요소에 적용할 수 있습니다. 해당 요소에 의미 체계 의미를 추가합니다. 컴파일러는 해당 의미 체계 의미를 사용하여 출력을 변경하고 코드를 사용하여 개발자의 가능한 실수를 보고합니다.

Conditional 특성

Conditional 특성을 사용하면 메서드 실행이 전처리 식별자에 따라 달라집니다. Conditional 특성은 ConditionalAttribute의 별칭이고 메서드 또는 특성 클래스에 적용할 수 있습니다.

다음 예제에서 Conditional은 프로그램 관련 진단 정보 표시를 사용하거나 사용하지 않도록 설정하는 메서드에 적용됩니다.

#define TRACE_ON
using System;
using System.Diagnostics;

namespace AttributeExamples
{
    public class Trace
    {
        [Conditional("TRACE_ON")]
        public static void Msg(string msg)
        {
            Console.WriteLine(msg);
        }
    }

    public class TraceExample
    {
        public static void Main()
        {
            Trace.Msg("Now in Main...");
            Console.WriteLine("Done.");
        }
    }
}

TRACE_ON 식별자가 정의되지 않으면 추적 출력이 표시되지 않습니다. 대화형 창에서 직접 살펴보세요.

Conditional 특성은 보통 DEBUG 식별자와 함께 사용하여 다음 예제에 표시된 대로 릴리스 빌드가 아닌 디버그 빌드의 추적 및 로깅 기능을 사용하도록 설정합니다.

[Conditional("DEBUG")]
static void DebugMethod()
{
}

조건부로 표시된 메서드를 호출하면 지정된 전처리 기호가 있는지 여부에 따라 컴파일러가 메서드 호출을 포함할지 여부가 결정됩니다. 기호가 정의되면 호출이 포함되고, 정의되지 않으면 호출이 생략됩니다. 조건부 메서드는 클래스 또는 구조체 선언의 메서드여야 하며 void 반환 형식을 포함해야 합니다. 메서드를 #if…#endif 블록 내부에 포함하는 것보다 Conditional을 사용하는 것이 더 분명하고 더 정교하며 오류 가능성이 더 작습니다.

메서드에 여러 Conditional 특성이 있는 경우 하나 이상의 조건부 기호가 정의되어 있으면 컴파일러가 메서드 호출을 포함합니다(기호는 OR 연산자를 사용하여 논리적으로 함께 연결됨). 다음 예제에서는 A 또는 B가 있으면 메서드 호출이 발생합니다.

[Conditional("A"), Conditional("B")]
static void DoIfAorB()
{
    // ...
}

특성 클래스와 함께 Conditional 사용

Conditional 특성을 특성 클래스 정의에 적용할 수도 있습니다. 다음 예제에서 사용자 지정 특성 DocumentationDEBUG가 정의된 경우에만 메타데이터에 정보를 추가합니다.

[Conditional("DEBUG")]
public class DocumentationAttribute : System.Attribute
{
    string text;

    public DocumentationAttribute(string text)
    {
        this.text = text;
    }
}

class SampleClass
{
    // This attribute will only be included if DEBUG is defined.
    [Documentation("This method displays an integer.")]
    static void DoWork(int i)
    {
        System.Console.WriteLine(i.ToString());
    }
}

Obsolete 특성

Obsolete 특성은 코드 요소를 더 이상 사용이 권장되지 않는 항목으로 표시합니다. 사용되지 않음으로 표시된 엔터티를 사용하면 경고나 오류가 생성됩니다. Obsolete 특성은 단일 사용 특성이고 특성을 허용하는 모든 엔터티에 적용할 수 있습니다. ObsoleteObsoleteAttribute의 별칭입니다.

다음 예제에서는 Obsolete 특성이 A 클래스 및 B.OldMethod 메서드에 적용됩니다. B.OldMethod에 적용된 특성 생성자의 두 번째 인수가 true로 설정되므로 이 메서드는 컴파일러 오류를 일으키지만, A 클래스를 사용하면 경고가 생성됩니다. 그러나 B.NewMethod를 호출하면 경고나 오류가 생성되지 않습니다. 예를 들어 이전 정의와 함께 사용할 경우 다음 코드에서는 두 개의 경고 및 하나의 오류가 생성됩니다.

using System;

namespace AttributeExamples
{
    [Obsolete("use class B")]
    public class A
    {
        public void Method() { }
    }

    public class B
    {
        [Obsolete("use NewMethod", true)]
        public void OldMethod() { }

        public void NewMethod() { }
    }

    public static class ObsoleteProgram
    {
        public static void Main()
        {
            // Generates 2 warnings:
            A a = new A();

            // Generate no errors or warnings:
            B b = new B();
            b.NewMethod();

            // Generates an error, compilation fails.
            // b.OldMethod();
        }
    }
}

특성 생성자에 첫 번째 인수로 제공된 문자열은 경고 또는 오류의 일부로 표시됩니다. A 클래스에 대한 두 개의 경고가 각각 클래스 참조 선언 및 클래스 생성자에 대해 생성됩니다. Obsolete 특성은 인수 없이 사용할 수 있지만 대신 사용이 권장되는 항목에 대한 설명을 포함합니다.

C# 10에서는 상수 문자열 보간 및 nameof 연산자를 사용하여 이름이 일치하는지 확인할 수 있습니다.

public class B
{
    [Obsolete($"use {nameof(NewMethod)} instead", true)]
    public void OldMethod() { }

    public void NewMethod() { }
}

AttributeUsage 특성

AttributeUsage 특성은 사용자 지정 특성 클래스를 사용하는 방법을 결정합니다. AttributeUsageAttribute는 사용자 지정 특성 정의에 적용되는 특성입니다. AttributeUsage 특성을 사용하면 다음을 제어할 수 있습니다.

  • 적용할 수 있는 프로그램 요소 특성 사용을 제한하지 않는 한, 다음과 같은 프로그램 요소 중 하나에 특성을 적용할 수 있습니다.
    • 어셈블리
    • 모듈
    • 필드
    • 이벤트
    • 메서드
    • 매개 변수
    • 속성
    • 반환 값
    • 형식
  • 특성을 단일 프로그램 요소에 여러 번 적용할 수 있는지 여부
  • 특성이 파생 클래스에게 상속되는지 여부

기본 설정을 명시적으로 적용할 경우 다음 예제와 같이 작성합니다.

[AttributeUsage(AttributeTargets.All,
                   AllowMultiple = false,
                   Inherited = true)]
class NewAttribute : Attribute { }

이 예제에서 NewAttribute 클래스는 모든 지원되는 프로그램 요소에 적용할 수 있습니다. 하지만 각 엔터티에 한 번만 적용할 수 있습니다. 이 특성은 기본 클래스에 적용될 때 파생 클래스에게 상속됩니다.

AllowMultipleInherited 인수는 선택 사항이므로 다음 코드는 동일한 효과를 가집니다.

[AttributeUsage(AttributeTargets.All)]
class NewAttribute : Attribute { }

첫 번째 AttributeUsageAttribute 인수는 AttributeTargets 열거형의 요소가 하나 이상이어야 합니다. 다음 예제와 같이 OR 연산자를 사용하여 여러 대상 형식을 함께 연결할 수 있습니다.

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
class NewPropertyOrFieldAttribute : Attribute { }

C# 7.3부터 특성은 속성 또는 자동 구현 속성의 지원 필드에 적용할 수 있습니다. 특성에 field 지정자를 지정하지 않는 한 특성이 속성에 적용됩니다. 두 경우 모두 다음 예제에서 표시됩니다.

class MyClass
{
    // Attribute attached to property:
    [NewPropertyOrField]
    public string Name { get; set; } = string.Empty;

    // Attribute attached to backing field:
    [field: NewPropertyOrField]
    public string Description { get; set; } = string.Empty;
}

AllowMultiple 인수가 true인 경우 다음 예제와 같이 결과 특성을 단일 엔터티에 두 번 이상 적용할 수 있습니다.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
class MultiUse : Attribute { }

[MultiUse]
[MultiUse]
class Class1 { }

[MultiUse, MultiUse]
class Class2 { }

이 경우 AllowMultipletrue로 설정되므로 MultiUseAttribute를 반복적으로 적용할 수 있습니다. 여러 특성을 적용하기 위해 표시된 두 형식이 모두 유효합니다.

Inheritedfalse인 경우 특성은 특성 클래스에서 파생된 클래스에서 상속하지 않습니다. 예를 들어:

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
class NonInheritedAttribute : Attribute { }

[NonInherited]
class BClass { }

class DClass : BClass { }

이 경우에 NonInheritedAttribute은 상속을 통해 DClass에 적용되지 않습니다.

이 키워드를 사용하여 특성을 적용할 위치를 지정할 수도 있습니다. 예를 들어 field: 지정자를 사용하여 field:의 지원 필드에 특성을 추가할 수 있습니다. 아니면 field:, property: 또는 param: 지정자를 사용하여 위치 레코드에서 생성된 요소에 특성을 적용할 수 있습니다. 예제의 경우 속성 정의에 대한 위치 구문을 참조하세요.

AsyncMethodBuilder 특성

C# 7부터 비동기 반환 형식일 수 있는 형식에 System.Runtime.CompilerServices.AsyncMethodBuilderAttribute 특성을 추가합니다. 이 특성은 지정된 형식이 비동기 메서드에서 반환될 때 비동기 메서드 구현을 빌드하는 형식을 지정합니다. AsyncMethodBuilder 특성은 다음과 같은 형식에 적용할 수 있습니다.

AsyncMethodBuilder 특성에 대한 생성자가 연결된 작성기의 형식을 지정합니다. 작성기는 다음과 같은 액세스 가능한 멤버를 구현해야 합니다.

  • 작성기의 형식을 반환하는 정적 Create() 메서드

  • 비동기 반환 형식을 반환하는 읽기 가능한 Task 속성

  • 작업에서 오류가 발생할 경우 예외를 설정하는 void SetException(Exception) 메서드

  • 작업을 완료됨으로 표시하고 선택적으로 작업의 결과를 설정하는 void SetResult() 또는 void SetResult(T result) 메서드

  • 다음 API 시그니처를 사용하는 Start 메서드:

    void Start<TStateMachine>(ref TStateMachine stateMachine)
              where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    
  • 다음 시그니처를 사용하는 AwaitOnCompleted 메서드:

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : System.Runtime.CompilerServices.INotifyCompletion
        where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    
  • 다음 시그니처를 사용하는 AwaitUnsafeOnCompleted 메서드:

          public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
              where TAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion
              where TStateMachine : System.Runtime.CompilerServices.IAsyncStateMachine
    

.NET에서 제공하는 다음 작성기를 검토하여 비동기 메서드 작성기에 대해 알아볼 수 있습니다.

C# 10 이상에서는 AsyncMethodBuilder 특성을 비동기 메서드에 적용하여 해당 형식에 대한 작성기를 재정의할 수 있습니다.

InterpolatedStringHandlerInterpolatedStringHandlerArguments 특성

C# 10부터 이러한 특성을 사용하여 형식을 ‘보간된 문자열 처리기’임을 나타냅니다. .NET 6 라이브러리에는 보간된 문자열을 string 매개 변수의 인수로 사용하는 시나리오에 대한 System.Runtime.CompilerServices.DefaultInterpolatedStringHandler가 이미 포함되어 있습니다. 보간된 문자열 처리 방법을 제어하려는 다른 인스턴스가 있을 수 있습니다. 처리기를 구현하는 형식에 System.Runtime.CompilerServices.InterpolatedStringHandlerAttribute를 적용합니다. 해당 형식 생성자의 매개 변수에 System.Runtime.CompilerServices.InterpolatedStringHandlerArgumentAttribute를 적용합니다.

보간된 문자열 향상된 기능에 대한 C# 10 기능 사양에서 보간된 문자열 처리기를 빌드하는 방법에 관해 자세히 알아볼 수 있습니다.

ModuleInitializer 특성

C# 9부터 ModuleInitializer 특성은 어셈블리가 로드될 때 런타임이 호출하는 메서드를 표시합니다. ModuleInitializerModuleInitializerAttribute의 별칭입니다.

ModuleInitializer 특성은 다음과 같은 메서드에만 적용할 수 있습니다.

  • 정적입니다.
  • 매개 변수가 없습니다.
  • void를 반환합니다.
  • 포함하는 모듈(internal 또는 public)에서 액세스할 수 있습니다.
  • 제네릭 메서드가 아닙니다.
  • 제네릭 클래스에 포함되지 않습니다.
  • 로컬 함수가 아닙니다.

ModuleInitializer 특성은 여러 메서드에 적용할 수 있습니다. 이 경우 런타임이 호출하는 순서는 결정적이지만 지정되지 않습니다.

다음 예제에서는 여러 모듈 이니셜라이저 메서드를 사용하는 방법을 보여 줍니다. Init1Init2 메서드는 Main 앞에 실행되며 각 메서드는 Text 속성에 문자열을 추가합니다. 따라서 Main이 실행될 때 Text 속성에는 양쪽 이니셜라이저 메서드의 문자열이 이미 포함되어 있습니다.

using System;

internal class ModuleInitializerExampleMain
{
    public static void Main()
    {
        Console.WriteLine(ModuleInitializerExampleModule.Text);
        //output: Hello from Init1! Hello from Init2!
    }
}
using System.Runtime.CompilerServices;

internal class ModuleInitializerExampleModule
{
    public static string? Text { get; set; }

    [ModuleInitializer]
    public static void Init1()
    {
        Text += "Hello from Init1! ";
    }

    [ModuleInitializer]
    public static void Init2()
    {
        Text += "Hello from Init2! ";
    }
}

경우에 따라 소스 코드 생성기는 초기화 코드를 생성해야 합니다. 모듈 이니셜라이저는 해당 코드의 표준 위치를 제공합니다. 다른 경우에는 대부분 모듈 이니셜라이저 대신 정적 생성자를 작성해야 합니다.

SkipLocalsInit 특성

C# 9부터 SkipLocalsInit 특성은 컴파일러가 메타데이터로 내보낼 때 .locals init 플래그를 설정하는 것을 방지합니다. SkipLocalsInit 특성은 단일 사용 특성이며 메서드, 속성, 클래스, 구조체, 인터페이스 또는 모듈에 적용하지만 어셈블리에는 적용할 수 없습니다. SkipLocalsInitSkipLocalsInitAttribute의 별칭입니다.

.locals init 플래그를 사용하면 CLR이 메서드에 선언된 모든 지역 변수를 기본값으로 초기화합니다. 컴파일러는 변수에 일부 값을 할당하기 전에 변수를 사용하지 않도록 하므로 .locals init는 일반적으로 필요하지 않습니다. 그러나 stackalloc를 사용하여 스택에서 배열을 할당하는 경우와 같이 일부 시나리오에서는 추가 0 초기화가 성능에 크게 영향을 줄 수 있습니다. 이 경우 SkipLocalsInit 특성을 추가할 수 있습니다. 메서드에 직접 적용되는 경우 특성은 해당 메서드 및 람다, 지역 함수를 포함한 모든 중첩 함수에 영향을 줍니다. 형식 또는 모듈에 적용되는 경우 내부에 중첩된 모든 메서드에 영향을 줍니다. 해당 특성은 추상 메서드에는 영향을 주지 않지만 구현을 위해 생성된 코드에는 영향을 줍니다.

해당 특성에는 AllowUnsafeBlocks 컴파일러 옵션이 필요합니다. 이 요구 사항은 일부 경우에 코드가 할당되지 않은 메모리를 읽을 수 있음을 나타냅니다(예: 초기화되지 않은 스택 할당 메모리에서 읽기).

다음 예제에서는 stackalloc를 사용하는 메서드에 대한 SkipLocalsInit 특성의 영향을 보여 줍니다. 해당 메서드는 정수 배열이 할당될 때 메모리에 있던 모든 항목을 표시합니다.

[SkipLocalsInit]
static void ReadUninitializedMemory()
{
    Span<int> numbers = stackalloc int[120];
    for (int i = 0; i < 120; i++)
    {
        Console.WriteLine(numbers[i]);
    }
}
// output depends on initial contents of memory, for example:
//0
//0
//0
//168
//0
//-1271631451
//32767
//38
//0
//0
//0
//38
// Remaining rows omitted for brevity.

이 코드를 직접 사용해 보려면 AllowUnsafeBlocks 파일에서 AllowUnsafeBlocks 컴파일러 옵션을 설정합니다.

<PropertyGroup>
  ...
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

참조