| Learn to Tango with D Excerpt: Chapter 2 | |
|
Learn to Tango with D Excerpt: Chapter 2
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. DeclarationsWhen 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 InitializationD 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 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 identifier—whether it is the name of a variable, function, class, struct, or whatever—you need to keep a few rules in mind:
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 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 ScopeThe term scope describes the context in which a particular declaration resides. Scope affects variable declarations in two ways:
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 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 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. Constant and Nonconstant ExpressionsThe term expression is often used in language specifications. It is bandied about by compiler writers, who tend to use a great deal of vocabulary that average programmers forgot about after their last comp-sci course, or never knew at all. The D Programming Language specification defines the term as follows: Expressions are used to compute values with a resulting type. These values can then be assigned, tested, or ignored. Expressions can also have side effects. Any single value, such a 1 or an x, is an expression. An arithmetic operation, such as 1+1 or 1+x, is an expression. A function call that returns a value is an expression. All of these can be used anywhere the language specification says an expression is legal, such as making an assignment to a variable. A constant expression is one whose value can be known at compile time and is never going to change. For example, 1 is a constant because it is always 1, just as 1+1 always results in the value 2. Because the values of constant expressions never change, the compiler can make certain optimizations with constant expressions that it otherwise wouldn’t be able to do. One popular feature of D is compile time function execution (CTFE). This feature is built around constant expressions, allowing programmers to create functions and other expressions that are evaluated at compile time rather than at runtime. This allows the D compiler to make extraordinary optimizations that would not be possible otherwise. You’ll learn about CTFE in Chapter 5. A nonconstant expression is one whose value can change, and usually does. For example, the declaration 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 TypesD 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 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. PropertiesAll 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 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
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
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 TypesIntegral types are types that represent integer values. Nearly all integral types in D come in two flavors:
The Also missing from Table 2-2 are With the exception of In D, all constant integer values are of type 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.
Floating-Point TypesIn 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: After reading about the integral types in the previous section, you might think that the default initializer for floating-point types is 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.
The types 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
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. 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 You can read more about D’s floating-point types, including the rationale behind including complex and imaginary types in the language here. Character TypesWhereas 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.
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 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. PointersIn 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 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 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. 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. Click here for details. ArraysAn 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 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 intsThe 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 intsThe 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 ArraysStatic 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).
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 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 ArraysUnlike 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 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 declarationNeither 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:
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 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.
StringsIn 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 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 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 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 ArraysAssociative 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.
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 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:
Here is an example that uses both
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 OperationsBoth 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 SlicingSlicing 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 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 .. $].
CopyingIn D, you can copy an array in three ways:
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. ConcatenationD has a special binary operator,
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 ControlIn 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 ConditionalsA conditional is a construct that branches based on a true or false condition. D supports three different types of conditionals: if-else BlocksPerhaps the most commonly used conditional is the form often referred to as an
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 OperatorOften, you don’t need to perform any overly complicated action based on an The ternary operator consists of two characters:
where The following example shows how you can replace a single assignment in an
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
You start with a You need to keep in mind a few restrictions on both switch and case statements:
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
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 ConstructsA 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: In loops, the The for LoopsThe venerable The form of the declaration is as follows:
where:
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 LoopsD supports two forms of Unlike the
The
The two The following example demonstrates both
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 LoopsThe A A foreach declaration looks like this:
where 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 Here are a few examples of using the
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 StatementThose of you familiar with programming language debates are well aware of the
A label is an identifier that is followed by a colon. When you jump to a label with The following demonstrates the use of
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 FunctionsNow 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:
where:
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:
So to call the sample
Function parameters can have three different modifiers, or storage classes, that affect their behavior:
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: 30You 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 HandlingIn C, errors are typically handled by checking for specific error codes after a function call or through the primitive Throwing ExceptionsTango provides an 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 ExceptionsWhen 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 The first step in handling an exception is to enclose the function call in a
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
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 |