Partager via


Difficulties with non-nullable types (part 3)

In the last post on this topic i talked about how it was critical that
by the time a non-null type was used it was provably assigned some
non-null value.  The cases that came up were specifically instance
fields in classes.  In that post i showed how would could change
how types were initialized to ensure that that these non-null
constraints could be met.

However, there is another type of field out there: static
fields.   And it turns out that these fields are  much
harder to deal with than instance fields.  Why?  Well, let's
take a look at a simple example:

 class Person {
    public static string! name;
}

How do we initialize that field?  Well, we could start with a static constructor like so:

 class Person {
    public static string! name;

    static Person() {
         a = new Cyrus().name;
    }
}

class Cyrus {
    public static string! name = "cyrus";
}

Ok.  Seems like this would work out and you'd end up with all
non-null "name"'s being assigned a value.  However, let's extend
that a little further:

 class Cyrus {
    public static string! name = "cyrus";

    public Cyrus() {
        Cyrus.name = Person.name;
    }
}

Uh oh.  Now we've got a problem.  Everything looks typesafe,
but when the Person class loads, it will call the 'Cyrus' constructor
which will then access the "Person.name" field before it has been
initialized!  Not good!

As it turns out this is the same problem as the instance-field issue
from the last post.  As i mentioned there, determining the set of
code that is dependent on a constraint is extremely difficult and so we
need a more restrictive set of rules that we can work with in order to
be able to reason about this properly.

In order to support the instance fields case we placed the restriction
that before the "this" pointer could be used, it has to be the case
that all the non-nullable fields had been assigned to.  Can we
extend that idea out to static fields?  Since there is no "this"
pointer we would have to say something like "before you can use the
actual type containing the static field, all fields must be assigned
to".  

This is a much more limiting set of circumstances, and far harder to
enforce.  With instance fields you at least have access to a rich
set of inputs to initialize them with.  You can use static data in
the system, or you can use parameters passed into a constructor. 
With static fields things are not so clear cut.  As you can see
above, arbitrary code can't be run as that code might statically access
the type being constructed.  Also, as static constructors don't
take any parameters it's hard to find inputs to assign to these fields.

So what can be done about this?  It's not clear to me what the
best solution is.  The easiest solution is to simply disallow
static non-nullable types.  But that would suck since i think that
people really want them.  The next easiest would be to have
non-nullable static fields, but guard all read/writes to them to check
for null.  That would put the onus on the programmer to ensure
that it's value is set before use, but it means that suddenly accessing
this field could fail at runtime.  Ugh*2.  The final thing
that i can think would be a solution like the following inside the
runtime:

1.
When loading a type that has non-nullable static fields, start executing the static cosntructor 2. When the static constructor enters, set a bit saying that the type
is currently being constructed.  Then, if a static non-nullable
field in that type is ever accessed by anyone else, throw an exception
saying that this type wasn't fully initialized and it's invalid to
access non-null static state yet. 3. If the code executed in the static constructor then causes something
like the above situation to happen, then you'll immediately fail at
typeload time.

This has the unfortunate outcome that the checking is still done at
runtime.  however, it will most likely occur early on when all
your types are being loaded, and not much later on the first access of
one of these fields.

In essence, what we've done is enforce a quasi ordering on how types
are loaded in the runtime.  As a type loads it can access static
non-nullable fields in any fully loaded type, but not in a type that is
still being loaded.

It's ugly, but i can't think of anything better.  Do you guys have any ideas?

