210 likes | 289 Views
Understand and implement non-null types in object-oriented languages for efficient error prevention and code robustness. Learn the advantages and challenges, with comprehensive examples and solutions provided.
E N D
Declaring and Checking Non-null Types in an Object-Oriented Language Authors: Manuel Fahndrich K. Rustan M. Leino OOPSLA’03 Presenter: Alexander Landau
Exceptions are Everywhere class A { private string s; public A(string str) { s = str; } public int length() {return s.Length);} } int x = new A(null).length(); Exception!
Exceptions • Used to signal about “bad things” during runtime. • Better to find errors at compile-time! • Types of exceptions: • Bad cast: dynamic type may only be known at runtime. • Division by zero: divisor unknown at compile time. • NULL dereference: compile-time checker can be created! • …
General Idea • Splitting reference types into non-null and possibly-null types • Already implemented in ML’s and some other languages’ type systems. • A non-null field provides a contract: • At construction, must be initialized with a non-null value. • Read access yields a non-null value. • Write access requires a non-null value.
Simple Solution • Require that an object under construction cannot be accessed until fully constructed. • Possible in some languages, but not in mainstream ones like C#/Java, where this may be accessed from the constructor or from methods called by it.
Example (1) class A { [NotNull] string name; public A([NotNull] string s) { this.name = s; this.m(55); } virtual void m(int x) { … } } name initialized before use
Example (2), but what if… class B : A { [NotNull] string path; public B([NotNull] string p, [NotNull] string s) : base(s) { this.path = p; } override void m(int x) { … this.path … } } m() called from A’s c’tor before B() initialized path!
A Glance at C++ • In C++ base-class object is created and initialized (by c’tor). Then, derived-class object is created and initialized. • Virtual functions act as non-virtual when called from within c’tors. • The problem from the previous example is eliminated. • In C#/Java, the object including all superclass objects is created first, then c’tors are called. • Virtual functions act as virtual even from within c’tors.
Advantages • Documentation of method input parameters, output parameters and return values. • Static (compile-time) check of object invariants such as non-null fields. • Error detection at the point of error commitment, not when dereferencing nulls. • No need to check for nulls at runtime – boosts performance. • Reduce/eliminate unexpected null reference exceptions.
Non-null Types • Notation: • T- - non-null references of type T. • Just like T& in C++ • T+ - possibly-null references of type T. • Just like T* in C++ • Examples: • T- t = new T(…); // newnever returns null • T+ n = t; // n may be null • if (n != null) t=n; // here n is of type T- • int x = t.f; // t must be non-null
Construction • Problem: half-baked objects in constructors. • this.f may be null even though f is declared as non-null. • Notation: Traw- denotes partially-initialized object types. • T- ≤ Traw-. • Rule: A T- field in Craw- object Read: May be nullWrite: Must be with a T- value.
The Construction Duty • The c’tor must initialize all non-null fields. • Restricted to the object proper, • not including sub- or super-class object fields. • Every path through a c’tor must include an assignment to every non-null field. • When a c’tor is called, all ancestor c’tors have already been called, thus members initialized. • The last c’tor called due to new C(…) casts this from Craw- to C-. • The annotation [Raw] allows a method to be called with this of type Craw-.
Example class B : A { [NotNull] string path; public B([NotNull] string p, [NotNull] string s) : base(s) { this.path = p; } [Raw] override void m(int x) { … this.path … } } class A { [NotNull] string name; public A([NotNull] string s) { this.name = s; this.m(55); } [Raw] virtual void m(int x) { … } }
Arrays • Arrays are references themselves and contain references. • The array itself and/or its elements may be non-null or possibly-null: • T- []- non-null array of non-null elements • T+ []- non-null array of possibly-null elements • T- []+ possibly-null array of non-null elements • T+ []+ possibly-null array of possibly-null elements
Arrays - Problem • In contrast to objects, there is no “constructor” that initializes all array elements to non-null values after allocation. • new T- [n] returns a reference of type T- []raw-. • Reading a[i] may yield null. • Writing a[i] requires non-null.
Arrays - Solution • Compiler can not know when array has finished initialization • Explicit cast required from programmer. • The cast validates the non-nullity of the elements. T- []raw- aTmp = new T- [n]; // initialize the elements of aTmp T- []- a = (T- []-)aTmp;
Other Language Constructs (1) • Structs • Default constructor initializes fields to zero-equivalent values (e.g. null for references). • Problem: Cannot be overridden! • Solution: All c’tors for a struct S produce a value of type S except the default c’tor which produces Sraw.
Other Language Constructs (2) • Call-by-reference parameters • Used for input – formal parameter type is a supertype of the actual parameter type. • Used for output - formal parameter type is a subtype of the actual parameter type. • Required in order to maintain conformance. • Thus, no-variance on ref parameters • Problem: For a raw object, a field f of type T- yields T+ on read and requires T- on write. • Solution: Disallow passing such fields as ref parameters.
Implementation • Possible! • Created by the authors. • Does not (yet) implement the full design. • Implemented at the CIL level. • Does not modify the compiler or runtime. • Works with other languages compiled into CIL. • Tested on a ~20,000 lines program.
Implementation - Benefits • Catches hard to find errors: • Vacuous initialization – this.foo = foo inside a c’tor. The goal was to initialize a field with a parameter, but there was no parameter named foo. • Wrong local bool m(Q other) { T that = other as T; if (other == null) return false; // should have used “that” if (this.bar != that.bar) … // “that” may be null
Conclusion • Non-null types allow moving some errors from runtime to compile-time. • Not a theoretical-only beast, implementations possible and exist. • Less runtime checks, faster code. • Backward compatible except in initialization, mainly in constructors.