점 축약 표기

점 축약 표기 (Dot shorthands)

Status.running보다 Status.running을 길게 쓰는 게 피곤하게 느껴질 때가 있어요. 점 축약 표기(dot shorthand) 문법 .foo는 컴파일러가 문맥에서 타입을 추론할 수 있을 때 타입을 생략해서 더 간결한 Dart 코드를 쓰게 해줘요.

출처: Dart 공식 문서

점 축약 표기는 언어 버전이 최소 3.10이어야 해요.

본문

개요 (Overview)

점 축약 표기 문법 .foo는 컴파일러가 문맥에서 타입을 추론할 수 있을 때 타입을 생략해서 더 간결한 Dart 코드를 쓰게 해줘요. enum 값, 정적 멤버, 생성자에 접근할 때 전체 ContextType.foo를 쓰는 것에 대한 깔끔한 대안을 제공하죠.

본질적으로 점 축약 표기는 표현식이 다음 중 하나로 시작한 뒤 다른 연산을 선택적으로 이어 붙이도록 해줘요.

  • 식별자 .myValue
  • 생성자 .new()
  • 상수 생성 const .myValue()

enum 할당을 어떻게 단순화하는지 빠르게 볼게요.

// Use dot shorthand syntax on enums:
enum Status { none, running, stopped, paused }

Status currentStatus = .running; // Instead of Status.running

// Use dot shorthand syntax on a static method:
int port = .parse('8080'); // Instead of int.parse('8080')

// Uses dot shorthand syntax on a constructor:
class Point {
  final int x, y;
  Point(this.x, this.y);
  Point.origin() : x = 0, y = 0;
}

Point origin = .origin(); // Instead of Point.origin()

문맥 타입의 역할 (The role of context type)

점 축약 표기는 문맥 타입(context type)을 사용해서 컴파일러가 해석할 멤버를 결정해요. 문맥 타입은 주변 코드가 표현식에서 기대하는 타입이에요. 예를 들어 Status currentStatus = .running에서 컴파일러는 Status가 기대된다는 걸 알기 때문에 .runningStatus.running을 의미한다고 추론해요.

어휘 구조와 문법 (Lexical structure and syntax)

정적 멤버 축약(static member shorthand)은 앞에 오는 점(.)으로 시작하는 표현식이에요. 주변 문맥에서 타입을 알 수 있을 때, 이 문법은 정적 멤버, 생성자, enum 값에 접근하는 간결한 방법을 제공해요.

enum

점 축약 표기의 주된, 그리고 매우 권장되는 사용 사례는 enum이에요. 특히 enum 타입이 아주 명확한 할당과 switch 문에서 유용하죠.

enum LogLevel { debug, info, warning, error }

/// Returns the color code to use for the specified log [level].
String colorCode(LogLevel level) {
  // Use dot shorthand syntax for enum values in switch cases:
  return switch (level) {
    .debug => 'gray', // Instead of LogLevel.debug
    .info => 'blue', // Instead of LogLevel.info
    .warning => 'orange', // Instead of LogLevel.warning
    .error => 'red', // Instead of LogLevel.error
  };
}

// Example usage:
String warnColor = colorCode(.warning); // Returns 'orange'

이름 있는 생성자 (Named constructors)

점 축약 표기는 이름 있는 생성자나 팩토리 생성자를 호출하는 데 유용해요. 이 문법은 제네릭 클래스의 생성자에 타입 인자를 제공할 때도 동작해요.

class Point {
  final double x, y;
  const Point(this.x, this.y);
  const Point.origin() : x = 0, y = 0; // Named constructor

  // Factory constructor
  factory Point.fromList(List<double> list) {
    return Point(list[0], list[1]);
  }
}

// Use dot shorthand syntax on a named constructor:
Point origin = .origin(); // Instead of Point.origin()

// Use dot shorthand syntax on a factory constructor:
Point p1 = .fromList([1.0, 2.0]); // Instead of Point.fromList([1.0, 2.0])

// Use dot shorthand syntax on a generic class constructor:
List<int> intList = .filled(5, 0); // Instead of List.filled(5, 0)

이름 없는 생성자 (Unnamed constructors)

.new 점 축약 표기는 클래스의 이름 없는 생성자를 호출하는 간결한 방법을 제공해요. 타입이 이미 명시적으로 선언된 필드나 변수를 할당할 때 유용하죠.

이 문법은 반복적인 클래스 필드 초기화를 정리하는 데 특히 효과적이에요. 아래 "after" 예시처럼 인자가 있는 생성자와 없는 생성자 모두에 쓸 수 있어요. 제네릭 타입 인자도 문맥에서 추론해요.

점 축약 표기가 없을 때:

class _PageState extends State<Page> {
  late final AnimationController _animationController = AnimationController(
    vsync: this,
  );
  final ScrollController _scrollController = ScrollController();

  final GlobalKey<ScaffoldMessengerState> scaffoldKey =
      GlobalKey<ScaffoldMessengerState>();

  Map<String, Map<String, bool>> properties = <String, Map<String, bool>>{};
  // ...
}

점 축약 표기를 쓸 때:

// Use dot shorthand syntax for calling unnamed constructors:
class _PageState extends State<Page> {
  late final AnimationController _animationController = .new(vsync: this);
  final ScrollController _scrollController = .new();
  final GlobalKey<ScaffoldMessengerState> scaffoldKey = .new();
  Map<String, Map<String, bool>> properties = .new();
  // ...
}

