멤버 액세스 연산자와 식 — 점(.), 인덱서, 호출 연산자
멤버 액세스 연산자와 식 — 점(.), 인덱서, 호출 연산자
타입의 멤버에 접근할 때 우리는 여러 연산자와 식을 써요. 멤버 액세스 연산자에는 멤버 액세스(.), 배열 요소 또는 인덱서 액세스([]), 끝에서부터의 인덱스(^), 범위(..), null 조건부 연산자(?.와 ?[]), 그리고 메서드 호출(())이 있어요. 이 중에서 null 조건부 멤버 액세스(?.)와 인덱서 액세스(?[]) 연산자도 포함되고요.
.(멤버 액세스): 네임스페이스나 타입의 멤버에 접근해요.[](배열 요소 또는 인덱서 액세스): 배열 요소나 타입 인덱서에 접근해요.?.와?[](null 조건부 연산자): 피연산자가 null이 아닐 때만 멤버나 요소 액세스 연산을 수행해요.()(호출): 접근한 메서드를 호출하거나 대리자를 실행해요.^(끝에서부터의 인덱스): 요소 위치가 시퀀스의 끝에서부터임을 나타내요...(범위): 시퀀스 요소의 범위를 얻는 데 쓸 인덱스 범위를 지정해요.
멤버 액세스 식 .
. 토큰으로 네임스페이스나 타입의 멤버에 접근해요. 아래 예시에서 보는 것과 같은 방식으로요.
- 네임스페이스 안의 중첩 네임스페이스에 접근할 때
.을 써요. 다음using지시문 예시가 그 경우예요.
NestedNamespace
(using 지시문으로 중첩 네임스페이스 접근을 단순화하는 예시는 원문의 실제 스니펫에서 확인해요.)
- 네임스페이스 안의 타입에 접근할 때
.으로 정규화된 이름(qualified name)을 만들어요. 다음 코드처럼요.
Namespace.Type
using 지시문을 쓰면 정규화된 이름을 쓰지 않아도 돼요.
- 정적/비정적의 타입 멤버에 접근할 때도
.을 써요. 다음 코드처럼요.
obj.Member
그리고 .은 확장 멤버에 접근할 때도 쓸 수 있어요.
참고: 위 예시들은 원문에서 개별 코드 스니펫(
MemberAccessOperators.cs의NestedNamespace,QualifiedName,TypeMemberAccess)으로 제공돼요. 대표 형태만 축약해 보여드렸으니, 정확한 코드는 문서 원본 스니펫을 확인해 주세요.
인덱서 연산자 []
대괄호 []는 보통 배열, 인덱서, 포인터 요소에 접근해요. C# 12부터는 []가 컬렉션 식을 감싸는 데도 쓰여요.
배열 액세스
배열 요소에 접근하는 방법은 다음 예시로 확인할 수 있어요.
// MemberAccessOperators.cs 의 "Arrays" 스니펫
int[] a = { 1, 2, 3 };
a[0] = 10; // 배열 요소 접근
배열 인덱스가 해당 차원의 경계를 벗어나면 런타임이 xref:System.IndexOutOfRangeException을 던져요.
앞 예시에서 봤듯이, 배열 타입을 선언하거나 배열 인스턴스를 만들 때도 대괄호를 써요.
배열에 대한 자세한 내용은 배열 문서를 참고하세요.
인덱서 액세스
다음 예시는 .NET의 xref:System.Collections.Generic.Dictionary`2 타입으로 인덱서 액세스를 보여줘요.
// MemberAccessOperators.cs 의 "Indexers" 스니펫
var dict = new Dictionary<string, int>();
dict["key"] = 1; // 인덱서 접근
인덱서는 사용자 정의 타입의 인스턴스를 배열 인덱싱과 비슷한 방식으로 인덱싱할 수 있게 해줘요. 배열 인덱스는 반드시 정수여야 하지만, 인덱서 매개변수는 어떤 타입으로도 선언할 수 있다는 점이 달라요.
인덱서에 대한 자세한 내용은 인덱서 문서를 참고하세요.
[]의 다른 쓰임
포인터 요소 액세스에 대한 내용은 포인터 연산자 문서의 포인터 요소 액세스 연산자 [] 섹션에서, 컬렉션 식에 대한 내용은 컬렉션 식 문서에서 확인할 수 있어요.
특성(attribute)을 지정할 때도 대괄호를 써요.
[System.Diagnostics.Conditional("DEBUG")]
void TraceMethod() {}
또한 패턴 매칭이나 테스트에 쓰는 목록 패턴을 지정할 때도 대괄호를 사용해요.
arr is ([1, 2, ..])
// 배열이 (1, 2)로 시작함을 지정
Null 조건부 연산자 ?.와 ?[]
null 조건부 연산자는 피연산자가 null이 아닌 값으로 평가될 때만 해당 피연산자에 멤버 액세스(?.) 또는 요소 액세스(?[]) 연산을 적용해요. 그렇지 않으면 null을 반환해요. 정리하면:
a가null로 평가되면a?.x또는a?[x]의 결과는null이에요.a가 null이 아닌 값으로 평가되면a?.x또는a?[x]의 결과는 각각a.x또는a[x]의 결과와 같아요.
참고
a.x나a[x]가 예외를 던진다면, null이 아닌a에 대해a?.x나a?[x]도 같은 예외를 던져요. 예를 들어a가 null이 아닌 배열 인스턴스인데x가a의 경계를 벗어났다면,a?[x]는 xref:System.IndexOutOfRangeException을 던져요.
null 조건부 연산자는 단락(short-circuit) 방식을 써요. 즉 조건부 멤버/요소 액세스 연산 체인에서 어떤 연산이 null을 반환하면 나머지 체인은 실행되지 않아요. 다음 예시에서 A가 null로 평가되면 B는 평가되지 않고, A나 B가 null로 평가되면 C는 평가되지 않아요.
A?.B?.Do(C);
A?.B?[C];
A가 null일 수 있지만, A가 null이 아닐 때 B와 C는 null이 아님이 보장된다면, null 조건부 연산자는 A에만 적용하면 돼요.
A?.B.C();
위 예시에서 A가 null이면 B는 평가되지 않고 C()도 호출되지 않아요. 하지만 괄호로 체인을 끊으면, 예를 들어 (A?.B).C()처럼 쓰면 단락은 일어나지 않아요.
?.와 ?[] 연산자의 실제 쓰임은 다음 예시로 확인해 봐요.
// MemberAccessOperators.cs 의 "SnippetNullConditional" 스니펫
// 및 MemberAccessOperators2.cs 스니펫
int? length = customers?[0]?.Orders?.Count;
첫번째 예시는 또한 null 조건부 연산 결과가 null일 때 평가할 대체 식을 지정하는 null 병합 연산자 ??를 함께 쓰고 있어요.
a.x나 a[x]가 null을 허용하지 않는 값 타입 T라면, a?.x나 a?[x]는 해당 nullable 값 타입 T?이 돼요. T 타입의 식이 필요하다면 null 조건부 식에 null 병합 연산자 ??를 적용해요. 다음 예시처럼요.
// MemberAccessOperators.cs 의 "NullConditionalWithNullCoalescing" 스니펫
if (numbers?.Length ?? 0 < 2) { ... }
위 예시에서 ?? 연산자를 쓰지 않으면, numbers가 null일 때 numbers?.Length < 2는 false로 평가돼요.
참고
?.연산자는 왼쪽 피연산자를 두 번 이상 평가하지 않아요. 그래서 한 번 null이 아님이 확인된 뒤에는null로 바뀔 수 없다는 게 보장돼요.
C# 14부터는 참조 타입에 대해 null 조건부 액세스 식(?.와 ?[])으로 대입(assignment)이 허용돼요. 예를 들어 다음 메서드를 볼게요.
// NullCoalescingOperator.cs 의 "NullForgivingAssignment" 스니펫
this.Value = value;
위 예시는 null일 수 있는 참조 타입의 속성과 인덱싱된 요소에 대입하는 모습이에요. 이 대입에서 중요한 동작은, = 오른쪽의 식이 왼쪽이 null이 아님이 확인될 때만 평가된다는 점이에요. 예를 들어 다음 코드에서 GenerateNextIndex 함수는 values 배열이 null이 아닐 때만 호출돼요. values 배열이 null이면 GenerateNextIndex는 호출되지 않아요.
// NullCoalescingOperator.cs 의 "ConditionalRHS" 스니펫
values?[values.Length - 1] = GenerateNextIndex();
다시 말해, 위 코드는 null 검사를 if 문으로 쓴 다음 코드와 동일해요.
// NullCoalescingOperator.cs 의 "EquivalentIfStatement" 스니펫
if (values != null)
{
values[values.Length - 1] = GenerateNextIndex();
}
대입 외에도 +=나 -= 같은 모든 형태의 복합 대입이 허용돼요. 다만 증가(++)와 감소(--)는 허용되지 않아요.
이 개선은 null 조건부 식을 변수로 분류하지는 않아요. ref로 대입될 수 없고, ref 변수에 대입되거나 메서드에 ref나 out 인자로 전달될 수도 없어요.
스레드 안전한 대리자 호출
?. 연산자로 대리자가 null인지 확인하고 스레드 안전하게 호출할 수 있어요. 예를 들어 이벤트를 발생시킬 때 그렇게 해요. 다음 코드처럼요.
PropertyChanged?.Invoke(…)
이 코드는 다음 코드와 같아요.
var handler = this.PropertyChanged;
if (handler != null)
{
handler(…);
}
위 예시는 null이 아닌 handler만 호출되도록 보장하는 스레드 안전한 방법이에요. 대리자 인스턴스는 불변이므로, 어떤 스레드도 handler 로컬 변수가 참조하는 객체를 바꿀 수 없어요. 특히 다른 스레드가 실행하는 코드가 PropertyChanged 이벤트에서 구독을 해제하고 handler가 호출되기 전에 PropertyChanged가 null이 되더라도, handler가 참조하는 객체는 그대로 유지돼요.
호출 식 ()
인자를 넣거나 넣지 않고 메서드를 호출하고 대리자를 실행하는 방법은 다음 코드로 확인할 수 있어요.
// MemberAccessOperators.cs 의 "Invocation" 스니펫
Console.WriteLine("Hello");
Action<string> action = s => Console.WriteLine(s);
action("Hello");
()의 다른 쓰임
식에서 연산을 평가하는 순서를 조정할 때도 괄호를 써요. 자세한 내용은 C# 연산자 문서를 참고하세요.
명시적 타입 변환을 수행하는 캐스트 식도 괄호를 사용해요.
끝에서부터의 인덱스 연산자 ^
인덱스 연산자와 범위 연산자는 countable 타입과 함께 쓸 수 있어요. countable 타입은 Count 또는 Length라는 이름의 int 속성이 있고 접근 가능한 get 접근자가 있는 타입이에요. 컬렉션 식도 countable 타입에 의존해요.
참고
1차원 배열은 countable이에요. 다차원 배열은 그렇지 않아요. 다차원 배열에서는
^와..(범위) 연산자를 쓸 수 없어요.
^ 연산자는 시퀀스의 끝에서부터의 요소 위치를 나타내요. 길이가 length인 시퀀스에서 ^n은 시퀀스의 시작에서 length - n만큼 떨어진 요소를 가리켜요. 예를 들어 ^1은 시퀀스의 마지막 요소를, ^length는 시퀀스의 첫 요소를 가리켜요.
// MemberAccessOperators.cs 의 "IndexFromEnd" 스니펫
int[] a = { 0, 1, 2, 3, 4 };
var last = a[^1]; // 마지막 요소
위 예시에서 볼 수 있듯, 식 ^e는 xref:System.Index?displayProperty=nameWithType 타입이에요. ^e 식에서 e의 결과는 int로 암시적으로 변환 가능해야 해요.
^ 연산자는 범위 연산자와 함께 쓰여 인덱스 범위를 만들 수도 있어요. 자세한 내용은 인덱스와 범위 문서를 참고하세요.
C# 13부터는 객체 이니셜라이저에서 끝에서부터의 인덱스 연산자인 ^를 쓸 수 있어요.
범위 연산자 ..
.. 연산자는 피연산자로 인덱스 범위의 시작과 끝을 지정해요. 왼쪽 피연산자는 범위의 포함 시작이고, 오른쪽 피연산자는 범위의 제외 끝이에요. 두 피연산자 모두 시퀀스의 시작에서 셌거나 끝에서부터 센 인덱스일 수 있어요. 다음 예시처럼요.
// MemberAccessOperators.cs 의 "Ranges" 스니펫
int[] a = { 0, 1, 2, 3, 4 };
var slice = a[1..3]; // 1부터 3 전까지
위 예시에서 볼 수 있듯, 식 a..b는 xref:System.Range?displayProperty=nameWithType 타입이에요. a..b 식에서 a와 b의 결과는 xref:System.Int32 또는 xref:System.Index로 암시적으로 변환 가능해야 해요.
중요
int에서Index로의 암시적 변환은 값이 음수일 때 xref:System.ArgumentOutOfRangeException을 던져요.
.. 연산자의 아무 피연산자나 생략하면 열린 범위(open-ended range)를 얻을 수 있어요.
a..는a..^0과 같아요...b는0..b와 같아요...는0..^0과 같아요.
// MemberAccessOperators.cs 의 "RangesOptional" 스니펫
다음 표는 컬렉션 범위를 표현하는 여러 방법을 보여줘요.
| 범위 연산자 식 | 설명 |
|---|---|
.. |
컬렉션의 모든 값. |
..end |
처음부터 end 전까지의 값. |
start.. |
start부터 끝까지 포함하는 값. |
start..end |
start부터 end 전까지 포함하는 값. |
^start.. |
끝에서부터 센 start부터 끝까지 포함하는 값. |
..^end |
처음부터 끝에서부터 센 end 전까지의 값. |
start..^end |
start부터 끝에서부터 센 end 전까지 포함하는 값. |
^start..^end |
끝에서부터 센 start부터 끝에서부터 센 end 전까지 둘 다 끝에서부터 세는 포함 값. |
앞 표에 나온 모든 범위를 썼을 때의 효과는 다음 예시에서 확인할 수 있어요.
// MemberAccessOperators.cs 의 "RangesAllPossible" 스니펫
자세한 내용은 인덱스와 범위 문서를 참고하세요.
.. 토큰은 컬렉션 식의 spread 요소에도 쓰여요.
연산자 오버로드 가능 여부
., (), ^, .. 연산자는 오버로드할 수 없어요. [] 연산자도 오버로드할 수 없는 연산자예요. 사용자 정의 타입에서 인덱싱을 지원하려면 인덱서를 사용하세요.
C# 언어 사양
자세한 내용은 C# 언어 사양의 다음 섹션을 참고하세요.
인덱스와 범위에 대한 자세한 내용은 기능 제안 노트를 참고하세요.
더 알아보기
- 인덱스 연산자 사용(스타일 규칙 IDE0056)
- 범위 연산자 사용(스타일 규칙 IDE0057)
- 조건부 대리자 호출 사용(스타일 규칙 IDE1005)
- C# 연산자와 식
??(null 병합 연산자)::연산자
출처: Member access operators and expressions - the dot, indexer, and invocation operators