사용자 지정 특성 정의 및 읽기

특성은 선언적으로 정보를 코드와 연결하는 방법을 제공합니다. 또한 다양한 대상에 적용할 수 있는 재사용 가능한 요소를 제공할 수도 있습니다. ObsoleteAttribute를 고려합니다. 이 특성은 클래스, 구조체, 메서드, 생성자 등에 적용될 수 있으며 요소가 더 이상 필요하지 않다는 사실을 선언합니다. 이 특성을 찾고 응답으로 특정 작업을 수행하는 것은 모두 C# 컴파일러가 진행합니다.

이 자습서에서는 코드에 특성을 추가하는 방법, 고유한 특성을 만들고 사용하는 방법, .NET에 기본 제공되는 일부 특성을 사용하는 방법을 알아봅니다.

필수 조건

.NET을 실행하려면 컴퓨터를 설정해야 합니다. .NET 다운로드 페이지에서 설치 지침을 찾을 수 있습니다. Windows, Ubuntu Linux, macOS 또는 Docker 컨테이너에서 이 애플리케이션을 실행할 수 있습니다. 원하는 코드 편집기를 설치해야 합니다. 다음 설명에서는 오픈 소스 플랫폼 간 편집기인 Visual Studio Code를 사용합니다. 그러나 자신에게 편리한 도구를 사용할 수 있습니다.

앱 만들기

이제 모든 도구를 설치했으므로 새 .NET 콘솔 앱을 만듭니다. 명령줄 생성기를 사용하려면 즐겨 사용하는 셸에서 다음 명령을 실행합니다.

dotnet new console

이 명령은 기본 .NET 프로젝트 파일을 만듭니다. dotnet restore를 실행하여 이 프로젝트를 컴파일하는 데 필요한 종속성을 복원합니다.

dotnet new, dotnet build, dotnet run, dotnet test, dotnet publishdotnet pack 등 복원이 필요한 모든 명령에 의해 암시적으로 실행되므로 dotnet restore를 실행할 필요가 없습니다. 암시적 복원을 사용하지 않으려면 --no-restore 옵션을 사용합니다.

dotnet restore 명령은 Azure DevOps Services의 연속 통합 빌드 또는 복원 발생 시점을 명시적으로 제어해야 하는 빌드 시스템과 같이 명시적으로 복원이 가능한 특정 시나리오에서 여전히 유용합니다.

NuGet 피드를 관리하는 방법에 대한 자세한 내용은 dotnet restore 설명서를 참조하세요.

이 프로그램을 실행하려면 dotnet run을 사용합니다. 콘솔에 "Hello, World" 출력이 표시됩니다.

코드에 특성 추가

C#에서 특성은 Attribute 기본 클래스에서 상속되는 클래스입니다. Attribute에서 상속되는 모든 클래스는 코드의 다른 부분에서 일종의 "태그"로 사용될 수 있습니다. 예를 들어, ObsoleteAttribute라는 특성이 있습니다. 이 특성은 코드가 더 이상 사용되지 않으며 더 이상 사용되어서는 안 된다는 신호입니다. 예를 들어 대괄호를 사용하여 클래스에 이 특성을 배치합니다.

[Obsolete]
public class MyClass
{
}

클래스 이름은 ObsoleteAttribute이지만 코드에서는 [Obsolete]만 사용하면 됩니다. 대부분의 C# 코드는 이 규칙을 따릅니다. 원할 경우 전체 이름 [ObsoleteAttribute]를 사용할 수 있습니다.

클래스를 더 이상 사용되지 않는 것으로 표시할 경우 더 이상 사용되지 않는 이유 및/또는 대싱 사용할 항목에 대한 정보를 제공하는 것이 좋습니다. 이 설명을 제공하려면 Obsolete 특성에 문자열 매개 변수를 포함합니다.

[Obsolete("ThisClass is obsolete. Use ThisClass2 instead.")]
public class ThisClass
{
}

문자열은 마치 var attr = new ObsoleteAttribute("some string")을 작성하는 것처럼 ObsoleteAttribute 생성자에 인수로 전달됩니다.