정적 멤버 (Static members)

점 축약 문법으로 정적 메서드를 호출하거나 정적 필드/getter에 접근할 수 있어요. 컴파일러는 표현식의 문맥 타입에서 대상 클래스를 추론해요.

// Use dot shorthand syntax to invoke a static method:
int httpPort = .parse('80'); // Instead of int.parse('80')

// Use dot shorthand syntax to access a static field or getter:
BigInt bigIntZero = .zero; // Instead of BigInt.zero

상수 표현식 (Constant expressions)

접근하는 멤버가 컴파일 타임 상수라면 상수 문맥 안에서도 점 축약 표기를 쓸 수 있어요. enum 값과 const 생성자 호출에서 흔히 쓰죠.

enum Status { none, running, stopped, paused }

class Point {
  final double x, y;
  const Point(this.x, this.y);
  const Point.origin() : x = 0.0, y = 0.0;
}

// Use dot shorthand syntax for enum value:
const Status defaultStatus = .running; // Instead of Status.running

// Use dot shorthand syntax to invoke a const named constructor:
const Point myOrigin = .origin(); // Instead of Point.origin()

// Use dot shorthand syntax in a const collection literal:
const List<Point> keyPoints = [.origin(), .new(1.0, 1.0)];
// Instead of [Point.origin(), Point(1.0, 1.0)]

규칙과 제약 (Rules and limitations)

점 축약 표기는 명확한 문맥 타입에 의존하므로, 알아 두면 좋은 몇 가지 규칙과 제약이 있어요.

체인에서 명확한 문맥 타입 필요 (Clear context type required in chains)

메서드 호출이나 프로퍼티 접근 같은 연산을 점 축약 표기 뒤에 이어 붙일 수는 있지만, 전체 표현식은 문맥 타입에 대해 검증돼요.

컴파일러는 먼저 문맥을 사용해서 점 축약 표기가 무엇으로 해석되는지 결정해요. 체인의 후속 연산은 그와 같은 초기 문맥 타입과 일치하는 값을 돌려줘야 하죠.

// .fromCharCode(72) resolves to the String "H",
// then the instance method .toLowerCase() is called on that String.
String lowerH = .fromCharCode(72).toLowerCase();
// Instead of String.fromCharCode(72).toLowerCase()

print(lowerH); // Output: h

비대칭 동등성 검사 (Asymmetric equality checks)

==!= 연산자에는 점 축약 표기에 대한 특별한 규칙이 있어요. 동등성 검사의 오른쪽에서 점 축약 문법을 직접 쓰면, Dart는 왼쪽의 정적 타입을 사용해서 축약의 클래스나 enum을 결정해요.

예를 들어 myColor == .green 같은 표현식에서는 변수 myColor의 타입이 문맥으로 쓰여요. 이 말은 컴파일러가 .greenColor.green으로 해석한다는 뜻이에요.

enum Color { red, green, blue }

// Use dot shorthand syntax for equality expressions:
void allowedExamples() {
  Color myColor = Color.red;
  bool condition = true;

  // OK: `myColor` is a `Color`, so `.green` is inferred as `Color.green`.
  if (myColor == .green) {
    print('The color is green.');
  }

  // OK: Works with `!=` as well.
  if (myColor != .blue) {
    print('The color is not blue.');
  }

  // OK: The context for the ternary is the variable `inferredColor`
  // being assigned to, which has a type of `Color`.
  Color inferredColor = condition ? .green : .blue;
  print('Inferred color is $inferredColor');
}

점 축약 표기는 ==!= 연산자의 오른쪽에 있어야 해요. 조건 표현식 같은 더 복잡한 표현식과 비교하는 것도 허용되지 않아요.

enum Color { red, green, blue }

void notAllowedExamples() {
  Color myColor = Color.red;
  bool condition = true;

  // ERROR: The shorthand must be on the right side of `==`.
  // Dart's `==` operator is not symmetric for this feature.
  if (.red == myColor) {
    print('This will not compile.');
  }

  // ERROR: The right-hand side is a complex expression (a conditional expression),
  // which is not a valid target for shorthand in a comparison.
  if (myColor == (condition ? .green : .blue)) {
    print('This will not compile.');
  }

  // ERROR: The type context is lost by casting `myColor` to `Object`.
  // The compiler no longer knows that `.green` should refer to `Color.green`.
  if ((myColor as Object) == .green) {
    print('This will not compile.');
  }
}

표현식 문은 .로 시작할 수 없다 (Expression statements can't start with .)

미래의 잠재적인 파싱 모호성을 피하기 위해, 표현식 문은 . 토큰으로 시작할 수 없어요.

class Logger {
  static void log(String message) {
    print(message);
  }
}

void main() {
  // ERROR: An expression statement can't begin with `.`.
  // The compiler has no type context (like a variable assignment)
  // to infer that `.log` should refer to `Logger.log`.
  .log('Hello');
}

유니언 타입의 제한적 처리 (Limited handling of union types)

nullable 타입(T?)과 FutureOr<T>에는 특별 처리가 있지만, 지원은 제한적이에요.

  • nullable 타입(T?)에 대해 T의 정적 멤버에는 접근할 수 있지만, Null의 멤버에는 접근할 수 없어요.
  • FutureOr<T>에 대해 T의 정적 멤버에는 접근할 수 있지만(주로 async 함수 반환을 지원하기 위해), Future 클래스 자체의 정적 멤버에는 접근할 수 없어요.

더 알아보기