Chapter 4. Data, Types, Objects, and References Managing your app’s data
Data and objects are the building blocks of your apps.
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!).
Owen could use our help!
Owen 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 works hard to keep things interesting for the players.
Storytelling, fantasy, and mechanics
Owen is a particularly good storyteller. 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 Owen improve his RPG?
Character sheets store different types of data on paper
If you’ve ever played an RPG, you’ve seen character sheets: a page with details, statistics, background information, and any other notes you might see about a character. If you wanted to make a class to hold a character sheet, what types would you use for the fields?
A variable’s type determines what kind of data it can store
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. There are a few others that you haven’t seen, and they can really come in handy too.
Here are some types you’ll use a lot.
⋆ string can hold text of any length (including the empty string ""
).
⋆ 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.
⋆ int can store any integer from –2,147,483,648 to 2,147,483,647. Integers don’t have decimal points.
⋆ double can store real numbers from ±5.0 × 10–324 to ±1.7 × 10308 with up to 16 significant digits. It’s a really common type when you’re working with XAML properties.
⋆ float can store real numbers from ±1.5 × 10–45 to ±3.4 × 1038 with up to 8 significant digits.
C# has several types for storing integers
C# has several different types for integers, as well as int. This may seem a little odd (pun intended). Why have so many types for numbers without decimals? For most of the programs in this book, it won’t matter if you use an int or a long. If you’re writing a program that has to keep track of millions and millions of integer values, then choosing a smaller integer type like byte instead of a bigger type like long can save you a lot of memory.
⋆ byte can store any integer between 0 and 255.
⋆ sbyte can store any integer from –128 to 127.
⋆ short can store any integer from –32,768 to 32,767.
⋆ long can store any integer from –9,223,372,036,854,775,808 to 9,223,372,036,854,775,807.
Note
Notice how we’re saying “integer” and not “whole number”? We’re trying to be really careful—our high school math teachers always told us that integers are any numbers that can be written without a fraction, while whole numbers are integers starting at 0, and do not include negative numbers.
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, (the “s” in sbyte stands for signed). 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.
Types for storing really HUGE and really tiny numbers
Sometimes float just isn’t precise enough. 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 different floating-point types to handle huge and tiny values:
⋆ float can store any number from ±1.5 × 10–45 to ±3.4 × 1038 with 6–9 significant digits.
⋆ double can store any number from ±5.0 × 10–324 to ±1.7 × 10308 with 15–17 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.
Note
The decimal type has a lot more precision (way more significant digits), which is why it’s appropriate for financial calculations.
Let’s talk about strings
You’ve written code that works with strings. So 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 the Random class you used earlier). 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 you’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 value written directly into your code
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.
Use suffixes to give your literals types
Go back to the first loop you wrote in the “Up Close” section and change 10 to 10D:
for (float f = 10D; float.IsFinite(f); f *= f)
Now your code will have a syntax error and won’t build. The C# compiler error mentions a “literal of type double.” 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.
C# assumes that an integer literal without a suffix (like 371) is an int, and one with a decimal point (like 27.4) is a double.
A variable is like a data to-go cup
All of your data takes up space in memory. (Remember the heap from the previous 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.
Note
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. Just like the different sizes of cups at a coffee shop, there are different sizes of variables too.
Use the Convert class to explore bits and bytes
You’ve always heard that programming is about 1s and 0s. .NET has a static Convert class that converts between different numeric data types. Let’s use it to see an example of how bits and bytes work. Type these Convert method calls into the Visual Studio C# Interactive window or CSI.
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 to convert some binary numbers to bytes:
Bytes can hold numbers between 0 and 255 because they use 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
to convert the binary value 111111111111111 (15 1s) to a short. An int is a 32-bit value, so we’ll use Convert.ToInt32
to convert the 31 1s to an int:
Convert.ToInt16("111111111111111", 2) // returns 32767 Convert.ToInt32("1111111111111111111111111111111", 2) // returns 2147483647
Other types come in different sizes too
Numbers that have decimal places are stored differently than integers, 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. 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—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 decimal is largest.
10 pounds of data in a 5-pound bag
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 because leaguesUnderTheSea
is declared as an int, C# sees it as int-sized and considers it too big to put in a short container. 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.
Casting lets you copy values that C# can’t automatically convert to another type
Let’s see what happens when you try to assign a decimal value to an int variable.
Create a new Console App project and add this code to your Program.cs:
float myFloatValue = 10; int myIntValue = myFloatValue; Console.WriteLine("myIntValue is " + myIntValue);
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:
So what happened?
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 saying to the compiler that you know the types are different, and promising that in this particular instance it’s OK for C# to cram the data into the new variable.
When you cast a value that’s too big, C# adjusts it to fit its new container
You’ve already seen that a float can be cast to an int. It turns out that any number can be cast to any other number. That doesn’t mean the value stays intact through the casting, though. Say you have an int variable set to 365. If you cast it to a byte variable (max value 255), instead of giving you an error, the value will just wrap around. 256 cast to a byte will have a value of 0, 257 will be converted to 1, 258 to 2, etc., up to 365, which will end up being 109. 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. When you concatenate a string with an int, bool, float, or another value type, it automatically converts the value. 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).
C# does some conversions automatically
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. You can convert it to a string by calling its ToString method.
When you call a method, the arguments need to be compatible with the types of the parameters
In Chapter 3, you used the Random class to choose a random number from 1 up to (but not including) 5, which you used to pick a suit for a playing card:
int value = Random.Shared.Next(1, 5);
Try changing the first argument from 1
to 1.0
:
int value = Random.Shared.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:
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). More specifically:
⋆ The C# compiler knows how convert an integer to a floating-point type.
⋆ And it knows how to convert an integer type to another integer 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 as 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.
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.
But Random.Shared.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’s top-level statements:
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.
Some folks have trouble remembering the difference between a parameter and an 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.
Owen is constantly improving his game...
Good game masters are dedicated to creating the best experience they can for their players. Owen’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.
...but the trial and error can be time-consuming
Owen’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.
Owen 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.
Let’s help Owen experiment with ability scores
In this next project, you’ll build a console app that Owen can use to test his ability score formula with different values to see how they affect the resulting score. The formula has four inputs: the starting 4d6 roll, the divide by value that the roll result is divided by, the add amount value to add to the result of that division, and the minimum to use if the result is too small.
Owen will enter each of the four inputs into the app, and it will calculate the ability score using those inputs. He’ll probably want to test a bunch of different values, so we’ll make the app easier to use by asking for new values over and over again until he quits the app, keeping track of the values he used in each iteration and using those previous inputs as default values for the next iteration.
This is what it looks like when Owen runs the app:
This project is a little larger than the previous console apps that you’ve built, so we’ll tackle it in a few steps. First you’ll Sharpen your Pencil to understand the code to calculate the ability score. Then you’ll do an Exercise to write the rest of the code for the app. And finally, you’ll Sleuth out a bug in the code. Let’s get started!
Fix the compiler error by adding a cast
If you entered the code correctly, you should see a C# compiler error on this line of code:
Any time the C# compiler gives you an error, read it carefully. It often has a hint that can help you track down the problem. This error tells us exactly what went wrong: it can’t convert a double to an int without a cast. The divided
variable is declared as a double, but C# won’t allow you to add it to an int field like AddAmount because it doesn’t know how to convert it. So here’s the answer to the “Sharpen your pencil” question:
Note
But this isn’t the whole answer! There’s still something wrong with that line of code. Can you spot it?
When the C# compiler asks “are you missing a cast?” it’s giving you a huge hint that you need to cast the double variable divided
before you can add it to the int field AddAmount.
Add a cast to get the AbilityScoreCalculator class to compile...
Now that you know what the problem is, you can add a cast to fix the problematic line of code in AbilityScoreCalculator. The line that caused the error because AddAmount += divided
returns a double value. When you try to store a double value in an int variable like added
, you’ll get a “Cannot implicitly convert type” error.
You can fix it by casting divided
to an int, so adding it to AddAmount returns another int. Modify that line of code to change divided
to (int)
divided
:
Adding that cast also addresses an important part of Owen’s ability score formula:
* ROUND DOWN TO THE NEAREST WHOLE NUMBER
When you cast a double to an int, C# rounds it down—so for example (int)19.7431D
gives us 19
. By adding that cast, you’re making sure the score is rounded down, like Owen’s formula asks for.
...but there’s still a bug!
We’re not quite done yet! You fixed the compiler error, so now the project builds. But even though the C# compiler will accept it, there’s still a bug in the code. So let’s go ahead and fix it! In the next exercise, you’ll use the AbilityScoreCalculator class as is, then you’ll use it to sleuth out the bug.
Did you get a “cannot read keys when either application does not have a console” error in VSCode? If you did, go back to Chapter 1 and follow the instructions to change the C# debug console setting so your console app runs in the Terminal and not the Debug Console.
Note
Try changing the csharp.debug.console setting to the external Terminal to run your app in an external terminal window. You might prefer debugging your apps that way!
You’re right, Owen. There’s a bug in the code.
Owen 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 Owen 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 Owen entered 5, but it gave him 10 as a default option. Then he entered 7, but it gave a default of 12. What’s going on?
Now we can finally fix Owen’s bug—and get the REAL Sharpen answer
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 +=:
And we can finally have the real answer to the “Sharpen your pencil” question in the first part of this project.
Try adding this if/else
statement to a console app and build the solution:
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");
That’s really 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 zeros between the 3 and 4). Now it prints They're equal
again. So apparently 0.1D plus 0.2D equals 0.30000000000000004D.
Note
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 some numbers 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). If you need more rigid precision—like for a financial app that deals with money—decimal is the way to go.
Use reference variables to access your objects
When you create a new object, you use a new
statement to instantiate it, like new Guy()
in your program at the end of Chapter 3—the new
statement created a new Guy object on the heap. 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 previous chapter:
References are like sticky notes for your objects
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 you changed the labels, 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 it 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). 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 there aren’t any more references, your object gets garbage-collected
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.
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.
Once we do this 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 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. Sometime after the last reference to the object disappears, so does the object.
Multiple references and their side effects
You’ve got to be careful when you start moving reference variables around. Lots of times, it might seem like you’re simply pointing a variable to a different object. 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:
Two references mean TWO variables that can change the same object’s data
Besides losing all the references to an object, when you have multiple references to an object, you can unintentionally change the 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.
Add one more else if
block to your top-level statements. Can you guess what will happen once it runs?
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. Swapping them makes no difference because you’re swapping two identical references.
Objects use references to talk to each other
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. 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 other’s 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:
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
HearMessage 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 field of whatever elephant was passed in.
Add a method that lets an Elephant send a message.
Now let’s add a SpeakTo 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:
Add one more else if
block to the top-level statements 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; }
Now run your program and press 5. You should see this output:
You pressed 5 Lloyd heard a message Lucinda said this: Hi, Lloyd!
The “this” keyword lets an object get a reference to itself.
Note
Remember, if your app doesn’t pause on the breakpoint, make sure you’re starting the app with debugging. Run the app by pressing F5 or choosing Start Debugging from the Debug (Visual Studio) or Run (VSCode) menu.
Use the debugger to understand what’s going on.
Place a breakpoint on the statement that you just added:
Run your program and press 5.
When it hits the breakpoint, use Debug >> Step Into (F11) to step into the SpeakTo 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 app just 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 to really understand what’s going on.
Arrays hold multiple values
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:
Note
We saw arrays of strings in Chapter 3. Now let’s take a deeper dive into how arrays work.
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 false
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;
Use each element in an array like it’s a normal variable
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 in the heap when you do it. The first element in the array has an index of 0.
Arrays can contain reference variables
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.
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.
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 0.
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.
null means a reference points to nothing
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 this:
Yes. The null keyword can be 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).
Yes! Console.ReadLine can return a null value.
Back at the beginning of Chapter 3, you hovered over Console.ReadLine so you could learn more about it from the description that the IntelliSense quick info window showed you. Let’s take another look at that window:
Console.ReadLine returns a null when there are no lines available
You’ve been running your apps in Visual Studio and typing input using the keyboard. But you can also run them from the command line. In Windows, there’s an executable in the bin\Debug folder. You can use this command to run your app from the project folder:
C:\Users\Public\source\repos\ConsoleApp1\ConsoleApp1>dotnet run Hello, World!
Note
Make sure you run from inside the project folder that has the .csproj file, not the solution folder that contains it.
You can also use your operating system’s pipe commands like << or < or | to send input to your app from a file or the output of another console app. When you do this, Console.ReadLine needs a way to tell your app that it hit the end of the file—and that’s when it returns null.
But there’s still one issue: what does your app do when Console.ReadLine returns null?
Use the string? type when a string might be null
You’ve been using two different (but related!) types to hold text values. First, there’s the string type, like you used for the Name field in the Elephant class:
public string Name = "";
Then there’s the string? type, like the type returned by Console.ReadLine or which int.TryParse takes as its first parameter, like you used in Owen’s ability score calculator app:
string? line = Console.ReadLine(); if (int.TryParse(line, out int value))
The difference is that in the Elephant class the Name field is never null. That’s why we asked you to initialize the Name field in your Elephant class.
What do you think would happen if you didn’t initialize the Name field in the Elephant class?
Change the field declaration in the Elephant class so it doesn’t initialize it to an empty string:
Visual Studio gives you a warning that has to do with null values, and asks you to consider declaring the field as nullable. That’s what the string? type is—a nullable string.
You can make the error disappear by changing the Name field to a nullable string? instead of a string:
public string? Name;
Now your app builds again, and runs exactly the same way as it did before.
int.TryParse takes a string? parameter
So what does your app do if Console.ReadLine returns null?
Luckily, int.TryParse also takes a string?
value, so if your app gets to the end of the input and Console.ReadLine returns null, int.TryParse will just return false—so the app will work just fine, and when it gets a null value it will treat it the way it treats any other value that can’t be parsed.
Visual Studio is smart enough to check for possible places where a value can be null. You can avoid that problem by making sure all of your reference variables are initialized.
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. 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—you’ll use it in many of your programs, and your experience using random numbers when playing tabletop games will make it a lot easier for you to understand what it does.
Welcome to Sloppy Joe’s Budget House o’ Discount Sandwiches!
Sloppy Joe has a pile of meat, a whole lotta bread, and more condiments than you can shake a stick at. 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 MAUI app, some arrays, your handy random number generator, and a couple of new, useful tools. Let’s get started!
Here’s the app you’ll build. It creates a menu with six random sandwiches. Each sandwich has a protein, a condiment, and a bread, all chosen at random from a list. Every sandwich is given a random price, and there’s a special random price at the bottom to add guacamole on the side.
Grid controls
The Grid control contains other controls, and works just like the other layout controls to contain child controls (the other controls nested inside it). There’s an opening <Grid>
tag and a closing </Grid>
tag, and the tags for all of the child controls are between them.
Cells in a grid are invisible—their only purpose is to determine where the child controls are displayed on the page. We used Border controls to make the grid visible. A Border control draws a border around a child control nested inside it:
<Border> <Label Text="I have a border!"/> </Border>
A Border can only contain one child control. In the app below we didn’t nest any controls inside the Borders—we just took advantage of the fact that each Border fills up the entire cell. We used the Border control’s BackgroundColor property to make some of the cells in the grid darker.
Use Grid properties to put a control in a cell
The rows and columns in a Grid are numbered starting with 0. To put a child control in a specific row and column, use the Grid.Row and Grid.Column properties. For example, putting <Border Grid.Row="1" Grid.Column="2" />
between Grid tags will make the Grid place the border in the second row and third column. You can also make a control span multiple rows or columns using the Grid.RowSpan and Grid.ColumnSpan properties.
Define the rows and columns for a Grid
The Grid control XAML has sections to define rows and columns. Each row or column can either have proportional sizes—for example, column 3 is twice as wide as column 2 and three times as wide as column 1—or absolute sizes in device-independent pixels.
The row and column definitions are in special sections inside the <Grid>
tag. The row definitions are inside a <Grid.RowDefinitions> section, and the column definitions are inside a <Grid.ColumnDefinitions> section.
Here’s the complete XAML for the app that we’ve been showing you. Create a .NET MAUI app called GridExample and add this XAML code (and delete the OnCounterClicked method in MainPage.xaml.cs
).
The C# code for the main page
Here’s the C# code for the main page of your Sloppy Joe app. We’re about to give you an exercise to build a class called MenuItem that generates random sandwiches and prices. As soon as the page loads, it calls a method called MakeTheMenu that uses an array of MenuItem objects to fill in all of the prices, and one last MenuItem object to get the price for the guacamole.
You’re right! This is a great time to improve accessibility.
Sloppy Joe has a wheelchair ramp and braille versions of all of his menus, because he wants to make sure everyone has a chance to eat his budget-friendly sandwiches. So let’s make sure our menu app is accessible too!
Start your operating system’s screen reader and read the menu page.
Windows Narrator Start Windows Narrator (Ctrl++N). Narrator will scroll through the contents of any window when you hold down the Narrator key ((typically the Insert key, but you can change that in Narrator settings)) and press the left or right arrows. Navigate to your app, then navigate through all the controls and listen to what Narrator says. |
macOS VoiceOver Start VoiceOver (+F5). VoiceOver will read the contents of any window when you hold down the VoiceOver activation key (^ control + option) and press A. Navigate to your app and press VO+A (or ^ A), and listen to what VoiceOver says. Press the either ^ or to stop reading. |
Can we make the app more accessible?
When a screen reader narrates a window, it navigates from item to item, reading each item aloud and drawing a rectangle around it. What did you hear when you listened to the screen reader narrate your app? What did you see? Try having it read the menu while you have your eyes closed. Did you still understand everything that you needed to? It’s pretty good! But accessibility is all about making things better for all of our users. Can we make it better?
Set the main header so the screen reader narrates it
You may have noticed that the first thing it said was “Home”—and if you watched carefully, you saw that was narrating the title bar. Modify AppShell.xaml to change “Home” to “Sloppy Joe’s menu” and have the screen reader narrate the page again.
It would be great to have the narrator tell the user that they’re looking at items on a menu. Let’s try adding a SemanticProperties.Description to the <Grid>
tag:
<Grid Margin="10" SemanticProperties.Description="Here are the items on the menu.">
Now try using the screen reader to narrate the window. It sounds fine in Windows, but if you’re using macOS there’s a problem: the screen reader won’t read the items or prices. That’s because if you set the SemanticProperties.Description on a control that has children, the screen reader can’t reach those children anymore. This is important even if you’re building software for Windows, because your MAUI apps are cross-platform, and you want your app to be accessible anywhere.
Try setting the item1 label’s SemanticProperties.Description instead
OK, let’s try something else. Remove the SemanticProperties.Description property from the <Grid>
tag. Then try setting the SemanticProperties.Description on the first label:
<Label x:Name="item1" FontSize="18" Text="item #1" SemanticProperties.Description="Here are the items on the menu." />
Try using the screen reader again. It’s still not right! When you have a Label, you always want the screen reader to read the contents of the label. Setting the SemanticProperties.Description causes the screen reader to read that description instead of the label text.
Go ahead and delete the SemanticDescription property from the item1 Label control (and also from the Grid, if you haven’t done it already).
Use the SetValue method to change a control’s semantic properties
Let’s find a different way to make the screen reader say “Here are the items on the menu” before it reads the menu items. We’ll still use the SemanticProperties.Description for the first menu item, but instead of using a XAML tag, we’ll use C# to make sure it preserves the text.
Add this line of code to the end of your MainPage method:
public MainPage() { InitializeComponent(); MakeTheMenu(); item1.SetValue(SemanticProperties.DescriptionProperty, "Here are the items on the menu. " + item1.Text); }
Note
If you type “item1.” into Visual Studio, you won’t see SemanticProperties in the IntelliSense pop-up. That’s why you need to use the SetValue method to set it instead.
This code sets the SemanticProperties.Description property—in this case, it’s setting it to the text “Here are the items on the menu” followed by the random sandwich generated by MenuItem. Try the screen reader one more time—now the page includes that text, and works on all operating systems.
Get Head First C#, 5th Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.