다음을 통해 공유


CallerArgumentExpression

메모

이 문서는 기능 사양입니다. 사양은 기능의 디자인 문서 역할을 합니다. 여기에는 기능 디자인 및 개발 중에 필요한 정보와 함께 제안된 사양 변경 내용이 포함됩니다. 이러한 문서는 제안된 사양 변경이 완료되고 현재 ECMA 사양에 통합될 때까지 게시됩니다.

기능 사양과 완료된 구현 간에 약간의 불일치가 있을 수 있습니다. 이러한 차이는 언어 디자인 모임(LDM) 관련 노트에 기록됩니다.

사양문서에서 기능 사양서를 C# 언어 표준으로 채택하는 절차에 대해 자세히 알아볼 수 있습니다.

챔피언 이슈: https://github.com/dotnet/csharplang/issues/287

요약

개발자가 메서드에 전달된 식을 캡처하여 진단/테스트 API에서 더 나은 오류 메시지를 사용하도록 설정하고 키 입력을 줄일 수 있습니다.

동기

어설션 또는 인수 유효성 검사에 실패하면 개발자는 실패한 위치와 이유에 대해 가능한 한 많이 알고 싶어 합니다. 그러나 오늘날의 진단 API는 이를 완전히 용이하게 하지는 않습니다. 다음 방법을 고려합니다.

T Single<T>(this T[] array)
{
    Debug.Assert(array != null);
    Debug.Assert(array.Length == 1);

    return array[0];
}

어설션 중 하나가 실패하면 파일 이름, 줄 번호 및 메서드 이름만 스택 추적에 제공됩니다. 개발자는 이 정보에서 실패한 어설션을 알 수 없습니다. 파일을 열고 제공된 줄 번호로 이동하여 무엇이 잘못되었는지 확인해야 합니다.

이는 테스트 프레임워크가 다양한 어설션 메서드를 제공해야 하는 이유이기도 합니다. xUnit에서는 실패한 항목에 대한 충분한 컨텍스트를 제공하지 않으므로 Assert.TrueAssert.False 자주 사용되지 않습니다.

잘못된 인수의 이름이 개발자에게 표시되므로 인수 유효성 검사에 좀 더 적합한 상황이지만 개발자는 이러한 이름을 예외에 수동으로 전달해야 합니다. 위의 예제가 Debug.Assert대신 기존 인수 유효성 검사를 사용하도록 다시 작성된 경우 다음과 같습니다.

T Single<T>(this T[] array)
{
    if (array == null)
    {
        throw new ArgumentNullException(nameof(array));
    }

    if (array.Length != 1)
    {
        throw new ArgumentException("Array must contain a single element.", nameof(array));
    }

    return array[0];
}

유의하세요, 맥락상 이미 명확한데도 불구하고 nameof(array)는 각 예외에 전달되어야 합니다.

상세 디자인

위의 예제에서는 어설션 메시지의 문자열 "array != null" 또는 "array.Length == 1" 포함하여 개발자가 실패한 내용을 결정하는 데 도움이 됩니다. CallerArgumentExpression입력합니다. 프레임워크가 특정 메서드 인수와 연결된 문자열을 가져오는 데 사용할 수 있는 특성입니다. 우리는 이렇게 Debug.Assert에 추가할 것입니다.

public static class Debug
{
    public static void Assert(bool condition, [CallerArgumentExpression("condition")] string message = null);
}

위의 예제의 소스 코드는 동일하게 유지됩니다. 그러나 컴파일러가 실제로 내보내는 코드는

T Single<T>(this T[] array)
{
    Debug.Assert(array != null, "array != null");
    Debug.Assert(array.Length == 1, "array.Length == 1");

    return array[0];
}

컴파일러는 Debug.Assert특성을 특별히 인식합니다. 호출 사이트에서 특성의 생성자(이 경우 condition)에 참조된 인수와 연결된 문자열을 전달합니다. 어떤 어설션이든지 실패하면 개발자는 false인 조건이 보여질 것이며, 어떤 어설션이 실패했는지를 알 수 있습니다.

인수 유효성 검사의 경우 특성을 직접 사용할 수 없지만 도우미 클래스를 통해 사용할 수 있습니다.

public static class Verify
{
    public static void Argument(bool condition, string message, [CallerArgumentExpression("condition")] string conditionExpression = null)
    {
        if (!condition) throw new ArgumentException(message: message, paramName: conditionExpression);
    }

