C# 컴파일러에서 해석하는 null 상태 정적 분석용 특성
null 허용 사용 컨텍스트에서 컴파일러는 코드의 정적 분석을 수행하여 모든 참조 형식 변수의 null 상태를 확인합니다.
- null이 아님: 정적 분석을 통해 변수에 null이 아닌 값을 갖는지 확인합니다.
- null일 수 있음: 정적 분석에서 변수에 null이 아닌 값이 할당되었는지 확인할 수 없습니다.
이러한 상태를 사용하면 null 값을 역참조하고 System.NullReferenceException을 throw할 때 컴파일러에서 경고를 제공할 수 있습니다. 이러한 특성은 인수 및 반환 값의 상태에 따라 인수의, 반환 값 및 개체 멤버의 null 상태에 대한 의미 체계 정보를 컴파일러에 제공합니다. 이 의미 체계 정보를 사용하여 API에 올바른 주석을 추가했을 때 컴파일러는 보다 정확한 경고를 제공합니다.
이 문서에서는 각 null 허용 참조 형식 특성 및 사용 방법에 대해 간단하게 설명합니다.
예제를 확인해보겠습니다. 라이브러리에 리소스 문자열을 검색하는 다음 API가 있다고 가정합니다. 이 메서드는 원래 nullable oblivious 컨텍스트에서 컴파일되었습니다.
bool TryGetMessage(string key, out string message)
{
if (_messageMap.ContainsKey(key))
message = _messageMap[key];
else
message = null;
return message != null;
}
앞의 예제는 .NET의 친숙한 Try*
패턴을 따릅니다. 이 API에 대한 두 개의 참조 매개 변수 key
및 message
가 있습니다. 이 API에는 해당 매개 변수의 null 상태에 관련된 다음 규칙이 있습니다.
- 호출자는
null
을key
의 인수로 전달하지 않아야 합니다. - 호출자는 값이
null
인 변수를message
의 인수로 전달할 수 있습니다. TryGetMessage
메서드가true
를 반환하면message
값은 null이 아닙니다. 반환 값이false
면 값message
이 null입니다.
key
에 대한 규칙은 간결하게 표현될 수 있습니다. key
는 null을 허용하지 않는 참조 형식이어야 합니다. message
매개 변수는 더 복잡합니다. 이 매개 변수는 null
인 변수를 인수로 허용하지만, 성공 시 out
인수가 null
이 아님을 보장합니다. 이 시나리오의 경우 기대치를 설명하는 더 다양한 어휘가 필요합니다. 아래에 설명된 NotNullWhen
특성은 message
매개 변수에 사용되는 인수의 null 상태를 설명합니다.
참고 항목
이 특성을 추가하면 API 규칙에 대한 자세한 정보가 컴파일러에 제공됩니다. 호출 코드가 null 허용 사용 가능 컨텍스트에서 컴파일되는 경우 컴파일러는 해당 규칙을 위반할 때 호출자에게 경고를 표시합니다. 이러한 특성은 구현에 대한 더 많은 검사를 사용하지 않습니다.
Attribute | 범주 | 의미 |
---|---|---|
AllowNull | 사전 조건 | null을 허용하지 않는 매개 변수, 필드 또는 속성은 null일 수 있습니다. |
DisallowNull | 사전 조건 | null 허용 매개 변수, 필드 또는 속성은 null일 수 없습니다. |
MaybeNull | 사후 조건 | null을 허용하지 않는 매개 변수, 필드, 속성 또는 반환 값은 null일 수 있습니다. |
NotNull | 사후 조건 | null 허용 매개 변수, 필드, 속성 또는 반환 값은 null일 수 없습니다. |
MaybeNullWhen | 조건부 사후 조건 | 메서드가 지정된 bool 값을 반환하는 경우 null을 허용하지 않는 인수는 null일 수 있습니다. |
NotNullWhen | 조건부 사후 조건 | 메서드가 지정된 bool 값을 반환하는 경우 null 허용 인수는 null이 아닙니다. |
NotNullIfNotNull | 조건부 사후 조건 | 지정된 매개 변수의 인수가 null이 아닌 경우 반환 값, 속성 또는 인수는 null이 아닙니다. |
MemberNotNull | 메서드 및 속성 도우미 메서드 | 메서드가 반환하는 경우 나열된 멤버는 null이 아닙니다. |
MemberNotNullWhen | 메서드 및 속성 도우미 메서드 | 메서드가 지정된 bool 값을 반환하는 경우 나열된 멤버는 null이 아닙니다. |
DoesNotReturn | 접근할 수 없는 코드 | 메서드 또는 속성이 값을 반환하지 않습니다. 즉, 항상 예외를 throw합니다. |
DoesNotReturnIf | 접근할 수 없는 코드 | 연결된 bool 매개 변수에 지정된 값이 있는 경우 이 메서드 또는 속성은 값을 반환하지 않습니다. |
앞의 설명은 각 특성이 수행하는 작업에 대한 빠른 참조입니다. 다음 섹션에서는 이러한 특성의 동작과 의미를 보다 자세히 설명합니다.
전제 조건: AllowNull
및 DisallowNull
읽기/쓰기 속성에는 적절한 기본값이 있으므로 null
을 반환하지 않는 읽기/쓰기 속성을 살펴봅니다. 호출자는 속성을 해당 기본값으로 설정할 때 set 접근자에 null
을 전달합니다. 예를 들어 대화방에서 화면 이름을 요청하는 메시지 시스템을 살펴봅니다. 아무것도 제공되지 않으면 시스템은 임의 이름을 생성합니다.
public string ScreenName
{
get => _screenName;
set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName;
null 허용 인식 불가능 컨텍스트에서 이전 코드를 컴파일하면 정상적으로 작동합니다. null 허용 참조 형식을 사용하도록 설정하면 ScreenName
속성이 null을 허용하지 않는 참조가 됩니다. get
접근자의 경우도 마찬가지입니다. 이 접근자는 null
을 반환하지 않습니다. 호출자는 null
에 대해 반환된 속성을 검사할 필요가 없습니다. 그러나 지금 속성을 null
로 설정하면 경고가 생성됩니다. 이 형식의 코드를 지원하려면 다음 코드와 같이 속성에 System.Diagnostics.CodeAnalysis.AllowNullAttribute 특성을 추가합니다.
[AllowNull]
public string ScreenName
{
get => _screenName;
set => _screenName = value ?? GenerateRandomScreenName();
}
private string _screenName = GenerateRandomScreenName();
이 특성과 이 문서에 설명된 다른 특성을 사용하려면 System.Diagnostics.CodeAnalysis에 대한 using
지시문을 추가해야 할 수 있습니다. 이 특성은 set
접근자가 아니라 속성에 적용됩니다. AllowNull
특성은 사전 조건을 지정하며 인수에만 적용됩니다. get
접근자에는 반환 값이 있지만 매개 변수가 없습니다. 따라서 AllowNull
특성은 set
접근자에만 적용됩니다.
앞의 예제에서는 인수에 대한 AllowNull
특성을 추가할 때 검색할 항목을 보여 줍니다.
- 해당 변수에 대한 일반 계약은
null
이 아니어야 하므로 null을 허용하지 않는 참조 형식이 필요합니다. - 가장 일반적으로 사용되는 것은 아니지만 호출자가
null
을 인수로 전달하는 시나리오가 있습니다.
일반적으로 속성이나 in
, out
및 ref
인수의 경우 이 특성이 필요합니다. AllowNull
특성은 일반적으로 변수가 null이 아닌 경우에 가장 적합하지만, null
을 전제 조건으로 허용해야 합니다.
DisallowNull
사용에 대한 시나리오와 비교해 보세요. 이 특성을 사용하여 null 허용 참조 형식의 인수가 null
이 아니도록 지정합니다. 기본값이 null
이지만 클라이언트에서 null이 아닌 값으로만 설정할 수 있는 속성을 살펴봅니다. 다음 코드를 생각해 봅시다.
public string ReviewComment
{
get => _comment;
set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string _comment;
이전 코드는 ReviewComment
가 null
일 수 있지만 null
로 설정될 수 없는 의도를 표현하는 가장 좋은 방법입니다. 이 코드가 null 허용 인식이 되면 System.Diagnostics.CodeAnalysis.DisallowNullAttribute를 사용하여 호출자에게 이 개념을 보다 명확하게 표현할 수 있습니다.
[DisallowNull]
public string? ReviewComment
{
get => _comment;
set => _comment = value ?? throw new ArgumentNullException(nameof(value), "Cannot set to null");
}
string? _comment;
null 허용 컨텍스트에서 ReviewComment
get
접근자는 null
의 기본값을 반환할 수 있습니다. 컴파일러는 액세스하기 전에 검사해야 한다는 경고를 표시합니다. 또한 null
일 수 있는 경우에도 호출자가 명시적으로 null
로 설정하지 않아야 한다는 경고를 호출자에게 표시합니다. 또한 DisallowNull
특성은 전제 조건을 지정하며 get
접근자에는 영향을 주지 않습니다. 다음에 대해 이 특성을 관찰할 때 DisallowNull
특성을 사용합니다.
- 변수는 처음 인스턴스화될 때 주로 핵심 시나리오에서
null
일 수 있습니다. - 변수를 명시적으로
null
로 설정하면 안 됩니다.
이 상황은 원래 ‘null 인식 불가능’이었던 코드에서 일반적입니다. 개체 속성은 두 개의 개별 초기화 작업에서 설정될 수 있습니다. 일부 속성은 일부 비동기 작업이 완료된 후에만 설정될 수 있습니다.
AllowNull
및 DisallowNull
특성을 사용하여 변수에 대한 전제 조건이 해당 변수의 null 허용 주석과 일치하지 않을 수 있도록 지정할 수 있습니다. 이 특성은 API의 특징에 대한 자세한 정보를 제공합니다. 이 추가 정보는 호출자가 API를 올바르게 사용하는 데 도움이 됩니다. 다음 특성을 사용하여 전제 조건을 지정해야 합니다.
- AllowNull: null을 허용하지 않는 인수는 null일 수 있습니다.
- DisallowNull: null 허용 인수는 null이 아니어야 합니다.
사후 조건: MaybeNull
및 NotNull
다음 시그니처를 사용하는 메서드가 있다고 가정합니다.
public Customer FindCustomer(string lastName, string firstName)
검색한 이름을 찾을 수 없는 경우 null
을 반환하도록 이 메서드를 작성했을 수 있습니다. null
은 분명히 레코드를 찾을 수 없음을 나타냅니다. 이 예제에서는 반환 형식을 Customer
에서 Customer?
로 변경할 수 있습니다. 반환 값을 null 허용 참조 형식으로 선언하면 이 API의 의도가 분명하게 지정됩니다.
public Customer? FindCustomer(string lastName, string firstName)
제네릭 null 허용 여부에 설명된 이유 때문에 해당 기술이 API와 일치하는 정적 분석을 생성하지 못할 수 있습니다. 비슷한 패턴을 따르는 제네릭 메서드가 있을 수 있습니다.
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)
검색한 항목을 찾을 수 없는 경우 메서드는 null
을 반환합니다. 메서드는 메서드 반환에 MaybeNull
주석을 추가하여 항목을 찾을 수 없을 때 null
을 반환한다는 것을 명확히 할 수 있습니다.
[return: MaybeNull]
public T Find<T>(IEnumerable<T> sequence, Func<T, bool> predicate)
위의 코드는 반환 값이 실제로 null일 수 있음을 호출자에게 알립니다. 또한 형식이 null을 허용하지 않는 경우에도 메서드가 null
식을 반환할 수 있음을 컴파일러에 알립니다. 해당 형식 매개 변수 T
의 인스턴스를 반환하는 제네릭 메서드가 있는 경우 NotNull
특성을 사용하여 null
이 반환되지 않도록 표현할 수 있습니다.
형식이 null 허용 참조 형식인 경우에도 반환 값이나 인수가 null이 아니도록 지정할 수도 있습니다. 다음 메서드는 첫 번째 인수가 null
인 경우 throw하는 도우미 메서드입니다.
public static void ThrowWhenNull(object value, string valueExpression = "")
{
if (value is null) throw new ArgumentNullException(nameof(value), valueExpression);
}
다음과 같이 이 루틴을 호출할 수 있습니다.
public static void LogMessage(string? message)
{
ThrowWhenNull(message, $"{nameof(message)} must not be null");
Console.WriteLine(message.Length);
}
null 참조 형식을 사용하도록 설정한 후 이전 코드가 경고 없이 컴파일되는지 확인하려고 합니다. 메서드가 값을 반환하는 경우 value
매개 변수는 null이 아님이 보장됩니다. 그러나 null 참조를 사용하여 ThrowWhenNull
를 호출할 수 있습니다. value
를 null 허용 참조 형식으로 만들고 NotNull
사후 조건을 매개 변수 선언에 추가할 수 있습니다.
public static void ThrowWhenNull([NotNull] object? value, string valueExpression = "")
{
_ = value ?? throw new ArgumentNullException(nameof(value), valueExpression);
// other logic elided
위의 코드는 기존 계약을 명확하게 표현합니다. 호출자는 null
값과 함께 변수를 전달할 수 있지만, 메서드가 예외를 throw하지 않고 반환하는 경우 인수가 null이 아님이 보장됩니다.
다음 특성을 사용하여 무조건 사후 조건을 지정합니다.
조건부 사후 조건: NotNullWhen
, MaybeNullWhen
및 NotNullIfNotNull
string
메서드 String.IsNullOrEmpty(String)에 대해 잘 알고 있을 수 있습니다. 인수가 null이거나 빈 문자열인 경우 이 메서드는 true
를 반환합니다. 이는 null 검사 형식입니다. 메서드가 false
를 반환하는 경우 호출자는 인수를 null 검사할 필요가 없습니다. 메서드를 이 null 허용 인식처럼 만들려면 인수를 null 허용 참조 형식으로 설정하고 NotNullWhen
특성을 추가합니다.
bool IsNullOrEmpty([NotNullWhen(false)] string? value)
이를 통해 반환 값이 false
인 코드는 null 검사가 필요하지 않음을 컴파일러에 알립니다. 특성을 추가하여 IsNullOrEmpty
가 필요한 null 검사를 수행한다는 것을 컴파일러의 정적 분석에 알립니다. false
를 반환하는 경우 인수는 null
이 아닙니다.
string? userInput = GetUserInput();
if (!string.IsNullOrEmpty(userInput))
{
int messageLength = userInput.Length; // no null check needed.
}
// null check needed on userInput here.
.NET Core 3.0의 경우 위에 표시된 대로 String.IsNullOrEmpty(String) 메서드가 주석으로 처리됩니다. null 값에 대한 개체의 상태를 확인하는 비슷한 메서드가 코드베이스에 있을 수 있습니다. 컴파일러는 사용자 지정 null 검사 메서드를 인식하지 않으며 주석을 직접 추가해야 합니다. 특성을 추가하면 컴파일러의 정적 분석은 테스트된 변수가 null 검사된 시기를 알 수 있습니다.
또한 이 특성은 Try*
패턴에 사용됩니다. ref
및 out
인수에 대한 사후 조건은 반환 값을 통해 전달됩니다. 앞서 표시된 다음 메서드를 사용하는 것이 좋습니다(null 허용 사용 안 함 컨텍스트).
bool TryGetMessage(string key, out string message)
{
if (_messageMap.ContainsKey(key))
message = _messageMap[key];
else
message = null;
return message != null;
}
이전 메서드는 일반적인 .NET 관용구를 따릅니다. 반환 값은 message
가 발견된 값으로 설정되었는지 또는 메시지가 없는 경우 기본값으로 설정되었는지 여부를 나타냅니다. 메서드가 true
를 반환하는 경우 message
의 값은 null이 아닙니다. 이 값을 반환하지 않는 경우 메서드는 message
를 null로 설정합니다.
null 허용 사용 컨텍스트에서는 NotNullWhen
특성을 사용하여 해당 관용구를 전달할 수 있습니다. null 허용 참조 형식의 매개 변수에 주석을 추가하는 경우 message
를 string?
으로 만들고 특성을 추가합니다.
bool TryGetMessage(string key, [NotNullWhen(true)] out string? message)
{
if (_messageMap.ContainsKey(key))
message = _messageMap[key];
else
message = null;
return message is not null;
}
이전 예제에서 TryGetMessage
가 true
를 반환하는 경우 message
값은 null이 아닌 것으로 알려져 있습니다. 동일한 방식으로 코드베이스에서 비슷한 메서드를 주석으로 처리해야 합니다. 인수는 null
과 동일할 수 있으며 메서드가 true
를 반환하는 경우 null이 아닌 것으로 알려져 있습니다.
한 가지 마지막 특성이 필요할 수도 있습니다. 경우에 따라 반환 값의 null 상태는 하나 이상의 인수의 null 상태에 따라 달라집니다. 해당 메서드는 특정 인수가 null
이 아닐 때마다 null이 아닌 값을 반환합니다. 해당 메서드를 주석으로 올바르게 처리하려면 NotNullIfNotNull
특성을 사용합니다. 다음 태그를 살펴봅니다.
string GetTopLevelDomainFromFullUrl(string url)
url
인수가 null이 아니면 출력은 null
이 아닙니다. null 허용 참조를 사용하도록 설정한 후에는 API에서 null 인수를 허용할 수 있는 경우 더 많은 주석을 추가해야 합니다. 다음 코드와 같이 반환 형식에 주석을 추가할 수 있습니다.
string? GetTopLevelDomainFromFullUrl(string? url)
또한 시그니처가 작동하지만 호출자가 추가 null
검사를 강제로 구현하는 경우가 많습니다. 계약은 인수 url
이 null
인 경우에만 반환 값이 null
이라는 것입니다. 해당 계약을 표현하려면 다음 코드와 같이 이 메서드를 주석으로 처리합니다.
[return: NotNullIfNotNull(nameof(url))]
string? GetTopLevelDomainFromFullUrl(string? url)
이전 예에서는 매개 변수 url
에 대해 nameof
연산자를 사용했습니다. 해당 기능은 C# 11에서 사용할 수 있습니다. C# 11 이전에는 매개 변수 이름을 문자열로 입력해야 합니다. 반환 값 및 인수 중 하나가 null
일 수 있음을 나타내는 ?
를 사용하여 두 항목을 모두 주석으로 처리했습니다. 특성은 url
인수가 null
이 아닌 경우 반환 값이 null이 아님을 명확하게 설명합니다.
해당 특성을 사용하여 조건부 사후 조건을 지정합니다.
- MaybeNullWhen: 메서드가 지정된
bool
값을 반환하는 경우 null을 허용하지 않는 인수는 null일 수 있습니다. - NotNullWhen: 메서드가 지정된
bool
값을 반환하는 경우 null 허용 인수는 null이 아닙니다. - NotNullIfNotNull: 지정된 매개 변수의 인수가 null이 아닌 경우 반환 값은 null이 아닙니다.
도우미 메서드: MemberNotNull
및 MemberNotNullWhen
이러한 특성은 생성자에서 공용 코드를 도우미 메서드로 리팩터링할 때 의도를 지정합니다. C# 컴파일러는 생성자 및 필드 이니셜라이저를 분석하여 각 생성자를 반환하기 전에 null을 허용하지 않는 모든 참조 필드가 초기화되도록 합니다. 그러나 C# 컴파일러가 모든 도우미 메서드에서 필드 할당을 추적하지는 않습니다. 컴파일러는 필드가 생성자에서 직접 초기화되지 않고 도우미 메서드에서 초기화되는 경우 CS8618
경고를 생성합니다. 메서드 선언에 MemberNotNullAttribute를 추가하고 메서드에서 Null이 아닌 값으로 초기화되는 필드를 지정합니다. 예를 들어 다음 예를 고려해 보겠습니다.
public class Container
{
private string _uniqueIdentifier; // must be initialized.
private string? _optionalMessage;
public Container()
{
Helper();
}
public Container(string message)
{
Helper();
_optionalMessage = message;
}
[MemberNotNull(nameof(_uniqueIdentifier))]
private void Helper()
{
_uniqueIdentifier = DateTime.Now.Ticks.ToString();
}
}
MemberNotNull
특성 생성자에 대한 인수로 여러 필드 이름을 지정할 수 있습니다.
MemberNotNullWhenAttribute에는 bool
인수가 있습니다. 도우미 메서드가 도우미 메서드에서 필드를 초기화했는지 여부를 나타내는 bool
을 반환하는 경우 MemberNotNullWhen
을 사용합니다.
호출한 메서드가 throw될 때 null 허용 분석 중지
일반적으로 예외 도우미 또는 기타 유틸리티 메서드와 같은 일부 메서드는 항상 예외를 throw하여 종료됩니다. 또는 도우미는 부울 인수 값을 기반으로 예외를 throw할 수 있습니다.
첫 번째 경우에 DoesNotReturnAttribute 특성을 메서드 선언에 추가할 수 있습니다. 컴파일러의 null 상태 분석은 DoesNotReturn
주석이 추가된 메서드의 호출 이후의 메서드에서 코드를 검사하지 않습니다. 이 메서드를 살펴봅니다.
[DoesNotReturn]
private void FailFast()
{
throw new InvalidOperationException();
}
public void SetState(object containedField)
{
if (containedField is null)
{
FailFast();
}
// containedField can't be null:
_field = containedField;
}
FailFast
를 호출한 후에는 컴파일러에서 경고를 생성하지 않습니다.
두 번째 경우에는 메서드의 부울 매개 변수에 System.Diagnostics.CodeAnalysis.DoesNotReturnIfAttribute 특성을 추가합니다. 이전 예제를 다음과 같이 수정할 수 있습니다.
private void FailFastIf([DoesNotReturnIf(true)] bool isNull)
{
if (isNull)
{
throw new InvalidOperationException();
}
}
public void SetFieldState(object? containedField)
{
FailFastIf(containedField == null);
// No warning: containedField can't be null here:
_field = containedField;
}
인수의 값이 DoesNotReturnIf
생성자의 값과 일치하는 경우 컴파일러는 해당 메서드 이후에 null 상태 분석을 수행하지 않습니다.
요약
null 허용 참조 형식을 추가하면 null
일 수 있는 변수의 API 기대치를 설명하는 초기 어휘가 제공됩니다. 특성은 변수의 null 상태를 전제 조건 및 사후 조건으로 설명하는 다양한 어휘를 제공합니다. 해당 특성은 기대치를 명확하게 설명하며 API를 사용하는 개발자에게 더 나은 환경을 제공합니다.
null 허용 컨텍스트의 라이브러리를 업데이트할 때 해당 특성을 추가하여 API 사용자가 올바르게 사용하도록 안내합니다. 해당 특성을 통해 인수 및 반환 값의 null 상태를 완벽하게 설명할 수 있습니다.
- AllowNull: null을 허용하지 않는 필드, 매개 변수 또는 속성이 null일 수 있습니다.
- DisallowNull: null 허용 필드, 매개 변수 또는 속성이 null일 수 없습니다.
- MaybeNull: null을 허용하지 않는 필드, 매개 변수, 속성 또는 반환 값이 null일 수 있습니다.
- NotNull: null 허용 필드, 매개 변수, 속성 또는 반환 값이 null일 수 없습니다.
- MaybeNullWhen: 메서드가 지정된
bool
값을 반환하는 경우 null을 허용하지 않는 인수는 null일 수 있습니다. - NotNullWhen: 메서드가 지정된
bool
값을 반환하는 경우 null 허용 인수는 null이 아닙니다. - NotNullIfNotNull: 지정된 매개 변수의 인수가 null이 아닌 경우 매개 변수, 속성 또는 반환 값이 null이 아닙니다.
- DoesNotReturn: 메서드 또는 속성이 값을 반환하지 않습니다. 즉, 항상 예외를 throw합니다.
- DoesNotReturnIf: 연결된
bool
매개 변수에 지정된 값이 있는 경우 이 메서드 또는 속성은 값을 반환하지 않습니다.
.NET