Working with objects

If you have experience with object-oriented programming, you'll find much of D's OOP support familiar. As such, I need to reiterate my warning from before: D is not C++, Java, or C#. Familiarity can help you pick some things up more quickly, but it can also lead you to take other things for granted. This section introduces D's OOP features.

Encapsulation with protection attributes

There are four levels of protection in D: public, package, private, and protected. The first three apply to classes, structs, and modules, but protected only has meaning with classes. We'll examine it later when we talk about inheritance.

Public

Anything declared public in a module, whether it's in module scope or as part of a class or struct declaration, is visible anywhere it's imported. With the exception of import declarations, all declarations in a module, class or struct are implicitly public. Save the following as $LEARNINGD/chapter03/protection1.d:

module protection1;
import std.stdio;  // private, only visible in this module
class MyClass {
  void sayHello() { writeln("Hello"); }
}
struct MyStruct {
  void sayHello() { writeln("Hello"); }
}

Now create a new file in the same directory, let's call it importer1.d, and add this:

void main() {
  import protection1;
  auto mc = new MyClass;
  mc.sayHello();
  MyStruct ms;
  ms.sayHello();
}

Compile both files. Every symbol declared in protection1.d is visible in importer.d, so there are no errors on compilation.

Private

Go back to protection1.d and add this right after the module declaration:

private:

As this uses the colon syntax rather than braces, every symbol following this line—until another protection attribute is encountered—is now private to the protection module. The import of std.stdio was already private, so this has no effect on it. Compile both modules again. Now two compiler errors are produced reporting that MyClass and MyStruct are private. That's just what one would expect. Now save the following as protection2.d in the same directory:

module protection2;
import std.stdio;
class MyClass {
  private void sayHello() { writeln("Hello"); }
}
struct MyStruct {
  void sayHello() {
    MyClass mc = new MyClass;
    mc.sayHello();  // calls private member function of MyClass
  }
}

Then save this as importer2.d, compile, and run:

void main() {
  import protection;
  MyStruct ms;
  ms.sayHello();
}

This will be a big surprise to most folks. In D, anything declared private is private to the module. In other words, MyClass.sayHello is accessible everywhere inside the protection module. There's no such thing as private to the type in D. Unlike C++, D does not have the concept of a friend function, as the same behavior arises via the private and package attributes.

Package

Any symbol-declared package is accessible only to modules in the same package or in any of its subpackages. To demonstrate, create two packages, encap and encap.internal, and populate them with a few modules. Filenames are commented:

// $LEARNINGD/chapter03/encap/support.d
module encap.support;
package void supportFunction() {
  import std.stdio: writeln;
  writeln("Providing support!");
}
// $LEARNINGD/chapter03/encap/base.d
module encap.base;
void primaryFunction() {
import encap.support;
  supportFunction();
}
// $LEARNINGD/chapter03/encap/internal/help.d
module encap.internal.help;
void helperFunction() {
  import encap.support;
  supportFunction();
  import std.stdio: writeln;
  writeln("Helping out!");
}

There are three modules: encap.base, encap.support, and encap.internal.help. Each module declares one function, two of which are public and one of which is package. Now create $LEARNINGD/chapter03/packtest.d, like so:

void main() {
  import encap.base;
  primaryFunction();
}

Finally, compile it all with the following command:

dmd packtest.d encap/base.d encap/support.d encap/internal/help.d

Ideally, the only function in the encap package that should be accessible to the outside world is primaryFunction. However, encap.internal.help.helperFunction is public. The default package protection does not extend accessibility to super packages, so helperFunction has to be public in order for it to be accessible inside encap. This comes with the consequence that it's also accessible outside the package. Actually, something can be done about that. Go back to encap.internal.help and change the declaration of helperFunction to look like this:

package(encap) void helperFunction() {…}

Specifying a package name with the package attribute makes the symbol accessible in that package and all of its subpackages. With this, helperFunction is still accessible in encap.base and is no longer accessible outside the encap package hierarchy.

Voldemort types

The preceding paragraphs use the terms accessible and accessibility quite a bit. We saw that, when the declarations of MyStruct and MyClass are private, instances cannot be instantiated in another module; the symbols are inaccessible. There's more to the story. Save the following as $LEARNINGD/chapter03/priv.d:

