연산자
연산자 (Operators)
Dart가 지원하는 연산자의 세계를 정리된 표로 풀어볼게요. 우선순위와 결합성, 각 연산자가 실제로 어떻게 동작하는지까지 함께 알아봐요.
출처: Dart 공식 문서
본문
Dart는 아래 표에 나오는 연산자들을 지원해요. 표는 Dart의 연산자 결합성(associativity)과 연산자 우선순위를 높은 것부터 낮은 것까지 보여줘요. 이 값들은 Dart 연산자 관계의 근사치라는 점을 기억하세요. 이 연산자들 중 상당수는 클래스 멤버로 구현할 수 있어요.
| 설명 | 연산자 | 결합성 |
|---|---|---|
| 단항 후위 (unary postfix) | expr++ expr-- () [] ?[] . ?. ! |
없음 |
| 단항 전위 (unary prefix) | -expr !expr ~expr ++expr --expr await expr |
없음 |
| 곱셈 (multiplicative) | * / % ~/ |
왼쪽 |
| 덧셈 (additive) | + - |
왼쪽 |
| 시프트 (shift) | << >> >>> |
왼쪽 |
| 비트 AND (bitwise AND) | & |
왼쪽 |
| 비트 XOR (bitwise XOR) | ^ |
왼쪽 |
| 비트 OR (bitwise OR) | | |
왼쪽 |
| 관계·타입 검사 (relational and type test) | >= > <= < as is is! |
없음 |
| 동등 (equality) | == != |
없음 |
| 논리 AND (logical AND) | && |
왼쪽 |
| 논리 OR (logical OR) | || |
왼쪽 |
| if-null | ?? |
왼쪽 |
| 조건부 (conditional) | expr1 ? expr2 : expr3 |
오른쪽 |
| 캐스케이드 (cascade) | .. ?.. |
왼쪽 |
| 할당 (assignment) | = *= /= += -= &= ^= 등 |
오른쪽 |
| 스프레드 (spread, 각주 참고) | ... ...? |
없음 |
경고
위 표는 도움을 주기 위한 안내로만 사용해야 해요. 연산자 우선순위와 결합성의 개념은 언어 문법에서 찾을 수 있는 진실의 근사치예요. Dart 연산자 관계의 권위 있는 동작은 Dart 언어 명세에 정의된 문법에서 찾을 수 있어요.
연산자를 사용하면 표현식을 만들게 돼요. 연산자 표현식의 예시 몇 가지를 볼게요.
a++
a + b
a = b
a == b
c ? a : b
a is T
연산자 우선순위 예시 (Operator precedence example)
연산자 표에서 각 연산자는 그 뒤에 오는 행의 연산자보다 높은 우선순위를 가져요. 예를 들어 곱셈 연산자 %는 동등 연산자 ==보다 우선순위가 높고(그래서 먼저 실행돼요), ==는 논리 AND 연산자 &&보다 우선순위가 높아요. 이 우선순위 덕분에 다음 두 줄의 코드는 같은 방식으로 실행돼요.
// Parentheses improve readability.
if ((n % i == 0) && (d % i == 0)) {
// ...
}
// Harder to read, but equivalent.
if (n % i == 0 && d % i == 0) {
// ...
}
경고
두 피연산자를 받는 연산자에서는 가장 왼쪽 피연산자가 어떤 메서드가 사용되는지 결정해요. 예를 들어
Vector객체와Point객체가 있다면aVector + aPoint는Vector의 덧셈(+)을 사용해요.
산술 연산자 (Arithmetic operators)
Dart는 다음 표에 나오는 일반적인 산술 연산자를 지원해요.
| 연산자 | 의미 |
|---|---|
+ |
더하기 |
- |
빼기 |
-expr |
단항 마이너스, 부정(negation)이라고도 함(표현식의 부호를 반전) |
* |
곱하기 |
/ |
나누기 |
~/ |
나누되 정수 결과 반환 |
% |
정수 나눗셈의 나머지(모듈로) 구하기 |
예시:
assert(2 + 3 == 5);
assert(2 - 3 == -1);
assert(2 * 3 == 6);
assert(5 / 2 == 2.5); // Result is a double
assert(5 ~/ 2 == 2); // Result is an int
assert(5 % 2 == 1); // Remainder
assert('5/2 = ${5 ~/ 2} r ${5 % 2}' == '5/2 = 2 r 1');
Dart는 전위·후위 증감 연산자도 지원해요.
| 연산자 | 의미 |
|---|---|
++var |
var = var + 1 (표현식 값은 var + 1) |
var++ |
var = var + 1 (표현식 값은 var) |
--var |
var = var - 1 (표현식 값은 var - 1) |
var-- |
var = var - 1 (표현식 값은 var) |
예시:
int a;
int b;
a = 0;
b = ++a; // Increment a before b gets its value.
assert(a == b); // 1 == 1
a = 0;
b = a++; // Increment a after b gets its value.
assert(a != b); // 1 != 0
a = 0;
b = --a; // Decrement a before b gets its value.
assert(a == b); // -1 == -1
a = 0;
b = a--; // Decrement a after b gets its value.
assert(a != b); // -1 != 0
동등·관계 연산자 (Equality and relational operators)
다음 표는 동등·관계 연산자의 의미를 나열해요.
| 연산자 | 의미 |
|---|---|
== |
같음; 아래 설명 참고 |
!= |
같지 않음 |
> |
큼 |
< |
작음 |
>= |
크거나 같음 |
<= |
작거나 같음 |
두 객체 x와 y가 같은 것을 나타내는지 검사하려면 == 연산자를 사용해요. (두 객체가 정말로 같은 객체인지 알아야 하는 드문 경우에는 identical() 함수를 대신 사용해요.) == 연산자가 동작하는 방식은 이래요.
- x나 y가 null이면, 둘 다 null일 때 true를, 하나만 null일 때 false를 반환해요.
- x에서 인자 y로
==메서드를 호출한 결과를 반환해요. (맞아요,==같은 연산자는 첫 번째 피연산자에서 호출되는 메서드예요. 자세한 내용은 Operators를 참고하세요.)
동등·관계 연산자를 각각 쓴 예시를 볼게요.
assert(2 == 2);
assert(2 != 3);
assert(3 > 2);
assert(2 < 3);
assert(3 >= 3);
assert(2 <= 3);
타입 검사 연산자 (Type test operators)
as, is, is! 연산자는 런타임에서 타입을 검사할 때 유용해요.
| 연산자 | 의미 |
|---|---|
as |
타입 캐스트(라이브러리 접두어 지정에도 사용) |
is |
객체가 지정된 타입을 가지면 true |
is! |
객체가 지정된 타입을 가지지 않으면 true |
obj is T의 결과는 obj가 T가 지정한 인터페이스를 구현하면 true예요. 예를 들어 obj is Object?는 항상 true예요.
as 연산자는 객체가 특정 타입임을 확신할 때만 그 타입으로 캐스트하는 데 사용해요. 예시:
(employee as Person).firstName = 'Bob';
객체가 타입 T인지 확신이 없다면, 객체를 사용하기 전에 is T로 타입을 검사해요.
if (employee is Person) {
// Type check
employee.firstName = 'Bob';
}
정보
두 코드는 동등하지 않아요.
employee가 null이거나Person이 아니면, 첫 번째 예시는 예외를 던지지만 두 번째는 아무것도 하지 않아요.
할당 연산자 (Assignment operators)
이미 보았듯이 = 연산자로 값을 할당할 수 있어요. 할당 대상 변수가 null일 때만 할당하려면 ??= 연산자를 사용해요.
// Assign value to a
a = value;
// Assign value to b if b is null; otherwise, b stays the same
b ??= value;
+= 같은 복합 할당 연산자는 연산과 할당을 결합해요.
= |
*= |
%= |
>>>= |
^= |
+= |
/= |
<<= |
&= |
|= |
-= |
~/= |
>>= |
복합 할당 연산자가 동작하는 방식은 이래요.
| 복합 할당 | 동등 표현식 | |
|---|---|---|
| 연산자 op에 대해: | a op= b |
a = a op b |
| 예시: | a += b |
a = a + b |
다음 예시는 할당과 복합 할당 연산자를 사용해요.
var a = 2; // Assign using =
a *= 3; // Assign and multiply: a = a * 3
assert(a == 6);
논리 연산자 (Logical operators)
논리 연산자로 불리언 표현식을 뒤집거나 결합할 수 있어요.
| 연산자 | 의미 |
|---|---|
!expr |
뒤따르는 표현식을 반전(false→true, true→false) |
|| |
논리 OR |
&& |
논리 AND |
논리 연산자를 쓰는 예시를 볼게요.
if (!done && (col == 0 || col == 3)) {
// ...Do something...
}
비트·시프트 연산자 (Bitwise and shift operators)
Dart에서 숫자의 개별 비트를 조작할 수 있어요. 보통 이 비트·시프트 연산자는 정수와 함께 사용해요.
| 연산자 | 의미 |
|---|---|
& |
AND |
| |
OR |
^ |
XOR |
~expr |
단항 비트 보수(0은 1로, 1은 0으로) |
<< |
왼쪽 시프트 |
>> |
오른쪽 시프트 |
>>> |
부호 없는 오른쪽 시프트 |
정보
크거나 음수인 피연산자를 사용한 비트 연산의 동작은 플랫폼마다 다를 수 있어요. 자세한 내용은 Bitwise operations platform differences를 확인해 보세요.
비트·시프트 연산자를 쓰는 예시를 볼게요.
final value = 0x22;
final bitmask = 0x0f;
assert((value & bitmask) == 0x02); // AND
assert((value & ~bitmask) == 0x20); // AND NOT
assert((value | bitmask) == 0x2f); // OR
assert((value ^ bitmask) == 0x2d); // XOR
assert((value << 4) == 0x220); // Shift left
assert((value >> 4) == 0x02); // Shift right
// Shift right example that results in different behavior on web
// because the operand value changes when masked to 32 bits:
assert((-value >> 4) == -0x03);
assert((value >>> 4) == 0x02); // Unsigned shift right
assert((-value >>> 4) > 0); // Unsigned shift right
버전 안내
>>>연산자(triple-shift 또는 unsigned shift라고 함)는 언어 버전이 최소 2.14 이상이어야 해요.
조건부 표현식 (Conditional expressions)
Dart에는 그렇지 않으면 if-else 문이 필요할 수 있는 표현식을 간결하게 평가하게 해주는 연산자가 두 개 있어요.
condition ? expr1 : expr2
condition이 true이면 expr1을 평가하고(그 값을 반환), 그렇지 않으면 expr2의 값을 평가하고 반환해요.
expr1 ?? expr2
expr1이 null이 아니면 그 값을 반환하고, 그렇지 않으면 expr2의 값을 평가하고 반환해요.
불리언 표현식에 기반해 값을 할당해야 할 때는 조건부 연산자 ?와 :를 사용하는 걸 고려해 보세요.
var visibility = isPublic ? 'public' : 'private';
불리언 표현식이 null을 검사한다면, if-null 연산자 ??(null-coalescing 연산자라고도 함)를 고려해 보세요.
String playerName(String? name) => name ?? 'Guest';
앞선 예시는 최소 두 가지 다른 방식으로 쓸 수 있지만, 이렇게 간결하진 않아요.
// Slightly longer version uses ?: operator.
String playerName(String? name) => name != null ? name : 'Guest';
// Very long version uses if-else statement.
String playerName(String? name) {
if (name != null) {
return name;
} else {
return 'Guest';
}
}
캐스케이드 표기법 (Cascade notation)
캐스케이드(.., ?..)는 같은 객체에 대해 일련의 연산을 연속으로 수행할 수 있게 해줘요. 인스턴스 멤버에 접근하는 것 외에도 같은 객체에서 인스턴스 메서드를 호출할 수도 있어요. 이러면 임시 변수를 만드는 단계를 줄이고 더 유연한 코드를 작성할 수 있어요.
다음 코드를 볼게요.
var paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 5.0;
생성자 Paint()는 Paint 객체를 반환해요. 캐스케이드 표기법 뒤에 오는 코드는 이 객체에 대해 동작하며, 반환될 수 있는 어떤 값도 무시해요.
앞선 예시는 다음 코드와 동등해요.
var paint = Paint();
paint.color = Colors.black;
paint.strokeCap = StrokeCap.round;
paint.strokeWidth = 5.0;
캐스케이드가 동작하는 객체가 null일 수 있다면, 첫 번째 연산에 null-단락(null-shorting) 캐스케이드(?..)를 사용해요. ?..로 시작하면 캐스케이드 연산 중 어떤 것도 그 null 객체에 대해 시도되지 않음을 보장해요.
document.querySelector('#confirm') // Get an object.
?..textContent =
'Confirm' // Use its members.
..classList.add('important')
..onClick.listen((e) => window.alert('Confirmed!'))
..scrollIntoView();
앞선 코드는 다음 코드와 동등해요.
final button = document.querySelector('#confirm');
button?.textContent = 'Confirm';
button?.classList.add('important');
button?.onClick.listen((e) => window.alert('Confirmed!'));
button?.scrollIntoView();
캐스케이드를 중첩할 수도 있어요. 예를 들면 이렇게요.
final addressBook =
(AddressBookBuilder()
..name = 'jenny'
..email = '[email protected]'
..phone =
(PhoneNumberBuilder()
..number = '415-555-0100'
..label = 'home')
.build())
.build();
실제 객체를 반환하는 함수에서 캐스케이드를 구성하도록 주의해야 해요. 예를 들어 다음 코드는 실패해요.
var sb = StringBuffer();
sb.write('foo')
..write('bar'); // Error: method 'write' isn't defined for 'void'.
sb.write() 호출은 void를 반환하고, void 위에는 캐스케이드를 구성할 수 없어요.
정보
엄밀히 말하면 캐스케이드의 '이중 점(double dot)' 표기법은 연산자가 아니에요. 그냥 Dart 문법의 일부일 뿐이에요.
스프레드 연산자 (Spread operators)
스프레드 연산자는 컬렉션을 산출하는 표현식을 평가하고, 결과 값을 풀어낸 뒤 다른 컬렉션에 삽입해요.
스프레드 연산자는 사실 '연산자 표현식'이 아니에요. .../...? 문법은 컬렉션 리터럴 자체의 일부예요. 그래서 스프레드 연산자에 대한 자세한 내용은 Collections 페이지에서 볼 수 있어요.
연산자가 아니므로 이 문법에는 '연산자 우선순위'가 없어요. 실질적으로는 가장 낮은 '우선순위'를 가져서, 어떤 종류의 표현식이든 스프레드 대상으로 유효해요. 예를 들면 이렇게요.
[...a + b]
기타 연산자 (Other operators)
나머지 연산자들은 대부분 다른 예시에서 이미 보았을 거예요.
| 연산자 | 이름 | 의미 |
|---|---|---|
() |
함수 적용 (Function application) | 함수 호출을 나타냄 |
[] |
첨자 접근 (Subscript access) | 오버라이드 가능한 [] 연산자 호출을 나타냄; 예: fooList[1]은 int 1을 fooList에 전달해 인덱스 1의 요소에 접근 |
?[] |
조건부 첨자 접근 (Conditional subscript access) | []와 같지만 가장 왼쪽 피연산자가 null일 수 있음; 예: fooList?[1]은 fooList가 null이 아니면 int 1을 전달해 접근하고, null이면 표현식이 null로 평가됨 |
. |
멤버 접근 (Member access) | 표현식의 속성을 참조; 예: foo.bar는 표현식 foo에서 속성 bar를 선택 |
?. |
조건부 멤버 접근 (Conditional member access) | .와 같지만 가장 왼쪽 피연산자가 null일 수 있음; 예: foo?.bar는 foo가 null이 아니면 속성 bar를 선택하고, null이면 foo?.bar의 값이 null |
! |
Not-null assertion 연산자 | 표현식을 그 기본 non-nullable 타입으로 캐스트하고, 캐스트가 실패하면 런타임 예외를 던짐; 예: foo!.bar는 foo가 null이 아님을 단언하고 속성 bar를 선택하며, foo가 null이면 런타임 예외가 던져짐 |
., ?., .. 연산자에 대한 더 자세한 내용은 Classes를 참고하세요.
더 알아보기
- Methods — 연산자를 클래스 멤버로 구현하기
- Collections — 스프레드 연산자
- Classes —
.,?.,..연산자