메모
이 문서는 기능 사양입니다. 사양은 기능의 디자인 문서 역할을 합니다. 여기에는 기능 디자인 및 개발 중에 필요한 정보와 함께 제안된 사양 변경 내용이 포함됩니다. 이러한 문서는 제안된 사양 변경이 완료되고 현재 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.True
및 Assert.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
이전 버전의 메서드에 대해 컴파일된 호출 사이트에서 해당 새 매개 변수를 실제로 수신하도록 보장되지는 않습니다.
해결되지 않은 질문
미정
디자인 회의
해당 없음
C# feature specifications