module priv;
private struct Priv {
  int x, y;
}
Priv makeAPriv(int x, int y) {
  return Priv(x, y);
}

You might assume that public members in a private struct declaration are meaningless. Test that assumption with $LEARNINGD/chapter03/privtest.d:

import std.stdio;
import priv;
void main() {
  auto priv = makeAPriv(10, 20);
  writeln(priv.x);
}

Pass both files to the compiler; not only will it compile them, but running the resulting executable will print 10, the correct value of priv.x. The symbol may not be accessible, but the type itself is. Change the highlighted line to the following:

Priv priv = makeAPriv(10, 20);

This yields a compiler error similar to the one we saw earlier with MyStruct and MyClass. This is another benefit of type inference; a type can be completely hidden while only exposing its interface.

We can take this a couple of steps further. The type is still exposed in the function declaration, but, by replacing Priv with auto, the compiler will infer the return type. Also, since types can be declared inside a function, the declaration of Priv can be moved out of module scope and into the function's scope. In this case, we have to use auto to infer the return type, since the type does not exist outside the function scope and cannot be part of the declaration:

auto makeAPriv(int x, int y) {
  struct Priv {
    int x, y;
  }
  return Priv(x, y);
}

With these changes to the priv module, the original privtest.d will still compile. The Priv symbol is not accessible anywhere outside makeAPriv. We can refer to Priv as a type that shall not be named, or a Voldemort type.

Constructors and destructors

Constructors and destructors can be implemented for classes, structs, and even modules. However, there are differences with each that need to be accounted for.

Class constructors and destructors

We've not explicitly declared any constructors in any class declarations so far, but we've still been able to construct new instances. That's because the compiler automatically adds a default constructor if no constructors are implemented. A default constructor is one that takes no arguments:

class OneCon {
  private int x;
  this(int x) { this.x = x; }
}

OneCon has an explicitly implemented constructor, so new OneCon() will not compile. To create a new instance of OneCon, a constructor argument must be provided:

auto oc = new OneCon(10);

Before the constructor is run, x will be initialized by default. Since no default initialization value was specified in the declaration, it will be initialized to int.init. After that, the constructor is executed and the parameter x is assigned to the member x. Note this.x is used to specify the member variable, which is legally shadowed by the parameter of the same name. The this reference is accessible in every non-static function of both classes and structs. To avoid the need to use this so often in member functions, I prefer to prefix private member variable names with an underscore.

To enable default construction when another constructor is defined, a default constructor must also be provided. That would look like this:

class TwoCon {
  private int _x;
  this() { this(10); }
  this(int x) { _x = x; }
}

The default constructor here sets x to 10 by calling the single-argument constructor. Note that, unlike Java, D places no restrictions on where such a call can take place inside a constructor, meaning it need not be the first line. To invoke a default constructor, parentheses are optional:

auto tc1 = new TwoCon();
auto tc2 = new TwoCon;

Perhaps the biggest issue for C++ programmers in D is the class destructor:

class Decon {
  this() { writeln("Constructed!"); }
  this() { writeln("Destroyed!"); }
  void print() { writeln("Printing."); }
}
void printDecon() {
  auto d = new Decon;
  d.print();
}
void main() {
  printDecon();
  writeln("Leaving main.");
}
static ~this() {
  writeln("Module destructor.");
}

Running this shows that the message in the class destructor is printed after the one in the static module destructor (more on that soon). Remember, the new expression allocates memory from the GC heap. That means the garbage collector is managing the memory behind the Decon instance. The GC will call the destructor when the memory is released, but not before. The language does not specify when the memory is released. In fact, the language says there is absolutely no guarantee that a class destructor will ever be executed. Let me repeat: you cannot depend on a class destructor ever being run. It's better to think of a class destructor in D as a closer relative of the Java finalizer than the C++ destructor. Attempting to use them for C++ style RAII is going to be painful.

In practice, the GC implementation that is used as I write runs in two scenarios: when new memory is allocated, and during the shutdown phase of DRuntime. In the previous example, the GC is never run once the Decon instance is allocated until the app terminates. The current implementation of DRuntime happens to run static destructors before terminating the GC, but that may not always be true. Nor is there any guarantee that the GC will call any destructors during termination.

