Reopening the debate about non-nullable-by-default: initialization of member fields
Idan Arye via Digitalmars-d
digitalmars-d at puremagic.com
Fri May 2 17:50:14 PDT 2014
We are all sick and tired of this debate, but today I've seen a
question in Stack Exchange's Programmers board that raises a
point I don't recall being discussed here:
http://programmers.stackexchange.com/questions/237749/how-do-languages-with-maybe-types-instead-of-nulls-handle-edge-conditions
Consider the following code:
class Foo{
void doSomething(){
}
}
class Bar{
Foo foo;
this(Foo foo){
doSomething();
this.foo=foo;
}
void doSomething(){
foo.doSomething();
}
}
Constructing an instance of `Bar`, of course, segfaults when it
calls `doSomething` that tries to call `foo`'s `doSomething`. The
non-nullable-by-default should avoid such problems, but in this
case it doesn't work since we call `doSomething` in the
constructor, before we initialized `foo`.
Non-nullable-by-default is usually used in functional languages,
where the emphasis on immutability requires a syntax that always
allow initialization at declaration, so they avoid this problem
elegantly. This is not the case in D - member fields are declared
at the body of the class or struct and initialized at the
constructor - separate statements that nothing stops you from
putting other statements between them.
Of course, D does support initialization at declaration for
member fields, but this is far from sufficient a solution since
very often the information required for setting the member field
resides in the constructor's arguments. In the example, we can't
really initialize `foo` at the declaration since we are supposed
to get it's initial value in the constructor's argument `Foo foo`.
I can think of 3 solutions - each with it's own major drawback
and each with a flaw that prevents it from actually solving the
problem, but I'll write them here anyways:
1) Using a static analysis that probes into function calls. The
major drawback is that it'll probably be very hard to implement.
The reason it won't work is that it won't be able to probe into
overriding methods, which might use an uninitialized member field
that the overrided method doesn't use.
2) Disallow calling functions in the constructor before *all*
non-nullable member fields are initialized(and of course, the
simple static analysis that prevent usage before initialization
directly in the constructor code). The major drawback is that
sometimes you need to call a function in order to initialize the
member field. The reason is best demonstrated with code:
class Foo{
void doSomething(){
}
}
class Bar{
this{
doSomething();
}
void doSomething(){
}
}
class Baz : Bar{
Foo foo;
this(Foo foo){
this.foo=foo;
}
override void doSomething(){
foo.doSomething();
}
}
`Bar`'s constructor is implicitly called before `Baz`'s
constructor.
3) Use a Scala-like syntax where the class' body is a constructor
that all other constructors must call, allowing initialization on
declaration for member fields in all cases. The major drawback is
that this is a new syntax that'll have to be used in order to
have non-nullable member fields - which means it'll break almost
every existing code that uses classes. Not fun. The reason it
won't work is that declarations in the struct\class' body are not
ordered. In Scala, for example, this compiles and breaks with
null pointer exception when trying to construct `Bar`:
class Foo{
def doSomething(){
}
}
class Bar(foo : Foo){
doSomething();
val m_foo=foo;
def doSomething(){
m_foo.doSomething();
}
}
Also, like the previous two methods, overriding methods breaks
it's promises.
This issue should be addressed before implementing
non-nullable-by-default.
More information about the Digitalmars-d
mailing list