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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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 |
---|---|
| The default initialization value |
| The size in bytes |
| The byte boundary upon which the type or instance is aligned |
| A string representing the "mangled" name |
| A string representing the name of the type or instance as it appears in source code |
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 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 |
---|---|---|---|---|
| 8 | 1 | 128 | 127 |
| 8 | 1 | 0 | 255 |
| 16 | 2 | 32768 | 32767 |
| 16 | 2 | 0 | 65535 |
| 32 | 4 | 2147483648 | 2147483647 |
| 32 | 4 | 0 | 4294967295 |
| 64 | 8 | 9223372036854775808 | 9223372936854775807 |
| 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
.
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.
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 |
---|---|---|---|
| 32 | 4 |
|
| 64 | 8 |
|
| Platform dependent | Platform dependent |
|
| 32 | 4 |
|
| 32 | 8 |
|
| Platform dependent | Platform dependent |
|
| 64 | 8 |
|
| 128 | 16 |
|
| Platform dependent | Platform dependent |
|
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 |
---|---|
| Value that is too large to represent |
| Value used to represent NAN |
| Number of decimal digits of precision |
| Smallest increment to the value 1 |
| Number of bits in the mantissa |
| Maximum power of 10 exponent this type can represent |
| Maximum power of 2 exponent this type can represent |
| Minimum power of 10 exponent this type can represent as a normalized value |
| Minimum power of 2 exponent this type can represent as a normalized value |
| Largest value this type can represent that is not infinity |
| Smallest normalized value this type can represent that is not 0 |
| Real part of the number |
| Imaginary part of the number |
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
.
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 |
---|---|---|---|---|---|---|
| 8 | 1 | UTF-8 | 0 | 255 | 0xFF |
| 16 | 2 | UTF-16 | 0 | 65535 | 0xFFFF |
| 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.
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.
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
.
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 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 |
---|---|
| Number of elements in the array; cannot be modified |
| Pointer to the first element in the array |
| Creates and returns a dynamic array that is an exact duplicate of the array |
| Reverses the array in place and returns the array |
| 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.
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.
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.
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.
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 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 |
---|---|
| Number of values in the associative array; as with static arrays, this is read-only |
| Dynamic array containing all of the keys in the associative array |
| Dynamic array containing all of the values in the associative array |
| 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.
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.
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 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.
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 .. $]
.
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.
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.
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
.
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.
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.
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.
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
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.
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.
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.
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
.
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.
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. }
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.
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. |
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.
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.
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.
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. }
try
, catch
, and finally
blocks create a new scope. Any variable declarations inside of one will be invisible to the outside.
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.