What would your apps be without data? Think about it for a minute. Without data, your programs are... well, it’s actually hard to imagine writing code without data. You need information from your users, and you use that to look up or produce new information to give back to them. In fact, almost everything you do in programming involves working with data in one way or another. In this chapter, you’ll learn the ins and outs of C#’s data types and references, see how to work with data in your program, and even learn a few more things about objects (guess what...objects are data, too!).
Ryan is a game master—a really good one. He hosts a group that meets at his place every week to play different role-playing games (or RPGs), and like any good game master, he really works hard to keep things interesting for the players.
Ryan is a particularly good storyteller, and over the last few months he’s created an intricate fantasy world for his party. But he’s not so happy with the mechanics of the game that they’ve been playing. Can we find a way to help Ryan improve his RPG?
If you’ve ever played an RPG, you’ve seen character sheets: a page with details, statistics, background information, and any other notes you’ll see about a character. If you wanted to make a class to hold a character sheet, what types would you use for the fields?
There are many types built into C#, and you’ll use them to store many different kinds of data. You’ve already seen some of the most common ones, like int, string, bool, and float. But there are a few that you haven’t seen, and they can really come in handy, too.
Here are some types you’ll use a lot.
int
can store any whole number from –2,147,483,648 to 2,147,483,647. Whole numbers don’t have decimal points.
float
can store real numbers from ±1.5 × 10–45 to ±3.4 × 1038 with up to 8 significant figures.
string
can hold text of any length (including the empty string ""
).
double
can store real numbers from ±5.0 × 10–324 to ±1.7 × 10308 with up to 16 significant figures. Double is really common when you’re working with XAML properties.
bool
is a Boolean value—it’s either true
or false
. You’ll use it to represent anything that only has two options: it can either be one thing or another, but nothing else.
C# has several different types for whole numbers. This may seem a little weird. Why have so many types for numbers without decimals? For most of the programs in this book, it won’t matter if we use an int or a long. But if you’re writing a program that has to keep track of millions and millions of whole number values, then choosing a smaller whole number type like byte instead of a bigger type like long can save you a lot of memory.
byte
can store any whole number between 0 and 255.
sbyte
can store any whole number from –128 to 127.
short
can store any whole number from –32,768 to 32,767.
long
can store any whole number from -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807.
Did you notice that byte only stores positive numbers, while sbyte stores negative numbers? They both have 256 possible values. The difference is that like short and long, sbyte can have a negative sign—which is why those are called signed types, and the “s” in sbyte stands for signed. And just like byte is the unsigned version of sbyte, there are unsigned versions of short, int, and long that start with “u”:
ushort
can store any whole number from 0 to 65,535.
uint
can store any whole number from 0 to 4,294,967,295.
ulong
can store any whole number from 0 to 18,446,744,073,709,551,615.
Sometimes seven significant figures just isn’t precise enough. And, believe it or not, sometimes 1038 isn’t big enough and 10–45 isn’t small enough. A lot of programs written for finance or scientific research run into these problems all the time, so C# gives us multiple floating-point types to handle huge and tiny values:
float
can store any number from ±1.5 × 10–45 to ±3.4 × 1038 with 7 significant digits.
double
can store any number from ±5.0 × 10–324 to ±1.7 × 10308 with 15-16 significant digits.
decimal
can store any number from ±1.0 × 10–28 to ±7.9 × 1028 with 28–29 significant digits. When your program needs to deal with money or currency, you always want to use a decimal to store the number.
The decimal type has a lot more precision (way more significant digits) which is why it’s appropriate for financial calculations.
You’ve written code that works with strings. But what, exactly, is a string?
In any .NET app, a string is an object. Its full class name is System.String—in other words, the class name is String and it’s in the System namespace (just like Random class you used). When you use the C# string
keyword, you’re working with System.String objects. In fact, you can replace string
with System.String
in any of the code you’ve written so far and it will still work! (The string
keyword is called an alias—as far as your C# code is concerned, string
and System.String
mean the same thing.)
There are also two special values for strings: an empty string ""
(or a string with no characters), and a null
string, or a string that isn’t set to anything at all. We’ll talk more about null
later in the chapter.
Strings are made up of characters—specifically, Unicode characters (which we’ll learn a lot more about later in the book). Sometimes you need to store a single character like Q
or j
or $
, and when you do you’ll use the char
type. Literal values for char
are always inside single quotes (’x’
, ’3’
). You can include escape sequences in the quotes, too (’\n’
is a line break, ’\t’
is a tab). You can write an escape sequence in your C# code using two characters, but your program stores each escape sequence as a single character in memory.
And finally, there’s one more important type: object
. If a variable has object as its type, you can assign any value to it. The object
keyword is also an alias—it’s the same as System.Object
.
A literal is a number, string, or other fixed value that you include in your code. You’ve already used plenty of literals—here are some examples of numbers, strings, and other literals that you’ve used:
So when you type “int i = 5;”, the 5 is a literal.
When you added statements like this in Unity, you may have wondered about the F
:
InvokeRepeating("AddABall", 1.5F, 1);
C# assumes that a whole number literal without a suffix (like 371) is an int, and one with a decimal point (like 27.4) is a double.
Did you notice that your program won’t build if you leave off the F in the literals 1.5F and 0.75F? That’s because literals have types. Every literal is automatically assigned a type, and C# has rules about how you can combine different types. You can see for yourself how that works. Add this line to any C# program:
int wholeNumber = 14.7;
When you try to build your program, the IDE will show you this error in the error list:
The IDE is telling you is that the literal 14.7
has a type—it’s a double
. You can use a suffix to change its type—try changing it to a float
by sticking an F
on the end (14.7F
) or a decimal by adding M
(14.7M
—the M actually stands for “money”). The error message now says it can’t convert float or decimal. Add a D
(or leave off the suffix entirely) and the error goes away.
All of your data takes up space in memory. (Remember the heap from the last chapter?) So part of your job is to think about how much space you’re going to need whenever you use a string or a number in your program. That’s one of the reasons you use variables. They let you set aside enough space in memory to store your data.
Not all data ends up on the heap. Value types usually keep their data in another part of memory called the stack. You’ll learn all about that later in the book.
Think of a variable like a cup that you keep your data in. C# uses a bunch of different kinds of cups to hold different kinds of data. And just like the different sizes of cups at the coffee shop, there are different sizes of variables, too.
Convert this!
You’ve always heard that programming is about 1’s and 0’s. .NET has a static Convert class in .NET that converts between different numeric data types. Let’s use it to see an example of how the bytes and bytes work with whole numbers.
A bit is a single 1 or 0. A byte is 8 bits, so a byte variable holds an 8-bit number, which means it’s a number that can be represented with up to 8 bits. What does that look like? Let’s use the Convert class convert binary numbers to bytes:
Bytes can hold numbers between 0 and 255 because it uses 8 bits of memory—an 8-bit number is a binary number between 0 and 11111111 binary (or 0 and 255 decimal).
A short is a 16-bit value. Let’s use Convert.ToInt16 convert the binary value 111111111111111 (15 1’s) to a short. And an int is a 32-bit value, so we’ll :use Convert.ToInt32 to convert the 31 1’s to an int:
Convert.ToInt16("111111111111111", 2); // returns 32767 Convert.ToInt32("1111111111111111111111111111111", 2); // returns 2147483647
Numbers that have decimal places are stored differently than whole numbers, and the different floating-point types take up different amounts of memory. You can handle most of your numbers that have decimal places using float, the smallest data type that stores decimals. If you need to be more precise, use a double. And if you’re writing a financial application where you’ll be storing currency values, you’ll always want to use the decimal type.
Oh, and one more thing: don’t use double for money or currency, only use decimal.
We’ve talked about strings, so you know that the C# compiler also can handle characters and non-numeric types. The char type holds one character, and string is used for lots of characters “strung” together. There’s no set size for a string object, either. It expands to hold as much data as you need to store in it. The bool data type is used to store true or false values, like the ones you’ve used for your if statements.
The different floating-point types take up different amounts of memory: float is smallest, and double is largest.
When you declare your variable as one type, the C# compiler allocates (or reserves) all of the memory it would need to store the maximum value of that type. Even if the value is nowhere near the upper boundary of the type you’ve declared, the compiler will see the cup it’s in, not the number inside. So this won’t work:
int leaguesUnderTheSea = 20000; short smallerLeagues = leaguesUnderTheSea;
20,000 would fit into a short
, no problem. But since leaguesUnderTheSea
is declared as an int, C#
sees it as int-
sized and considers it too big to put in a short
container. But the compiler won’t make those translations for you on the fly. You need to make sure that you’re using the right type for the data you’re working with.
Let’s see what happens when you try to assign a decimal value to an int variable.
Do this!
Create a new Console App project and add this code to the Main method:
Try building your program. You should get the same CS0266 error you saw earlier:
Look closely at the last few words of the error message: “are you missing a cast?” That’s the C# compiler giving you a really useful hint about how to fix the problem.
Make the error go away by casting the decimal to an int. You do this by adding the type that you want to convert to in parentheses: (int)
. Once you change the second line so it looks like this, your program will compile and run:
The C# compiler won’t let you assign a value to a variable if it’s the wrong type—even if that variable can hold the value just fine! It turns out that a LOT of bugs are caused by type problems, and the compiler is helping by nudging you in the right direction. When you use casting, you’re essentially making a promise to the compiler that you know the types are different, and that in this particular instance it’s OK for C# to cram the data into the new variable.
You’ve already seen that a decimal can be cast to an int
. It turns out that any number can be cast to any other number. But that doesn’t mean the value stays intact through the casting. If you cast an int variable that’s set to 365 to a byte variable, 365 is too big for the byte
. But instead of giving you an error, the value will just wrap around: for example, 256 cast to a byte will have a value of 0. 257 would be converted to 1, 258 to 2, etc., up to 365, which will end up being 109. And once you get back to 255 again, the conversion value “wraps” back to zero.
If you use + (or *
, /
, or -
) with two different numeric types, the operator automatically converts the smaller type to the bigger one. Here’s an example:
int myInt = 36; float myFloat = 16.4F; myFloat = myInt + myFloat;
Since an int
can fit into a float
but a float
can’t fit into an int
, the + operator converts myInt
to a float
before adding it to myFloat
.
Yes! When you concatenate strings, C# converts values.
When you use the + operator to combine a string with another value, it’s called concatenation. And when you concatenate a string with an int, bool, float, or another value type, it automatically converts the value. But this kind of conversion is different from casting, because under the hood it’s really calling the ToString method for the value... and one thing that .NET guarantees is that every object has a ToString method that converts it to a string (but it’s up to the individual class to determine if that string makes sense.)
There are two important conversions that don’t require you to do casting. The first is the automatic conversion that happens any time you use arithmetic operators, like in this example:
The other way C# converts types for you automatically is when you use the + operator to concatenate strings (which just means sticking one string on the end of another, like you’ve been doing with message boxes). When you use + to concatenate a string with something that’s another type, it automatically converts the numbers to strings for you. Here’s an example—try adding these lines to any C# program. The first two lines are fine, but the third one won’t compile.
long number = 139401930; string text = "Player score: " + number; text = number;
The C# compiler gives you this error on the third line:
ScoreText.text is a string field, so when you used the + operator to concatenate a string it assigned the value just fine. But when you try to assign x to it directly, it doesn’t have a way to automatically convert the long value to a string. But you can convert it to a string really easily by calling its ToString method.
In the last chapter, you used the Random class to choose a random number from 1 up to (but not including) 5, and used it to pick a suit for a playing card:
int value = random.Next(1, 5);
Try changing the first argument from 1 to 1.0:
int value = random.Next(1.0, 5);
You’re passing a double literal to a method that’s expecting an int value. So it shouldn’t surprise you that the compiler won’t build your program—instead, it shows an error:
When the compiler gives you an “invalid argument” error, it means that you tried to call a method with variables whose types didn’t match the method’s parameters.
Sometimes C# can do the conversion automatically. It doesn’t know how to convert a double to an int (like converting 1.0 to 1), but it does know how to convert an int to a double (like converting 1 to 1.0).
The C# compiler knows how convert a whole number to a floating-point.
And it knows how to convert a whole number type to another whole number type, or a floating-point type to another floating-point type.
But it can only do those conversions if the type it’s converting from is the same size or smaller than the type it’s converting to. So it can convert an int to a long or a float to a double, but it can’t convert a long to an int or a double to a float.
But Random.Next isn’t the only method that will give you compiler errors if you try to pass it a variable whose type doesn’t match the parameter. All methods will do that, even the ones you write yourself. Add this method to a Console App:
public int MyMethod(bool add3) { int value = 12; if (add3) value += 3; else value -= 2; return value; }
Try passing it a string or long—you’ll get one of those CS1503 errors telling you it can’t convert the argument to a bool. And by the way, a lot of people have trouble remembering the difference between parameter and argument. So just to be clear:
A parameter is what you define in your method. An argument is what you pass to it. You can pass a byte argument to a method with an int parameter.
Good game masters are dedicated to creating the best experience they can for their players. Ryan’s players are about to embark on a new campaign with a brand new set of characters, and he thinks a few tweaks to the formula that they use for their ability scores could make things more interesting.
Ryan’s been experimenting with ways to tweak the ability score calculation. He’s pretty sure that he has the formula mostly right—but he’d really like to tweak the numbers.
Ryan likes the overall formula: 4d6 roll, divide, subtract, round down, use a minimum value. But he’s not sure that the actual numbers are right.
You’re right, Ryan. There’s a bug in the code.
Ryan wants to try out different values to use in his ability score formula, so we used a loop to make the app ask for those values over and over again.
To make it easier for Ryan to just change one value at a time, we included a feature in the app that remembers the last values he entered and presents them as default options. We implemented that feature by keeping an instance of the AbilityScoreCalculator class in memory, and updating its fields in each iteration of the while loop.
But something’s gone wrong with the app. It remembers most of the values just fine, but it remembers the wrong number for the “add amount” default value. In the first iteration Ryan entered 5, but it gave him 10 as a default option. He entered 7, but it says he default 12. What’s going on?
Now that you know what’s happening, you can fix the bug—and it turns out to be a pretty small change. You just need to change the statement to use + instead of +=:
Try this!
Try adding this if/else statement to a console app:
if (0.1M + 0.2M == 0.3M) Console.WriteLine("They’re equal"); else Console.WriteLine("They aren’t equal");
You’ll see a green squiggle under the second Console
— it’s an Unreachable code detected
warning. The C# compiler knows that 0.1 + 0.2 is always equal to 0.3, so the code will never reach the else
part of the statement. Run the code—it prints They’re equal
to the console.
Next, change the float literals to doubles (remember, literals like 0.1 default to double):
if (0.1 + 0.2 == 0.3) Console.WriteLine("They’re equal"); else Console.WriteLine("They aren’t equal");
Strange. The warning moved to the first line of the if
statement. Try running the program. Hold on, that can’t be right! It printed They aren’t equal
to the console. How is 0.1 + 0.2 not equal to 0.3?
Now do one more thing. Change 0.3 to 0.30000000000000004 (with 15 zeroes between the 3 and 4.). Now it prints They’re equal
again. So apparently 0.1D plus 0.2D equals 0.30000000000000004D.
Wait what?!
Exactly. Decimal has a lot more precision than double or float, so it avoids the 0.30000000000000004 problem.
Some floating-point types—not just in C#, but in most programming languages!—can give you rare weird errors. This is so strange! How can 0.1 + 0.2 be 0.30000000000000004?
It turns out that there are some numbers that just can’t be exactly represented as a double—it has to do with how they’re stored as binary data (0s and 1s in memory). For example, .1D
is not exactly .1. Try multiplying .1D * .1D
— you get 0.010000000000000002, not 0.01. But .1M * .1M
gives you the right answer. That’s why floats and doubles are really useful for a lot of things (like positioning a GameObject in Unity). But if you need more rigid precision— like for a financial app that deals with money—decimal is the way to go.
The 0.1D + 0.2D != 0.3D example is an edge case, or a problem or situation that only happens under certain rare conditions, usually when a parameter is at one of its extremes (like a very big or very small number). If you want to learn more about it, read this great article by Jon Skeet about how floating-point numbers are stored in memory in .NET. You can read it here: https://csharpindepth.com/Articles/FloatingPoint
Jon gave us some amazing technical review feedback for the 1st edition of this book that made a huge difference for us. Thanks so much, Jon!
When you create a new object, you use a new
statement to instantiate, like new Guy()
in your program the end of the last chapter—the new
statement created a new Guy object on the heap. But you still needed a way to access that object, and that’s where a variable like joe
came in: Guy joe = new Guy()
. Let’s dig a little deeper into exactly what’s going on there.
The new statement creates the instance, but just creating that instance isn’t enough. You need a reference to the object. So you created a reference variable: a variable of type Guy with a name, like joe
. So joe
is a reference to the new Guy object you created. Any time you want to use that particular guy, you can reference it with the reference variable called joe
.
When you have a variable that’s an object type, it’s a reference variable: a reference to a particular object. Let’s just make sure we get the terminology right since we’ll be using it a lot. We’ll use the first two lines of the “Joe and Bob” program from the last chapter:
In your kitchen, you probably have containers of salt and sugar. If you switched their labels, it would make for a pretty disgusting meal— even though the labels changed, the contents of the containers stayed the same. References are like labels. You can move labels around and point them at different things, but it’s the object that dictates what methods and data are available, not the reference itself—and you can copy references just like you copy values.
A reference is like a label that your code uses to talk about a specific object. You use it to access fields and call methods on an object that it points to.
We stuck a lot of sticky notes on that object! In this particular case, there are a lot of different references to this same Guy object—because a lot of different methods use him for different things Each reference has a different name that makes sense in its context.
That’s why it can be really useful to have multiple references pointing to the same instance. So you could say Guy dad = joe
, and then call dad.GiveCash()
(that’s what Joe’s kid does every day). But if you want to write code that works with an object, you need a reference to that object. If you don’t have that reference, you have no way to access the object.
If all of the labels come off of an object, programs can no longer access that object. That means C# can mark the object for garbage collection. That’s when C# gets rid of any unreferenced objects and reclaims the memory those objects took up for your program’s use.
Here’s some code that creates an object.
And just to recap what we’ve been talking about: when you use the new
statement, you’re telling C# to create an object. When you take a reference variable like joe
and assign it to that object, it’s like you’re slapping a new sticky note on it.
Guy joe = new Guy() { Cash = 50, Name = "Joe" };
Now let’s create our second object.
Now we’ll have two Guy object instances and two reference variables: one variable (joe
) for the first Guy object, and another variable (bob
) for the second.
Guy bob = new Guy() { Cash = 100, Name = "Bob" };
Let’s take the reference to the first Guy object and change it to point to the second Guy object.
Take a really close look at what you’re doing when you create a new Guy object. You’re taking a variable and using the = assignment operator to set it—in this case, to a reference that’s returned by the new
statement. That assignment works because you can copy a reference just like you copy a value.
So let’s go ahead and copy that value
joe = bob;
That tells C# to take make joe
point to the same object that bob
does. Now the joe
and bob
variables both point to the same object.
there’s no longer a reference to the first Guy object... so it gets garbage-collected.
Now that joe
is pointing to the same object as bob
, there’s no longer a reference to the Guy object it used to point to. So what happens? C# marks the object for garbage collection, and eventually trashes it. Poof—it’s gone!
For an object to stay in the heap, it has to be referenced. Some time after the last reference to the object disappears, so does the object.
you can pet the dog in head first c#
public partial class Dog { public void GetPet() { Console.WriteLine("Woof!"); } }
You’ve got to be careful when you start moving around reference variables. Lots of times, it might seem like you’re simply pointing a variable to a different object. But you could end up removing all references to another object in the process. That’s not a bad thing, but it may not be what you intended. Take a look:
Dog rover = new Dog(); rover.Breed = "Greyhound";
Objects: 1
References: 1
Dog fido = new Dog(); fido.Breed = "Beagle"; Dog spot = rover;
Objects: 2
References: 3
Dog lucky = new Dog(); lucky.Breed = "Dachshund"; fido = rover;
Objects: 2
References: 4
Besides losing all the references to an object, when you have multiple references to an object, you can unintentionally change an object. In other words, one reference to an object may change that object, while another reference to that object has no idea that something has changed. Let’s see how that works.
Do this!
Add one more “else if ” block to your Main method. Can you guess what will happen once it runs?
Now go ahead and run your program. Here’s what you’ll see:
You pressed 4 My name is Lucinda My ears are 4321 inches tall. You pressed 1 Calling lloyd.WhoAmI() My name is Lucinda My ears are 4321 inches tall. You pressed 2 Calling lucinda.WhoAmI() My name is Lucinda My ears are 4321 inches tall.
After you press 4 and run the new code that you added, both the lloyd and lucinda variables contain the same reference to the second Elephant object. pressing 1 to call lloyd.WhoAmI prints exactly the same message as pressing 2 to call lucinda.WhoAmI. And swapping them makes no difference because you’re swapping two identical references.
So far, you’ve seen forms talk to objects by using reference variables to call their methods and check their fields. Objects can call one another’s methods using references, too. In fact, there’s nothing that a form can do that your objects can’t do, because your form is just another object. And when objects talk to each other, one useful keyword that they have is this
. Any time an object uses the this
keyword, it’s referring to itself—it’s a reference that points to the object that calls it. Let’s see what that looks like by modifying the Elephant class so instances can call each others’ methods.
Add a method that lets an elephant hear a message.
Let’s add a method to the Elephant class. Its first parameter is a message from another Elephant object. Its second parameter is the Elephant object that sent the message.
Do this!
public void HearMessage(string message, Elephant whoSaidIt) { Console.WriteLine(Name + " heard a message"); Console.WriteLine(whoSaidIt.Name + " said this: " + message); }
Here’s what it looks like when it’s called:
lloyd.HearMessage("Hi", lucinda);
We called Lloyd’s TellMe method, and passed it two parameters: the string "Hi"
and a reference to Lucinda’s object. The method uses its whoSaidIt parameter to access the Name parameter of whatever elephant was passed into TellMe using its second parameter.
Add a method that lets an elephant send a message.
Now let’s add this SpeakTos method to the Elephant class. It uses a special keyword: this.
That’s a reference that lets an object get a reference to itself.
Let’s take a closer look at what’s going on.
When we call the Lucinda object’s SpeakTo method:
lucinda.SpeakTo(lloyd, "Hi, Lloyd!");
It calls the Lloyd object’s HearMessage method like this:
Call the new methods.
Add one more else if block to the Main method to make the Lucinda object send a message to the Lloyd object:
else if (input == ’4’) { lloyd = lucinda; lloyd.EarSize = 4321; lloyd.WhoAmI(); } else if (input == ’5’) { lucinda.SpeakTo(lloyd, "Hi, Lloyd!"); } else { return; }
The “this” keyword lets an object get a reference to itself.
Now run your program and press 5. You should see this output:
You pressed 5 Lloyd heard a message Lucinda said this: Hi, Lloyd!
Use the debugger to understand what’s going on.
Place a breakpoint on the statement that you just added to the Main method:
Run your program and press 5.
When it hits the breakpoint, use Debug >> Step Into (F11) to step into the Speak To method.
Add a watch for Name to show you which Elephant object you’re inside. You’re currently inside the Lucinda object—which makes sense because the Main method called Lucinda.SpeakTo.
Hover over the this
keyword at the end of the line and expand it. It’s a reference to the Lucinda object.
Hover over whoToTalkTo
and expand it—it’s a reference to the Lloyd object.
The SpeakTo method has one statement—it calls whoToTalkTo.HearMessage. Step into it.
You should now be inside the HearMessage method. Check your watch again—now the value of the Name field is “Lloyd”—the Lucinda object called the Lloyd object’s HearMessage method.
Hover over whoSaidIt
and expand it. It’s a reference to the Lucinda object.
Finish stepping through the code. Take a few minutes and really understand what’s going on.
Strings and arrays are different from the other data types you’ve seen in this chapter because they’re the only ones without a set size (think about that for a bit).
If you have to keep track of a lot of data of the same type, like a list of prices or a group of dogs, you can do it in an array. What makes an array special is that it’s a group of variables that’s treated as one object. An array gives you a way of storing and changing more than one piece of data without having to keep track of each variable individually. When you create an array, you declare it just like any other variable, with a name and a type—except the type is followed by square brackets:
bool[] myArray;
Use the new keyword to create an array. Let’s create an array with 15 bool elements:
myArray = new bool[15];
Use square brackets to set one of the values in the array. This statement sets the value of the fifth element of myArray to true by using square brackets and specifying the index 4. It’s the fifth one because the first is myArray[0], the second is myArray[1], etc.:
myArray[4] = false;
You use the new
keyword to create an array because it’s an object—so an array variable is a kind of reference variable. In C#, arrays are zero-based, which means the first element has index zero.
When you use an array, first you need to declare a reference variable that points to the array. Then you need to create the array object using the new
statement, specifying how big you want the array to be. Then you can set the elements in the array. Here’s an example of code that declares and fills up an array—and what’s happening on the heap when you do it. The first element in the array has an index of zero.
The prices
variable is a reference, just like any other object reference. The object it points to is an array of decimals values, all in one chunk on the heap.
// declare a new 7-element decimal array decimal[] prices = new decimal[7]; prices[0] = 12.37M; prices[1] = 6_193.70M; // we didn’t set the element // at index 2, it remains // the default value of 0 prices[3] = 1193.60M; prices[4] = 58_000_000_000M; prices[5] = 72.19M; prices[6] = 74.8M;
You can create an array of object references just like you create an array of numbers or strings. Arrays don’t care what type of variable they store; it’s up to you. So you can have an array of ints, or an array of Duck objects, with no problem.
When you set or retrieve an element from an array, the number inside the brackets is called the index. The first element in the array has an index of zero.
Here’s code that creates an array of seven Dog variables. The line that initializes the array only creates reference variables. Since there are only two new Dog() lines, only two actual instances of the Dog class are created.
The first line of code only created the array, not the instances. The array is a list of seven Dog reference variables—but only two Dog objects have been created.
There’s another important keyword that you’ll use with objects. When you create a new reference and don’t set it to anything, it has a value. It starts off set to null
, which means it’s not pointing to any object at all. Let’s have a closer look at it:
Yes. The null keyword can be very useful.
There are a few ways you see null
used in typical programs. The most common way is making sure a reference points to an object:
if (lloyd == null) {
That test will return true
if the lloyd
reference is set to null
.
Another way you’ll see the null
keyword used is when you want your object to get garbage-collected. If you’ve got a reference to an object and you’re finished with the object, setting the reference to null
will immediately mark it for collection (unless there’s another reference to it somewhere).
Any time you’ve got code in an object that’s going to be instantiated, the instance can use the special this variable that has a reference to itself.
If you’ve never played Go Fish, take a few minutes and read the rules. We’ll use them later in the book!
Even if we’re not writing code for video games, there’s a lot we can learn from tabletop games.
A lot of our programs depend on random numbers. For example, you’ve already used the Random class to create random numbers for several of your apps. But most of us don’t actually have a lot of real-world experience with genuine random numbers... except when we play games. Rolling dice, shuffling cards, spinning spinners, flipping coins... these are all great examples of random number generators. The Random class is .NET’s random number generator that you’ll use in many of your programs, and your experience using random numbers playing tabletop games will make it a lot easier for you to understand what it does.
You’ll be using the .NET Random class throughout the book, so let’s get to know it better by kicking its tires and taking it for a spin. Fire up Visual Studio and follow along—and make sure you run your code multiple times, since you’ll get different random numbers each time.
Create a new console app—all of this code will go in the Main method. Start by creating a new instance of Random, generating a random int, and writing it to the console.
Random random = new Random(); int randomInt = random.Next(); Console.WriteLine(randomInt);
Specify a maximum value to get random numbers from 0 up to—but not including— the maximum value. A maximum of 10 generates random numbers from 0 to 9.
int zeroToNine = random.Next(10); Console.WriteLine(zeroToNine);
Now simulate a the roll of a die. We can specify a minimum and maximum value. A minimum of 1 and maximum of 7 generates random numbers from 1 to 6.
int dieRoll = random.Next(1, 7); Console.WriteLine(dieRoll);
The NextDouble method generates random double values. Hover over the method name to see a tooltip—it generates a floating-point number from 0.0 up to 1.0.
double randomDouble = random.NextDouble();
You can use multiply a random double to generate much larger random numbers. So if you want a random double value from 1 to 100, multiply the random double by 100.
Console.WriteLine(randomDouble * 100);
Use casting to convert the random double to other types. Try running this code a bunch of times—you’ll see tiny precision differences in the float and decimal values.
Console.WriteLine((float)randomDouble * 100F); Console.WriteLine((decimal)randomDouble * 100M);
Use a maximum value of 2 to simulate a coin toss. That generates a random value of either 0 or 1. Use the special Convert class, which has a static ToBoolean method that will convert it to a boolean value.
int zeroOrOne = random.Next(2); bool coinFlip = Convert.ToBoolean(zeroOrOne); Console.WriteLine(coinFlip);
Sloppy Joe has a pile of meat, a whole lotta bread, and more condiments than you can shake a stick at. But what he doesn’t have is a menu! Can you build a program that makes a new random menu for him every day? You definitely can... with a new WPF app project, some arrays, and a couple of useful new techniques.
Do this!
Start with the fields for a new MenuItem class.
Have a look at the class diagram. It has four fields: an instance of Random and three arrays to hold the various sandwich parts. The array fields use collection initializers, which let you define the items in an array by putting them inside curly braces.
class MenuItem { public Random Randomizer = new Random(); public string[] Proteins = { "Roast beef", "Salami", "Turkey", "Ham", "Pastrami", "Tofu" }; public string[] Condiments = { "yellow mustard", "brown mustard", "honey mustard", "mayo", "relish", "french dressing" }; public string[] Breads = { "rye", "white", "wheat", "pumpernickel", "a roll" }; }
Add the GenerateMenuItem method.
This method uses the same Random.Next method you’ve seen many times to random items from the arrays in the Proteins, Condiments, and Breads fields and concatenate them together into a string.
public string GenerateMenuItem() { string randomProtein = Proteins[Randomizer.Next(Proteins.Length)]; string randomCondiment = Condiments[Randomizer.Next(Condiments.Length)]; string randomBread = Breads[Randomizer.Next(Breads.Length)]; return randomProtein + " with " + randomCondiment + " on " + randomBread; }
Add the RandomPrice method.
This method makes a random price by converting two random ints to decimals to make a random price between 2.01 and 5.97. Have a close look at the last line—it returns price.ToString("c")
. The parameter to the ToString method is a format. In this case, the "c"
format tells ToString to format the value with the local currency: if you’re in the United States you’ll see a $; in the UK you’ll get a £, in the E.U. you’ll see €, etc.
public string RandomPrice() { decimal bucks = Randomizer.Next(2, 5); decimal cents = Randomizer.Next(1, 98); decimal price = bucks + (cents * .01M); return price.ToString("c"); }
Create the XAML to lay out the window.
Your app will display random menu items in a window with two columns, a wide one for the menu item and a narrow one for the price. Each cell in the grid has a TextBlock control with its FontSize set to 18px
—except for the bottom row, which just has a single right-aligned TextBlock that spans both columns. The window’s title is “Welcome to Sloppy Joe’s Budget House o’ Discount Sandwiches!” and it’s got a height of 350
and width of 550
. And the Grid has a Margin of 20
.
We’re building on the XAML you learned in the last two WPF projects. You can lay it out in the designer, type it in by hand, or do some of each.
Here’s the Code-Behind for your XAML window.
The menu is generated by a method called MakeTheMenu, which your window calls right after it calls InitializeComponent. It uses an array of MenuIttem classes to generate each item in the menu. We want the first three items to be normal menu items. The next two are only served on bagels. And the last is a special item with its own set of ingredients.
How it works...
If your computer is fast enough, your program may not run into this problem. But if you run it on a much slower computer, you’ll see it.
Run your program and behold the new randomly generated menu.
Uh... something’s wrong. The prices on the menu are all the same. And the menu items are weird—the first three are the same, so are the next two, and they all seem to have the same protein. What’s going on?
It turns out that the .NET Random class is actually a pseudo-random number generator, which means that it uses a mathematical formula to generate a sequence of numbers that can pass certain statistical tests for randomness. That makes them good enough to use in any app we’ll build (but don’t use it as part of a security system that depends on truly random numbers!). That’s why the method is called Next—you’re getting the next number in the sequence. The formula starts with a “seed value”—it uses that value to find the next one in the sequence. When you create a new instance of Random, it uses the system clock to “seed” the formula, but you can provide your own seed. Try using the C# Interactive window to call new Random(12345).Next()
a bunch of times. You’re telling it to create a new instance of Random with the same seed value (12345), so the Next method will give you the same “random” number each time.
So when you see a bunch of different instances of Random give you the same value, it’s because they were all seeded close enough that the system clock didn’t change time, so they all have the same seed value. So how do we fix this? Easy—just use a single instance of Random by making the Randomizer field static so all MenuItems share a single Random instance:
public static Random Randomizer = new Random();
Run your program again—now the menu will be randomized.