Replacing the single call to printDecon with a loop that calls it ten times prints ten destructor messages on termination. The GC will only release memory, and therefore call destructors, as needed, and it never needs to during runtime in this case. Call it a thousand times in a loop and the GC will need to do some work; some of the destructors are called inside printDecon, with others called at termination.

One more potential problem spot is that class destructors in D are nondeterministic, meaning that their order of execution is unpredictable. The GC can call destructors in any order. A direct consequence of this is that manipulating GC-managed memory inside a class destructor will inevitably cause an abrupt termination of the program:

class Innocent {
  void bye() { writeln("Bye!"); }
}
class Boom {
  Innocent _inno;
  this() { _inno = new Innocent(); }
  ~this() { _inno.bye(); }
}

It's quite easy to believe that because _inno is a member of Boom, it will always be valid when the destructor is run. That's just not the case. From the garbage collector's perspective, the only notable fact about _inno being a member of Boom is that, if any given Boom instance is no longer accessible (that is, it's eligible for collection), then _inno is also no longer accessible. As long as no other references to that instance of Innocent exist, it can be collected at any time. It is quite possible for the instance behind _inno to be destroyed before the destructor is run on the Boom instance. In that case, you'll see a segfault when _inno.bye() is eventually called in the destructor. Never access GC-managed memory inside a class destructor.

Struct constructors and destructors

A struct instance can be explicitly initialized using struct literals or C-style initializers:

auto ms1 = MyStruct(10, 11);// struct literal
MyStruct ms2 = {10, 11};   // C-style, not preferred
MyStruct ms3 = {b:11, a:10};// Named initializers

In the first two lines, the members are initialized in the order they were declared, so 10 goes to a and 11 goes to b. If there are fewer initializers than members, the remaining members will be default-initialized using the .init value for the relevant type. If there are more initializers than members, a compiler error results.

Struct literals are convenient for simple types (though they can become an annoyance if the type declaration changes), but they only allow for direct initialization of member variables. If more complex initialization is required, struct constructors should be used. A struct does not have a default constructor. It doesn't have one generated for it, nor can one be explicitly implemented. Default construction for a struct is the same as setting it to its .init value. Look at the following declaration:

struct StructCon {
    int x, y;
    this(int val) { x = y = val; }
}

This type has two publicly accessible members and a constructor that sets each member to the same value. Here are different ways to declare a StructCon instance:

StructCon s1;                // .init: x = 0, y = 0
auto s2 = StructCon();       // .init literal: x = 0, y = 0
auto s3 = StructCon(12);     // constructor: x = y = 12

In the first declaration, s1 is default initialized. In the second, s2 is explicitly initialized with the default .init value, making it equal to s1. In the declaration of s3; both x and y are assigned the value 12. When a struct constructor is defined, struct literals can no longer be used to initialize any instances of that type.

Default initialization can be turned off completely:

struct StructCon {
    int x, y;
    this(int val) { x = y = val; }
    @disable this();
}

The highlighted line tells the compiler that instances of StructCon cannot be implicitly default initialized; they must be explicitly initialized with the .init value or through the constructor. Be aware that this has far-reaching consequences. Consider the following example:

struct Container {
  StructCon sc;
}
Container container; // Error: default construction disabled

In order for container to be default constructed, its member sc must be as well, but default construction is disabled for StructCon. To fix it, initialize sc explicitly:

StructCon sc = StructCon.init;

A struct in D can have a special constructor called the postblit constructor. On the surface, it looks much like a C++ copy constructor, but it isn't. When a struct instance is copied in D, a bitwise copy, or blit, is performed. If the type has a postblit constructor, it is invoked after the copy is complete. They can be used to fix up anything that needs fixing up after the copy. The following example is a good use case:

struct PostBlit {
  int[] foo;
}
void printPtr(PostBlit cpb) {
  writeln("Inside: ", cpb.foo.ptr);
}
void main() {
  auto pb = PostBlit([10,20,30]);
  writeln("Outside: ", pb.foo.ptr);
  printPtr(pb);
}

When pb is passed into printPtr, a copy is made, meaning the array member foo is copied as well. Recall that, when arrays are passed around, only the metadata, and not the elements, gets copied. As such, the two writeln calls in this example print the same address. Pass pb into a function that operates on the array and it will be subject to the same array issues discussed in the previous chapter. To prevent the original elements from being accessed, a deep copy of the array is needed. Using the postblit constructor:

this(this) { foo = foo.dup; }

