멤버 액세스 연산자와 식 — 점(.), 인덱서, 호출 연산자

멤버 액세스 연산자와 식 — 점(.), 인덱서, 호출 연산자

타입의 멤버에 접근할 때 우리는 여러 연산자와 식을 써요. 멤버 액세스 연산자에는 멤버 액세스(.), 배열 요소 또는 인덱서 액세스([]), 끝에서부터의 인덱스(^), 범위(..), null 조건부 연산자(?.?[]), 그리고 메서드 호출(())이 있어요. 이 중에서 null 조건부 멤버 액세스(?.)와 인덱서 액세스(?[]) 연산자도 포함되고요.

멤버 액세스 식 .

. 토큰으로 네임스페이스나 타입의 멤버에 접근해요. 아래 예시에서 보는 것과 같은 방식으로요.

  • 네임스페이스 안의 중첩 네임스페이스에 접근할 때 .을 써요. 다음 using 지시문 예시가 그 경우예요.
NestedNamespace

(using 지시문으로 중첩 네임스페이스 접근을 단순화하는 예시는 원문의 실제 스니펫에서 확인해요.)

  • 네임스페이스 안의 타입에 접근할 때 .으로 정규화된 이름(qualified name)을 만들어요. 다음 코드처럼요.
Namespace.Type

using 지시문을 쓰면 정규화된 이름을 쓰지 않아도 돼요.

  • 정적/비정적의 타입 멤버에 접근할 때도 .을 써요. 다음 코드처럼요.
obj.Member

그리고 .확장 멤버에 접근할 때도 쓸 수 있어요.

참고: 위 예시들은 원문에서 개별 코드 스니펫(MemberAccessOperators.csNestedNamespace, 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을 반환해요. 정리하면:

  • anull로 평가되면 a?.x 또는 a?[x]의 결과는 null이에요.
  • a가 null이 아닌 값으로 평가되면 a?.x 또는 a?[x]의 결과는 각각 a.x 또는 a[x]의 결과와 같아요.

참고

a.xa[x]가 예외를 던진다면, null이 아닌 a에 대해 a?.xa?[x]도 같은 예외를 던져요. 예를 들어 a가 null이 아닌 배열 인스턴스인데 xa의 경계를 벗어났다면, a?[x]xref:System.IndexOutOfRangeException을 던져요.

null 조건부 연산자는 단락(short-circuit) 방식을 써요. 즉 조건부 멤버/요소 액세스 연산 체인에서 어떤 연산이 null을 반환하면 나머지 체인은 실행되지 않아요. 다음 예시에서 Anull로 평가되면 B는 평가되지 않고, ABnull로 평가되면 C는 평가되지 않아요.

A?.B?.Do(C);
A?.B?[C];

A가 null일 수 있지만, A가 null이 아닐 때 BC는 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.xa[x]가 null을 허용하지 않는 값 타입 T라면, a?.xa?[x]는 해당 nullable 값 타입 T?이 돼요. T 타입의 식이 필요하다면 null 조건부 식에 null 병합 연산자 ??를 적용해요. 다음 예시처럼요.

// MemberAccessOperators.cs 의 "NullConditionalWithNullCoalescing" 스니펫
if (numbers?.Length ?? 0 < 2) { ... }

위 예시에서 ?? 연산자를 쓰지 않으면, numbersnull일 때 numbers?.Length < 2false로 평가돼요.

참고

?. 연산자는 왼쪽 피연산자를 두 번 이상 평가하지 않아요. 그래서 한 번 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 변수에 대입되거나 메서드에 refout 인자로 전달될 수도 없어요.

스레드 안전한 대리자 호출

?. 연산자로 대리자가 null인지 확인하고 스레드 안전하게 호출할 수 있어요. 예를 들어 이벤트를 발생시킬 때 그렇게 해요. 다음 코드처럼요.

PropertyChanged?.Invoke(…)

이 코드는 다음 코드와 같아요.

var handler = this.PropertyChanged;
if (handler != null)
{
    handler(…);
}

위 예시는 null이 아닌 handler만 호출되도록 보장하는 스레드 안전한 방법이에요. 대리자 인스턴스는 불변이므로, 어떤 스레드도 handler 로컬 변수가 참조하는 객체를 바꿀 수 없어요. 특히 다른 스레드가 실행하는 코드가 PropertyChanged 이벤트에서 구독을 해제하고 handler가 호출되기 전에 PropertyChangednull이 되더라도, handler가 참조하는 객체는 그대로 유지돼요.

호출 식 ()

괄호 ()메서드를 호출하거나 대리자를 실행해요.

인자를 넣거나 넣지 않고 메서드를 호출하고 대리자를 실행하는 방법은 다음 코드로 확인할 수 있어요.

// MemberAccessOperators.cs 의 "Invocation" 스니펫
Console.WriteLine("Hello");
Action<string> action = s => Console.WriteLine(s);
action("Hello");

new 연산자생성자를 호출할 때도 괄호를 써요.

()의 다른 쓰임

식에서 연산을 평가하는 순서를 조정할 때도 괄호를 써요. 자세한 내용은 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];   // 마지막 요소

위 예시에서 볼 수 있듯, 식 ^exref: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..bxref:System.Range?displayProperty=nameWithType 타입이에요. a..b 식에서 ab의 결과는 xref:System.Int32 또는 xref:System.Index로 암시적으로 변환 가능해야 해요.

중요

int에서 Index로의 암시적 변환은 값이 음수일 때 xref:System.ArgumentOutOfRangeException을 던져요.

.. 연산자의 아무 피연산자나 생략하면 열린 범위(open-ended range)를 얻을 수 있어요.

  • a..a..^0과 같아요.
  • ..b0..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# 언어 사양의 다음 섹션을 참고하세요.

인덱스와 범위에 대한 자세한 내용은 기능 제안 노트를 참고하세요.

더 알아보기

출처: Member access operators and expressions - the dot, indexer, and invocation operators