특성 생성자에 대한 매개 변수는 단순 형식/리터럴인 bool, int, double, string, Type, enums, etc 및 해당 형식의 배열로 제한됩니다. 식이나 변수를 사용할 수 없습니다. 위치 매개 변수나 명명된 매개 변수를 자유롭게 사용할 수 있습니다.

고유한 특성 만들기

Attribute 기본 클래스에서 상속되는 새 클래스를 정의하여 특성을 만듭니다.

public class MySpecialAttribute : Attribute
{
}

앞의 코드를 사용하면 코드베이스의 다른 곳에서 [MySpecial](또는 [MySpecialAttribute])를 특성으로 사용할 수 있습니다.

[MySpecial]
public class SomeOtherClass
{
}

ObsoleteAttribute 트리거 같은 .NET 기본 클래스 라이브러리의 특성은 컴파일러 내에 특정 동작을 포함합니다. 그러나 만드는 특성은 메타데이터의 역할만 수행하며 특성 클래스 내의 코드는 실행되지 않습니다. 코드의 다른 곳에서 해당 메타데이터에 대해 조치를 취하는 것은 사용자에게 달려 있습니다.

여기서 주의해야 할 '문제'가 있습니다. 앞서 언급했듯이 특성을 사용할 때 특정 형식만 인수로 전달될 수 있습니다. 그러나 특성 유형을 만들 때 C# 컴파일러는 사용자가 해당 매개 변수를 만들지 못하게 하지 않습니다. 다음 예에서는 올바르게 컴파일되는 생성자를 사용하여 특성을 만들었습니다.

public class GotchaAttribute : Attribute
{
    public GotchaAttribute(Foo myClass, string str)
    {
    }
}

그러나 이 생성자를 특성 구문과 함께 사용할 수는 없습니다.

[Gotcha(new Foo(), "test")] // does not compile
public class AttributeFail
{
}

앞의 코드는 Attribute constructor parameter 'myClass' has type 'Foo', which is not a valid attribute parameter type과 같은 컴파일러 오류를 발생시킵니다.

특성 사용을 제한하는 방법

특성은 다음 "대상"에 사용할 수 있습니다. 앞의 예에서는 클래스에 대해 보여 주지만 다음에서도 사용할 수 있습니다.

  • 어셈블리
  • 클래스
  • 생성자
  • 대리인
  • 열거형
  • 이벤트
  • 필드
  • GenericParameter
  • 인터페이스
  • 메서드
  • 모듈
  • 매개 변수
  • 속성
  • ReturnValue
  • 구조체

특성 클래스를 만들 때 기본적으로 C#을 사용하면 가능한 모든 특성 대상에서 해당 특성을 사용할 수 있습니다. 특성을 특정 대상으로 제한하려는 경우 특성 클래스에 대해 AttributeUsageAttribute를 사용하면 됩니다. 맞습니다. 특성에 대한 특성입니다.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class MyAttributeForClassAndStructOnly : Attribute
{
}

클래스 또는 구조체 이외의 항목에 대해 위 특성을 적용하려고 하면 Attribute 'MyAttributeForClassAndStructOnly' is not valid on this declaration type. It is only valid on 'class, struct' declarations와 같은 컴파일러 오류가 발생합니다.

public class Foo
{
    // if the below attribute was uncommented, it would cause a compiler error
    // [MyAttributeForClassAndStructOnly]
    public Foo()
    { }
}

코드 요소에 연결된 특성을 사용하는 방법

특성는 메타데이터로 작동합니다. 외부의 힘이 없으면 실제로 아무것도 하지 않습니다.

특성을 찾아서 작업하려면 리플렉션이 필요합니다. 리플렉션을 사용하면 다른 코드를 검사하는 코드를 C#으로 작성할 수 있습니다. 예를 들어 리플렉션을 사용하여 클래스에 대한 정보(코드 헤드에 using System.Reflection; 추가)를 가져올 수 있습니다.

TypeInfo typeInfo = typeof(MyClass).GetTypeInfo();
Console.WriteLine("The assembly qualified name of MyClass is " + typeInfo.AssemblyQualifiedName);

다음과 같이 인쇄됩니다. The assembly qualified name of MyClass is ConsoleApplication.MyClass, attributes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