Add this to the PostBlit declaration and two different addresses will be printed. Remember, the struct bits have already been copied by the time the postblit constructor is run, so duping the slice creates a deep copy of the original array. By assigning the new slice to foo, cbp inside printPtr is fixed up with a completely separate array from the original.

Let's try one more thing. Replace the postblit constructor with the following line:

@disable this(this);

Recompiling produces an error. Disabling the postblit constructor completely prevents any instance of PostBlit from being copied.

Tip

Disable anything

Not only constructors, but any function can be annotated with @disable, be they free functions or members of a class or struct. You can even apply it to destructors, though good luck with compiling your program if you do. @disable is most often used to prevent default construction and copying of struct types.

Now that you've seen so much that likely appears foreign to you, it may be a relief to know that struct destructors behave more like they do in C++:

struct Destruct {
  ~this() { writeln("Destructed!"); }
}
void doSomething() {
  writeln("Initializing a Destruct");
  Destruct d;
  writeln("Leaving the function");
}
void main() {
  doSomething();
  writeln("Leaving main");
}

Compiling and running this example should demonstrate that the destructor is run on the Destruct instance as soon as doSomething exits. In this case, a struct destructor is both reliable (it will always be called) and deterministic—if you declare multiple struct instances in any scope, they will be destroyed in the reverse order of declaration when the scope is exited.

Static constructors and destructors

Consider the following example:

module classvar;
class A {}
A anInstance = new A;

Adding an empty main function to this module and compiling produces this error:

classvar.d(3): Error: variable classvar.anInstance is mutable. Only const or immutable class thread local variables are allowed, not classvar.A

As anInstance is mutable and thread-local, we can't initialize it with a runtime value (which is exactly what we get from the new expression). Were it declared immutable, const, shared or __gshared (more on these later), it would compile, but it can be made to compile without them if the assignment of new A is moved to a static constructor:

A anInstance;
static this() {
  anInstance = new A;
}

When the program is executed, DRuntime will call the static constructor before main is entered. Let's do a little experiment. Create two modules: stat1.d and stat2.d. Here's the implementation of stat1:

module stat1;
static this() { writeln("stat1 constructor"); }

stat2.d should look the same (with the name changed, of course). Now create a module, let's call it statmain.d, consisting solely of an empty main function. Compile it like so:

dmd statmain.d stat1.d stat2.d

When you run the executable, you should see that stat1 constructor is printed first, followed by stat2 constructor. Now let's change the compilation order:

dmd statmain.d stat2.d stat1.d

Running this will show you that the order of the output has also been reversed.

Although neither stat1 nor stat2 was imported anywhere, their constructors are still run. In this case, the order of execution is not specified and is implementation-dependent. Now add the following line to the top of stat1.d:

import stat2;

Compile again, using both of the above command lines. This time, you'll see that the stat2 constructor is executed first in both cases. This is because the language guarantees that the static constructors of any imported modules will be executed before the static constructor, if any, of the importing module. Since stat1 imports stat2, then the latter's constructor is always executed before that of stat1.

Static destructors are always executed in the reverse of the order in which the static constructors were called. Let's add destructors to both stat1 and stat2:

static ~this() { writeln("stat1 destructor"); }

When stat1 imports stat2, the destructor for stat1 is always run first. Remove the import and the destructor execution order is always the opposite of the constructor execution order, whatever that may be.

Static constructors and destructors can also be declared in class or struct scope:

class Stat {
  private static Stat anInstance;
  static this() {
    writeln("Stat constructor");
    anInstance = new Stat;
  }
  static ~this() { writeln("Stat destructor") }
}

Add this to stat1 and you'll find the previous order of execution depends on the order of declaration: multiple static constructors in a module are executed in lexical order, with destructors executed in the reverse order. The reason to use a static class or struct constructor is the same as that for using module constructors: to initialize variables that can't be initialized at compile time. Since private variables are visible in the entire module, a module constructor can do the job as well:

static this() { Stat.anInstance = new Stat; }

Static constructors and destructors are always executed once per thread. We'll skip the details for now but, to guarantee that a static constructor or destructor is executed only once for the lifetime of the program, use the shared storage class:

shared static this() { } 

All static constructors marked shared are executed before those that aren't.

Inheritance and polymorphism

