Chapter 2. D Fundamentals

A movie director once made the observation that Charlie Sheen and Emilio Estevez both resemble their father, Martin Sheen, but look nothing like each other. Something similar might be said of programming languages that belong to the C family. They all share certain features of C, but beyond that, they have several differences. For the most part, the differences are to be found in expanded features, such as inherent support for object-oriented or generic programming, automatic memory management, or features intended to make programs more secure and robust. But at the core of each of these languages is a set of features that vary little from one language to the next. D, too, follows this pattern, but does so in a way that makes it stand out from the rest.

As you'll see in later chapters, some of D's more advanced features improve on ideas already implemented in other modern languages derived from C; some were inspired by languages outside the family; and a few are not to be found in any other mainstream programming language. While these features all contribute to D's unique identity, many users are first drawn to the language by the core feature set. In this chapter, we'll look at these core features.

We'll start out with declarations before moving on to the basic types. Next, we'll look at the different kinds of arrays and array operations. Then we'll get into flow-control constructs. Finally, we'll discuss functions and error handling.

Declarations

When declaring variables in D, the syntax varies depending on the type of the variable. When we discuss basic types, arrays, and pointers, we'll look at the syntax for variable declarations of each. Before we get that far though, it is helpful to understand some general rules about declarations in D.

Declaration Syntax and Variable Initialization

D is a statically typed language, which means that the type of a variable must be known at compile time. Therefore, D variable declarations usually require the type to be a part of the declaration. We say "usually," because there is one exception to this rule, which we'll get to in a moment. Declarations read from right to left and must be terminated by a semicolon, as in the following examples:

int x;
int y = 1;
char[] myString = "Hello";
float[5] fiveFloats;
long* pointer = null;

This code declares one variable x of type int that is not explicitly initialized, and another variable y of type int that is explicitly initialized to 1. The variable myString is an example of declaring an initialized, dynamic array. All strings in D are arrays of one of three character types. fiveFloats is a static array, which is not explicitly initialized in this example. Finally, the pointer variable is an example of a pointer declaration in D.

Notice that we said that the variables x and fiveFloats are "not explicitly initialized," rather than "uninitialized." This is because no D variable is ever left uninitialized at the point of declaration. If you do not explicitly initialize the variable to some value, it will automatically be initialized to a specific default value by the compiler. The value used for initialization depends on the variable's type, but it is guaranteed to be the same for all variables of the same type. This is a very useful feature for debugging. You'll see the different default initialization values when we examine each type.

Caution

Automatic variable initialization is intended to catch uninitialized variables, a common source of bugs. However, don't consider it an opportunity to avoid initializing variables yourself. As you'll see when we discuss floating-point numbers, it is not a good idea get into the habit of relying on automatic variable initialization to do your job for you.

Another important part of a variable declaration is the name, or identifier, used to represent the variable. When creating any identifierwhether it is the name of a variable, function, class, struct, or whateveryou need to keep a few rules in mind:

  • Identifiers can begin with a letter, an underscore (_), or a universal alpha character.

  • The first character can be followed by any number of letters or universal alpha characters.

  • You can use as many underscores in the identifier as you like, as long as you don't use them for both the first and second characters. Identifiers beginning with two underscores are reserved for use by the compiler.

  • Identifiers are case-sensitive, so x and X are not the same.

Note

Universal alphas are characters from several different languages. They are defined, using hexadecimal codes, in Appendix D of the C99 standard as being legal for use in C identifiers. Because D is derived from C, it accepts the same characters in identifier names.

An optional part of variable declaration is the storage class. The storage class of a construct determines when it is allocated, where it is stored, how long it lives, how it is accessed, and, in some cases, how the compiler views it. D reserves several keywords for indicating the storage class of different language constructs. In this chapter, we are concerned with only those that affect variables and functions, as using a storage class alters the syntax of a declaration.

A storage class commonly used with individual variables is const. This tells the compiler that a given variable is to be treated as a constant expression, meaning that its value should not change during runtime. Another commonly used storage class is extern, which indicates that a variable is initialized outside the current binary. This is frequently used when creating D modules that interact with C libraries.

When using a storage class in a variable declaration, it must precede the type:

const int x = 1;

However, in some cases, the type can be omitted:

const y = 1;

Here, the type of y is omitted. This form uses a feature of D called automatic type inference. As long as a declaration contains a storage class, the type can be omitted, and the compiler will infer it automatically. Because a storage class is intended to affect the variable in some way, D provides a special storage class, auto, for those cases where you want to use automatic type inference but don't want any storage class side effects. In other words, auto does not affect the variable in any way at all and indicates only that type inference is to be used. Using auto together with the type in the declaration is not an error, but has no meaning. Here's an example of using the auto storage class:

auto x = 1;        // The type will automatically be inferred as int.
auto int y = 1;    // auto has no effect here, since the type is specified.

There's quite a bit more to say about declarations. We'll get to the specifics for various constructs as the chapter progresses. First, we need to lay some more groundwork and talk about D's scoping rules.

Declarations and Scope

The term scope describes the context in which a particular declaration resides. Scope affects variable declarations in two ways:

  • It determines when and how you can initialize your variables.

  • Because scope controls which variables are visible, it also affects how you can name your identifiers.

In this chapter, we are concerned with two basic types of scope: module scope and block scope. When you create a new D source file, you are working in module scope by default. You usually create a new block scope with each matching pair of curly braces you add to the file.

Note

Module scope is also referred to as global scope. Block scope is often called local scope. D also has a special scope that is unique to classes and structs, generally referred to as class scope. You'll learn about classes and structs in Chapter 3.

The following example shows module scope and global scope.

// This is module scope. Here, we declare x and initialize it with a constant
// expression.
int x = 1;

void main()
{   // A new block scope starts here--a child of the module scope.
// y is declared inside main's block scope, meaning it is local to main.
    // It can see x, but x can't see it.
    int y = x;

    if(1 < 2)
    {   // A new block scope starts here--a child of main's scope.

        // Because x is visible in main's scope, it is also visible here. And
        // because main's scope is this scope's parent, y is visible, too.
        // However, z is visible neither in main's scope nor in the module
        // scope.
        int z = x + y;

    }   // The end of the if block scope

}   // The end of main's block scope

void someFunc()
{   // A new block scope starts here--a child of the module scope and a
    // sibling of main's scope.

    // This y is declared inside someFunc's scope. It can see x, but x can't
    // see it. Also, neither it nor the y in main's scope are visible to each
    // other.
    int y = x;

}   // The end of someFunc's block scope

Note

The example of using module and block scope employs some features that we haven't yet discussed, such as functions. For now, you just need to focus on the meaning of scope demonstrated by the code. You'll learn about the other features later in this chapter and in upcoming chapters.

