Open HosseinYousefi opened 1 month ago
without additional annotations, every Java object can be nullable and the resulting code will be full of null-checks
From what I've seen in our own plugin development, annotating the nullability of everything public is best pratice (for interop with Kotlin, I assume). Given that annotations to solve this problem exist and are readily available I would think we would want to optimize for making better type safety easier, rather than optimizing for the ease of use of code that's not annotated.
Also, without annotations, the correct interpretation in a language like Dart is (as you said) that everything can be nullable. If everything could be nullable, then code being full of null checks is correct. So that doesn't actually seem like a problem to me (in the case of non-annotated code that can't be annotated for whatever reason).
Sounds good. I can use the same annotations Kotlin uses to detect nullability by default: https://kotlinlang.org/docs/java-interop.html#nullability-annotations
and also add a way to include more annotations.
One problem with the null-safe code generation is that the auto-detection of type classes no longer works for nullable types. For example
// BEFORE: user can do `foo(jstring)` and not pass `T` as it will be taken from bar
void foo($T bar, {JObjType<$T>? T}) {
T ??= bar.$type;
// ...
}
// AFTER: user has to write `foo(jstring, T: JString.type)`
void foo($T? bar, {required JObjType<$T> T}) {
// bar?.$type can be null
// ...
}
The migration to extension types for all JObject
s will eliminate the need for type classes for generics, so the regression is temporary: #1634.
We can make T extends JObject
nullable by writing T?
, but we can't make T extends JObject?
non-nullable in Dart. I'll add some docs and assertion in this case.
What to do with generic parameters if a class does not have annotations? A class Foo<T>
, can have a method T foo()
. As T
is not specified to non-nullable, in Dart we can create both a Foo<JString>
and Foo<JString?>
. In case of Foo<JString>
, will the method foo
return a JString
or a JString?
?
Kotlin treats it as JString
, but of course this is not a given, as in a code without any annotations, T
and T?
are the same. In this case if T = JString
and foo
wants to return null
, we can throw a runtime error, forcing users to use Foo<JString?>
instead.
What about type-classes? Currently we have a JString.type
, we can add a JString.nullableType
.
Kotlin treats it as
JString
, but of course this is not a given, as in a code without any annotations,T
andT?
are the same.
Does it treat it as non-nullable, or is it just usable silently as if it were because of type erasure?
I would expect generics to be treated like any other unannotated Java type, and be considered nullable in Dart, because they can in fact be null.
I would expect generics to be treated like any other unannotated Java type, and be considered nullable in Dart, because they can in fact be null.
If the user constructs it passing T
to be JString.type
and not JString.nullableType
, then they are basically implying that T
will not be nullable. Otherwise there is no difference between Foo<JString>
and Foo<JString?>
.
Of course you will always get Foo<T?>?
in the wild in unannotated code. This is only for when you explicitly pass a non-null type class to construct an object.
Does it treat it as non-nullable, or is it just usable silently as if it were because of type erasure?
It treats it as non-nullable.
A real life example of the above is java.util.Map
. If we create a map with using non-nullable types, without any nullability annotations, it's assumed that V put(K key, V value)
will return V = JString
and not V?
even though it returns the previously associated value of key
and null
if there was no value associated with key
. Same is true for get
.
// Dart
final jmap = JMap.hash(JString.type, JString.type);
jmap.put('hello'.toJString(), 'world'.toJString()); // Error!
Kotlin simply knows about map:
// Kotlin
val map = java.util.HashMap<String, String>()
map.put("hello", "world") // Fine, it returns a `String?`
and map.values.first()
will return String
when the type is non-nullable in Kotlin.
What about a type that it doesn't know about? Like MyMap
:
// Java
public class MyMap<K, V> {
V put(K key, V value) {
return null;
}
}
In this case the return type of put
is String!
:
// Kotlin
val map = MyMap<String, String>()
val bar = map.put("hello", "world") // Returns `String!` which is basically `String? | String`.
What if MyMap
has annotations on the type parameters but not on the return types?
// Java
public class MyMap<@NotNull K, @NotNull V> {
V put(K key, V value) {
return value;
}
}
Now Kotlin assumes put
returns String
even though we have not explicitly specified @NotNull V put(...)
on the method itself.
I'll go with the same logic for jnigen and since Dart doesn't have a T!
we use T?
for unannotated code.
More importantly, if we explicitly specify V
to be @Nullable
, still put
will have a return type of V
and not V?
in Kotlin:
// Java
public class MyMap<K, @Nullable V> {
V put(K key, V value) {
return value;
}
}
val map = MyMap<String, String>()
val bar: String = map.put("hello", "world")
Basically the moment that we have any nullability annotation on the type param origin, not putting a nullability annotation on the spot means "the same nullability of the origin".
So far we decided not to make every Java object nullable in Dart because without additional annotations, every Java object can be nullable and the resulting code will be full of null-checks. However, we may even prefer this because then we can't forget to do
if (nullableJObject.isNull)
as the type system prevents us.I recently made
fromReference
internal, so now the only way to passnull
without hitting a lint is the verboseJObject.fromReference(jNullReference).as(Foo.type)
!Foo?
and ifnull
is passed, we interpret that as a Java object with a reference ofnullptr
.jnull
constant which users have to cast to the right objectjnull.as(Foo.type)
wdyt @liamappelbe @dcharkes @stuartmorgan?