Learn to Tango with D Excerpt: Chapter 2
D Fundamentals
|
A movie director once made the observation that Charlie Sheen and Emilio Estevez both resemble their father, Martin Sheen, but look nothing like each other. Something similar might be said of programming languages that belong to the C family. They all share certain features of C, but beyond that, they have several differences. For the most part, the differences are to be found in expanded features, such as inherent support for object-oriented or generic programming, automatic memory management, or features intended to make programs more secure and robust. But at the core of each of these languages is a set of features that vary little from one language to the next. D, too, follows this pattern, but does so in a way that makes it stand out from the rest. As you’ll see in later chapters, some of D’s more advanced features improve on ideas already implemented in other modern languages derived from C; some were inspired by languages outside the family; and a few are not to be found in any other mainstream programming language. While these features all contribute to D’s unique identity, many users are first drawn to the language by the core feature set. In this chapter, we’ll look at these core features. We’ll start out with declarations before moving on to the basic types. Next, we’ll look at the different kinds of arrays and array operations. Then we’ll get into flow-control constructs. Finally, we’ll discuss functions and error handling. 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.
|
|