In this example, the variable x is declared outside any curly braces. This indicates that it is in module scope. The curly braces in the main function introduce a new block scope. It is in this scope that a variable y resides. Similarly, the function someFunc creates a new block scope with its own y variable.

The code comments in the listing explain scope visibility. Essentially, children can see identifiers that are visible in, or declared in, their parent, but parents can never see identifiers declared in their children. Neither can siblings see each other's identifiers.

As noted, both main and someFunc declare a variable y. They can do this since neither scope is visible to the other. However, neither main nor someFunc could declare a variable x, since x is already visible in both scopes. Identifier names must be unique within the scope in which they are declared. If you create a new identifier using a name that is already visible in that scope, the compiler will fail with an error.

This example also explicitly initializes the variables it declares. Within a block scope, there aren't any restrictions on how you explicitly initialize a variable. However, in the module scope, you can initialize only variables with constant expressions. It is illegal to use a nonconstant expression to explicitly initialize a variable at module scope. Doing so will result in a compiler error. The following shows examples of legal and illegal module scope variable initialization.

int x = 1;        // OK: 1 is a constant expression.
int y = x;        // Error: x is not a constant expression.
int z = 1 + 1;    // OK: 1 + 1 is a constant expression.
int a = 1 + x;    // Error: 1 is constant, x is not, so 1 + x is a
                  // nonconstant expression.

Because both y and a depend on x, which is a nonconstant expression, neither can be explicitly initialized at module scope. Instead, they should be assigned their values elsewhere in the program in a block scope, such as a function. Assignments, other than those made at the point of declaration, are illegal at module scope. Again, none of these restrictions apply to block scope.

Tip

One way to "fake" initialization with nonconstant expressions at the module scope is to make the declaration as normal, without explicitly initializing it, and then assign the variable its value inside a static module constructor. A module constructor is a unique D feature that is quite handy for this purpose. You will learn about module constructors in Chapter 4.

For the basic types, which we'll discuss next, the declaration syntax doesn't change from what we've looked at so far. Things do change a bit for pointers, arrays, and functions, as you'll see in the sections about those constructs.

Basic Types

D supports the same basic types as other languages in the C family, but goes beyond those languages with its own unique twists. For example, D has three character types, nine floating-point types, and a reserved 128-bit integral type that currently doesn't exist in other C languages. All of these are a core part of the language and not specially defined types in a library. In practice, most programmers do not need to be concerned with every data type D supports, but some programmers will put them all to good use. For example, numerical or scientific application programmers will appreciate the variety of floating-point types available.

Something that nearly all of D's basic data types have in common is that the specification explicitly defines the bit size of each. One of the primary benefits of such an approach is that data of a given type will remain the same size across platforms. But there are times when a fixed-size type is not the best option, and you may require a type that is best suited for the current platform. Types with sizes that vary across CPU architectures are available as part of Tango's C modules (which are briefly described in Chapter 8). In fact, the C type size_t is available automatically, without the need to import any additional modules.

In this section, we'll look at integral types, floating-point types, and character types. Most programming books categorize characters as integers. However, characters in D are quite different from other integers and deserve their own section. Before we discuss any of the data types, however, we should first talk about properties.

Properties

All data types in D expose certain properties that can be queried for specific information about the type or, in some cases, can be used to perform a specific operation on a type instance. For example, all types have the sizeof property, which tells the size, in bytes, of a given type. Arrays have a special sort property, which can be used to sort the contents of the array in place. Properties can be queried by using dot notation with the name of the type, as in int.sizeof.

One potentially confusing point about properties is that they can also be accessed through a variable, or instance, of a certain type. If you have a variable x declared as type int, you can query its sizeof property in the same manner: x.sizeof. Keep in mind that, depending on the property, the result you get by querying through the type may not be the same result you get when querying through the variable. Furthermore, some default properties, such as array's sort, work on only type instances and not on the types at all. In the majority of cases, however, the default type properties and instance properties are the same.

Table 2-1 lists properties that are common to all data types and their instances.

Table 2.1. Properties Common to All Data Types and Their Instances

Property

Description

init

The default initialization value

sizeof

The size in bytes

alignof

The byte boundary upon which the type or instance is aligned

mangleof

A string representing the "mangled" name

stringof

A string representing the name of the type or instance as it appears in source code

Note

When compilers parse source code files, they usually convert the names of functions, variables, and other entities into an internal format that incorporates information about the type or signature of the entity. This form is called the mangled name.

The following code queries each of these properties for the type int and for an instance of that type.

import tango.io.Stdout;

void main()
{
    Stdout.formatln("int.init is {}", int.init);
    Stdout.formatln("int.sizeof is {}", int.sizeof);
    Stdout.formatln("int.alignof is {}", int.alignof);
    Stdout.formatln("int.mangleof is '{}'", int.mangleof);
    Stdout.formatln("int.stringof is '{}'", int.stringof);

    int x;
    Stdout.formatln("x.init is {}", x.init);
    Stdout.formatln("x.sizeof is {}", x.sizeof);
    Stdout.formatln("x.alignof is {}", x.alignof);
    Stdout.formatln("x.mangleof is '{}'", x.mangleof);
    Stdout.formatln("x.stringof is '{}'", x.stringof);
}

The result of compiling and executing this code is as follows:

int.init is 0
int.sizeof is 4
int.alignof is 4
int.mangleof is 'i'
int.stringof is 'int'
x.init is 0
x.sizeof is 4
x.alignof is 4
x.mangleof is 'i'
x.stringof is 'x'

As you can see from this example, the only property that shows different results for the type and instance queries is stringof, which is inherently different from instance to instance due to what it represents. You may encounter other properties that differ between a type and an instance. So before you use a particular property, make sure you understand what it represents so that you can make the appropriate query.

Integral Types

Integral types are types that represent integer values. Nearly all integral types in D come in two flavors:

  • Signed types can represent both positive and negative numbers.

  • Unsigned types can represent only positive numbers. They have the same name as the corresponding signed type, but with a u prefix.

Table 2-2 shows each integral type, its size in both bits and bytes, and the minimum and maximum values it can represent.

Table 2.2. Integral Types

Type

Size (Bits)

Size (Bytes)

Min

Max

byte

8

1

128

127

ubyte

8

1

0

255

short

16

2

32768

32767

ushort

16

2

0

65535

int

32

4

2147483648

2147483647

uint

32

4

0

4294967295

long

64

8

9223372036854775808

9223372936854775807

ulong

64

8

0

18446744073709551615

The bool data type is not listed in Table 2-2 because it has only two possible values and is neither signed nor unsigned. A bool is 1 byte in size and can be either true or false. It can be converted to any integral type, at which time true will be converted to 1 and false to 0. The default value of a bool is false.