    public static void InRange(int argument, int low, int high,
        [CallerArgumentExpression("argument")] string argumentExpression = null,
        [CallerArgumentExpression("low")] string lowExpression = null,
        [CallerArgumentExpression("high")] string highExpression = null)
    {
        if (argument < low)
        {
            throw new ArgumentOutOfRangeException(paramName: argumentExpression,
                message: $"{argumentExpression} ({argument}) cannot be less than {lowExpression} ({low}).");
        }

        if (argument > high)
        {
            throw new ArgumentOutOfRangeException(paramName: argumentExpression,
                message: $"{argumentExpression} ({argument}) cannot be greater than {highExpression} ({high}).");
        }
    }

    public static void NotNull<T>(T argument, [CallerArgumentExpression("argument")] string argumentExpression = null)
        where T : class
    {
        if (argument == null) throw new ArgumentNullException(paramName: argumentExpression);
    }
}

static T Single<T>(this T[] array)
{
    Verify.NotNull(array); // paramName: "array"
    Verify.Argument(array.Length == 1, "Array must contain a single element."); // paramName: "array.Length == 1"

    return array[0];
}

static T ElementAt<T>(this T[] array, int index)
{
    Verify.NotNull(array); // paramName: "array"
    // paramName: "index"
    // message: "index (-1) cannot be less than 0 (0).", or
    //          "index (6) cannot be greater than array.Length - 1 (5)."
    Verify.InRange(index, 0, array.Length - 1);

    return array[index];
}

이러한 도우미 클래스를 프레임워크에 추가하는 제안은 https://github.com/dotnet/corefx/issues/17068진행 중입니다. 이 언어 기능이 구현된 경우 이 기능을 활용하도록 제안을 업데이트할 수 있습니다.

확장 메서드

확장 메서드의 this 매개 변수는 CallerArgumentExpression참조할 수 있습니다. 예를 들어:

public static void ShouldBe<T>(this T @this, T expected, [CallerArgumentExpression("this")] string thisExpression = null) {}

contestant.Points.ShouldBe(1337); // thisExpression: "contestant.Points"

thisExpression 점 앞에 있는 개체에 해당하는 식을 받습니다. 정적 메서드 구문(예: Ext.ShouldBe(contestant.Points, 1337))을 사용하여 호출되는 경우 첫 번째 매개 변수가 this표시되지 않은 것처럼 동작합니다.

항상 this 매개 변수에 해당하는 식이 있어야 합니다. 클래스 인스턴스가 확장 메서드 자체를 호출하는 경우에도(예: 컬렉션 형식 내에서 this.Single()) this 컴파일러에 의해 위임되므로 "this" 전달됩니다. 나중에 이 규칙이 변경되면 null 또는 빈 문자열을 전달하는 것이 좋습니다.

추가 세부 정보

  • CallerMemberName같은 다른 Caller* 특성과 마찬가지로 이 특성은 기본값이 있는 매개 변수에서만 사용할 수 있습니다.
  • 위와 같이 CallerArgumentExpression 표시된 여러 매개 변수가 허용됩니다.
  • 특성의 네임스페이스는 System.Runtime.CompilerServices.
  • null 또는 매개 변수 이름이 아닌 문자열(예: "notAParameterName")이 제공되면 컴파일러는 빈 문자열을 전달합니다.
  • CallerArgumentExpressionAttribute 매개 변수가 적용되는 형식은 string에서 표준 변환이 가능해야 합니다. 즉, string에서의 사용자 정의 변환이 허용되지 않으며, 실제로 이러한 매개 변수의 유형은 string, object또는 string이 구현한 인터페이스여야 합니다.

단점

  • 디컴필러를 사용하는 방법을 아는 사용자는 이 특성으로 표시된 메서드에 대한 호출 사이트에서 일부 소스 코드를 볼 수 있습니다. 이는 폐쇄 소스 소프트웨어에 바람직하지 않거나 예상치 못한 것일 수 있습니다.

  • 기능 자체의 결함은 아니지만, 현재 bool만 사용하는 Debug.Assert API가 존재할 수 있다는 점이 우려될 수 있습니다. 메시지를 가져오는 오버로드에 이 특성으로 표시된 두 번째 매개 변수가 있고 선택적으로 만든 경우에도 컴파일러는 오버로드 확인에서 메시지 없음을 선택합니다. 따라서 이 기능을 활용하려면 메시지 없는 오버로드를 제거해야 합니다. 이는 이진 수준에서는 호환성이 손상될 수 있는 변경이지만, 소스 코드에는 영향을 미치지 않습니다.

