Description
Consider a non-covariant member, as described in dart-lang/sdk#57371. That is, roughly, an instance member of a class whose return type has a non-covariant occurrence of a type parameter from the enclosing class/mixin/enum/etc.
For example, in class A<X>...
it could be a method or getter whose return type is void Function(X)
, or a variable whose declared type is List<X> Function(X)
.
Such members are dangerous, in the sense that any reference to the member will give rise to a run-time type error if the statically known value for said type parameter differs from the run-time value:
class A<X> {
void Function(X) fun; // <-- This member is non-covariant.
A(this.fun);
}
void main() {
A<num> a = A<int>((i) => print(i.isEven));
a.fun; // Throws, we don't even have to call it!
}
In this example, the statically known value of X
in a
is num
, but the run-time value is int
. Those two types differ, so a.fun
throws. We can try to call it with a value which is actually ok for the run-time value of X
(like a.fun(10)
), but we won't even reach the point where the type correct actual argument is passed to the function, because the expression a.fun
will throw before we even get hold of the function object.
I've created a proposal for a lint, dart-lang/sdk#59050, which would flag every non-covariant member.
However, a non-covariant member can be used in a type safe manner with an extra constraint: When the receiver of the access is this
, the given type variable is in scope, and type checking can be performed safely:
class A<X> {
void Function(X) fun;
A(this.fun);
void callFun(X x) => fun(x); // This invocation of `fun` is type safe.
}
void main() {
A<num> a = A<int>((i) => print(i.isEven));
a.callFun(42); // Accepted at compile time, and succeeds.
}
We can easily enforce that all accesses to a given member must occur with this
as the receiver, we just need to specify in the declaration of the given member that it has this constraint.
As a strawman syntax, I'll use this.
in the declaration of an instance member in order to indicate that this member is "private to this
".
class A<X> {
void Function(X) this.fun; // <-- This member is private to `this`.
A(this.fun);
void callFun(X x) => fun(x); // OK, receiver of `fun` is `this`.
}
void main() {
A<num> a = A<int>((i) => print(i.isEven));
a.fun; // Compile-time error, receiver is not `this`.
a.callFun(42); // OK.
}
The ability to constrain the allowed set of receivers to this
can be useful in other ways, too. For example, this particular notion of privacy could be useful based on software engineering considerations (like maintainability, readability, enforcement of application domain specific constraints, etc.).
Note that being private to this
and being private are orthogonal concepts (where the latter is the normal, Dart privacy which is specified via names of the form _...
). That is, we can have declarations that are not private at all, that are private to this
, that are private (in the normal sense), or that are private and private to this
; each of those 4 combinations have their own properties.
One reason to use privacy to this
which is technical as well as relevant from a software engineering perspective is that it is much safer to use a private member (in the usual sense) if it is also private to this
:
abstract class A {
void this._foo(); // <-- This member is private to `this`.
}
class _B implements A {
void this._foo() {}
void bar() => _foo();
}
At the invocation of _foo
, it is known that this
has type _B
or a subtype thereof, and it is possible to detect through analysis of the current library whether or not we can know that every subtype of _B
has an implementation of _foo
.
The name _foo
is only accessible in the same library, and this means that we can check every invocation, to see that it occurs in a location where it is guaranteed that this
has an implementation of _foo
. That's not true if _foo
is invoked in the body of A
because some other library could create a concrete subtype of A
that does not have an implementation of _foo
. If that is true then this invocation is safe; if it is not known to be true then we may encounter a 'no such member' failure at run time.
The point is that if an instance member is private to this
then it is a lot easier to establish some level of trust in the assumption that said member actually exists at each call site, up to a situation where it is a firm guarantee.
Based on these considerations, I think it may be worthwhile to support the notion of instance members that are private to this
.