D (The Programming Language)/Printable version
This is the print version of D (The Programming Language) You won't see this message or any elements not part of the book's content when you print or preview this page. |
The current, editable version of this book is available in Wikibooks, the open-content textbooks collection, at
https://en.wikibooks.org/wiki/D_(The_Programming_Language)
Contributors
Style Guides
[edit | edit source]To Do List
[edit | edit source]- Finish the rest of the D (The Programming Language)/d2/.
List of Contributors
[edit | edit source]Add yourself to this list if you have contributed to this wikibook:
Userbox
[edit | edit source]You may add this box to your user page.
|
{{User:Jcao219/Userboxes/DTPLContributor}} |
d2/Lesson 1
Lesson 1: Hello, World!
[edit | edit source]In this lesson, you will learn how to use the Phobos library to write to the console. Also, you will learn some about the structure of D programs.
Introductory Code
[edit | edit source]We will start with the absolutely necessary Hello World example.
Hello World
[edit | edit source]/* This program prints a
hello world message
to the console. */
import std.stdio;
void main()
{
writeln("Hello, World!");
}
Concepts
[edit | edit source]In this lesson we see the import
statement, the main function, the Phobos standard library in use, and also a code comment.
import std.stdio
[edit | edit source]The Phobos library contains the std.stdio module which in turn contains the writeln function (along with various other functions). In order to use that function, you must first import that module. Notice that statements in D end in a semicolon.
void main()
[edit | edit source]All executable D programs contain a main function.
This function does not return a value, so it is declared "void" (Technically, this is an over-simplification. Return values of main
will be explained in more detail later.)
This function is executed when the program runs.
Function body code is enclosed in curly brackets. Although the indentation and whitespace is only for the reader, not for the compiler, make sure you indent and format your code properly and consistently; it's generally a good coding practice.
writeln and the write family
[edit | edit source]writeln
is for writing to the standard output (console in this case). You can supply a string such as "Hello, World!"
as an argument, and that string will be printed out. There are four basic flavors of this function: With or without formatting, and with or without a trailing newline. To demonstrate how they work, all of the following are equivalent (although the first and thirds ones don't flush stdout
on Windows):
// write: Plain vanilla
write("Hello, World!\n"); // The \n is a newline
write("Hello, ", "World!", "\n");
write("Hello, ");
write("World!");
write("\n");
// writeln: With automatic newline
writeln("Hello, World!");
writeln("Hello, ", "World!");
// writef: Formatted output
writef("Hello, %s!\n", "World");
// writefln: Formatted output with automatic newline
writefln("Hello, %s!", "World");
writefln("%s, %s!", "Hello", "World");
writefln("%2$s, %1$s!", "World", "Hello"); // Backwards order
/* I am a comment */
[edit | edit source]The first few lines of this program are comments. They are ignored by the compiler. Block comments are enclosed in /* */
. Line comments continue after //
.
This is an example of a line comment:
import std.stdio; // I am a comment
void main(){} //this program does nothing
D also supports nested block comments, enclosed in /+ +/
:
/+
thisIsCommentedOut();
/+ thisIsCommentedOut(); +/
thisIsSTILLCommentedOut();
+/
This is different from ordinary block comments which behave just like in C:
/*
thisIsCommentedOut();
/* thisIsCommentedOut(); */
thisIsNOTCommentedOut();
// The following line is a syntax error:
*/
Tips
[edit | edit source]writeln
is different fromwrite
because it adds a newline to the end.
d2/Lesson 2
Lesson 2: Types and Declarations
[edit | edit source]In this lesson, you will learn how to declare and use variables.
Introductory Code
[edit | edit source]The Basic Declaring of Variables
[edit | edit source]import std.stdio;
int b;
void main()
{
b = 10;
writeln(b); // prints 10
int c = 11;
// int c = 12; Error: declaration c is already defined
string a = "this is a string";
writeln(a); // prints this is a string
// a = 1; Error, you can't assign an int to a string variable
int d, e, f; // declaring multiple variables is allowed
}
Manipulation of Variables
[edit | edit source]import std.stdio;
void main()
{
int hot_dogs = 10;
int burgers = 20;
auto total = hot_dogs + burgers;
string text = burgers + " burgers"; // Error! Incompatible types int and string
writeln(hot_dogs, " hot dogs and ", burgers, " burgers");
writeln(total, " total items");
}
Concepts
[edit | edit source]In this lesson we see the declaration and assignment of variables.
Declaration Syntax
[edit | edit source]The syntax of declaring variables is
type identifier;
You can also assign a value at the same time.
type identifier = value;
Declaring Multiple Variables in the Same Line
[edit | edit source]D allows you to declare multiple variables of the same type in the same line. If you write:
int i, j, k = 30;
i, j, and k are all declared as ints but only k is assigned 30. It is the same as:
int i, j;
int k = 30;
Implicit Type Inference
[edit | edit source]If the declaration starts with auto
, then the compiler will automatically infer the type of the declared variable.
In fact, it doesn't have to be auto
. Any storage class would work. Storage classes are built-in D constructs such as auto
or immutable
. This code declares an unchangeable variable a with a value of 3. The compiler figures out that the type is int
.
immutable a = 3;
You will learn much more about storage classes later.
More about writeln
[edit | edit source]writeln
is a very useful function for writing to stdout. One thing is special about writeln: it can take variables of any type. It can also take an unlimited amount of arguments. For example:
writeln("I ate ", 5, " hot dogs and ", 2, " burgers.");
// prints I ate 5 hot dogs and 2 burgers.
Don't worry, there is nothing magical about writeln
. All the functions in the write
family are implemented (in 100% valid D code) in stdio.d, which is in the Phobos standard library.
Tips
[edit | edit source]- Syntax like this:
int i, j, k = 1, 2, 3;
is not allowed. - Syntax like this:
int, string i, k;
is also not allowed. auto
is not a type. You cannot do this:auto i;
because there's no way the compiler can infer the type of i.auto
is merely a storage class, which tells the compiler to infer the type from the value that you assign it to in that same line.
d2/Lesson 3
Lesson 3: Intro to Functions
[edit | edit source]In this lesson, you see more of functions, which were used back in Lesson 1. If you are already familiar with another C-like language, you only need to skim this over.
Introductory Code
[edit | edit source]import std.stdio;
int watch_tv(int first_watched, int then_watched, string channel = "The PHBOS Channel")
{
writefln("Finished watching %s.", channel);
return first_watched + then_watched;
}
int commercials(int minutes)
{
writefln("Watching %s minutes of commercials.", minutes);
return minutes;
}
int cartoons(int minutes)
{
writefln("Watching %s minutes of cartoons.", minutes);
return minutes;
}
void main()
{
auto minutesoftv = watch_tv(commercials(10), cartoons(30));
writeln(minutesoftv, " minutes of TV watched!");
}
/*
Output:
Watching 10 minutes of commercials.
Watching 30 minutes of cartoons.
Finished watching The PHBOS Channel.
40 minutes of TV watched!
*/
Concepts
[edit | edit source]Functions
[edit | edit source]Functions allow you to write more organized code. With functions, you can write some code once, and then call it whenever you need to use that code.
Function Syntax
[edit | edit source]Let's take a look at the syntax:
return_type name_of_function(arg1type arg1, arg2type arg2)
{
// function body code here
return something;
}
d2/Lesson 4
Lesson 4: Types, Continued
[edit | edit source]In this lesson, you will see all the other types. Many of these types will be familiar, but be aware that there are many differences between C and D in their built-in types.
Introductory Code
[edit | edit source]All-in-One
[edit | edit source]import std.stdio;
void main()
{
bool a; // default initialized to false
a = 1;
a = false;
uint aa = 53; //unsigned int
ulong bb = 10395632; //unsigned long
short cc = 100;
ushort dd = cc; //dd == 100
byte b = 0x5E;
writeln(b); // prints 94
b = 110;
ubyte c = 255; //maximum value for a ubyte
assert(c == ubyte.max);
short d = 50;
long e = d;
writeln(e); // 50
float f; // default initialized to float.nan
float g = 3.13;
double h = g * 2.5;
// largest hardware implemented floating-point
real i = 373737373737377373.0;
writeln(i); // 3.73737e+17
// these numbers are not real:
idouble j; //default = double.nan * 1.0i
ifloat k;
ireal l;
cfloat m = 5 + 32.325i; // a complex number
// unsigned 8-bit UTF-8
char n; // default initialized to 0xFF
n = 'm';
writeln(n); // m
// unsigned 16-bit UTF-16
wchar o; // default = 0xFFFF
// unsigned 32-bit UTF-32
dchar p; // default = 0x0000FFFF
}
Concepts
[edit | edit source]Assertion
[edit | edit source]Using assert
allows you to check if a certain expression is true or not. ==
means is equal to (do not confuse this with =
, which is for assigning variables). The checking is done at runtime, and if whatever is inside the parentheses evaluates to false, then an Assertion Error will happen and the program will terminate.
void main()
{
bool a = true;
assert(a);
assert(2346924); // anything that's not 0 or false
// assert(false); Assertion failure
int i = 4628;
assert(i == 4628); // (i == 4628) evaluates to true
assert(i != 4528); // (i != 4528) evaluates to true
assert(i >= 0); // yes, i is greater or equal to 0
}
Properties and Default Initializers
[edit | edit source]Types have default initializers. That means, if they are declared but not assigned any value, they will be equal to something by default.
int i; //i = int.init
assert(int.init == 0); // i is definitely 0
float b = float.init; //b = float.nan
Properties can be queried from any type or object. When a property of a type or object is queried, a dot goes between the property name and the identifier.
type.property
The init
property of any type contains the value that objects of such a type are initialized to by default. For numeral types, you can find the min and max values with their respective properties: type.min
and type.max
. You can also find the size of a type in memory: type.sizeof
.
If you want to declare but don't want to initialize something, you can do this:
int i = void;
Then, i is uninitialized; its value is undefined garbage.
A Bit of D's Lexical Grammar
[edit | edit source]D allows you to write numbers in a few useful bases and forms. So, if you wanted to add the hexadecimal number of A4 to one million three hundred thirteen thousand six hundred thirty-seven and the binary number of 1011010101, you can write this:
import std.stdio;
void main()
{
writeln(0xA4 + 1_113_637 + 0b10_11_010101);
// hex numbers are prefixed by 0x
// binary numbers by 0b
// the answer is 1114526
}
Note that underscores in the middle of numbers are completely ignored. They are 100% optional and are useful for formatting long numbers.
Tips
[edit | edit source]- You may look through this reference for more information about D's lexical grammar. Soon a lesson will be written that contains up-to-date information on the subject.
d2/Lesson 5
Lesson 5: Type Conversion
[edit | edit source]In this lesson, you will learn how the types of variables can be implicitly and explicitly converted.
Introductory Code
[edit | edit source]import std.stdio;
void main()
{
short a = 10;
int b = a;
// short c = b;
// Error: cannot implicitly convert
// expression b of type int to short
short c = cast(short)b;
char d = 'd';
byte e = 100;
wchar dw = 'd';
int f = d + e + dw;
writeln(f); //300
float g1 = 3.3;
float g2 = 3.3;
float g3 = 3.4;
int h = cast(int)(g1 + g2 + g3);
writeln(h); //10
int i = cast(int)g1 + cast(int)g2 + cast(int)g3;
writeln(i); //9
}
Concepts
[edit | edit source]Implicit Integral Conversions
[edit | edit source]An object of an integral type can be converted to another object of an integral type as long as the destination type is wider than the original type. These conversions are implicit:
bool
⇒ int
byte
⇒ int
ubyte
⇒ int
short
⇒ int
ushort
⇒ int
char
⇒ int
wchar
⇒ int
dchar
⇒ uint
Explicit Conversions
[edit | edit source]Casting is a way to tell the compiler to try to force an object to change type. In D, you do this by writing cast(type)
.
Tips
[edit | edit source]- You cannot convert an integral value to a string (or the other way around) with a cast. There is a library function which you will learn later for that.
d2/Lesson 6
Lesson 6: Modules
[edit | edit source]In this lesson, you will gain an understanding of D's module system.
A module is a unit of code that contains definitions for variables, functions, and other D constructs that belong together. This is useful for creating reusable code. You've seen modules in every single lesson. In fact, all D source files are modules. The stdio
module contains functions like writeln
. The module stdio
is written in a file called stdio.d at dmd/src/phobos/std/stdio.d
. It contains code that is useful for console input and output and file input and output. It is within a package called std
, which also contains modules such as std.algorithm
or std.string
, which you will learn about later. The Phobos library is just a big collection of modules that comes with a D compiler.
Introductory Code
[edit | edit source]// main.d
module main;
import std.stdio;
import module1;
import byebye : sayBye;
void main()
{
happy();
writeln("Mothers Day!");
sayBye();
// sayByeInItalian(); undefined identifier 'sayByeInItalian'
}
// happy.d
module module1;
import std.stdio;
void happy()
{
write("Happy ");
}
private void sad()
{
write("Who gives a darn about ");
}
private:
void mad() { }
void bad() { }
//bye.d
module byebye;
import std.stdio;
void sayBye()
{
writeln("Goodbye!");
}
void sayByeInItalian()
{
writeln("Arrivederci");
}
Compile like this: dmd main.d happy.d bye.d
Concepts
[edit | edit source]A Deeper Look at Imports
[edit | edit source]All D code is inside modules. In D programs, there has to be one module with a main
function. If one module imports another module, the imported module's definitions becomes available to the first module. In the introductory code, the main
module imported two other modules: module1
and byebye
. However, when byebye
was imported, only one function was imported: the sayBye function. The syntax for that is import modulename : identifier1, identifier2, identifier3;
.
Module Declarations
[edit | edit source]D source files can specify the name of the module and what package it belongs to with a module declaration, which goes at the beginning. For example, in the stdio
module, there is this line:
module std.stdio;
That line specifies the name of the module (stdio
) and the package it is located in (std
). Each source file can only have a maximum of one module declaration. If there is none, the source file name (without the extension) becomes the module name.
Visibility
[edit | edit source]Before the void
in the definition of sad
in module1
, you see the modifier private
. That means that the sad
function cannot be imported. You wouldn't be able to do import module1 : sad
, either.
If you see a line that says private:
, everything below that line is private
. Likewise, if you see private { }
, everything inside the curly brackets is private.
Tips
[edit | edit source]- The name of the module does not have to relate to the names of the definitions within that module.
- The name of the module does not have to relate to the file name.
- There is also a
public
modifier, which is the opposite of theprivate
modifier. Declarations marked by neitherpublic
norprivate
arepublic
by default. - All D source files are modules, even if there is no module declaration in the file.
d2/Lesson 7
Lesson 7: Pointers, Pass-By-Reference and Static Arrays
[edit | edit source]In this chapter, you will learn about how to use pointers and static arrays in D. You will also learn how to pass by reference without using pointers. The lesson text will not explain the concepts behind pointers and arrays, and it will assume that you have that knowledge from C or C++. D also has dynamic arrays and array slices, both of which will be covered in a later lesson.
Introductory Code
[edit | edit source]Using Pointers
[edit | edit source]import std.stdio;
void add_numbers(int* a, int* b)
{
*a = *a + *b;
}
void main()
{
int a = 50;
int* b = &a;
*b = 30;
writeln(a); // 30
// int* error = &50; Error: constant 50 is not an lvalue
int c = 2;
int d = 2;
add_numbers(&c, &d);
writeln(c); // 4
}
Pass By Reference
[edit | edit source]If you're a C++ programmer and you read the above thinking it's a step back from C++ references, then rejoice: while D supports pointers, it also supports better solutions for passing variables by reference to functions:
import std.stdio;
void add_numbers(ref int a, int b)
{
a += b;
}
void main()
{
int c = 2;
int d = 2;
add_numbers(c, d);
writeln(c); // 4
// add_numbers(4, 2); Error: integer literals are not lvalues and cannot be passed by reference
}
There is also special support for out parameters:
import std.stdio;
void initializeNumber(out int n)
{
n = 42;
}
void main()
{
int a;
initializeNumber(a);
writeln(a); // 42
}
Using out
, n in initializeNumber is default initialized at the beginning of the function.
Using Static Arrays
[edit | edit source]import std.stdio;
void main()
{
int[5] a;
a[0] = 1;
writeln(a); // [1, 0, 0, 0, 0]
// a[10] = 3; Error: Array index 10 is out of bounds
int* ptr_a = &a[0];
ptr_a[0] = 5;
ptr_a[4] = 3;
writeln(a); // [5, 0, 0, 0, 3]
// ptr_a[10] = 3; // Bad: This memory is not owned by 'a' as it is out of bounds
// Compiler would allow it, but the result is undefined!
int[3] b = 3;
writeln(b); // [3, 3, 3]
b[] = 2;
writeln(b); // [2, 2, 2]
}
Concepts
[edit | edit source]Reference and Dereference
[edit | edit source]The syntax in D is the same as C and C++. The operator &
takes the reference of an object, and *
dereferences.
Pointer Types
[edit | edit source]In D, a pointer to typeA is:
typeA*
and the code for declaring one is:
typeA* identifier;
.
Static Arrays
[edit | edit source]Static arrays have fixed lengths. You declare them like this:
byte[10] array_of_ten_bytes;
You can also do this:
byte[2] array_of_two_bytes = [39, 0x3A];
Arrays are indexed starting with the number zero for the first element. The compiler will catch you if you attempt to access an element with an index greater than or equal to that array's length. You access elements of an array like this:
my_array[index]
You can also fill an array with a single value, so that all of that array's elements equal that value:
int[7] a = 2;
writeln(a); // [2, 2, 2, 2, 2, 2, 2]
a[] = 7;
writeln(a); // [7, 7, 7, 7, 7, 7, 7]
As Function Arguments
[edit | edit source]Look at this code:
import std.stdio;
void addOne(int[3] arr)
{
arr[0] += 1;
// this means arr[0] = arr[0] + 1
}
void main()
{
int[3] array1 = [1, 2, 3];
addOne(array1);
writeln(array1); // [1, 2, 3]
}
The output is [1,2,3]
, not [2,2,3]
. Why? It's because static arrays are copied whenever they are passed as arguments to a function. The arr
in the addOne
function is only a copy of array1
. Rewrite it like this:
import std.stdio;
void addOne(ref int[3] arr)
{
arr[0] += 1;
}
void main()
{
int[3] array1 = [1, 2, 3];
addOne(array1);
writeln(array1); // [2, 2, 3]
}
d2/Lesson 8
Lesson 8: Strings and Dynamic Arrays
[edit | edit source]In this chapter, you will learn about strings in depth. In the meantime, you will also learn about another type: the dynamic array.
Introductory Code
[edit | edit source]Dynamic Arrays
[edit | edit source]import std.stdio;
void main()
{
int[] a = [1,2,3,4];
int[] b = [5,6];
auto c = a ~ b;
writeln(c); // [1,2,3,4,5,6]
writeln(c.length); // 6
int* ptr_c = c.ptr;
ptr_c[0] = 3;
writeln(c); // [3,2,3,4,5,6]
}
Immutable and Strings
[edit | edit source]import std.stdio;
void main()
{
// Concept: Immutable
immutable(int) a = 10;
// a = 11; Error: cannot modify immutable
immutable a = 10;
// Concept: Strings as Arrays
immutable(char)[] str = "Hello";
auto str1 = str ~ "1";
writeln(str1); // Hello1
writeln(str1.length); // 6
// str1[] = 'z'; error! str1's elements are not mutable
char[] mutablestr1 = str1.dup;
// Concept: Char Literals
mutablestr1[] = 'z'; // 'z' is not a string, but a char
str1 = mutablestr1.idup; // The str1 itself is mutable
// only its elements are not mutable
writeln(str1); // zzzzzz
}
Concepts
[edit | edit source]Dynamic Arrays
[edit | edit source]Dynamic Arrays versus Static Arrays
[edit | edit source]In the previous lesson, you learned about static arrays. Dynamic arrays are different from those in that they do not have a fixed length. Essentially, a dynamic array is a structure with this info:
- A pointer to the first element
- The length of the entire array
You create a dynamic array like this:
int[] a;
You can create a dynamic array initiated with a specific length:
int[] a = new int[](5);
You will learn more about the new
keyword later.
The syntax for filling an array with a single value is the same for both static and dynamic arrays:
int[] a = [1,2,3];
a[] = 3;
writeln(a); // [3, 3, 3]
Dynamic arrays are indexed in the same fashion as static arrays. The syntax for accessing an element at a certain index is the same:
a[2];
However, since dynamic arrays don't have a length known at compile-time, the compiler can't check if the index that you are accessing is really within that length. Code like this would compile but would also cause a runtime Range Violation error:
int[] a = [1,2,3];
writeln(a[100]); //Runtime error, Range violation
Manipulation of Dynamic Arrays
[edit | edit source]Dynamic arrays can be combined with other dynamic arrays or even with static arrays using the ~
operator. Appending a new element to the array uses the same operator.
int[] a = [1,2];
auto b = a ~ [3,4];
writeln(b); //[1, 2, 3, 4]
b ~= 5; // same as b = b ~ 5;
writeln(b); //[1, 2, 3, 4, 5]
You shouldn't assign a dynamic array to a static array (my_static_arr = my_dynamic_arr;
) unless if you are certain that my_dynamic_arr.length
is the equal to my_static_arr.length
. Otherwise, a runtime error will occur. You can always assign a static array to a dynamic array variable, though, since the dynamic array will automatically resize if the static array's length is too big.
int[] a = [9,9,9,9,9,9];
int[3] b = [1,2,3];
int[200] c;
a = b;
writeln(a); // [1, 2, 3]
a = c;
writeln(a.length); // 200
int[] d = [5,4,3];
b = d; // OK: lengths are both 3
// c = d; runtime error! lengths don't match.
Passing Dynamic Arrays to Functions
[edit | edit source]Dynamic Arrays are passed by value to functions. That means, when you pass a dynamic array to a function, the structure that contains the pointer to the first element and the length is copied and passed.
void tryToChangeLength(int[] arr)
{
arr.length = 100;
}
void main()
{
int[] a = [1,2];
tryToChangeLength(a);
writeln(a.length); // still 2
}
You learned in the last chapter that you can cause things to be passed by reference instead of by value by adding the ref
modifier.
Array and Other Properties
[edit | edit source]These properties apply to both static and dynamic arrays:
Property | Description |
---|---|
.init | For static arrays, it returns an array with each element initialized to its default value. |
.sizeof | Returns the size of the array in memory. |
.length | Returns the number of elements in the array. This is fixed in static arrays. |
.ptr | Returns a pointer to the first element of the array. |
.dup | Creates a dynamic array that is a copy of the array and returns it. |
.idup | Similar to .dup but the copy's elements are immutable .
|
.reverse | Returns the array with its elements in reverse order. |
.sort | Sorts in place the elements in the array and returns the result. |
There are also other properties which are common to every object or expression. Two of such properties are the .init
and .sizeof
properties.
Immutable
[edit | edit source]In D, immutable
is a storage-class just like auto
. It converts a type to a non-modifiable type.
immutable(int) fixed_value = 37;
immutable int another_value = 46;
Note that immutable(type)
means the same as immutable type
to the compiler.
Storage-Classes and auto
[edit | edit source]When you have a storage class like immutable
, you can omit auto
for type inference.
immutable fixed_value = 55;
The type of whatever is immutable is inferred from the compiler. This next code example is not valid because there is no way the compiler could infer the type:
immutable fixed_value; //Error!
Using immutable
Variables
[edit | edit source]This is allowed and perfectly fine code:
immutable(int) a = 300;
int b = a;
It only sets b
equal to the value of a
. b
does not have to be immutable
. That changes if you are taking a reference:
immutable(int) a = 13;
immutable(int)* b = &a;
// int* c = &a; Error.
You are allowed to cast the immutable
away, but if you were to modify an immutable value using that hack, the result is undefined.
immutable(int) a = 7;
int* b = cast(int*)&a;
// Just make sure you do not modify a
// through b, or else!
Strings as Arrays
[edit | edit source]You've seen strings since Lesson 1. A string
is the exact same thing as immutable(char)[]
, a dynamic array of immutable char elements. Likewise, a wstring
is the same as immutable(wchar)[]
, and a dstring
is the same as immutable(dchar)[]
.
String Properties
[edit | edit source]Strings have the same built-in properties as dynamic arrays. One useful property is the .dup
property, for creating a mutable char[]
copy of a string if you want to modify the individual characters of the string.
string a = "phobos";
char[] b = a.dup;
b[1] = 'r';
b[4] = 'e';
writeln(b); // probes
The .idup
property is useful for creating a copy of an existing string, or for creating a string copy of a char[]
.
string a = "phobos";
string copy_a = a.idup;
char[] mutable_a = copy_a.dup;
mutable_a[3] = 't';
copy_a = mutable_a.idup;
writeln(mutable_a); // photos
writeln(a); // phobos
Char Literals
[edit | edit source]A char
literal is enclosed by single-quotes. There are also wchar
and dchar
literals.
auto a = "a"; // string
auto b = 'b'; // char
auto c = 'c'c; // char
auto d = 'd'w; // wchar
auto e = 'e'd; // dchar
d2/Lesson 9
Lesson 9: Slicing and a Deeper Look into Dynamic Arrays
[edit | edit source]Slicing in D is one of the language's most powerful and useful aspects. This lesson is really more of a continuation of the last lesson - you will also get a deeper look into how D's arrays work.
Introductory Code
[edit | edit source]import std.stdio;
void writeln_middle(string msg)
{
writeln(msg[1 .. $ - 1]);
}
void main()
{
int[] a = [1,3,5,6];
a[0..2] = [6,5];
writeln(a); // [6, 5, 5, 6]
a[0..$] = [0,0,0,0];
writeln(a); // [0, 0, 0, 0]
a = a[0 .. 3];
writeln(a); // [0, 0, 0]
a ~= [3,5];
writeln(a); // [0, 0, 0, 3, 5]
int[] b;
b.length = 2;
b = a[0 .. $];
writeln(b.length); // 5
b[0] = 10;
writeln(b); // [10, 0, 0, 3, 5]
writeln(a); // [10, 0, 0, 3, 5]
writeln_middle("Phobos"); // hobo
writeln_middle("Phobos rocks");
}
Concepts
[edit | edit source]What are Slices?
[edit | edit source]You can take slices of arrays in D with this syntax:
arr[start_index .. end_index]
The element at end_index
is not included in the slice.
Remember that dynamic arrays are just structures with a pointer to the first element and a length value. Taking a slice of a dynamic array simply creates a new one of those pointer structures that points to the elements of that same array.
string a = "the entire part of the array";
string b = a[11 .. $]; // b = "part of the array"
// b points to the last 17 elements of a
// If you modify individual elements of b, a will also
// change since they point to the same underlying array!
Three Ways to Do the Same Thing
[edit | edit source]Notice that the $
is automatically replaced with the length of the array being sliced from. The following three lines are equivalent and both create slices of the entirety of an array arr
.
char[] a = arr[0 .. $];
char[] a = arr[0 .. arr.length];
char[] a = arr[]; // shorthand for the above
A Visual Representation
[edit | edit source]The .capacity
Property
[edit | edit source]All dynamic arrays in D have a .capacity
property. It is the maximum number of elements that can be appended to that array without having to move the array somewhere else (reallocation).
int[] a = [1,2,3,45];
writeln("Ptr: ", a.ptr);
writeln("Capacity: ", a.capacity);
a.length = a.capacity; // the array reaches maximum length
writeln("Ptr: ", a.ptr, "\nCapacity: ", a.capacity); // Still the same
a ~= 1; // array has exceeded its capacity
// it has either been moved to a spot in memory with more space
// or the memory space has been extended
// if the former is true, then a.ptr is changed.
writeln("Capacity: ", a.capacity); // Increased
Maximizing Efficiency by Only Reallocating When Necessary
[edit | edit source]It is good for efficiency to make sure that appending and concatenation do not cause too many reallocations because it is an expensive procedure to reallocate a dynamic array. The following code may reallocate up to 5 times:
int[] a = [];
a ~= new int[10];
a ~= [1,2,3,4,5,6,7,8,9];
a ~= a;
a ~= new int[20];
a ~= new int[30];
Make sure that the array capacity
is big enough at the beginning to allow for efficient non-reallocating array appends and concatenations later if performance is a concern.
You can't modify the .capacity
property. You are only allowed to either modify the length, or use the reserve
function.
int[] a = [1,2,3,45];
a.reserve(10); // if a's capacity is more than 10, nothing is done
// else a is reallocated so that it has a capacity of at least 10
When Passed to Functions
[edit | edit source]Remember that D's arrays are passed to functions by value. When static arrays are passed, the entire array is copied. When a dynamic array is passed, only that structure with the pointer to the underlying array and the length is copied - the underlying array is not copied.
import std.stdio;
int[] a = [1,2,3];
void function1(int[] arr)
{
assert(arr.ptr == a.ptr); // They are the same
// But the arr is not the same as a
// If arr's .length is modified, a is unchanged.
// both arr and a's .ptr refer to the same underlying array
// so if you wrote: arr[0] = 0;
// both arr and a would show the change, because they are both
// references to the same array.
// what if you forced arr to reallocate?
arr.length = 200; // most likely will reallocate
// now arr and a refer to different arrays
// a refers to the original one, but
// arr refers to the array that's reallocated to a new spot
arr[0] = 0;
writeln(arr[0]); // 0
writeln(a[0]); // 1
}
void main()
{
function1(a);
}
As you can see, there are a few possibilities if you pass a dynamic array to a function that looks like this:
void f(int[] arr)
{
arr.length = arr.length + 10;
arr[0] += 10;
}
- First possibility: the array's capacity was large enough to accommodate the resizing, so no reallocation occurred. The original underlying array's first element was modified.
- Second possibility: the array's capacity was not large enough to accommodate the resizing, but D's memory management was able to extend the memory space without copying the entire array. The original underlying array's first element was modified.
- Third possibility: the array's capacity was not large enough to accommodate the resizing. D's memory management had to reallocate the underlying array into a completely new space in memory. The original underlying array's first element was not modified.
What if you want to make sure that the following works?
int[] a = [0,0,0];
f(a);
assert(a[0] == 10);
Simply change the function f
so that dynamic arrays are passed by reference:
void f(ref int[] arr)
{
arr.length = arr.length + 10;
arr[0] += 10;
}
Appending to Slices
[edit | edit source]When you take a slice of a dynamic array, and then append to that slice, whether the slice is reallocated depends on where the slice ends. If the slice ends in the middle of the data from the original array, then appending to that slice would cause a reallocation.
int[] a = [1,2,3,4];
auto b = a[1 .. 3];
writeln(b.capacity); // 0
// b cannot possibly be appended
// without overwriting elements of a
// therefore, its capacity is 0
// any append would cause reallocation
Lets say you took a slice of a dynamic array, and that slice ends at where the dynamic array ends. What if you appended to the dynamic array so that the slice no longer ends at where the dynamic array's data ends?
int[] a = [1,2,3,4];
writeln(a.capacity); // 7
auto b = a[1 .. 4];
writeln(b.capacity); // 6
a ~= 5; // whoops!
// now the slice b does *not* end at the end of a
writeln(a.capacity); // 7
writeln(b.capacity); // 0
The .capacity
property of a slice does indeed depend on other references to the same data.
Assignment-To-Slices
[edit | edit source]An Assignment-To-Slice looks like this:
a[0 .. 10] = b
You are assigning b
to a slice of a
You have actually seen Assignment-to-Slices in the last two lessons, even before you learned about slices. Remember this?
int[] a = [1,2,3];
a[] = 3;
Remember that a[]
is shorthand for a[0 .. $]
When you assign a int[]
slice to a single int
value, that int
value is assigned to all the elements within that slice.
An Assignment-To-Slice always causes data to be copied.
int[4] a = [0,0,0,0];
int[] b = new int[4];
b[] = a; // Assigning an array to a slice
// this guarantees array-copying
a[0] = 10000;
writeln(b[0]); // still 0
Beware! Whenever you use Assignment-To-Slice, the left and right sides' .length
values must match! If not, there will be a runtime error!
int[] a = new int[1];
a[] = [4,4,4,4]; // Runtime error!
You also must make sure that the left and right slices do not overlap.
int[] s = [1,2,3,4,5];
s[0 .. 3] = s[1 .. 4]; // Runtime error! Overlapping Array Copy
Vector Operations
[edit | edit source]Let's say you wanted to double each and every integer element of an array. Using D's vector operation syntax, you can write any of these:
int[] a = [1,2,3,4];
a[] = a[] * 2; // each element in the slice is multiplied by 2
a[0 .. $] = a[0 .. $] * 2; // more explicit
a[] *= 2 // same thing
Likewise, if you wanted to perform this operation: [1, 2, 3, 4] (int[] a) + [3, 1, 3, 1] (int[] b) = [4, 3, 6, 5] You would write this:
int[] a = [1, 2, 3, 4];
int[] b = [3, 1, 3, 1];
a[] += b[]; // same as a[] = a[] + b[];
Just like for Assignment-To-Slice, you have to make sure the left and right sides of vector operations have matching lengths, and that the slices do not overlap. If you fail to follow that rule, the result is undefined (There would be neither a runtime error nor a compile-time error).
Defining Array Properties
[edit | edit source]You can define your own array properties by writing functions in which the first argument is an array.
void foo(int[] a, int b)
{
// do stuff
}
void eggs(int[] a)
{
// do stuff
}
void main()
{
int[] a;
foo(a, 1);
a.foo(1); // means the same thing
eggs(a);
a.eggs; // you can omit the parentheses
// (only when there are no arguments)
}
Tips
[edit | edit source]- Steven Schveighoffer's article "D Slices" is an excellent resource if you want to learn more.
d2/Lesson 10
Lesson 10: Conditionals and Loops
[edit | edit source]Conditionals and loops are essential for writing D programs.
Introductory Code
[edit | edit source]Palindrome Checker
[edit | edit source]module palindromes;
import std.stdio;
bool isPalindrome(string s)
{
int length = s.length;
int limit = length / 2;
for (int i = 0; i < limit; ++i)
{
if (s[i] != s[$ - 1 - i])
{
return false;
}
}
return true;
}
void main()
{
string[] examples = ["", "hannah", "socks", "2002", ">><<>>", "lobster"];
foreach(e; examples)
{
if(!e.length) continue; // skip that empty string
if(isPalindrome(e))
writeln(e, " is an example of a palindrome.");
else
writeln(e, " is an example of what's not a palindrome.");
}
while(true)
{
write("Type any word: ");
string input = readln();
if(input.length <= 1) // length == 1 means input == "\n"
break; // nothing was typed
input = input[0 .. $-1]; // strip the newline
if(isPalindrome(input))
writeln(input, " is a palindrome.");
else
writeln(input, " is not a palindrome.");
}
}
More Conditionals and Branching
[edit | edit source]import std.stdio;
string analyzeHoursOfSleep(int hours)
{
if(!hours) return "You didn't sleep at all.";
string msg = "";
switch(hours)
{
case 1,2,3:
msg ~= "You slept way too little! ";
goto case 7;
case 4: .. case 6:
msg ~= "Take a nap later to increase alertness. ";
case 7:
msg ~= "Try to go back to sleep for a bit more. ";
break;
default:
msg ~= "Good morning. Grab a cup of coffee. ";
}
return msg ~ '\n';
}
void main()
{
writeln(analyzeHoursOfSleep(3));
writeln(analyzeHoursOfSleep(6));
writeln(analyzeHoursOfSleep(7));
writeln(analyzeHoursOfSleep(13));
int i = 0;
L1: while(true)
{
while(true)
{
if(i == 3)
break L1;
i++;
break;
}
writeln("Still not out of the loop!");
}
}
/*
Output:
You slept way too little! Try to go back to sleep for a bit more.
Take a nap later to increase alertness. Try to go back to sleep for a bit more.
Try to go back to sleep for a bit more.
Good morning. Grab a cup of coffee.
Still not out of the loop!
Still not out of the loop!
Still not out of the loop!
*/
Concepts
[edit | edit source]The if
and else
Statements
[edit | edit source]Using if
allows you to make part of your code only execute if a certain condition is met.
if(condition that evaluates to true or false) { // code that is executed if condition is true } else { // code that is executed if condition is false }
In fact, if the section of code that's inside the if or else is only one line long, you can omit the curly brackets.
if(condition1) do_this(); else if(condition2) do_that(); // only executed if condition1 is false, but // condition2 is true else do_the_other_thing(); // only executed if both condition1 and condition2 are false
As a result, this is often seen:
if (condition1) { do_something1(); something_more1(); } else if(condition2) { do_something2(); something_more2(); } else if(condition3) { do_something3(); something_more3(); } else if(condition4) { do_something4(); something_more4(); } else { do_something_else(); }
The Condition
[edit | edit source]The condition that goes inside of the parentheses in conditional statements such as if
can be anything convertible to bool
. That includes integral and floating-point types (true
if nonzero, false
if otherwise) and pointers (null
is false
, and dynamic arrays (always true
).
The while
Loop
[edit | edit source]A while loop will allow you to repeat a block of code as long as a certain condition is met. There are two forms of the while loop:
while(condition1) { do_this(); }
and
do { do_this(); } while(condition1)
The difference is, in the first example, if condition1 is false, do_this
is never called, while in the second example it would be called once (the conditional check happens after the code is executed once).
The foreach
Loop
[edit | edit source]This loop is for iteration.
Take a look at these two ways to use foreach
:
foreach(i; [1,2,3,4]) {
writeln(i);
}
foreach(i; 1 .. 5) { writeln(i); } // equivalent to above
The for
Loop
[edit | edit source]This type of looping is the most complex, but it is also the one that gives a lot of control. It is defined in the same way as other C-like languages:
for(initialization; condition; counting expression) { ... }
The initialization expression is executed only once during the beginning. Then condition is checked to be true
or false
. If it is true
, the code inside of the conditional block (inside of the brackets) is executed. After that execution, the counting expression is executed. Then, the condition is checked, and if it is true
, the loop continues. For example:
for(int i=0; i <= 5; i++)
{
write(i);
}
// output: 012345
You can even omit parts of what goes inside the parentheses of the for
. These two are equivalent:
for(int i=0; i==0; ) {
i = do_something();
}
int i = 0;
while(i == 0) {
i = do_something();
}
break
and continue
[edit | edit source]These are two statements that are used inside of loops.
The break
statement breaks out of the loop. Whenever the break
statement is encountered, the loop is immediately exited. This statement can go inside of while
, for
, foreach
, and switch
blocks (you will learn about those later).
The continue
statement causes a loop to restart at the beginning. Let's see, through code, exactly how this works. This code example counts to 7 but skips 5.
for(int i = 0; i <= 7; i++)
{
if(i == 5) continue;
writeln(i);
}
Switches and More
[edit | edit source]D allows absolute branching with labels and goto
.
int i = 0;
looper: // this is a label
write(i);
i++;
if(i < 10) goto looper;
writeln("Done!");
// 0123456789Done!
Do not use these unless if you have to. Code that uses labels can most often be written with more readable looping constructs, like for
, while
, and foreach
.
There is something in D, C and C++ called the switch
. D's switches are actually more powerful than C and C++'s switches.
switch(age)
{
case 0,1: // if(age == 0 || age == 1) { ... }
writeln("Infant");
break;
case 2,3,4: // else if (age == 2 || age == 3 || age == 4) { .. }
writeln("Toddler");
break;
case 5: .. case 11:
writeln("Kid");
break;
case 12:
writeln("Almost teen");
break;
case 13: .. case 19:
writeln("Teenager");
break;
default: // else { .. }
writeln("Adult");
}
Note that you must have a break
in order to get out of the switch. Otherwise, fall-through occurs.
Also, you can use goto
.
int a = 3;
switch(a)
{
case 1:
writeln("Hello");
case 2:
writeln("Again");
break;
case 3:
goto case 1;
default:
writeln("Bye");
}
/* output: Hello
Again
*/
Strings can be used in case
. This is a feature that's not in C or C++.
else
[edit | edit source](It doesn't seems to work in recent compilers).
You can use else for foreach
, while
, and for
loops, too. If any of those loops have an else
clause, then the else
is only executed if the loop terminates normally (i.e. not with break
).
int[] arr = [1,2,3,5,6];
foreach(item; arr)
{
if(item == 4) break;
}
else
{
writeln("No four found.");
}
//Output: No four found.