In D, inheritance is only available for class types. It looks much like Java's: multiple inheritance is prohibited and there is no such thing as public or private inheritance. All classes inherit from a DRuntime class called Object. Take this empty class declaration:

class Simpleton {}

One of the member functions in Object is the toString function. You can invoke it manually when you need it, but any time a class instance is passed to one of the write or format functions, toString will be invoked inside it. Try the following:

module inherit;
import std.stdio;
class Simpleton {}
void main() {
  writeln(new Simpleton);
}

This will print inherit.Simpleton. The Object.toString implementation always prints the fully qualified name of the class. By default, class member functions in D are virtual, meaning that they can be overridden by subclasses. Let's make a class that overrides the toString method to print a message:

class Babbler {
  override string toString() {
    return "De do do do. De da da da.";
  }
}

Instantiate a Babbler instead of a Simpleton in the example and it prints the message from Babbler.toString. The override keyword must be applied to any member function that overrides a super class function. To call the super class implementation, prefix super to the function name:

override string toString() {
  import std.format : format;
  return format("%s: %s", super.toString(), 
  "De do do do. De da da da.");
}

Tip

Structs and the Object functions

Although a struct cannot be extended and does not descend from Object, it is still possible to implement some of the Object functions for the runtime and library to make use of. For example, if you add a toString function to a struct declaration, it will work with writeln, the same as it does for classes. override isn't used here, since there's no inheritance.

Let's add a new function to generate a message and have toString call that instead:

protected string genMessage() {
  return " De do do do. De da da da.";
}
override string toString() {
  import std.format : format;
  return format("%s says: %s", super.toString(), genMessage());
}

Notice how genMessage is protected. This makes the function accessible only to subclasses of Babbler. Let's extend Babbler and override genMessage:

class Elocutioner : Babbler {
  protected override string genMessage() {
    return super.genMessage() ~
      " That's all I want to say to you.";
  }
}

Now you can take an Elocutioner and use it anywhere a Babbler is expected, such as an argument to a function that takes a Babbler parameter, or in an array of Babbler. OOP programmers will know this as polymorphism:

void babble(Babbler babbler) {
  writeln(babbler);
}
void main() {
  babble(new Elocutioner);
}

Only public and protected functions can be overridden. Although private and package functions are still accessible to subclasses declared in the same module and package respectively, they are implicitly final. A member function explicitly declared final, no matter its protection level, cannot be overridden by subclasses, though it can still be overloaded. Adding final to a class declaration prevents the class from being extended.

One point that often bites new D programmers is that overriding a function hides all overloads of the overridden function in the super class. An example:

class Base {
  void print(int i) {
    writeln(i);
  }
  void print(double d) {
    writeln(d);
  }
}
class Sub : Base {
  override void print(int i) {
    super.print(i * 10);
  }
}
void main() {
  auto s = new Sub;
  s.print(2.0);
}

This produces a compiler error saying that Sub.print is not callable using double. To fix it, add an alias inside Sub:

class Sub : Base {
  alias print = super.print;
  override void print(int i) {
    super.print(i * 10);
  }
}

Note that the name of the super class, in this case Base, can be substituted for super.

Tip

Calling functions outside the class namespace

Imagine a class or struct with a member function named writeln. Inside any other functions in the class scope (or that of its subclasses) a call to writeln will always call the member function, and not the function in std.stdio. To break out of the class namespace, prepend a dot to the function call: .writeln.

D also supports abstract classes, as in the following two declarations:

abstract class Abstract1{}
class Abstract2 {
  abstract void abstractFunc();
}

Abstract1 is explicitly declared abstract, whereas Abstract2 is implicitly so, since it has at least one abstract member function. Neither class can be instantiated directly. Further, any class that extends Abstract2 must either provide an implementation for abstractFunc or itself be declared abstract:

class Subclass : Abstract2 {
  override void abstractFunc() { writeln("Hello"); }
}

It's always possible to upcast an instance of a subclass to a super class. For example:

auto eloc = new Elocutioner;
Babbler babb = eloc;

However, it's not always possible to go in the other direction, or downcast. Given an instance of Babbler, explicitly casting it to an Elocutioner will succeed only if the original instance was an Elocutioner. If it was created directly as a Babbler, or perhaps another subclass called Orator, the cast will fail. In that case, the result is null:

if(cast(Elocutioner)babb) {
  writeln("It's an elocutioner!");
}