Also missing from Table 2-2 are cent and ucent, both of which are intended to be 128 bits, or 16 bytes, in size. Currently, neither is implemented. However, the keywords cent and ucent are reserved for future use.

With the exception of long and ulong, the default initialization value of all integral types is 0. In a perfect world, the default would be an invalid value. Unfortunately, in all of the range of values that an integral type can hold, none of them are inherently invalid. So, we have to settle for 0.

In D, all constant integer values are of type int. D supports a special modifier, L, that can be appended to an integer constant to make it type long instead of int. The initialization value of both long and ulong is 0L.

Note

The distinction may seem minor, but internally there is a big difference between 0 and 0L. The former is a 32-bit value and is treated as an int, whereas the latter is a 64-bit value and is treated as a long. Appending L to any constant values you assign to long variables is a good habit.

In addition to the properties listed in Table 2-1, integral types expose the two properties shown in Table 2-3. The values these properties return are listed in the Min and Max columns of Table 2-2.

Table 2.3. Properties Specific to Integral Types

Property

Description

min

Minimum value this type can represent

max

Maximum value this type can represent

Floating-Point Types

In layman's terms, a floating-point type is any type whose values contain a decimal point. In most programming languages, the built-in floating-point types are used to represent what mathematicians call a real number. A real number is one that includes the numbers between the integers. For example, the number 1.011 can represent a real number in mathematical terms and is a floating-point value in programming terms.

Although D supports nine floating-point types, the average user of D will probably be interested in only two of the three basic floating-point types: float and double. These two types, and one more called real, represent real numbers in the mathematical sense. But D goes beyond built-in support for real numbers. It offers three floating-point types that represent imaginary numbers and three more that represent complex numbers. If you don't know the difference between real, imaginary, and complex numbers, you'll likely have no use for these other types.

After reading about the integral types in the previous section, you might think that the default initializer for floating-point types is 0.0. Many newcomers to D incorrectly make that assumption. The default initializer for floating-point types is actually a form of a special value called Not a Number (NaN), which helps to detect errors caused by uninitialized variables. Each floating-point type has a nan property, which is used to initialize it. Outside initialization, if you find yourself with a floating-point value equivalent to NaN after a calculation, you can be fairly certain that you have a programming error.

Table 2-4 lists all of D's floating-point types, their size in bits and bytes, and the default initializer as defined in the language specification.

Table 2.4. Floating-Point Types

Type

Size (Bits)

Size (Bytes)

Initializer

float

32

4

float.nan

double

64

8

double.nan

real

Platform dependent

Platform dependent

real.nan

ifloat

32

4

float.nan * 1.0i

idouble

32

8

double.nan * 1.0i

ireal

Platform dependent

Platform dependent

real.nan * 1.0i

cfloat

64

8

float.nan * float.nan * 1.0i

cdouble

128

16

double.nan * double.nan * 1.0i

creal

Platform dependent

Platform dependent

real.nan * real.nan * 1.0i

The types float and double are the standard fare found in any C-family programming language. The real type is defined to be the largest size on the hardware used to compile the application. For example, on x86 platforms, a real is 80 bits (10 bytes) in size. The types prefixed with an i represent imaginary numbers. The types prefixed with a c represent complex numbers.

In addition to the properties listed Table 2-1, floating-point types expose all of the properties shown in Table 2-5. The average programmer will likely be concerned with only the nan, max, and min properties.

Table 2.5. Properties Specific to Floating-Point Types

Property

Description

infinity

Value that is too large to represent

nan

Value used to represent NAN

dig

Number of decimal digits of precision

epsilon

Smallest increment to the value 1

mant_dig

Number of bits in the mantissa

max_10_exp

Maximum power of 10 exponent this type can represent

max_exp

Maximum power of 2 exponent this type can represent

min_10_exp

Minimum power of 10 exponent this type can represent as a normalized value

min_exp

Minimum power of 2 exponent this type can represent as a normalized value

max

Largest value this type can represent that is not infinity

min

Smallest normalized value this type can represent that is not 0

re

Real part of the number

im

Imaginary part of the number

Tip