대안

  • 메서드의 호출 지점에서 소스 코드를 볼 수 있는 것이 문제로 드러날 경우, 이 특성의 효과를 선택적으로 활성화할 수 있도록 할 수 있습니다. 개발자는 AssemblyInfo.cs에 전체 어셈블리에 적용할 [assembly: EnableCallerArgumentExpression] 특성을 추가하여 이를 활성화할 것입니다.
    • 특성의 효과를 사용할 수 없는 경우 특성으로 표시된 메서드를 호출하는 것은 오류가 되지 않으므로 기존 메서드가 특성을 사용하고 원본 호환성을 유지할 수 있습니다. 그러나 특성은 무시되고 제공된 기본값으로 메서드가 호출됩니다.
// Assembly1

void Foo(string bar); // V1
void Foo(string bar, string barExpression = "not provided"); // V2
void Foo(string bar, [CallerArgumentExpression("bar")] string barExpression = "not provided"); // V3

// Assembly2

Foo(a); // V1: Compiles to Foo(a), V2, V3: Compiles to Foo(a, "not provided")
Foo(a, "provided"); // V2, V3: Compiles to Foo(a, "provided")

// Assembly3

[assembly: EnableCallerArgumentExpression]

Foo(a); // V1: Compiles to Foo(a), V2: Compiles to Foo(a, "not provided"), V3: Compiles to Foo(a, "a")
Foo(a, "provided"); // V2, V3: Compiles to Foo(a, "provided")
  • Debug.Assert새 호출자 정보를 추가할 때마다 이진 호환성 문제가 발생하지 않도록 하기 위해 대체 솔루션은 호출자에 대한 모든 필요한 정보를 포함하는 프레임워크에 CallerInfo 구조체를 추가하는 것입니다.
struct CallerInfo
{
    public string MemberName { get; set; }
    public string TypeName { get; set; }
    public string Namespace { get; set; }
    public string FullTypeName { get; set; }
    public string FilePath { get; set; }
    public int LineNumber { get; set; }
    public int ColumnNumber { get; set; }
    public Type Type { get; set; }
    public MethodBase Method { get; set; }
    public string[] ArgumentExpressions { get; set; }
}

[Flags]
enum CallerInfoOptions
{
    MemberName = 1, TypeName = 2, ...
}

public static class Debug
{
    public static void Assert(bool condition,
        // If a flag is not set here, the corresponding CallerInfo member is not populated by the caller, so it's
        // pay-for-play friendly.
        [CallerInfo(CallerInfoOptions.FilePath | CallerInfoOptions.Method | CallerInfoOptions.ArgumentExpressions)] CallerInfo callerInfo = default(CallerInfo))
    {
        string filePath = callerInfo.FilePath;
        MethodBase method = callerInfo.Method;
        string conditionExpression = callerInfo.ArgumentExpressions[0];
        //...
    }
}

class Bar
{
    void Foo()
    {
        Debug.Assert(false);

        // Translates to:

        var callerInfo = new CallerInfo();
        callerInfo.FilePath = @"C:\Bar.cs";
        callerInfo.Method = MethodBase.GetCurrentMethod();
        callerInfo.ArgumentExpressions = new string[] { "false" };
        Debug.Assert(false, callerInfo);
    }
}

이것은 원래 https://github.com/dotnet/csharplang/issues/87에서 제안되었습니다.

이 방법의 몇 가지 단점은 다음과 같습니다.

  • 지정한 속성을 사용할 수 있도록 하여 과금 친화적임에도 불구하고, 어설션이 통과하더라도 식에 대한 배열을 할당하고 MethodBase.GetCurrentMethod을 호출하여 성능이 크게 저하될 수 있습니다.

  • 또한 새 플래그를 CallerInfo 특성에 전달하는 것은 호환성이 손상되는 변경이 아니지만 Debug.Assert 이전 버전의 메서드에 대해 컴파일된 호출 사이트에서 해당 새 매개 변수를 실제로 수신하도록 보장되지는 않습니다.

해결되지 않은 질문

미정

디자인 회의

해당 없음