Interfaces

So far, we've been talking about implementation inheritance. D also supports interface inheritance. Again, this only works for classes. As in Java, an interface in D is declared with the interface keyword and can contain member functions that have no implementation. Any class that implements an interface must implement each function or declare them abstract. In this case, the override keyword is not needed. Interfaces cannot be instantiated directly:

interface Greeter {
  void greet();
}
class EnglishGreeter : Greeter {
  void greet() { writeln(this); }
  override string toString() { return "Hello"; }
}

An instance of EnglishGreeter can be passed anywhere a Greeter is wanted. Be careful, though:

void giveGreeting(Greeter greeter) {
  greeter.greet();
  writeln(greeter);   // Error!
}

Remember that, when given a class instance, writeln calls its toString function. While EnglishGreeter does have a toString overriding the one that it inherited from Object, inside giveGreeting the instance is being viewed as a Greeter, not an EnglishGreeter. In D, interfaces do not inherit from Object, so there is no toString function for the writlen to call.

An interface can have static member variables and functions (the functions must have an implementation). These behave exactly as they do in a class or a struct. They can be accessed using the interface name as a namespace and the functions cannot be overridden. An interface can also have implementations of per-instance member functions, but these must be marked with final and cannot be overridden:

interface Boomer {
    final string goBoom() { return "Boom!"; }
}
class BoomerImp : Boomer {
    override string toString() { return goBoom(); }
}

A single class can implement multiple interfaces. Additionally, extending any class that implements any interface causes the subclass to inherit the interfaces, for example subclasses of EnglishGreeter are also Greeters.

Fake inheritance

One interesting feature of D is called alias this. Although it works with classes, the biggest benefit is to be seen with struct types, since they can't use inheritance:

struct PrintOps {
  void print(double arg) { writeln(arg); }
}
struct MathOps {
  PrintOps printer;
  alias printer this;
  double add(double a, double b) { return a + b; }
}

Notice the syntax of the highlighted line. It differs from the standard alias syntax. This indicates that, when a function is called on MathOps that is not part of the MathOps declaration, the compiler should try to call it on the printer member instead:

MathOps ops;
auto val = ops.add(1.0, 2.0); // Calls Mathops.add
ops.print(val);                // Calls Printer.print

Here, the call to ops.print in the highlighted line is the same as calling ops.printer.print. Obviously this isn't exactly the same thing as inheritance, but it's a simple way to reuse code. It's also handy when working with template functions. As I write, the language only allows one alias this per struct or class declaration, but multiple alias this may be supported in the future.

Nested classes

It's possible to nest one class declaration inside another:

class Outer {
  private int _x;
  this(int x) { _x = x; }
  Inner createInner() { return new Inner; }
  override string toString() { return "Outer"; }
  class Inner {
    override string toString() {
      writeln(_x);
      return this.outer.toString ~ ".Inner";
    }
  }
}

The first highlighted line shows that an instance of an inner class can be allocated from inside an outer class. The bottom two highlighted lines demonstrate that Inner has access to the members of Outer. As Inner has no member named _x, it can access the _x member of Outer directly. However, both classes have a toString method, so we have to use this.outer.toString to call outer's implementation. This is like using super to call a super class function from a subclass.

Since the declaration of Inner is public, the createInner function isn't needed to get a new Inner instance; it can be allocated directly. An instance of Outer is needed to do so:

auto outer = new Outer(1);
auto inner1 = outer.new Inner;  // OK
auto inner2 = new Outer.Inner;  // Error -- need Outer 'this'
auto inner2 = new outer.Inner;  // Error -- same as above

By prefixing the name of the outer class instance variable to new, that instance is associated with the inner class instance.

When a nested class is declared as static, an instance of the outer class is no longer needed. However, a static nested class no longer has an associated outer class instance, which means that there is no .outer property. Change the declaration of Inner to be static and it can no longer access the _x and toString members of Outer. In other words, a static inner class is just a normal class with an additional namespace:

class Outer2 {
  static class StaticInner {}
}

To get an instance of StaticInner:

auto inner = new Outer2.StaticInner;

Structs can also be nested, but in this case the type name of the outer struct simply becomes an additional namespace for the inner one, for example Outer.Inner oi;. The inner struct is just like any other struct, with no .outer property.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.246.223