unsafe_variance 진단
unsafe_variance 진단
unsafe_variance는 타입 매개변수가 공변(covariant)이 아닌 위치에 있을 때 Dart 분석기가 알려주는 린트(lint) 규칙이에요. 이런 코드는 보통 실행 시점에 타입 검사가 조용히 깨지기 쉬워서, 정적 오류도 없이 문제를 일으키는 경우가 많죠.
출처: unsafe_variance
본문
이 진단은 인스턴스 멤버의 결과 타입이, 그 멤버를 둘러싸고 있는 선언의 타입 매개변수에 대해 반변(contravariant)이거나 불변(invariant)일 때 발생해요. 여기서 변수의 결과 타입은 그 변수의 타입이고, getter나 메서드의 결과 타입은 반환 타입이에요. 그럼 왜 이런 멤버를 경고할까요? 이런 멤버들은 실행 시점에 타입 검사가 실패할 가능성이 높은데, 호출부에서는 정적 경고나 오류가 전혀 나오지 않아서 문제를 눈치채기 어렵거든요.
예시
다음 코드는 X가 f의 타입에서 매개변수 타입으로 나타나므로, 타입 매개변수 X가 반변 위치에 놓여 이 진단이 발생해요.
class C<X> {
final bool Function(X) f;
C(this.f);
}
여기서 위험이 보이시나요? c의 정적 타입이 C<num>이고 실행 타입이 C<int>라면, c.f는 throw를 일으켜요. 그래서 c.f(a)를 호출할 때, 설령 인자 a가 c.f의 인자로 올바른 타입을 갖는 경우조차 모든 호출이 throw돼요.
해결 방법
먼저, 린트 대상 멤버가 private이거나 private으로 만들 수 있다면 this 외의 다른 리시버에서 절대 접근하지 못하도록 강제하는 방법이 있어요. 이렇게 하면 실행 타입 오류가 아예 발생하지 않을 수 있어요.
class C<X> {
// NB: `_f`가 `this`에서만 접근되도록 직접 보장해야 해요.
// ignore: unsafe_variance
final bool Function(X) _f;
C(this._f);
// 클라이언트가 `_f`를 호출할 수 있도록 전달 메서드를 만들 수 있어요.
bool f(X x) => _f(x);
}
이번엔 멤버의 타입을 더 일반적인 타입으로 바꾸는 방법도 있어요. 다만 이 경우엔 호출부에서 실행 타입을 확인하고 다운캐스트(downcast)를 직접 수행해야 해요.
class C<X> {
final bool Function(Never) f;
C(this.f);
}
이렇게 하면 c의 정적 타입이 C<num>일 때 타입을 검사할 수 있어요. 예를 들어 c.f is bool Function(num)처럼요. 만약 그 타입이라면 num 타입의 인자로 안전하게 호출할 수 있죠.
또는 훨씬 더 일반적인 타입인 Function을 쓰는 방법도 있어요. Function은 함수에 대한 dynamic과 본질적으로 같아요.
class C<X> {
final Function f;
C(this.f);
}
이렇게 하면 c.f(a)가 동적으로 안전해져요. 인자 a가 함수가 요구하는 타입이 아닐 때만 throw하고, 정적 타입이 어긋났다는 이유로는 throw하지 않아요. 즉 타입 건전성(soundness)을 위해 반드시 throw해야 할 때만 throw하는 거죠. 원래 버전보다 훨씬 낫죠?