Comments

  • Anonymous
    April 25, 2005
    You could make it illegal to reference a non-nullable static within a static constructor, thus breaking the dependency cycle.
  • Anonymous
    April 25, 2005
    Gatsby: If we didn't allow access to the static fields in the static constructor, then how would we initialize the fields?

    Note, if you have:

    public static string! name = new Cyrus().name;

    then that code in the initializer just goes into the static constructor.

    So, without the static constructor, there is no way to initialize the static non-nullable fields. And if you don't initialize them, then they certainly won't be "not" null :)
  • Anonymous
    April 25, 2005
    Gatsby: If we go down that path, that seems like my first option which is to just not allow non-nullable statics.
  • Anonymous
    April 25, 2005
    Sorry, let me make that more precise: "In a static constructor, you cannot have anything non-nullable in the RHS of an expression, which cannot be proved to be non-null." This doesn't stop you from assigning to A.staticNonNullableVar in the static constructor of A.

    However one variable that can't be proved to be non-null in your example is Person.name, namely because we can't know at compile time that Person hasn't been initialized yet. So in general, referencing non-nullable statics of other classes would not fly.

    Make sense?
  • Anonymous
    April 25, 2005
    And by 'RHS of an expression' I meant right hand side of an assignment operation. :)
  • Anonymous
    April 25, 2005
    The comment has been removed
  • Anonymous
    April 25, 2005
    The comment has been removed
  • Anonymous
    April 26, 2005
    You could require that all non-null reference types are assigned a default value at declaration. It would incur a penalty of the overhead of instantiating a possibly unneeded reference type but all of these other issues would be moot.

    Possibly not the best suggestion but its worth considering.
  • Anonymous
    April 26, 2005
    The comment has been removed
  • Anonymous
    April 26, 2005
    The comment has been removed
  • Anonymous
    April 26, 2005
    The comment has been removed
  • Anonymous
    April 26, 2005
    Joe: "This is something Haskell got right and C[++|#|blah] got wrong. Nullability should be completely opt-in. Maybe<T> = Just T | nil works for me."

    I have to disagree with taht Joe. The Haskell style of Maybe (also known as "Optional", or in
    other funtional languages) is a distinctly differnt concept than Nullable vs. Nonnullable in OO languages. In OO Nullable follows the "is a" relationship. So Nullable<T> is-a T. In Haskell/Scheme/Ocaml, this is not the case with Maybe. A Maybe<int> is not an int.

    Note: in C# i have my own "Maybe" class (which i call "Optional"). I blogged about this before on this blog. However, it serves a very different purpse than nullable.


    "With that said, I love the idea of pre-/post-/invariant-condition annotations to the C# syntax. This is a great use of that style of programming. "

    Cool :)
  • Anonymous
    April 26, 2005
    So far i've discussed two interesting problems that arise when you try to add non-nullable types to C#...
  • Anonymous
    April 27, 2005
    In static constructors you could disallow access to static non-nullable fields (as well as static non-nullable properties, functions with static non-nullable return types or non-nullable out and ref parameters) of other types: e.g. inside the static constructor of Cyrus, the static fields of Person would not be accessible.
  • Anonymous
    April 27, 2005
    The comment has been removed
  • Anonymous
    April 27, 2005
    Joc: "In static constructors you could disallow access to static non-nullable fields (as well as static non-nullable properties, functions with static non-nullable return types or non-nullable out and ref parameters) of other types: e.g. inside the static constructor of Cyrus, the static fields of Person would not be accessible."

    If you disallow all that... then how do you initialize your static fields? :)

    You're basically disallowing me from touching any type that might transitively touch a type with a static initializer. This will be very constraining.
  • Anonymous
    April 27, 2005
    The comment has been removed
  • Anonymous
    April 27, 2005
    Cyrus, cool... think we're on the same page. Although we may differ in that I think nullability should be opt-in rather than opt-out.

    In Haskell for example, forcing people to unbox a "Maybe of a" makes them realize they have to deal with the null case, or pushes it onto the caller if it understands only an "a"... (i.e. not nullable) And with pattern matching it becomes so much simpler than doing null branch testing to avoid sporadic NullRefExceptions.
  • Anonymous
    May 02, 2005
    Joc: "In static constructors you could disallow access to static non-nullable fields (as well as static non-nullable properties, functions with static non-nullable return types or non-nullable out and ref parameters) of other types: e.g. inside the static constructor of Cyrus, the static fields of Person would not be accessible."

    CyrusN: "If you disallow all that... then how do you initialize your static fields? :)"

    With your static constructors! Person's static constructor is allowed to initialize Person's static non-nullable fields and Cyrus' static constructor is allowrd to initialize Cyrus' static non-nullable fields but you may not use the former to initialize the latter and conversely.
  • Anonymous
    February 21, 2006
    Beijing hotels
  • Anonymous
    July 25, 2007
    There is no-doubt that the C#2 nullable-types is a cool feature. However I regret that C# don&#39;t support