If you find some of the terminology used in this section puzzling, you might want to visit the Wikipedia page about floating-point numbers (http://en.wikipedia.org/wiki/Floating_point). There, you can learn what a mantissa is, what normalization means, and a great deal more. At the very least, it should give you a better understanding of what some of the floating-point properties represent.

Another important thing to mention here is that the re and im properties are instance properties, rather than type properties. If you try to access either of them through a type, such as float.re, you will be confronted with a compiler error. This is because, by definition, both properties require a number to exist. There's no such thing as the real or imaginary part of the type float, but these parts do exist in the number 1.11. So assuming that you have a variable of type float named f, you can access both properties through it: f.im.

You can read more about D's floating-point types, including the rationale behind including complex and imaginary types in the language, at http://www.digitalmars.com/d/1.0/float.html.

Character Types

Whereas most languages in the C family support only one character type, D supports three. What makes D's character types different from integrals is that they don't necessarily represent integer values. More accurately, each type is intended to represent a Unicode code point. Several different Unicode encodings exist. D's character types provide built-in support for the three most common encodings.

Another big difference between integrals and characters is the default initializer. Remember that the goal of automatic initialization is to aid in debugging. Ideally, all built-in data types would be initialized to an invalid value. Integrals do not have invalid values. Floating-point values do. Since D's character types represent Unicode code points, they also have values that are invalid. Specifically, certain values are not valid Unicode. D uses three such values as the default initializer for each character type.

Table 2-6 lists each character type, its size in both bits and bytes, the Unicode encoding it represents, its minimum and maximum values, and it default initializer.

Table 2.6. Character Types

Type

Size (Bits)

Size (Bytes)

Unicode Encoding

Min

Max

Initializer

char

8

1

UTF-8

0

255

0xFF

wchar

16

2

UTF-16

0

65535

0xFFFF

dchar

32

4

UTF-32

0

1114111

0x0000FFFF

Individual characters can be used anywhere an integral can. Each character type also exposes the same properties that integrals do, as listed in Table 2-3.

When we look at strings later in this chapter (in the "Strings" section), you'll see that they are sequences of characters. D programmers generally work with strings more frequently than with individual characters, but character types come in handy when you're modifying strings or searching them to find a specific character. Character values can be assigned any integer value that is valid for an integral of the same bit size, such as char c = 10, or can be assigned a single-quoted letter, such as char c = 'a'.

That wraps up the three basic data types. Now it's time to take a quick look at pointers, and then move on to arrays.

Pointers

In short, a pointer is a variable that represents a memory address rather than actual program data. Often, the address "points" to program data, such as the beginning of a series of integers or other data type. We mention pointers here because those with experience in C or C++ need to know that pointers work a bit differently in the world of D.

First, pointer declarations are subtly different in D than in C. Consider the following line of code:

int *x, y;

This code is valid in both the C and D languages, but has different results in each. In C, x is a pointer to int, and y is an int, not a pointer. In D, both x and y are pointers to int. This is a subtle, but very important, distinction.

When you use D's typeof operator on an int, the type returned is int. But use the typeof operator on a pointer to int, and the type returned is int*. Because of this, it is more common for D programmers to move the asterisk over to the left, transforming the preceding declaration into the following:

int* x, y;

By using this syntax, it is clear that you are declaring two int pointers. It is good practice to follow this convention for all pointer declarations.

Another thing to know about D pointers is that there is no -> operator. Struct and class pointers are manipulated using dot notation. However, you can still use the syntax *x to dereference the pointer.

Note

If you have no idea what pointers are, a good place to start learning about them and some of the terminology used in this section is the Wikipedia page about pointers (http://en.wikipedia.org/wiki/Pointer). Not only does this page discuss pointers in general terms, but it also gives a little background about pointer usage in different languages.

Finally, because D has built-in garbage collection, you need to adhere to a few restrictions when using pointers that point to garbage-collected memory. Most of these restrictions relate to a number of pointer tricks that C programmers have implemented over the years, such as using the low-order bits of a pointer to store extra data. In a nutshell, don't do anything that depends on the address of the pointer to stay the same. Pointers that point to memory not managed by the garbage collector are free from these restrictions. For details, see http://www.digitalmars.com/d/1.0/garbage.html.

Arrays

An array is a sequence of data, usually of the same type, that can vary in length and is stored in a contiguous block of memory. D supports four types of arrays as part of the core language: static arrays, dynamic arrays, strings, and associative arrays. We'll look at each in turn. We'll also discuss array operations. But first, let's examine some things that all arrays have in common.

Conceptually, you can think of a D array as an item that is made up of two components: a pointer and a length. The pointer points to the memory address that contains the first element in the array, and the length represents the number of elements in the array. Both the length and the pointer are accessible as properties. Because each array knows how many elements it contains, it is possible for the compiler to do automatic bounds checking (DMD does this by default, but it can be turned off by passing -release on the command line).

D supports the same syntax as C for array declarations, called postfix syntax, but only for backward compatibility.

// C-style, or postfix, declarations
int x[3];        // Declares an array of 3 ints
int x[3][5];     // Declares 3 arrays of 5 ints
int (*x[5])[3];  // Declares an array of 5 pointers to arrays of 3 ints

The preferred syntax is called prefix syntax.

// D-style, or prefix, declarations
int[3] x;        // Declares an array of 3 ints
int[3][5] x;     // Declares 5 arrays of 3 ints
int[3]*[5] x;    // Declares 5 pointers to arrays of 3 ints

The syntactical differences between the two styles are obvious, but prefix multidimensional array declarations can be confusing to those with a C background. You'll notice that the order is reversed from the postfix declarations. However, indexing values from the multidimensional arrays is done in the same way, no matter how they are declared, as in the following example.

int x[5][3];    // Postfix declaration of 5 arrays of 3 ints
int[3][5] y;    // Prefix declaration of 5 arrays of 3 ints

x[0][2] = 1;    // The third element of the first array is set to 1.
y[0][2] = 2;    // the third element of the first array is set to 2.

As you can see, postfix and prefix come into play only in array declarations. You index them both in the same way.

Static Arrays

Static arrays are the simplest to understand; in fact, all of the arrays declared in the previous examples are static. These are arrays that have a fixed length that is established at compile time. The length of a static array can never change during the course of program execution. Static arrays are allocated on the stack. Table 2-7 lists the properties that are unique to static arrays (remember that these are in addition to the properties listed in Table 2-1, which are common to all data types).

Table 2.7. Properties Specific to Static Arrays

Property

Description

length

Number of elements in the array; cannot be modified

ptr

Pointer to the first element in the array

dup

Creates and returns a dynamic array that is an exact duplicate of the array

reverse

Reverses the array in place and returns the array

sort

Sorts the array in place and returns the array

Unlike most of the properties that you have seen so far, each of those listed in Table 2-7 is exclusively an instance property, and three of the static array properties have side effects. The dup property will allocate enough memory to hold an exact duplicate of the array. The reverse and sort properties will change the order of items in the array. While these are very convenient properties to use, be aware that they could have performance implications when used on large arrays or in performance-critical code.

Note

The sizeof property of a static array (see Table 2-1) returns the length of the array multiplied by the number of bytes per array element. This means that the result varies based on the type and number of elements in the array.

Dynamic Arrays

Unlike with static arrays, the length of a dynamic array does not need to be known at compile time. Dynamic arrays are allocated on the heap. Because the length is not fixed, a dynamic array can be resized as needed. Up to now, all of the properties you have seen have been read-only. The length property of dynamic arrays is actually writable. To resize the array, simply set its length property to the new size.

You make an array dynamic by declaring it without any numbers to indicate the size of the array. Instead, you use empty brackets, as follows:

int x[];    // Postfix dynamic array declaration
int[] y;    // Prefix dynamic array declaration

Neither of the arrays in this example allocates any space for its elements. Both arrays are empty, meaning each has a length of 0 and a null pointer.

You can allocate space for a dynamic array in three ways:

  • Use D's new keyword.

  • Explicitly set the length property to the number of elements required.

  • Use an array literal. An array literal is a sequence of values contained within brackets, such as [0,2,5,6] or [2.0, 5.0, 3.0].

The following shows examples of these methods.

int[] x = new int[10];    // A dynamic array of 10 ints all initialized to int.init
int[] y;                  // An empty dynamic array of ints
y.length = 10;            // y can now hold 10 ints.
int[] z = [0,1,2,3,4];    // A dynamic array that holds 5 ints initialized to
                          // the values 0, 1, 2, 3, and 4
int[5] z1 = [0,1,2,3,4];  // A static array of 5 ints initialized to the values
                          // 0, 1, 2, 3, and 4
int[] a;                  // An empty dynamic array
a= new int[10];           // a can now hold 10 ints. All values are now set
                          // to int.init
a.length = 5;             // a has been resized to hold only 5 ints.
a = [0,1,2,3,4,5];        // a now has a length of 6 and contains the values
                          // 0, 1, 2, 3, 4, and 5

No matter how the space for a dynamic array is initially allocated, whether via new or its length property, it can be resized at any time. Typically, you resize an array by adjusting its length property, by appending new values to the array, by copying one array to another, or by assigning an array literal to the reference. If the new length is greater than the old length, more space is allocated to accommodate it. If the new length is less than the old length, no memory operations are performed, meaning nothing is allocated, reallocated, or freed.

In addition to the properties listed in Table 2-1, dynamic arrays expose all of the properties that static arrays do, as listed in Table 2-7. The only difference is that the length property of a dynamic array is not constant, so it is both readable and writable.

Note

The sizeof property of a dynamic array (see Table 2-1) returns the size, in bytes, of the array reference rather than the amount of memory used by the array.

Strings

In D, strings are not so much a separate array type as they are a special case of normal static and dynamic arrays. Strings are arrays that are of type char, wchar, or dchar and represent UTF-8, UTF-16, and UTF-32 sequences, respectively.

String literals can be used to initialize a new string. A string literal, frequently just referred to as a string, is a sequence of characters contained within double quotation marks, such as "Hello World". Strings initialized with a string literal are immutable, meaning they cannot be modified. Because strings are arrays, they can be static or dynamic.

Here are examples of initializing strings:

char[] s1 = "abcd";  // s1 is a dynamic array.
s1[0] = 'x';         // since s1 is immutable, this should be an error. Although
                     // the compiler does not complain, this could cause a crash.
char[4] s2 = "abcd"; // s2 is a static array.
s2[0] = 'x';         // Again, this could cause a crash since s2 was initialized
                     // with a string literal.

Another special feature of strings is that a postfix can be attached to a string literal in order to specify how it should be treated by the compiler. By default, the type of a string is determined automatically during compilation. However, you can use the postfix character c, w, or d to force a literal to be treated as an array of char, wchar, or dchar, respectively. For example, the literal "hello"w will be treated as an array of wchar because of the w postfix.

Tango provides several library functions that operate on strings. These include functions to convert between one string type and another. While it is possible to cast between string types, such as from wchar[] to dchar[] and vice versa, this is a technique that should be used cautiously. Because each character type represents a different Unicode encoding, it is possible that casting from one type to the other could have unexpected results. A safer approach is to use Tango's Unicode conversion routines instead. You'll learn about Tango's text-processing functions in Chapter 6.

There are no special properties exposed by strings, other than those exposed by static and dynamic arrays. Which properties are available depends on how the string was allocated.

Associative Arrays

Associative arrays are distinct from static and dynamic arrays. What sets them apart is that they are allowed to be indexed by types other than integers and they can be sparsely populated. Associative arrays must be declared to hold values of a certain type and to have keys of a certain type. D's associative arrays are analogous to hash maps in other languages. They are dynamic by nature and always reside on the heap. The following example shows some different associative array declarations.

int[char[]] x;    // An associative array with values of type int and keys of type
                  // char[]
float[double] y;  // An associative array with values of type float and keys of type
                  // double
char[][char[]] z; // An associative array with values of type char[] and keys of
                  // type char[]

Once an associative array has been declared, you can associate values with keys using the following syntax:

aa[key] = value;

If any value is already associated with the given key, it will be overwritten. You can retrieve values from an associative arrays using similar syntax:

value = aa[key];

If a key does not exist in the associative array, the D runtime will throw an error indicating that the array index is out of bounds. One way to avoid this is to test if the key exists using the in operator, which we'll look at shortly.

Associative arrays have a few unique properties, which are listed in Table 2-8.

Table 2.8. Properties Specific to Associative Arrays

Property

Description

length

Number of values in the associative array; as with static arrays, this is read-only

keys

Dynamic array containing all of the keys in the associative array

values

Dynamic array containing all of the values in the associative array

rehash

Reorganizes the associative array in place to make lookups more efficient and returns the new array

Because of the nature of an associative array, it doesn't make sense to be able to modify the number of key/value pairs it contains without adding and removing them, so the length property is read-only. The rehash property is an expensive operation, but can result in more efficient lookups if a large number of keys have been set.

Note

The sizeof property of an associative array (see Table 2-1) returns the size, in bytes, of the array reference, rather than the amount of memory used by the array.

In the next section, we'll look at some operations that can be performed on dynamic and static arrays. Since associative arrays have their own unique set of operations, we'll cover two handy associative array operators here:

in: This operator can be used to determine if a value has been associated with a given key in an associative array. This is very handy to avoid overwriting a key that already exists, for example. The in operator returns a pointer to the value if it exists and null if it doesn't.
remove: This function is called through an associative array much like a property, but you pass a key value as an argument (you'll learn more about functions later in this chapter, in the "Functions" section). When the remove function is called, it removes the key and its associated value from the array.

Here is an example that uses both in and remove:

int[char[]] aa;    // Declare an associative array of int values and char[] keys.
aa["One"] = 1;     // Associate the value 1 with the key "One".
int x = aa["One"]; // Retrieve the value associated with the key "One".
int *y = ("Two" in aa);    // See if the key "Two" has been set.
if(y is null)
{
    aa["Two"] = 2;         // Only set "Two" if it isn't set already.
}
aa.remove("One");  // Remove the key "One" and its associated value.

Array Operations

Both static and dynamic arrays (and, as such, strings) have certain operations that can be performed using different operators. For example, as you have already seen, the [] operator is used in declarations, as well as to set and get array values. It is also used in slicing and copying, which we will look at now, before moving on to concatenation.

Slicing

Slicing is an operation that essentially creates another view of an array. It does not copy any array data, but simply creates a new array reference that shares a portion of an existing array. In other words, it's a different view of the same data.

A slice is performed using the following syntax:

a[start .. end]

where start is the index at which the slice begins, and end is one index beyond the index where the slice should end.

If you are slicing an entire array, you can use the shorthand [], without specifying the start and end points, thereby creating an identical view of the existing array.

Here are examples of slicing:

int x[] = [0, 1, 2, 3, 4];
int y[] = x[1 .. x.length];    // y is a view of x starting from the second index
                               // and ending with the last, i.e., 1, 2, 3, 4.
int z[];
z = x[1 .. x.length 1];       // z is a view of x starting from the second index
                               // and ending with the next-to-last, i.e., 1, 2, 3.
int all = x[];                 // all is a view of all of x, from the first
                               // element to the last, i.e., 0, 1, 2, 3, 4.

In the next chapter, you'll see how to implement custom slicing operations on your own data types using operator overloading.

Tip

In a slice operation, when you use the length of the array you are slicing as one of the end points, you can substitute the call to the length property with the special $ operator. For example, x[1 .. x.length] can be rewritten as x[1 .. $].

Copying

In D, you can copy an array in three ways:

  • Manually fetch the values from an array and assign them all to another array of the same length. This is rather inefficient and should never be the first choice.

  • Use the dup property common to all static and dynamic arrays.

  • Use the slice operator ([]).

In the previous section, you learned that an array slice does not copy an array, but instead creates a new reference that shares part of the same data. However, you can use the slice operator in a copying operation by placing it on the left side of an assignment and another array on the right side. The following example demonstrates how to copy arrays using the slice operator.

int[] x = [0, 1, 2, 3, 4];
int[] y = x;                  // All 5 elements of x are copied to y.
int[] z = x[];                // All 5 elements of x are copied to z.
y[0 .. 2] = x[1 .. 3];        // Same as y[1] = x[2];.

Although the example uses only dynamic arrays, the same operations can be performed using static arrays.

Concatenation

D has a special binary operator, ~, which is used to perform array concatenation. This operator works with any array, but it is most often used with strings. You can also use the ~= operator, which effectively appends one array to another in place. Here are some examples of array concatenation:

char[] x = "Hello";
char[] y = "World";
char[] z = x ~ " " ~ y;    // z is a new string, "Hello World", while x an y are
                           // unchanged.

int[] a = [0, 1, 2, 3];
int[] b = [4, 5];
a ~= b;                    // a now contains the values 0, 1, 2, 3, 4, and 5, while
                           // b is unchanged.

As with the slice operator, custom concatenation behavior can be implemented using operator overloading, as described in the next chapter.

Flow Control

In computer programming, flow control refers to language constructs that direct the flow of program execution. D supports three basic types of flow-control constructs: conditionals, loops, and goto.

Conditionals

A conditional is a construct that branches based on a true or false condition. D supports three different types of conditionals: if-else blocks, the ternary operator, and switch-case constructs. All of these can be found in other C-family programming languages, with very few differences.

if-else Blocks

Perhaps the most commonly used conditional is the form often referred to as an if-else block. These conditionals always begin with an if statement that tests for a certain condition and will execute only if that condition evaluates to true. This statement can be followed by any number of else if statements, which test for more conditions that will execute only if they evaluate to true. Optionally, a final else statement can execute if none of the preceding conditions evaluated to true. The following are examples of if-else blocks.

bool x = true;
if(x)
{
    // This will execute because x is true.
}
else
{
    // This will not execute because x is true.
}

bool y = false;
int a = 1;
int b = 2;
if(y)
{
    // This will not execute because y is false.
}
else if(a > b)
{
    // This will not execute because a > b evaluates to false.
}
else
{
    // This will execute because both of the two preceding conditions evaluated
    // to false.
}

if-else blocks can be nested inside each other for as many levels as you have stack space available. Realistically, nested if-else blocks are quite ugly and hard to follow, so most programmers rarely go beyond two or three levels deep. If you find yourself going deeper, you probably need to rethink your design.

The Ternary Operator

Often, you don't need to perform any overly complicated action based on an if-else conditional. For example, you might need to simply assign a value to a single variable based on a specific condition. This is where the ternary operator comes in handy. If you need to perform more than one operation for each statement, however, you should stick with the if-else block.

The ternary operator consists of two characters: ? and :. The entire expression takes the following form:

condition ? true action : false action

where true action and false action are expressions that will be evaluated if condition is true or false, respectively.

The following example shows how you can replace a single assignment in an if-else block with the ternary operator.

int x;
int y = 1;
int z = 2;

// The if-else block
if(y < z)
{
x = 1;
}
else
{
    x = 0;
}

// The same code using a ternary operator

x = y < z ? 1 : 0;

The ternary operator should be used sparingly, because overuse can make code confusing and ugly.

Caution

When using a ternary operator to assign a value to an auto variable, the result may not be what you expect. Variables declared auto will take the type of the last item in the ternary expression, regardless of the type actually assigned. So if the expression auto a = b ? c : d evaluates to c, the type of a will become the type of d. If c and d are the same type, this doesn't matter, but it is something to be aware of when using multiple types in a ternary expression.

switch-case Statements

switch-case is another conditional construct that is often considered to be an ideal alternative to if-else blocks when you have several conditions to test or when you need to test for specific values rather than Boolean conditions.

You start with a switch statement that evaluates an expression. The switch statement creates a new scope in which you implement several case statements that evaluate expressions that correspond to the possible values the switch evaluated. You may also end the switch with a special default statement, which does not evaluate an expression and executes only if no case matched the result.

You need to keep in mind a few restrictions on both switch and case statements:

  • The expression evaluated by the switch must result in an integral type, including character types, or a string.

  • The expressions in each case statement must evaluate to a constant value or array.

  • The resultant value in each case statement must be implicitly convertible to the type of the evaluated result in the switch.

  • Each value in the case statements must be unique. It is illegal to have two case statements whose expressions evaluate to the same value.

The following shows the switch-case statement in action.

int x = 1;
int y;
int z;
switch(x)
{
    case 2 - 1:   // Evaluates to case 1:
        y = 2;
        break;
    case 1 + 1:   // Evaluates to case 2:
        y = 3;
        break;
    case 3:
        y = 5;
    case 4:
        z = 2;
        break;
    default:
        break;
}

Notice the break statements here. break is a statement that can be used in a switch or a loop as a quick exit. The case statements containing the break statements will cause the switch to exit as soon as they execute the code before the break. For example, in the first case, after y = 2 executes, the switch will exit. Notice, however, that the third case has no break statement. This means that after y = 5 is evaluated, execution will "fall through" to the next case, so that z = 2 is executed. Essentially, every case without a break statement will fall through to the next until a break is encountered.

Much of what you have seen here is the same in D as it is in other C-family languages. However, one thing that D does differently is to accept strings in both the switch and case statements. For example, the following is possible in D:

char[] str;
switch(str)
{
    case "Hello":
        . . .
    case "World":
        . . .
. . .
}

You can always use library functions to compare different strings, but the ability to use strings in switch-case blocks can greatly simplify code.

Looping Constructs

A loop is a block of code that executes repeatedly until a certain condition is met. D supports five different loops, but for our purposes, we will split them up into three categories: for, while, and foreach.

In loops, the break and continue statements provide a way to exit. You've already seen break in the discussion of switch-case constructs. In loops, it serves the same function: an early exit. If you are performing an operation in a loop and determine that you no longer need to repeat the operation, you can execute a break statement and exit the loop immediately, rather than waiting for it to run its course. This is often used as an optimization in loops that are searching a data structure for a specific value, because once the value is found, you don't need to continue searching.

The continue statement is also handy. Rather than exiting the entire loop as break does, it exits the current iteration and continues to the next. The effect is that any code following the continue statement will not be executed in the same iteration in which continue is called. This statement is usually wrapped in a conditional, as it would be rather silly to call it on every iteration. The result is that the code following a continue will execute on some iterations, and it won't execute on other iterations.

for Loops

The venerable for loop has found a place in many different programming languages, both inside and outside the C family. D's version follows the traditional form. The loop declaration consists of three expressions that are evaluated at different points.

The form of the declaration is as follows:

for(initializer; termination condition; per-iteration action) { }

where:

  • The initializer expression is evaluated only once when the loop is getting ready to start.

  • The termination condition is evaluated at the beginning of each iteration (and on the first iteration, just after the initializer).

  • The per-iteration action is evaluated at the end of each iteration.

These can each actually be expression lists (multiple, comma-separated expressions), and each expression in the list will be evaluated in the order it appears.

Here's an example of using a for loop.

for(int i=0; i < 100; ++i)
{
    // Do something you want to repeat 100 times.
}

At the start of this loop, the variable i is initialized to 0. Because it is part of the loop declaration, i is part of the loop's scope and is not visible outside it. At the beginning of each iteration, the loop checks the termination condition and will quit if i is no longer less than 100. At the end of each iteration, i is incremented by 1. The result is that the loop will execute 100 times before terminating.

while Loops

D supports two forms of while loops: the standard while and the special do..while, also called the do loop.

Unlike the for loop, a while loop has only one expression to evaluate: a termination condition, which is evaluated at the beginning of each iteration. If any variables need to be updated, that must be done within the loop. The while syntax is as follows:

while(termination condition) { }

The do loop is very similar to the while loop. It also has only a termination condition to evaluate. The difference is that it is evaluated at the end of each iteration, rather than at the beginning:

do
{

} while(termination condition)

The two while loop forms also have a scope difference. Variables declared in the termination condition of a while loop are visible within the scope of the loop. But in a do loop, there are actually two scopes: the do scope (in the curly braces) and the while scope. The result is that any variables declared in the while portionthe termination conditionof a do loop are not visible to the scope in the curly braces and vice versa.

The following example demonstrates both while loop forms.

int i = 0;
while(i < 100)
{
    ++i;
}

int j = 0;
do
{
    ++j;
} while(j < 100);

In practice, the standard while loop is used much more frequently than the do loop form. Which you choose to use depends on your circumstances. For example, a do loop should never be used if there is a chance that the termination condition could evaluate to true before the first iteration. Doing so could open the door for some bad bugs. On the other hand, anywhere you can use a do loop, you can use a while loop. In practice though, the do loop is generally used when you know for sure that at least one iteration will run. It's more a way of expressing the intent of the code than of any technical necessity of choosing do over while.

foreach Loops

The foreach loop is another extremely popular D feature. Not every language has adopted the foreach loop, but it is popping up in more and more modern languages.

A foreach loop is essentially a more efficient for, specialized for the case when you want to visit each element in an array or other container. The idea is that the loop automatically visits every element in a container without you needing to give it any expressions to evaluate, other than a place to store the current element and a container to iterate. A container can be a static, dynamic, or associative array. In other words, D works with all three types right out of the box, allowing you to automatically iterate over any type of array and visit each element it contains. You can add this functionality to your own data types as well with a special operator overload, as you'll see in Chapter 3.

A foreach declaration looks like this:

foreach([index], value; container) { }

where value is a variable that will hold the current element, and container is the array being iterated. index is an optional value that will be assigned the array index, or sequence position, of the current element.

The loop operates in order, from front to back, and executes one iteration for each element in the container. After all of the elements have been iterated, the loop exits.

One of the great things about the foreach loop is that the type of value can be inferred automatically from the type of container, so you don't need to specify the type in the declaration of value.

Here are a few examples of using the foreach loop:

int[] x = [0, 1, 2, 3, 4, 5];

// This version specifies the type.
foreach(int y; x)
{
   // Do something with y.
}

// This version uses automatic type inference.
foreach(y; x)
{
    // Do something with y.
}

// This version uses the optional index, and automatic type inference for both
// the index and the value.
foreach(i, y; x)
{
    // Do something with y and x.
}

In addition to the standard foreach, D supports foreach_reverse, which does the same thing, except it iterates the container in reverse, from back to front. This is quite handy for implementing custom containers, for iterating the most recent items added to a queue, as an alternative to the reverse property of arrays, and for handling other special cases where you might need to start an iteration from the rear of a container.

The goto Statement

Those of you familiar with programming language debates are well aware of the goto statement's controversial history. The father of D, Walter Bright, happens to fall into the camp that finds goto useful and, as such, it has found a place in the language.

goto jumps from one point in the program to another point that is marked by a label. Older programming languages literally allowed you to jump to any labeled location in the program, which undoubtedly is the major source of early criticism. Modern languages are more restrictive, however, and allow you to jump only to a label in the current scope.

A label is an identifier that is followed by a colon. When you jump to a label with goto, execution of the program halts at the point where the goto is encountered and picks up again at the instruction immediately following the label.

The following demonstrates the use of goto.

void main()
{
    int x = 1;
repeat:    // This is a label.
    x += 1;
    if(x < 3) goto repeat;    // Repeat x+=1 until x is no longer less than 3.
}

Note

Admittedly, the example here is a horrible application of goto. However, it does demonstrate the use of goto and labels using constructs you have already seen.

These days, the goto statement is most commonly used in C to jump to the end of a function in order to clean up multiple resources in case of an error. But even that is not needed in D, as there are other alternatives, which you'll see in the "Error Handling" section later in this chapter. In D, goto should primarily be considered as a backward-compatibility tool for porting C source directly to D. You should probably avoid it in new code.

Functions

Now that you've seen the basic data types and flow-control constructs, it's time to look at D's function syntax. Functions in D work much as they do in other C-family languages, although they have a few differences that are uniquely D.

There's nothing revolutionary or unusual about declaring functions in D. The syntax is as follows:

ReturnType Identifier(ParameterList) { }

where:

  • ReturnType is the type returned by the function, which will be void if there is no return value.

  • Identifier is the name of the function, which follows the identifier guidelines discussed earlier in the chapter, in the "Declarations" section.

  • ParameterList is a comma-separated list of zero or more variables as type/identifier pairs.

The following shows the anatomy of a function.

int myFunction(int a)   // The declaration
{    // The beginning of the function body and a new scope

    return a+1;    // Immediately exits the function, the result of a + 1
                   // being passed to the caller

}    // The end of the function body and scope

In this example, myFunction is declared to return type int. It accepts one parameter of type int. And, finally, the implementation returns the value of the expression a + 1.

To call a function, use this syntax:

functionName(parameters);

So to call the sample myFunction function and pass it an integer value of 1, you would do this:

myFunction(1);

Function parameters can have three different modifiers, or storage classes, that affect their behavior:

in: By default, all function parameters are in parameters if no storage class is specified. An in parameter is passed into a function from the call site with whatever value it has been assigned. If you attempt to modify the value of an in parameter, the modification is seen locally only and does not affect the variable at the call site. Effectively, an in parameter is a new variable that is a copy of the value of the original.
out: In contrast to in parameters, out parameters are reset to the default initializer of their type, and modifying them does change the value at the call site.
ref: These parameters are a bit of a combination of the other two. They are passed into a function with whatever value has been assigned to them, and any modifications are reflected at the call site. So you can say that ref parameters are another view of the variable at the call site.

Caution

In the case of arrays, the value of the array is the object that contains the length and the pointer. Changing this in a function will have no effect at the call site. However, if the contents of an array parameter are modified inside a function, the changes will be seen at the call site, even if the parameter is declared as in.

The following is a complete program that shows how all this works.

import tango.io.Stdout;

void main()
{
    int x = 1;
    int y = 2;
    int z = 3;

    Stdout.formatln("In main, x: {} y: {} z: {}", x, y, z);

    // Call some function.
    someFunction(x, y, z);

    Stdout.formatln("In main again, x: {} y: {} z: {}", x, y, z);
}

void someFunction(int x, out int y, ref int z)
{
    Stdout.formatln("In someFunction, x: {} y: {} z: {}", x, y, z);
x = 10;
    y = 20;
    z = 30;

    Stdout.formatln("In someFunction again, x: {} y: {} z: {}", x, y, z);
}

If you compile and execute this program, you should see the following:

In main, x: 1 y: 2 z: 3
In someFunction, x: 1 y: 0 z: 3
In someFunction again, x: 10 y: 20 z: 30
In main again, x: 1 y: 20 z: 30

You can see quite clearly that y is set to reset 0, the value of int.init, when it is passed to someFunc, since it is declared in the parameter list as out. You can also see that the assignment made to x in someFunc has no effect on the original value of x in the main function, since the parameter x has the default in storage. Finally, the assignment to both y and z affects the original values back in main, because the parameters were declared as out and ref, respectively.

Error Handling

In C, errors are typically handled by checking for specific error codes after a function call or through the primitive setjmp mechanism. Perhaps the biggest headache with these dated techniques is the freeing of resourcesany resources allocated prior to the point of error must be manually deallocated after the error occurs. Modern programming languages provide a more robust means of error handling through a mechanism called exception handling, which allows for the guaranteed deallocation of resources. D, too, supports exception handling, but also provides a handy alternative that offers more precise control over when resources are deallocated.

Throwing Exceptions

Tango provides an Exception class that is used as the base construct for exception handling. You can think of Exception as an object that contains data that describes an error. When an error occurs in a function, a new Exception object can be allocated and thrown back to the caller. This throwing of an exception bypasses a function's normal return mechanism.

The following example demonstrates how to throw an exception from a function.

void trouble()
{
    // Do something here, but throw an exception on error.
    if(someErrorOccurs)
        throw new Exception("We're in trouble!");

    // Any code here will not be executed if the exception is thrown.
}

Here, if the if condition evaluates to true, the throw statement is executed and causes the function to immediately exit, skipping the execution of any code following the throw. The exception will be pushed back to the caller. If the caller ignores the exception, it will be pushed up the call stack until it is handled. By default, all D programs include a top-level exception handler, which catches any unhandled exceptions that make it all the way back to the main method. This causes any string message associated with the exception to be printed to the console and the program to exit. In the next section, you'll see how to handle, or catch, an exception.

Catching Exceptions

When you call a function that you know can potentially throw an exception, it's generally a good idea to try to handle that exception at the call site. This will allow you to determine how the program should respond. In some cases, you might want to abort execution immediately; in other situations, you might want to try an alternative operation or ignore the exception altogether. Exceptions are handled via constructs called try/catch/finally blocks.

The first step in handling an exception is to enclose the function call in a try block. try blocks do not work alone, however, and must be followed by a catch block, a finally block, or both. In the declaration of a catch block, you must specify the type of exception you are interested in catching. For our purposes, we'll use the Exception object that is available in the default namespace, so you don't need to import any special modules to use it. Inside the catch block, you can implement the code you need to respond to the exception. The following example demonstrates this.

try
{
    trouble();
}
catch(Exception e)
{
    // Here you can access e like any other variable, but it is invisible outside
    // the scope of the catch block. If you want, you can 'rethrow' the
    // exception using the throw statement, or return from the function. We'll
    // return for simplicity's sake.
    return;
}

One problem with catch blocks is what to do about the code that follows them. If the proper response to the exception is to abort the current function, either via a return statement or by rethrowing the exception, then the code that follows the catch block will not be executed. Sometimes, this is unacceptable, particularly if you need to deallocate any resources that were allocated prior to entering the try/catch blocks. You would need to duplicate the deallocation inside the catch block and after it. That's where finally blocks come in handy.

A finally block must follow either a try or a catch. Any code inside a finally block is guaranteed to execute, whether or not an exception is thrown, and whether or not a thrown exception is caught, as the following example shows.

try
{
    trouble();
}
catch(Exception e)
{
    return;
}
finally
{
    // Even if an exception is caught above and the return statement executed,
    // any code in this finally block will still execute before the current
    // function returns.
}

Caution

try, catch, and finally blocks create a new scope. Any variable declarations inside of one will be invisible to the outside.

Using a scope Statement

finally blocks are great for cleaning up resources, but using them isn't always feasible. When you cannot ensure that the finally block is the last piece of code in a function, you still may need to duplicate code. D's solution to this problem is the scope statement.

You've seen the word scope used throughout this chapter in reference to the grouping and visibility of variables and functions. A scope statement is a built-in language feature that you can use to guarantee the execution of a block of code when the current scope exits. A scope can exit either because the end of the scope has been reached through normal execution or because an exception has been thrown. By using a scope statement, you can tell the compiler that a block of code should be executed only when the current scope exists normally, only when an exception is thrown, or when either condition occurs.

The following example demonstrates all three uses of the scope statement.

void main()
{
    scope(success) doSomethingOnSuccess();
    scope(failure) doSomethingOnFailure();
    scope(exit)
    {
        doSomethingAlways();
        doSomethingElse();
    }
    // Put your code here.
}

Here, the first scope statement will execute only if the current scopein this case, the main functionexits normally. The second statement will execute only if the current scope exits due to an exception being thrown. The last statement will execute when the current scope exits, regardless of the reason. As you can see, scope statements are a powerful mechanism that can be used to make your code more robust and secure.

This completes our tour of D fundamentals. We've covered a wide range of topics, from basic declarations to error handling. You've learned enough to get your own D programs up and running, but you would be restricted to a subset of D's capabilities. The next chapter opens up more options related to the objected-oriented features of D.

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

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