TypeInfo 개체(또는 MemberInfo, FieldInfo 또는 기타 개체)가 있으면 GetCustomAttributes 메서드를 사용할 수 있습니다. 이 메서드는 Attribute 개체의 컬렉션을 반환합니다. GetCustomAttribute를 사용하고 특성 유형을 지정할 수도 있습니다.

MyClassMemberInfo 인스턴스에 대해 GetCustomAttributes를 사용하는 예제는 다음과 같습니다(앞에서 살펴본 [Obsolete] 특성을 포함하는 예제).

var attrs = typeInfo.GetCustomAttributes();
foreach(var attr in attrs)
    Console.WriteLine("Attribute on MyClass: " + attr.GetType().Name);

콘솔에 인쇄됩니다. Attribute on MyClass: ObsoleteAttribute. MyClass에 다른 특성을 추가해 보세요.

이러한 Attribute 개체가 지연되어 인스턴스화되는 것에 유의해야 합니다. 즉, GetCustomAttribute 또는 GetCustomAttributes를 사용할 때까지 인스턴스화되지 않습니다. 또한 매번 인스턴스화됩니다. GetCustomAttributes를 연속으로 두 번 호출하면 ObsoleteAttribute의 서로 다른 두 인스턴스가 반환됩니다.

런타임의 공통 특성

특성은 많은 도구 및 프레임워크에서 사용됩니다. NUnit은 NUnit Test Runner에서 사용되는 [Test][TestFixture] 같은 특성을 사용합니다. ASP.NET MVC는 [Authorize]와 같은 특성을 사용하고 MVC 작업에 대해 크로스 커팅(Cross-Cutting) 문제를 해결하기 위한 작업 필터 프레임워크를 제공합니다. PostSharp은 특성 구문을 사용하여 C#을 사용한 AOP(Aspect-Oriented Programming)를 허용합니다.

.NET Core 기본 클래스 라이브러리에 기본 제공되는 몇 가지 유의할 만한 특성은 다음과 같습니다.

  • [Obsolete]. 이 특성은 위 예제에서 사용되었으며 System 네임스페이스에 있습니다. 기본 코드를 변경하는 방법에 대해 선언적 설명서를 제공하는 것이 유용합니다. 메시지는 문자열의 형태로 제공될 수 있으며 다른 부울 매개 변수가 컴파일러 경고를 컴파일러 오류로 에스컬레이션하는 데 사용될 수 있습니다.
  • [Conditional]. 이 특성은 System.Diagnostics 네임스페이스에 있습니다. 이 특성은 메서드(또는 특성 클래스)에 적용할 수 있습니다. 생성자에는 문자열을 전달해야 합니다. 해당 문자열이 #define 지시문과 일치하지 않으면 C# 컴파일러는 해당 메서드에 대한 모든 호출을 제거합니다(메서드 자체는 제외). 일반적으로 디버깅(진단) 목적으로 이 기술을 사용합니다.
  • [CallerMemberName]. 이 특성은 매개 변수에 사용될 수 있으며 System.Runtime.CompilerServices 네임스페이스에 있습니다. CallerMemberName은 다른 메서드를 호출하는 메서드의 이름을 삽입하는 데 사용되는 특성입니다. 다양한 UI 프레임워크에서 INotifyPropertyChanged를 구현할 때 '매직의 문자열'을 제거하는 방법입니다. 예를 들어
public class MyUIClass : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    public void RaisePropertyChanged([CallerMemberName] string propertyName = default!)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private string? _name;
    public string? Name
    {
        get { return _name;}
        set
        {
            if (value != _name)
            {
                _name = value;
                RaisePropertyChanged();   // notice that "Name" is not needed here explicitly
            }
        }
    }
}

위의 코드에서는 리터럴 "Name" 문자열이 없어도 됩니다. CallerMemberName을 사용하면 오타 관련 버그를 방지하고 리팩터링/이름 바꾸기를 더 원활하게 할 수 있습니다. 특성은 C#에 선언적 기능을 제공하지만 코드의 메타데이터 형식이며 단독으로 동작하지 않습니다.