연산자

연산자 (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 + aPointVector의 덧셈(+)을 사용해요.

산술 연산자 (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() 함수를 대신 사용해요.) == 연산자가 동작하는 방식은 이래요.

  • xy가 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의 결과는 objT가 지정한 인터페이스를 구현하면 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?.barfoo가 null이 아니면 속성 bar를 선택하고, null이면 foo?.bar의 값이 null
! Not-null assertion 연산자 표현식을 그 기본 non-nullable 타입으로 캐스트하고, 캐스트가 실패하면 런타임 예외를 던짐; 예: foo!.barfoo가 null이 아님을 단언하고 속성 bar를 선택하며, foo가 null이면 런타임 예외가 던져짐

., ?., .. 연산자에 대한 더 자세한 내용은 Classes를 참고하세요.

더 알아보기

  • Methods — 연산자를 클래스 멤버로 구현하기
  • Collections — 스프레드 연산자
  • Classes., ?., .. 연산자