Description
The Dart language currently supports the following kinds of implicit conversions:
- Conversion from an integer literal to an equivalent double literal
- Conversion from
dynamic
to some other type via an implicit cast - Conversion from an interface type containing a
.call
method to a function type, by implicitly tearing off.call
The first conversion only happens on integer literals, and is based on the context type. The other two conversions are currently only performed at sites where the spec demands assignability, and is based on the type being assigned to. The language team is considering changing the behavior so that all implicit conversions are based on context type, and happen at the innermost expression where they're applicable, regardless of assignability constraints. (We would retain the restriction that int-to-double conversion only happens to literals).
Here are some examples to illustrate the user-observable consequences of this change (changes are in bold). (Assume F
is a function type, f
is an expression of type F
, fq
is an expression of type F?
, C
is a callable class with a foo
method of type void Function()
and a call method of type F
, c
is an expression of type C
, cq
is an expression of type C?
, and d
is an expression of type dynamic
. Also assume there's a declaration of Object o = f;
in scope, with declared type Object
and promoted type F
):
- (1)
F x = b ? c : f;
will be interpreted asF x = b ? c.call : f;
, which will work. Today it's a compile time error becauseb ? c : f
has static typeObject
, which is not assignable toF
. - (2)
F x = b ? c : d;
will be interpreted asF x = b ? c.call : (d as F)
, which will work, unlessb
isfalse
andd
's runtime type is notF
. Today this code is accepted, becauseb ? c : d
has static typedynamic
, but it fails at runtime ifb
istrue
, because there is no implicit tearoff ofcall
. - (3)
F x = b ? "x" : d;
won't change. Today it's interpreted asF x = (b ? "x" : d) as F;
. This will change toF x = b ? ("x" as F) : (d as F);
, which is indistinguishable. - (4)
o = c;
will be interpreted aso = c.call;
, retaining the promotion ono
. Today this code is accepted, but there is no implicit tearoff ofcall
, so it demoteso
toObject
, which is probably not what the user wants. - (5)
o = d;
will be interpreted aso = (d as F);
, which retains the promotion ono
, but fails at runtime ifd
's runtime type is notF
. Today this code is interpreted aso = (d as Object);
, which demoteso
. - (6)
F x = cq ?? f;
won't change. This code is rejected because we don't imply implicit.call
tearoffs to nullable expressions, socq ?? f
has typeObject
, which is not assignable toF
. - (7)
var x = cq ?? f;
wont' change. This code is accepted and interpreted asObject x = cq ?? f;
(no downcasts or implicit tearoffs). - (8)
var x = cq ?? d;
will be interpreted asC? x = cq ?? (d as C?)
, which has a more specific type, but will fail at runtime ifcq
isnull
andd
's runtime type is notC?
. Today this code is interpreted asdynamic x = cq ?? d;
, which never fails but has a less specific type. - (9)
var x = fq ?? d;
will be interpreted asF? x = fq ?? (d as F?)
, which has a more specific type, but will fail at runtime iffq
isnull
andd
's runtime type is notF?
. Today this code is interpreted asdynamic x = fq ?? d;
, which never fails but has a less specific type. - (10)
F x = fq ?? c;
will be interpreted asF x = fq ?? c.call;
, which will work. Today it's a compile time error becausefq ?? c
has static typeObject
, which is not assignable toF
. - (11)
var x = fq ?? c
will be interpreted asF x = fq ?? c.call;
, which is more useful. Today it's accepted and interpreted asObject x = fq ?? c
, which doesn't fail, but also leavesx
in a state where it's hard for the user to do anything useful with it, because at runtime it might either be an instance ofC
or a function. - (12)
F x = c..foo();
will be a compile-time error because it's interpreted asF x = c.call..foo();
, andc.call
has typeF
, which has no methodfoo
. Today it's interpreted asF x = (c..foo()).call;
, which works. - (13)
F x = (c);
won't change. Today it's interpreted asF x = (c).call;
. This will change toF x = (c.call);
, which is indistinguishable.
(Thanks @lrhn for working through most of these examples).
A rough summary of this might be:
- For the conditional operator (cases (1) through (3)) it's a clear win: code that leads to compile-time and runtime errors today will "just work".
- For assignments to local variables (cases (4) and (5)) it's probably a win; I suspect that retaining the promotion is more likely to be what the user wants in most circumstances. But it's not as clear cut, since the downside is a tighter downcast for (5), which might lead to a runtime failure if it's not what the user intended.
- For the null-aware operator (cases (6) through (11)) it's also not 100% clear whether it's an improvement. Cases (10) and (11) seem like clear wins, but cases (8) and (9) could lead to runtime failures. Personally I think it's still a worthwhile tradeoff, because even though (8) and (9) could lead to runtime failures, they also lead to the variable
x
having a more specific type, which should help reduce dynamism (and catch errors at compile time) in the code that follows. - For cascades (case (12)) it's a clear loss of functionality, although IMHO it's probably an acceptable loss, since I think doing this kind of thing is rare, and there's an easy workaround (the user can just explicitly write
F x = (c..foo()).call;
).