`unsafe_variance` 린트 규칙: 공변성이 아닌 위치의 타입 변수

unsafe_variance 린트 규칙: 공변성이 아닌 위치의 타입 변수

unsafe_variance클래스·믹스인·enum의 타입 매개변수(type parameter)가 공변(covariant)이 아닌 위치에 등장하는 멤버를 선언했을 때 Dart 분석기가 알려주는 린트 규칙이에요. 이런 멤버는 타입 검사가 실패해서 런타임 오류로 이어지기 쉬워요. 이 규칙은 실험적(Experimental) 규칙이에요.

출처: unsafe_variance

본문

설명

인스턴스 변수의 타입 안에 감싸고 있는 클래스·믹스인·enum의 타입 매개변수가 공변이 아닌 위치에 있으면, 타입 검사가 실패하면서 런타임 오류를 일으키기 쉬워요. 예를 들어 class C<X> {...}에서 void Function(X) myVariable; 형태의 인스턴스 변수가 이런 런타임 실패를 일으킬 수 있어요.

반환 타입에 감싸고 있는 선언의 타입 매개변수가 공변이 아닌 위치에 있는 getter나 메서드도 마찬가지예요. 이 린트는 이런 종류의 멤버 선언을 잡아내요.

BAD:

class C<X> {
  final bool Function(X) fun; // LINT
  C(this.fun);
}

void main() {
  C<num> c = C<int>((i) => i.isEven);
  c.fun(10); // Throws.
}

문제는 Xfun의 타입에서 매개변수 타입으로 등장한다는 점이에요. C<num> 타입으로 선언했는데 실제로는 C<int>를 넣었고, fun(10)을 호출할 때 타입 검사가 실패하며 예외가 던져져요.

런타임 타입 오류의 가능성을 줄이는 방법 중 하나는, 공변이 아닌 멤버 fun오직 this에 대해서만 사용하도록 하는 거예요. 이를 엄격하게 강제할 수는 없지만, 멤버를 private으로 만들고 전달 메서드(forwarding method) fun을 추가해서 같은 라이브러리 안에서 이 제약이 지켜지는지 로컬하게 확인할 수 있어요.

BETTER:

class C<X> {
  // ignore: unsafe_variance
  final bool Function(X) _fun;
  bool fun(X x) => _fun(x);
  C(this._fun);
}

void main() {
  C<num> c = C<int>((i) => i.isEven);
  c.fun(10); // Succeeds.
}

_fun을 private으로 만들고 fun(X x)_fun(x)를 호출하는 구조예요. 이제 fun(10) 호출은 성공해요.

완전히 안전한 접근은 Dart가 아직 지원하지 않는 기능, 즉 정적으로 검사되는 공변성(statically checked variance) 이 필요해요. 그 기능이 있으면 타입 매개변수 X가 불변(invariant, inout X)임을 명시할 수 있어요.

정적으로 검사되는 공변성 지원 없이도 불변성을 흉내내는 게 가능해요. 이 방식은 하위 타입 생성에 몇 가지 제약을 두지만, inout이 주는 타입을 충실히 제공해요.

GOOD:

typedef Inv<X> = X Function(X);
typedef C<X> = _C<X, Inv<X>>;

class _C<X, Invariance extends Inv<X>> {
  // ignore: unsafe_variance
  final bool Function(X) fun; // Safe!
  _C(this.fun);
}

void main() {
  C<int> c = C<int>((i) => i.isEven);
  c.fun(10); // Succeeds.
}

이 방식에서는 C<int>C<num>의 하위 타입이 아니게 되므로, c는 다른 선언 타입을 가져야 해요.

또 다른 방법은 변수를 안전하지만 더 일반적인 타입으로 선언하는 거예요. 그러면 변수 자체를 사용하는 건 안전하지만, 호출할 때마다 런타임에 검사가 필요해요.

HONEST:

class C<X> {
  final bool Function(Never) fun;
  C(this.fun);
}

void main() {
  C<num> c = C<int>((int i) => i.isEven);
  var cfun = c.fun; // Local variable, enables promotion.
  if (cfun is bool Function(int)) cfun(10); // Succeeds.
  if (cfun is bool Function(bool)) cfun(true); // Not called.
}

타입을 Function(Never)으로 더 일반적으로 선언하고, 지역 변수로 옮겨 프로모션을 활용한 뒤 타입을 확인하고 호출해요.

활성화하기

unsafe_variance 규칙을 켜려면 analysis_options.yaml 파일의 linter > rulesunsafe_variance를 추가해요.

linter:
  rules:
    - unsafe_variance

YAML 맵 문법을 쓴다면 linter > rules 아래에 unsafe_variance: true를 넣으면 돼요.

linter:
  rules:
    unsafe_variance: true

더 알아보기