`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.
}
문제는 X가 fun의 타입에서 매개변수 타입으로 등장한다는 점이에요. 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 > rules에 unsafe_variance를 추가해요.
linter:
rules:
- unsafe_variance
YAML 맵 문법을 쓴다면 linter > rules 아래에 unsafe_variance: true를 넣으면 돼요.
linter:
rules:
unsafe_variance: true