뺄셈 연산자 `-`와 `-=`
뺄셈 연산자 -와 -= (마이너스 연산자)
C#의 뺄셈 연산자는 숫자 타입에서의 뺄셈 말고도, 델리게이트(대리자)를 다룰 때 특별한 의미로 동작해요. 이 문서에서는 내장된 -와 -= 연산자가 어떤 타입을 지원하고, 어떻게 동작하는지 하나씩 살펴볼게요.
출처: Microsoft Learn
본문
내장된 delegate 타입과, 정수 타입, 부동 소수점 타입은 모두 -와 -= 연산자를 지원해요.
숫자에서 쓰는 산술 - 연산자에 대한 자세한 내용은 Arithmetic operators 문서의 Unary plus and minus operators와 Subtraction operator - 섹션을 참고하세요.
델리게이트 제거 (Delegate removal)
같은 delegate 타입의 피연산자에 - 연산자를 쓰면, 결과는 다음과 같이 계산된 delegate 인스턴스가 돼요. 복잡해 보이지만, 핵심은 오른쪽 피연산자(뺄 대상)의 호출 목록을 왼쪽 피연산자의 호출 목록에서 떼어낸다는 것에요.
-
두 피연산자가 모두
null이 아니고, 오른쪽 피연산자의 호출 목록이 왼쪽 피연산자의 호출 목록의 연속된 부분 목록(sub-list)일 때, 그 부분을 제거한 새로운 호출 목록이 결과가 돼요. 오른쪽 목록이 왼쪽 목록 안의 여러 연속 부분 목록과 일치한다면, 가장 오른쪽에 있는 일치 항목만 제거해요. 제거 결과 목록이 비어 있으면 결과는null이에요.Action a = () => Console.Write("a"); Action b = () => Console.Write("b"); var abbaab = a + b + b + a + a + b; abbaab(); // output: abbaab Console.WriteLine(); var ab = a + b; var abba = abbaab - ab; abba(); // output: abba Console.WriteLine(); var nihil = abbaab - abbaab; Console.WriteLine(nihil is null); // output: Trueabbaab에서ab(a + b)를 빼면 결과가abba가 되는 걸 볼 수 있어요.ab가 왼쪽 목록에서 "가장 오른쪽"의 연속 부분과 일치해서 제거되죠. 그리고abbaab - abbaab처럼 전부 빼면 목록이 비어null이 돼요. -
오른쪽 피연산자의 호출 목록이 왼쪽 피연산자의 호출 목록의 연속 부분 목록이 아니라면, 결과는 왼쪽 피연산자 그대로예요. 예를 들어 multicast delegate에 포함되지 않은 delegate를 빼려고 하면 아무 일도 일어나지 않고, 원래 multicast delegate 그대로 남아요.
Action a = () => Console.Write("a"); Action b = () => Console.Write("b"); var abbaab = a + b + b + a + a + b; var aba = a + b + a; var first = abbaab - aba; first(); // output: abbaab Console.WriteLine(); Console.WriteLine(object.ReferenceEquals(abbaab, first)); // output: True Action a2 = () => Console.Write("a"); var changed = aba - a; changed(); // output: ab Console.WriteLine(); var unchanged = aba - a2; unchanged(); // output: aba Console.WriteLine(); Console.WriteLine(object.ReferenceEquals(aba, unchanged)); // output: Trueabbaab에서aba를 빼도 결과가 그대로abbaab(ReferenceEquals도True)인 게, "일치하는 부분 목록이 아니라서 변화가 없다"는 의미에요. 위 예시는 또 하나 중요한 점을 보여줘요. delegate를 제거할 때 delegate 인스턴스의 동등성(equality)을 비교한다는 것인데, 동일한 람다 식을 평가해서 만든 delegate끼리는 서로 같지 않아요. 그래서a2(a와 같은 람다 식에서 만들었지만)는aba에 포함된a와 같지 않아unchanged가 그대로aba로 남는 거예요. delegate 동등성에 대한 자세한 내용은 C# 언어 사양의 Delegate equality operators 섹션을 참고하세요. -
**왼쪽 피연산자가
null이면 결과도null**이고, 오른쪽 피연산자가null이면 결과는 왼쪽 피연산자 그대로예요.Action a = () => Console.Write("a"); var nothing = null - a; Console.WriteLine(nothing is null); // output: True var first = a - null; a(); // output: a Console.WriteLine(); Console.WriteLine(object.ReferenceEquals(first, a)); // output: True
delegate를 결합할 때는 + 연산자를 사용해요. delegate 타입에 대한 자세한 내용은 Delegates를 참고하세요.
빼기 할당 연산자 -=
-= 연산자를 쓰는 표현식, 예를 들어
x -= y
는 다음 코드와 같아요.
x = x - y
단, 차이점이 하나 있어요. x는 딱 한 번만 평가(evaluate) 된다는 거예요. (x = x - y에서는 x가 두 번 평가될 수 있어요.)
다음 예시는 -= 연산자를 어떻게 쓰는지 보여줘요.
int i = 5;
i -= 9;
Console.WriteLine(i);
// Output: -4
Action a = () => Console.Write("a");
Action b = () => Console.Write("b");
var printer = a + b + a;
printer(); // output: aba
Console.WriteLine();
printer -= a;
printer(); // output: ab
숫자 타입에서는 i -= 9처럼 값을 갱신하고, delegate 타입에서는 printer -= a처럼 멤버를 제거하죠. printer는 a + b + a인데 a를 빼면 a + b가 되어 ab를 출력해요.
-= 연산자는 이벤트(event)에서 구독을 해제할 때 핸들러 메서드를 지정하는 데도 써요. 자세한 내용은 How to subscribe to and unsubscribe from events를 참고하세요.
연산자 오버로드 가능 여부
사용자 정의 타입은 - 연산자를 오버로드할 수 있어요. 이진 - 연산자를 오버로드하면 -= 연산자도 암시적으로 오버로드돼요. 그리고 C# 14부터는 사용자 정의 타입이 -= 연산자를 명시적으로 오버로드해서 더 효율적인 구현을 제공할 수 있어요. 보통 타입이 -=를 오버로드하는 이유는, 뺄셈 결과를 저장할 새 인스턴스를 할당하는 대신 값 자체를 제자리(in-place)에서 갱신할 수 있기 때문이에요. 타입이 명시적 오버로드를 제공하지 않으면, 컴파일러가 암시적 오버로드를 생성해요.
C# 언어 사양
자세한 내용은 C# 언어 사양의 Unary minus operator와 Subtraction operator 섹션을 참고하세요. C# 14 이상에서 복합 할당 연산자의 오버로딩에 대한 내용은 user defined compound assignment 기능 사양을 참고하세요.