The C Playerx27s Guide 5th Edition 500

Download as pdf or txt
Download as pdf or txt
You are on page 1of 400

It is illegal to redistribute this digital book.

Please
do not share this file via email, websites, or any
other means. Be mindful about where you store it
and who might gain access to it.

The digital format of this book is only legally


distributed via https://gumroad.com/l/oKNdk . If
you have received this book through
through any other
means, please report it to
rbwhitaker@outlook.com..
rbwhitaker@outlook.com

You may make


make any copies you need for your o
own
wn
personal use.
Many of the designations used by manufacturers and sellers to distinguish their products are
claimed as trademarks. Where those designations appear in this book, and the author and
publisher were aware
aware of those claims, those designations have been printed with initial capital
letters or in all capitals.
The author and publisher of this book have made every effort to ensure that this book’s
information was correct at press time. However, the author and publisher do not assume and
hereby disclaim any liability to any party for any loss, damage, or disruption caused by errors
or omissions, whether such errors or omissions result from negligence,
negligenc e, accident, or any other
cause.
Copyright © 2012-2022 by RB Whitaker
All rights reserved. No part of this book may be reproduced or transmitted in any forform
m or by
any means, electronic or mechanical, including photocopying, recording, or by any
information storage and retrieval system, without written
wr itten permission from the author, except
for the inclusion of brief quotations in a review. For information regarding permissions, write
to:
RB Whitaker
rbwhitaker@outlook.com
ISBN-13:
ISBN-13: 978-0-9855801-5-5

Starbound Software
Part 1: The Basics
Page Name XP
10 Knowledge Check - C# 25
14 Install Visual Studio 75
19 Hello, World! 50
24 What Comes Next 50
24 The Makings
Makings of a Programmer 50
26 Consolas and Telim 50
31 The Thing
Thing Namer
Namer 3000 100
37 Knowledge Check - Variables 25
45 The Variable Shop 100
45 The Variable
Variable Shop Returns 50
48 Knowledge Check - Type System 25

53
56 The Triangle
The Four Farmer
Four Sisters
Sisters and the Duckbear 100
100
57 The Dominion
Dominion of Kings
Kings 100
68 The Defense of Consolas 200
75 Repairing the Clocktower 100
78 Watchtower 100
82 Buying Inventory 100
83 Discounted Inventory 50
88 The Prototype 100
89 The Magic Cannon 100
94 The Replicator
Replicator of D’To 100
95 The Laws
Laws of Freach 50
106 Taking a Number 100
107 Countdown 100
123 Knowledge Check - Memory 25
124 Hunting the Manticore
Manticore 250

Part 2: Object-Oriented Programming


Page Name XP
131 Knowledge Check - Objects 25
135 Simula’s Test 100
143 Simula’s Soups 100
153 Vin Fletcher’s Arrows 100
162 Vin’s Trouble 50
168 The Properties
Properties of Arrows 100
173 Arrow Factories
Factories 100
191 The Point 75
191 The Color 100
191 The Card 100
192 The Locked Door 100
192 The Password
Password Validator
Validator 100
193 Rock-Paper-Scissors 150
194 15-Puzzle 150
194 Hangman 150
195 Tic-Tac-Toe 300
205 Packing Inventory 150

Page Name XP
209 Labeling Inventory 50
210 The Old Robot 200
217 Robotic Interface 75
225 Room Coordinates 50
231 War Preparations 100
240 Colored Items 100
242 The Fountain of Objects 500
244 Small, Medium, or Large 100
244 Pits 100
244 Maelstroms 100
245 Amaroks 100
245 Getting Armed 100
246 Getting Help 100
249 The Robot Pilot 50
251 Time in the Cavern
Cavern 50
Page Name XP
255 Lists of Commands 75
398 The Great Humanizer 100
403 Knowledge Check - Compiling 25
Part 3: Advanced Features 408 Knowledge Check - .NET 25
413 Altar of Publication 100
Page Name XP
269 Knowledge Check - Large Programs 25
270 The Feud 75 Part 4: The Endgame
270 Dueling Traditions 100
Page Name XP
276 Safer Number Crunching 50
419 Core Game: Building Character 300
278 Knowledge Check - Methods 25
420 Core Game:
Game: The True Programmer
Programmer 100
278 Better Random 100 420 Core Game:
Game: Actions and Players 300
290 Exepti’s
Exepti’s Game 100
421 Core Game: Attacks 200
295 The Sieve 100
421 Core Game:
Game: Damage
Damage and HP 150
301 Knowledge Check - Events 25
422 Core Game: Death 150
302 Charberry Trees 100
422 Core Game: Battle Series 150
307 Knowledge Check - Lambdas 25
422 Core Game:
Game: The Uncoded One 100
307 The Lambda Sieve 50
423 Core Game:
Game: The Player Decides 200
315 The Long Game 100
423 Expansion: The Game’
Game’ss Status 100
324 The Potion
Potion Masters of Pattren 150
424 Expansion: Items 200
331 Knowledge Check - Operators 25
424 Expansion: Gear 300
331 Navigating
Navigating Operand City 100
425 Expansion: Stolen Inventory 200
332 Indexing Operand City 75
426 Expansion: Vin Fletcher 200
332 Converting Directions to Offsets 50
426 Expansion: Attack Modifiers 200
341 Knowledge Check - Queries 25 426 Expansion: Damage Types 200
342 The Three Lenses 100
427 Expansion: Making it
it Y
Yours
ours ?
349 The Repeating Stream 150
428 Expansion: Restoring Balance 150
359 Knowledge Check - Async 25
359 Asynchronous Random Words
Words 150
360 Many Random Words
Words 50
Part 5: Bonus Levels
365 Uniter of Adds 75 Page Name XP
366 The Robot Factory
Factory 100 441 Knowledge Check - Visual Studio 25
372 Knowledge Check - Unsafe Code 25 446 Knowledge Check - Compiler Errors 25
392 Knowledge Check - Other Features 25 451 Knowledge Check - Debugging 25
397 Colored Console 100
TABLE OF CONTENT󰁓
Acknowledgments xix
Introduction 1
The Great Game of Programming 1
Book Features 2
I Want Your Feedback 6
An Overview 6

PART 1: THE BA󰁓IC󰁓


1. The C# Program
Programming
ming Language 9
What is C#? 9
What is .NET? 10
2. Getting an IDE 11
A Comparison of IDEs 11
Installing Visual Studio 13
3. Hello World: Your First Program 15
Creating a New Project 15
A Brief Tour of Visual Studio 17
Compiling and Running Your Program 18
Syntax and Structure 19
Beyond Hello World 24
Compiler Errors, Debuggers, and Configurations 27
4. Comments 29
How to Make Good Comments 30

x TABLE OF CONTENTS

5. Variables 32
What is a Variable?
Variable? 32
Creating and Using Variables in C# 33
Integers 34
Reading from a Variable Does Not Change It 35
Clever Variable Tricks 35
Variable Names 36
6. The C# Type 󰁓ystem 38
Representing Data in Binary 38
Integerr Types
Intege 39
Text: Characters and Strings 42
Floating-Point Types 43
The bool Type 45
Type Inference 46
The Convert Class and the Parse Methods 47
7. Math 50
Operations and Operators 50
Addition, Subtraction, Multiplication, and Division 51
Compound Expressions and Order of Operations 52
Special Number Values 54
Integer Division vs. Floating-Point Division 54
Division by Zero 55
More Operators 55
Updating Variables 56
Working with Different Types and Casting 58
Overflow and Roundoff Error 60
The Math and MathF Classes 61
8. Console 2.0 63
The Console Class 63
Sharpening Your String Skills 65
9. Decision Making 69
The if Statement 69
The else Statement 73
else if Statements 73
Relational Operators: ==, =, <, >, <=, >= 74
Using bool in Decision Making 75
Logical Operators 76
Nesting if Statements 77
The Conditional Operator 77
10. 󰁓witches 79

TABLE OF CONTENTS xi

Switch Expressions
Switch Statements 80
81
Switches as a Basis for Pattern Matching 82
11. Looping 84
The while Loop 84
The do/while Loop 86
The for Loop 86
break Out of Loops and continue to the Next Pass 87
Nesting Loops 88
12. Arrays 90
Creating Arrays 91
Getting and Setting Values in Arrays 91
Other Ways to Create Arrays 93
Some Examples with Arrays 94
The foreach Loop 95
Multi-Dimensional Arrays 95
13. Methods 97
Defining a Method 97
Calling a Method 99
Passing Data to a Method 101
Returning a Value from a Method 103
Method Overloading 104
Simple Methods with Expressions 105
XML Documentation Comments 106
The Basics of Recursion 107
14. Memory Management 109
Memory and Memory Management 110
The Stack 110
Fixed-Size Stack Frames 115
The Heap 115
Cleaning Up Heap Memory 122

PART 2: OBJECT-ORIENTED PROGRAMMING


15. Object-Oriented Concepts 129
Object-Oriented Concepts 129
16. Enumerations 132
Enumeration Basics 133
Underlying Types 136
17. Tuples 137

xii TABLE OF CONTENTS

The
TupleBasics of Tuples
Element Names 138
139
Tuples and Methods 139
More Tuple Examples 140
Deconstructing Tuples 141
Tuples and Equality 142
18. Classes 144
Defining a New Class 145
Instances of Classes 147
Constructors 148
Object-Oriented Design 153
19. Information Hiding 155
The public and private Accessibility Modifiers 156
Abstraction 159
Type Accessibility Levels and the internal Modifier 160
20. Properties 163
The Basics of Properties 163
Auto-Implemented Properties 166
Immutable Fields and Properties 167
Object Initializer Syntax and Init Properties 168
Anonymous Types 169
21. 󰁓tatic 170
Static Members 170
Static Classes 173
22. Null Referenc
References
es 174
Null or Not? 175
Checking for Null 176
23. Object-Oriented Design 178
Requirements 179
Designing the Software 180
Creating Code 185
How to Collaborate 187
Baby Steps 189
24. The Catacombs of the Class 190
The Five Prototypes 190
Object-Oriented Design 193
Tic-Tac-Toe 195
25. Inheritance 197
Inheritance and the object Class 198
Choosing Base Classes 200

TABLE OF CONTENTS xiii

Constructors
Casting and Checking for Types 201
203
The protected Access Modifier 204
Sealed Classes 204
26. Polymorphism 206
Abstract Methods and Classes 208
New Methods 209
27. Interfaces 211
Defining Interfaces 212
Implementing Interfaces 213

Interfaces and Base


Explicit Interface Classes
Implementations 214
214
Default Interface Methods 215
28. 󰁓tructs 218
Memory and Constructors 219
Classes vs. Structs 220
Built-In Type Aliases 224
Boxing and Unboxing 225
29. Records 227
Records 227
Advanced Scenarios 229
Struct- and Class-Based Records 230
When to Use a Record 231
30. Generics 232
The Motivation for Generics 232
Defining a Generic Type 235
Generic Methods 237
Generic Type Constraints 237
The default Operator 239
31. The Fountain
Fountain of Objects 241
The Main Challenge 242
Expansions 244
32. 󰁓ome Useful Types 247
The Random Class 248
The DateTime Struct 249
The TimeSpan Struct 250
The Guid Struct 251
The List<T> Class 252
The IEnumerable<T> Interface 255
The Dictionary<TKey, TValue> Class 256

xiv TABLE OF CONTENTS

The Nullable<T>
ValueTuple Struct
Structs 258
258
The StringBuilder Class 259

PART 3: ADVANCED TOPIC󰁓


33. Managing Larger Programs 263
Using Multiple Files 263
Namespaces and using Directives 264
Traditional Entry Points 268
34. Methods Revisited 271
Optional Arguments 271
Named Arguments 272
Variable Number of Parameters 272
Combinations 273
Passing by Reference 273
Deconstructors 276
Extension Methods 277
35. Error Handling and Exceptions 280
Handling Exceptions 281
Throwing Exceptions 283
The finally Block 284
Exception Guidelines 285
Advanced Exception Handling 288
36. Delegates 291
Delegate Basics 291
The Action, Func, and Predicate Delegates 294
MulticastDelegate and Delegate Chaining 295
37. Events 296
C# Events 296
Event Leaks 300
EventHandler and Friends 300
Custom Event Accessors 301
38. Lambda Expressions 303
Lambda Expression Basics 303
Lambda Statements 305
Closures 306
39. Files 308
The File Class 308
String Manipulation 310
File System Manipulation 312

TABLE OF CONTENTS xv

Other Ways to Access Files 313


40. Pattern Matching 316
The Constant Pattern and the Discard Pattern 317
The Monster Scoring Problem 317
The Type and Declaration Patterns 318
Case Guards 319
The Property Pattern 319
Relational Patterns 320
The and, or, and not Patterns 321
The Positional Pattern 321
The var Pattern 322
Parenthesized Patterns 322
Patterns with Switch Statements and the is Keyword 322
Summary 323
41. Operator Overloading 325
Operator Overloading 326
Indexers 327
Custom Conversions 329
42. Query Expressions 333
Query Expression Basics 334
Method Call Syntax 336
Advanced Queries 338
Deferred Execution 340
LINQ to SQL 341
43. Threads 343
The Basics of Threads 343
Using Threads 344
Thread Safety 347
44. Asynchronous Programm
Programming
ing 351
Threads and Callbacks 352
Using Tasks 353
Who Runs My Code? 356
Some Additional Details 358
45. Dynamic Objects 361
Dynamic Type Checking 362
Dynamic Objects 362
Emulating Dynamic Objects with Dictionaries 363
Using ExpandoObject 363
Extending DynamicObject 364
When to Use Dynamic Object Variations 365

xvi TABLE OF CONTENTS

46. Unsafe Code 367


Unsafe Contexts 368
Pointerr Types
Pointe 368
Fixed Statements 369
Stack Allocations 370
Fixed-Size Arrays 370
The sizeof Operator 370
The nint and nuint Types 371
Calling Native Code with Platform Invocation Services 371
47. Other Language Features 373

Constantsand the yield Keyword


Iterators 374
375
Attributes 376
Reflection 378
The nameof Operator 379
Nested Types 379
Even More Accessibility Modifiers 380
Bit Manipulation 380
using Statements and the IDisposable Interface 384
Preprocessor Directives 385
Command-Line Arguments 387
Partial Classes 387
The Notorious goto Keywor
Keyword d 388
Generic Covariance and Contravariance 389
Checked and Unchecked Contexts 391
Volatile Fields 392
48. Beyond a 󰁓ingle Project 393
Outgrowing a Single Project 393
NuGet Packages 396
49. Compiling in Depth 399
Hardware 399
Assembly 401
Programming Languages 401
Instruction Set Architectures 402
Virtual Machines and Runtimes 402
50. .NET 404
The History of .NET 404
The Components of .NET 405
Common Infrastructure 405
Base Class Library 406
App Models 407

TABLE OF CONTENTS xvii

51. Publishing 409


Build Configurations 409
Publish Profiles 410

PART 4: THE ENDGAME


52. The Final Battle 417
Overview 418
Core Challenges 419
Expansions 423
53. Into Lands Uncharted 429
Keep Learning 429
Where Do I Go to Get Help? 430
Parting Words 431
PART 5: BONU󰁓 LEVEL󰁓
A. Visual 󰁓tudio 435
Windows 435
The Options Dialog 441
B. Compiler Errors 442
Code Problems: Errors, Warnings, and Messages 442
How to Resolve Compiler Errors 443
Common Compiler Errors 445
C. Debugging
Debug ging Your Code 447
Print Debugging 448
Using a Debugger 448
Breakpoints 449
Stepping Through Code 450
Breakpoint Conditions and Actions 451

Glossary 452
Index 468
ACKNOWLEDGMENT󰁓
It is hard to separate the 5th Edition from the 4th Edition when it comes to acknowledgments.
The 4th Edition kept the bones of earlier editions but otherwise
other wise was a complete rewrite (twice!).
Despite being 20 years old, C# 9 and 10 have changed the language in meaningful, exciting,
and fundamental ways. Indeed, most random code you find on the Internet now looks like
“old” C# code. These recent changes
c hanges are somehow both tiny and game-changing. I don don’t’t have
a great way to measure, but I’ve often guessed that the 5 Edition is 98% the same as the 4 th
th

Edition. I might have even called this edition 4.1 if that were that a thing books did. Yet that
last 2%, primarily reflecting C# 10 changes and the fast-evolving language, was enough to feel
a new edition was not only helpful but necessary.
I want to thank the hundreds of people who joined Early Access for 4 th and 5th Editions and the
readers who have joined the book’s Discord server. The discussions I have had with you have
changed this book for the better in a thousand different ways. With so many involved, I cannot
thank everyone by name, though you all deserve it for your efforts. Having
Having said that, UD Simon
deserves special mention for providing me with a tsunami of suggestions and error reports
week after week, rivaling the combined total of all other Early Access readers. The book is
immeasurably better because of your collective
coll ective efforts.
I also need to thank my family. My parents’ confidence and encouragement to do my best have
caused me to do things I could
coul d never have done without them.
Most of all, I want to thank my beautiful wife, who was there to lift my spirits when the weight
of
andwriting a book
creative was unbearable,
feedback who
and guidance. Sheread
has through my book
been patient with and gave
me as I’vehonest, thoughtful,
done five editions
of this book over the years. Without her, this book would still be a random scattering of files
buried in some obscure folder on my computer
c omputer,, collecting green silicon-based mold.
I owe all of you my sincerest
since rest gratitude.
gratitude.
-RB Whitaker
INTRODUCTION
THE GREAT GAME OF PROGRAMMING
I have a firmly held personal belief, grown from decades of programming: in a very real sense,
programming is a game. At least, it can be like playing a game with the right mindset.
min dset.
For me, spending a few hours programming—crafting code that bends these amazing
computational devices to my will and creating worlds of living software—is entertaining and
rewarding.
reward ing. It competes with delving into the Nether in Minecraft, snatching the last
l ast Province
Province
card in Dominion, or taking down a reaper in Mass Effect.
I don’t mean that programming is mindless
mindle ss entertainment. It is rarely that. Most of your time
is spent puzzling out the right next step or figuring out why things aren’t working as you
expected. But part of what makes games engaging is that they are challenging. You have to
work for it. You apply creativity and explore possibilities. You practice and gain abilities that
help you win.
Y
You'll
ou'll be in good shape if you approach programming with this same mindset because
programming requires
requires this same set of skills. Some days, it will feel like you are playing Flappy
Bird, Super Meat Boy, or Dark Souls—all notoriously difficult games—but creating softwaresoftware is
challenging in all the right ways.
The “game” of programming is a massively multiplayer,
multiplayer, open-world sandbox game with role-
playing elements. By that, I mean:
• Massively multiplayer: While you may tackle specific problems independently, you are
never alone. Most programmers are quick to share their knowledge and experience with
others. This book and the Internet ensure you are not alone in your journey.
• An open-world sandbox game: You have few constraints or limitations; you can build
what, when, and how you want. want.
• Role-playing elements: With practice, learning, and experience, you get better in the
skills and tools you work with, going from a lowly Le
Level
vel 1 beginner to a master,
master, sharpening
your skills and abilities
abilities as you go
go..
If programming is to be fun or rewarding, then learning to program must also be so. Rare is
the book that can make learning complex technical topics anything more than tedious. This
book attempts to do just that.
that. If a spoonful of sugar can help the medicine go down, then there

2 LEVEL 1 INTRODUCTION
INTRODUCT ION

must be some
technical blend
topic have anofelement
elevenofherbs and spicesand
fun, challenge, that will make even the most complex
reward.
Over the years, strategy guides, player handbooks, and player’s guides have been made for
popular games. These guides help players learn and understand the game world and the
challenges they will encounter. They provide time-saving tips and tricks and help prevent
players from getting stuck anywhere for too long. This book attempts to be that player’s guide
for the Great Game of Programming in C#.
This book skips the typical business-centric examples found in other books in favor of samples
with a little more spice. Many are
are game-related, and many of the hands-on
hands-on challenges involve
building small games or slices of games. This makes the journey more entertaining and
exciting. While C# is an excellent
excell ent language for game development, this book is not specifically
a C# game programming book. You will undoubtedly come away with ideas to try if that’s the
path youuse
you can choose, but this
it to build anybook is focused
type of program,on becoming
not skilled
just games. with
(Most the C# language
professional so that
programmers
make business-centric applications, web apps, and smartphone apps.)
This book focuses on console applications. Console applications are those text-based
programs where the computer receives text input from the user and displays text responses in
the stereotypical white text on a black background window. We’ll learn some things to make
console applications more colorful and exciting, but console applications are,
are, admittedly, not
the most exciting type of application.
Why not use something more exciting? The main
main reason
reason is that regardless
regardless of whether you want
to build games, smartphone apps, web apps, or desktop apps, the starting points in those
worlds already expect you to know much about C#. For
For example, I just looked over
over the starter
code for a certain C# game development
devel opment framework. It demands you alralready
eady know how to use
advanced topics
(generics) just tocovered in Level
get started! While25 some
(inheritance), Level 26 (polymorphism),
people successfully dive in and stayand Levelit 30
afloat, is
usually wiser to build up your swimming skills in a lap pool before trying to swim across the
raging ocean. Starting from the basics gives you a better foundation. After building this
foundation, learning how to make specific application types will go much more smoothly. Few
will be satisfied with just console applications, but spending a few weeks covering the basics
before moving on will make the learning process far easier.

BOOK FEATURE󰁓
Creating a fun and rewarding book (or at least not a dull and useless one) means adding some
features that most programming books do not have. Let’s look at a few of these so that you
know what to expect.

󰁓peedruns
At the start of each level (chapter)
(chapter) is a Speedrun section that
that outlines the key points
points described
in the level. It is not a substitute for going through the whole level in detail but is helpful in a
handful of situations:
1. Y
You’re
ou’re reviewing
reviewing the material and want a reminder of the key points
points..
2. Y
You
ou are skimming to see if some level has information tthat
hat you will need soon.
3. Y
You
ou are trying to remember
remember which level covered some paparticular
rticular topic.

BOOK FEATURES 3

Challenges and Boss Battles


Scattered throughout the book are hands-on challenges that give you a specific problem to
work on. These start small early in the book, but some of the later ones are quite large. Each
of these challenges is marked with the following icon:

When a challenge is especially


especially tough, it is upgraded to a Boss Battle,
Battle, shown
shown by the icon below:

Boss Battles are sometimes split across multiple parts to allow you to work through them one
step at a time.
I strongly recommend that you do these challenges. You don’t beat a game by reading the
player’s guide. You don’t learn to program by reading a book. You will only truly learn if you
sit down and program.
I also recommend you do these challenges as you encounter them instead of reading ten
chapters and returning to them. The read a little, program a little model is far better at helping
you learn fast.
I also feel that these challenges should not be the only things you program as you learn,
especially if you are relatively new to programming. Half of your programming time should
come from these challenges and the rest from your own imagination. Working on things of
your own invention will be more exciting to you. But also, when you are in that creative
mindset, you mentally explore the programming world better. You start to think about how
you can apply the tools you have learned
l earned in new situations,
situations, rather than being told, “Go use
this tool over here in this specific way.”
As you do that, keep in mind the size of the challenges you are inventing for yourself. If you
are learning how to draw, you don’t go find millennia-old chapel ceilings to paint (or at least
you don’t expect it to turn out like the Sistine Chapel). Aim for things that push your limits a
little but aren’t intimidating. Keep in mind that everything is a bit harder than you initially
expect. And don’t be afraid to dive in and make a mess. Continuing the art analogy, you aren't
learning if you don’t have a few garbage drawings in your sketchbook. Not every line of code
you write will be a masterpiece. You have permission to write strange, ugly, and awkward
awkward
code.
If these specific challenges are not your style, then skip them. But substitute them with
something else. You will learn little if you don’t sit down and write some code.

When a challenge contains a HintHint,, these are suggestions or possibilities, not things you must
do. If you find a different path that works, go for it.
Some challenges also include things labeled Answer this question.
question. I recommend writing out
your answer.
answer. (Comments, covered in Level 4, could be a good approach.) Our brains like to
tell us it understands something without proving it does. We mentally skip the p
proof,
roof, often to
our detriment. Writing it out ensures we know it. These questions usually only take a few
seconds to answer.
answer.
I have posted my answers to these challenges on the book’s website, described later in this
introduction. If you want a hint or compare
c ompare answers, you can review what I did. Just because
our solutions are different doesn’t make yours bad or wrong. I make plenty of my own

4 LEVEL 1 INTRODUCTION
INTRODUCT ION

mistakes, haveinmy
programming C#own
for apreferences forlong
long time. As the various toolsain
as you have the language,
working and
solution, have
you’r
you’re also been
e doing fine.

Knowledge Checks
Some levels in this book focus
foc us on conceptual topics that are not well-tested by a programming
problem. In these cases, instead of a Challenge problem, these levels will have a Knowledge
Check, containing a quiz with true/false, multiple-choice, and short answer questions. The
answers are immediately below the Knowledge Check, so you can see if you learned the key
points right away. These are marked with the knowledge scroll icon below:

Experience Points and Levels


Since this book is a player’s guide, I’ve attempted to turn the learning process into a game.
Each Challenge and Knowledge Check section is marked in the top right with experience
points (written as XP, as most games do) that you earn by completing the challenge. When
you complete a challenge, you can claim
c laim the XP associated with it and add it to your total.
Towards the front of this book, after the title page and the map, is an XP Tracker. You can use
this to track your progress, check off challenges as you complete them, and mark off your
progress as you go.
Y
You
ou can also get extra
extra copies of the XP Tracker on the book’
book’ss website (described
(described below) if you
do not want to write in your book, have a digital copy, or have a used copy where somebody
else has already marked it.

As you collect XP,


XP, you will accumulate enough points to level up from Level 1 to Level 10. If
you reach Level 10, you will have completed nearly every challenge
c hallenge in this book and should
have an excellent grasp of C# programming.
The XP assigned to each challenge is not random. Easier challenges have fewer points; more
demanding challenges are worth more XP.
XP. While measuring
me asuring difficulty is somewhat subjective,
you can generally count
count on spending more
more time on challenges with more points and will gain
a greater reward for it.

Narratives and the Plot


The challenges form a loose storyline that has you, the (soon to be) Master Programmer
journeying through a land that has been stripped of the abability
ility to program by
by the malevolent,
shadowy, and amorphous Uncoded One. Using your growing C# programming
p rogramming skills, you will
be able to help the land’s inhabitants, fend off the Uncoded One’s onslaught, and eventually
face the Uncoded One in a final battle at the end of the book.
Even if this plot is not attractive to you, the challenges are still worth doing. Feel free to ignore
the book-long storytelling if it isn’t helpful for you.
While much of the book’s
book’s “plot” is revealed in the Challenge descriptions themselves, there
were places where it felt shoehorned. Narrative
Narrative sections supplement the descriptions in the
challenges but otherwise have no purpose beyond advancing this book-long plot. These are
marked with the icon below:

BOOK FEATURES 5

If you are ignoring the plot, you can skip these sections. They do not contain information that
helps you be a better C# programmer.

󰁓ide Quests
While everything in this book is
is worth knowing (skilled C# programmers
programmers know all of it), some
sections are more important than others. Sections that may be skipped in your first pass
through this book are marked as Side Quests, indicated with the following icon:

These often deal with situations that are less common or less impactful. If you’re pressed for
time, these sections are better to skip than the rest. However,
However, I recommend returning to them
later if you don’t read them the first time around.
Glossary
Programmers have a mountain of unique jargon and terminology. Beyond learning a new
programming language, understanding this jargon is a second massive challenge for new
programmers. ToTo help you with this undertaking, I have carefully defined new concepts within
the book as they arise and collected all of these
th ese new words and concepts into a glossary at the
back of the book. Only the lucky few will remember all such words from seeing it defined once.
Use the glossary to refresh your mind on any term you don’t remember well.

The Website
This book has a website associated with it, which has a lot of supporting content: https://
csharpplayersguide.com.. Some of the highlights
csharpplayersguide.com h ighlights are below:
• https://csharpplayersguide.com/solutions. Contains my solutions to all the Challenge
https://csharpplayersguide.com/solutions.
sections in this book. My answer is not necessarily more correct than yours, but it can give
you some thoughts on a different way to solve
s olve the problem and perhaps some hints on
how to progress if you are stuck. This also contains more thorough explanations for all of
the Knowledge Checks in the book.
• https://csharpplayersguide.com/errata.. This page contains errata (errors in the book)
https://csharpplayersguide.com/errata
that have been reported to clarify what was meant. If you notice something that seems
wrong or inconsistent,
inconsistent, you may find a correction here.
• http://csharpplayersguide.com/articles.. This page contains a collection of articles that
http://csharpplayersguide.com/articles
add to this book’s content. They often cover
c over more in-depth information beyond what I felt
is reasonable to include in this book or answer
an swer questions readers hav
havee asked me. In a few
places in this book, I call out articles with more information for the curious.

Discord
This book has an active Discord server where you can interact with me and other readers to
discuss the book, ask questions, report problems, and get feedback on your solutions to the
challenges. Go to https://csharpplayersguide.com/discord to see how to join the server.
This server is a guildhall where you can rest from your travels and discuss C# with others on a
similar journey as you.

6 LEVEL 1 INTRODUCTION
INTRODUCT ION

I WANT YOUR FEEDBACK


I depend on readers like you to help me see how to make the book better. This book is much
better because past readers helped me know what parts were good and
an d bad.
Naturally, I’d
I’d love to hear that you loved the book. But I need constructive criticism
c riticism too. If there
is a challenge that was too hard, a typo you found, a sec
section
tion that wasn’t clear
clear,, or even that you
felt an entire level or the whole book was bad, I want to hear it. I have gone to ggreatreat lengths to
make this book as good as possible, but with your help, I can make it even better for those who
follow in our footsteps. Don’t hesitate to reach out to me, whether your feedback is positive or
negative!
I have many ways that you can reach out to me. Go to https://csharpplayersguide.com/
contact to find a way that works for you.

AN OVERVIEW
Let s take a peek at what lies ahead. This book has five major parts:
• Part 1—The Basics. This first part covers many of the fundamental elements of C#
programming. It focuses on procedural programming, including storing data, picking and
choosing which lines of code
c ode to run, and creating reusable chunks of code.
• Part 2—Object-Oriented Programming. C# uses an approach called object-oriented
programming to help you break down a large program into smaller pieces that are each
responsible for a little slice of the whole program. These tools are essential as you begin
building bigger programs.
• Part 3—Advanced Topics. While Parts 1 and 2 deal with the most critical elements of the
C# language, there are various other language features that are worth knowing. This part
consists of mostly independent topics. You can jump around and learn the ones you feel
are most important to you (or skip them all entirely, for a while). In some
som e ways, you could
consider all of Part 3 to be a big Side Quest, though you will be missing out on some cool
C# features if you skip it all.
• Part 4—The Endgame. While hands-on challenges are scattered throughout the book,
Part 4 consists of a single, extensive, final program that will test the knowledge and skills
that you have learned. It will also wrap up the book, pointing you toward Lands Uncharted
and where you might go after finishing this book.
• Part 5—Bonus Levels. The end of the book contains a few bonus levels that guide you on
what to do when you don’t know what else to do—dealing with compiler errors and
debugging your code. The glossary and index are also back here at the end of the book.

Please do not feel like


l ike you must read this book cover to cover to get value from it.
If you are new to programming, I recommend a slow, careful pace through Parts 1 and 2,
skipping the Side Quests and only advancing when you feel comfortable taking the next step.
After Part 2, you might continue your course through the advanced ffeatureseatures of Part 3, or you
might also choose to skim it to get a flavor for what else C# offers without going into depth.
Even if you skim or skip Part 3, you can still attempt the Final Battle in Part 4. If you’re making
consistent progress
progress and getting
g etting good practice, it doesn’t matter if your progress feels slow.
If you are an experienced
experience d programmer,
programmer, you will likely be able to race through Part 1, slow down
only a little in Part 2 as you learn
l earn how C# does object-oriented programming, and then spend
most of your time in Part 3, learning the things that make C# unique.
Adapt the journey however you see fit. It
It is your book and your adventure!

Part 1
The Basics
The world of C# programming lies in front of you, waiting to be explored. In Part 1, we begin our
adventure and learn the basics of programming in C#:

Learn what C# and .NET are (Level 1) 1)..
• Install tools to allow us to program in C# (Level 2)
2)..
• Write our first few programs and learn the basic ingredients of a C# program (Level 3)
3)..
• Annotate your code with comments (Level 4). 4).
• Store data in variables (Level 5)
5)..
• Understand
Understan d the type system (Levels 6).
• Do basic math (Level 7) 7)..
• Get input from the user (Level 8) 8)..
• Make decisions (Levels 9 and 10).
10).
• Run code more than once in loops (Level 11).11).

Make arrays, which contain multiple pieces of data (Level 12).
12).
• Make methods, which are named, packaged, reusable bits of code (Level 13).
13).
• Understand
Understand how memory is used in C# (Level 14).
14).
LEVEL 1
THE C# PROGRAMMING LANGUAGE
󰁓peedrun
• C# is a general-purpose programming language. You can make almost anything with it.
• C# runs on .NET, which is many things: a runtime that supports your program, a library of code to
build upon, and a set of tools to aid in constructing programs.

Computers are amazing machines, capable of running billions of instructions every second.
Y
Yet
et computers have no innate intelligence and do not know which instructions will solve a
problem. The people who can harness these powerful machines to solve meaningful
problems are the wizards of the computing world we call programmers.
Humans and computers do not speak the same language. Human language is imprecise and
open to interpretation. The binary instructions computers use, formed from 1’s and 0’s, are
precise but very difficult for humans to use. Programming languages bridge the two—precise
enough for a computer to runr un but clear enough for a human to understand.

WHAT I󰁓 C#?
There are many programming languages out there, but C# is one of the few that is both widely
used and very loved. Let’
L et’ss talk about some of its key features.
C# is a general-purpose programming language. Some languages solve only a specific type of
problem. C# is designed to solve virtually any problem equally well. You can use it to make
games, desktop programs, web applications, smartphone apps, and more. However, C# is at
its best when building applications (of any sort) with it. You probably wouldn’t write a new
operating system or device driver with it (though both have been done).
C# strikes a balance between power and ease of use. Some languages give the programmer
more control than C#, but with more ways to go wrong. Other languages do more to ensure
bad things can’t happen by removing some of your power. C# tries to give you both power and
ease of use and often manages to do both but always strikes a balance between the two when
needed.

10 LEVEL 1 THE C# PROGRAMMI


PROGRAMMING
NG LANGUAGE

C# is a living language. It changes over time to adapt to a changing programming world.


Programming has changed significantly in the 20 years since it was created. C# has evolved
and adapted over time. At the time of publishing, C# is on version 10.0, with new major
updates every year or two.
C# is in the same family of languages as C, C++, and Java, meaning that C# will be easier to
pick up if you know any of those. After learning C#, learning any of those will also be easier.
This book sometimes points out the differences between C# and these other languages for
readers who may know them.
C# is a cross-platform language. It can run on every major operating system, including
Windows, Linux, macOS, iOS,
iOS, and Android.
This next paragraph is for veteran programmers; don’t worry if none of this makes sense.
(Most will make sense after this book.) C# is a statically typed, garbage collected, object-
oriented programming language with imperative, functional, and event-driven
e vent-driven aspects. It also
allows for dynamic typing and unmanaged code in small doses when needed.
WHAT I󰁓 .NET?
C# is built upon a thing called .NET (pronounced “dot net”). .NET is often called a framework
f ramework
or platform, but .NET is the entire ecosystem surrounding C# programs and the programmers
that use it. For example, .NET includes a runtime, which is the environment your C# program
runs within. Figuratively speaking, it is like the air your program breathes and the ground it
stands on as it runs. Every programming language has a runtime of one kind or another, but
the .NET runtime is extraordinarily capable, taking a lot of burden off of you, the programmer.
programmer.

.NET
calledalso
the includes a pile
Base Class of code
Library that
(BCL you can
). You can use inof
think your
thisprogram directly.
like mission Thissupporting
control collection isa
rocket launch: a thousand people who each know their specific job j ob well, ready to jump in and
support the primary mission (your code) the moment they are needed. For example, you
won’t have to write your own code to open files or compute a square root because be cause the Base
Class Library can do this for you.
.NET includes a broad set of tools called a Software Development Kit (SDK) that makes
programming life easier.
easier.
.NET also includes things to help you build specific kinds of programs like web, mobile, and
desktop applications.
.NET is an ecosystem shared by other programming languages. Aside from C#, the three other
most popular languages are Visual Basic, F#, and PowerShell. You could write code in C# and
use it in a Visual Basic program. These languages have many similarities because of their
shared ecosystem,
ecosystem, and I’ll point these out in some cases.

Knowledge Check C# 25 XP
Check your knowledge with the following questions:
1. True/False. C# is a special-purpose language optimized for making web applications.
2. What is the name
name of the framework
framework that C# runs
runs on?
Answers: (1) False. (2) .NET

LEVEL 2
GETTING AN IDE
󰁓peedrun
• Programming is complex; you want an IDE to make programming life easier.
• Visual Studio is the most used IDE for C# programming. Visual Studio Community is free, feature-
rich, and recommended for beginners.
• Other C# IDEs exist, including Visual Studio Code and Rider.

Modern-day programming is complex and challenging, but a programmer does not have to
go alone. Programmers work with an extensive collection of tools to help them get their job
done. An integrated development environment ( IDE) is a program that combines these tools
into a single application designed to streamline the programming process. An IDE does for
programming what Microsoft WordWord does for word processing
p rocessing or Adobe Photoshop for image
editing. Most programmers will use an IDE as they work.
There are several C# IDEs to choose from. (Or you can go without one and use the raw tools
directly; I don’t recommend that for new programmers.)
p rogrammers.) W Wee will look at the most popular C#
IDEs and discuss their strengths and weaknesses in this level.
We’ll use an IDE to program in C#. Unfortunately,
We’ll Unfortunately, every IDE is different,
different, and this book cannot
cover them all. While this book focuses on the C# language and not a specific IDE, when
necessary, this book will illustrate
ill ustrate certain tasks using Visual Studio Community Edition. Feel
free to use a different IDE. The C# language itself
i tself is the same regardless of which IDE you pick,
but you may find slight differences when performing
per forming a task in the IDE. Usually, the process is
intuitive, and if tinkering fails, Google usually knows.

A COMPARI󰁓ON OF IDE󰁓
There are several notable IDEs that you can choose from.

12 LEVEL 2 GETTING AN IDE

Visual 󰁓tudio
Microsoft Visual Studio is the stalwart, tried-and-true IDE used by most C# developers. Visual
Studio is older than even C#, though it has grown up a lot since those days.
Of the IDEs we discuss here, this is the most feature-rich and capable, though it has one
significant drawback:
drawback: it works on Windows but not Mac or Linux.
Visual Studio comes in three different “editions” or levels: Community, Professional, and
Enterprise. The Community and Professional editions have the same feature set, while
Enterprise has an expanded set with some nice bells and whistles at extra cost.
The difference between the Community Edition and the Professional Edition is only in the
cost and the license. Visual Studio Community Edition is free but is meant for students,
hobbyists,, open-source projects, and individuals, even for commercial use. Large companies
hobbyists
do notmore
make fit into this$1
than category
millionand
an d must buy
annually, Professional.
or have more than If five
you Visual
have more tha
than
Studio n 250 computers,
users, you’ll need
to pay for Professional. But that’s almost certainly not you right
r ight now.
Visual Studio Community edition is my recommendation for new C# programmers running
on Windows and is what this book uses throughout.

Visual 󰁓tudio Code


Microsoft Visual Studio Code is a lightweight editor (not a fully-featured IDE) that works on
Windows, Mac, and Linux. Visual Studio Code is free and has a vibrant community. It does
not have the same expansive feature set as Visual Studio, and in some places, the limited
feature set is harsh; you sometimes have to run commands on the command line. If you are
used to command-line interfaces, this cost is low. But if you’re new to programming, it may
feel alien. Visual Studio Code is probably your best bet if Visual Studio isn’t an option for you
(Linux and Mac, for example), especially if you have experience using the command line.
Visual Studio Code can also run online (vscode.dev ), but as of right now, you can’t run your
code. (Except by purchasing a codespace via github.com
github.com..) Perhaps this limitation will be fixed
someday soon.

Visual 󰁓tudio for Mac


Visual Studio for Mac is a separate IDE
IDE for C# programming
programming that
that works
works on Mac. While itit shares
its name with Visual Studio, it is a different product with many notable differences. Like Visual
Studio (for Windows), this has Community, Professional, and Enterprise editions. If you are
on a Mac, this IDE is worth considering.
c onsidering.

JetBrains Rider
The only non-Microsoft IDE on this list is the Rider IDE from JetBrains. Rider is comparatively
new, but JetBrains is very experienced at making IDEs for other languages. Rider does not
have a free tier; the cheapest option is about $140 per year. But it is both feature-rich and cross-
platform. If you have the money to spend, this is a good choice on any operating system.

Other IDEs
There are other IDEs out there, but most C# programmers use one of the above. Other IDEs
tend to be missing lots of features, aren’t well supported, and have less online help and
documentation. But if you find another IDE that you enjoy, go for it.

INSTALLING VISUAL STUDIO 13

Online Editors
There are a handful of online C# editors that you can use to tinker with C# without
downloading tools. These have certain limitations and often do not keep up with the current
language version. Still, you may find these useful if you just want to experiment without a huge
commitment. An article on the book’s website (csharpplayersguide.com/articles/online-
editors)) points out some of these.
editors

No IDE
Y
You
ou do not need an IDE to program in C#. If you are a veteran programmer,
programmer, skilled at using
the command line, and accustomed to patching together different editors and scripts,
sc ripts, you can
skip the IDE. I do not recommend this approach for new programmers. It is a bit like building
your car from parts before you
you can drive it. For the seasoned mechanic,
mechanic, this may be part of the
the
enjoyment. Everybody else needs something that they can hop in and go. The IDEs above are
in that category. Working without an IDE requires using the dotnet command-line tool to
create, compile, test, and package your programs. Even if you use an IDE, you may still find
the dotnet tool helpful. (If you use Visual Studio Code, you will need to use it occasionally.)
But if you are new to programming, start with an IDE and learn
l earn the basics first.

IN󰁓TALLING VI󰁓UAL 󰁓TUDIO


This book’s focus is the C# language itself, but when I need to illustrate a task in an IDE, this
book uses Visual Studio Community Edition. The Professional and Enterprise Editions should
be identical. Other IDEs are usually similar
similar,, but you will find differences.

Visual
how toStudio Code
Codewith
get started is popular enough that I p
posted
osted an article on the book’s
book’s website
website illustrating
it: https://csharpplayersguide.com/articles/visual-studio-codeillustrating
https://csharpplayersguide.com/articles/visual-studio-code. .
Y
You
ou can download Visual
Vi sual Studio Community Edition from https://visualstudio.microsoft.
com/downloads.. You will want to download Visual Studio 2022 or newer to use all of the
com/downloads
features in this book.
Note that this will download the Visual Studio Installer rather than Visual Studio itself. The
Visual Studio Installer lets you customize which components Visual Studio has installed.
Anytime you want to tweak the available features, you will rerun the installer and make the
desired changes.
As you begin installing
installing Visual Studio,
Studio, it will ask you which components
components to include:

14 LEVEL 2 GETTING AN IDE

With everything installed, Visual Studio is a lumbering,


l umbering, all-powerful behemoth. You do not
need all possible features of Visual Studio.
Studio. In fact, for this book, we will only need a small slice
of what Visual Studio offers.
Y
You
ou can install anything you find interesting, but there
there is only one item you must install for the
code in this book. On the Workloads tab, find the one called .NET desktop development and
click on it to enable it. If you forget to do this, you can always re-run the Visual Studio Installer
and change what components you have installed.
Warning! Be
Warning! B e sure you get the right workload installed. If you don’t, you won’t be able to
use all of the C# features described in this book.
Once Visual Studio is installed, open it. You may end up with a desktop icon, but you can
always find it in the Windows Start Menu under Visual Studio 2022.
Visual Studio will ask you to sign in with a Microsoft account, even for the free Community
Edition. You
You don’t need to sign in if you don’t want to, but it does enable a few minor features
like synchronizing your settings across multiple devices.
If you are installing Visual Studio for the first time, you will also get a chance to pick
development settings—keyboard shortcuts and a color theme. I have used the light theme in
this book because it looks clearer in print. Many developers like the dark theme. Whatever you
pick can be changed later.
Y
You
ou know you are
are done when you make
make it to the launch screen shown
shown below:

Challenge Install Visual 󰁓tudio 75 XP


As your journey begins, you must get your tools ready to start programming in C#. Install Visual Studio

2022 Community edition (or another IDE) and get it ready to start programming.

LEVEL 3
HELLO WORLD: YOUR FIR󰁓T PROGRAM
󰁓peedrun
• New projects usually begin life by being generated from a template.
• A C# program starts running in the program’s entry point or main method.
• A full Hello World program looks like this: Console.WriteLi
Console.WriteLine("Hello,
ne("Hello, World ");
• Statements are single commands for the computer to perform. They run one after the next.
• Expressions allow you to define a value that is computed as the program runs from other elements.
• Variables let you store data for use later.
• Console.ReadLine() retrieves a full line of text that a user types from the console window.

Our adventure begins in earnest in this level, as we make our first real programs in C# and
learn the basics of the language. We’ll start with a simple program called Hello World, the
classic first program to write in any new language. It is the smallest meaningful program we
could make. It gives us a glimpse of what the language looks like and verifies that our IDE is
installed and working. Hello World is the traditional first program to make, and beginning
anywhere else would make the programming gods mad. We don’t want that!

CREATING A NEW PROJECT


A C# project is a combination of two things. The first
first is your C# source code—instructions you
write in C#
C# for the computer to run. The second is configuration—instructions
configuration—instructions you give to the
computer to help it know how to compile or translate C# code into the binary
binar y instructions the
computer can run. Both of these live in simple text files on your computer
computer.. C# source code files
use the .cs extension. A project’s configuration uses the .csproj extension. Because these are
both simple text files, we could handcraft them ourselves if needed.
nee ded.
But most C# programs are started by being generated from one of several templates. T Templat
emplateses
are standard starting points; they help you get the configuration right for specific project types
and give you some starting code. We will use a template
templ ate to create our projects.

16 LEVEL 3 HELLO WORLD: YOUR FIRST PROGRAM

Y
You
ou may be tempted to skip over thisthis section, assuming
assuming you can
can just figure it out. Don’t! There
There
are several pitfalls here, so don’t skip this section.
Start Visual Studio so that you can see the launch screen below:
Click on the Create a new project button on the bottom right. Doing this advances you to the
Create a new project page:

There are many templates to choose from, and your list might not exactly match what you see
above. Choose the C# template called Console Application.
Application.
Warning! You want the C# project called Console Application.
Warning! Appli cation. Ensure you aren’t getting
the Visual Basic one (check the tags below the description). Also, make sure you aren’t
getting the Console Application (.NET Framework) one, which is an older template. If you
don’t see this template, re-run the installer and add the right workload.
We will always use this Console Application template in this book, but you will use other
templates as you progress in the C# world.
After choosing the C# Console Application template, press the Next button to advance to a
page that lets you enter your new program’
program’ss details:

A BRIEF TOUR OF VISUAL STUDIO 17

Always give projects a good name.


name. You
You won’t remember what ConsoleApp12 did in two weeks.
weeks.
For the location, pick a spot that you can find later on. (The default location is fine, but it isn’t
a prominent spot, so note where it is.)
There is also a checkbox for Place solution and project in the same directory. For small
projects, I recommend checking this box. Larger programs (solutions) may be formed from
many projects. For those, putting projects in their own directory (folder) under a solution
directory makes sense. But for small programs with a single project, it is simpler just to put
everything in a single folder.
Press the Next button to choose your target framework on the final page:

Make sure you pick .NET 6.0 for this book! We will be using many .NET 6 features. You can
change it after creation, but it is much easier to get
g et it right in the first place.
Once you have chosen the framework, push the Create button to create the project.
Warning! Make sure you pick .NET 6.0 (or newer), so you can take advantage of all of the
Warning!
C# features covered in this book.

A BRIEF TOUR OF VI󰁓UAL 󰁓TUDIO


With a new project
project created, we get our first glimpse a
att the Visual S
Studio
tudio window:

18 LEVEL 3 HELLO WORLD: YOUR FIRST PROGRAM

Visual Studio is extremely capable, so there is much to explore. This book focuses on
programming in C#, not becoming a Visual Studio expert. We won’t get into every detail of
Visual Studio,
Studio, but we’ll cover
cover some essential elements here and throughout the book.
Right now, there are three things you need to know to get started. First, the big text editor on
the left side is the Code Window or the Code Editor. You
You will spend most of your time working
here.
Second, on the right side is the Solution Explorer. That shows you a high-level view of your
code and the configuration needed to turn it into working code. You will spend only a little
time here initially, but you will use this more as you begin to make larger programs.
Third, we will run our programs using the part of the Standard Toolbar shown below:

Bonus Level A covers


levels whenever Visual
you are Studio
ready inEven
for it. morethough
depth. You
theycan
areread that
at the endlevel andbook,
of the the other
theybonus
don’t
require knowing everything else before them. If you’re new to Visual Studio, consider reading
Bonus Level A before too long. It will give you a better feel for Visual Studio
Studio..
Time for a sanity check. If you don’t see code in the Code Window, double click on
Program.cs in the Solution Explorer. Inspect
Inspect the code you see in the Code Window. If you see
class Program or static void Main, or if the file has more than a couple of lines of
text, you may have chosen the wrong template. Go back and ensure you pick the correct
template. If the right template isn’t there, re-run the installer to add the right workload.

COMPILING AND RUNNING YOUR PROGRAM


Generating a new project from the template has produced a complete program. Before we
start dissecting it, let’s run it.
The computer’s circuitry cannot run C# code itself. It only runs low-level binary instructions
formed out of 1’s 1’s and 0’s.
0’s. So before the computer
c omputer can run our program, we must transform it
into something it can run. This transformation is called compiling, done by a special program
called a compiler. The compiler takes your C# code and your project’s configuration and
produces the final binary instructions that the computer can run directly. The result is either
a .exe or .dll file, which the computer
c omputer can run. (This is an oversimplification, but it’
it’ss accurate
enough for now.)

SYNT
SYNTAX
AX AND STRUCTURE 19

Visual Studio makes it easy to compile and then immediately run your program with any of
the following: (a) choose Debug > Start Debugging from the main menu, (b) press F5
F5,, or (c)
push the green start button on the toolbar, shown below:

When you run your program,


program, you
you will see a black and white
white console window appear:
Look at the first line:
Hello, World!

That’s what our program was supposed to do! (The rest of the text just tells you that the
program has ended and gives you instructions on how not to show it in the future. You can
ignore that text for now.)

Challenge
Challe nge Hello, World! 50 XP
You open
shore not your eyes
far off. A and
voicefind yourself
nearby callsface
out,down
“Hey,on theYou’re
you! beachfinally
of a large island,
awake!” Youthesitwaves crashing
up and on the
look around.
Somehow, opening your IDE has pulled you into the Realms of C#, a strange and mysterious land where
it appears that you can use C# programming to solve problems. The man comes closer, examining you.
“Are you okay? Can you speak?” Creating and running a “Hello, World!” program seems like a good way
to respond.
Objectives:
• Create a new Hello World program from the C# Console Application template, targeting .NET 6.
• Run your program using any of the three methods described above.

󰁓YNTAX AND 󰁓TRUCTURE


Now that we’ve made and run our first C# program, it is time to look at the fundamental
elements of all C# programs. We will touch on many topics in this section, but each is covered
in more depth later in this book. You don’t need to master it all here.
Every programming language has its own distinct structure—its own set of rules that describe
desc ribe
how to make a working program in that language. This set of rules is called the language’s
syntax.
Look in your Code Editor window to find the
th e text shown below:
Console.WriteLine("Hello, World!");

20 LEVEL 3 HELLO WORLD: YOUR FIRST PROGRAM

Y
You g reen text that starts with two slashes (//). That is a comment.
ou might also see a line with green
We’ll
We’ll talk about comments in Level 4, but you can ignore or even delete tthat
hat line for now.
We’re going to analyze this one-line
We’re on e-line program in depth. As short as it is, it reveals a great deal
about how C# programming works.

󰁓trings and Literals


First, the "Hello, World " part is the displayed text. You can imagine changing this text to
get the program to show something else instead.
In the programming world, we often use the word string to refer to text for reasons we’ll see
later. There are many ways we can work with strings or text, but this is the simplest. This is
called a literal, or specifically, a string literal. A literal is a chunk of code that defines some
specific value, precisely as written. Any text in double quotes will be a string literal. The quote
marks aren’t part of the text. They just indicate
in dicate where the string literal begins an
andd ends. Later
on, we’ll see how to make other types of literals, such as number literals.
Identifiers
The two other big things in our code are Console and WriteLine. These are known formally
as identifiers or, more casually, as names. An identifier allows you to refer to some existing
code element. As we build code elements
ele ments of our own, we will pick names for them as well, so
we can refer back to them. Console and WriteLine both refer to existing code elements.

Hierarchical Organization
Between Console and WriteLine, there is a period ( .) character. This is called the member
access operator or the dot operator. Code elements like Console and WriteLine are
organized hierarchically. Some code elements live inside of other code elements. They are
said to be members or children of their container. The dot operator allows us to dig down in
the hierarchy, from the big parts to their children.
In this book, I will sometimes illustrate this hierarchical organization using a diagram like the
one shown below:

I’ll refer tosimilar


generate this type of diagram
drawings, but as a code sketch
I usually map inthem
this book. Some
by hand if I versions
need one.of Visual Studio can
These code maps can help us see the broad structure of a program, which is valuable. Equally
important is that a code map can help us understand when a specific identifier can be used.
The compiler must determine which code element
eleme nt an identifier refers to. This proces
processs is called
name binding. But don’t let that name scare you. It really is as simple as, “When the code says,
WriteLine, what exactly is that referring to?”
Only a handful of elements are globally available. We can start with Console, but we can’t
just use WriteLine on its own. The identifier WriteLine is only available in the context of
its container, Console.

SYNT
SYNTAX
AX AND STRUCTURE 21

Classes and Methods


You may have noticed that I used a different icon for Console and WriteLine in the code
You
map above. Named Specifically, Console is a
Named code elements come in many different flavors. Specifically,
class, while WriteLine is a method . C# has rules that govern what can live inside other things.
For example, a class can have methods as members, but a method cannot have a class as a
member.
We’ll talk about both methods and classes at great
We’ll great length in this book, but let’s
let’s start with some
basic definitions to get us started.
For now, think of classes as entities that solve a single problem or perform a specific job or
role. It is like a person on a team. The entire workload is spread across many people, and
an d each
one performs their job and works with others to achieve the overarching goal. The Console
class’s
anythingjobelse—it
is to interact withhow
only knows the to
console window.
work with It does
the console
cons that well, but don’t ask it to do
ole window.
Classes are primarily composed of two things: (1) the data they need to do their job and (2)
tasks they can perform. These tasks come in the form of methods, and WriteLine is an
example. A method is a named, reusable block of code that you can request to run.
WriteLine’s task is to take text and display it in the console window on its own line.
The act of asking a method to run is called method invocation or a method call. These method
calls or invocations are performed by using a set of parentheses after the method name, which
c ode contains WriteLine(...) .
is why our one line of code
Some methods require data to perform their task. WriteLine works that way. It needs to
know what text to display. This data is supplied to the method call by placing it inside the
parentheses, as we have seen with WriteLine("Hello, World "). Some methods don’t
need any extra information, while others need multiple pieces of information. We will see
examples of those soon. Some methods can also return information when they finish,
allowing data to flow to and from a method call. We’ll
We’ll soon see examples
e xamples of that as well.

Namespaces
All methods live in containers like
like a class,
class, but even most classes live in other containers
containers called
namespaces. Namespaces are purely code organization tools, but they are valuable when
c lasses. The Console class lives in a namespace called
dealing with hundreds or thousands of classes.
System. If we add this to our code map, it looks like this:

In code, we could have referred to Console through its namespace name. The following code
is functionally identical to our earlier code:
System.Console.WriteLine("Hello, World!");

Using C# 10 features and the project template we chose, we can skip the System. In older
have somehow needed to account for System. One way to account
versions of C#, we would have

22 LEVEL 3 HELLO WORLD: YOUR FIRST PROGRAM

for it was shown above. A second way is with a special line called a using directive. If you
stumble into older C# code online or elsewhere, you may notice that most old C# code files
start with a pile of lines that look like this:
using System;

These lines tell the compiler, “If you come across an identifier, look in this namespace for it.”
it.”
It allows you to use a class name
n ame without sticking the namespace name in front of it. But with
C# 10, the compiler will automatically search System and a handful of other extremely
common namespaces without you needing to call it out.
For the short term, we can almost ignore namespaces entirely. (We’ll cover them in more
depth in Level 33.)
33. ) But namespaces are an important element of the code structure, so even
though it will be a while before we need to deal with namespaces directly, I’m still going to
call out which namespaces things live in as we encounter them. (Most of it will be the System
namespace.)
The Base Class Library
Our code map is far from complete. System, Console, and WriteLine are only a tiny slice
of the entire collection of code called the Base Class Library (BCL). The Base Class Library
contains many namespaces, each with many classes, each with many members. The code
map below fleshes this out a bit more:

It is huge! If we drew the complete diagram, it might be longer


long er than this whole book!
The Base Class Library provides every C# program with a set of fundamental building bl
blocks.
ocks.
We won’t cover
c over every single method or class in the Base Class Library, but we will cover its
most essential parts throughout this book (starting with Console).

Program and Main


The code we write also adds new code elements. Even our simple Hello World program adds
new code elements that we could show in a code map:

SYNT
SYNTAX
AX AND STRUCTURE 23

The compiler takes the code we write, places it inside a method called Main, and then puts
that inside a class called Program, even though we don’t see those names in our code.
c ode. This is
a slight simplification; the compiler uses a name you can’t refer to (<Main>$), but we’ll use
the simpler name Main for now.
In the code map above, the icon for Main also has a little black arrow to indicate that Main is
the program’s entry point. The entry point or main method is the code that will automatically
automatically
run when the computer runs your program. Other methods won’t run unless the main
method calls them, as our Hello World program does with WriteLine.
wr ite out code to define both Program and Main. You rarely
In the early days of C#, you had to write
need to do so now, but you can if you want (Level 33). 33).

󰁓tatements
We have program except the semicolon (;)
have accounted for every character in our Hello World program
at the end. The entire Console.Writ
Console.WriteLine("Hell
eLine("Hello,
o, World "); line is called a
statement. A statement is a single step or command for the computer to run. Most C#
statements end with a semicolon.
This particular statement instructs the computer to ask the Console class to run its
WriteLine method, giving it the text "Hello, World " as extra information. This “ask a
thing to do a thing” style of statement is common, but it is not the only kind. We will see others
as we go.
Statements don’t have names, so we won’t put them in a code map.
map.
Statements are an essential building block of C# programs. You instruct the computer to
perform a sequence of statements one after the next. Most programs have many statements,
which are executed
e xecuted from top to bottom and left to right (though C# programmers rarely put
more than one statement on a single line).
l ine).
One thing that may surprise new programmers is how specific you need nee d to be when giving
g iving the
computer statements to run. Most humans can be given vague instructions and make
judgment calls to fill in the gaps. Computers have no such capacity.
capacity. They do exactly what
what they
are told without variation. If it does something unexpected, it isn’t that the computer made a
mistake. It means what you thought you commanded and what you actually commanded were
not the same. As a new programmer,
programmer, it is easy to think, “The computer isn’t doing what I told
it!” Instead, try to train your mind to think, “Why did the computer do that instead of what I
expected?” You will be a better programmer with that mindset.

Whitespace
C# ignores whitespace (spaces, tabs,
tabs, newlines) as long as it can tell where one thing ends and
the next begins. We could have written the above line like this, and the compiler wouldn’t care:
Console . WriteLine
( "Hello, World!"
)
;

But which is easier for you to read? This is a critical


c ritical point about writing code: You
You will spend
more time reading code than writing it. Do yourself a favor and go out of your way to make
code easy to understand,
under stand, regardless of what the compiler will tolerate.

24 LEVEL 3 HELLO WORLD: YOUR FIRST PROGRAM

Challenge What Comes Next 50 XP


The man seems surprised that you’ve produced a working “Hello, World!” program. “Been a while since I
saw somebody program like that around here. Do you know what you’re doing with that? Can you make
it do something besides just say ‘hello’?”
‘hello’?”
Build on your original Hello World program with the following:
Objectives:
• Change your program to say something besides “Hello, World!”

BEYOND HELLO WORLD


With an understanding of the basics behind us, let’
l et’ss explore a few other essential features of
C# and make a few more
m ore complex programs.

Multiple 󰁓tatements
A C# pr
program
ogram runs one statement
statement at
at a time in the order they appear in the file. Putting
Putting multiple
statements into your program makes it do multiple things. The following code displays three
lines of text:
Console.WriteLine("Hi there!");
Console.WriteLine("My name is Dug.");
Console.WriteLine("I have just met you and I love you.");

Each line asks the Console class to perform its WriteLine method with different data. Once
all statements in the program have been completed, the program ends.

Challenge The Makings of a Programmer 50 XP


The man, who tells you his name is Ritlin, asks you to follow him over to a few of his friends, fishing on
the dock. “This one here has the makings of a Programmer!” Ritlin says. The group looks at you with eyes
widening and mouths agape. Ritlin turns back to you and continues, “I haven’t seen nor heard tell of
anybody who can wield that power in a million clock cycles of the CPU. Nobody
Nobody has been able to do that
since the Uncoded One showed up in these lands.” He describes the shadowy and mysterious Uncoded
One, an evil power that rots programs and perhaps even the world itself. The Uncoded One’s presence
has prevented anybody from wielding the power of programming, the only thing that might be able to
stop it. Yet
Yet somehow, you have been able to grab
gr ab hold of this power anyway. Ritlin’s companions suddenly
s uddenly
seem doubtful. “Can you show them what you showed me? Use some of that Programming of yours to
make a program? Maybe something with more than one statement in it?”
Objectives:
• Make a program with 5 Console.WriteLine statements in it.
• Answer this question: How many statements do you think a program can contain?

Expressions
Our next building block is an expression. Expressions are bits of code that your program must
process or evaluate to determine their value. We use the same word in the math world to refer

BEYOND HELLO WORLD 25


to something like 3 + 4 or -2 × 4.5. Expressions describe how to produce a value from smaller
elements. The computer can use an expression to compute a value as it runs.
C# programs use expressions
expressions heavily. Anywhere a value is needed, an expression can be put
in its place. While we could do this:
Console.WriteLine("Hi User");

We can
can also use an expression
expression instead:
Console.WriteLine("Hi " + "User");

The code "Hi " + "User" is an expression rather than a single value. As your program
runs, it will evaluate the expression to determine its value. This code shows that you can use
+ between two bits of text to produce the combined text ( "Hi User").
The + symbol is one of many tools that can be used to build expressions. We will learn more
as we go.
Expressions are powerful because they can be assembled out of other, smaller expressions.
Y
You singl e value like "Hi User" as the simplest type of expression. But if we
ou can think of a single
we could split "User" into "Us" + "er" or even into "U" + "s" + "e" + "r".
wanted, we
That isn’t very practical, but it does illustrate how you can build expressions out of smaller
expressions. Simpler expressions are better than complicated ones that do the same job, but
you have lots of flexibility when you need it.
Every expression, once evaluated, will result in a single new value. That single value can be
used in other expressions or other parts of your code.
c ode.

Variables
Variables are containers for data. They are called variables because their contents can change
or vary as the program runs. Variables allow us to store data for later use.
Before using a variable, we must indicate that we need one. This is called declaring the
variable. In doing so, we must provide a name for the variable and indicate its type. Once a
variable exists, we can place data in the variable
variable to use later.
later. Doing so is called assignment , or
assigning a value to the variable. Once we have done that, we can use the variable in
expressions later.
later. All of this is shown below:
string name;
name = "User";
Console.WriteLine("Hi " + name);

The first line declares the variable with a type and a name. Its type is string (the fancy
programmer word for text), and its name is name. This line ensures we have a place to store
text that we can refer to with the identifier name.
The second line assigns it a value of "User".
We use the variable in an expression on the final line. As your program runs, it will evaluate
the expression "Hi " + name by retrieving the current value in the name variable, then
combining it with the value of "Hi ". We’ll see plenty more examples of expressions and
variables soon.
Anything with a name can be visualized on a code map, and this name variable is no
exception. The following code map shows this variable inside of Main, using a box icon:

26 LEVEL 3 HELLO WORLD: YOUR FIRST PROGRAM

In Level 9, we’ll see why it can be helpful to visualize where variables fit on the code ma
map.
p.
You may notice that when you type string in your editor, it changes to a different color
You
(usually blue). That is because string is a keyword. A keyword is a word with special
meaning in a programming language. C# has over 100 keywords! We’ll discuss them all as we
go.
Reading Text from the Console
Some methods produce a result as a part of the job they were designed to do. This result can
be stored in a variable or used in an expression. For example, Console has a ReadLine
method that retrieves text that a person types until they hit the Enter key. It is used like so:
Console.ReadLine()

ReadLine does not require any information to do its job, so the parentheses are empty. But
the text it sends back can be stored in a variable or used in an expression:
string name;
Console.WriteLine("What is your name?");
name = Console.ReadLine();
Console.WriteLine("Hi " + name);

This code no longer displays the same text every time. It waits for the user to type in their name
and then greets them by name.
When a method produces a value, programmers
programmers say it returns the value. So you might say that
Console.ReadLine() returns the text the user typed.
Challenge
Challe nge Consolas
Consola s and Telim 50 XP
These lands have not seen Programming in a long time due to the blight of the Uncoded One. Even old
programs are now crumbling to bits. Your skills with Programming are only fledgling now, but you can
still make a difference in these people’s lives. Maybe someday soon, your skills will have grown strong
enough to take on the Uncoded One directly. But for now, you decide to do what you can to help.
In the nearby city
c ity of Consolas, food is running shor
short.
t. Telim
Telim has a magic oven that can produce bread from
thin air. He is willing to share, but Telim is an Excelian, and Excelians love paperwork; they demand it for
all transactions—no exceptions. Telim will share his bread with the city if you can build a program that
lets him enter the names of those receiving it. A sample run of this program looks like this:
Bread is ready.
Who is the bread for?
RB
Noted: RB got bread.

Objectives:

COMPILER ERRORS
ERRORS,, DEBUGGER
DEBUGGERS,
S, AND CONFIGURATIONS 27
• Make a program that runs as shown above, including taking a name from the user.

COMPILER ERROR󰁓, DEBUGGER󰁓, AND CONFIGURATION󰁓


There are a few loose ends that we should tie up before we move on: compiler errors,
debugging, and build configurations. These are more about how programmers construct C#
programs than the language itself.

Compiler Errors and Warnings


As you write C# programs, you will sometimes accidentally write code that the compiler
cannot figure out. The compiler will not be able to transform your code into something the
computer can understand.
When this happens, you will see two things. When you try to run your program, you will see
the Error List window appear,
appear, listing problems that the compiler sees. D
Double-clicking
ouble-clicking on an
error takes you to the problematic line. You will also see broken code underlined with a red
squiggly line. You may even see this appear as you type.
Sometimes, the problem and its solution are apparent. Other times, it may not be so obvious.
Bonus Level B provides suggestions for what to do when you cannot get your program to
compile. As with all of the bonus levels, feel free to jump over and do it whenever you have an
interest or need. You do not need to wait until you have completed all the levels before it.
If you’re watching your code closely, you might have already seen the compiler error’s less-
scary cousin: the compiler warning. A compiler warning means the compiler can make the
code work, but it thinks it is suspicious. For example, when we do something like string
name = Console.ReadLine();, you may have noticed that you get a warning that states,
“Converting null literal or possible null value to a non-nullable type.” That code even has a
green squiggly mark under it to highlight the potential problem.
This particular warning is trying to tell you that ReadLine may not give you any response
back (a lack of value called null, which is distinct from text containing no characters). We’ll
learn how to deal with these missing values in Level 22. For now, you can ignore this particular
compiler warning; we won’t be doing anything that would cause it to happen.

Debugging
Writing code that the compiler can understand is only the first step. It also needs to do what
you expected it to do. Trying to figure out whywhy a program does not do what you expected a and
nd
then adjusting it is called debugging. It is a skill that takes practice, but Bonus Level C will show
you the tools you can use in Visual Studio to make this task less intimidating. Like the other
bonus levels, jump over and read this whenever you have an interest or a need.

Build Configurations
The compiler uses your source code and configuration data to produce software the computer
can run. In the C# world, configuration data is organized into different build configurations.
Each configuration provides different information to the compiler about how to build things.
There are two configurations defined by default, and you rarely need more. Those
configurations are the Debug configuration and the Release
Rele ase configuration. The two are mostly
the same. The main difference is that the Release configuration has optimizations turned on,

28 LEVEL 3 HELLO WORLD: YOUR FIRST PROGRAM


which allow the compiler to make certain adjustments so that your code can run faster
without changing what it does. For example, if you declare a variable and never use it,
optimized code will strip it out. Unoptimized code will leave it in. The Debug configuration
has this turned off. When debugging your code, these optimizations can make it harder to
hunt down problems. As you are building your program, it is usually better to run with the
Debug configuration. When you’re ready to share your program with others, you compile it
with the Release configuration instead.
Y
You
ou can choose which configuration you’re using by picking it from the toolbar’s dropdown
list, near where the green
g reen arrow button is to start your program.
LEVEL 4
OMMENT󰁓
C
󰁓peedrun
• Comments let you put text in a program that the computer ignores. They can provide information
to help programmers understand or remember what the code does.
• Anything after two slashes (//) on a line is a comment, as is anything between /* and */.

Comments are bits of text placed in your program, meant to be annotations on the code for
humans—you and other programmers. The compiler ignores comments.
Comments have a variety of uses:
• Y
You
ou can add a description about
about how some tricky
tricky piece of code works,
works, so you don’t have
have
to try to reverse engineer it
i t later.
later.
• Y
You
ou can leave
leave reminders in your code of things you still
still need to do.
do. These are sometimes
called TODO comments.
• Y
You
ou can add documentation about how some specific thing should be used or works.
Documentation comments like this can be handy because somebody (even yourself) can
look at a piece of code and know how it works without needing to study every line of code.
• They are sometimes used to remove code from the compiler’s view temporarily. For

example,
commentsuppose some
until you’re codetoisbring
ready not working. YouThis
it back in. canshould
temporarily turn
only be the code into
temporary! Don’ta
leave large chunks of commented-out code hanging around.
There are three ways to add a comment, though we will only discuss two of them here and
save the third for later.
You can start a comment anywhere within your code by placing two forward slashes (//).
You
After these two slashes, everything on the line will become a comment,
comm ent, which the compiler
will pretend doesn’t exist.
exist. For example:
// This is a comment where I can describe what happens next.
Console.WriteLine("Hello, World!");

Console.WriteLine("Hello again!"); // This is also a comment.

30 LEVEL 4 COMMENTS
Some programmers have strong preferences for each of those two placements. My general
rule is to put important comments above the code and use the second placem
placement
ent (on the same
line) only for side notes about that line of code.
Y
You by putting it between a /* and */:
ou can also make a comment by
Console.WriteLine("Hi!"); /* This is a comment that ends here... */

Y
You
ou can use this to make both multi-line
multi-line comments and embedded comments:
/* This is a multi-line comment.
It spans multiple lines.
Isn't it neat? */

Console.WriteLine("Hi " /* Here comes the good part! */ + name);

That second example is awkward but has its uses, such as when commenting out code that
you want to ignore temporarily).
Of course, you can make multi-line comments with double-slash comments; you just have to
put the slashes on every line. Many C# programmers prefer double-slash comments over
multi-line /* and */ comments, but both are common.

HOW TO MAKE GOOD COMMENT󰁓


The mechanics of adding comments are simple enough. The real challenge is in making
meaningful comments.
My first suggestion is not to let TODO or reminder comments (often in the form of // TODO:
Some message here) or commented-out code last long. Both are meant to be short-lived.
They have no long-term benefit and only clutter the code.
Second, don’t say things that can be quickly gleaned from the code itself. The first comment
below adds no value, while the second one does:
// Uses Console.WriteLine to print "Hello, World!"
Console.WriteLine("Hello, World!");

// Printing "Hello, World!" is a common first program to make.


Console.WriteLine("Hello, World!");

The second comment explained why this was done, which isn’t apparent from the code itself.
Third, write comments roughly at the same time as you write the code. You will never
remember what the code did three weeks from now, so don’t wait to describe what it does.
Fourth, find the balance in how much you comment. It is possible to add both too few and too
many comments. If you can’t make sense of your code when you revisit it after a couple of
weeks, you
you probably
probably aren’t
aren’t commenting enough.
enough. If you keep discovering that
that comments have
gotten out of date, it is sometimes an indication that you are using too many comments or
putting the wrong information in comments. (Some corrections are to be expected as code
evolves.) As a new programmer, the consequences of too few comments are usually worse
than too many comments.
Don’t use comments to excuse hard-to-read code. Make the code easy to understand first,
then add just enough comments to clarify any important but unobvious details.

HOW TO MAKE GOOD COMMENTS 31

Challenge The Thing Namer 3000 100 XP


As you walk through the city of Commenton, admiring its forward-slash-based architectural buildings, a
young man approaches you in a panic. “I dropped my Thing Namer 3000 and broke it. I think it’s mostly
working, but all my variable names got reset! I don’t understand what they do!” He shows you the
following program:
Console.WriteLine("What kind of thing are we talking about?");
string a = Console.ReadLine();
Console.WriteLine("How would you describe it? Big? Azure? Tattered?");
string b = Console.ReadLine();
string c = "of Doom";
string d = "3000";
Console.WriteLine("The " + b + " " + a + " of " + c + " " + d + "!");

“You gotta help me figure it out!”


Objectives:
• Rebuild the program above on your computer.
• Add comments near each of the four variables that describe what they store. You must use at least
one of each comment type (// and /* */).
• Find the bug in the text displayed and fix it.
• Answer this question: Aside from comments, what else could you do to make this code more
understandable?

LEVEL 5
VARIABLE󰁓

󰁓peedrun
• A variable is a named location in memory for storing data.
• Variables have a type, a name, and a value (contents).
• Variables are declared (created) like this: int number;.

Assigning values to variables is done with the assignment operator: number = 3;


• Using a variable name in an expression will copy the value out of the variable.
• Give your variables good names. You will be glad you did.

In this level, we will look at variables in more depth. We will also look at some rules around
good variable names.

WHAT I󰁓 A VARIABLE?
A crucial part of building software is storing data in temporary memory to use later.
later. For
example, we might store a player’s current score or remember a menu choice long enough to
respond to it. When we talk about memory and variables, we are talking about “volatile”
memory (or RAM) that sticks around while your program runs but is wiped out when your
program closes or the computer is rebooted. (To let data survive longer than the program, we
must save it to persistent storage in a file, which is the topic of Level 39.)
39.)
A computer’s
computer’s total memory is gigantic.
gigantic. Even my old smartphone has 3 gigabytes of memory—
large enough to store 750 million different numbers. Each memory location has a unique
numeric memory address, which can be used to access any specific location’s contents. But
remembering what’s in spot #45387 is not practical. Data comes and goes in a program. We
might need something for a split second or the whole time the program is running. Plus, not
all pieces of data are the same size. The text “Hello, World!” takes up more space than a single
singl e
number does. We need something smarter than t han raw memory addresses.

CREATING AND USING VARIA


VARIABLES
BLES IN C# 33
A variable solves this problem for us. Variables are named locations where
data is stored in memory. Each variable has three parts: its name, type, and
contents or value. A variable’s type is important because it lets us know how
many bytes to reserve for it in memory, and it also allows the compiler to
ensure that we are using its contents correctly.
The first step in using a variable is to declare it. Declaring a variable allows
the computer to reserve a spot for it in memory of the appropriate size.
After declaring a variable, you can assign values or contents to the variable. The first time you
assign a value to a variable is called initializing it. Before a variable is initialized, it is
impossible to know what bits and bytes might be in that memory location, so initialization
ensures we only work with legitimate data.

While you can only declare


declare a variable once, you can assign it di
different
fferent values over ti
time
me as the
program runs. A variable for the player’s score can update as they collect points. The
underlying memory location remains the same,
sa me, but the contents change with new value
valuess over
time.
The third thing you can do with a variable is retrieve its current value. The purpose of saving
the data was to come back to it
i t later.
later. As long as a variable has been initialized, we can retrieve
its current contents whenever we need it.

CREATING AND U󰁓ING VARIABLE󰁓 IN C#


The following code shows all three primary variable-related activit
activities:
ies:

string username;
username = Console.ReadLine(
Console.ReadLine();
); // Declaring
Assigning a variable
value to a variable
Console.WriteLine("Hi
Console.WriteLine("H i " + username); // Retrieving its current value

A variable is declared type and its name together (string username;).


declared by listing its type
A variable
variable is assigned a value by
by placing the variable name on the left side of an equal sign and
the new value on the right side. This new value may be an expression that the computer will
evaluate to determine the value (username = Console.ReadLine();).
Retrieving the variable’s current value is done by simply using the variable’s name in an
expression ("Hi " + username). In this case, your program will start by retrieving the
current value in username. It then uses that value to produce the complete "Hi [name]"
message. The combined message is what is supplied to the WriteLine method.

Y
You
ou can before
declared declarethey
a variable anywhere
are used, variable within your code.
declarations Still,
tend to because
gravitate variables
toward must
the top be
of the
code.
Each variable can only be declared once, though your programs can create many variables.
Y
You
ou can assign new values to variables or retrieve the current value in a variable as often as
you want:
string username;

username = Console.ReadLine();
Console.WriteLine("Hi " + username);

34 LEVEL 5 VARIABLES
username = Console.ReadLine();
Console.WriteLine("Hi " + username);

Given that username above is used to store two different usernames over time, it is
reasonable to reuse the variable. On the other hand, if the second value represents something
else—say a favorite color—then it is usually better to make a second variable:
string username;
username = Console.ReadLine();
Console.WriteLine("Hi " + username);

string favoriteColor;
favoriteColor = Console.ReadLine();
Console.WriteLine("Hi " + favoriteColor);

Remember that variable names are meant for humans to use, not the computer. Pick names
that will help human programmers understand their intent. The computer does not care.
Declaring a second variable technically takes up more space in memory, but spending a few
extra bytes (when you have billions) to make the code more understandable is a clear win.

INTEGER󰁓
Every variable, value, and expression in your C# programs has a type associated
a ssociated with it. Before
now, the only type we have seen has been strings (text). But many other types exist, and we
can even define our own types. Let’s sec ond type: int, which represents an integer.
Let’s look at a second
An integer is a whole number (no fractions or decimals) but either posi
positive,
tive, negative, or zero.
zero.
Given the computer’s capacity to do math, it should be no surprise that storing numbers is
common, and many variables use the int type. For example, all of these would be well
represented as an int: a player’
player’ss score, pixel locations on a screen, a file’s size, and a country’s
population.
Declaring an int-typed variable is as simple as using the int type instead of the string type
when we declare it:
int score;

This score variable is now built to hold int values instead of text.
This type concept is important, so I’ll state it again: types matter in C#; every value, variable,
and expression has a specific type, and the compiler will ensure that you don’t mix them up.
The following fails to compile because the types don’t match:
score = "Generic User"; // DOESN'T COMPILE!

The text "Generic User" is a string, but score’s type is int. This one is more subtle:
score = "0"; // DOESN'T COMPILE!

number. But enclosed in quotes like that, "0" is a string representation


At least this looks like a number.
of a number, not an actual number. It is a string literal, even though the characters could be
used in numbers. Anything in double quotes will always be a string. To To make an int literal, you
write the number without the quote marks:
marks:
score = 0; // 0 is an int literal.

READING FROM A VARIABLE DOES NOT CHANGE IT 35


After this line of code runs, the score variable—a memory location reserved to hold ints
under the name score—has a value of 0.
The following shows that you can assign different values to score over time, as well as
negative numbers:
score = 4;
score = 11;
score = -1564;

READING FROM A VARIABLE DOE󰁓 NOT CHANGE IT


When you read the contents of a variable,
variable, the variable’s
variable’s contents are
are copied out. To
To illustrate:
int a;
int b;

a = 5;
b = 2;

b = a;
a = -3;

In the first two lines, a and b are declared and given an initial value (5 and 2, respectively),
which looks something like this:
this:

On that fifth line, b = a;, the contents of a are copied out of a and replicated into b.

The variables a and b are distinct, each with its own copy
c opy of the data. b = a does not mean a
and b are now always going to be equal! That = symbol means assignment, not equality.

Once theafinal line


(Though and b will be equal immediately after running that line until something changes.)
runs, assigning a value of -3 to a, a will be updated as expected, but b
retains the 5 it already had. If we displayed the values of a and b at the end of this program,
we would see that a is -3 and b is 5.
There are some additional nuances to variable assignment, which we will cover in Level
L evel 14.

CLEVER VARIABLE TRICK󰁓


Declaring and using variables is so common that there are some useful shortcuts to learn
before moving on.

36 LEVEL 5 VARIABLES
The first is that you can declare a variable and initialize it on the same line, like this:
int x = 0;

This trick is so useful that virtually all experienced


experi enced C# programmers would use this instead of
putting the declaration and initialization on back-to-back
back-to-back lines.
Second, you can declare multiple variables simultaneously if they are the same type:
int a, b, c;

Third, variable assignments are also expressions that evaluate to whatever the assigned value
was, which
which means you can assign
assign the same thing to many variables all at once like tthis:
his:
a = b = c = 10;

The value of 10 is assigned to c, but c = 10 is an expression that evaluates to 10, which is


then assigned to b. b = c = 10 evaluates to 10, and that value is placed in a. The above
code is the same as the following:
c = 10;
b = c;
a = b;

In my experience, this is not very common,


c ommon, but it does have its uses.
matter,, Console.WriteLine can display both strings and integers:
And finally, while types matter
Console.WriteLine(42);

In the next level, we will introduce many more variable types. Console.WriteLine can
display every single one of them. That is, while types matter and are not interchangeable,
Console.WriteLine is built to allow it to work with any type. We will see how this works
and learn to do it ourselves in the future.

VARIABLE NAME󰁓
Y
You
ou have a lot of control over what names you give to your variables, but the language has a
few rules:
Variable names must start with a letter or the underscore character (_). But C# casts a wide
1. Variable
net when defining “letters”—almost anything in any language is allowed. taco and
_taco are legitimate variable names, but 1taco and *taco are not.
you can also use numeric digits (0 through 9).
2. After the start, you
3. Most symbols and whitespace characters are banned because they make it impossible for
the compiler to know where a variable name ends and other code begins. (For example,
taco-poptart is not allowed because the - character is used for subtraction. The
compiler assumes this is an attempt to subtract something called poptart from
something called taco.)
4. Y
You
ou cannot name a variable the same thing as a keyword. For example, you cannot call a
variable int or string, as those are reserved,
reser ved, special words in the language.
I also recommend the following guidelines for naming variables:
1. Accurately describe what the variable holds. If the variable contains a player’s score,
score or playerScore are acceptable. But number and x are not descriptive enough.

VARIABLE NAMES 37
2. Don’t abbreviate or remove letters. You spend more time reading code than you do
writing it, and if you must decode every variable name you encounter,
encounter, you’re doing
yourself a disservice. What did plrscr (or worse, plain ps) stand for again? Plural scar?
Plastic Scrabble? No, just player score. Common acronyms like html or dvd are an
exception to this rule.
3. Don’t fret over long names. It is better to use a descriptive name than “save characters.”
With any half-decent IDE, you can use features
features like AutoComplete to finish long names
after typing just a few letters anyway, and skipping the meaningful parts of names makes
it harder to remember what it does.
4. Names ending in numbers are a sign of poor names. With a few exceptions, variables
named number1, number2, and number3, do not distinguish one from another well
enough. (If they are part of a set that ought to go together, they should be packaged that
way; see Level 12.)
12.)
5. Avoid generic catch all names. Names like item, data, text, and number are too
vague to be helpful in most cases.
6. Make the boundaries between multi-word names clear. A name like playerScore is
easier to read than playerscore . Two conventions among C# programmers are
camelCase (or lowerCamelCase ) and PascalCase (or UpperCamelCase ), which
are illustrated by the way their names are written. In the first, every word but the first starts
with a capital letter
l etter.. In the second, all words begin with a capital letter. The big capital
letter in the middle of the word makes it look like a camel’s hump
hump,, which is why it has this
name. Most C# programmers use lowerCamelCase for variables and UpperCamel
Case for other things. I recommend sticking with that convention as you get started, but
the choice is yours.

Picking good variable names doesn’t guarantee readable code, but it goes a long
l ong way.
Knowledge Check Variables 25 XP
Check your knowledge with the following questions:
1. Name the three things all variables have.
2. True/False. Variables must always be declared before being used.
3. Can you redeclare a variable?
4. Which of the following are legal C# variable names? answer, 1stValue, value1, $message ,
delete-me, delete_me, PI.
Answers: (1) name, type, value. (2) True. (3) No. (4) answer, value1, delete_me, PI.

LEVEL 6
THE C# TYPE 󰁓Y󰁓TEM
󰁓peedrun
• Types of variables and values matter in C#. They are not interchangeable.
• There are eight integer types for storing integers of differing sizes and ranges: int, short, long,
byte, sbyte, uint, ushort, and ulong.
• The char type stores single characters.
• The string type stores longer text.
• There are three types for storing real numbers: float, double, and decimal.
• The bool type stores truth values (true and false) used in logic.
• These types are the building blocks of a much larger type system.
• Using var for a variable’s type tells the compiler to infer its type from the surrounding code, so you
do not have to type it out. (But it still has a specific
s pecific type.)
• The Convert class helps convert one type to another.

In C#, types of variables and values matter (and must match), but we only know about two
types so far. In this level, we will introduce a diverse set of types we can use in our programs.
These types are called built-in types or primitive types. They are building blocks for more
complex types that we will see later.

REPRE󰁓ENTING DATA IN BINARY


Why do types
types matter so much?
Every piece of data you want to represent in your programs must be stored in the computer’s
circuitry, limited to only the 1’s and 0’s of binary. If we're going to store a number, we need a
scheme for using bits (a single 1 or 0) and bytes (a group of 8 bits and the standard grouping
size of bits) to represent the range of possible numbers we want to store. If we’re going to
represent a word, we need some scheme for using the bits and bytes to represent both letters
and sequences (strings) of letters. More broadly, anything we want to represent in a program
requires a scheme for expressing it in binary.

INTEGER TYPES 39
Each type defines its own rules for representing values in binary, and different types are not
interchangeable. You
You cannot take bits and bytes meant to represent an integer and reinterpret
reinterpret
those bits and bytes as a string and expect to get meaning out of it. Nor can you take bits and
bytes meant to represent text and reinterpret them as an integer and expect it to be
meaningful. They are not the same. There’s
There’s no getting around it.
That doesn’t mean that each type is a world unto itself that can never interact with the other
worlds. We
We can and will convert
convert from one type to another frequently. But
But the costs associated
associated
with conversion are
are not free, so we do it conscientiously rather than accidentally.
Notably, C# does not invent entirely new schemes and rules for most of its types. The
computing world has developed schemes for common types like numbers and letters, and C#
reuses these schemes when possible. The physical
physical hardware of the computer also uses these

same schemes. Since it is baked into the circuitry, it can be fast.


The specifics of these schemes are beyond this book s scope, but let s do a couple of thought
experiments to explore.
Suppose we want to represent the numbers 0 through 10. We need to invent a way to describe desc ribe
each of these numbers with only
onl y 0’s
0’s and 1’s. Step 1 is to decide how many bits to use. One bit
can store two possible states (0 and 1), and each bit you add after that doubles the total
possibilities. We
We have 11 possible states, so we will need at least 4 bits to represent all of them.
Step 2 is to figure out which bit patterns to assign to each number. 0 can be 0000. 1 can be
0001. Now it gets a little more complicated. 2 is 0010, and 3 is 0011. (We’re counting in
binary if that happens to be familiar to you.) We’ve used up all possible combinations of the
two bits on the right and need to use the third bit. 4 is 0100, 5 is 0101, and so on, all the way
to 10, which is 1010. We have some unused bit patterns. 1011 isn’t anything yet. We could go
all the way up to 15 without needing any more bits.
We have
have one problem: the computer doesn’t deal well with anything smaller than full bytes.
Not a big deal; we’ll just use a full byte of eight
e ight bits.
If we want to represent letters, we can do a similar thing. We could assign the letter A to
01000001, B to 01000010, and so on. (C#( C# actually uses two bytes for every character.)
character.)
If we want to represent text (a string), we can use our letters as a building block. Perhaps we
could use a full byte to represent how many letters long our text is and then use two bytes for
each letter in the word. This is tricky because short words need to use fewer bytes than longer
words, and
and our system hashas to account for that.
that. But this would be a workable scheme.
We don’t have to invent these schemes for types ourselves, fortunately. The C# language has
taken care of them for us. But hopefully, this illustrates why we can’t magically treat an integer
i nteger
and a string as the same thing. (Though we will be able to convert from one type to another.)
another.)

INTEGER TYPE󰁓
Let’s explore the basic types available in a C# program, starting with the types used to
represent integers. While we used the int type in the previous level, there are eight different
types for working with integers. These eight types are called integer types or integral types.
Each uses a different number of bytes, which allows you to store bigger numbers using more
memory or store smaller numbers while conserving
conser ving memory.
The int type uses 4 bytes and can represent numbers between roughly -2 billion and +2
billion. (The specific numbers are in the table below.)
bel ow.)

40 LEVEL 6 THE C# TYPE SYSTEM


In contrast, the short type uses 2 bytes and can represent numbers between about -32,000
and +32,000. The long type uses 8 bytes and can represent numbers between about -9
quintillion and +9 quintillion (a quintillion is a billion billion).
Their sizes and ranges tell you when you might choose short or long over int. If memory
is tight and a short’s range is sufficient, you can use a short. If you need to represent
numbers larger than an int can handle, you need to move up to a long, even at the cost of
more bytes.
The short, int, and long types are signed types; they include a positive or negative sign and
store positive and negative values. If you only need positive numbers, you could imagine
shifting these ranges upward to exclude negative values but twice as many positive values.
This is what the unsigned types are for: ushort, uint, and ulong. Each of these uses the
same number of bytes as their signed counterpart, cannot store negative numbers, but can
store twice as many positive numbers. Thus ushort s range is 0 to about 65,000, uint s range
is 0 to about 4 billion, and ulong’s range is 0 to about 18 quintillion.
The last two integer types are a bit different. The first is the byte type, using a single byte to
represent values from 0 to 255 (unsigned). While integer-like, the byte type is more often
used to express a byte or collection of bytes with no specific structure (or none known to the
program). The byte type has a signed counterpart, sbyte, representing values in the
range -128 to +127. The sbyte type is not used very often but makes the set complete.
c omplete.
The table below summarizes this information.
infor mation.
Name Bytes Allow Negatives Minimum Maximum
byte 1 No 0 255

short 2 Yes -32,768 32,767


int 4 Yes -2,147,483,648
-2,147,483,6 48 2,147,483,647
long 8 Yes -9,223,372,
-9,223,372,036,854,775,808
036,854,775,808 9,223,372,036,854,775,807
9,223,372,036, 854,775,807
sbyte 1 Yes -128 127
ushort 2 No 0 65,535
uint 4 No 0 4,294,967,295
ulong 8 No 0 18,446,744,073,
18,446,744,073,709,551,615
709,551,615

Declaring and Using Variables with Integer Types


Declaring variables of these other types is as simple as using their type names instead of int
or string, as we have done before:
byte aSingleByte = 34;
aSingleByte = 17;

short aNumber = 5039;


aNumber = -4354;

long aVeryBigNumber = 395904282569;


aVeryBigNumber = 13;

In the past, we saw that writing out a number directly in our code creates an int literal. But
this brings up an interesting question. How do we create a literal that is a byte literal or a
ulong literal?
For things smaller than an int, nothing special is needed to create a literal of that type:

INTEGER TYPES 41
byte aNumber = 32;

The 32 is an int literal, but the compiler is smart enough to see that you are trying to store it
in a byte and can ensure by inspection that 32 is within the allowed range for a byte. The
compiler handles it. In contrast, if you used a literal that was too big for a byte, you would get
a compiler error, preventing you from compiling and running your program.
This same rule also applies to sbyte, short, and ushort.
If your literal value is too big to be an int, it will automatically become a uint literal, a long
literal, or a ulong literal (the first of those capable of representing the number you typed).
Y
You
ou will get a compiler error if you make a literal whose value is too big for everything. To
illustrate how these bigger literal types work, consider this code:
long aVeryBigNumber = 10000000000; // 10 billion would be a `long` literal.
Y
You
ou may occasionally
occ asionally find that you want to force a smaller number to be one of the larger
literal types. You can force this by putting a U or L (or both) at the end of the literal value:
ulong aVeryBigNumber = 10000000000U;
aVeryBigNumber = 10000000000L;
aVeryBigNumber = 10000000000UL;

A U signifies that it is unsigned and must be either a uint or ulong. L indicates that the literal
must be a long or a ulong, depending on the size. A UL indicates that it must be a ulong.
These suffixes can be uppercase or lowercase and in either order. However, avoid using a
lowercase l because that looks too much like a 1.
Y
You
ou shouldn’t need these suffixes
suffixes very often.

The Digit 󰁓eparator


When humans writewrite a long number like 1,000,000,000, we often use a sepa
separator
rator like a comma
to make interpreting the number easier.
easier. While we can’t use the ccomma
omma for that in C#, there is
an alternative: the underscore character (_).
int bigNumber = 1_000_000_000;

The normal convention for writing numbers is to group them by threes (thousands,
(thousands, millions,
billions, etc.), but the C# compiler does not care where these appear in the middle of numbers.
If a different grouping makes more logical sense, use it that way. All the following are allowed:
int a = 123_456_789;
int
int b
c =
= 12_34_56_78_9;
1_2__3___4____5;

Choosing Between the Integer Types


With eight types
types for storing integers,
integers, how do you decide which one to use?
On the one hand, you could carefully consider the possible range of values you might want for
any variable and then pick the smallest (to save on memory usage) that can fit the intended
range. For example, if you need a player’s score and know it can never be negative, you have
cut out half of the eight options right there. If the player’s score may be in the hundreds of
thousands in any playthrough, you can rule out byte and ushort because they’re not big
enough. That leaves you with only uint and ulong. If you think a player’s score might

42 LEVEL 6 THE C# TYPE SYSTEM


approach 4 billion, you’d better use ulong, but if scores will only reach a few million, then a
uint is safe. (You can always change a variable’s type and recompile your program if you got
it wrong—software
wrong—software is soft after all—but it is easier to have just been right the first time.)
The strategy of picking the smallest practical range for any given variable has merit, but it has
two things going against it. The first is that in modern programming, rarely does saving a single
byte of space matter. There is too much memory around to fret over individual bytes. The
second is that computers do not have hardware that supports math with smaller types. The
computer upgrades them to ints and runs the math as ints, forcing you to then go to the
trouble of converting the result back to the smaller type. The int type is more convenient than
sbyte, byte, short, and ushort if you are doing many math operations.
Thus, the more common strategy is to use int, uint, long, or ulong as necessary and only
use byte, sbyte, short, and ushort when there is a clear
cle ar and significant benefit.
Binary and Hexadecimal Literals
So far,
far, the integer literals
l iterals we have written have all been written using base 10, the normal 10-
digit system humans typically use. But in the programming world, it is occasionally easier to
write out the number
number using either base 2 (binary digits) or base 16 (hexadecimal digits, which
are 0 through 9, and then the letters
l etters A through F).
nu mber with a 0b. For example:
To write a binary literal, start your number
int thirteen = 0b00001101;

For a hexadecimal literal, you start your number with 0x:


int theColorMagenta = 0xFF00FF;
This example shows one of the places where this might be useful. Colors are often represented
as either six or eight hexadecimal digits.

TEXT: CHARACTER󰁓 AND 󰁓TRING󰁓


There are more numeric types, but let’s turn our attention away from numbers for a moment
and look at representing single letters and longer
l onger text.
In C#, the char type represents a single character, while our old friend string represents
text of any length.

type char
The type
banner is very
with the closely related to
other integer the integer
types. types. It isofeven
Each character lumped
interest into the
is given integral
a number
representing it, which amounts to a unique bit pattern. The char type is not limited to just
keyboard characters. The char type uses two bytes to allow for 65,536 distinct characters. The
number assigned to each character follows a widely used standard called Unicode. This set
covers English characters and every character in every human-readable language and a whole
slew of other random characters and emoji. A char literal is made by placing the character in
single quotes:
char aLetter = 'a';
char baseball = '⚾';

FLOATING-POIN
FLOATING-POINTT TYPES 43
Y
You
ou won’t find too many uses for the esoteric characters. The console window doesn’t even
know how to display the baseball character above). Still, the diversity of characters available
is nice.
If you know the hexadecimal Unicode number for a symbol and would prefer to use that, you
can write that out after a \u:
char aLetter = '\u0061'; // An 'a'

The string type aggregates many characters into a sequence to allow for arbitrary text to be
represented. The word “string” comes from the math world, where a string is a sequence of
symbols chosen from a defined set of allowed symbols, one after the other, of any length. It is
a word that the programming world has stolen from the math world, and most programming
languages refer to this idea as strings.
A string literal is made by placing the desired text in double quotes:
string message = "Hello, World!";

FLOATING-POINT TYPE󰁓
We now
now return to the number world to look at types tthat
hat represent
represent numbers besides integers.
integers.
How do we represent 1.21 gigawatts or the special number π?
C# has three types that are called floating-point data types. These represent what
mathematicians call real numbers, encompassing integers and numbers with a decimal or
fractional component. While we cannot represent 3.1415926 as an integer (3 is the best we
could do), we can represent it as a floating-point number.
number.
The “point” in the name refers to the decimal
dec imal point that often appears when writing out these
numbers.
The “floating” part comes because it contrasts with fixed-point types. The number of digits
before and after the decimal point is locked in
i n place with a fixed-point type. The decimal point
may appear anywhere within the number with a floating-point type. C# does not have fixed-
point types because they prevent you from efficiently using veryver y large or very small numbers.
In contrast, floating-point numbers let you represent a specific number of significant digits
and scale them to be big or small. For example, they allow you to express the numbers
1,250,421,012.6 and 0.00000000000012504210126 equally well, which is something a fixed-
point representation cannot reasonably do.
With floating-point types, some of the bits store the significant digits, affecting how precise
you can be, while other bits define how much to scale it up or down, affecting the magnitudes
you can represent.
represent. The more bits you use, the more
more of either you can do.
There are three flavors of floating-point numbers: float, double, and decimal. The float
type uses 4 bytes, while double uses twice that many (hence the “double”) at 8 bytes. The
decimal type uses 16 bytes. While float and double follow conventions used across the
computing world, including in the computer’s circuitry itself, decimal does not. That means
float and double are faster. However, decimal uses most of its many bits for storing
significant figures and is the most precise floating-point type. If you are doing something that
needs extreme precision, even at the cost of speed, decimal is the better choice.
All floating-point numbers have ranges that are so mind-boggling in size that you wouldn’t
woul dn’t
want to write them out the typical way. The math world often uses scientific notation to

44 LEVEL 6 THE C# TYPE SYSTEM


compactly write extremely big or small numbers. A thorough discussion of scientific notation
is beyond the scope of this book, but you can think of it as writing the zeroes in a number as a
power of ten. Instead of 200, we could write 2×10 2. Instead of 200000, we could write 2×10
2 ×105. As
the exponent grows by 1, the number of total digits also increases by 1. The exponent tells us
the scale of the number.
The same technique can be used for very tiny numbers, though the exponent is negative.
Instead of 0.02, we could write 2×10-2. Instead of 0.00002, we could
coul d write 2×10-5.
Now imagine what the numbers 2×1020 and 2×10-20 would look like when written the
traditional way. With that image in your mind, let’s look at what ranges the floating-point types
can represent.
A float can store numbers as small as 3.4×10
3 .4×10-45 and as large as 3.4×1038. That is small enough
to measure quarks and large enough to measure the visible universe many times over. A
float has 6 to 7 digits of precision, depending on the number, meaning it can represent the
number 10000 and the number 0.0001, but does not quite have the resolution to differentiate
between 10000 and 10000.0001.
A double can store numbers as small as 5×10-324 and as large as 1.7×10308, with 15 to 16 digits
of precision.
A decimal can store numbers as small as 1.0×10 -28 and as large as 7.9×1028, with 28 to 29 digits
of precision.
I’m not going to write out all of those numbers in normal notation, but it is worth taking a
moment to imagine what they might look like.
All three floating-point representations
representations are insane in size, but seeing the exponents, you
other. The float type uses the fewest bytes,
should have a feel for how they compare to each other.
and its range and precision are good enough for almost everything. The double type can
store the biggest big numbers and the smallest small numbers with even more precision than
a float. The decimal type’s range is the smallest of the three but is the most precise and is
great for calculations where accuracy matters (like financial or monetary calculations).
The table below summarizes how these types compare to each other:
other :
Type Bytes Range Digits of Precision Hardware Supported
float 4 ±1.0 × 10-45 to ±3.4 × 1038 7 Yes
double 8 ±5 × 10 -324 308
to ±1.7 × 10 15-16 Yes
decimal 16 ±1.0 × 10-28 to ±7.9 × 1028 28-29 No
Creating variables of these types is the same as any other type, but it gets more interesting
when you make float, double, and decimal literals:
double number1 = 3.5623;
float number2 = 3.5623f;
decimal number3 = 3.5623m;

If a number literal contains a decimal point, it becomes a double literal instead of an integer
literal. Appending an f or F onto the end (with or without the decimal point) makes it a float
literal. Appending an m or M onto makes it into a decimal literal. (The “m” is for “monetary”
or “money.”
“money.” Financial calculations often need incredibly high precision.)
All three types can represent a bigger range than any integer type, so if you use an integer
literal, the compiler will automatically convert it.

THE BOOL TYPE 45

󰁓cientific Notation
As we saw when we first introduced
introduced the range floating-point
floating-point numbers can represent,
represent, really
really big
and really small numbers are more concisely represented in scientific notation. For example,
6.022×1023 instead of 602,200,000,000,000,000,000,000. (That number, by the way, is called
Avogadro’
Avogad ro’ss Number—a number with special significance in chemistry.)
c hemistry.) The × symbol is not
one on a keyboard, so for decades, scientists have written a number like 6.022×10 23 as
6.022e23, where the e stands for “exponent.” Floating-point literals in C# can use this same
notation by embedding an e or E in the number:
double avogadrosNumber = 6.022e23;
THE BOOL TYPE
The last type we will cover in this level is the bool type. The bool type might seem strange if
you are before long. The bool type gets its name
are new to programming, but we will see its value before
from Boolean logic, which was named after its creator, George Boole. The bool type
represents truth values. These are used in decision-making, which we will cover
c over in Level 9. It
has two possible options: true and false. Both of those are bool literals that you can write
into your code:
bool itWorked = true;
itWorked = false;

Some languages treat bool as nothing more than fancy ints, with false being the number
0 and true being anything else. But C# delineates ints from bools because conflating the
two is a pathway to lots of common bug categories.
A bool could theoretically use just a single bit, but it uses a whole byte.

Challenge The Variable 󰁓hop 100 XP


You see an old shopkeeper struggling to stack up variables in a window display. “Hoo-wee! All these
variable types sure are exciting but setting them all up to show them off to excited new programmers
like yourself is a lot of work for these aching bones,” she says. “You wouldn’t mind helping me set up this
program with one variable of every type, would you?”
Objectives:
• Build a program with a variable of all fourteen types described in this level.
• Assign each of them a value using a literal of the correct type.
• Use Console.WriteLine to display the contents of each variable.
variable.

Challenge The Variable 󰁓hop Returns 50 XP


“Hey! Programmer!” It’s the shopkeeper from the Variable Shop who hobbles over to you. “Thanks to
your help, variables are selling like RAM cakes! But these people just aren’t any good at programming.
They keep asking how to modify the values of the variables they’re buying, and… well… frankly, I have no
clue. But you’re a programmer, right? Maybe you could show me so I can show my customers?”
Objectives:

46 LEVEL 6 THE C# TYPE SYSTEM


• Modify your Variable Shop program to assign a new, different literal value to each of the 14 original
variables. Do not declare any additional variables.
• Use Console.WriteLine to display the updated contents of each variable.
variable.

This level has introduced the 14 most fundamental types of C#. It may seem a lot to take in,
and you may still be wondering when to use one type over another. But don’t worry too much.
This level will always be here as a reference when you need it.
These are not the only possible types in C#. They are more like chemical elements, serving as
the basis or foundation for producing other types.
TYPE INFERENCE
Types matter greatly in C#. Every variable, value, and expression has a specific, known type.
We have been very specific when declaring
dec laring variables to call out each variable’s
variable’s type. But the
compiler is very smart. It can often look at your code and figure out (“infer”) what type
something is from clues and cues around it. This feature is called type inference. It is the
Sherlock Holmes of the compiler.
Type inference is used for many language features, but a notable one is that the compiler ccan an
infer the type of a variable based on the code that it is initialized with. You don’t always need
variable’s type yourself. You can use the var keyword instead:
to write out a variable’s
var message = "Hello, World!";

The compiler can tell that "Hello, World " is a string, and therefore,
therefore, message must be
a string for this code to work. Using var tells the compiler,
compiler, “You’ve
“You’ve got this. I know you can
figure it out. I’m not going to bother writing
wr iting it out myself.
myself.””
This only works if you initialize the variable on the same line it is declared. Otherwise,
Other wise, there is
not enough information for the compiler to infer its type. This won’t work:
var x; // DOES NOT COMPILE!

There are no clues to facilitate type inference here, so the type inference fails. You will ha
have
ve to
fall back to using specific, named types.
In Visual Studio, you can easily see what type the compiler inferred by hovering the mouse
over the var keyword until the tooltip appears, which shows the inferred type.

Many programmers prefer to use var everywhere they possibly can. It is often shorter and
cleaner,, especially when we start using types with longer names.
cleaner
But there are two potential problems to consider with var. The first is that the computer
sometimes infers the wrong type. These errors are sometimes subtle. The second problem is
that the computer is faster at inferring a variable’
vari able’ss type than a human. Consider this code:
var input = Console.ReadLine();

The computer can infer that input is a string since it knows ReadLine returns strings.
It is much harder for us humans to pull this information out of memory.
It is worse when the code comes from the Internet or a book because you don’t necessarily
infor mation to figure it out. For that reason, I will usually avoid var in this book.
have all of the information

THE CONVERT CLASS AND THE PARSE METHODS 47


I recommend that you skip var and use specific types as you start working in C#. Doing this
helps you think about types more carefully. After some practice, if you want to switch to var,
go for it.
I want to make this next point very clear, so pay attention: a variable that uses var still has a
specific type. It isn’t a mystery type, a changeable type, or a catch-all type. It still has a specific
type; we have just left it unwritten. This does not work:
work :
var something = "Hello";
something = 3; // ERROR. Cannot store an int in a string-typed variable.

THE CONVERT CLA󰁓󰁓 AND THE PARSE METHOD󰁓


With 14 types at our disposal, we will sometimes
some times need to convert between types. The easiest
way is with the Convert class. The Convert class is like the Console class—a thing in the
system that provides you with a set of tasks or capabilities that it can perform. The Convert
class is for converting between these different built-in types. To illustrate:
Console.Write("What is your favorite number?");
string favoriteNumberText = Console.ReadLine();
int favoriteNumber = Convert.ToInt32(favoriteNumberText);
Console.Write(favoriteNumber + " is a great number!");

You can see that Convert’s ToInt32 method needs a string as an input and gives back or
You
returns an int as a result, converting the text in the process. The Convert class has
ToWhatever methods to convert among the built-in types:
Method Name Target Type Method Name Target Type
ToByte byte ToSByte sbyte
ToInt16 short ToUInt16 ushort
ToInt32 int ToUInt32 uint
ToInt64 long ToUInt64 ulong
ToChar char ToString string
ToSingle float ToDouble double
ToDecimal decimal ToBoolean bool

Most of the names above are straightforward, though a few deserve some explanation. The
names are not a perfect match because the Convert class is part of .NET’s
.NET’s Base Class Library,
l anguages use. No two languages use the same name for things like int and
which all .NET languages
double.
The short, int, and long types, use the word Int and the number of bits they use. For
example, a short uses 16 bits (2 bytes), so ToInt16 converts to a short. ushort, uint,
and ulong do the same, just with UInt.
The other surprise is that converting to a float is ToSingle instead of ToFloat. But a
double is considered “double precision,” and a float is “single precision,” which is where
the name comes from.
All input from the console window starts as strings. Many of our programs will need to
convert the user’s text to another type to work with it. The process of analyzing text, breaking

48 LEVEL 6 THE C# TYPE SYSTEM


it apart, and transforming it into other data is called parsing. The Convert class is a great
starting point for parsing text, though we will also learn
l earn additional parsing tools over time.

Parse Methods
Some C# programmers prefer an alternative to the Convert class. Many of these types have
a Parse method to convert a string to the type. For example:
int number = int.Parse("9000");

Some people prefer this style over the mostly equivalent Convert.ToInt32 . I’ll generally
use the Convert class in this book. But feel free to use this second approach if you prefer it.
Knowledge Check Type 󰁓ystem 25 XP
Check your knowledge with the following questions:
1. True/False. The int type can store any possible integer.
2. Order the following by how large their range is, from smallest to largest: short, long, int, byte.
3. True/False. The byte type is signed.
4. Which can store higher numbers, int or uint?
5. What three types can store
store floating-point
floating-point numbers?
numbers?
6. Which of the options in question 5 can hold the largest numbers?
7. Which of the options in question 5 is the most precise?
precise?
8. What type does the literal value "8" (including the quotes) have?
9. What type stores
stores true or false values?
Answers: (1) false. (2) byte, short, int, long. (3) false. (4) uint. (5) float, double,
decimal. (6) double. (7) decimal. (8) string. (9) bool.

The following page contains a diagram that summarizes the C# type system. It includes
everything we have discussed in this level and quite a few other types and categories we will
discuss in the future.

THE CONVERT CLASS AND THE PARSE METHODS 49


LEVEL 7
MATH
󰁓peedrun
• Addition (+), subtraction (-), multiplication (*), division (/), and remainder (%) can all be used to do
math in expressions: int a = 3 + 2 / 4 * 6;
• The + and - operators can also be used to indicate a sign (or negate a value): +3, -2, or -a.
• The order of operations matches the math world.
worl d. Multiplic
Multiplication
ation and division happen before addition
additio n
and subtraction, and things are evaluated left to right.
• Change the order by using parentheses to group things you want to be done first.
• Compound assignment operators (+=, -=, *=, /=, %=) are shortcuts that adjust a variable with a math
operation. a += 3; is the same as a = a + 3;
• The increment and decrement operators add and subtract one: a++; b--;
• Each of the numeric types defines special values for their ranges (int.MaxValue, double.
MinValue, etc.), and the floating-point types also define PositiveInfinity,
NegativeInfinity, and NaN.
• Integer division drops remainders while floating-point division does not. Dividing by zero in either
system is bad.
• You can convert between types by casting: int x = (int)3.3;

The Math and


operations MathF
such as Absclasses containvalue,
for absolute a collection
Pow andofSqrt
utilityformethods for dealing
powers and with common
square roots, and Sin,math
Cos,
and Tan for the trigonometry functions sine, cosine, and tangent, and a definition of π ( Math.PI)

Computers were built for math, and it is high time we saw how to make the computer
c omputer do some
basic arithmetic.

OPERATION󰁓 AND OPERATOR󰁓


Let’s start by defining a few terms. An operation is a calculation that takes (usually) two
numbers and produces a single result by combining them somehow. Each operator indicates

ADDITION, SUBTRACTION, MUL


MULTIPLICATION,
TIPLICATION, AND DIVISION 51
how the numbers are combined, and a particular symbol represents each operator. For
example, 2 + 3 is an operation. The operation is addition, shown with the + symbol. The
things an operation uses—the 2 and 3 here—are called operands.
Most operators need two operands. These are called binary operators (“binary” meaning
“composed of two things”). An operator that needs one operand is a unary operator, while one
that needs three is a ternary operator. C# has many binary operators, a few unary operators,
and a single ternary operator.
operator.

ADDITION, 󰁓UBTRACTION, MULTIPLICATION, AND DIVI󰁓ION


C# borrows the operator symbols from the math world where it can. For example, to add
together 2 and 3 and
an d store its result into a variable looks like this:
int a = 2 + 3;

The 2 + 3 is an operation, but all operations are also expressions. When our program runs,
it will take these two values and evaluate them using the operation listed. This expression
evaluates to a 5, which is the result placed in a’s memory.
The same thing works for subtraction:
int b = 5 - 2;

Arithmetic like this can be


be used in any expression, not just when initializing
initializing a variable:
int a; // Declaring the variable a.
a = 9 - 2; // Assigning a value to a, using some math.
a = 3 + 3; // Another assignment.

int b = 3 + 1; // Declaring b and assigning a value to b all at once.


b = 1 + 2; // Assigning a second value to b.

Operators do not need literal values; they can use any expression. For example, the code
below uses more complex expressions that contain variables:
int a = 1;
int b = a + 4;
int c = a - b;

That is important. Operators and expressions allow us to work through some process
(sometimes called an algorithm) to compute a result that we care about, step by step. Variables
can be updated over time as our process runs.
Multiplication uses the asterisk (*) symbol:
float totalPies = 4;
float slicesPerPie = 8;
float totalSlices = totalPies * slicesPerPie;

Division uses the forward slash (/) symbol.


double moneyMadeFromGame = 100000;
double totalProgrammers = 4;
double moneyPerPerson = moneyMadeFromGame / totalProgrammers;

52 LEVEL 7 MATH
These last two examples show that you can do math with any numeric type, not just int.
There are some complications when we intermix types in math expressions and use the
ll” integer types (byte, sbyte, short, ushort). For the moment, let’s stick with a single
“small”
“sma
type and avoid the small types. We’ll address those problems before the end of this level.

COMPOUND EXPRE󰁓󰁓ION󰁓 AND ORDER OF OPERATION󰁓


So far, our math expressions
expressions have involved only a single operator at a time. But like in the math
world, our math expressions can combine many operators. For example, the followingf ollowing uses
two different operations in a single expression:
int result = 2 + 5 * 2;
When this happens, the trick is understanding which operation happens first. If we do the
addition first, the result is 14. If we do the multiplication first, the result is 12.
There is a set of rules that governs what operators
operators are evaluated first. This ruleset is called the
order of operations. There are two parts to this: (1) operator precedence determines which
operation types come before others (multiplication before addition, for example), and (2)
operator associativity tells you whether two operators of the same precedence should be
evaluated from left to right or right to left.
le ft.
Fortunately, C# steals the standard mathematical order of operations (to the extent that it
can), so it will all feel natural if you are familiar with the order of operations in math.
C# has many operators beyond addition, subtraction, multiplication, and division, so the
complete ruleset is complicated. The book’s website has a table that shows the whole picture:
csharpplayersguide.com/articles/operators-table.. For now, it is enough to say that the
csharpplayersguide.com/articles/operators-table
following two rules apply:
• Multiplication and division are done first, left to right.
• Addition and subtraction
subtraction are done last, left to right.
With these rules, we can know that expression 2 + 5 * 2 will evaluate the multiplication
that the expression
first, turning it into 2 + 10, and the addition is done after, for a final result of 12, which is
stored in result.
If you ever need to override the natural order of operations, there are two tools you can use.
The first is to move the part you want to be done first to its own statement. Statements run
from top to bottom, so doing this will force an operation to happen before another:

int
int partialResult = 2 + 5; * 2;
result = partialResult

This is also handy when a single math expression has


has grown too big to understand at a glance.
The other option is to use parentheses. Parentheses
Parentheses create a sub-expression that is evaluated
first:
int result = (2 + 5) * 2;

Parentheses c omputer to do 2 + 5 before the multiplication. The math world uses


Parentheses force the computer use s
this same trick.

COMPOUND EXPRE
EXPRESSIONS
SSIONS AND ORDER OF OPERATIONS 53
In the math world, square brackets ([ and ]) and curly braces ({ and }) are sometimes used
as more “powerful” grouping symbols. C# uses those symbols for other things, so instead, you
just use multiple
multiple sets of parentheses inside
inside of each other:
int result = ((2 + 1) * 8 - (3 * 2) * 2) / 4;

Remember, though: the goal isn’t to cram it all into a single line, but to write code you’ll be
able to understand when you come back to it in two weeks.
Let’s walk through another example. This code computes the area of a trapezoid:
// Some code for the area of a trapezoid (http://en.wikipedia.org/wiki/Trapezoid)

double side1 = 4.5;


double height
side2 == 3.5;
1.5;

double areaOfTrapezoid = (side1 + side2) / 2.0 * height;

xpression side1 + side2. Our


Parentheses are evaluated first, so we start by resolving the eexpression
Parentheses
program will retrieve the values in each variable and then perform the addition (a value of 8).
At this point, the overall expression coul d be thought of as the simplified 8.0 / 2.0 *
e xpression could
height. Division and multiplication have the same precedence, so we divide before we
multiply because those are done left to right. 8.0 / 2.0 is 4.0, and our expression is
simplified again to 4.0 * height. Multiplication is now the only operation left to address,
so we perform it by retrieving the value in height (1.5) and multiplying for a final result of
6.0. That is the value we place into the areaOfTrapezoid variable.

Challenge
Challe nge The Triangle Farmer 100 XP
As you pass through the fields near Arithmetica City, you encounter P-Thag, a triangle farmer, scratching
numbers in the dirt.
“What is all of that writing for?” you ask.
“I’m just trying to calculate the area of all of my triangles. They sell by their size. The bigger they are, the
more they are worth! But I have many triangles on my farm, and the math gets tricky, an andd I sometimes
make mistakes. Taking a tiny triangle to town thinking you’re going to get 100 gold, only to be told it’s
only worth three, is not fun! If only I had a program that could help me….” Suddenly, P-Thag looks at you
with fresh eyes. “Wait just a moment. You have the look of a Programmer about you. Can you help me
write a program that will compute the areas for me, so I can quit worrying about math mistakes and get
back to tending to my triangles?
t riangles? The equation I’m using is tthis
his one here,” he says, pointing to the formula,
formu la,
etched in a stone beside him:

  ℎℎ


=
×

Objectives:
• Write a program that lets you input the triangle’s base size and height.
• Compute the area of a triangle by turning the above equation into code.
• Write the result of the computation.

54 LEVEL 7 MATH

󰁓PECIAL NUMBER VALUE󰁓


Each of the 11 numeric types—eight integer types and three floating-point types—defines a
handful of special values you may find useful.
All 11 define a MinValue and a MaxValue, which is the minimum and maximum value they
can correctly represent. These are essentially defined as variables (technically properties,
which we’ll learn more about in Level 20) that you get to through the type name. For example:
int aBigNumber = int.MaxValue;
short aBigNegativeNumber = short.MinValue;

These things are a little different than the methods we have seen in the past. They are more
like variables than methods, and you don’t use parentheses to use them.
The double and float types (but not decimal) also define a value for positive and negative
infinity called PositiveInfinity and NegativeInfinity :
double infinity = double.PositiveInfinity;

Many computers will use the ∞ symbol to represent a numeric value of infinity. This is the
symbol used for infinity in the math world. Awkwardly, some computers (depending on
operating system and configuration) may use the digit 8 to represent infinity in the console
window. That can be confusing
c onfusing if you are not
n ot expecting it. You can tweak settings to get the
computer to do better.
double and float also define a weird value called NaN, or “not a number.” NaN is used when
a computation results in an impossible value, such as division by zero. You can refer to it as
shown in the code below:
double notAnyRealNumber = double.NaN;

INTEGER DIVI󰁓ION V󰁓. FLOATING-POINT DIVI󰁓ION


Try running this program and see if the displayed result is what you expected:
int a = 5;
int b = 2;
int result = a / b;
Console.WriteLine(result);

On a computer, there are two approaches to division. Mathematically, 5/2 is 2.5. If a, b, and
result were all floating-point
called floating-po types,
types, that’s
int division because
floating-point that’ s what
it is what would
you havefloating-point
get with happened. This division style is
types.
The other option is integer division. When you divide with any of the integer types, fractional
bits of the result are dropped. This is different from rounding; even 9/10, which
mathematically is 0.9, becomes a simple 0. The code above uses only integers, and so it uses
integer division. 5/2 becomes 2 instead of 2.5, which is placed into result.
This does take a little getting used to, and it will catch you by surprise from time to time. If you
want integer division, use integers. If you want floating-point division, use floating-point
floating-point
types. Both have their uses. Just make sure you know which one you need and which one
you’ve got.

DIVISION BY ZERO 55

DIVI󰁓ION BY ZERO
In the math world, division by zero is not defined—a meaningless operation without a
specified result. When programming, you should also expect problems when dividing by zero.
Once again, integer types and floating-point types have slightly different behavior here,
though either way, it is still “bad things.”
If you divide by zero with integer types, your program will produce an error that, if left
unhandled, will crash your program. We
We talk about error handling of this nature in Le
Level
vel 35.
If you divide by zero with floating-point types, you do not get the same kind of crash. Instead,
it assumes that you actually wanted to divide by an impossibly tiny but non-zero number (an
“infinitesimal” number), and the result will either be positive infinity, negative infinity, or NaN
depending
respectively.on whether
Mathema theoperations
Mathematical
tical numerator was
with a positive
infinities
i nfinities andnumber, negative
NaNs always resultnumber, or zero
in more infinities
and NaNs,
NaNs, so you will want to protect yourself against dividing by zero in the first place when
you can.

MORE OPERATOR󰁓
Addition, subtraction,
subtraction, multiplication,
multiplication, and division are not the only operators in C#.
C#. There are
are
many more. We
We will cover a few more here and others throughout this book.

Unary + and - Operators


While + and – are typically used for addition and subtraction, which requires two operands
(a - b, for example), both have a unary version,
ve rsion, requiring only a single operand:
int a = 3;
int b = -a;
int c = +a;

The – operator negates the value after it. Since


Sinc e a is 3, -a will be -3. If a had been -5, -a would
evaluate to +5. It reverses the sign of a. Or you could think of it as multiplying it by -1.
The unary + doesn’t do anything for the numeric types we have seen in this level, but it can
sometimes add clarity to the code (in contrast to -). For example:
int a = 3;
int b = -(a + 2) / 4;
int c = +(a + 2) / 4;

The Remainder Operator


Suppose I bring 23 apples to the apple party (doctors beware)
beware) and you, me, and Johnny are at
the party. There are two ways we could divide the apples. 23 divided 3 ways does not come out
even. We could chop up the apples and have fractional apples (we’d each get 7.67 apples).
Alternatively, if apple parts are not valuable (I don’t want just a core!), we can set aside
anything that doesn’t divide out evenly. This leftover amount is called the remainder. That is,
each of the three of us would get 7 whole apples, with a remainder of 2.
C#’s remainder operator computes remainders in this same fashion using the % symbol. (Some
call this the modulus operator or the mod operator, though those two terms mean slightly
different things for negative numbers.) Computing the leftover apples looks like this in code:

56 LEVEL 7 MATH
int leftOverApples = 23 % 3;

The remainder operator may not seem useful initially, but it can be handy. One ccommon
ommon use
is to decide if some number is a multiple of another number.
number. If so, the remainder would be 0.
Consider this code:
int remainder = n % 2; // If this is 0, then 'n' is an even number.

If remainder is 0, then the number is divisible by two—which also tells us that it is an even
number.
The remainder operator has the same precedence as multiplication and division.

Challenge The Four 󰁓isters and the Duckbear 100 XP


Four sisters own a chocolate farm and collect chocolate eggs laid by chocolate chickens every day. But
more often than not, the number of eggs is not evenly divisible among the sisters, and everybody knows
you cannot split a chocolate egg into pieces without ruining it. The arguments have grown more heated
over time. The town is tired of hearing the four sisters complain, and Chandra, the town’s Arbiter, has
finally come up with a plan everybody can agree to. All four sisters get an equal number of chocolate
eggs every day, and the remainder is fed to their pet duckbear. All that is left is to have some skilled
Programmer build a program to tell them how much each sister and the duckbear should get.
Objectives:
• Create a program that lets the user enter the number of chocolate eggs gathered that day.
• Using / and %, compute how many eggs each sister should get and how many are left over for the
duckbear.
• Answer this question: What are three total egg counts where the duckbear gets more than each
sister does? You can use the program you created to help you find the answer.

UPDATING VARIABLE󰁓
The = operator is the assignment operator, and while it looks the same as the equals sign, it
does not imply that the two sides are equal. Instead, it indicates that some expression on the
right side should be evaluated and then stored in the variable shown on the leleft.
ft.
It is common for variables to be updated with new values over time. It is also common to
compute a variable’s new value based on its current value. For example, the following code
increases the value of a by 1:
int a = 5;
a = a + 1; // the variable a will have a value of 6 after running this line.

That second line will cause a to grow by 1,


1 , regardless of what was previously in it.
The above code shows how assignment differs from the mathematical idea of equality. In the
math world, a = a + 1 is an absurdity. No number exists that is equal to one more than itself.
But in C# code, statements that update a variable based on its current value are common.
There are even some shortcuts for it. Instead of a = a + 1;, we could do this instead:
a += 1;

UPDATING VARIABLES 57
This code is exactly equivalent to a = a + 1;, just shorter. The += operator is called a
compound assignment operator because it combines an operation (addition, in this case) with
a variable assignment. There are compound assignment operators for each of the binary
far, including +=, -=, *=, /=, and %=:
operators we have seen so far,
int a = 0;
a += 5; // The equivalent of a = a + 5; (a is 5 after this line runs.)
a -= 2; // The equivalent of a = a – 2; (a is 3 after this line runs.)
a *= 4; // The equivalent of a = a * 4; (a is 12 after this line runs.)
a /= 2; // The equivalent of a = a / 2; (a is 6 after this line runs.)
a %= 2; // The equivalent of a = a % 2; (a is 0 after this line runs.)
Increment andone
Adding Decrement Operators
to a variable is called incrementing the variable, and subtracting one is called
decrementing the variable. These two words are derived from the words increase and decrease.
They move the variable up a notch or down a notch.
Incrementing and decrementing are so common that there are specific operators for adding
one and subtracting one from a variable. These are the increment operator ( ++) and the
decrement operator (--). These operators are unary, requiring only a single operand to work,
but it must be a variable and not an
a n expression. For example:
int a = 0;
a++; // The equivalent of a += 1; or a = a + 1;
a--; // The equivalent of a -= 1; or a = a - 1;

We will
will see many uses for these operators
operators shortly.

Challenge The Dominion of Kings 100 XP


Three kings, Melik, Casik, and Balik, are sitting around a table, debating who has the greatest kingdom
among them. Each king rules an assortment of provinces, duchies, and estates. Collectively, they agree
to a point system that helps them judge whose kingdom is greatest: Every estate is worth 1 point, every
duchy is worth 3 points, and every province is worth 6 points. They just need a program that will allow
them to enter their current holdings and compute a point total.
Objectives:
• Create a program that allows users to enter how many provinces, duchies, and estates they have.
• Add up the user’s total score, giving 1 point per estate, 3 per duchy, and 6 per province.
• Display the point total to the user.

Prefix and Postfix Increment and Decrement Operators


The way we used the increment and decrement operators above is the way they are typically
used. However,
However, assignment statements are also expressions and return the value assigned to
the variable. Or at least, it does for normal assignment (with the = operator) and compound
assignment operators (like += and *=).
The same thing is true with the ++ and -- operators, but the specifics are nuanced. These two
operators can be written before or after the modified variable. For example, you can write
either x++ or ++x to increment x. The first is called postfix notation, and the second is called
prefix notation. There is no meaningful difference between the two when written as a

58 LEVEL 7 MATH
complete statement (x++; or ++x;). But when you use them as part of an expression, x++
evaluates to the original value of x, while ++x evaluates to the updated value of x:
int x;

x = 5;
int y = ++x;

x = 5;
int z = x++;

Whether we do x++ or ++x, x is incremented and will have a value of 6 after each code block.
But in the first part, ++x will evaluate to 6 (increment first, then produce the new value of x),
so 5y, will
of whichhave a value ofto6zas
is assigned , even secxond
well.though
The second to 6. evaluates to x’s original value
part, in contrast,
is incremented
The same logic applies to the -- operator.
e ver,, use ++ and -- as a part of an expression. It is far more common
C# programmers rarely, if ever
to use it as a standalone statement, so these nuances are rarely significant.

WORKING WITH DIFFERENT TYPE󰁓 AND CA󰁓TING


Earlier,, I said doing math that intermixes numeric types is p
Earlier problematic.
roblematic. Let’
Let’ss address that now.
Most math operations are only defined for operands of the same type. For example, ad
addition
dition
is defined between two ints and two doubles but not between an int and a double.
But we often need to work with different data types in our programs. C# has a system of
conversions between types. It allows one type to be converted to another type to facilitate
mixing types.
There are two broad categories of conversions. A narrowing conversion risks losing data in the
conversion process. For example, converting a long to a byte could lose data if the number
is larger than what a byte can accurately represent. In contrast, a widening conversion does
not risk losing information. A long can represent everything a byte can represent, so there
is no risk in making the conversion.
c onversion.
Conversions can also be explicit or implicit. A programmer must specifically ask for an explicit
conversion to happen. An implicit conversion will occur automatically without the
programmer stating it.

As a general rule, narrowing conversions, which risk losing data, are explicit. Widening
conversions, which have no chance of losing data, are always implicit.
There are conversions defined among all of the numeric types in C#. When it is safe to do so,
these are implicit conversions. When it is not safe, these are explicit conversions. Consider
this code:
byte aByte = 3;
int anInt = aByte;

The simple expression aByte has a type of byte. Yet, it needs to be turned into an int to be
stored in the variable anInt. Converting from a byte to an int is a safe, widening
conversion, so the computer
c omputer will make this conversion happen automatically. The code above
compiles without you needing to do anything fancy.
fanc y.

WORKING WITH DIFFEREN


DIFFERENTT TYPES AND CASTING 59
If we are going the other way—an int to a byte—the conversion is not safe. To To compile, we
need to specifically state that we want to use the conversion, knowing the risks involved. To
explicitly ask for a conversion, you use the casting operator, shown below:
int anInt = 3;
byte aByte = (byte)anInt;

The type to convert to is placed in parentheses before the expression to convert. This code
says, “I know anInt is an int, but I can deal with any consequences of turning this into a
byte, so please convert it.”
Y
You
ou are allowed to write out a specific request for an implicit conversion using this same
casting notation (for example, int anInt = (int)aByte;
(int)aByte;), but it isn’t strictly necessary.
There are conversions from every numeric type to every other numeric type in C#. When the
conversion is a safe, widening conversion, they are implicit. When the conversion is a
potentially dangerous narrowing conversion, they are explicit. For example, there is an
implicit conversion from sbyte to short, short to int, and int to long. Likewise, there
c onversion from byte to ushort, ushort to uint, and uint to ulong. There
is an implicit conversion
is also an implicit conversion from all eight integer types to the floating-point types, but not
the other way around.
However, casting conversions are not defined
However, defin ed between every
e very possible type. For example, you
cannot do this:
string text = "0";
int number = (int)text; // DOES NOT WORK!

There is no conversion defined (explicit or implicit) that goes from string to int. We can
always fall back on Convert and do int number = Convert.ToInt32(text);.
Conversions and casting solve the two problems we noted earlier: math operations are not
defined for the “small”
“small” types, and intermixing types cause issues.
Consider this code:
short a = 2;
short b = 3;
int total = a + b; // a and b are converted to ints automatically.

Addition is not defined for the short type, but it does exist for the int type. The computer
convert both to an int and use int’s + operation. This produces a result that is
will implicitly convert
an int, not a short, so if we want to get back to a short, we need to cast it:
short a = 2;
short b = 3;
short total = (short)(a + b);

That last line raises an important point:


point : the casting operator has higher precedence than most
other operators. To let the addition happen first and the casting second, we must put the
addition in parentheses to force it to happen first. (We could have also separated the addition
and the casting conversion onto two separate lines.)
Casting and conversions also fix the second problem that intermixing types can cause.
Consider this code:

60 LEVEL 7 MATH
int amountDone = 20;
int amountToDo = 100;
double fractionDone = amountDone / amountToDo;

Since amountDone and amountToDo are both ints, the division is done as integer division,
giving you a value of 0. (Integer division ditches fractional values, and 0.2 becomes a simple
0.) This int value of 0 is then implicitly converted to a double (0.0). But that’s probably not
was intended. If we convert either of the parts involved in the division to a double, then
what was
the division happens with floating-point division instead:
int amountDone = 20;
int amountToDo = 100;
double fractionDone = (double)amountDone / amountToDo;
Now, the conversion of amountDone to a double is performed first. Division is not defined
between a double and an int, but it is defined between two doubles. The program knows
it can implicitly convert amountToDo to a double to facilitate that. So amountToDo is
“promoted” to a double, and now the division happens between two doubles using floating-
point division, and the result is 0.2. At this point, the expression is already a double, so no
additional conversion is needed to assign the value to fractionDone .
Keeping track of how complex expressions work can be tricky. It gets easier with practice, but
don’t be afraid to separate parts onto separate lines to make it easier to think through.

OVERFLOW AND ROUNDOFF ERROR


In the math world, numbers can get as big as they need to. Mathematically, integers don’t have
an upper limit. But our data types do. A byte cannot get bigger than 255, and an int cannot
represent the number 3 trillion. What happens when we surpass this limit?
Consider this code:
short a = 30000;
short b = 30000;
short sum = (short)(a + b); // Too big to fit into a short. What happens?

Mathematically
Mathematically speaking, it should be 60000,
6 0000, but the computer gives a value of -5536.
When an operation causes a value to go beyond what its type can represent, it is called
overflow. For integer types, this results in wrapping around back to the start of the range—0
for unsigned types and a large negative number for signed types. Stated differently,
int.MaxValue
int.MaxValue + 1 exactly equals int.MinValue. There is a danger in pushing the limits
of a data type: it can lead to weird results. The original Pac-Man game had this issue when you
go past level 255 (it must have been using a byte for the current level). The game went to an
undefined level 0, which was glitchy and unbeatable.
Performing a narrowing conversion with a cast is a fast way to cause overflow, so cast wisely.
wisel y.
With floating-point
floating-point types,
types, the behavior
behavior is a little different.
different. Since all floating-point
floating-point types have a
way to represent infinity, if you go too far up or too far down, the number will switch over to
the type’s
type’s positive or negative infinity representation. Math with infinities just results in more
infinities (or NaNs), so even though the behavior is different from integer types, the
consequences are just as significant.
Floating-point types have a second category of problems called roundoff error. The number

10000 can be correctly represented with a float, as can 0.00001. In the math world, you can

THE MATH AND MATHF CLASSES 61


100 00.00001. But a float cannot. It only has six or
safely add those two values together to get 10000.00001.
seven digits of precision and cannot distinguish 10000 from 10000.00001.
float a = 10000;
float b = 0.00001f;
float sum = a + b;

The result is rounded to 10000, and sum will still be 10000 after the addition. Roundoff error
is not usually a big deal, but occasionally, the lost digits accumulate, like when adding huge
piles of tiny numbers. You can sometimes sidestep this by using a more precise type. For
example, neither double nor decimal have trouble with this specific situation. But all three
have it eventually, just at different scales.
THE MATH AND MATHF CLA󰁓󰁓E󰁓
C# also includes two classes with the job of helping you do common math operations. These
classes are called the Math class and the MathF class. We won’t cover everything contained
in them, but it is worth a brief overview.

π and e
The special, named numbers e and π are defined in Math so that you do not have to redefine
them yourself (and run the risk of making a typo). These two numbers are Math.E and
Math.PI respectively. For example, this code calculates the area of a circle
c ircle (Area = πr2):
double area = Math.PI * radius * radius;

Powers and 󰁓quare Roots


C# does not have a power operator in the same way that it has multiplication and addition.
But Math provides methods for doing both powers and square roots: the Pow and the Sqrt
method:
double x = 3.0;
double xSquared = Math.Pow(x, 2);

Pow is the first method that we have seen that needs two pieces of information to do its job.
The code above shows how to use these methods: everything goes into the parentheses,
separated by commas. Pow’s two pieces of information
in formation are the base and the power it is raised
to. So Math.Pow(x, 2) is the same as x .
2

To do a square root, you use the Sqrt method:


double y = Math.Sqrt(xSquared);

Absolute Value
The absolute value of a number is merely the positive version of the number. The absolute
absolute value of -4 is 4. The Abs method computes absolute values:
value of 3 is 3. The absolute
int x = Math.Abs(-2); // Will be 2.

62 LEVEL 7 MATH

Trigonometric Functions
The Math class also includes trigonometric functions like sine, cosine, and tangent. It is
beyond this book’s scope to explain these trigonometric functions, but certain types of
programs (including games) use them heavily. If you need them, the Math class is where to
find them with the names Sin, Cos, and Tan. (There are others as well.) All expect angles in
radians, not degrees.
double y1 = Math.Sin(0);
double y2 = Math.Cos(0);

Min, Max, and Clamp


The Math class also has methods for returning the minimum and maximum of two numbers:
int smaller = Math.Min(2, 10);
int larger = Math.Max(2, 10);

Here, smaller will contain a value of 2 while larger will contain 10.
convenient : Clamp. This allows you to provide a value
There is another related method that is convenient:
and a range. If the value is within the range, that value is returned. If that value is lower than
the range, it produces the low end of the range. If that value is higher than the range, it
produces the high end of the range:
health += 10;
health = Math.Clamp(health, 0, 100); // Keep it in the interval 0 to 100.

More
This is a slice of some of the most widely used Math class methods, but there is more. Explore
the choices when you have a chance so that you are familiar with the other options.

The MathF Class


The MathF class provides many of the same methods as Math but uses floats instead of
doubles. For example, Math’s Pow method expects doubles as inputs and returns a double
as a result. You can cast that result to a float, but MathF makes casting unnecessary:
float x = 3;
float xSquared = MathF.Pow(x, 2);

LEVEL 8
CON󰁓OLE 2.0
󰁓peedrun
• The Console class can write a line without wrapping ( Write), wait for just a single keypress
(ReadKey), change colors (ForegroundColor, BackgroundColor), clear the entire console
window (Clear), change the window title ( Title), and play retro 80’s beep sounds ( Beep).
• Escape sequences start with a \ and tell the computer to interpret the next letter differently. \n is
a new line, \t is a tab, \" is a quote within a string literal.
• An @ before a string ignores any would-be escape sequences: @"C:\Users\Me\File.txt".
• A $ before a string means curly braces contain code: $"a:{a} sum:{a+b}".

In this level, we will flesh out our knowledge of the console and learn some tricks to make
working with text and the console window easier and more exciting. While
While a console window
isn’t as flashy as a GUI or a web page, it doesn’t have to be boring.

THE CONSOLE CLA󰁓󰁓


’ve been using the Console class since our very first Hello World program, but it is time
We’ve
We
we dug deeper
provides a few ofinto it tovariables
its own see what(technically
else it is capable of. Console
properties, as we willhas
seemany methods
in Level 20) thatand
we
can use to do some nifty things.

The Write Method


Aside from Console.WriteLine , another method called Write, does all the same stuff as
WriteLine, without jumping to the following line when it finishes. There are many uses for
this, but one I like is being able to ask the user a question and letting them answer on the same
line:
Console.Write("What is your name, human? "); // Notice the space at the end.
string userName = Console.ReadLine();

The resulting program looks like this:

64 LEVEL 8 CONSOLE 2.0


What is your name, human? RB

The Write method is also helpful when assembling many small bits of text into a single line.

The ReadKey Method


The Console.ReadKey method does not wait for the user to push enter before completing.
It waits for only a single keypress. So if you want to do something like “Press any key to
continu
con tinue… u se Console.ReadKey :
e…””, you can use
Console.WriteLine("Press any key when you're ready to begin.");
Console.ReadKey();
This code has a small problem. If a letter is typed, that letter will still show up on the screen.
There is a way around this. There are two versions of the ReadKey method (called
“overloads,” but we’ll cover that in more detail in Level 13).
13). One version, shown above, has no
inputs. The other version has an input whose type is bool, which indicates whether the text
should be “intercepted” or not. If it is intercepted, it will not be displayed in the console
window. Using
Using this version looks like the following:
Console.WriteLine("Press any key when you're ready to begin.");
Console.ReadKey(true);

Changing Colors
The next few items we will talk about are not methods but properties. There are important
differences between properties and variables, but for now, it is reasonable for us to just think
of them as though they are variables.
The Console class provides variables that store the colors it uses for displaying text. We’re
not stuck with just black and white! This is best illustrated with an example:
Console.BackgroundColor = ConsoleColor.Yellow;
Console.ForegroundColor = ConsoleColor.Black;

After assigning new values to these two variables,


variables, the console will begin using black text on a
background. BackgroundColor and ForegroundColor are both variables instead
yellow background.
of methods, so we don’t use parentheses as we have done in the past. These variables belong
to the Console class, so we access them through Console.VariableName instead of just
by variable name like other variables we have used. These lines assign a new value to those
variables, though we have never seen anything like ConsoleColor.Yellow or
ConsoleColor.Black before. ConsoleColor is an enumeration, which we will learn
more about in Level 16. The short version is that an enumeration defines a set of values in a
collection and gives each a name. Yellow and Black are the names of two items in the
ConsoleColor collection.

The Clear Method


After changing the console’s background color,
color, you may notice that it doesn’t change the
window’ss entire background, just the background of the new letters you write. You can use
window’
Console’s Clear method to wipe out all text on the screen and change the entire
background to the newly set background color:
Console.Clear();

SHARPENING YOUR STRING SKILLS 65


For better or worse, this does wipe out all the text currently on the screen (its primary
objective, in truth), so you will want to ensure you do it only at the right moments.

Changing the Window Title


Console also has a Title variable, which will change the text displayed in the console
bar.. Its type is a string.
window's title bar
Console.Title = "Hello, World!";

Just about anything is better than the default name, which is usually nonsense like “C:\Users\
RB\Source\Repos
RB\Sou rce\Repos\Hello
\HelloWorld\HelloWorld\bin
World\HelloWorld\bin\Debug\n
\Debug\net6.0\He
et6.0\HelloWorld.e
lloWorld.exe”
xe”.
The Beep Method
The Console class can even beep! (Before you get too excited, the only sound the console
window can make
make is a retro 80’ wave.) The Beep method makes the beep sound:
80’ss square wave.)
Console.Beep();

If you’re musically inclined, there is a version that lets you choose both frequency and
duration:
Console.Beep(440, 1000);

This Beep method needs two pieces of information to do its job. The first item is the
frequency. The higher the number, the higher the pitch, but 440 is a nice middle pitch. (The
Internet canistell
information theyou whichmeasured
duration, frequencies belong to which
in milliseconds (1000 notes.)
is a fullThe second
second, 500 piece
is half of
a
second, etc.). You could imagine using Beep to play a simple melody, and indeed, some
people have spent a lot of time doing just this and posting their code to the Internet.

󰁓HARPENING YOUR 󰁓TRING 󰁓KILL󰁓


Let’s turn our attention to a few features of strings to make them more powerful.

Escape 󰁓equences
Here is a chilling challenge: how do you display a quote mark? This does not work:

Console.WriteLine("""); // ERROR: Bad quotation marks!


The compiler sees the first double quote as the start of a string and the second as the end. The
third begins another string that never ends, and we get
ge t compiler errors.
An escape sequence is a sequence of characters
c haracters that do not mean
me an what
w hat they would usually
indicate. In C#, you start escape sequences with the backslash character ( \), located above
the <Enter> key on most keyboards. A backslash followed by a double quote ( \") instructs
the compiler to interpret the character as a literal quote character within the string in
instead
stead of
interpreting it as the end of the string:
Console.WriteLine("\"");

66 LEVEL 8 CONSOLE 2.0


The compiler sees the first quote mark as the string’s beginning, the middle \" as a quote
character within the text, and the third as the end of the string.
A quotation mark is not the only character you can escape. Here are a few other useful ones:
\t is a tab character, \n is a new line character
c haracter (move down to the following line), and \r is a
carriage return (go back to the start of the line). In the console window, going down a line with
\n also goes back to the beginning of the line.
So what if we want to have a literal \ character in a string? There’s
There’s an escape sequence for the
escape character as well: \\. This allows you to include backslashes in your strings:
Console.WriteLine("C:\\Users\\RB\\Desktop\\MyFile.txt");
That code displays the following:
C:\Users\RB\Desktop\MyFile.txt

In some instances, you do not care to do an escape sequence, and the extra slashes to escape
everything are just in your way. You can put the @ symbol before the text (called a verbatim
string literal) to instruct the compiler to treat everything exactly as it looks:
Console.WriteLine(@"C:\Users\RB\Desktop\MyFile.txt");

󰁓tring Interpolation
It is common to mix simple expressions among fixed text. For example:
Console.Write("My favorite number is " + myFavoriteNumber + ".");

This code uses the + operator with strings to combine multiple strings (often called string
concatenation instead of addition). We first saw this in Level 3, and it is a valuable tool. But
with all of the different quotes and
and plusses, it can get har
hard
d to read. String interpolation allows
you to embed expressions
expressions within a string
string by surrounding
surrounding it with curly bra
braces:
ces:
Console.WriteLine($"My favorite number is {myFavoriteNumber}.");

To use string interpolation, you put a $ before the string begins. Within the string, enclose
encl ose any
expressions you want to evaluate inside of curly braces like myFavoriteNumber is above. It
becomes a fill-in-the-blank game for your program to perform. Each expression is evaluated
to produce its result. That result
result is then turned into a string and placed in the overall text.
String interpolation usually gives you much more readable code, but be wary of many long
expressions embedded into your text. Sometimes, it is better to compute a result and store it
in a variable first.
Y
You
ou can combine string interpolation strings by using $ and @ in either
interpolation and verbatim strings e ither order.
order.

Alignment
While string interpolation is
is powerful, it
it is only
only the beginning. Two other
other features
features make string
interpolation even better: alignment and formatting.
Alignment lets you display a string
string with a specific
specific preferred
preferred width.
width. Blank space is added before
the value to reach the desired width if needed. Alignment is useful if you structure text in a
table and need things to line up horizontally.
hor izontally. To
To specify a preferred width, place a comma and
the desired width in the curly braces after your expression to evaluate:

SHARPENING YOUR STRING SKILLS 67


string name1 = Console.ReadLine();
string name2 = Console.ReadLine();
Console.WriteLine($"#1: {name1,20}");
Console.WriteLine($"#2: {name2,20}");

If my two names were Steve and Captain America,


Amer ica, the output would be:
#1: Steve
#2: Captain America

This code reserves 20 characters for the name’s display. If the length is less than 20, it adds
whitespace before
before it to achieve the desired
desired width.
If you want the whitespace to be after the word, use a negative number:
Console.WriteLine($"{name1,-20} - 1");
Console.WriteLine($"{name2,-20} - 2");

This has the following output:


Steve - 1
Captain America - 2

There are two notable limitations to preferred widths. First, there is no convenient way to
center the text. Second, if the text you are writing is longer than the preferred width, it won’t
truncate your text, but just keep writing the characters, which will mess up your columns. You
could write code to do either
either,, but there is no special syntax to do it automatically.
automatically.

Formatting
With interpolated strings,
strings, you can also perform formatting. Formatti
Formatting
ng allows you to pro
provide
vide
hints or guidelines about how you want to display data. Formatting is a deep subject that we
won’t exhaustively
exhaustively cover here, but let’s
let’s look at a few examples.
examples.
Y
You
ou may have seen that when you display a floating-point number,
number, it writes out lots of digits.
For example, Console.WriteLine(Math.PI); displays 3.141592653589793. You
often don’t care about all those digits and would rather round. The following instructs the
string interpolation to write the number with three digits after the decimal place:
Console.WriteLine($"{Math.PI:0.000}");

To format something, after the expression, put a colon and then a format string. This also
comes after the preferred width if you use both. This displays 3.142. It even rounds!

Any 0 innecessary.
strictly the formatFor
indicates thatusing
example, you want a number
a format stringtoofappear therewith
000.000 eventhe
if the number
number 42isn’t
will
display 042.000.
In contrast, a # will leave a place for a digit but will not display a non-significant 0 (a leading
or trailing 0):
Console.WriteLine($"{42:#.##}");// Displays "42"
Console.WriteLine($"{42.1234:#.##}");// Displays "42.12"

Y ou can also use the % symbol to make a number be represented as a percent instead of a
You
fractional value. For example:

68 LEVEL 8 CONSOLE 2.0


float currentHealth = 4;
float maxHealth = 9;
Console.WriteLine($"{currentHealth/maxHealth:0.0%}"); // Displays "44.4%"

Several shortcut formats exist. For example, using just a simple P for the format is equivalent
to 0.00%, and P1 is equal to 0.0%. Similarly, a format string of F is the same as 0.00, while
F5 is the same as 0.00000.
Y
You
ou can use quite a few other symbols for format st
strings,
rings, but that is enough to give us a ba
basic
sic
toolset to work with.

Challenge The Defense of Consolas 200 XP


The Uncoded
situation One has
is dire. Frombegun an assault
a moving o n thecalled
on
airship city ofthe
Consolas; the,
Manticore
massive fireballs capable of destroying city blocks are being
catapulted into the city.
The city is arranged in blocks, numbered like a chessboard.
The city’s only defense is a movable magical barrier, operated by a
squad of four that can protect a single city block by putting
themselves in each of the target’s four adjacent blocks, as shown in
the picture to the right.
For example, to protect the city block at (Row 6, Column 5), the
crew deploys themselves to (Row 6, Column 4), (Row 5, Column 5),

(Row 6, Column 6), and (Row7, Column 5).


The good news is that if we can compute the deployment locations fast enough, the crew can be
deployed around the target in time to prevent catastrophic damage to the city for as long as the siege
lasts. The City of Consolas needs a program to tell the squad where to deploy, given the anticipated
target. Something like this:
Target Row? 6
Target Column? 5
Deploy to:
(6, 4)
(5, 5)
(6, 6)
(7, 5)

Objectives:
• Ask the user for the target row and column.
• Compute the neighboring rows and columns of where to deploy the squad.
• Display the deployment instructions in a different color of your choosing.
• Change the window title to be “Defense of Consolas”.
• Play a sound with Console.Beep when the results have been computed and displayed.

LEVEL 9
DECI󰁓ION MAKING

󰁓peedrun
• An if statement lets some code run (or not) based on a condition. if (condition)
DoSomething;
• An else statement identifies code to run otherwise.
• Combine if and else statements to pick from one of several branches of code.

A blockastatement
around lets youif
block statement: put(condition)
many statements into a single bundle.
{ DoSomething; An if statement }can work
DoSomethingElse;
• Relational operators relationship between two elements: ==, =, <, >, <=, and >=.
operators let you check the relationship
• The operator inverts a bool expression.
• Combine multiple bool expressions with the && (“and”) and || (“or”) operators.

All of our previous programs have executed statements


statements one at a time from top to bottom.
bottom. Over
Over
the next few levels, we will learn some additional tools to change the flow of execution to allow
for more complexity beyond just one statement after the next. In this level, we will learn about
if statements. An if statement allows us to decide which sections of code to run.

THE IF 󰁓TATEMENT
Let’s say we need to determine a letter grade based on a numeric score. Our grading scale is
that an A is 90+, a B is 80 to 89, a C is 70 to 79, a D is 60 to 69, and an F is anything else.
It is easy to see how we could apply elements we already know in this situation. We need to
input the score and convert it to an int. We probably want a variable to store the score. We
might also want a variable to store the letter grade.
What we don’t have yet is the ability to pick and choose. We don’t have the tools to decide to
do one thing or another, depending on decision criteria. We need those tools to solve this
problem. The if statement is the primary tool for doing this. Here is a simple example:
string input = Console.ReadLine();
int score = Convert.ToInt32(input);

70 LEVEL 9 DECISION MAKING

if (score == 100)
Console.WriteLine("A+! Perfect score!");

Our statements have


have always been executed one at a time from top to bottom in the p past.
ast. With
an if statement, some of our statements may not always run. The statement immediately
following the if only runs if the condition indicated by the if statement is true. This program
will run differently depending on what score the user typed. If they typed in 100, it would
display that A+ text. Otherwise, it will display nothing at all.
An if statement is constructed using the keyword if, followed by a set of parentheses
containing an expression whose type is bool (any expression that evaluates to a bool value).
The expression inside of the parentheses is called the if statement’s condition.
This is the first time we have seen the == operator, which is the equality operator, sometimes
called the double equals operator . This operator determines if the things on either side are
equal, evaluating to true if they are and false if they are not. Thus, this expression will be
true only if the score that the user types equals 100. The statement following the if only runs
if the condition evaluates to true.
I have indented the line following the if statement—the one the if statement protects. C#
does not care about whitespace, so this indentation is for humans. Indenting like this
illustrates the code’s structure better, giving you a visual clue that this line is tied to the if
statement and does not always run. A second option is to write it all on a single line:
if (score == 100) Console.WriteLine("A+! Perfect score!");

This formatting also helps indicate that the WriteLine call is attached to the if statement.
Both of the above are commonly done in C# code. Even though the compiler doesn’t care
about the whitespace, you should always use one of these options (or a third with curly braces
that we will see in a moment). But don’t write it like this:
if (score == 100)
Console.WriteLine("A+! Perfect score!");

At a glance, you assume that the WriteLine statement happens every time and is not
you would assume
part of the if statement. This becomes especially problematic as you write llonger
onger programs.
Get in the habit of avoiding writing it this way now.

Block 󰁓tatements
The simplest if statement allows us to run a single statement conditionally. What if we need
to do the same with many statements?
We could just stick a copy of the if statement in front of each statement we want to protect,
but there is a better way. C# has a concept called a block statement. A block statement allows
al lows
you to lump many statements together and then use them anywhere that a single statement
is valid. A block statement is made by enclosing the statements in curly braces, shown below:
{
Console.WriteLine("A+!");
Console.WriteLine("Perfect score!");
}

An if statement can be applied to block statements just like a single statement:

THE IF STA
STATEMENT
TEMENT 71
if (score == 100)
{
Console.WriteLine("A+!");
Console.WriteLine("Perfect score!");
}

Using block statements with ifs is almost more common than not. Some C# programmers
prefer to use curly braces all the time, even if they only contain a single statement. They feel it
adds more structure, looks more organized, and helpshel ps them avoid mistakes.
Remember, even if you indent, if you don’t use a block statement, only the next statement is
guarded by the if. The code below
bel ow does not work as you’d expect from
from the indentation:
if (score == 100)
Console.WriteLine("A+!");
Console.WriteLine("Perfect score!"); // BUG!

The “Perfect score!” text runs every single time. If you keep making this mistake, consider
always using block statements to avoid this type of bug from the get-go.

Blocks, Variables, and 󰁓cope


One thing that may be surprising about block statements is that they get their own variables.
Variables
Variables created within a block cannot be used outside of the block. For example, this code
won’t compile:
string input = Console.ReadLine();
int score = Convert.ToInt32(input);

if (score == 100)
{
char grade = 'A';
}

Console.WriteLine(grade); // COMPILER ERROR.

The variable grade no longer exists once you get to Console.WriteLine on the last line.
If we were to draw this situation on a code map, it would look like this:
this :

72 LEVEL 9 DECISION MAKING


The input and score variables live directly in our main method, but the grade variable
lives in the if block. We can use grade within the if block. And, importantly, we can reach
outward and use input and score as well. But for code in our main method outside the if
block, we can’t refer to grade, only input and score. (We can sometimes dig into elements
operator, as we do with Console.WriteLine , but there is no
with the member access operator,
named code element to refer to here; that isn’t an option.) Thus, the identifiers input and
score are valid throughout the main method, including the if block, while the identifier
grade is only valid inside the block.
The code section where an identifier or name can be used is called its scope. Both input and
score have a scope that covers all of the main method. These two variables have method
scope. But grade’s scope is only big enough to cover the block. It has block scope.
If we want to use grade outside of the method, we must declare it outside of the block:
string input = Console.ReadLine();
int score = Convert.ToInt32(input);
char grade = '?';

if (score == 100)
{
grade = 'A';
}

Console.WriteLine(grade);

This change gives us a code map that looks like this:

Interestingly, because of scope, two blocks are allowed to reuse a name for different variables:
string input = Console.ReadLine();
int score = Convert.ToInt32(input);

if (score == 100)
{
char grade = 'A';
Console.WriteLine(grade);
}

if (score == 82)
{

THE ELSE STA


STATEMENT
TEMENT 73
char grade = 'B';
Console.WriteLine(grade);
}

I try to avoid this because it can be confusing, but it is allowed because the scope of the two
variables don’t overlap.
overlap. IItt is always clear which variable
variable is being referred to.
On the other hand, a block variable cannot reuse a name that is still in scope from the me
method
thod
You wouldn’t be able to make a variable in either of those blocks called input or score.
itself. You
THE ELSE 󰁓TATEMENT
alternative statementif
to run ifelse
The counterpart to is an statement. An else statement allows you to specify an
the if statement’s condition is false:
string input = Console.ReadLine();
int score = Convert.ToInt32(input);

if (score == 100)
Console.WriteLine("A+! Perfect score!");
else
Console.WriteLine("Try again.");

When this code runs, if the score is exactly 100, the statement after the if executes. In all
other cases, the statement after the else executes.
Y
You wrap an else statement around a block statement:
ou can also wrap
char letterGrade;

if (score == 100)
{
Console.WriteLine("A+! Perfect score!");
letterGrade = 'A';
}
else
{
Console.WriteLine("Try again.");
letterGrade = 'B';
}

ELSE IF 󰁓TATEMENT󰁓
While if and else let us choose from one of two options, the combination can create third
and fourth options. An else if statement gives you a second condition to check after the
initial if condition and before the final else:
if (score == 100)
Console.WriteLine("A+! Perfect score!");
else if (score == 99)
Console.WriteLine("Missed it by THAT much."); // Get Smart reference, anyone?
else if (score == 42)
Console.WriteLine("Oh no, not again."); // A more subtle reference...
else
Console.WriteLine("Try again.");

74 LEVEL 9 DECISION MAKING


The above code will only run one of the four pathways. The pathway chosen will be the first
one from top to bottom whose condition is true, or if none are true, then the statement
under the final else is the one that
t hat runs.
And like if and else, an else if can contain a block with multiple statements if needed.
The trailing else is optional; just like how you can have a simple if without an else, you
can have an if followed by several else if statements without a final else.

RELATIONAL OPERATOR󰁓: ==, !=, <, >, <=, >=


Checking if two things are exactly equal with the equality operator ( ==) is useful, but it is not
the only way to define a condition. It is one of many relational operators that check for some
particular relation between two values.
The inequality operator ( =) is its opposite, evaluating to true if the two things are not equal
and false if they are. So 3 = 2 is true while 3 = 3 is false. For example:
if (score != 0) // Usually read aloud as "if score does not equal 0."
Console.WriteLine("It could have been worse!");

There are also the “greater than” and “less than” operators, > and <. The greater than operator
(>) is true if the value on the left is greater than the right, while the less than operator (<) is
true if the value on the left is less than the right. These two operators are enough to write a
decent solution to the letter grade problem:

string input
int score = Console.ReadLine();
= Convert.ToInt32(input);

if (score > 90)


Console.WriteLine("A");
else if (score > 80)
Console.WriteLine("B");
else if (score > 70)
Console.WriteLine("C");
else if (score > 60)
Console.WriteLine("D");
else
Console.WriteLine("F");

There is a small problem with the code above. Our initial description said that 90 should count

as an A.than
greater In this
90, code, a score
after all. of 90 shift
We could will not
ourexecute
numbers the first one
down block but
and the second.
make 90 is not
the condition be
score > 89, but that feels less natural.
To solve this problem, we can use the “greater than or equal” operator ( >=) and its
counterpart, the “less than or equal” operator (<=). The >= operator evaluates to true if the
left thing is greater than or equal to the thing on the right. The <= operator evaluates to true
if the left thing is less
l ess than or equal to the thing on the right. These operators allow us to write
a more natural solution to our grading problem:
if (score >= 90)
Console.WriteLine("A");
else if (score >= 80)
Console.WriteLine("B");
else if (score >= 70)
Console.WriteLine("C");

USING BOOL IN DECISION MAKING 75


else if (score >= 60)
Console.WriteLine("D");
else
Console.WriteLine("F");

These symbols look similar to the ≥ and ≤ symbols used in math, but those symbols are not
on the keyboard, so the C# language uses something more keyboard-friendly.

U󰁓ING BOOL IN DECI󰁓ION MAKING


The conditions of an if and else if do notn ot just have to be one of these operators. You
You can
use any bool expression. These operators just happen to be simple bool expressions.
Another example of a simple bool expression is to refer to a bool variable. The code below
uses an if/else to assign a value to a bool variable. That variable is then used in the
condition of another if statement later on.
int score = 45; // This could change as the player progresses through the game.
int pointsNeededToPass = 100;

bool levelComplete;

if (score >= pointsNeededToPass)


levelComplete = true;
else
levelComplete = false;

if (levelComplete)
Console.WriteLine("You've beaten the level!");

With a little cleverness and practice, you might also recognize


recognize that you could shorten the code
above. levelComplete always takes on the same value as the condition score >=
pointsNeedToPass . We could make this code be:
bool levelComplete = score >= pointsNeededToPass;

if (levelComplete)
Console.WriteLine("You've beaten the level!");

The above code also illustrates that you can use relational operators like >= in any expression,
not just in if statements. (Though the two pair nicely.)

Perhapsvariable)
named the besttobenefit
the logic of score
of the >= pointsNeededToPass
above code is that we have given a. name (in theit form
That makes easieroffor
a
us to remember what the code is doing.

Challenge Repairing the Clocktower 100 XP


The recent attacks damaged the great Clocktower of Consolas. The citizens of Consolas have repaired
most of it, except one piece that requires the steady hand of a Programmer. It is the part that makes the
clock tick and tock. Numbers flow into the clock to make it go, and if the number is even, the clock’s
pendulum should tick to the left; if the number is odd, the pendulum should tock to the right. Only a
programmer can recreate this critical clock element to make it work again.
Objectives:
• Take a number as input from the console.

76 LEVEL 9 DECISION MAKING


• Display the word “Tick” if the number is even. Display the word “Tock” if the number is odd.
• Hint: Remember that you can use the remainder operator to determine if a number is even or odd.

LOGICAL OPERATOR󰁓
Logical operators allow you to combine other bool expressions in interesting ways.
The first of these is the “not” operator ( ). This operator takes a single thing as input and
produces the Boolean opposite: true becomes false, and false becomes true:
bool levelComplete = score >= pointsNeededToPass;

if (!levelComplete)
Console.WriteLine("This level is not over yet!");

The other two are a matching set: the “and” operator (&&) and the “or” operator ( ||). (The |
character is above the <Enter> key on most keyboards and typically requires also pushing
.) && and || allow you to combine two bool expressions into a compound expression.
<Shift>.)
<Shift>
For &&, the overall expression is only true if both sub-expressions are also true. For ||, the
overall expression is true if either sub-expression is true (including if both expressions are
true). The code below deals with a game scenario where the player has both shields and armor
and only loses the game if their shields and armor both reach 0:
int shields = 50;
int armor = 20;

if (shields <= 0 && armor <= 0)


Console.WriteLine("You're dead.");

This can be read as “if shields is less than or equal to zero, and armor is less than or equal
to zero….” With the && operator, both parts of the condition must be true for the whole
expression to be true.
The || operator is similar, but if either sub-expression is true, the whole expression is true:
int shields = 50;
int armor = 20;

if (shields > 0 || armor > 0)


Console.WriteLine("You're still alive! Keep going!");

With either
e ither of these, the computer will do lazy evaluation, meaning if it already knows the
whole expression’s
expression’s answer after evaluating only the first part, it won’t bother evaluating the
second part. Sometimes, people will use that rule to put the more expensive expressions on
the right side, allowing them to skip its evaluation when not needed.
These expressions let us form new expressions from existing expressions. For example, we
could have an && that joins two other && expressions—an amalgamation of four total
expressions. Like many tools we have learned about, just because you can do this doesn’t
mean you should. If a single compound expression becomes too complicated to understand
readily, split it into multiple pieces across multiple lines to improve the clarity of your code:
int shields = 50;
int armor = 20;

NESTIN
NESTINGG IF STA
STATEMENTS
TEMENTS 77
bool stillHasShields = shields > 0;
bool stillHasArmor = armor > 0;

if (stillHasShields || stillHasArmor)
Console.WriteLine("You're still alive! Keep going!");

NE󰁓TING IF 󰁓TATEMENT󰁓
An if statement is just another statement. That means you can put an if statement inside of
another if statement. Doing so is called nesting, or you might say, “this if statement is nested
inside this other one.” For example:
if (shields <= 0)
{
if (armor <= 0)
Console.WriteLine("Shields and armor at zero! You're dead!");
else
Console.WriteLine("Shields are gone, but armor is keeping you alive!");
}
else
{
Console.WriteLine("You still have shields left. The world is safe.");
}

But if you can nest if statements once, you can do it a dozen


d ozen times. An if statement in an if
statement in an if statement. Occasionally, you will encounter (or write) deeply nested if
statements with many layers. These can get difficult to read, and I recommend keeping them
as shallow as you can. Using bool variables can help with this.

THE CONDITIONAL OPERATOR


C# has another operator that works like an if statement but is an expression instead of a
statement. It is called the conditional operator (or sometimes the ternary operator because it
is the only operator in C# that takes three inputs). It operates on three different expressions: a
condition to check (a bool expression), followed by two other expressions, one that should
be evaluated if the condition is true and the other that should be evaluated if the condition
is false. It is done by placing these three expressions before, between, and after the ? and :
symbols like this:
condition expression ? expression if true : expression if false

A simple example might look like this:


string textToDisplay = score > 70 ? "You passed!" : "You failed.";
Console.WriteLine(textToDisplay);

Remember that literals and variable access are both simple expressions. While the above code
uses string literals, they could have been more complex. Combining three expres
expressions
sions can
lead to complex code, so be cautious when using this to ensure that your code stays
understandable.

78 LEVEL 9 DECISION MAKING


Challenge Watchtower
Watchtower 100 XP
There are watchtowers in the region around Consolas that can alert
you when an enemy is spotted. With some help from you, they can tell
you which direction the enemy is from the watchtower.
Objectives:
• Ask the user for an x value and a y value. These are coordinates of
the enemy relative to the watchtower’s location.
• Using the image on the right, if statements, and relational
operators, display a message about what direction the enemy is
coming from. For example, “The enemy is to the northwest!” or
“The enemy is here!”

LEVEL 10
󰁓WITCHE󰁓

󰁓peedrun
• Switches are an alternative to multi-part if statements.
• The statement form: switch (number) { case 0: DoStuff(); DoStuff() ; break; case 1:
DoStuff(); break; default: DoStuff() break; }
• The expression form: number switch { 0 => "zero", 1 => "one", _ => "other" }

Most if statements are simple: a single if, an if/else, an if/else if, or an if/else
if/else. But sometimes, they end up with long chains with many possible paths to take. In
these lengthy cases, it can start to look and feel like a railroad switchyard—one track splits into
many to allow for grouping or categorizing railcars along the various paths, like in the image
below.

This analogy isn’t a coincidence; C# has a switch concept named after this exact railroad
switching analogy. They are for situations where you want to go down one of many possible
pathways
pathwa ys called arms, based on a single value’s
value’s properties.
Every switch could also be written with if and else. The code might be simpler for either,
depending on the situation.
There are two kinds of switches in C#: a switch statement and a switch expression. We will
introduce both here. In Level 40, we will learn about patterns, which make switches much
more powerful.

80 LEVEL 10 SWITCHES
󰁓WITCH 󰁓TATEMENT󰁓
To illustrate the mechanics of a switch, consider a menu system where the user picks the
number of the menu item they want to activate, and the program performs the chosen task:
task :
Avast, matey! What be ye desire?
1 – Rest
2 – Pillage the port
3 – Set sail
4 – Release the Kraken
What be the plan, Captain?
We will
will keep the mechanics simple here and just
just display a mess
message
age in response.

The if-based version might look like this:


int choice = Convert.ToInt32(Console.ReadLine());

if (choice == 1)
Console.WriteLine("Ye rest and recover your health.");
else if (choice == 2)
Console.WriteLine("Raiding the port town get ye 50 gold doubloons.");
else if (choice == 3)
Console.WriteLine("The wind is at your back; the open horizon ahead.");
else if (choice == 4)
Console.WriteLine("'Tis but a baby Kraken, but still eats toy boats.");
else
Console.WriteLine("Apologies. I do not know that one.");

e quivalent switch statement looks like this:


This is a candidate for a switch, and the equivalent
switch (choice)
{
case 1:
Console.WriteLine("Ye rest and recover your health.");
break;
case 2:
Console.WriteLine("Raiding the port town get ye 50 gold doubloons.");
break;
case 3:
Console.WriteLine("The wind is at your back; the open horizon ahead.");
break;
case 4:
Console.WriteLine("'Tis but a baby Kraken, but still eats toy boats.");
break;
default:
Console.WriteLine("Apologies. I do not know that one.");
break;
}

This illustrates the basic structure of a switch statement. It starts with the switch keyword.
A set of parentheses enclose the value that decisions are based on. Curly braces denote the
beginning and end of the switch block.
Each possible path or arm of the switch statement starts with the case keyword, followed
by the value to check against. This is followed
foll owed by any statements that should run if this arm’s
arm’s
condition matches. Here, in each arm, we use Console.WriteLine to print out an
appropriate message.
message. Many statements can go into each arm (no curly braces ne necessary).
cessary).

SWITCH EXPRESSIONS 81
Each arm must end with a break statement. The break signals that the flow of execution
should stop where it is and resume after the switch.
The default keyword provides a catch-all if nothing else was a match. If the user entered a
0 or an 88, this arm is the one that would execute. Strictly speaking, default can go anywhere
in the list and still be the default option if there is no other match. But the convention is to put
it at the end, which is a good
g ood convention to follow.
Having a default arm is common but optional. If your situation doesn’t need it, skip it.
Execution through a switch statement starts by determining which arm to execute—the first
matching condition or default if there is no other matching condition. It then runs the
matching arm’s
arm’s statements and, when finished, jumps past the end of the switch.
The above code uses an int in the switch’s
switch’s condition, but any type can be used.

Multiple Cases for the 󰁓ame Arm


While most arms
arm s in a switch statement are independent of each other, C# does allow you to
include multiple case statements for any given arm:
case 1:
case 2:
Console.WriteLine("That's a good choice!");
break;

In this case, if the value was 1 or 2, the statements in this arm will be executed.

󰁓WITCH EXPRE󰁓󰁓ION󰁓
Switches also come in an expression format as well. In expression form, each arm is an
expression, and the whole switch is also an expression. Our pirate menu looks like this when
written as a switch
switch expression:
string response;

response = choice switch


{
1 => "Ye rest and recover your health.",
2 => "Raiding the port town get ye 50 gold doubloons.",
3 => "The wind is at your back; the open horizon ahead.",
4
_ => "'Tis but a I
"Apologies. baby Kraken,
do not knowbut still
that eats toy boats.",
one."
};

Console.WriteLine(response);

A switch expression
ex pression has a lot in common with a switch statement structurally but also has
quite a few differences. For starters, in a switch expression, the switch’s target comes before
the switch keyword instead of after.
Aside from that difference, much of the clutter has been removed or simplified to produce
more streamlined code. The case labels are gone, replaced with just the specific value you
for. Each arm also has that arrow operator (=>), which separates the arm’s
want to check for.
condition from its expression. The breaks are also gone; each arm can have only one
expression, so the need to indicate the end
en d is gone.

82 LEVEL 10 SWITCHES
Each arm is separated by a comma, though it is typical to put arms on separate lines.
The default keyword is also gone, replaced with a single underscore—the “wildcard.”
Switch expressions do not need a wildcard but often have one. If there is no match on a switch
statement, the default behavior is to do nothing. No problem there. With a switch expression,
the overall expression has to evaluate to something, and if it can’t find an expression to
evaluate, the program will crash. So switch expressions should either provide a default
through a wildcard or ensure that the other arms cover all possible scenarios.
sce narios.
Both flavors of switches, as well as if/else statements, have their uses. One is not universally
better than the others. You will generally want to pick the version that results in the cleanest,
simplest code for the job.

󰁓WITCHE󰁓 A󰁓 A BA󰁓I󰁓 FOR PATTERN MATCHING


We have only scratched the surface of what switches can do. We have seen how switches
categorize data into one of several options. Yet the categorization rules we have seen so far
have been only of the simplest flavors: “Is this exactly equal to this other thing?” and “Is this
anything besides one of the other categories?” In Level 40, we will see many other ways to
categorize things that make switches far more powerful.

Challenge Buying Inventory 100 XP


It is time to resupply. A nearby outfitter shop has the supplies you need but is so disorganized that they
cannot sell things to you. “Can’t sell if I can’t find the price list,” Tortuga, the owner, tells you as he turns
over and attempts to go back to sleep in his reclining chair in the corner.
There’s a simple solution: use your programming powers to build something to report the prices of
various pieces of equipment, based on the user’s selection:
The following items are available:
1 – Rope
2 – Torches
3 – Climbing Equipment
4 – Clean Water
5 – Machete
6 – Canoe
7 – Food Supplies
What number do you want to see the price of? 2
Torches cost 15 gold.

You search around the shop and find ledgers that show the following prices for these items: Rope: 10
gold, Torches: 15 gold, Climbing Equipment: 25 gold, Clean Water: 1 gold, Machete: 20 gold, Canoe: 200
gold, Food Supplies: 1 gold.
Objectives:
• Build a program that will show the menu illustrated above.
• Ask the user to enter a number from the menu.
• Using the information above, use a switch (either type) to show the item’s cost.

SWITCHES AS A BASIS FOR PA


PATTERN
TTERN MAT
MATCHING
CHING 83
Challenge Discounted Inventory 50 XP
After sorting through
throu gh Tortuga’s
Tortuga’s outfitter shop and making it viable again, T
Tortuga
ortuga realizes
realize s you’ve put him
back in business. He wants to repay the favor by giving you a 50% discount on anything you buy from
him, and he wants you to modify your program to reflect that.
After asking the user for a number, the program should also ask for their name. If the name supplied is
your name, cut the price in half before reporting it to the user.
Objectives:
• Modify your program from before to also ask the user for their name.
• If their name equals your name, divide the cost in half.

LEVEL 11
LOOPING
󰁓peedrun
• Loops repeat code.
• while loop: while (condition) { ... }
• do/while loop: do { ... } while (condition);
• for loop: for (initialization; condition;
condition; update) { ... }

break exits the loop. continue immediately jumps to the next iteration of the loop.

In Level 3, we learned that listing statements one after the next causes them to run in that
order. In Levels 9 and 10, we learned that we could use if statements and switches to skip
over statements and pick which of many instructions to run. In this level, we’ll discuss the
third and final essential element of procedural programming: the ability to go back and repeat
code—a loop.
C# has four types of loops. We discuss three of these here
he re and save the fourth for the next
n ext level.

THE WHILE LOOP


The first loop type is the while loop. A while loop repeats code over and over for as long as
some given condition evaluates to true. Its structure closely resembles an if statement:
while ( condition )
{
// This code is repeated as long as the condition is true.
}

A while loop can be placed around a single statement. The above code just ha
happens
ppens to use a
block.
The following code illustrates a while loop that displays the numbers 1 through 5:
int x = 1;
while (x <= 5)

THE WHILE LOOP 85


Console.WriteLine(x);
x++;
}

Let’s step through this code to see how the computer handles a while loop. Before we start,
we make sure we’ve got me mory for x and initialize that spot to the value 1. When
g ot a spot in memory
the while loop is reached, its expressi on is evaluated. If it is false, we skip past the loop and
e xpression
continue with the rest of our program. In this case, x <= 5 is true, so we enter the loop’s
body and execute it. The body will display the current value of x (1) and then increment x,
which bumps it up to 2.
At this point, we’re
we’re done running the loop’
loop’s body, and execution jumps back to the start
start of the
loop.
we runThe condition
through is evaluated
the loop’s
loop’ s body aasecond
second
sec x has changed,
time.displaying
ond time, but x
the value <= incrementing
2 and 5 is still truex, so
to
3.
This process repeats until after several cycles, x is incremented to 6. At this point, the loop’s
condition is no longer true, and execution continues after the loop.
A loop is a powerful construct, enabling us to write complex programs with simple logic. If we
were to display the numbers 1 through 100 without a loop, we would have 100
Console.WriteLine s! With a loop, we need only a singlesingl e Console.WriteLine.
Here are a few crucial subtleties of while loops to keep in mind:
loop’s condition is false initially, the loop’s body will not run at all.
1. If the loop’s
2. The loop’s condition is only evaluated when we check it at the start of each cycle. If the
condition changes in the middle of executing the loop’s body, it does not immediately
leave the loop.
c ondition never becomes false. For example,
3. It is entirely possible to build a loop whose condition
if we forgot the x++; in the above loop, it would run over and over with no escape. This is
called an infinite loop. It is occasionally done on purpose but usually represents a bug. If
your program seems like it has gotten stuck, check to see if you crcreated
eated an infinite loop
loop..
Let’s look at another example before moving on. This code asks the user to enter a number
between 0 and 10. It keeps asking (with a loop) until they enter a number in that range:
int playersNumber = -1;

while (playersNumber < 0 || playersNumber > 10)


{
Console.Write("Enter a number between 0 and 10: ");
string playerResponse = Console.ReadLine();
playersNumber = Convert.ToInt32(playerResponse);
}

This code initializes playersNumber to -1. Why? First, all variables need to be initialized
before they can be used, so we had to assign playersNumber something. It is a -1 because
that is a number that will guarantee that the loop runs at least once. If we had initialized it to
0, the loop’s
loop’s condition would have been false the first time, the body of the loop would not
run even once, and we would have never asked the user to eenter
nter a value.
This code also shows that a loop’s condition can be any bool expression, and we’re allowed
to use things like <, =, &&, and || here as well.

86 LEVEL 11 LOOPING
THE DO/WHILE LOOP
The second loop type is a slight variation on a while loop. A do/while loop evaluates its
condition at the end of the loop instead of the beginning. This ensures the loop runs at least
once. The following code is the do/while version of the previous sample:
int playersNumber;

do
{
Console.Write("Enter a number between 0 and 10: ");
Console.Write( Enter a number between 0 and 10: );
string playerResponse = Console.ReadLine();
playersNumber = Convert.ToInt32(playerResponse);
}
while (playersNumber < 0 || playersNumber > 10);

The beginning of the loop is marked with a do. The while and its condition come after the
loop’ss body. Don’t forget the semicolon at the end of the lline;
loop’ ine; it is necessary.
Because this loop’s
loop’s body always runs at least once, we no longer need to initialize the variable
to -1. playersNumber will be initialized inside
in side the loop to whatever the player chooses.

Variables Declared in Block 󰁓tatements and Loops


Blocks used in a loop are still just blocks. Like any block, variables decl
declared
ared within the lloop’
oop’ss
block are inaccessible once you leave
le ave the block. You can declare a variable inside the body of
a loop, but these variables will not be accessible outside of the loop or even in the loop’s
condition. In the code above, we had to declare playersNumber outside of the loop to use it
in the loop’s
loop’s condition.

THE FOR LOOP


The third loop type is the for loop. Let’s return to the first example in this level: counting to
5. The while loop solution was this:
int x = 1;
while (x <= 5)
{
Console.WriteLine(x);
x++;
}

Out of all this code, there is only one line with meat on it: the Console.WriteLine
statement. The rest is loop management. The first line declares and initializes x. The second
marks the start of the loop and defines the loop’s condition. The fifth line moves to the next
item.
This loop management overhead can be a distraction from the main purpose of the code. A
for loop lets you pack loop management code into a single lline.
ine. It is structured like this:
for (initialization statement; condition to evaluate; updating action)
{
// ...
}

If we rewrite this code as a for loop, we end up with the following:


fol lowing:

BREAK OUT OF LOOPS AND CONTINUE TO THE NEXT PASS 87


for (int x = 1; x <= 5; x++)
Console.WriteLine(x);

The for loop’s parentheses contain the loop management code as three statements,
separated by semicolons.
The first part, int x = 1, does any one-time setup needed to get the loop started. This nearly
always involves declaring a variable and initializing it to its starting value.
The second part is the condition to evaluate every time through the loop. A for loop is more
like a while loop than a do while loop if its condition is false initially, the for loop s
body will not run at all.

The final part defines how to change the variable used in the loop’s condition.
This change simplified things so that a block statement was no longer needed; I ditched the
curly braces to simplify the code. But a for loop, like while and do/while loops, can use
both single statements or block statements.
For certain types of loops, a for loop lets the meat of the loop stand out better than a while
or do-while loop allows, but all of them have their place.
While most for loops use all three statements, any of them can be left out if nothing needs to
t hat looks like for (;;) { ... } to
be done. You will even occasionally encounter a loop that
indicate a for loop with no condition and will loop forever
forever,, though I prefer while (true)
{ ... } myself.

BREAK OUT OF LOOP󰁓 AND CONTINUE TO THE NEXT PA󰁓󰁓


The break and continue statements give you more control over how looping is handled.
A break statement forces the loop to terminate immediately without reevaluating the loop’s
loop’s
condition. This lets us escape a loop we no longer
longe r want to keep running. The loop’s condition
is not reevaluated, so it means we can leave the loop while its condition is still technically
true.
A continue statement will cause the loop to stop running the current pass through the loop
but will advance to the next pass, recheck the condition, and keep looping if the condition still
You can think of continue as “skip the rest of this pass through the loop and continue
holds. You
to the next pass.”
The following code illustrates each of these mechanics in a simple program that asks the user
for a number and then makes some commentary on the number before going back to the start
and doing it over again:
while (true)
{
Console.Write("Think of a number and type it here: ");
string input = Console.ReadLine();

if (input == "quit" || input == "exit")


break;

int number = Convert.ToInt32(input);

if (number == 12)

88 LEVEL 11 LOOPING
{
Console.WriteLine("I don't like that number. Pick another one.");
continue;
}
Console.WriteLine($"I like {number}. It's the one before {number + 1}!");
}

This loop’s condition is true and would never finish without a break. But if the user types
"quit" or "exit", the break; statement is encountered. This causes the flow of execution
to escape the loop and carry on to the rest of the program.
If the user enters a 12, then that continue statement is reached. Instead of displaying the text
about the number being good, it tells the user to pick another one. The flow of execution
jumps to the loop’
loop’s beginning, the condition is rechecked, and the loop runs agai
again.
n.
Most loops don’t need breaks and continues. But the nuanced control is sometimes
helpful.

NE󰁓TING LOOP󰁓
We saw that n est if statements inside other if statements. We can also nest loops
that we could nest
inside of other loops. You can also put if statements inside of loops and loops inside of if
statements.
Nested loops are common when you need to do something with every combination of two
sets of things. For example, the following displays a basic multiplication table, multiplying
multiplying the
numbers 1 through 10 against the same set of numbers:
for (int a = 1; a <= 10; a++)
for (int b = 1; b <= 10; b++)
Console.WriteLine($"{a} * {b} = {a * b}");

This code displays a grid of *’s based on the number of rows and columns dictated by
totalRows and totalColumns .
int totalRows = 5;
int totalColumns = 10;

for (int currentRow = 1; currentRow <= totalRows; currentRow++)


{
for (int currentColumn = 1; currentColumn <= totalColumns; currentColumn++)

Console.Write("*");
Console.WriteLine();
}

Challenge The Prototype 100 XP


Mylara, the captain of the Guard of Consolas, has approached you with the beginnings of a plan to hunt
down The Uncoded One’s airship. “If we’re going to be able to track this thing down,” she says, “we need
you to make us a program that can help us home in on a location.” She lays out a plan for a program to
help with the hunt. One user, representing the airship pilot, picks a number between 0 and 100. Another
user, the hunter, will then attempt to guess the number. The program will tell the hunter that they
guessed correctly or that the number was too high or low. The program repeats until the hunter guesses
the number correctly. Mylara claims, “If we can build this program, we can use what we learn to build a
better version to hunt down the Uncoded One’s airship.”

NESTING LOOPS 89
󰁓ample Program:
User 1, enter a number between 0 and 100: 27

After entering this number, the program should clear the screen and continue like this:
User 2, guess the number.
What is your next guess? 50
50 is too high.
What is your next guess? 25
25 is too low.
What is your next guess? 27
You guessed the number!

Objectives:
• Build a program that will allow a user, the pilot, to enter a number.
• If the number is above 100 or less than 0, keep asking.
• Clear the screen once the program has collected a good number.
• Ask a second user, the hunter, to guess numbers.
• Indicate whether the user guessed too high, too low, or guessed right.
• Loop until they get it right, then end the program.

Challenge The Magic Cannon 100 XP


Skorin, a member of Consolas’s wall guard, has constructed a magic cannon that draws power from two
gems: a fire gem and an electric gem. Every third turn of a crank, the fire gem activates, and the cannon
produces a fire blast. The electric gem activates every fifth turn of the crank, and the cannon makes an
electric blast. When the two line up, it generates a potent combined blast. Skorin would like your help to
produce a program that can warn the crew about which turns of the crank will produce the different
blasts before they do it.
A partial output of the desired program looks like this:
1: Normal
2: Normal
3: Fire
4: Normal
5: Electric
6: Fire
7: Normal
...

Objectives:
• Write a program that will loop through the values between 1 and 100 and display what kind of blast
the crew should expect. (The % operator may be of use.)
• Change the color of the output based on the type of blast. (For example, red for Fire, yellow for
Electric, and blue for Electric and Fire).

LEVEL 12
ARRAY󰁓

󰁓peedrun
• Arrays contain multiple values of the same type. int[] scores = new int[3];
• Square brackets access elements in the array, starting with 0: scores[2] = scores[0] +
scores[1];
• Indexing from end: int last = scores[^1];

Getting a range: int[] someScores = scores[1..3];
• Length tells you how many elements an array can hold: scores.Length
• Lots of ways to create arrays: new int[3], new int[] { 1, 2, 3 } , new [] { 1, 2, 3 }
• Arrays can be of any type, including arrays of arrays ( string[], bool[][], int[][][]).
• The foreach loop: foreach (int score in scores) { ... }
• Multi-dimensional
Multi-dimension al arrays: int[,] grid = new int[3, 3];

Imagine you’re making a high scores table for a game. It is easy to see how we could make a
we’d use int or uint for its type. But we need
variable to represent a single score. Maybe we’d
many scores, not just one. Using only what we already know, you could imagine making
several variables for the different scores. If we want a Top 10, perhaps we’d
we’d do something like:
int score1 = 100;
int score2 = 95;
int score3 = 92;
// Keep going to 10.

It technically works. Writing out ten variables is not so bad


b ad to write out. But let’s hope we don’t
change our minds and want 100 or 1000!
C# can create space for a whole collection of values all at once. This is called an array. A single
variable can store an array of values, and each item within the array can be accessed by its
index—its number in the array. Thus, instead of creating score1, score2, etc., we can create
a single scores array for the job.

CREATING ARRAYS 91
CREATING ARRAY󰁓
The following declares a variable whose type is an “array of ints”:
int[] scores;

The square brackets ([ and ]) indicate that this variable contains an array of many values
rather than just a single one. Square brackets are a common sight when working with arrays.
Each array contains only elements of a specific type. The above was an array of ints, indicated
by int[]. You could also call this an int array. We could make a string array with a type of
string[] or a bool array with a type of bool[].

After declaring an array variable,


variable, the next step is to construct a new array to hold our items:
int[] scores = new int[10];

The new keyword creates new things in your program. For the built-in types like int and
bool, the C# language has simple syntax for creating new values: literals like 3, true, and
"Hello". As we begin working with more complex types like arrays, we’ll use new. The code
above creates a space large enough to hold ten int values, hence the int[10]. This new
collection of numbers is stored in the scores variable.
We could have made this array any size we want, but once an array value has been
constructed, it can no longer change size. You cannot extend or shrink it. The contents of
scores cannot be resized. However, we can use new a second time to create a second array
with more (or We could update scores with this new, longer array:
(or fewer) items. We
scores = new int[20];

This is a brand new array using new memory for its contents. The scores variable switches
to use this new memory instead of the memory of the initial 10-item array. That means any
data we may have put in the original 10-item array is still over there, not in this new 20-item
array. If we wanted that data in the new array, we would need to copy it over.
In Level 32, we will learn about lists.
li sts. Lists are a much more powerful tool than ar
arrays,
rays, and they
allow you to add and remove items as needed. Once we learn about lists, we probably won’t
use arrays very often. But lists build on top of arrays, so they are still important to know.

GETTING AND 󰁓ETTING VALUE󰁓 IN ARRAY󰁓


Let’s look at how to work with specific items within the array. To rrefer
efer to a specific item in an
array, you use the indexer operator ([ and ]). For example, this code assigns a value to spot #0
in the scores array:
scores[0] = 99;

The number in the brackets is called the index. The code above stores the value 99 into
scores at index 0. This index can be any int expression, not just a literal. For example, you
could also do this: scores[someSpot + 1].
Perhaps surprisingly, indexing starts at 0 instead of 1. You You can think of this as a family
tradition; Java, C++, and C start indexing at 0. Doing so is called 0-based indexing. Not every
programming language works this way, but many do. do. In C#, the first spot is #
#0.
0.

92 LEVEL 12 ARRAYS
Other values in the array can be accessed with other numbers:
scores[1] = 95;
scores[2] = 90;

Y
You
ou can also use the indexer
indexer operator to read the
the current value in an array
array at a specific index:
Console.WriteLine(scores[0]);

This writes out the current value of the first (0th index) element in the scores array.
Default Values
When a new
bit to 0. Thisarray is created,initializes
automatically the computer
everywill
spottake
takin
e the array’s
array’sbut
an array, memory location
what does and set every
it initialize it to?
The meaning of “every bit is 0” depends on the type. For every numeric type, including both
integers and floating-point types, this is the number 0. For bool, this is false. For a
character, this is a special character called the null character. For a string, it is a thing that
represents a missing or non-existent value called null. We’ll learn more about null values later. later.
For now, treat null strings as though they were uninitialized.
But the good part is that we don’t need to go through a whole array and populate it with
specific values if the default value is good enough. For example, suppose we do this:
int[] scores = new int[5];

c ontain five spots, each with a value of 0.


This array of length five will contain

Crossing Array Bounds


Attempting to access an index beyond what its size supports would lead to bad and even
dangerous things. C# ensures that any attempt to reach beyond the beginning or end of an
array is stopped before it can happen, creating an index out-of-range error that will crash your
program if not addressed (Level 34).
34 ). Such a problem would occur with the code below:
int[] scores = new int[5];
scores[10] = 1000;

scores has five items, and they are numbered 0 through 4. Those are the only safe numbers,
and the attempt to access spot #10 will fail. An attempt to access index -1 would fail for the
same reason.

Y
You
ou want to make sure you only access legitimate spots within an array. Luckily, each array
remembers how long it is. It can tell you if you ask. By referring to the array’s Length variable
(technically a property, but more on that later), you can see how many items it contains:
Console.WriteLine(scores.Length);

This is especially useful when we don’t know how big an array might be. The code below asks
ar ray of that size, then uses a for loop to fill it with values:
the user for a length, creates an array
int length = Convert.ToInt32(Console.ReadLine()); // Combined into one line!
int[] array = new int[length];

for(int index = 0; index < array.Length; index++)


array[index] = 1;

This will produce an array full of 1’s,


1’s, with as many elements as the user asked for.

OTHER WAYS TO CREATE ARRAYS 93


for loops are commonly used with arrays. The scheme above is typical and worth noting
when you need to do it yourself. Most C# programmers will start the index at 0, loop as long
as the loop’s variable is less than the array’s length, incrementing each time through the loop.

Indexing from the End


On occasion, you want to access items relative to the back of the array
ar ray instead of the front. You
can use the ^ operator to accomplish this. The code below gets the last item in scores:
int lastScore = scores[^1];

And yes, from start at 0, but from the back, you start at 1.
from the front, you start

Ranges
Y
You
ou can also grab a copy of a section or range within an array with the range operator (..):
within
int[] firstThreeScores = scores[0..3];

With arrays, this makes a copy. Making a change in firstThreeScores will not affect the
original scores array.
The numbers on the range deserve
deser ve a brief discussion. The first number is the index to start at.
The second number is the index to end at, but it is not included in the copy. 0..3 will grab
the elements at indexes 0, 1, and 2, but not at 3.
These numbers can be any int expression, and you can also use ^ to index from the back. For
example, this code makes a copy of the array except for the first and last items:
int[] theMiddle = scores[1..^1];

If your endpoint is before your start point, your program will crash, so you’ll want to ensure
that this doesn’t happen.
Y
You
ou can also leave off either end (or both ends) to use a default of the arra
array’
y’ss start or end. For
example, scores[2..] creates a copy of the entire array except the first two.

OTHER WAY󰁓 TO CREATE ARRAY󰁓


While the simple new int[10] approach is a common way to create new arrays, some
variations
can onalternative:
use this that idea exist. If you know
know what values you want
want your array to hold initially, you

int[] scores = new int[10] { 100, 95, 92, 87, 55, 50, 48, 40, 35, 10 };

Each value is listed, separated by commas, and enclosed in curly braces. This scheme is called
collection initializer syntax. The number of items and the length you have listed must match
each other, but
but if you list all of the items, you can also skip stating the length in the first place:
int[] scores = new int[] { 100, 95, 92, 87, 55, 50, 48, 40, 35, 10 };

If the type of values listed is clear enough for the compiler to infer the type, you don’t even
need to specify the type when you create an array:
int[] scores = new [] { 100, 95, 92, 87, 55, 50, 48, 40, 35, 10 };

94 LEVEL 12 ARRAYS
󰁓OME EXAMPLE󰁓 WITH ARRAY󰁓
Let’s look at some examples with a little more complexity.
This first example calculates the minimum value in an array. The basic process is to hang on
to the smallest value we have found so far and work our way down the array looking at each
item. For each item, we check to see if it is less than the smallest number we have found so far.
far.
If so, we start using that as our smallest number instead. Once we reach the end of the array,
we know the item we’ve set aside is the smallest in the array.
array.
int[] array = new int[] { 4, 51, -7, 13, -99, 15, -8, 45, 90 };

int currentSmallest = int.MaxValue; // Start higher than anything in the array.


for (int index = 0; index < array.Length; index++)
{
if (array[index] < currentSmallest)
currentSmallest = array[index];
}

Console.WriteLine(currentSmallest);

The following example calculates the average value of the numbers in an array. The average
value is the total of all items in the array, divided by the number of items it contains.
c ontains. We
We can
determine the sum of all items in the array by keeping a running total, starting at 0, and adding
each item to that running total as we iterate across them with a loop. Once we have finished
that, we compute the average by taking the total and dividing it by the number of items:
int[] array = new int[] { 4, 51, -7, 13, -99, 15, -8, 45, 90 };

int total = 0;
for (int index = 0; index < array.Length; index++)
total += array[index];

float average = (float)total / array.Length;


Console.WriteLine(average);

Challenge
Challe nge The Replicator of D’To
D’To 100 XP
While searching an abandoned storage building containing strange code artifacts, you uncover the
ancient Replicator of D’To. This can replicate the contents of any int array into another array. But it
appears broken and needs a Programmer to reforge the magic that allows it to replicate once again.
Objectives:
• Make a program that creates an array of length 5.
• Ask the user for five numbers and put them in the array.
• Make a second array of length 5.
• Use a loop to copy the values out of the original array and into the new one.
• Display the contents of both arrays one at a time to illustrate that the Replicator of D’To works
again.

THE FOREACH LOOP 95


THE FOREACH LOOP
Arrays and loops often go together because doing something with each item in an array is
common. For example, this displays all items in an array:
int[] scores = new int[10];

for(int index = 0; index < scores.Length; index++)


{
int score = scores[index];
Console.WriteLine(score);
}

The fourth and final loop type in C# is the foreach loop. It is designed for this scenario, with
simpler syntax than a for loop. The following is the same as the previous code:
int[] scores = new int[10];

foreach (int score in scores)


Console.WriteLine(score);

To make a foreach loop, you use the foreach keyword. Inside of parentheses, you declare
a variable that will hold each item in the array in turn. The in keyword separates the variable
from the array to iterate over. The variable can be used inside the loop, as shown above.
The downside to a foreach loop is that you lose knowledge about which index you are at—
something a for loop makes clear with the loop’s
loop’s variable. If you want access to both the item
and its index (for example, to display text like “Score #3 is 82”), your best bet is a for loop.
A foreach loop is typically easier to read than its for counterpart, but a foreach loop also
runs slightly slower than a for loop. If performance becomes a problem, you might rewrite a
problematic foreach loop as a for loop to speed it up.

Challenge The Laws of Freach 50 XP


The town of Freach recently had an awful looping disaster.
disas ter. The lead investigator found that it was a faulty
++ operator in an old for loop, but the town council has chosen to ban all loops but the foreach loop.
Yet Freach uses the code presented earlier in this level to compute the minimum and the average value
in an int array. They have hired you to rework their existing for-based code to use foreach loops
instead.
Objectives:
• Start with the code for computing an array’s minimum and average values in the section Some
Examples with Arrays, starting on page 94.
• Modify the code to use foreach loops instead of for loops.

MULTI-DIMEN󰁓IONAL ARRAY󰁓
Most of our array examples have been int arrays, but there are no limits on what types can
be used in an array. We could just as easily use double[], bool[], and char[]. You can
even make arrays of arrays! For example, imagine if you have the following matrix of numbers:

96 LEVEL 12 ARRAYS
1 2
� 
3
5
4
6

Y
You
ou could represent this structure and its contents with something like
like the following:
int[][] matrix = new int[3][];
matrix[0] = new int[] { 1, 2 };
matrix[1] = new int[] { 3, 4 };
matrix[2] = new int[] { 5, 6 };
Console.WriteLine(matrix[0][1]); // Should be 2.

The setup for an array of arrays is ugly because each array within the main array must be
initialized independently. Arrays of arrays are most often used when each of the smaller arrays
needs to be a different size. This is sometimes referred to as a jagged array.
Y
You
ou often want a grid
gr id with a specific number of rows and columns. C# arrays can be multi-
dimensional, containing more than one index. Arrays of this nature are called multi-
dimensional arrays or rectangular arrays. An example is shown below:
int[,] matrix = new int[3, 2] { { 1, 2 }, { 3, 4 }, { 5, 6 } };
Console.WriteLine(matrix[0, 1]);

With multi-dimensional arrays,


arrays, you indicate that it has more than one dimension by placing
a comma inside the square brackets. When creating a new multi-dimensional array, place its
sizes in the square brackets,
brackets, separated by commas. To initialize it with specific values, you use
sets of curly braces inside other curly braces. The setup is not trivial, but it is easier than jagged
arrays.
Working
Worki ng with items in a multi-dimensional array is done by sup supplying
plying two indices (the plural
of index, though indexes is sometimes used) in the square brackets, separated by commas, as
shown above.
Multi-dimensional arrays can have as many dimensions as you need (for example,
bool[,,]), and you can have multi-dimensional arrays of regular arrays or regular arrays of
multi-dimensional arrays (int[,][], float[][,,,] , etc.). These get tough to understand
very quickly, so proceed
proceed with caution.
To loop through all elements
e lements in a multi-dimensional array, you will probably want to use the
GetLength method, handing it the dimension
dimen sion you are interested in (starting with 0, not 1):
1 ):
int[,] matrix = new int[4,4];

for (int row = 0; row < matrix.GetLength(0); row++)


{
for (int column = 0; column < matrix.GetLength(1); column++)
Console.Write(matrix[row, column] + " ");

Console.WriteLine();
}

LEVEL 13
METHOD󰁓

󰁓peedrun
• Methods let you name and reuse a chunk of code: void CountToTen() { ... }
• Parameter m ethod to work with different data each time it is called: void CountTo(int
Parameterss allow a method
amount) { ... }
• Methods can produce a result with a return value: int GetNumber() { return 2; }

Two methods can have the same name (an overload) if their parameters are different.
• Some simple methods can be defined with an expression body: int GetNumber() => 2;
• Recursion is when a method calls itself.

As we have collected more programming tools tool s for our inventory, our programs are
a re growing
bigger. We need to start learning how to begin organizing our code. C# has quite a few tools
for code organization, but the first one we’ll learn
le arn is called a method.
We have
have already been
b een both using and creating methods already. For example, we have used
Console’s WriteLine method and Convert’s ToInt32 method. And every program we
have made has also had a main method, which contains the code we have written and is the
entry point for our program.
But in this level, we will look at how we can make additional methods and use them to break
our code into small, focused, and reusable elements
eleme nts in our code.

DEFINING A METHOD
To make a new method, we need to understand where and how to make a method. The
following code illustrates one way to do it:
Console.WriteLine("Hello, World!");

void CountToTen()
{
for (int current = 1; current <= 10; current++)

98 LEVEL 13 METHODS
Console.WriteLine(current);
}

The line that says void CountToTen(), the curly braces, and everything inside them
defines a new CountToTen method.
For the moment, let’s focus on that void CountToTen() line. This line declares or creates a
method and establishes how to use it.
CountToTen is the method s name. Like variables, you have a lot of flexibility in naming your
methods, but most C# programmers will use UpperCamelCase for all method names.
The void part, before the name, is the method’s return type. We’ll deal with this in more depth
later. For now, all we need to know is that void means the method does not produce a result.
later.
Every method declaration includes a set of parentheses containing information for the
method to use. CountToTen doesn’t need any information to do its job, so we’ve left the
parentheses empty for now.
After the declaration is the method’s
method’s body, containing all the code that should run when
called. In this case, the body is the curly braces and all statements in between. All of the code
we have past—loops, ifs, calls to Console.WriteLine , etc.—can all be used in
have used in the past—loops,
any method you create.

Local Functions
Our definition of CountToTen above puts it inside of the main method. The code map below
illustrates this arrangement:

Until now, we have only seen methods that live directly in a class. For example, WriteLine
lives in Console, and Main lives in Program. This code map shows that methods can also
be defined inside other methods.
Once we start making classes (Level 18)
1 8),, nearly all of our methods will live in a class. Until
then, we can define methods inside our main method.
While we’re
we’re on the subject, let’s get precise in our terminology:
terminology: C# programmers
programmers often use the
words method and function synonymously. But there are some subtle differences. Formally,
any reusable, callable code block is a function. A function is also a method if it is a member of
a class. So technically, Main is a method, but CountToTen is not. Functions that are defined
inside of other functions are known as local functions. So CountToTen is a local function, but

CALLING A METHOD 99
Main is not. In casual conversation, C# programmers will use both method or function
interchangeably (with method being more common) and only get formal or specific when the
distinction matters. For example, somebody might say, “I don’t think that should be a local
function. I think it should be an actual method.”
method.”
A local function can live anywhere within its containing method. You could put them at the
top, above your other statements, at the bottom, after all your other statements, somewhere
in the middle, or scattered across the method. The compiler doesn’t care where they go. The
compiler extracts them and gives them slightly different names behind the scenes, so it
compiler extracts them and gives them slightly different names behind the scenes, so it
doesn’t care about the ordering. Since they can gog o anywhere, use that to your advantage, and
put them in the place that makes the code most understandable.
understandable. For our main method, I feel
it makes the most sense to put these after everything
ev erything else, so that is what I will do in this book.

CALLING A METHOD
Our code above defined a CountToTen method but didn’t put it to use. Let’s fix that. We’ve
called methods before, like Console.WriteLine, so the syntax should be familiar:
CountToTen();

void CountToTen()
{
for (int current = 1; current <= 10; current++)
Console.WriteLine(current);
}

The most notable difference is that we didn’t put a class name first, as we’ve done with
Console.WriteLine . Since CountToTen lives in our main method, we can refer to it
without any qualifiers
qualifiers from anywhere
anywhere in the main method.
Let’s take a moment to consider how this code runs. When this main method begins, it
encounters the call to CountToTen. Your program notes where it was in the main method,
jumps over to the CountToTen method, and runs the instructions it finds there (the for
loop). After running the loop to completion, the flow of execution hits the end of
CountToTen, looks back at the notes it made about where it came from, and returns to that
place, resuming execution back in the main method. In this case, there are no more
statements to run, and the main method ends,
en ds, finishing the program.
Notably, just because the definition of CountToTen lives at the end of the method does not
mean it will
definition get iscalled
alone then. Only
not sufficient an actual method call will cause the method to run. A
for that.
We can, of course, call our new method more than once. Reusing code is a key reason for
methods in the first place:
CountToTen();
CountToTen();

void CountToTen()
{
for (int current = 1; current <= 10; current++)
Console.WriteLine(current);
}

100 LEVEL 13 METHODS


Hopefully, you can begin to see how methods are helpful. We can bundle a pile of statements
into a method and name it. Once defined, we can trust the method to do its intended job (as
long as there are no bugs). It becomes a new high-level command we can run whenever we
need it. We can reuse it without copying and pasting it.

Methods Get Their Own Variables


Methods get their own set of variables to work with. This gives them their own sandbox to play
in without interfering with the data of another method. Multiple methods can each use the
same variable name, and they won’t interfere with each other:
int current = Convert.ToInt32(Console.ReadLine());
CountToTen();
CountToTwenty();

void CountToTen()
{
for (int current = 1; current <= 10; current++)
Console.WriteLine(current);
}

void CountToTwenty()
{
for (int current = 1; current <= 20; current++)
Console.WriteLine(current);
}

CountToTen , CountToTwenty
the three variables , and
are distinct. Each the
has itsmain method location
own memory each have current
forathe variablevariable,
and will but
not
affect the others. This separation allows you to work on one method at a time without worrying
about what’s
what’s happening in other methods. You don’t need to keep the workings of the entire
program in your head all at once.
The following code map shows this organization:

Think back to Level 9, when we first introduced blocks and scope. Code in one of these
elements can access everything in that code element, but also things in containing elements.
That means local functions have access to the variables in the Main method:
string text = Console.ReadLine();

void DisplayText()

PASSING DAT
DATA
A TO A METHOD 101
{
Console.WriteLine(text); // DANGER!
}
Because the scope for the text variable is the main method, and because that encompasses
the DisplayText method, DisplayText can reach up to the main method and use its
text variable.
There is a place for this, but it is rare. We’ll talk through when this is useful in Level 34.
34 . The
main problem with reaching up to these other variables is that it curtails your ability to work
on each method independently since they’re sharing variables. There are other tools that let
us share data between methods without this problem. We’ll discuss two of them (parameters
and return values) later in this level.

If
canyou’re
put the static
worried thatkeyword
you might accidentally
at the usemethod
front of your a variable from the containing method, you
definition:
static void CountToTen() { ... }

With static on your method, if you use a variable in the containing method, the compiler
will give you an error.
error. I won’t typically do that in this book, but if you keep accidentally using
variables from outside might consider using static as a safety precaution.
outside the method, you might

PA󰁓󰁓ING DATA TO A METHOD


If a method needs data to do its job, it can define special variables called parameters to hold
this data. Unlike the variables we have seen so far, the calling method initializes these
variablesbwhen
we have been the method
the
een using is ecalled.
so far—are
far—ar called(By thevariables
local way, variables
variables
.) that are not parameters—the
parameters—the kind

Parameters give methods flexibility. Earlier, we defined a CountToTen and a


CountToTwenty method. With parameters, we can replace both with a single method.
Parameters are defined inside of a method’s
method’s parentheses:
void Count(int numberToCountTo)
{
for (int current = 1; current <= numberToCountTo; current++)
Console.WriteLine(current);
}

102 LEVEL 13 METHODS


We can use this numberToCountTo parameter within the method like any other variable.
Parameters don’t need to be initialized inside the method; the calling method initializes them
as the method call begins by placing those values (or expressions to evaluate) in parentheses:
Count(10);
Count(20);

The value that the calling method provides in a method call is an argument. So on that first
line, 10 is an argument for the numberToCountTo parameter. On the second line, 20 is the
argument. Programmers will also call this passing data to the method. They might say, “On
the first line, we pass in a 10,
1 0, and on the second
sec ond line, we pass in 20.”
20.”

With thislets
method code, our program
us count willany
to virtually count to 10number
positive nuand . count to 20 afterward. This Count
then
mber.
We have seen this before. Console’s WriteLine method has a value parameter.
this mechanic before.
When we call Console.WriteLine("Hello, World "), we are just passing "Hello,
World " as an argument.
Our Count method illustrates the key benefits of methods:
1. We can compartmentalize. When we write our Count method, we can forget the rest of
the code and focus on the narrow task of counting. Once Count has been created and
works, we no longer need to think about how it does its job. We’ve
We’ve brought a new high-
hig h-
level command into existence.
2. We add organization to the code. Giving a chunk of code a name and separating it from
code that uses it makes it easier to understand and manage.
3. We can reuse it. We can call the method without copying and pasting large
l arge chunks of
code.

Multiple Parameters
A method can have as many parameters as necessary.
nece ssary. If you need two pieces of information
to complete a job, you can have two parameters. If you need twenty pieces of information, you
can have twenty parameters. If you need 200 parameters… well, you probably need somebody
to wake you up from the nightmare you are in, but you can do it. More than a handful usually
means you need to break your problem down differently; it gets tough to remember what you
were doing with that many parameters.
parameters.
Multiple parameters are defined by listing them in the parentheses, separated by commas:
void CountBetween(int start, int end)
{
for (int current = start; current <= end; current++)
Console.WriteLine(current);
}

Calling a method that needs multiple parameters is done by putting the values in the
corresponding spots in the parentheses, separated by commas:
CountBetween(20, 30);

Copied Values in Method Calls


In previous levels, we saw that assigning the value in one variable to another will copy the
contents of that variable. To
To illustrate:

RETURN
RETURNING
ING A VALUE FROM A METHOD 103
int a = 3;
int b = a;
b += 2;

a is initialized to 3, and then on the second line, the contents of a are retrieved to evaluate
what should be assigned to b. That result (also 3) is what is placed into b. Both a and b have
their own 3 value, independent of each other. When b has 2 added to its value on the final
line, b changes to a 5 while a stays a 3.
This same behavior holds for a method call:
int number = 10;
Count(number);

When Count is invoked, the value currently in number is evaluated and copied into Count’s
numberToCountTo parameter.

RETURNING A VALUE FROM A METHOD


While parameters let us send data over to the called method, return values allow the method
to send data back to the calling method. A return value allows a method to produce a result
when it completes. We have seen return values
v alues in the past. For example, we are using the
return values of the two methods below:
string input = Console.ReadLine();
int number = Convert.ToInt32(input);

To make a method return a value, we must do two things. First, we indicate the data type that
will be returned, and
and second, we must state
state what value is returned:
returned:
int ReadNumber()
{
string input = Console.ReadLine();
int number = Convert.ToInt32(input);
return number;
}

Instead of a void return type, this method indicates that it returns an int upon completion.
We can value when calling ReadNumber, as we have done in the past:
can then use the returned value
Console.Write("How high should I count?");
int chosenNumber = ReadNumber();
Count(chosenNumber);

void Count(int numberToCountTo)


{
for (int current = 1; current <= numberToCountTo; current++)
Console.WriteLine(current);
}

int ReadNumber()
{
string input = Console.ReadLine();
int number = Convert.ToInt32(input);
return number;
}

104 LEVEL 13 METHODS


Returning Early
A return statement
statement on the final line of a method is
is common, but a return statement
statement can occur
anywhere in a method. Returning before the last line of the method is called returning early
or an early exit. Returning early is especially common if you have loops and ifs in your code.
The method below will repeatedly ask for a name until the user enters some actual text instead
of just hitting Enter
Enter::
string GetUserName()
{
while (true)
{
Console.Write("What is your name? ");
string name = Console.ReadLine();
if (name != "") // Empty string
return name;
Console.WriteLine("Let's try that again.");
}
}

Whenever a return statement is reached, the flow of execution will leave the method
immediately, regardless
regardless of whether it is the last line in the method or not.
While return statements can go anywhere in a method, all pathways must specify the
returned value. By listing a non- void return type, you promise to produce a result. You have
have
to deliver on that promise no matter what if statements and loops you encounter.
encounter.
A method whose return ttypeype is void indicates that it does not produce or return a value. They
can just run until the end of the method with no return statements. However, void methods
can still return early with a simple return; statement:
void Count(int numberToCountTo)
{
if (numberToCountTo < 1)
return;

for (int index = 1; index <= numberToCountTo; index++)


Console.WriteLine(index);
}

Multiple Return Values?


In C#,
But aslimitation
this in many programming
sounds worselanguages, you cannot
than it is. There return
are several more
ways wethan
can one
workthing
a at a it.
around
round time.
We
will soon see ways to pack multiple pieces of data into a single container, starting
starting with tuples
in Level 17 and classes in Level 18. Later,
Later, we will see
se e how to make an output parameter (Level
34),, which also supplies data to the calling method. While we can only technically return a
34)
single item, the tools available mean this isn’t very limiting in practice.

METHOD OVERLOADING
Each method you create should get a unique name that describes what it does. However,
sometimes you have two methods
me thods that do essentially the same job, just with slightly different
parameters. Two methods can share a name as long as their parameter lists are different.

Sharing methods
various a name is
bycalled
by method
the same nameoverloading
overloads. , or simply overloading, and people call the

SIMPLE METHODS WITH EXPRESSIONS 105


A good example is Console’s WriteLine method, which has many overloads. That is what
allows the following to work:
Console.WriteLine("Welcome to my evil lair!");
Console.WriteLine(42);

There is a version of WriteLine with a string parameter and one with an int parameter.
When the compiler encounters a method call to an overloaded method, it must figure out
which overload to use. This process is called overload resolution. It is a complex topic, full of
nuance for tricky situations, but the simple version is that it can usually tell which one you
want from arguments. When we write Console.WriteLine(42) ,
from the types and number of arguments.

the compiler picks the version of WriteLine with a single int parameter.
Console.WriteLine has a total of 18 different overloads. Most have a single parameter,
each with a different type (string, int, float, bool, etc.), but there is also an overload with
no parameters (Console.WriteLine()) that just moves to the following line.
The set of all overloads of a method name is called a method group. In this book, I will
sometimes refer to a method name without the parentheses. This refers to either a non-
overloaded method (no other method shares its name) or the entire method group. In rare
cases where a specific overload matters, I will call it out by either listing the parameters’ types
or types and names in parentheses: Console.WriteLine(string) or Console.
WriteLine(string value).
Unfortunately, local functions like the ones we’re creating now don’t allow overloads. When
we start building classes
classes in Level 18, you will be able to overload methods there.

󰁓IMPLE METHOD󰁓 WITH EXPRE󰁓󰁓ION󰁓


Some methods are simple, and the infrastructure used to define a method drowns out what
the method does. For example, consider this DoubleAndAddOne method:
int DoubleAndAddOne(int value)
{
return value * 2 + 1;
}

If you can represent a method with a single expression, ther


theree is another way to write it :
int DoubleAndAddOne(int value) => value * 2 + 1;

Instead of curly braces and a return statement, this format uses the arrow operator (=>) and
the expression to evaluate, followed by a semicolon. The two above versions of
DoubleAndAddOne are equivalent. The first version is said to have a block body or statement
body, while the second is said to have an expression body. The => is used to indicate that an
expression is coming next. We saw
saw it with switch expressions, and we will see it again.
Y
You
ou can only use an expression
expression body if the whole method can be represented in a single
expression. If you need a statement or many statements, you must use a block body. The
following example may be short, but it cannot be written
wr itten with an expression body:
void PrintTwice(string message)
{
Console.WriteLine(message);

106 LEVEL 13 METHODS


Console.WriteLine(message);
}

Many C# programmers prefer expression bodies when possible because they are shorter and
easier to understand, at least once you get comfortable with the expression syntax.
XML DOCUMENTATION COMMENT󰁓
In Level 4, we covered adding comments with // and /* ... */. Let’s look at the third
approach: XML Documentation Comments.

Methods are an
many people, excellent
even tool forConsole
worldwide. building and
reusable code. are
Convert Some code is meant
examples of that.toPeople
be used by
have
written tools to dig through C#
C# source code to automatically
automatically harvest comments connected to
methods and other elements
el ements to generate documentation about their use. To allow these tools
to be automatic, comments must be written in a specific
spec ific format so that the tools can find
fin d and
interpret them. This is the problem XML Documentation Comments solve.
The simplest way to start using XML Documentation Comments is to go to the line
immediately before a method and type three forward slashes: ///. When you type ///, Visual
Studio expands that into several comment lines that serve as a template for a documentation
comment, allowing you to fill in the details. For example, I have added a simple XML
documentation comment to the Count method:
/// <summary>
/// Counts
/// to the given number, starting at 1 and including the number provided.
</summary>
void Count(int numberToCountTo)
{
for (int index = 1; index <= numberToCountTo; index++)
Console.WriteLine(index);
}

These documentation comments build on XML, XML , which is why you see things written the way
they are. If you are not familiar with XML, it is worth looking into someday. Filling this out
allows tools to use these comments
c omments in the documentation, including Visual Studio.

Challenge
Challe nge Taking a Number 100 XP
Many previous tasks have required getting a number from a user. To save time writing this code
repeatedly, you have decided to make a method to do this common task.
Objectives:
• Make a method with the signature int AskForNumber(string text). Display the text
parameter in the console window, get a response from the user, convert it to an int, and return it.
This might look like this: int result = AskForNumber("What is the airspeed velocity
of an unladen swallow?");.
• Make a method with the signature int AskForNumberIn
AskForNumberInRange(string
Range(string text, int min, int
max). Only return if the entered number is between the min and max values. Otherwise, ask again.
• Place these methods in at least one of your previous programs to improve it.

THE BASICS OF RECURSION 107

THE BA󰁓IC󰁓 OF RECUR󰁓ION


Methods can call other methods as needed. Sometimes, it even makes sense for a method to
call itself. When a method calls itself, it is called recursion. A simple but broken example is
this:
void MethodThatUsesRecursion()
{
MethodThatUsesRecursion();
}

The above code shows why recursion is dangerous and requires extra caution. This code will

never finish!
do when When MethodThatUsesRecursion
a method isso
is called. We record where we are called, we return
we can do the to
same
the things
correctwe always
location,
make room for any variables that the method has (none,
( none, in this case), and then shift execution
over to the new method. However, that begins a second call to MethodThatUses
Recursion, which begins a third, a fourth, and so on. The computer will eventually run out
of memory to store each method call’s information. This code ultimately crashes instead of
running forever.
But recursion can work if we can guarantee that we eventually stop going deeper and start to
come back out. We need some situation where we do not keep diving deeper—the base case—
and each time we call the method recursively, we must always get closer to that
t hat base case.
An example is the factorial math operator, represented with an exclamation point. The
factorial of a number is the multiplication of all integers smaller than it. 3! is 3×2×1. 5! is
5×4×3×2×1 . We could
make a Factorial also think of 5! as 5×4! since 4! is 4×3×2×1. We could use recursion to
method:
int Factorial(int number)
{
if (number == 1) return 1;
return number * Factorial(number - 1);
}

The first line is our base case. When we reach 1, we are done. For larger numbers, we use
recursion to compute the factorial of the number smaller than it and then multiply it by the
current number.
number. Because we’re always subtracting one, we will get one step closer to the base
case each time we call Factorial recursively. Eventually, we will
will hit the base case and begin
returning. (This code assumes you don’t pass in 0 or a negative number.)

Recursion is tricky and easy to get wrong. It requires thinking about a problem at different
levels at the same time. Don’t worry if all you take away from this section is that methods can
call themselves but require caution. It takes time to master recursion, but it is worth knowing
kn owing
it exists.

Challenge Countdown 100 XP


Note: This challenge requires reading The Basics of Recursion side quest to attempt.
The Council of Freach has summoned you. New writing has appeared on the Tomb of Algol the Wise, the
last True Programmer to wander this land. The writing strikes fear and awe into the hearts of the loop-
loving people of Freach: “The next True Programmer shall be able to write any looping code with a
method call instead.” The Senior Counselor, scared of a world without loops, asks you to put your skill to
the test and rewrite the following code, which counts down from 10 to 1, with no loops:

108 LEVEL 13 METHODS


for (int x = 10; x > 0; x--)
Console.WriteLine(x);

As you consider the words on the Tomb of Algol the Wise, you begin to think it might be correct and that
you might be able to write this code using recursion instead of a loop.
Objectives:
• Write code that counts down from 10 to 1 using a recursive method.
• Hint: Remember that you must have a base case that ends the recursion and that every time you
call the method recursively, you must be getting closer and closer to that base case.

LEVEL 14
MEMORY MANAGEMENT
󰁓peedrun
• When you get done using memory, it needs to be cleaned up.
• The stack: When a method is called, enough space
space is reserved for its local variables and paramete
parameters
rs
(its stack frame). When you return from a method, space is reclaimed and reused. The stack’s
memory management strategy is most straightforward when data is always a known size.

The heap:
objects Whenondata
placed the isheap.
needed, a free spot in memory is found. A reference is used to keep track of
• The garbage collector has the task of inspecting things on the heap to see if they are still in use. If
not, it lets the heap memory be reused.
• Some types are value
valu e types: they store
sto re their data in the variable’s location in memory. All the numeric
types (int, double, long, etc.), bool, and char are value types.
• reference types: string and arrays.
Some types are reference
• Value semantics means two things are equal if their data elements are equal. Reference semantics
means two things are equal if they’re the same location in memory.

Now that we have learned about methods, and before we move on to object-oriented
programming, it is time to look at how C# stores data for your variables in depth. This topic
touches on some complex
c omplex stuff, and I’ve taken a few shortcuts to keep things simple, but what
is shown here is generally correct, mainly ignoring things like optimizations.
The concepts covered here are critical for all C# programmers to understand. Without this,
you may find yourself creating
creating subtle bugs with no knowledge of important conceptual ideas
that would allow you even to attempt fixing them, unable to fix a problem right in front of you.
Because of this topic’s complexity and importance, I recommend reading this level more than
once. Perhaps back-to-back,
back-to-back, or maybe now and then after Part 2.

110 LEVEL 14 MEMORY MANAGEMENT

MEMORY AND MEMORY MANAGEMENT


Y
Your
our program
program stores
stores data in the computer’s memory. You You can think of memory as a large bank
of storage locations, like mailboxes at a post office or the vast parking llots
ots at Disneyland. Each
storage location has its own memory address you could use to reference it. While
programmers think in terms of variables identified by name, computers have no
understanding of those names.
Modern computers have vast quantities of memory available, but it is not unlimited.
un limited. If we are
reckless with our memory usage, it does not take long to use it all up. I just saw a single
incorrect line in a multi-million-line program lead to the program consuming 31 gigabytes of
memory over a few days. And we were being careful!
Memory is a limited resource that must be carefully managed. Its limited nature requires that
we always follow this rule:
Using memory is fine, but you need to clean up after yourself when you finish using it.
At this point,
point, you might be thinking,
thinking, “I haven’t done anything to clean up my memory yet. Was
I doing something wrong?” But the answer is no. The environment that C# programs run in
solves most of this problem for you so that you don’t have to worry about it. But in doing so,
important decisions have been made that impact how you work and how the language itself
must work, so it is important to understand how it works.
On modern computers, the operating system is the great gatekeeper of memory. The
operating system gives your programs memory to use when they start up, and your program
can negotiate with the operating system for more. The operating system doesn’t care how you
use your memory, as long as you don’t attempt to use another program’s memory. Each
program and programming language can do as it sees fit. Theoretically, every programming
language could handle memory differently. But two models are almost universal: the stack
and the heap. C#, like many other languages, uses both.

THE 󰁓TACK
The first memory management strategy is a stack (often called the stack, even though there
could be more than one). There are a handful of principles that lead us to this strategy.
When our program starts, what it might do as it runs. if statements, while
starts, we cannot predict what
loops, and user input makes it unpredictable. We cannot know with certainty what memory

should be used for which data for the lifetime


l ifetime of our program.
But we know that when we enter a method, we’re probably going to use all of its variables
(both local variables and parameters). Allocating or reserving a spot in memory for those
variables when a method starts makes sense. Similarly, we know we are done using that
method’ss variables when we finish a method. At that point, we can clean
method’ cle an up (deallocate) those
variables’ memory. Even if we call the method a second time, it is independent
i ndependent of the first,
with different parameter
parameter values.
But when returning from a method, the method we are going back to is still alive. Data from
any method in the chain back to the program’s entry point is still alive because we will
eventually return there.
The stack is a model that allows us to allocate space for each method we call and then free or
deallocate the memory when we are done using it as the flow of execution moves from one
method to another. The stack is like a stack of boxes, one on top of the next. Each box contains

THE STACK 111


the data for a single method. When a method is called, we grab a new box, put it on top of the
other boxes already in our stack, and fill it with the new method’s data. When we do this, the
boxes underneath the top box become inaccessible temporarily. When we return from a
method, we are done with the entire box and cast it aside. The space is available for our next
method call, and the contents of the previous box—the variables from the method we are
returning to—are readily accessible once again.
Consider this simple program:
int x = 3;
double y = 6.022;
Method1();

void Method1()
{
int a = 3;
int b = 6;
}

As our main method (referred to as Main below) starts, the stack looks something like this:

Four bytes are reserved for x because it is an int. Eight bytes are reserved for y because it is a
double. So 12 bytes total are set aside for Main in a bundle. The image above shows them
grouped and labeled on the side with Main. A collection of data needed for a single method
is called a stack frame. The memory itself doesn’t
d oesn’t know that the bytes are used in the way it is,
but our program understands which bytes are for x and which bytes are for y.
In the image above, the dashed line marks an important location in the stack. Everything
beneath it is currently allocated for specific variables in specific frames on the stack.
Everything above it is not being used and contains garbage. There is memory there, and that
memory’s bits and bytes are set to something, but it doesn’t mean anything to the program.
Even when some of that memory is claimed for a new stack frame, it isn’t initialized to
anything meaningful yet. The memory contains whatever it was last set to. That is why you
cannot use local variables until you initialize them. Their memory contains old bits and bytes
that your program cannot interpret. The contents of x and y are displayed as ? for that reason.
Once the frame for Main is on the stack, we’re ready to begin running statements contained
in it. The first two statements initialize the variables.
v ariables. When we finish running those, the stack
looks like this:

112 LEVEL 14 MEMORY MANAGEMENT


At this point, we’re ready to run the third line, which invokes Method1. As this method is
called, several things happen. We reserve more space for the variables in the method we are
calling. We also note where we are at, so we can return to that spot. Both pieces of information
go into the new frame we are adding to the stack. The dashed line advances upward, as we
now have two frames on the stack, each using its own segment of memory.

Main’s frame is buried beneath the one for Method1. The memory is still reserved for Main’s
variables, but it is generally inaccessible. That’ thing. Method1 can work with its own
That’ss a good thing.
variables as needed without interfering with Main’s variables.
The specifics of how exactly that “Main line 3” part is done is a bit too low level for this book,
but the concept is correct;
cor rect; we simply record the right information on the stack and use it when
we get done in Method1.
Local variables don’t automatically start with valid data, so a and b in the above diagram show
a ? for their value. At this point, we are ready to run the code in Method1, which assigns values
to a and b in short order.

THE STACK 113


After this, we are done with Method1 and ready to return. We can use the “Main line 3”
information to know where to resume execution in our main method. The data that was
reserved for Method1’s can be cleaned up. This cleanup is simple: we shift the dashed line
back down, marking everything once used by Method1 as no longer used, but we can reuse it
in future method calls.

The main method is ready to resume execution. Its frame is back on the top of the stack.
This example illustrates how the stack follows our fundamental rule of memory
management—that we must clean up any memory we use when we finish using it. As a
method begins, the dashed line is moved up enough
enoug h to make room for that method’s variables.
The program runs, filling that memory with data to do its job and eventually completes it.
Upon completing the method, the dashed line that separates in-use memory from unused
memory can move back down, freeing it up for the following method call.
The computer doesn’t need anything fancy to clean up old stack frames for finished methods.
The dashed line is shifted downward, and that memory finds itself on the side of the line for
unused memory, ready to be reused.
The code above only shows the main method calling a single other method, but if Method1
called another method, another frame would be placed on top of the stack for it. Frames are
added and removed from the top of the stack as needed,
nee ded, as methods are called and compl
completed.
eted.

Parameters
How do parameters work with the stack? Let’s give Method1 a parameter:

114 LEVEL 14 MEMORY MANAGEMENT


int x = 3;
double y = 6.022;
Method1(x);

void Method1(int n)
{
int a = 3;
int b = 6;
}

Let’s fast forward to the point where Method1 is called. Like before, a frame for Method1 is
placed on the stack. A place is also created for the parameter n.

Before control transfers to Method1, the arguments Main supplies for Method1’s parameters
loc ations. Method1 was called like Method1(x), so
are copied into their respective memory locations.
the value currently in the variable x is copied into the parameter n. It is its own variable and
has its own copy of the data. x and n are separate
se parate from each other.

If Method1 had multiple parameters, we would do the same thing for them. Parameters and
local variables are mostly the same things, just that parameters are initialized as the method
is being called by values provided in the calling method. In contrast, local variables are
initialized only once inside of the method.

FIXED-SIZE STACK FRAMES 115


From this point on, execution behaves precisely like before. The statements that initialize a
and b run, filling them with valid data. Eventually, Method1 completes and returns, and the
frame for Method1, including its local variables and parameter, is removed from the stack.
Execution resumes back where it came from in Main.

Return Values
Y
You
ou could imagine
imagine doing a similar thing with return values, making
making a spot for the return
return value
on the stack, having the called method populate it before returning, and then allowing the
calling method access to it temporarily as the method returns.

On paper,
tricks thatthat approach
typically would
allow thework, butvalue
return realityto
is messier.
sidestepThere
Ther
thee are many
stack optimizations
entirely. and
(And these
optimizations are part of why methods can have many parameters but only a single return
value.)

FIXED-󰁓IZE 󰁓TACK FRAME󰁓


The stack-based
stack-based approach follows the rule that you must free up memory
memor y for reuse when you
are done with it. All stack memory is either reserved
reser ved for a method we will eventually return to,
or it is available for use. As methods are called, the line advances and space is reserved for it.
As methods return, the the line retreats,
retreats, leaving the memory available
available for reuse.
This approach makes it fast
fast to determine if a specific spot in memory is in use. We can simply
check if it is above or below the magic line. Cleaning up a frame for a method with 100
parameters is just as fast as cleaning up a frame with none.
There’s a catch, though. This approach works best if stack frames are predictable in size. It is
okay for different methods to create frames of different sizes, but it is more efficient if each
method will always have a known, fixed size. That means we need to know exactly how many
bytes to reserve for a method ahead of time. Most of the types we have discussed won’t be a
problem. For example, an int variable will always take up exactly 4 bytes.
But there are two types that we have encountered that will be problematic—arrays and
strings—with more to come. These types have unpredictable sizes. They depend on how long
the array or string is. For these, we lose our ability to use the stack efficiently.
We could develop a scheme to allow stacks to deal with ever-changing sizes, but instead of
doing that, C# uses a different approach for things of this nature and let the stack use
predictable sizes for stack frames. This different approach is called a heap and is the subject
of the next section.

THE HEAP
When we need memory that can be created in arbitrary sizes, we ditch the stack and find
another spot. This other spot is called the heap. The heap is not as structured as the stack. It is
a random assortment of various allocated data with no rigid organizational patterns, hence
the name. In truth, there is a lot of organization to the heap; it is not just randomness. But in
comparison to the stack, it is less organized. Consider this simple example:
int x = 3;
string a = "Hello";

116 LEVEL 14 MEMORY MANAGEMENT


Knowing how the stack works, we might have assumed memory looks something like this:
But that is not correct! Variables
Variables on the stack need to be of a known, fixed size, so we can’t just
put their contents on the stack like this.
Because a string’s size is dynamic—some require more bytes than others—we must find a
spot for this data on the heap instead. We search for an open location with enough space for
five characters and allocate it for the string. That can be anywhere within the heap, which
means its location isn’t predictable or computable. T To
o keep track of items placed on the heap,
we capture a reference to the new object
objec t when we create it. The reference allows us to look up
the object’s memory location when needed. The variable stores the reference instead of the
data itself. You can think of a reference as a phone number, email address, or a Bat-Signal for
the data—a way
programming to track down
languages, the data when
this reference it is needed,
is nothing without
more than it being
a memory the data.
address. In some
References
in C# are more sophisticated than that, but thinking of it as a memory address is also a
meaningful way to think about it.
Therefore, the variable a holds only a reference, while the full data lives somewhere on the
heap. Memory looks more like this:

While the variable x contains its own data, a contains only the reference. All references are
the same size, so we can count on frames for a method being known sizes. (References are 8
bytes (64 bits) on a 64-bit computer and 4 bytes (32 bits) on a 32-bit computer.
computer.))
Arrays have
have the same problem
problem with the same solution:

int x a
int[] = =
3;new int[3] { 1, 2, 4 };

THE HEAP 117


An array variable holds a reference to the array while the contents live elsewhere on the heap:
The Heap as a Graph of Objects
Real programs get more complicated than those two samples. You can think of the heap as a
set of objects interconnected by references like a web—what mathematicians would call a
directed graph. For example, consider this code:
string[] words = new string[3] { "Hi", "Hola", "Salut" };

This code has an array of strings. Each string is created somewhere in the heap. The array
itself is full of references to those strings, while words contains a reference to the array:

When we had an array of ints, the data elements themselves have a fixed size (4 bytes), and
the array contains the data directly. Here, with an array of strings, the data elements do not
have a fixed size, so our array holds a collection of references, and the data for those items
lives in another place on the heap.

118 LEVEL 14 MEMORY MANAGEMENT

Value Types and Reference Types


Programmers often do not have to think too hard about what happens in memory when they
run code that looks like string[] words = new string[] { "Hello", "Hola",
"Salut" }. Yet this clearly illustrates that we have two very different categories of types in
C#. This realization is one of the most important things to discover as you begin making C#
programs. If you treat all variables the same, you will make subtle mistakes without
understanding what is wrong and how to fix it.
The first category is value types. Variables
Variables whose types are value types contain their data right
there, in place. They have a known, fixed size.

The second
reference to category
the data, is
andreference types
the data . Variables
is placed
place whose types
d somewhere on theare reference
heap. types of
Two pieces hold
theonly
samea
type of data are not guaranteed to have the same size in memory, though the references
themselves are all the same size.
This single difference has far-reaching consequences, so it is essential to know what category
any given type is in. This is also a way that C# differs from similar languages. C++ and Java, the
two most similar programming languages to C#, handle memory quite differently.
Most types we have discussed are value types. All the integer types ( byte, sbyte, short,
ushort, int, uint, long, ulong), all the floating-point types (float, double, decimal),
char, and bool are value types.
The string type is a reference type, as are arrays. Regardless of whether the array holds a
value type or a reference type, that is true. If an array is an array of a value type, wherever
wherever the
array lives in the heap, its data will live there inside it, as we showed earlier with the int[]
example. If an array contains reference types, the array will contain references to other places
place s
in memory where the full data lives, as we showed earlier with the string[] example.
There is more to this difference than just how data is stored in memory. Let’s see another
example to illustrate these differences:
int i1, i2; // Two of everything.
string s1, s2;
int[] a1, a2;

i1 = 2; // Assign a value to the first.


s1 = "
"Hello";
Hello";
a1 = new int[] { 1, 2, 4 };

i2 = i1; // Copy to the second.


s2 = s1;
a2 = a1;

i2 = 4; // Make changes.
a2[0] = -1;

This code creates two ints, two strings, and two arrays, initializes new values for one set
(the ones ending with a 1), and then copies the first set’s contents to the second’s. Afterward,
it makes two additional changes to the variables. After running through the first set of
assignments (just after a1 = new int[] { 1, 2, 4 };) our memory looks like this:

THE HEAP 119


i1 contains a 2, while the string and int[] are allocated on the heap, with s1 and a1
containing references to those things. i2, s2, and a2 have not been assigned a value yet, but
that is the next set of lines:
i2 = i1;
s2 = s1;
a2 = a1;

This next part is tricky. For all three, assigning one value to another works similarly: the
variable's contents are copied from the source to the target variable. For the integers i1 and
i2, i2 ends up containing its own copy of the number 2. But for s1/s2 and a1/a2, this copies
the reference from source to target. That is worth restating:
restating: the references are copied, not the
entire chunk of data! s1and s2 each have their own string references, but they are both
references to the same thing. The same is true of a1 and a2.

There is still only one occurrence of the string "Hello" on the heap, and only one int array
on the heap, even though we have two variables with a reference to each.
e ach.

The final two lines illustrate this important difference more starkly:

120 LEVEL 14 MEMORY MANAGEMENT


i2 = 4;
a2[0] = -1;

The variables i1 and i2 are value types (int), and so when we assign a new value to i2, it is
updated while i1 still contains its original value of 2.
But when we change a2, we work with what a2 references, which is the array on the heap. The
value at index 0 changes
changes as we would expect:

Even though we changed the array through a2’s reference,


reference, we can see the change through a1
as well. a1 has its own storage location, but it stores a reference that leads you to the same
object on the heap. If we displayed a1[0] afterward, we would see that it is -1. The two
variables share
share a reference to the same underlying dadata,
ta, so the change will be visible tthrough
hrough
either copy of the reference.
In contrast, if we assigned a completely new array with the same data to a2, it would have a
reference to this second array on the heap, and the two would not be interconnected:
a2 = new int[3] { 1, 2, 4 };
a2[0] = -1;

Which would look like this in memory:

THE HEAP 121


Value 󰁓emantics and Reference 󰁓emantics
There is another important consequence of the value vs. reference type discussion, which
relates to equality. By default, two things are considered equal if the bits and bytes stored in
the two variables are the same. For a value type like int, two things can be equal if they
represent the same value:
int a = 88;
int b = 88;

bool areEqual = (a == b); // Will be true

While a and b are independent memory


memor y locations, the bits and bytes used to represent a value
of 88 are identical. When two things are equal because their values are
a re equal, they have value
semantics. Value types have value semantics.
In contrast, consider these two int arrays:
int[] a = new int[] { 1, 2, 3 };
int[] b = new int[] { 1, 2, 3 };

bool areEqual = (a == b); // Will be false!


Even though a and b both contain references to int arrays containing 1, 2, and 3, a and b are
not equal. They hold references to two different arrays on the heap. Since the two variables
contain different references, they will not be equal, even though the data at the other end of
the reference are an exact match. They are only equal if they reference the same object on the
heap. When two things are equal only if they are the same reference, they have reference
semantics.
By default, reference types have reference semantics. But notably, C# allows a type to redefine
what equality means for itself. Some reference types redefine equality to be more like value
semantics. The string type does this. For example:
string a = "Hello";
string b = "Hel" + "lo";

122 LEVEL 14 MEMORY MANAGEMENT

bool areEqual = (a == b); // true even though a and b are different references.

The string type has redefined equality to mean two strings contain the same characters,
effectively giving it value semantics, even though it is a reference type.
CLEANING UP HEAP MEMORY
Now that we understand how the heap works, let’s look at how the heap cleans up dead
memory. Because the heap allocates memory in a less structured way, cleaning things up is
trickier.
The actual mechanics of cleaning up memory on the heap is not too complicated. When you
know that it is time for some heap object or entity to be cleaned up, you notify the heap that
your program will no longer need it and that the heap can reuse the space.
Some programming languages such as C++ work in precisely this way. The programmer
recognizes that it is time to clean something up and inserts a statement in their code to cause
the memory to be freed up for future use.
But the hard part is not in releasing the memory but in knowing when to release the memory.
Getting it wrong has dire consequences.
c onsequences.
If our program uses memory and fails to clean it up, it cannot be reused by something else.
The memory is unused as it stands but cannot be put back into useful service either. This is
called a memory leak. If a program does not use excessive memory or only runs for a few
seconds, this isn’t the end of the world. The program will use more and more memory as it
runs, but it will finish before it becomes a problem. On the other hand, memory-intensive or
long-running programs will eventually consume all memory on the computer, slowing
everything down for a while before bringing things crashing down around it.
Additionally, if we return
return memory to the heap too early, some part of our program
program is still using
it for what it once was. For a time, the rest of the system may just see it as unused memory,
and the consequences aren’t high. But eventually, the heap will reuse that section of memory
for a second item, and two parts of our program will be using the same memory for two
different things. This is called a dangling reference or a dangling pointer. Part of your program
unknowingly uses memory that was already given back to the heap.
With the
the heap,
heap, it is imperative
imperative to get it right. You
You must
must free up or return
return all memory to the heap
for reuse once the program does not need it, but never do it too early. The challenge is
especially tough when many different parts of your program reference the same thing on the
heap. If four things all have a reference to the same thing, whose job is it to know who is still
using the reference and when to clean it up? Various strategies are employed to make this
problem manageable, but we’rewe’re in luck:
luck : in C#, the system manages it all for you safely.

Automatic Memory Management


C# takes the burden of tracking and cleaning up heap objects off of programmers. This task
falls on the runtime that your C# programs run within. This approach is called automatic
memory management or garbage collection. T To
o illustrate, consider this piece of code:
int[] numbers = new int[10];
numbers = new int[5];

CLEANIN
CLEANINGG UP HEAP MEMORY 123
Two arrays are created on the heap as this code runs, one on each line. After the first line runs,
the first array (of length 10) exists and is still usable by the program through the numbers
variable. After the second line runs, the second array (of llength
ength 5) exists and is usable by the
program, but nothing has access to the original 10-element array. It is ready to be ccleaned
leaned up.
Within the .NET runtime, an element called the garbage collector (sometimes abbreviated to
the GC) periodically wakes up and scans the system for anything the program can no longer
reach. The search starts from a set of root objects that includes any variable on the stack. For
any item still on the heap that is no longer reachable, it recognizes it as garbage and returns
that space in memory to the heap for reuse.
The garbage collector has a lot
l ot of complexity, nuance, and optimization, and we have skipped
over many fascinating details with that description, but that is the core of it.
The garbage collector will never return memory to the heap too early. If the program can still
reach it, something may still use it, and it does not get cleaned up. So dangling references
cannot happen.
While memory does not get cleaned up immediately (the garbage collector is not perpetually
running), we know that memory that is no longer reachable will get cleaned up eventually,
before enough piles up to be problematic.
problematic. So memory
memor y leaks are not a problem either
e ither,, so long
as we don’t accidentally keep a reference to something that we don’t care about anymore.
The advantages of a garbage collector are huge, but it is not without downsides. We do not
precisely control when memory is returned to the heap to reuse. It happens when the garbage
collector next runs, whenever that is (but typically several times a second). The second
notable
and seedownside
what isisin
thatuse.
the garbage
Under collector must inevitablythis
most circumstances, suspend your programor
is microseconds to check
even
nanoseconds—nothing to worry about. But when your program is churning through memory,
the delays can cause issues. The garbage collector is heavily optimized and minimizes these
concerns, but that doesn’t mean it never happens.
In general, you still want to take reasonable steps to
to allocate only the space you need and not
abuse it. But for the most part, you can relax and let the garbage collector do its job.
The garbage collector works well for the heap but does nothing for the stack. But the stack was
managing its memory just fine on its own.

Knowledge Check Memory 25 XP


Check your knowledge with the following questions:
1. True/False. You can access
access anything on the stack at any time.
2. True/False. The stack keeps track of local variables.
3. True/False. The contents of a value type can be placed on the heap.
4. True/False. The contents of a value type are always placed on the heap.
5. True/False. The contents of reference types are always placed on the heap.
6. True/False. The garbage collector cleans up old, unused space on the heap and stack.
7. True/False. If a and b are array variables referencing the same object, modifying a affects b as well.
8. True/False. If a and b are ints with the same value, changing a will also affect b.
Answers: (1) False. (2) True. (3) True. (4) False. (5) True. (6) False. (7) True. (8) False.

124 LEVEL 14 MEMORY MANAGEMENT

Boss Battle Hunting the Manticore 250 XP


The Uncoded One’s airship, the Manticore, has begun an all-out attack on the city of Consolas. It must be
destroyed, or the city will fall. Only by combining Mylara’s prototype, Skorin’s cannon, and your
programming skills will you have a chance to win this fight. You must build a program that allows one
user—the pilot of the Manticore—to enter the airship’s range from the city and a second user—the city’s
defenses—to attempt to find what distance the airship
airship is at and destroy it before it can lay waste to the
t he
town.
The first user begins by secretly establishing how far the Manticore is from the city, in the range 0 to 100.
The program then allows a second player to repeatedly attempt to destroy the airship by picking the
range to target until either the city of Consolas or the Manticore is destroyed. In each attempt, the player
is told if they overshot (too far), fell short (not far enough), or hit the Manticore. The damage dealt to the
Manticore depends on the t he turn number. For
For most turns,
tur ns, 1 point of damage is dealt.
deal t. But if the turn number
num ber
is a multiple of 3, a fire blast deals 3 points of damage; a multiple of 5, an electric blast deals 3 points of
damage, and if it is a multiple of both 3 and 5, a mighty fire-electric blast deals 10 points of damage. The
Manticore is destroyed after taking 10 points of damage.
However, if the Manticore survives a turn, it will deal a guaranteed 1 point of damage to the city of
Consolas. The city can only take 15 points of damage before being annihilated.
Before a round begins, the user should see the
t he current status: th
thee current round number, the city’s health,
and the Manticore’s health.
A sample run of the program is shown below. The first player gets a chance to place the Manticore:
Player 1, how far away from the city do you want to station the Manticore? 32

At this point, the display is cleared, and the second player gets their chance:
Player 2, it is your turn.
-----------------------------------------------------------
STATUS: Round: 1 City: 15/15 Manticore: 10/10
The cannon is expected to deal 1 damage this round.
Enter desired cannon range: 50
That round OVERSHOT the target.
-----------------------------------------------------------
STATUS: Round: 2 City: 14/15 Manticore: 10/10
The cannon is expected to deal 1 damage this round.
Enter desired cannon range: 25
That round FELL SHORT of the target.
-----------------------------------------------------------
STATUS: Round: 3 City: 13/15 Manticore: 10/10
The cannon
Enter is expected
desired to deal
cannon range: 32
3 damage this round.
That round was a DIRECT HIT!
-----------------------------------------------------------
STATUS: Round: 4 City: 12/15 Manticore: 7/10
The cannon is expected to deal 1 damage this round.
Enter desired cannon range: 32
That round was a DIRECT HIT!
-----------------------------------------------------------
STATUS: Round: 5 City: 11/15 Manticore: 6/10
The cannon is expected to deal 3 damage this round.
Enter desired cannon range: 32
That round was a DIRECT HIT!
-----------------------------------------------------------
STATUS: Round: 6 City: 10/15 Manticore: 3/10
The cannon is expected to deal 3 damage this round.
Enter desired cannon range: 32
That round was a DIRECT HIT!
The Manticore has been destroyed! The city of Consolas has been saved!

CLEANING
CLEANIN G UP HEAP MEMORY 125
Objectives:
• Establish the game’s starting state: the Manticore begins with 10 health points and the city with 15.
The game starts at round 1.
• Ask the first player to choose the Manticore ’s distance from the city (0 to 100). Clear the screen
afterward.
• Run the game in a loop until either the Manticore’s or city’s health reaches 0.
• Before the second player’s turn, display the round number, the city’s health, and the Manticore’s
health.
• Compute how much damage the cannon will deal this round: 10 points if the round number is a
multiple of both 3 and 5, 3 if it is a multiple of 3 or 5 (but not both), and 1 otherwise. Display this to
the player.
• Get a target range from the second player, and resolve its effect. Tell the user if they overshot (too
far), fell short, or hit the Manticore. If it was a hit, reduce the Manticore ’s health by the expected
amount.
• If the Manticore is still alive, reduce the city’s health by 1.
• Advance to the next round.
• When the Manticore or the city’s health reaches 0, end the game and display the outcome.
• Use different colors for different types of messages.
• Note: This is the largest program you have made so far. Expect it to take some time!


Note:
Note: Use methods to focus on solving one problem at a time.
This version requires two players, but in the future, we will modify it to allow the computer
to randomly place the Manticore so that it can be a single-player game.
Part
Object-Oriented 2
Programming
C# is an object-oriented programming language, meaning that the code we write is typically organized
into little blocks, each responsible for a small slice of the whole program. Each object has its own data
(variables) and capabilities (methods), and the objects all work together to form a cohesive system.
Without an understanding of object-oriented programming in C#, our knowledge of the language is far
from complete. This is the topic of Part 2.
We will look at the following:

Introduce what object-oriented programming is about (Level 15). 15).
• Discuss the many ways C# lets you create custom types: enumerations (Level 16), 16), tuples (Level 17),
17),
classes (Level 18),
18), interfaces (Level 27),
27), structs (Level 28),
28), records (Level 29),
29), and generics (Level 30).
30).
• Discuss the key points of object-oriented programming: information hiding (Level 19) 19),, properties
(Level 20),
20), static members (Level 21) 21),, null references (Level 22),
22), inheritance (Level 25), 25), and
polymorphism (Level 26).26).
• Get some practice designing and building larger object-oriented programs (Levels 23, 24, and 31). 31).
• A final level describing some common types that come with .NET’s Base Class Library, including
Random, DateTime , TimeSpan, lists, and dictionaries (Level 32). 32).
LEVEL 15
OBJECT-ORIENTED CONCEPT󰁓

󰁓peedrun
• Object-oriented programming allows you to separate large programs into individual components
called objects, each responsible for a small slice of the overall program.
• Objects belong to a class, which
which defines a category of things with the same structure and capabilities.
• Building custom types is a powerful tool for building large programs.

OBJECT-ORIENTED CONCEPT󰁓
In Part 2, we turn our attention from C# programming basics to a pair of problems with the
same solution. Those two problems are shown in the picture below:

How do we go about building a program as complex as the game Asteroids, shown above? How

do we take
hundreds orthe raw ingredients
thousands weacross
of variables know thousands
and wield of
them to create something that spans
methods?

130 LEVEL 15 OBJECT-ORIENTED CONCEPTS


Second, how do we represent complex concepts such as the ship, bullet, and asteroid shown
above? What about concepts like the season of the year, dates, and scores on a high scores
table?
The solution to both of these problems in C# is object-oriented programming.
The basic concept of object-oriented programming is that instead of putting all of our code
into a single ever-growing blob of code, we split our program into multiple components
c omponents called
objects. Each object has a single responsibility (or perhaps a set of closely related
responsibilities),
responsibiliti es), and the objects work together
toget her to solve the overall problem.
Each object in the system performs its job in coordination with the other objects.
Objects can be created while the program runs. Objects that are no longer needed can be
removed from the system.
Each object contains a set of methods and variables. Its variables store its data while its
methods allow other objects to make requests of it. Most objects have a few of each. Some
objects only need to represent the state something is in and contain only variables. Some do
not need to remember any state or data and have only methods.
An object can coordinate with
with another object by calling
calling one of its methods.
This paradigm is not unique to programming. For example, businesses and team projects
work in the same way. Large challenges
c hallenges are too big for a single person, so the overall task is
split among many people. They each fulfill their part of the larger system and can make
requests and get information from others.
In C#, every object belongs to a specific class or type. An object’
objec t’ss class determines
deter mines the object’s
“shape.” All objects of the same class have the same data elements and methods. You can think
of classes as categories; everything
ever ything in a class is similar in nature and structure.
Many different objects can belong to the same class.
cl ass. You
You can interact with objects
objec ts of the same
class in the same way, but each object is independent of the others. Objects of a particular
class are often called instances of the class.
Many classes of objects already exist as a part .NET. The string type is a class,
p art of .NET. c lass, for example.
But C# allows us to define new classes
c lasses as well.
This ability to define new types of objects gives us some hints about how we can solve our
second problem: representing things that can’t be represented well with any of the 14 simple
types we learned about in Level 6. If one of the existing types isn’t a good fit for something we
need to represent, we can use these built-in types as building blocks to craft new tailor-made
tailor-made
types to represent these more complex concepts.
For example, we could use three ints to represent a date on the calendar (day, month, and
year), all bundled into a new class for representing dates. Or we could define a new class for
representing asteroids, with their position, rotation, and speed, and give them methods for
doing things asteroids do, like updating position over time.
C# has many ways to define new types from built-in ones, including enumerations, tuples,
structs, and classes, with classes being the most sophisticated. Part 2 focuses on defining
defini ng new
types and especially on defining new classes. We’ll see how to make new objects and get
multiple objects working together to solve larger programs.
As we will see in the coming levels, when we encounter new concepts and ideas that don’t fit
nicely into the basic types we already know, we craft new types and classes to represent these

OBJECT-ORIENTED CONCEPTS 131


more complex concepts. Using objects, we will be able to build programs to solve more
complex problems.
After defining
defining a new type,
type, we will be able to work with it as a cohesive new, reusable element—
a new type that other variables can use. If we build a new Ship type, we will be able to make
variables of the Ship type elsewhere in our program without recreating the logic and data that
a ship encapsulates every time.
This concept will become an essential rule in C# programming: Use the right type for
everything you create. If the right type doesn’t exist, create it first.

Knowledge Check Objects


Check your knowledge with the following questions:
25 XP
1. What two things does an object bundle together?
2. True/False. C# lets you define new types of objects.
Answers: (1) data and operations on that data (methods). (2) True.
LEVEL 16
ENUMERATION󰁓

󰁓peedrun
• An enumeration is a custom type that lists the set of allowed values: enum Season { Winter,
Spring, Summer, Fall }
• Define your enumerations after your main method and other methods or in a separate file.

defining an enumeration, you can use it for a variable’s type: Season


Season.Winter;
After now =

In C#, types matter. You don’t use a string when working with numbers, and you don’t use
an int when working with text. What do we do when we encounter
enc ounter something that doesn’t fit
nicely into one of our pre-existing types? For example, what if we need to represent the
seasons of the year (winter,
(winter, spring, summer, fall)?
Using only data types that we’re already familiar with, we have two choices: an integer type
like int or a string. With an int, we could assign a number to each season:
int current = 2; // Summer

And:
if (current == 3) Console.WriteLine("Winter is coming.");

This approach can work but has two problems. First, it’s hard to remember which season is
which. Did we start with winter or spring? Do we start counting at 0 or 1? Only with the
comment does it become clear. The second problem is that nothing prevents us from using
weird numbers.
numbers. Somebody could make
make the current season -14 or 2 million.
We could use the text "Summer" to represent summer:
What if we used strings? We
string current = "Summer";

And:
if (current == "Fall") Console.WriteLine("Winter is coming.");

ENUMERATION
ENUMERATION BASICS 133
This approach has similar problems. While the text "Fall" is far less likely to be
misinterpreted, "Fall", "fall", "Faall", and "Autumn" are not the same string. And
nothing prevents us from doing something like current = "Monday";.
C# provides a better solution to this problem: defining a new type called an enumeration.
ENUMERATION BA󰁓IC󰁓
An enumeration or an enumerated type is a type whose choices are one of a small list of
possible options. The verb enumerate means “to list off things, one by one,” hence the name.
We can
can define new enumerations in our code to represent concepts of tthis
his nature.
Enumerations only work when you have a relatively small set (a few, tens, or maybe hundreds)
of choices, especially when you can make an exhaustive list, not leaving anything out. For
example, the Boolean values true and false would be an excellent enumeration if they were
not already part of the bool type. With only four choices, the year’s seasons are also a great
candidate for an enumeration.
en umeration.

Defining an Enumeration
Before we can use an enumeration, we have to define it. New type definitions, including
enumerations, must come after our main method and the methods it owns (or in a separate
file, as we will do later). However,
However, when we create
c reate multiple new types, their relative order does
not matter.
Console.WriteLine("Hello, World!");

// <-- Add new enumerations here, at the end of your file.

The following defines a new enumeration to represent seasons:


enum Season { Winter, Spring, Summer, Fall }

ne w enumeration, you start with the enum keyword, followed by the enumeration’s
To define a new
name (Season). A set of curly
c urly braces contains the options for the enume
enumeration,
ration, separat
separated
ed by
commas. In C#, it is common to use UpperCamelCase for type names (including enumeration
names) and enumeration members. The above code used Season instead of season or
SEASON, and Winter instead of WINTER or winter for that reason. Of course, the choice is
yours, but I recommend giving this
this standard convention a try.
Once placed in the rest of the code, the entire file might look like this:
Console.WriteLine("Hello, World!");

enum Season { Winter, Spring, Summer, Fall } // New types MUST go after other
// code (or in another file).

Unlike methods, type definitions like this do not live inside your main method. The code map
looks like this instead:

134 LEVEL 16 ENUMERATIONS


ENUMER ATIONS
Once your file encounters a type definition like this, it marks it as the end of your main
method, and no further statements can come afterward. But you can add as many types to the
bottom of your file as you want. Some people prefer putting new type definitions, like an
enumeration, into separate .cs files. We’ll cover that in Level 33, but feel free to jump ahead
and read that section if you think you’d prefer separate files.
Whitespace does not matter
matter,, so the following style
style is also typical:
enum Season
{
Winter,
Spring,
Summer,
Fall
}

The first item you list will be the enumeration’


e numeration’ss default value, so choose it wisely.

Using an Enumeration
With our Season enumeration defined, we can use it like any other type. For example, we can
declare a variable whose type is Season:
Season current;

The compiler can now help us enforce that only legitimate seasons are assigned to this
variable. You
You can pick a specific value
value like this:
Season current = Season.Summer;

We access a specific enumeration value through the enumeration type name and the dot
operator. This is a bit more complicated than literals like 2 or "Summer", had we just used
ints or strings, but it is not bad.
integ ers. For example, we can use the == operator
Enumerations have much in common with integers.
to check for equality:
Season current = Season.Summer;

if (current == Season.Summer || current == Season.Winter)


Console.WriteLine("Happy solstice!");
else
Console.WriteLine("Happy equinox!");

ENUMERAT
ENUMERATION
ION BASICS 135
enum Season { Winter, Spring, Summer, Fall } // New types MUST go after other
// code (or in another file).

Revisiting ConsoleColor
In the past, we have used the ConsoleColor type like this:
Console.BackgroundColor = ConsoleColor.Yellow;

That code should have new meaning now. ConsoleColor is an enumeration! Somewhere
out there is code like enum ConsoleColor { Black, Yellow, Red, ... }. Equipped

with the knowledge of enumerations, we could have written that


that ourselves!
Challenge
Challe nge 󰁓imula’s Test 100 XP
As you move through the village of Enumerant, you notice a short, cloaked figure following you. Not
being one to enjoy a mysterious figure tailing you, you seize a moment to confront the figure. “Don’t be
alarmed!” she says. “I am Simula. They are saying you’re a Programmer. Is this true?” You answer in the
affirmative, and Simula’s eyes widen. “If you are truly a Programmer, you will be able to help me. Follow
me.” She leads you to a backstreet and into a dimly lit hovel. She hands you a small, locked chest. “We
haven’t seen any Programmers in these lands in a long time. And especially not ones that can craft types.
If you are a True Programmer, you will want what is in that chest. And if you are a True Programmer, I
will gladly give it to you to aid you in your quest.”
The chest is a small box you can hold in your hand. The lid can be open, closed (but unlocked), or locked.
You’d normally be able to go between these states, opening, closing, locking, and unlocking the box, but
the box is broken. You
You need to create a program with an enumeration to recreat
recreatee this locking mechanis
mechanism.
m.
The image below shows how you can move between the three states:

Nothing happens if you attempt an impossible action in the current state, like opening a locked box.
The program below shows what using this might look
loo k like:
The chest is locked. What do you want to do? unlock
The chest
The chest is
is open.
unlocked.
WhatWhat do you
do you wantwant to do?
to do? open
close
The chest is unlocked. What do you want to do?

Objectives:
• Define an enumeration for the state of the chest.
• Make a variable whose type is this new enumeration.
• Write code to allow you to manipulate the chest with the lock, unlock, open, and close
commands, but ensure that you don’t transition between states that don’t support it.
• Loop forever, asking for the next command.

136 LEVEL 16 ENUMERATIONS


ENUMER ATIONS

UNDERLYING TYPE󰁓
The deep dark secret of enumerations is that they are integers at heart, though the compiler
will ensure you don’t accidentally misuse them. EachE ach enumeration has an underlying type,
builds upon. The default underlying type is int, but you could
which is the integer type that it builds
change that:
enum Season : byte { Winter, Spring, Summer, Fall }

It is usually not worth the trouble to change this, but it is worth considering if memory is tight.
Because enumerations are based on integers, there are some other tricks you may find useful.
Each enumeration member is assigned an int value. By default, these are given in the order
they appear in the definition, starting with 0. So above, Winter is 0, Spring is 1, etc. If you
want, you can
can assign custom numbers:
enum Season { Winter = 3, Spring = 6, Summer = 9, Fall = 12 }

Any enumeration member


mem ber without an assigned number is automatically given the one after
the member before it. So below, Winter is 1, Spring is 2, Summer is 3, and Fall is 4:
enum Season { Winter = 1, Spring, Summer, Fall }

The default value for an enumeration is whichever one is assigned the number 0. That remains
true even if nothing is assigned 0, which means the default value may not even be a legal
choice! In that case, consider adding something like Unknown = 0 so you can still refer to
the default value by name.
Y
You between ints and enumerations:
ou can also cast between
int number = (int)Season.Fall;
Season now = (Season)2;

Use this cautiously. It can result in using a number that is not a valid enumeration option:
Season another = (Season)822; // Not a valid season!
LEVEL 17
TUPLE󰁓

󰁓peedrun
• Tuples e lements into a single bundle: (double, double) point = (2, 4);
Tuples combine multiple elements
• You can give (ephemeral) names to tuple elements, which can be used later: (double x, double
y) point = (2, 4);
• Tuples can be used like any other type, including variable and return types.
• Deconstruction unpacks tuples into multiple variables: (double x, double y) =
MakePoint();
• Two tuple values are equal if their corresponding items are all equal.

The next tool we will acquire in our arsenal combines many variables into a single bundle: a
tuple. Before we go too far, I need to point out that tuples have their place, but we will soon
learn better tools for most situations. Most C# programmers only use tuples occasionally.
To understand where we can use tuples, let’s consider the problem they solve, illustrated by
the picture below:

This picture is roughly what the original Tetris high score table looks like. How could we
represent these scores in our program? These scores are more than just a single int value.
Each score has the player’s name, points,
points, and the level they reached in getting the score.

138 LEVEL 17 TUPLES


We could imagine making three variables along the lines of string name, int points,
and int level for just a single score. But to make the full table, we need three of each. We
could do this with arrays:
string[] names = new string[3] { "R2-D2", "C-3PO", "GONK" };
int[] points = new int[3] { 12420, 8543, -1 };
int[] level = new int[3] { 15, 9, 1 };

But this feels like we’ve organized our data sideways. Instead of putting R2-D2 with his score
and level, we have put all names, points, and levels together.
In this case, a score feels like its own concept or idea. And as we learned in Level
L evel 15, we should
make a new type to capture the idea when this happens. We need some way to represent an
entire score’s information—name, point total, and level—in a bundle. When multiple data
elements are combined like this, it is sometimes referred to as a composite type because the
larger thing is composed of the smaller pieces. Or you could say that we use composition to
build the larger element.

THE BA󰁓IC󰁓 OF TUPLE󰁓


In C#, the simplest tool for creating composite types is called a tuple (pronounced “TOO-ples
“TO O-ples””
or “TUP-ples”).
“TUP-ples”). A tuple allows us to combine multiple pieces into a single ele
element.
ment. The name
comes from the math world to generalize the naming pattern double, triple, quadruple,
quintuple, etc. These are also sometimes referred to by the number of items in them: a 2-tuple
if it has two things, an 8-tuple if it has eight
e ight things, etc.
Forming a new tuple value is as simple as taking the pieces you need and placing them in
parentheses, separated by commas:
(string, int, int) score = ("R2-D2", 12420, 15);

The variable type is formed similarly, listing the types in parentheses, separated by commas.
That leads to a long type name, and while I have been avoiding var for clarity, this is a good
example of why some people prefer var:
var score = ("R2-D2", 12420, 15);

The type for score is a 3-tuple composed of a string, an int, and an int.
Y
You
ou can access the items
items inside of the tuple like so:
Console.WriteLine($"Name:{score.Item1} Level:{score.Item3} Score:{score.Item2}");

These names leave a lot to be desired. Was it Item2 or Item3 that contained the point total?
It is easy to get them mixed up, and it gets worse with tuples with many items. We will soon
see ways to attach alternative names to the items
i tems in a tuple, but behind the sc
scenes,
enes, the names
really are Item1, Item2, and Item3.
The type of a tuple is determined by the type and order of the parts of the tuple. That means
you can do something like
like this:
(string, int, int) score1 = ("R2-D2", 12420, 15);
(string, int, int) score2 = score1; // An exact match works.

But you cannot do either of these:

TUPLE ELEMENT NAMES 139


(string, int) partialScore = score1; // Not the same number of items.
(int, int, string) mixedUpScore = score1; // Items in a different order.

Tuples are value types, like int, bool, and double. That means they store their data inside
them. Assigning one variable to another will copy all the data from all of the items in the
process. That is made a bit more complicated because tuples are composite types. If a tuple
has parts that are value types themselves, those bytes will get copied. But if an item is a
reference type, then the reference is copied.

TUPLEThe
ELEMENT AME󰁓
names ofNthe items in a tuple are Item1, Item2, etc. Behind the scenes, that is precisely
how they work. However,
However, the compiler lets you pretend that the items have alternative names.
Doing so can lead to much more readable code.
If you don’t use var, you can assign names to each item in the tuple like so:
(string Name, int Points, int Level) score = ("R2-D2", 12420, 15);
Console.WriteLine($"Name:{score.Name} Level:{score.Level} Score:{score.Points}");

Placing names next to the types when the variable is declared, you will be able to refer to those
names later, as shown in the second line.
Y
You
ou are not required to give a name to every
ever y tuple member. Any unnamed item will keep its
original ItemN name:
(string Name, int, int) score = ("R2-D2", 12420, 15);
Console.WriteLine($"Name:{score.Name} Level:{score.Item3} Score:{score.Item2}");

If you use var, you lose your chance to give the items a name in this manner. But you are not
out of luck. You can also apply names to a tuple when the tuple is formed:
var score = (Name: "R2-D2", Points: 12420, Level: 15);
Console.WriteLine($"Name:{score.Name} Level:{score.Level} Score:{score.Points}");

When you use var in this way, not only are the tuple’s constituent types inferred, but the
names will be as well.
However, if you do not use var, then the names will not be inferred, and any name supplied
However,
when you declared the variable
variable would be used:
(string, int P, int L) score = (Name: "R2-D2", Points: 12420, Level: 15);
Console.WriteLine($"Name:{score.Item1} Level:{score.L} Score:{score.P}");

With the above are Item1, P, and L, not Name, Points, and Level.
above code, the names are
These examples help illustrate that even though adding names can lead to clearer code, the
names are fluid and are not a part of the tuple itself. For tuples, names are only cosmetic.

TUPLE󰁓 AND METHOD󰁓


While tuple types are more complicated, they are
are just another type for all practical purposes.
For example, you can use them as parameter types or return values. We can take the code we
have been working with and turn it into a method for displaying scores, passing in a score as
a tuple:

140 LEVEL 17 TUPLES


void DisplayScore((string Name, int Points, int Level) score)
{
Console.WriteLine(
$"Name:{score.Name} Level:{score.Level} Score:{score.Points}");
}
Alternatively, we could have left out the tuple element names and just used Item1, Item2,
and Item3 in the method itself. Parameters cannot use var, so we are obligated to list the
tuple item types in this case.
The syntax here is trickier because tuples and the parameters of a method both use
parentheses and commas. You
You will want to pay careful attention when using it this way.
A parameter whose type is a tuple is just another parameter
parameter,, and you can mix and match tuple
parameters with non-tuple (normal) parameters as needed.
The same is true of return types. You can return a tuple from a method by placing its
constituent parts in parentheses (names optional) in the spot where we list the return type:
(string Name, int Points, int Level) GetScore() => ("R2-D2", 12420, 15);

A tuple’s types and names can be inferred


infer red from a called me
method’
thod’ss return type, just like when
we created the new value
value inline:
var score = GetScore();
Console.WriteLine($"Name:{score.Name} Level:{score.Level} Score:{score.Points}");

But
is the names
shown provided
below, by your return
where everything value
is using do not need
different to match those of your variable. This
names:
(string One, int Two, int Three) score = GetScore();
DisplayScore(score);

(string N, int P, int L) GetScore() => ("R2-D2", 12420, 15);

void DisplayScore((string Name, int Points, int Level) score)


{
Console.WriteLine(
$"Name:{score.Name} Level:{score.Level} Score:{score.Points}");
}

This illustrates more clearly that names are ephemeral and not a part of the tuple.

MORE TUPLE EXAMPLE󰁓


Let’s look at a few more examples of tuples before moving on.
This tuple represents a point in two-dimensional space:
(double X, double Y) point = (2.0, 4.0);

Think of how nice it could be to combine these two coordinates into a single thing and pass it
around in your code if you were making a game in a 2D world.
Or,
Or, if we have a gr
grid-based
id-based world, what about using a tuple with elements for the grid square’s
type and location? We could define the tile’s type as an enumeration like this:

enum TileType { Grass, Water, Rock }

DECONSTRUCTING TUPLES 141


We can
can place that into a tuple
tuple with a row and a column:
var tile = (Row: 2, Column: 4, Type: TileType.Grass);

And here is a tuple with 16 elements to show a much bigger


bigger tuple, representing a 4×4 matrix—
matrix—
something often used in games:
var matrix = (M11: 1, M12: 0, M13: 0, M14: 0,
M21: 0, M22: 1, M23: 0, M24: 0,
M31: 0, M32: 0, M33: 1, M34: 0,
M41: 0, M42: 0, M43: 0, M44: 1);

(Perhaps an array would be better for that last one?)


And finally, let’s look at an example that creates and returns an array of (string, int,
int) tuples to create the full scoreboard we introduced at the beginning of this level:
(string Name, int Points, int Level)[] CreateHighScores()
{
return new (string, int, int)[3]
{
("R2-D2", 12420, 15),
("C-3PO", 8543, 9),
("GONK", -1, 1),
};
}

The above code creates a fixed list of scores, but in a real-world situation, we’
we’d
d probably store
these in a file and load them from there (Level 39).
39).

DECON󰁓TRUCTING TUPLE󰁓
We have
have seen many examples of creating
creating tuples. Let’s
Let’s look at the opposite.
opposite. Suppose you have
have
the following tuple:
var score = (Name: "R2-D2", Points: 12420, Level: 15);

The simplest way to grab data out of a tuple is just to reference the item by name:
string playerName = score.Name;

When you only need a single item


item from the tuple, this is a good way
way to do it.
But there is a way to take all of the parts of a tuple and place them each into separate variables
all at once. This is called deconstruction or unpacking. It is done by listing each of the variables
to store the deconstructed tuple in parentheses:
string name;
int points;
int level;

(name, points, level) = score;


Console.WriteLine($"{name} reached level {level} with {points} points.");

The highlighted line copies each item in the tuple to their respective variables.
Y
You
ou can declare new variables at the same time, so we could also have written the above code
like this:

142 LEVEL 17 TUPLES


(string name, int points, int level) = score;

That starts to look precariously close to declaring a new tuple variable with named items. The
difference is that this version does not provide a name after the parentheses to refer to the
entire tuple.
Tuple deconstruction has many uses, but a clever usage is swapping the contents of two
variables:
double x = 4;
double y = 2;
(x, y) = (y, x);

The two variables’ contents are copied over to a new tuple and then copied back to x and y.
The result is x and y have swapped values with only a single line.

Ignoring Elements with Discards


Tuple deconstruction demands that the variables on the left match the tuple in count and
types. Sometimes, you only care about some of the values. Rather than make a variable called
junk or unused, you can use a discard variable using a simple underscore, and no type:
(string name, int points, _) = score;

The _ is a discard variable. The compiler will invent a name for it behind the scenes so the
code can work, but it won’t clutter up the code with useless names and leads to more readable
code. Wins all around.

TUPLE󰁓 AND EQUALITY


Tuples are value types and thus, use value semantics when checking for equality. Two tuple
values are considered equal if they have the same number of items, the corresponding items
are the same types, and if each item is equal to the corresponding
cor responding item in the other tuple. That
last item is a little tricky because if some part of a tuple is a reference type, then the references
(and not the data) will be checked for equality. The following will display True and then
False:
(int, int) a = (1, 2);
(int, int) b = (1, 2);

Console.WriteLine(a == b);
Console.WriteLine(a != b);

There is one potential surprise to tuple equality. Will a and b below be equal or not equal?
var a = (X: 2, Y: 4);
var b = (U: 2, V: 4);
Console.WriteLine(a == b);

The only difference is the names given to the tuple elements. Do the names of the tuple
elements matter? Since names are not officially part of the tuple, a and b above are equal
despite the name differences.

TUPLES AND EQUALITY 143

Challenge 󰁓imula’s
󰁓imula’s 󰁓oup 100 XP
Simula is impressed with how you reconstructed the box with an enumeration. When the box opened,
you saw a glowing emerald gem inside. You don’t know what it is, but it seems important. Also in the box
were three
t hree vials of powder labeled
labe led HOT, SALTY,
SALTY, and
and SWEET.
SWEE T.
“Finally! I can make soup again!” Simula says. She casually tosses the small glowing gem to you but is
wholly focused on the powders. “You stick around and help me make soup with your programming skills,
and I’ll tell you what that gem does.”
She pulls out a cookpot, knocks the clutter off the table with a quick sweep of her arm, and begins
cooking. She says, “I’m the best soup maker in town, and you’re in for a treat. I’ve got recipes for soup,
stew, and gumbo. I’ve got mushrooms, chicken, carrots, and potatoes for ingredients. And thanks to you
getting that box open, I’ve got seasonings again! Spicy, salty, and sweet seasoning. Pick a recipe, an
ingredient, and a seasoning,
seasoning , and I’ll make it. Use your programming skil
skills
ls to help us track what we make.”
Objectives:
• Define enumerations for the three variations on food: type (soup, stew, gumbo), main ingredient
(mushrooms, chicken, carrots, potatoes), and seasoning (spicy, salty, sweet).
• Make a tuple variable to represent a soup composed of the three above enumeration types.
• Let the user pick a type, main ingredient, and seasoning from the allowed choices and fill the tuple
with the results. Hint: You could give the user a menu to pick from or simply compare the user’s
text input against specific strings to determine which enumeration value represents their choice.

When done, display the contents of the soup tuple variable in a format like “Sweet Chicken Gumbo.”
Hint: You don’t need to convert the enumeration value back to a string. Simply displaying an
enumeration value with Write or WriteLine will display the name of the enumeration value.)

Narrative The Fountain of Objects


As you eat soup with Simula, she explains that she is the Caretaker of the Heart of Object-Oriented
Programming—the glowing green gem in the box. For thousands of clock cycles, she has held onto it,
hoping that someday, a Programmer who understood object-oriented programming would appear to
restore the Fountain of Objects, destroyed by The Uncoded One, back to what it once was: the lifeblood
of the entire island.

She tells you that to do this, you must gather the five keys of Object-Oriented Programming and make
your way to the Fountain of Objects, whose location is secret. She tells you you can discover its location
if you visit the Catacombs of the Class and marks that location on your map.
You leave Simula’s hovel behind and begin the quest to restore the Fountain of Objects to what it once
was. Your next destination: the Catacombs of the Class!
LEVEL 18
CLA󰁓󰁓E󰁓

󰁓peedrun
• Classes are the most powerful way to define new types.
• A class bundles together data (fields) and operations on that data (methods): class Score {
public int points; public int level; public void Method() { } }

Constructors define how new instances are created: public Score(int p) { points = p; }
• Classes are reference types.

We got our first taste of making and using custom types with enumerations. We got our first
taste of composite types with tuples. With the appetizers out of the way, it is time to dive into
the main course: classes. Classes are the king of the object-oriented world. We’ll revisit
representing a score once again, this time using classes
c lasses to solve the problem.
Let’s start by
by giving official definitions for the concepts of objects, classes, and instances.
An object is a thing in your software, responsible for a slice of the entire program, containing
data and methods, which define what information the object must remember and the
capabilities it can perform when requested.
An object-oriented program typically has many objects, each performing its own job in the
system. Some objects know about other objects and work with them to get their jobs done by
invoking others’ methods. We
We have already been doing this in the programs we have
h ave created.
Our main method lives in an object and asks the Console, Convert, and Math objects to
perform tasks from the ones they are built to do. (Though we will soon see that Console,
Convert, and Math fit into a different category
categor y than most objects.)
A big part of programming in an object-oriented world is deciding how to split the program
into objects. Which responsibilities should each have? What data and methods does the
object need to fulfill those responsibilities? Which other objects does it need to work with?
These questions are at the heart of software design, or more specifically, object-orient ed design.
object-oriented
We will get into this topic later in this book, but it is also a topic that takes months and years
of study and practice to get good at. On the bright side, software is soft—malleable. If you try
something and later decide that another way is better, you can change the code.

DEFINING A NEW CLASS 145


In some programming languages, objects are flexible. Variables and methods can be added
and stripped from an object over time. This scheme is great for tiny programs because there
is low formality and high flexibility. But you can never be sure that an object has a particular
piece of data or is capable of performing a specific method. As your programs get larger in this
scheme, this problem grows out of control.
In C#, types matter.
matter. Rather than morphing over their lifetime, C# objects are categorized into
classes. By defining a class, you establish the variables and methods of any object belonging
to the class. You
You can think of a class
c lass definition as a blueprint or pattern for objects that belong
to the class.

Programmers will also


example, if we define refer toclass,
a Score objects that fit
an object into
that a classtoas
belongs instance
theanScore ofmight
class that class. For
be called
an instance of the Score class or a Score instance. The words “object” and “instance” are
almost synonyms, though “object” is used more often when the specific class matters less,
while “instance” is
is typically used in conjunction with a type name.
Defining a class also defines a new type that you can use for variables. Classes are reference
types, putting them in the same bucket as strings and arrays. Variables whose type is a class
hold only a reference; the object’s data lives somewhere on the heap. In this book, I sometimes
use the word “value” to indicate the contents of a value type and “object” to indicate the
contents of a reference type, though these terms are often blurred together in the
programming world.

DEFINING A NEW CLA󰁓󰁓


Before we can use a class, we must first define it. Many C# programmers place each class in a
separate file. Indeed, as your programs grow big enough to have 10 or 100 classes defined, you
will not want to keep it all in a single file. We will see how to split your program across many
files later (Level 33, though you can probably figure it out on your own). For now, you can
place them in the same place we have placed enumerations, at the bottom of our main file,
after all other statements and methods.
Defining a new class is done with the class keyword, followed by the class’s
class’s name, followed
by a set of curly braces. Names are usually capitalized with UpperCamelCase, just like
enumerations and methods. Inside the class’s curly braces, we can place the variables and
methods that the class will need to do its job.

Using the Tetris score table example from the previous level, we know we need three
th ree variables:
l evel the player reached. A simple Score class looks like this:
a name, a point total, and the level
// <-- Your main method goes here.

class Score
{
public string name;
public int points;
public int level;
}

// <-- Other classes and enumerations can go here.

These variables are not the same thing as local variables or parameters. They are another
category of variables called fields or instance variables. Local variables and parameters belong

146 LEVEL 18 CLASSES


to a method and come and go as the method is called and ends. Fields are variables created
inside the object’s memory on the heap. They live for as long as the object
objec t lives and are a part
of the object itself.
In Level 19, we will look at what that public does, but for now, we will just blindly apply that
to the fields we make.
Unlike a tuple, we can also add methods to a class.
c lass. For example, the method below indicates
whether the score earned a star,
star, defined by averaging
averaging at least 1000 points per level:
class Score
{
public string name;
public int points;
public int level;

public bool EarnedStar() => (points / level) > 1000;


}

That EarnedStar method is like most methods we have seen in the past, but with two
notable differences. The first is that it also has a public on it. Again, for now, we will just
assume that’s
that’s what you do for methods that belong to a class.
m ethod is how it uses the points and level fields in
Perhaps the most notable part of that method
its code. This lines up with what we’ve seen in the past about scope. Since EarnedStar lives
in Score, this method will have access to its
it s own local variables and parameters (though this
method has neither) and also any variables defined in the class itself.
Classes give us a way to bundle together data and the operations on that data into a well-
defined cohesive unit. This principle is called encapsulation. That principle is so important of
an idea that we want to call it out formally and give it a name. It is the first of five object-
oriented principles that form
for m the foundation of obje
object-oriented
ct-oriented programming.
Object-Oriented Principle #1: Encapsulation—Combining data (fields) and the
operations on that data (methods) into a well-defined unit (like a class).
Encapsulation is a crucial element in building objects that solve a slice of the overall problem,
which lets us make
make larger programs.
programs.
Much like what we saw with enumerations, when we define classes, they do not live within the
main method but are separate:

INSTANCES OF CLASSE
CLASSESS 147

IN󰁓TANCE󰁓 OF CLA󰁓󰁓E󰁓
The code above defines the Score class. It describes how scores in our program will work. It
provides the blueprint for all scores that come into existence as our program runs.
With a class defined, we can
can use it like any other type.
type. We can
can declare a variable whose type is
Score, for example, and then assign it a new instance:
Score best = new Score();

class Score
{
public string name;
public int points;
public int level;

public bool EarnedStar() => (points / level) > 1000;


}

Instances of a class are created with the new keyword. That Score() thing refers to a special
method called a constructor, used to get new
ne w instances ready for use. We
We didn’t define such a
constructor in our Score class, but the compiler was nice enough to generate a default one
for us. That is what is being used here. We will see how to define our own in a moment. The
expression new Score() creates a new instance of the Score class, placing its data on the
heap (it is a reference type, after all) and grabbing a reference to it. That reference is then
stored in the best variable.
Now that
that our instance has been created, we can work with its fields and iinvoke
nvoke its methods:
Score best = new Score();

best.name = "R2-D2";
best.points = 12420;
best.level = 15;

if (best.EarnedStar())
Console.WriteLine("You earned a star!");

The middle section of that code assigns new values to each of the instance’s
instance’s fields. These fields
belong to the instance, so we must access them through a reference to an instance, such as
the one contained in best.
In the if statement’s condition, the EarnedStar method is invoked. This is different from
how we have invoked methods before. Here, too, we must access the method through an
instance of the Score class, such as the one contained in the best variable. It is more like
how we call Console’s and Convert’s methods. However, in those cases, we used the class
name rather than using an instance. We’ll sort out that particular difference in Level 21.
We can create more
more than one instance of a class. When we do this, each instance has its
it s own
data, independent of the other instances:
Score a = new Score();
a.name = "R2-D2";
a.points = 12420;
a.level = 15;

Score
b.nameb=="C-3PO";
new Score();

148 LEVEL 18 CLASSES


b.points = 8543;
b.level = 8;

if (a.EarnedStar())
Console.WriteLine($"{a.name} earned a star!");
if (b.EarnedStar())
Console.WriteLine($"{b.name} earned a star!");

This code creates two Score instances and places a reference to each in the variables a and
b. Because each instance has its own data, when we call a.EarnedStar() , it is making the
determination based on a’s data, and for b.EarnedStar(), on b’s data.
If we look at the memory
memor y used in the program
p rogram above, it looks like this after running:

CON󰁓TRUCTOR󰁓
Creating a new object reserves space for the object’s data on the heap. But it is also vital that
new objects come into existence in legitimate starting states. Constructors are special methods
that run when an object comes to life to ensure it begins life in a good state. The following
adds a constructor to the Score class, giving each field a good starting value:
class Score
{
public string name;
public int
public int level;
points;

public Score()
{
name = "Unknown";
points = 0;
level = 1;
}

public bool EarnedStar() => (points / level) > 1000;


}

Constructors are like other methods in most ways, but with two caveats. Constructors must
use the same name as the class, and they cannot list a return type. Otherwise, constructors are
essentially the same as any other method. Add if statements, loops, and call other methods
as needed.

CONSTRUCTORS 149
A constructor’s
c onstructor’s job is to get new instances
in stances into a legitimate starting state. The specifics will
vary from class to class, but assigning
assigning initial values to each field is common.
Default Constructors and Default Field Values
At this point, you may be wondering about the fact tha
thatt we didn’t define a constructor before
but could still create new instances of the Score class. How did that work?
If you don’t define any constructors, the compiler inserts one that looks like this:
public Score() { }

The constructor exists and can be used when creating new instances, but it doesn’t do
anything fancy. The purpose of a constructor is to put new instances of the class into a valid
starting state. Yet
Yet this constructor
con structor doesn’t initialize anything. What is the starting state of our
fields in this case? Like we saw with arrays (Level 12), 12), each field is initialized to the type’s
default value. This initialization is done by filling the object’s memory with all 0 bits. In fact,
anything allocated on the heap is initialized in this same way, which is why we get default
values in both arrays and new objects. As we saw before, the int type’s default value is the
number 0, and the string type’s default value is the special value null (a lack of a value,
and something we will cover in more depth in Level 22) 22).. Thus, a new Score instance would
have had a null name (a lack of a name), with 0 points and a level of 0.
As soon as we add our own constructor to a class,
class, the default one no longer auto
auto-generates.
-generates.

Constructors with Parameters


Constructors are allowed to have parameters, just like other methods. It is quite common for
constructors to use parameters to let the outside world provide the initial values for some
fields. The constructor below does this:
class Score
{
public string name;
public int points;
public int level;

public Score(string n, int p, int


int l) // That's a lowercase 'L', not a 1.
{
name = n;
points = p;
level = l;
}

public bool EarnedStar() => (points / level) > 1000;


}

The names n, p, and l are not good I’d name them name, points,
g ood variable names. Normally, I’d
and level, but that causes a problem. Fields, local variables, and parameters are all
accessible from inside a class’s
class’s methods, including con
constructors.
structors. A local variable or parameter
is allowed to have the same name as a field, but when they share names, weird things happen.
’ll sort that out in a minute, but we’ll use the names n, p, and l to avoid sharing names for
We’ll
We
now.

This
field.constructor has three parameters, letting the calling code provide initial values for each

150 LEVEL 18 CLASSES


constructor, we will need to change how we ask for a new Score instance, but
With this new constructor,
with this change,
change, we no longer need to initialize each field afterward.
Score score = new Score("R2-D2", 12420, 15);
Multiple Constructors
A class can define as many constructors as you need. Each of these must differ in number or
types of parameters. The code below defines two constructors. The first one has no
parameters (a parameterless constructor) and gives each field a reasonable starting value. The
second constructor has three parameters to supply initial values for each field.
class Score
{
public string name;
public int points;
public int level;

public Score()
{
name = "Unknown";
points = 0;
level = 1;
}

public Score(string n, int p, int l)


{ name = n;
points = p;
level = l;
}

public bool EarnedStar() => (points / level) > 1000;


}

With two constructors,


constructors, the outside world
world pick which constructor
constructor it wants to
to use:
Score a = new Score();
Score b = new Score("R2-D2", 12420, 15);

Initializing Fields
Another wayInline
way to initialize
initialize fields is by
by doing so inline, where
where they are
are declared, as shown below:
class Score
{
public string name = "Unknown";
public int points = 0;
public int level = 1;

public bool EarnedStar() => (points / level) > 1000;


}

These assignments happen after the memory is zeroed out but before any constructor code
runs. These then become the default values for these fields. If these defaults are sufficient and
no other initialization needs to happen, you can skip defining your own constructors. But any
constructor can also override these defaults as needed:

CONSTRUCTORS 151
class Score
{
public string name = "Unknown";
public int points;
public int level = 1;

public Score()
{
name = "Mystery";
}

public bool EarnedStar() => (points / level) > 1000;


}
The points field will take on the default int value of 0. The level field will be assigned a
value of 1 because of the field’s initializer. name will first be assigned "Unknown" and
subsequently updated with "Mystery".
Like parameters, we cannot use var for a field’s type. It must
must always be written out.

Name Hiding and the this Keyword


Let’s get back to those bad single-letter variable names:
public Score(string n, int p, int l)
{

name = n;
points = p;
level = l;
}

Our Score class, with this constructor,


constructor, looks like this on a code map:

Within the Score constructor, we have access to the n/p/l set of variables and the
name/points/level set. But those single-letter names are not great. Typically, I’d have
given n, p, and l the names name, points, and level. In this case, doing so would use those
names twice. For better or worse, C# allows this. But consider what happens when you do:
public Score(string name, int points, int level)
{
name = name; // These will not do what you want!
points = points;
level = level;
}

152 LEVEL 18 CLASSES


On a line like name = name;, both usages of name refer to the element in the more narrow
scope, which is the constructor parameter.
parameter. This code takes a variable’s content and assigns it
back into that same variable. The class’s name field is technically still in scope, but the
parameter with the same name hides access to it. This is called name hiding.
There are two ways to address this. The first is to just use different variable names for the two.
That’s what we did above, though the names we chose are not great. A much more common
convention in C# is to place an underscore before field names, as shown below:
class Score
{
public string
public int _name;
_points;
public int _level;

public Score(string name, int points, int level)


{
_name = name;
_points = points;
_level = level;
}
}

The underscores let us use similar names with a clear way to differentiate fields from local
variables and parameters.
parameters.
Using underscores is so common that it is the de facto standard for naming fields. You may
also see some variations on the idea, such as using an m_ or a my prefix. These conventions
are used in other programming languages, and some programmers bring them into the C#
world because
because they are familiar
familiar.. But most C# programmers prefer the single underscore.
underscore. What
you choose is far less important than being consistent. You don’t want fields named name,
myPoints, level_ and constructor parameters called _name, points, and my_level.
Y
You’ll
ou’ll never keep them straight.
straight.
The second solution to name hiding is the this keyword. The this keyword is like a special
variable that always refers to the object you are currently in. Using it, we can access fields
directly, regardless of what names we have used for local variables and parameters:
class Score
{
public string name;
public int points;
public int level;

public Score(string name, int points, int level)


{
this.name = name;
this.points = points;
this.level = level;
}
}

All three parameters hide fields of the same name, but we can still reach them using this.
The this keyword allows us to use straightforward names without decoration while still
allowing everything to work out. This approach is also popular among C# programmers. I’ll
follow the underscore convention in this book;
book ; it is the more common cchoice.
hoice.

OBJECT-ORIENTED DESIGN 153

Calling Other Constructors with this


Sometimes, you’d like to reuse the code in one constructor from another. But you can’t just
call a constructor without using new, and if you did that in a constructor, you’d be creating a
second object while creating
c reating the first, which isn’t what you want. If you want one constructor
to build off another one, use the this keyword:
class Score
{
public string _name;
public int _points;
public int _level;

public Score() : this("Unknown", 0, 1)


{
}

public Score(string name, int points, int level)


{
_name = name;
_points = points;
_level = level;
}
}

This allows one constructor to run another constructor


c onstructor first, eliminating duplicate code.

Leaving Off the Class Name


When you are creating new instances of a class, if the compiler has enough
en ough information to
know which class you are using, you can leave the class name out:
out :
Score first = new();
Score second = new("R2-D2", 12420, 15);

This is like var, only on the opposite side of the equals sign. The compiler can infer that you
are creating an instance of the Score class because it is assigned to a Score-typed variable.
This feature is most valuable when our type name is long and complex.

OBJECT RIENTED
-Oconcept
The DE󰁓IGNlarge programs down into small parts, each managed by an object
of breaking
and all working together, is powerful. We will continue to learn about the mechanics and tools
for doing this throughout Part 2.
The harder challenge is figuring out the right breakdown. Which objects should exist? Which
classes should be defined? How do they work together? These questions are a topic called
ed design. Their answers are not always clear,
object-oriented
object-orient clear, even for veteran programmers. It is
also a subject that deserves its own book (or dozens). But in a few levels (Level 23),
23), we will get
a crash course in object-oriented design to have a foundation to build on.

Challenge Vin Fletcher’s


Fletcher’s Arrows 100 XP
Vin Fletcher is a skilled arrow maker. He asks for your help building a new class to represent arrows and
determine how much he should sell them for. “A tiny fragment of my soul goes into each arrow; I care
not for the money; I just need to be able to recoup my costs and get food on the table,” he says.

154 LEVEL 18 CLASSES


Each arrow has three parts: the arrowhead (steel, wood, or obsidian), the shaft (a length between 60 and
100 cm long), and the fletching (plastic, turkey feathers, or goose feathers).
His costs are as follows: For arrowheads, steel costs 10 gold, wood costs 3 gold, and obsidian costs 5 gold.
For fletching, plastic costs 10 gold, turkey feathers cost 5 gold, and goose feathers cost 3 gold. For the
shaft, the price depends on the length: 0.05 gold per centimeter.
Objectives:
• Define a new Arrow class with fields for arrowhead type, fletching type, and length. ( Hint:
arrowhead types and fletching types might be good enumerations.)
enumerations.)

Allow a user to pick the arrowhead, fletching type, and length and then create a new Arrow instance.
• Add a GetCost method that returns its cost as a float based on the numbers above, and use this
to display the arrow’s cost.
LEVEL 19
INFORMATION HIDING

󰁓peedrun
• Information hiding is where some details are hidden from the outside world while still presenting a
public boundary that the outside
outs ide world can still interact with.
• Class members should be marked public or private to indicate which of the two is intended.
• Data (fields) should be private in nearly all cases.
• Abstraction: when things are private, they can change without affecting the outside world. The
outside world depends on the public parts, while anything private can change without problems.
• A third level is internal, which is meant to be used only inside the project.
• Classes and other types also have an accessibility level: public class X { ... }

This level covers the next two fundamental concepts of object-oriented programming:
information hiding and abstraction.
We just saw that with encapsulation, an object
objec t could be responsible for a part of the system,
containing its own data in special variables called fields, and provide its own list of abilities in
the form of methods.
me thods.
Our second principle is a simple extension of encapsulation (treated as the same by some):
Object-Oriented Principle #2: Information Hiding—Only the object itself should directly
access its data.
To illustrate why this matters, consider the code below:
class Rectangle
{
public float _width;
public float _height;
public float _area;

public Rectangle(float width, float height, float area)


{
_width = width;
_height = height;
_area = area;

156 LEVEL 19 INFORMATION HIDING


}
}
This is a good beginning, but there is a problem brewing. A rectangle s area is defined as its
width and height multiplied together. A rectangle with a length of 1 and a nd a height of 1 has an
area of 1. A rectangle with a length of 2 and a height of 3 has an area of 6. However, our current
definition of Rectangle could allow this:
Rectangle rectangle = new Rectangle(2, 3, 200000);

kind of rule? Removing the area parameter from


Wouldn’t it be nice if we could enforce this kind
the constructor
weeks when we and computing
forget thefrom
the details)
details) areaaccidentally
instead prevents somebody
supplying a
an (including
n illogical area.ourselves in 3
public Rectangle(float width, float height)
{
_width = width;
_height = height;
_area = width * height;
}

This ensures new rectangles always start with the correct area. But we still have a problem:
problem :
Rectangle rectangle = new Rectangle(2, 3);
rectangle._area = 200000;
Console.WriteLine(rectangle._area);

While the area is initially computed correctly, this code does not stop somebody from
accidentally or intentionally changing the area. The outside world can reach in and mess with
the rectangle’s
rectangle’s data in ways that shouldn’t be allowed.
If the Rectangle class could keep its data hidden, the outside world could not put
Rectangle instances into illogical or inconsistent states. Of course, the outside world will
sometimes want to know about the rectangle’s current size and area and may want to change
its size. But all of that can be carefully protected through methods.

THE PUBLIC AND PRIVATE ACCE󰁓󰁓IBILITY MODIFIER󰁓


When we started making classes in the previous level, we slapped a public on all our fields
and methods. This is the root of our information hiding problem because it makes it so the
outside world can reach it.
Every member of a class—fields and methods alike—has an accessibility level. This level
determines where the thing is accessible from. The public keyword gives the member public
accessibility—usable anywhere. Instead of public, we could use private, which gives the
member private accessibility—usable only within the class itself. The public and private
keywords are both called accessibility modifiers because they change the accessibility
acce ssibility level of
the thing they are applied to. If we make our fields private, then the outside world cannot
directly interfere with them:
class Rectangle
{
private float _width;
private float _height;
private float _area;

THE PUBLIC AND PRIVATE ACCESSIBI


ACCESSIBILITY
LITY MODIFIERS 157
public Rectangle(float width, float height)
{
_width = width;
_height = height;
_area = width * height;
}
}

Our data is now private. We can still use the fields inside the class as the constructor does to
initialize them. But making them private ensures the outside world cannot change the area
and create an inconsistent rectangle.
But now we have the opposite problem.
p roblem. We’ve
We’ve sealed off all access to those fields. The outside
world will
will want some visibility and perhaps some control over the rectangle. With all our fields
fiel ds
marked private, we can no longer even do this:
Rectangle rectangle = new Rectangle(2, 3);
Console.WriteLine(rectangle._area); // DOESN'T COMPILE!

Since the outside world needs


ne eds to know the rectangle’s area, does that mean we must make the
field public anyway? In general, no. Instead of allowing direct access to our fields, we provide
controlled access through methods. For example, the outside world will want to know the
rectangle’ss width, height, and area. So we add these methods to the Rectangle class:
rectangle’
public float GetWidth() => _width;
public float GetHeight() => _height;

public float GetArea() => _area;


The fields stay private, and the outside world can still get their questions answered without
having unfettered access to the data.
If the outside world also needs to change the rectangle’s dimensions, we can also solve that
with methods:
public void SetWidth(float value)
{
_width = value;
_area = _width * _height;
}

public void SetHeight(float value)


{
_height = value;
_area = _width * _height;
}

We’ve decided it is reasonable to ask a rectangle to update its width and height and added
We’ve
methods for those. But we’ve decided we don’t want to let people directly change the area, so
we skip that
that one.
I intentionally chose names that start with Get and Set. Methods that retrieve a field’s current
value are called getters. Methods that assign a new value to a field are called setters. The above
code shows that these methods allow us to perform more than just setting a new value for the
field. Both SetWidth and SetHeight update the rectangle’s area to ensure it stays
consistent with its width and height.

fol lowing Rectangle class:


These changes give us the following

158 LEVEL 19 INFORMATION HIDING


class Rectangle
{
private float _width;
private float _height;
private float _area;

public Rectangle(float width, float height)


{
_width = width;
_height = height;
_area = _width * _height;
}

public float GetWidth() => _width;


public float GetHeight() => _height;
public float GetArea() => _area;

public void SetWidth(float value)


{
_width = value;
_area = _width * _height;
}

public void SetHeight(float value)


{
_height = value;
_area = _width * _height;
}
}

With these changes, if we want to create a rectangle and change its size, we use the new
methods instead of directly accessing its fields:
Rectangle rectangle = new Rectangle(2, 3);
rectangle.SetWidth(3);
Console.WriteLine(rectangle.GetArea());

Information hiding allows an object to protect its data. Each object is its own gatekeeper. If
another object wants to see what state the object is in or change its state, it must request that
information by calling a getter or setter method, rather than just reaching in and grabbing it.
This way, objects can enforce rules about their data, as we see here with the rules around a
rectangle’s area.
As written above, information hiding came at the cost of substantially
substantially more complex code—
the statement rectangle.SetWidth(3); is harder to understand than rectangle.
_width = 3;. Even if this were the end of the story, the benefits of information hiding would
outweigh the added complexity costs. But it isn’t the end of the story; we will see a better way
to do this kind of stuff in Level 20. This solution is just a temporary one.
What if you don’t have
have any rules to enforce? Is it okay to use public fields then? The principle
principle
of information hiding will nearly always prevent more pain than it causes. Even if you don’t
have any rules to enforce now, they usually arise as the program grows, and there are often
more rules to enforce than might appear at first glance. For example, should our Rectangle
class allow negative widths and heights? Arguably, that shouldn’t be allowed, and our setter
methods should check for it. But it is a guideline,
g uideline, and there are (rare!) exceptions.

ABSTRACTION 159

The Default Accessibility Level is private


While we have intentionally put public or private on all our class members, this is not
strictly necessary. We could leave it off entirely. If you don’t specify an accessibility level,
members of a class will be private.
In most cases, I recommend that you don’t leave off the accessibility level; always put either
public or private (or one of the other levels that we will learn later)
l ater) on each class member.
member.
This forces you to think through how accessible the member ought to be. That exercise is
worth the time it takes.
takes.

When to Use private and public


Two rules of thumb give us clues about whether to make things private or public.
The first, which we touched on earlier, is that a class should protect its data. Fields should
almost always be private. There are exceptions, but these are rare.
The second is that things should always be as inaccessible as possible while allowing the class
to fulfill its role in the system. For example, you could say that the getters and setters in our
most recent Rectangle class definition are part of the job of representing a rectangle. So it is
reasonable for each of those to be public. But three different times, we had a line of code
that looked like _area = _width * _height;. We could make a method called
UpdateArea() that contains this logic and then call it in three different spots (the
constructor, SetWidth, and SetHeight). Should UpdateArea be private or public?
Updating the area is not something the outside world should have to request specifically. It is
details of how we have created the Rectangle class. Since the outside world doesn’t need to
do it, this new method would probably be better as a private
pr ivate method.
Sometimes, you’ll get the accessibility level wrong. That’s part of making software.
Fortunately, you can change it later. But it is easier to take something private and make it
public than the reverse. The outside world may already be using something initially made
public, and you’d have to eliminate those.

Accessibility Levels as Guidelines, Not Laws


When you make something private, it does not mean the outside world has no possible way
to use the code. It just means the compiler is enlisted to help ensure things intended to be kept

hidden don’t accidentally


private members. Howeverget
However, used.
, there areItways
creates
to getcompiler
g et errors
around this; when you
somebody attempt
creative toreckless
and misuse
enough can skirt the protections the compiler provides. (Reflection, described briefly in Level
47, is one such way.) It will ensure you don’t accidentally shoot yourself in the foot but can’t
stop you from doing so intentionally.

AB󰁓TRACTION
A magical thing happens when the principles of encapsulation and information hiding are
followed. The inner workings of a class are not visible to the outside world. It is like a cell
phone’s insides: as long as the phone’s buttons and screen work, we don’t care how the
circuitry on the inside works. The human body is like this, as well. We don’t need to know how
the nerves and tendons connect, as long as things are working correctly.

160 LEVEL 19 INFORMATION HIDING


With the clear boundary provided by encapsulation and the inner workings kept secret
through information hiding, those inner workings can change entirely without any visible
effect on the outside world. This ability is called abstraction and is our third fundamental
principle of object-oriented
obje ct-oriented programming:
Object-Oriented Principle #3: Abstraction—The outside world does not need to know
each object or class’s inner workings and can deal with it as an abstract concept.
Abstraction allows the inner workings to change without affecting
affecting the outside world.
That doesn’t mean you can’t poke around and see how things work on the inside. Curious
minds will always do that. But if a class correctly does the job it advertises through its public
members, you can put the details of how it works out of your mind. It also provides isolation
from the rest of the world when working on the inside of a class.
c lass. You
You can change anything you
want that
that doesn’t affect the public boundary,
boundary, and the rrest
est of the program
program won’t be affected by
it. You can swap out a battery in a cell phone or put artificial valves in the human heart, and
the outside world won’t be affected by it. Abstraction is essential in breaking down big
problems into smaller ones because you can work on each part in isolation. You You don’t have to
remember how every aspect of the entire program works to do anything. Once a class has been
created, you can quit worrying about its details and use it as a cohesive whole.
Let’s illustrate with an example. Earlier versions of our Rectangle class had a field for the
rectangle’ss area, which got updated any time the width or he
rectangle’ height
ight changed. But we can change
this to compute the area as needed and ditch the field without affecting the rest of our
program:
class Rectangle
{
private float _width;
private float _height;

public Rectangle(float width, float height)


{
_width = width;
_height = height;
}

public float GetWidth() => _width;


public float GetHeight() => _height;
public float GetArea() => _width * _height;

public void SetWidth(float value) => _width = value;


public void SetHeight(float value) => _height = value;
}

The _area field is gone, and SetWidth, SetHeight, and the constructor no longer
calculate the area. Instead, it is calculated on demand when somebody asks for the area via
GetArea. The outside world is oblivious to this change.
c hange. They used to retrieve the rectangle’s
area through GetArea and still do.
Abstraction is a vital ingredient in building larger programs.
programs. It lets you make
make one piece
piece of your
program at a time without having to remember every detail as you go.

TYPE AYYou
CCE󰁓󰁓IBILITY LEVEL󰁓
ou can (and usually AND
should)
should THEaccessibility
) place MODIFIER
INTERNAL levels on the ttypes
ypes you define:

TYPE ACCESSIB
ACCESSIBILITY
ILITY LEVELS AND THE INTERNAL MODIFIER 161
public class Rectangle
{
// ...
}

For type definitions like this, private is not meaningful and is not allowed. It limits usage to
just within the class, so it doesn’t
doesn’t make sense to apply it to the whole class.
You might think that leaves public as the only option, but there is another: internal.
You
Initially, you won’t see many differences between public and internal. The difference is
that things made public can be accessed everywhere, including in other projects, while
internal can only be used in the project it is defined in. Consider, for example, all of the
code in .NET’s Base Class Library, like Console and Convert. That code is meant to be
reused everywhere. Console and Convert are both public.
If you make a new type (class, enumeration, etc.) and feel that its role is a supporting role—
details that help other classes accomplish their job, but not something you would want the
outside world to know exists—you might choose to make this type internal.
Right now, we are building self-contained
sel f-contained programs. W
Wee haven’t made anything that we would
expect other projects to reuse. You might be thinking, “I don’t expect any of this to be reused
by myself or anyone else. Why should I make anything public?”
Indeed, that is a legitimate thought process, and some would argue for making everything
internal until you create something you specifically intend to reuse. It is a reasonable
approach, and you can use it if you choose. But most C# programmers follow a somewhat
different thought process.
There are three levels of share/don’t-shar
share/don’t-sharee decisions to make. (1) Do I share a project or not?
(2) Should this individual type definition be shared or not? (3) Should this member—a field
or a method—be shared or not? C# programmers usually consider these different levels in
isolation. Suppose you are deciding whether to make something public, internal, or
private. You assume that its container is as broadly available as possible and say, “If this
thing’s container were useable anywhere, how available should this specific item be?” For a
class, you would say, “If this project were available to anybody, would I want them to be able
to reuse this class? Or is this a secret detail that
t hat I’d
I’d want to reserve the right to change
c hange without
affecting anybody?” For a method, “If this class were public, would I want this method to be
public, or is this something I want to make less accessible so that I can change it more easily
later?”
This second approach is more nuanced. It leads to more accessible things in less accessible
things—a public class in a project you are not sharing, a public method in an internal
class, etc. But it allows every accessibility decision to be made independent of every other
accessibility decision. If you change a class from internal tto
o public or vice versa, you don’t
need to reconsider which of its members should also change with it. The same is true if you
decide to start or stop sharing the project as a whole.
This second approach leads to most types being public, many methods being public, and
nearly all fields being private, with only a handful of internal types and methods, even
for a project that is never reused.
I’m bringing up internal here because it is the default accessibility level for a type if none
is explicitly written out. My advice is to always write out your intended acc
accessibility
essibility level rather
than leave it to the defaults. It forces you to build the habit of conscientiously deciding

162 LEVEL 19 INFORMATION HIDING


accessibility levels instead of leaving it to coincidence. If you decide you prefer using the
default in a few months, you can quit writing it out explicitly.
By the way, while type definitions must be either public or internal, members of a class
can be public, private, or internal. (For an enumeration, members are automatically
public, and you cannot change that.)
The compiler ensures that you cohesively use accessibility levels and flags inconsistencies as
compiler errors. For example, if you have a public class with a public method whose return
type is an internal class, the compiler will report it as an error. This method would
inadvertently publicly leak something you indicated should be internal.

Challenge
Challe nge Vin’s Trouble 50 XP
“Master Programmer!” Vin Fletcher shouts at you as he races to catch up to you. “I have a problem. I
created an arrow for a young man who took it and changed its length to be half as long as I had designed.
It no longer fit in his bow correctly and misfired. It sliced his hand pretty bad. He’ll survive, but is there
any way we can make sure somebody doesn’t change an arrow’s length when they walk away from my
shop? I don’t want to be the cause
caus e of such self-inflicted
self-inflicte d pain.”
pain.” With your knowledge of information hiding,
you know you can help.
Objectives:
• Modify your Arrow class to have private instead of public fields.
• Add in getter methods for each of the fields that you have.
LEVEL 20
PROPERTIE󰁓

󰁓peedrun
• Properties give you field-like access while still protecting data with methods: public float
Width { get => width; set => width = value; }. To use a property: rectangle.Width
= 3;
• Auto-properties are for when no extra logic is needed: public float Width { get; set; }
• Properties can be read-only, only settable in a constructor: public float Width { get; }
• read-only: private readonly float _width = 3;
Fields can also be read-only:
• With properties, objects can be initialized using object initializer syntax: new Rectangle() {
Width = 2, Height = 3 } .
• An init accessor is like a setter but only usable in object initializer syntax. public float Width
{ get; init; }

THE BA󰁓IC󰁓 OF PROPERTIE󰁓


While information hiding
hiding has significant
significant benefits, it adds
adds complexity to our code.
code. Instead of a
simple class with three public fields and a constructor
c onstructor,, we ended up with this:
public class Rectangle
{
private float _width;
private float _height;

public Rectangle(float width, float height)


{
_width = width;
_height = height;
}

public float GetWidth() => _width;


public float GetHeight() => _height;
public float GetArea() => _width * _height;

public void SetWidth(float value) => _width = value;

164 LEVEL 20 PROPERTIES


public void SetHeight(float value) => _height = value;
}
instead of rectangle._wi
And instead rectangle._width
dth = 3; we ended up with rectangle.SetWidth(3); .
But we had rules we needed to enforce and wanted to preserve the benefits of abstraction to
change the inner workings without affecting anything else. Those two things pushed us to this
more complex version of the code.
But in C#, there is a tool we can use to get the benefits of both information hiding and
abstraction while keeping our code simple: properties. A property pairs a getter and setter
under a shared name with field-like access.
Consider the three elements that dealt with the rectangle’s
rectangle’s width above:
private float _width;

public float GetWidth() => _width;

public void SetWidth(float value) => _width = value;

To swap this out for a property, we would write the following code:
private float _width;

public float Width


{
get
set => _width
_width;= value;
}

This defines a property with the name Width whose type is float. Properties are another
type of member that we can put in a class. They have their own accessibility level. I made this
one public since the equivalent methods, GetWidth and SetWidth were public. Each
property has a type. This one uses float. After modifiers and the type is the name ( Width).
Note the capitalization. It is typical to use UpperCamelCase for property names.
The body of a property is defined with a set of curly braces. Inside tha
that,
t, you can define a getter
(with the get keyword) and a setter (with the set keyword), each with its own body. The
above code used expression bodies, but you can also use block bodies for either or both :
public float Width
{
get
{
return _width;
}
set
{
_width = value;
}
}

In this case, the expression body is simpler. In other situations, you’


you’ll
ll need a block body.
The getter is required to return a value of the same type as the property ( float). The setter
has access to the special value variable in its body. We didn’t define a value parameter, but
in essence, one automatically exists in a property setter.

THE BASICS OF PROPERTIES 165


Many properties provide logic around accessing a single field, as the Width does with
_width. In these cases, the field is called the property’s
property’s backing field or backing store. In most
situations, the property and its backing field share the same name, aside from underscores
and capitalization, which helps you track which property is tied to which field.
Properties do not require both getters and setters.
s etters. You can have a get-only property or a set-
You
only property. A get-only property makes sense for something that can’t be changed from the
outside. The rectangle’s area is like this. We could make a get-only property for it:
public float Area
{
get => _width * _height;
}

If a property is get-only and the getter has an expression body, we can simplify it further
further::
public float Area => _width * _height;

p roperty-based Rectangle class might look like this:


Thus, the first stab at a property-based
public class Rectangle
{
private float _width;
private float _height;

public Rectangle(float width, float height)


{ _width = width;
_height = height;
}

public float Width


{
get => _width;
set => _width = value;
}

public float Height


{
get => _height;
set => _height = value;
}

public float Area => _width * _height;


}

The most significant benefit comes in the outside world, which now has field-like access to
the properties instead of method-like access:
Rectangle r = new Rectangle(2, 3);
r.Width = 5;
Console.WriteLine($"A {r.Width}x{r.Height} rectangle has an area of {r.Area}.");

In the code above, the line r.Width = 5; will call the Width property’s setter, and the
special value variable will be 5 when the setter code runs.
On the final line, referencing the Width, Height, and Area properties will call the getters for
each of those properties.

166 LEVEL 20 PROPERTIES


Our code can use clean, simple syntax without giving up information hiding and abstraction!
A property s getter and setter do not need to have
have the same accessibility level. Either getter or
setter can reduce accessibility from what the property has. If we want the property to have a
public getter and a private setter, we could do this:
public float Width
{
get => _width;
private set => _width = value;
}

AUTO-IMPLEMENTED PROPERTIE󰁓
Some properties will have complex logic for its getter, setter, or both. But others do not need
anything fancy and end up looking like this:
public class Player
{
private string _name;

public string Name


{
get => _name;
set => _name = value;

} }

Because these are commonplace, there is a concise way to define properties of this nature
called an auto-implemented property or an auto property:
public class Player
{
public string Name { get; set; }
}

Y
You
ou don’t define bodies for either getter or setter,
setter, and you don’t even define
define the backing field.
Y
You
ou just end the getter and setter with
with a semicolon. The compiler will generate a backing
backing field
for this property and create a basic getter and
an d setter method around it.
The backing field is no longer directly accessible in your code, but that’s rarely an issue.
However,, one problematic place is initializing the backing field to a specific starting value. We
However
can still solve that with an auto-property like this:
public string Name { get; set; } = "Player";

Don’t forget the semicolon at the end of the line!


l ine! It won’t compile if you forget it.
A version of the Rectangle class that uses auto-properties might look like this:
public class Rectangle // Note how short this code got with auto-properties.
{
public float Width { get; set; }
public float Height { get; set; }
public float Area
Area => Width * Height;

public
{ Rectangle(float width, float height)
Width = width;

IMMUTABLE FIELDS AND PROPERTIES 167


Height = height;
}
}

IMMUTABLE FIELD󰁓 AND PROPERTIE󰁓


Auto-properties can be get-only, like a regular property. (They cannot be set
Auto-properties set-only;
-only; there is no
scenario where that is useful as it would be a black hole for data.) This makes the property
immutable, “im-” meaning “not” and “mutable,”
“mutable,” meaning changeable. When a property is get-

only, it canreferred
sometimes still be to
assigned values,
as read-only but only
properties fromawithin
. When a constructor.
property is immutable,These are also
its behavior is
like concrete or a tattoo. You have complete control when the object is being created, but it
cannot be changed again once the object is created.
Consider this version of the Player class, which has made Name immutable:
public class Player
{
public string Name { get; } = "Player 1";

public Player(string name)


{
Name = name;
}
}

The getter is public, so we can always retrieve Name’s current value. And even without a setter,
assign a value to Name in an initializer or constructor.
we can still assign constructor. But after creation, we cannot
change Name from inside or outside the class.
While this sounds restrictive,
restrictive, there
there are many benefits to immutability.
immutability. For example,
example, we spent
a lot of time worrying about our Rectangle class’
class’ss area becoming inconsistent with its width
and height. If we made all of Rectangle’s properties immutable and only gave them values
in the constructor, there would be no possible way for the data to become inconsistent
afterward.
If immutable properties are beneficial, what about fields? If you have a field that you don’t
change after construction, you can apply the readonly keyword to it as a modifier:
want to change

public class Player


{
private readonly string _name;

public Player(string name)


{
_name = name;
}
}

Like immutable properties, this can be assigned a value inline as an initializer or in a


constructor, but nowhere else.
class’s properties and fields are immutable (get-only auto-properties and
When all of a class’s
readonly fields), the entire object is immutable. Not every object should be made
immutable. But when they can be, they are much easier to work with because you know the
object cannot change.

168 LEVEL 20 PROPERTIES

OBJECT INITIALIZER 󰁓YNTAX AND INIT PROPERTIE󰁓


While constructors should get the object into a good starting state, some initialization is best
done immediately after the object is constructed, changing the values of a handful of
properties right after construction. It is like making some final adjustments as the concrete
c oncrete is
still drying. Let’s say we have this Circle class:
public class Circle
{
public float X { get; set; } = 0; // The x-coordinate of the circle's center.
public float Y { get; set; } = 0; // The y-coordinate of the circle's center.
public float Radius { get; set; } = 0;
}

With this definition,


definition, we could make a new circle
circle and set its properties like this:
Circle circle = new Circle();
circle.Radius = 3;
circle.X = -4;

C# provides a simple syntax for setting properties right as the object is created called object
initializer syntax, shown below:
Circle circle = new Circle() { Radius = 3, X = -4 };

If the constructor is parameterless, you can even leave out the parentheses:
Circle circle = new Circle { Radius = 3, X = -4 };
Y ou cannot use object initializer syntax with properties that are get-only. While you can
You
assign a value to them in the constructor,
constructor, object initializer syntax comes after the constructor
finishes. This is a predicament because it would mean you must make your properties
mutable (have a setter) to use them in object initializer syntax, which is too much power in
some situations.
The middle ground is an init accessor. This is a setter that can be used in limited
circumstances, including with an inline initializer (the 0’s below) and in the constructor,
constructor, but
also in object initializer syntax:
public class Circle
{
public float
public float Y
X {
{ get;
get; init;
init; }
} =
= 0;
0;
public float Radius { get; init; } = 0;
}

Which can be used like


like this:
Circle circle = new Circle { X = 1, Y = 4, Radius = 3 };

// This would not compile if it were not a comment:


// circle.X = 2;

Challenge The Properties of Arrows


Arrows 100 XP
Vin Fletcher once again has run to catch up to you for help with his arrows. “My apologies, Programmer!

This
area.will
He be the last
doesn’t caretime I bother
for his you.makes
craft and My cousin,
wildlyFlynn Vetcher,
dangerous andisoverpriced
the only other arrow
arrows. Butmaker
peopleinkeep
the
buying them because they think my GetLength() methods are harder to work with than his public

ANONYMOUS TYPES 169


_length fields. I don’t want to give up the protections we just gave these arrows, but I remembered you
saying something about properties. Maybe you could use those to make my arrows easier to work with?
Objectives:
• Modify your Arrow class to use properties instead of GetX and SetX methods.
• Ensure the whole program can still run, and Vin can keep creating arrows with it.

ANONYMOU󰁓 TYPE󰁓
Using object initializer syntax and var, you can create new types that don’t even have a formal
name or definition—an anonymous type.
var anonymous = new { Name = "Steve", Age = 34 };
Console.WriteLine($"{anonymous.Name} is {anonymous.Age} years old.");

This code creates a new instance of an unnamed class with two get-only properties: Name
and Age. Since this type doesn’t have a name, you must use var. You can only use anonymous
types within a single method. You cannot use one as a parameter,
parameter, return type, or field.
Anonymous types have the occasional use but don’t underestimate
underestimate the value of just creating
a small, simple class for what you are doing (giving things a name is valuable) or using a tuple.
LEVEL 21
󰁓TATIC

󰁓peedrun
• Static things are owned by the type rather than a single instance (shared across all instances).
• Fields, methods, and constructors can all be static.

If a class is marked static, it can only contain static members (Console, Convert, Math).

󰁓TATIC MEMBER󰁓
By this point, you may have noticed an inconsistency. We have used Console, Convert, and
Math but have never done new Console(). We have used our own classesc lasses differently.
In C#, class members naturally belong to instances of the class.
cl ass. Consider this simple example:
public class SomeClass
{
private int _number;
public int Number => _number;
}
Each instance of SomeClass has its own _number field, and calling methods or properties
like the Number property is associated with specific instances and their individual data. Each
instance is independent of the others, other than sharing the same class definition.
But you can also mark members of a class with the static keyword to detach them from
individual instances and tie it to the class itself. In Visual Basic, the equivalent keyword is
Shared, which is a more intuitive name.
All member types that we have seen so far can be made static.

󰁓tatic Fields
By applying the static keyword to a field, you create a static field or static variable. These
are especially useful for defining variables that affect every instance in the class. For example,

STATIC
STATIC MEMBERS 171
we can add these two static fields that will help determine if a score is worth putting on the
high score table:
public class Score
{
private static readonly int PointThreshold = 1000;
private static readonly int LevelThreshold = 4;

// ...
}

Earlier, wethey
are static, sawtend
that to
C#be
programmers usually
UpperCamelCase name fields with _lowerCamelCase, but if they
instead.
These two fields are private and readonly, but we can use all the same modifiers on a
static field as a normal field. Occasionally,
Oc casionally, regular,
regular, non-static fields are referred to as instance
fields when you want to make a clear distinction.
Static fields are used within the class in the same way that you would use any other field:
public bool IsWorthyOfTheHighScoreTable()
{
if (Points < PointThreshold) return false;
if (Level < LevelThreshold) return false;
return true;
}

If a static field is public, it can be used outside the class through the class name
(Score.PointThreshold , for example).

Global 󰁓tate
Static fields have their uses, but a word of caution is in order
order.. If a field is static, public, and not
read-only, it creates global state. Global state is data that can be changed and used anywhere
in your program. Global state is considered dangerous because one part of your program can
affect other parts even though they seem unrelated to each other. Unexpected changes to
global state can lead to bugs that take a long time to figure out, and in most situations, you’re
better off not having it.
It is the combination that is dangerous. Making the field private instead of public limits

access
changetoover
justtime,
the class, which is
preventing easier
one partto
of manage.
the code Making the field readonly
from interfering ensures
with other parts. If ititis
can’t
not
static, only parts of the program that have a reference to the object will be able to do anything
with it. Just
Just be cautious any make a public static field.
any time you make

󰁓tatic Properties
Properties can also be made static. These can use static fields as their backing fields, or you
can make them auto-properties. These have the same global state issue that fields have, so be
careful with public static properties as well.
Below is the property version of those two thresholds that we made as fields
fiel ds earlier:
public class Score
{
public static int PointThreshold { get; } = 1000;
public static int LevelThreshold { get; } = 4;

172 LEVEL 21 STATIC


STATIC
// ...
}

We use static properties on the Console class. Console.ForegroundColor and


Console.Title are examples. Console.ForegroundColor is a good example of the
danger of global state. If one part of the code changes the color to red to display an error,
everything afterward will also be written in red until somebody changes it back.

󰁓tatic Methods
Methods can also
any non-static be static.
(instance) A static
fields, method is
properties, ornot tied to a single instance, so it cannot refer to
methods.
Static methods are most often used for utility or helper methods that provide some sort of
service related to the class they are placed in, but that isn’t tied directly to a single instance.
For example, the following method determines how many scores in an array belong to a
specific player:
public static int CountForPlayer(string playerName, Score[] scores)
{
int count = 0;
foreach (Score score in scores)
if (score.Name == playerName) count++;
return count;
}

This method would not make sense as an instance method because it is about many scores,
me thod in the Score class because it is closely
not a single one. But it makes sense as a static method closel y
tied to the Score concept.
Another common use of static methods is a factory method , which creates new instances for
the outside world as an alternative to calling a constructor.
constructor. For example, this method could be
a factory method in our Rectangle class:
public static Rectangle CreateSquare(float size) => new Rectangle(size, size);

This method can be called like this:


Rectangle rectangle = Rectangle.CreateSquare(2);

This
look code also illustrates
familiar; this is howhowwe’ve
to invoke static
been members
calling from
things Console.WriteLine
likeoutside the class. But it should
and
Convert.ToInt32 , which are also static methods.
me thods.

󰁓tatic Constructors
If a class has static fields or properties, you may need to run some logic to initialize them. To
address this, you could define a static constructor:
public class Score
{
public static readonly int PointThreshold;
public static readonly int LevelThreshold;

static Score()
{
PointThreshold = 1000;
LevelThreshold = 4;

STATIC
STATIC CLASSES 173
}
// ...
}

A static constructor cannot have parameters, nor can you call it directly. Instead, it runs
automatically the first time you use the class. Because of this, you cannot place an accessibility
modifier like public or private on it.

󰁓TATICSome
CLA󰁓󰁓E󰁓
classes are nothing more than a collection of related utility methods, fields, or
properties. Console, Convert, and Math are all examples of this. In these cases, you may
cla ss, which is done by marking it with the static
want to forbid creating instances of the class,
keyword:
public static class Utilities
{
public static int Helper1() => 4;
public static double HelperProperty => 4.0;
public static int AddNumbers(int a, int b) => a + b;
}

The compiler will ensure that you don’t accidentally add non-static members to a static class
and prevent new instances from being created with the new keyword. Because Console,
Convert, and Math are all static classes, we never needed—nor were we allowed—to make
an instance with the new keyword.

Challenge Arrow Facto


Factories
ries 100 XP
Vin Fletcher sometimes makes custom-ordered arrows, but these are rare. Most of the time, he sells one
of the following standard arrows:
• The Elite Arrow, made from a steel arrowhead, plastic fletching, and a 95 cm shaft.
• The Beginner Arrow, made from a wood arrowhead, goose feathers, and a 75 cm shaft.
• The Marksman Arrow, made from a steel arrowhead, goose feathers, and a 65 cm shaft.
You can make static methods to make these specific variations of arrows easy.
Objectives:
• Modify your Arrow class one final time to include static methods of the form public static
Arrow CreateEliteArrow()
CreateEliteArrow() { ... } for each of the three above arrow types.
• Modify the program to allow users to choose one of these pre-defined types or a custom arrow. If
they select one of the predefined styles, produce an Arrow instance using one of the new static
methods. If they choose to make a custom arrow, use your earlier code to get their custom data
about the desired arrow.
LEVEL 22
NULL REFERENCE󰁓

󰁓peedrun
• Reference types may contain a reference to nothing: null, representing a lack of an object.
• Carefully consider whether null makes sense as an option for a variable and program accordingly.
• Check for null with x == null, the null conditional operators x?.DoStuff() and x?[3], and use
?? to allow null values to fall back to some other default: x ?? "empty"

Reference type variables like string, arrays, and classes don’t store their data directly in the
variable. The variable holds a reference and the data lives on the heap
he ap somewhere. Most of
the time, these references point to a specific object, but in some cases, the reference is a
special one indicating the absence of a value. This special reference is called a null reference.
In code, you can indicate a null reference with the null keyword:
string name = null;

Null references are helpful when it is possible for there to be no data available for something.
Imagine making a game where you control a character that can climb into a vehicle and drive
it around. The vehicle may have a Character _driver field that can point out which
character is currently in the driver’s seat. The driver’s seat might be empty, which could be
represented by having _driver contain a null reference. null is the default value for
reference types.
But null values are not without consequences. Consider this code:
string name = null;
Console.WriteLine(name.Length);

This code will crash because it tries to get the Length on a non-existent string. Spotting this
flaw is easy because name is always null; it is less evident in other situations:
string name = Console.ReadLine(); // Can return null!
Console.WriteLine(name.Length);

Did ReadLine give us an actual string instance or null? You have probably not have
encountered it yet, but there are certain situations where ReadLine can return null. (Try

NULL OR NOT? 175


pressing Ctrl + Z when the computer is waiting for you to enter something.) The mere
possibility that it could be null requires us to proceed with caution.

NULL OR NOT?
For reference-typed variables, stop and think if null should be an option. If null is allowed,
you will want to check it for null before using its members (methods, properties, fields, etc.).
If null is not allowed, you will want to check any value you assign to it to ensure you don’t
accidentally assign null to it. We’ll see several ways to check for null in a moment.
After deciding if a variable should allow null, we want to indicate this decision in our code.
v ariable can either have a ? at the end or not. A ? means that it may
Any reference-typed variable
legitimately contain a null value. For example:
string? name = Console.ReadLine(); // Can return null!

In the code above, name’s type is now string?, which indicates it can contain any legitimate
string instance, but it may also be null. Without the ?, as we’ve done until now, we show
that null is not an option.
Until now, we’ve been ignoring the possibility of null. There’s
There’s even a good chance you’ve come
away unscathed. In all the code we’ve seen so far, the only real threat has been that
Console.ReadLine() might return null, and we haven’t been accounting for it. However,
you probably
we’ve usually haven’t been
taken our pressing
input Z, so it probably
Ctrl displayed
and either + Z, it directlyhasn’t come up.
or converted Even
it to if youtype,
another did,
and both Console.WriteLine and Convert.ToInt32 (and its other methods) safely
handle null.
But from now on, we’re far more likely to encounter problems related to null, so it’s time to
start being more careful and making an intentional choice for each reference-typed variable
about whether null should be allowed or not.
If we correctly apply (or skip) the ? to our variables, we’ll be able to get the compiler’
compiler ’s help to
check for null correctly.
cor rectly. This help is immensely valuable. It is easy to miss something on your
own. With the compiler helping you spot null-related issues, you won’t miss much. Of course,
the second benefit is that the code clearly shows whether null is a valid option for a variable.
That is helpful to programmers (including yourself ) who later look at your code.
Our examples have only used strings so far, but this applies to all reference types, including
arrays and any class you make. We could (and should!) do a similar thing for usages of our
Score and Rectangle classes.

Disabling Nullable Type Warnings


Annotating a variable with ? is a relatively new feature of C# (starting in C# 9). If you look at
older C# code (including most Internet code), you won’t see any ? symbols on reference-
typed variables. All reference-typed variables were assumed to allow null as an option, and
the compiler didn’t help you find places where null might cause problems.
I don’t recommend it, but if you want (or have a need) to go back to the old way, you can turn
this feature off. This article describes how: csharpplayersguide.com/articles/disable-null-
checking.

176 LEVEL 22 NULL REFER


REFERENCES
ENCES
CHECKING FOR NULL
Once you take null references into account, you’ll find yourself needing to check for null often.
The mechanics of checking for null is quite simple. The easiest way is to compare a reference
against the null literal, which is called a null check:
string? name = Console.ReadLine();
if (name != null)
Console.WriteLine("The name is not null.");

If a variable indicates that null is an option, you will want to do a null check before using its
members. If a variable indicates that null is not an option,
opt ion, you will want to do a null check on
any value you’re about to assign to the variable to ensure you don’t accidentally put a null in
it.
It is important to point out that, once compiled, there isn’t a difference between string? and
string. If you ignore the compiler warnings that are trying to help you get it right, even a
plain string (without the ?) can still technically hold a null value. Look for these compiler
warnings and fix them by adding appropriate
appropriate null checking or correctly marking a variable as
allowing or forbidding null.

Null-Conditional Operators: ?. and ?[]


One problem with null checking is that there may be implications down the line. For example:
ex ample:
private string? GetTopPlayerName()
{
return _scoreManager.GetScores()[0].Name;
}

_scoreManager could be null, GetScores() could return null, or the array could contain
a null reference at index 0. If any of those are null, it will crash. We need to check at each step:
private string? GetTopPlayerName()
{
if (_scoreManager == null) return null;

Score[]? scores = _scoreManager.GetScores();


if (scores == null) return null;

Score? topScore
if (topScore == = scores[0];
null) return null;

return topScore.Name;
}

The null checks make the code hard to read. They obscure the interesting parts.
There is another way: null-condition al operators. The ?. and ?[] operators can be used in
null-conditional
place of . and [] to simultaneously check for null and access the member:
private string? GetTopPlayerName()
{
return _scoreManager?.GetScores()?[0]?.Name;
}

Both ?. and ?[] evaluate the part before it to see if it is null.


null . If it is, then no further evaluation
happens, and the whole expression evaluates to null. If it is not null, evaluation will continue

CHECKING FOR NULL 177


as though it had been a normal . or [] operator. So if _scoreManager is null, then the above
code returns a null value without calling GetScores. If GetScores() returns null, the
above code returns a null without accessing index 0.
These operators do not cover every null-related scenario—you will sometimes need a good
old-fashioned if (x == null)—but they can be a simple solution in many scenarios.

The Null Coalescing Operator: ??


The null coalescing operator (??) is also a useful tool. It takes an expression that might be null
and provide a value or expression to use as a fallback if it is:
private string GetTopPlayerName() // No longer needs to allow nulls.
{
return _scoreManager?.GetScores()?[0]?.Name ?? "(not found)";
}

If the code before the ?? evaluates to null, then the fallback


fall back value of "(not found)" will be
used instead.
There is also a compound assignment operator for this:
private string GetTopPlayerName()
{
string? name = _scoreManager?.GetScores()?[0]?.Name;
name ??= "(not found)";
return name; // No compiler warning. `??=` ensures we have a real value.
}

The Null-Forgiving Operator: !


The compiler is pretty thorough in analyzing what can and can’t be null and giving you
appropriate warnings.
warnings. On infrequent occasions, you know something about the code that the
compiler simply can’t infer from
f rom its analysis. For example:
string message = MightReturnNullIfNegative(+10);

the return type of MightReturnNullIfNegative is string?, the compiler will


Assuming the
flag this as a situation where you are assigning a potentially null value to a variable that
indicates null isn’t allowed. But assuming the method name isn’t a lie (which isn’t always a
safe assumption), we know the returned value can’t be null.
To get rid of the compiler warning, we can use the null-forgiving operator: . (C# uses this
same symbol for the Boolean not operator, as we saw earlier in the book.) This operator tells
the compiler, “I know this looks like a potential null problem, but it won’t be. Trust me.”
me.”
Using it looks like this:
string message = MightReturnNullIfNegative(+10)!;

Y
You
ou place it at the end
en d of an expression that might be null to tell the compiler that it won’t
actually evaluate to null. With the in there,
there, the compiler warning
warning will go away.
away.
There’s a danger to this operator. You want to be sure you’re right. I’ve had times where I
thought the compiler was wrong, and I knew better, but after studying the code a bit more, I
realized the compiler was catching things
things I had missed.
missed. Use sparingly, but use it when
needed.
L EVEL
23
OBJECT-ORIENTED DE󰁓IGN

󰁓peedrun
• Object-oriented design is figuring out which objects should exist in your program, which classes
they belong to, what responsibilities each should have, and how they should work together.
• The design starts with identifying the requirements of what you are building.
• Noun extraction helps get the design process started by identifying conc
concepts
epts and jobs to do in the
requirements.
• CRC cards are a tool to think through a design with physical cards for each object, containing their
class, responsibilities, and collaborators.
• Object-oriented design is hard, but you don’t have to figure out the entire program all at once, nor
do you have to get it right the first time.

As we tackle larger problems, our solutions grow


g row in size as well. Objects allow us to take the
entire problem and break it into small pieces, where each piece—each object—has its job in
the overall system. Many objects—each doing their part and coordinating with the other
objects—allow us to solve the overall problem in small pieces.
Object-oriented design is the part of crafting
c rafting software
software where we decide:
• which objects should exist in our program,
program,
• the classes each of those objects belong to,
• what responsibilities
responsibilities each class or object should handle,
• when objects should come into existence,
existence,
• when objects should go out of existence,
• which objects must collaborate with or rely
rely upon which other objects,
• and how an object knows about the other objects it works with.

Object-oriented design
truly master. The focusisofa vast
this topic
book that deserves
is the its own book
C# programming (or ten) and
language, notcan take years to
object-oriented
design. Yet if you don’t know the basics of programming with objects (object-oriented

REQUIREMENTS 179
programming) and know how to structure your program to use them (object-oriented design),
you will have difficulty making large programs. YouYou won’t get all the bene
benefits
fits that come from
objects and classes in C#. While this level is not a complete guide, it is a starting point in that
journey.
Object-oriented design is sometimes referred to by the simpler terms software design or
design; you will see those terms used in this level and book to mean the same thing.
If there is one thing you should know about object-oriented design, it is that you are going to
get it wrong sometimes. Even after 15 years of professional programming, I still look around
after a few days or weeks of programming
p rogramming and realize I took the wrong path. The good news is
that software
software is soft;
soft ; it can always be changed. Unlike pouring concrete for a bridge, it is never
too late to switch a design in software. This softness provides a sense of safety and freedom.
Y
You
ou can never be irrevocably wrong with software.
software. You just
just need to be willing to recognize that
that
there might be a better path and be ready to change it.
Y
You
ou should also know that programs are not designed in a design Big Bang before typing out
a single line of code (aside from programs like Hello World). More experience may let you
work on larger chunks, but software is built a slice at a time and evolves as you create it. So
don’t fret over having to solve gigantic problems all at once; not
n ot even the pros do that.
As we go through this, we will use the classic game of Asteroids as an example. If you are not
familiar with this game, look it up online and play it for a bit. Playing the game will help the
examples in this level make more sense. We will be focusing on design elements,
e lements, not drawing
this game on the screen. (You could technically draw this in the console window, but that is
far from ideal.)

REQUIREMENT󰁓
The first step of building object-oriented systems is understanding what the software needs to
do. This is sometimes called requirements gathering, though that word has baggage. ToTo many
people, “gathering requirements”
requirements” means spending weeks rehashing the same dry, dusty Word
documents replete with proclamations like ”THE SOFTWARE SHALL THIS” and “THE
SOFTWARE SHALL THAT,” with far too much detail here, far too little detail there, and
conflicting details throughout. You may find yourself doing requirements this way someday,
but something much simpler is usually sufficient.

Things like homework


requirements assignmentsIn
in their descriptions. and challenges
other in this
cases, you maybook
havetypically
to hunt come
down with detailed
or invent the
requirements yourself. I recommend putting these requirements—what the software needs to
do—into words. Whether that is on paper,paper, whiteboard, or digital document, the act of writing
it out forces you to describe what you mean. Without this, the human brain likes to play this
trick on you where it says, “I know this,” and skips past the part where it proves that it knows
it. (Besides, if you are working with others, you will need to do this so that everybody is on the
same page.)
The simplest solution is to write out a sentence or two describing each feature. For example,
a couple of requirements for the game of Asteroids could be “Asteroids
“Asteroids drift through space at
some specific velocity,
veloc ity,” and “When a bullet hits an asteroid,
asteroid, the asteroid is removed from the
game.”

180 LEVEL 23 OBJECT-ORIENTED DESIGN


For some things, a picture or illustration is a better way to show intent, so don t be afraid to
sketch something out to support your short sentences. Quality doesn’t matter in this situation;
you do not need to be an artist.
artist.
Y
You
ou can also augment these short
short sentences with specific, concrete examples. Examples
Examples help
you discover details that might have otherwise been missed and help ensure everybody
understands things the same way. “An asteroid is at the coordinates (2, 4) with a velocity of
(-1, 0). After 1 second passes, its coordinates should be (1, 4
4).
).” Even this single example shows
that positions and velocities are measured in two directions (side-to-side and top-to-bottom)
and that velocities are measured in units per second.
Y
You
ou do not need to collect every
ever y single requirement before moving forward
for ward.. Software is best
built a little at a time because your plans for the software evolve as they come together. You
can sometimes benefit by having a long-term view of what might be needed later, but those
long-term plans nearly always change. (There are situations where change is rare and
knowing more details ahead of time is more beneficial. But these are rare.)

DE󰁓IGNING THE 󰁓OFTWARE


Once we have identified the next thing to build through writing, supported with pictures and
examples, we are ready to begin design. There are many ways to approach design. We will
touch on a few, though programmers use a wide variety of techniques. Find a system that
works well for you.
you.

Noun Extraction
A possible
possible first step is to identify the
the concepts and jobs that
that the requirements reveal. Concepts
that appear in the requirements will often lead to classes of objects in your design. Jobs or
tasks that appear in the requirements will often lead to responsibilities that your software
must be able to do. Some object in your design must eventually handle that responsibility.
responsibility.
Y
You
ou can start this process by highlighting
highlighting the nouns (and noun phrases) and verbs (and verb
phrases) that appear in the requirements. This is called noun extraction or noun and verb
extraction. It can be a good first step, but it is not magic. Not all nouns deserve to be classes in
our program and not every important concept is explicitly stated in our requirements. It
usually involves more work to discover which concepts and tasks are a re involved. But if you miss
something, you can always change it later.
later.
Let’s look closely at this requirement: Asteroids drift through space at some specific velocity.
The nouns asteroid , space, and velocity are all potential concepts
conc epts that we may or may not make
classes around, and the verb drift is a job that some object (or several objects) in our system
will need to do. We may have some thoughts on how we could start designing our program
from this.
While we may use noun extraction
e xtraction (or the other tools described here)
he re) to come up with the
beginnings of a potential design—a guess
gu ess about the design—you are not done designin
designingg until
you have code that solves the
the problem and whose structure is is something you
you can live with.
with. In
that sense, the code itself is the only accurate representation of your design. But most
programmers will begin exploring design options in lighter weight and more flexible tools
than actual code, such as a whiteboard or pen and paper.
paper. With a whiteboard or pen and paper,
change is trivial.

DESIGNING THE SOFTWARE 181


UML
Before moving on to the tool we will spend most of our time on, I must mention another.
another. There
is a very formal diagramming tool called the Unified Modeling Language, or UML. Many
programmers around the world use this, and it is helpful to know it. However, it is a
complicated system that is not ideal for new programmers. It is complex enough that even
many experienced programmers prefer simpler tools when discussing design possibilities. I
mention this so that you are aware
aware of a tool that most developers know of and that many use.
The technique we will see below (CRC cards) is far less formal and much lighter. I find it a
helpful tool for people beginning with software design while still being meaningful for
experienced object-oriented designers. But my experience has been that more programmers
know about UML than CRC cards.
c ards.

CRC Cards
CRC cards are a way to think through potential object-oriented designs and flesh out some
detail. It helps you figure out which objects should exist, what parts of the overall problem
each object should solve, and how they should work together. The short description of CRC
cards is that you get a stack of blank 3 x5 cards (or something similar) and create one card c ard per
object in your system. On each card, you will list three things: (1) the class that the object
belongs to, written at the top, (2) the responsibilities that the object has in a list on the left side,
and (3) the object’s collaborators—other objects that help the object fulfill its responsibilities.
CRC is short for Class-Responsibility-Collaborator
Class-Responsibility-Collaborator.. A sample CRC card might look like this:

Class names should be nouns or noun phrases. A good name gives you and others a simple
way to refer
refer to each type of object and is worth spendi
spending ng some time identifying a good name
name..
Each responsibility should be listed as a verb or verb phrase. If you run out of space on a card,c ard,
you are probably asking it tto o do
do too much. Split its responsibilities
responsibilities into other cards
cards and objects.
A responsibility can be a thing to know or a thing to do. However,However, you should describe
desc ribe what
the job is, not how to do it. Remember that each object needs the capacity to fulfill its
responsibilities.
responsibi lities. It will need to know the data for its job,
j ob, be handed the data in a m
method
ethod call,
or ask its collaborators for it.
The collaborators of an object are the names of other classes that this object needs
ne eds to fulfill its
responsibilities. You could also use the word “helpers” here if you like that better. Just because
one object uses another as a collaborator
coll aborator does not require that the relationship go both ways.
One object can rely on another without the second object even knowing about the first.

Making
you will CRC
need.cards usually
You then
You walkstarts with different
through
through the parts “what
you know
“wha the best—the
t if” scenarios
scenar most
ios and talk throbvious
through
ough howobjects
your

182 LEVEL 23 OBJECT-ORIENTED DESIGN


objects might work together to solve the problem. Eventually, you will discover a
responsibility that
that no current card has listed. You must either add it to an existing card or make
a new card with a new class
c lass to add it to, growing your collection of cards. As you walk through
these “what if” scenarios, talking through how the objects may interact to complete the
scenario, you will often find yourself pointing to cards (or picking them up and holding them)
as you follow the flow of execution from object to object.
Let’s walk through an example. You start by gathering your supplies: cards, pens, you, your
teammates, the requirements,
requirements, and any code you already have written for reference. (There are
online CRC card creators as well, but I find paper or whiteboards far more flexible.) We begin
requirement that Asteroids drift through space at some specific velocity. The most
with the requirement
obvious thing here is the concept of an asteroid, so we start there. Suppose we start the game
with five asteroids.
asteroids. We
We might create five cards
cards and assign them to
to the Asteroid class.

I only wrote the responsibilities of asteroids on one card. The others would be the same. (I
might even just create a single Asteroid card and remember that it could represent many.)
But who is keeping track of these asteroids? Who knows that these all exist? That “space”
concept hints at this. These all exist
ex ist within the game itself. We need a card for that:

DESIGNING THE SOFTWARE 183


I have used the physical arrangement of these cards to reflect structure. I expected the
Asteroids Game object to own or manage the Asteroid objects, so I put it above them on the
table.
We still haven’t addressed the actual drifting of asteroids yet. We have not assigned that
responsibility to anybody. That responsibility needs to be given to either asteroids, the
asteroid game, or a new object. This might actually be two distinct responsibilities: knowing
when to update each asteroid
asteroid and knowing exactly how
how to update each asteroid.
asteroid.
One approach—let’s call it Option A—is to give the job of making an asteroid drift to asteroids

themselves.
responsibilityyThat
responsibilit feels appropriate
of knowing since
when to update it more
feels is changing
at home data
in thethe asteroid
Asteroids owns.
Game The
object:

But let’s consider more than one option. What else could
coul d we do? We could combine these two
related responsibilities into one and just have the Asteroids Game object do it. This is Option
B:

184 LEVEL 23 OBJECT-ORIENTED DESIGN


In this case, Asteroids Game would need to tell the asteroid of its new position as time passes.
The Asteroid objects end up with just data.
Option C would be to give this responsibility to another object that doesn’t exist yet. This
would be an object that does nothing but update
update asteroid
asteroid positions when the time is right.
right. I’m
going to call this the Asteroid Drifting System object. The Asteroids Game object would not
update asteroid positions directly but ask this system instead. But it still owns the
responsibility of knowing when the time is right:
right :

In this case, Asteroids Game periodically determines that it is time to run its systems and asks
the Asteroid Drifting System
System to do its job, which updates each asteroid.
At first,
first, this may seem like
like a more complex solution. But consider the
the future under a scenario
like this. We could make other systems to handle various game aspects. For example, in
Asteroids, the player’s ship eventually slows down to a stop because of drag. We could add a
Ship Drag System object to handle that. Asteroids that reach the edge of the world wrap
around to the other side. We could add a Wraparound System object for that. Most of the
game rules could be made as a system. This approach is close to an architecture sometimes
used in games
g ames called the Entity-Component-System
Entity-Component-System architecture.
architecture. It has some merit, but at this
point, it feels more complicated than our first two options.

Evaluating a Design
In truth, we could probably make any of the above designs work, and probably several dozen
other designs as well. But we do need to pick one to turn into code. How do we decide?
There are a lot of rules and guidelines that programmers will use to judge a design. We don’t
have time to cover them all here, but here are four of the m
most
ost basic, most important rules that
should give us a foundation.
Rule #1: It has to work. Look carefully at each design that you come up with. Does it do what
it was supposed to do? If not, it isn’t a useful design.
All three of our above
above options seem workable,
workable, so this rule does not eliminate
eliminate anything.

CREATING CODE 185


Rule #2: Prefer designs that convey meaning and intent. Programmers spend more time
reading code than writing it. When you come back and look at the classes, objects, and their
interactions in two weeks or two years, which of the choices
choic es will be most understandable?
To shed some light on how this might work, consider this question. You have a working
program handed to you (from your past self or another programmer), and you don’t know
how it works. But you need to make a tweak to how asteroids drift in space. Where do you
look? My first thought would be to look at the Asteroid class. Perhaps that is a hint that having
this logic live in the Asteroid class is better, which would give Option A an advantage.

Of the four
putting gamerules,
rulesthis
intoone is thethey’d
systems, mostd subjective.
they’ For example,
look for a drifting system.ifIfsomebody knew
this were the onewe
rulewere
not
done as a system, it would be hard to remember and understand. Conveying meaning and
intent is not always clear-cut.
Rule #3: Designs should not contain duplication. If one design contains the same logic or
data in more than one place, it is worse than one that does not. Anything you need to change
would have to be modified in many places
places instead of just one.
one.
I don’t think any of our options have this problem yet. But consider what things look like after
adding the rule that the player’s
player’s ship must also drift as asteroids do. A design that copies and
pastes the drifting logic to two things is objectively worse than one that only does it once. We
will learn some tools to help with
with that in Level 25.
Rule #4: Designs should not have unused or unnecessary elements. Make things as
streamlined and straightforward as you can. Designs that add in extra stuff “just in case” are
worse than ones that
that are as simple
simple as possible for the current situation.
situation.
There are a few rare counterexamples to that rule. You should only accept a more complex
design if you need the extra complexity in the immediate future. Most of the time, you can
count on the fact that you can always change software later and add in the extra parts when
you actually need it.
Option C might violate this rule with its additional object. That is the second time we have
found an issue with Option C.
All things totaled, Option A seems like it has the most going for it, and it is what I’ll turn into
code next.

CREATING CODE
The next step is to turn our design into working code. Remember: creating the actual code
may give us more information, and we may realize that our initial pick was not ideal. When
this happens, we should adapt and change our plan. Software is soft, after all. (Have I said that
enough yet?) Here is what I came up with:
AsteroidsGame game = new AsteroidsGame();
game.Run();

public class Asteroid


{
public float PositionX { get; private set; }
public float PositionY { get; private set; }
public float VelocityX { get; private set; }
public float VelocityY { get; private set; }

186 LEVEL 23 OBJECT-ORIENTED DESIGN


public Asteroid(float positionX, float positionY,
float velocityX, float velocityY)
{
PositionX = positionX;
PositionY = positionY;
VelocityX = velocityX;
VelocityY = velocityY;
}

public void Update()


{
PositionX
PositionY +=
+= VelocityX;
VelocityY;
}
}

public class AsteroidsGame


{
private Asteroid[] _asteroids;

public AsteroidsGame()
{
_asteroids = new Asteroid[5];
_asteroids[0] = new Asteroid(100, 200, -4, -2);
_asteroids[1] = new Asteroid(-50, 100, -1, +3);
_asteroids[2] = new Asteroid(0, 0, 2, 1);
_asteroids[3]
_asteroids[4] =
= new
new Asteroid(400,
Asteroid(200, -100,
-300, -3, -1);
0, 3);
}

public void Run()


{
while (true)
{
foreach (Asteroid asteroid in _asteroids)
asteroid.Update();
}
}
}

Even after making CRC cards, the act of turning something into code still requires a lot of

decision-making. CRC cards don’t capture every detail, just the big picture.
As you write the code, you will find other ways to
to improve
improve the design. For example, those four
properties on Asteroid are bothering me. Variables that begin or end the same way often
indicate that you may be missing a class of some sort. We could make a Coordinate or a
Velocity class with X and Y properties and simplify that to two properties.
p roperties. The X and Y parts
are closely tied together and make more sense as a single object.
A few loose ends in this code bother me, though we don’t have the tools to make it right (as I
see it) yet. Here are a few that stand out to me:
• I do not like that
t hat we hardcode the starting locations of those five asteroids. We would play
the same game every single time. In Level 32, we will learn about the Random class and
see how it can generate random numbers for something like this.

Array instances but


list of asteroids, keepwe
the same
will size once
e ventually
eventually be created. Right
adding and now, weasteroids
removing are okayfrom
to have
thealist.
fixe
fixedd
In

HOW TO COLLABORATE 187


Level 32, we will learn about the List class, which is better than arrays for changing
sizes.
• My other complaint is the while (true) loop. Until we have a way to win or lose the
game, looping forever is fine, but this loop
l oop updates asteroids as fast as humanly possible.
(As fast as computerly possible?) One pass leads right into the next. The
AsteroidsGame class has that responsibility, and it does the job; it just does it poorly.
To wait a while between iterations (Level 43) or allow the asteroids to know how much
time has passed and update it accordingly (Level
(L evel 32) would both be improvements.

HOW TO COLLABORATE
Objects collaborate by calling members (methods, properties, etc.) on the object they need
help from. Calling a method is straightforward. The tricky part is how does an object know
about its collaborators in the first place? There are a variety of ways this can happen.

Creating New Objects


The first way to get a reference to an object is by creating a new instance with the new keyword.
This is how the AsteroidsGame object gets a reference to the game’s asteroids in the code
above. These references to new Asteroid instances are put in an array and used later.

Constructor Parameters
A second way is to have something else hand it the reference when the object is created as a
constructor parameter. We could have passed the asteroids to the game through its
constructor like this:
public AsteroidsGame(Asteroid[] startingAsteroids)
{
_asteroids = startingAsteroids;
}

The main method, which creates our AsteroidsGame instance, would then make the game’s game’s
asteroids. Come to think of it, creating the initial list of asteroids is a responsibility we never
explicitly assigned to any object. I placed the asteroid creation in AsteroidsGame, but we
could have also given this responsibility to another class (maybe an AsteroidGenerator
class?). Passing
Passing in the object
objec t through a constructor parameter is a popular choice if an object
objec t
needs another object from the beginning but can’t or shouldn’t just use new to make a new
one.

Method Parameters
On the other hand, if an object only needs a reference to something for a single method, it can
be passed in as a method parameter.
parameter.
We did not end up implementing the design that used the AsteroidDriftingSystem
class. Had we done that, the game object might have given the asteroids to this object as a
method parameter:
public class AsteroidDriftingSystem
{
public void Update(Asteroid[] asteroids)
{
foreach (Asteroid asteroid in asteroids)

188 LEVEL 23 OBJECT-ORIENTED DESIGN


{
asteroid.PositionX += asteroid.VelocityX;
asteroid.PositionY += asteroid.VelocityY;
}
}
}

Asking Another Object


An object can also get a reference to a collaborator by asking a third object to supply the
reference. Let’s say that AsteroidsGame had a public Asteroids property that returned
the list of asteroids. The AsteroidDriftingSystem object could then take the game as a
parameter, instead of the asteroids, and ask the game to supply the list by calling its
Asteroids property:
public void Update(AsteroidsGame game)
{
foreach (Asteroid asteroid in game.Asteroids)
{
asteroid.PositionX += asteroid.VelocityX;
asteroid.PositionY += asteroid.VelocityY;
}
}

󰁓upplying the Reference via Property or Method


Suppose you can’t supply a reference to an object in the constructor but need it for more than
one method. Another option is to have the outside world supply the reference through a
property or method call and then save off the reference to a field for later use. The
AsteroidDriftingSystem could have done this like so:
public class AsteroidDriftingSystem
{
// Initialize this to an empty array, so we know it will never be null.
public Asteroid[] Asteroids { get; set; } = new Asteroid[0];

public void Update()


{
foreach (Asteroid asteroid in Asteroids)
{
asteroid.PositionX += asteroid.VelocityX;
asteroid.PositionY += asteroid.VelocityY;
}
}
}

Before this object’s Update method runs, the AsteroidsGame object must ensure this
property has been set. (Though it only needs
ne eds to be set onc e, not before every Update.)
once,

󰁓tatic Members
A final approach
approach would
would be to use a static
static property, method, or field. If
If it is public, these can be
be
reached from anywhere. For example, we could make this property in AsteroidsGame to
store the last created game:

public
{ class AsteroidsGame
public static AsteroidsGame Current { get; set; }

BABY STEPS 189


// ...
}

When the main method runs, it can assign a value to this:


AsteroidsGame.Current = new AsteroidsGame();
// ...

Then AsteroidDriftingSystem can access the game through the static property:
public void Update()

{ foreach (Asteroid asteroid in AsteroidsGame.Current.Asteroids)


{
asteroid.PositionX += asteroid.VelocityX;
asteroid.PositionY += asteroid.VelocityY;
}
}

In most circumstances, I recommend against this approach because it is global state (Level
21),, but it has its occasional use.
21)

Choices, Choices
Y
You
ou can see that there are many options for building an interconnected network of objects—
almost too many. But if we make the wrong choice, we can always go back and change it.

BABY 󰁓TEP󰁓
This level has been a flood of information
infor mation if you are new to object-oriented programming and
design. Just keep these things in mind:
Y
You
ou don’t have to get it right the first time.
time. It can always be changed. (Changing the structure
of your code without changing what it does is called refactoring.)
Y
You
ou do not have
have to come up with a design to solve everything all at once. Softwar
Softwaree is typically
built a little at a time, making one or several closely
closel y related requirements work before movi
moving
ng
on to the next. Following that model makes it so that no single design ccycle
ycle is too scary.
Don’t be afraid to dive in and
an d try stuff out. Your first few attempts may be rrough
ough or ugly. But if
you just start trying it and seeing what is working for you and what isn’t, your skills will grow
quickly. (Don’t worry, the whole next level
l evel will get you more practice with this stuff.)
LEVEL
THE CATACOMB󰁓 OF THE CLA󰁓󰁓
24
󰁓peedrun
This level is made entirely of problems to work through to gain more practice creating classes and doing
object-oriented design and culminates in building the game of Tic-Tac-Toe from scratch.

We now know the basics of programming in C# and have more than enough skills to begin
building interesting, complex programs with our knowledge. Before moving on to more
advanced parts of C#, let’s spend some time doing some challenges that will put our
knowledge and skills to the test. This level contains nine different challenges to test your skill.
The first five challenges involve designing and programming a single class (possibly with
some supporting enumerations and always with a main method that uses it).
The next three are object-oriented design challenges. You do not need to create a working
program on these. Indeed, we haven’t quite learned enough to do justice to some aspects of
these challenges. (Though by the time you finish this book, you should be able to do any of
these.) You
You only need to make an object-oriented
obje ct-oriented design that you think could work in the form
of CRC cards or some alternative that you feel comfortable
com fortable with.
The final challenge requires you to both design and program the game of Tic-Tac-Toe. This is
the most complex program we have made in our challenges. It will take some time to get it
right, but that is time well spent.
Remember that you can find my answers to these challenges on the book’s website.

THE FIVE PROTOTYPE󰁓


Narrative Entering the Catacombs
You arrive at the Catacombs of the Class, the place that will reveal the path to the Fountain of Objects.
The Catacombs lie inside a mountain, with a wide stone entrance leading you into a series of three
chambers. In the first chamber, you find five pedestals with the remnants of a class definition
definitio n and specific
instructions by each. Etched above a sealed doorway at the back of the room is the text, “Only the True

THE FIVE PROTOTYPES 191


Programmer who can remake the Five Prototypes can proceed.” Each pedestal appears to have
instructions for crafting a class. These are the Five Prototypes that you must reassemble.

Boss Battle The Point 75 XP


The first pedestal asks you to create a Point class to store a point in two dimensions. Each point is
represented by an x-coordinate (x), a side-to-side distance from a special central point called the origin,
and a y-coordinate (y), an up-and-down distance away from the origin.
Objectives:
• Define a new Point class with properties for X and Y.
• Add a constructor to create a point from a specific x- and y-coordinate.
• Add a parameterless constructor to create a point at the origin (0, 0).
• In your main method, create a point at (2, 3) and another at (-4, 0). Display these points on the
console window in the format (x, y) to illustrate that the class works.
• Answer this question: Are your X and Y properties immutable? Why did you choose what you did?

Boss Battle The Color 100 XP


The second pedestal asks you to create a Color class to represent a color. The pedestal includes an
etching of this diagram that illustrates its potential usage:

The color consists of three parts or channels: red, green, and blue, which indicate how much those
channels are lit up. Each channel can be from 0 to 255. 0 means completely off; 255 means completely
on.
The pedestal also includes some color names, with a set of numbers indicating their specific values for
each channel. These are commonly used colors: White (255, 255, 255), Black (0, 0, 0), Red (255, 0, 0),
Orange (255,165, 0), Yellow (255, 255, 0), Green (0, 128, 0), Blue (0, 0, 255), Purple (128, 0, 128).
Objectives:
• Define a new Color class with properties for its red, green, and blue channels.
• Add appropriate constructors that you feel make sense for creating new Color objects.
• Create static properties to define the eight commonly used colors for easy access.
• In your main method, make two Color-typed variables. Use a constructor to create a color instance
and use a static property for the other. Display each of their red, green, and blue channel values.

Boss Battle The Card 100 XP


The digital Realms of C# have playing cards like ours but with some differences. Each card has a color
(red, green, blue, yellow) and a rank (the numbers 1 through 10, followed by the symbols $, %, ^, and &).
The third pedestal requires that you create a class to represent a card of this nature.

192 LEVEL 24 THE CAT


CATACOMBS
ACOMBS OF THE CLASS
Objectives:
• Define enumerations for card colors and card ranks.
• Define a Card class to represent a card with a color and a rank, as described above.
• Add properties or methods that tell you if a card is a number or symbol card (the equivalent of a
face card).
• Create a main method that will create a card instance for the whole deck (every color with every
rank) and display each (for example, “The Red Ampersand” and “The Blue Seven”).

Answer
in this question:
the previous Why do you think we used a color enumeration here but m
challenge? made
ade a color class

Boss Battle The Locked Door 100 XP


The fourth pedestal demands constructing a door class with a locking mechanism that requires a unique
numeric code to unlock. You have done something similar before without using a class, but the locking
mechanism is new. The door should only unlock
un lock if the passcode is the right one. The following statements
describe how the door works.
• An open door can always be closed.
• A closed (but not locked) door can always be opened.

A closed door can always be locked.
• A locked door can be unlocked, but a numeric passcode is needed, and the door will only unlock if
the code supplied matches
m atches the door’s current passcode.
• When a door is created, it must be given an initial passcode.
• Additionally, you should be able to change the passcode by supplying the current code and a new
one. The passcode should only change if the correct, current code is given.
Objectives:
• Define a Door class that can keep track of whether it is locked, open, or closed.
• Make it so you can perform the four transitions defined above with methods.
• Build a constructor that requires the starting numeric passcode.

Build a method
current passcodethat
andwill
newallow you toOnly
passcode. change thethe
change passcode forifan
passcode theexisting
currentdoor by supplying
passcode the
is correct.
• us er for a starting passcode, then create a new Door instance. Allow
Make your main method ask the user
the user to attempt the four transitions described above (open, close, lock, unlock) and change the
code by typing in text commands.

Boss Battle The Password Validator 100 XP


The fifth and final pedestal describes a class that represents a concept more abstract than the first four:
a password validator. You must create a class that can determine if a password is valid (meets the rules
defined for a legitimate password). The pedestal initially doesn’t describe any rules, but as you brush the
dust off the pedestal, it vibrates for a moment, and the following rules appear:
• Passwords must be at least 6 letters long and no more than 13 letters long.
Passwords
• Passwords must contain at least one uppercase letter, one lowercase letter, and one number.

OBJECT-ORIENTED DESIGN 193


• Passwords cannot contain a capital T or an ampersand (&) because Ingelmar in IT has decreed it.
That last rule seems random, and you wonder if the pedestal is just tormenting you with obscure rules.
You ponder for a moment about how to decide if a character is uppercase, lowercase, or a number, but
while scratching your head, you notice a piece of folded parchment on the ground near your feet. You
pick it up, unfold it, and read it:
foreach with a string lets you get its characters!
> foreach (char letter in word) { ... }

char has static methods


> char.IsUpper('A'), to categorize letters!
char.IsLower('a'), char.IsDigit('0')

That might be useful information! You are grateful to whoever left it behind. It is signed simply “A.”
Objectives:
• Define a new PasswordValidator class that can be given a password and determine if the
password follows the rules above.
• Make your main method loop forever, asking for a password and reporting whether the password is
allowed using an instance of the PasswordValidator class.

OBJECT-ORIENTED DE󰁓IGN
Narrative The Chamber of Design
As you finish the final class and place its complete definition back on its pedestal, the writing on each
pedestal begins to glow a reddish-orange. A beam forms from each pedestal, extending upward towards
the high cavernous ceiling. Additional runes on the wall begin to shine as well, and the far walls slide
apart, revealing an opening further into the Catacombs.
You pass through to the next chamber and find three more pedestals with etched text. On the floor, in
a ring running around the three pedestals, lie the words, “Only a True Programmer can design a system
of objects for the ancient games of the people.”
You must make an object-oriented design (not a complete program) for each game described on the
three pedestals in the room’s center to continue further.
The following three challenges will help you practice object-oriented
obje ct-oriented design. You
You do not need
to make the full game! Yo You
u only need a starting point in the form of CRC cards (or a suitable
alternative). Some parts of these games might be tough to write code for, given our current
knowledge. For example, the Hangman game would be easier to read a list of words from a
file, a topic covered in Level 39.

Boss Battle Rock-Paper-󰁓cissors


Rock-Paper-󰁓cissors 150 XP
The first design pedestal requires you to provide an object-oriented design—a set of objects, classes,
and how they interact—for the game of Rock-Paper-Scissors, described below:
• Two human players compete against each other.
• Each player picks Rock, Paper, or Scissors.

194 LEVEL 24 THE CAT


CATACOMBS
ACOMBS OF THE CLASS
• Depending on the
t he players’ choices, a winner is determined: Rock be
beats
ats Scissors,
Scisso rs, Scissors beats Paper,
Paper beats Rock. If both players pick the same option, it is a draw.
• The game must display who won the round.
• The game will keep running rounds until the window is closed but must remember the historical
record of how many times each player won and how many draws there were.
Objectives:
• Use CRC cards (or a suitable alternative) to outline the objects and classes that may be needed to
make the game of Rock-Paper-Scissors. You do not need to create this full game; just come up
with a potential design as a starting point.

Boss Battle 15-Puzzle 150 XP


The second pedestal requires you to provide an object-oriented design for the game of 15-Puzzle.

The game of 15-Puzzle contains a set of numbered tiles on a board with a single open slot. The goal is to
rearrange the tiles to put the numbers in order, with the empty space in the bottom-right corner.
• The player needs to be able to manipulate the board to rearrange it.
• The current state of the game needs to be displayed to the user.
• The game needs to detect when it has been solved and tell the player they won.
• The game needs to be able to generate random puzzles to solve.
• The game needs to track and display how many moves the player has made.
Objectives:
• Use CRC cards (or a suitable alternative) to outline the objects and classes that may be needed to
make the game of 15-Puzzle. You do not need to create this full game; just come up with a
potential design as a starting point.
• Answer this question: Would your design need to change if we also wanted 3×3 or 5×5 boards?

Boss Battle Hangman 150 XP


The third pedestal in this room requires you to provide an object-oriented design for the game of
Hangman. In Hangman, the computer picks a random word for the player to guess. The player then
proceeds to guess the word by selecting letters from the alphabet, which get filled in, progressively
revealing the word. The player can only get so many letters wrong (a letter not found in the word) before
losing the game. An example run of this game could look like this:

TIC-TAC-TOE 195
Word: _ _ _ _ _ _ _ _ _ | Remaining: 5 | Incorrect: | Guess: e
Word: _ _ _ _ _ _ _ _ E | Remaining: 5 | Incorrect: | Guess: i
Word: I _ _ _ _ _ _ _ E | Remaining: 5 | Incorrect: | Guess: u
Word: I _ _ U _ _ _ _ E | Remaining: 5 | Incorrect: | Guess: o
Word: I _ _ U _ _ _ _ E | Remaining: 4 | Incorrect: O | Guess: a
Word: I _ _ U _ A _ _ E | Remaining: 4 | Incorrect: O | Guess: t
Word: I _ _ U T A _ _ E | Remaining: 4 | Incorrect: O | Guess: s
Word: I _ _ U T A _ _ E | Remaining: 3 | Incorrect: OS | Guess: r
Word: I _ _ U T A _ _ E | Remaining: 2 | Incorrect: OSR | Guess: m
Word: I M M U T A _ _ E | Remaining: 2 | Incorrect: OSR | Guess: l
Word: I M M U T A _ L E | Remaining: 2 | Incorrect: OSR | Guess: b
Word: I M M U T A B L E
You won!

• The game picks a word at random from a list of words.


• The game’s state is displayed to the player, as shown above.
• The player can pick a letter. If they pick a letter they already chose, pick again.
• The game should update its state based on the letter the player picked.
• The game needs to detect a win for the player (all letters have been guessed).
• The game needs to detect a loss for the player (out of incorrect guesses).
Objectives:
• Use CRC cards (or a suitable alternative) to outline the objects and classes that may be needed to
make the game of Hangman. You do not need to create this full game; just come up with a
potential design as a starting point.

TIC-TAC-TOE
This final challenge requires building a more complex object-oriented program from start to
finish: the game of Tic-Tac-Toe. This is the most significant program we have made so far, so
expect to take some time to get it right.

Boss Battle Tic-Tac-T


Tic-Tac-Toe
oe 300 XP
Completing designs for the three games in the
t he Chamber of Design causes the pedestals to light up red
again, and another door opens, letting you into the final chamber. This chamber has only a single large,
broad pedestal.
pedest al. Inscribed on the stone floor
flo or in a circle around the pedest
pedestal
al are the engraved words, “Only
a True Programmer can build object-oriented programs.”
More text engraved on the pedestal describes what you recognize as the game of Tic-Tac-Toe, stating
that in ancient times, inhabitants of the land would use this as a Battle of Wits to determine the outcome
of political strife. Instead of fighting wars, they would battle it out in a game of Tic-Tac-Toe.
Your job is to recreate the game of Tic-Tac-Toe, allowing two players to compete against each other. The
following features are required:
• Two human players take turns entering their choice using the same keyboard.
• The players designate which square they want to play in. Hint: YYou
ou might consider
cons ider using the number
pad as a guide. For example, if they enter 7, they have chosen the top left corner of the board.
• The game should prevent players from choosing squares that are already occupied. If such a move
is attempted, the player should be told of the problem and given another chance.

196 LEVEL 24 THE CAT


CATACOMBS
ACOMBS OF THE CLASS
• The game must detect when a player wins or when the board is full with no winner (draw/”cat”).
• When the game is over, the outcome is displayed to the players.
• The state of the board must be displayed to the player after each play. Hint: One possible way to
show the board could be like this:
It is X's turn.
| X |
---+---+---
| O | X
---+---+---
O | |
What square do you want to play in?
Objectives:
• Build the game of Tic-Tac-Toe as described in the requirements above. Starting with CRC cards is
recommended, but the goal is to make working software, not CRC cards.
• Answer this question: How might you modify your completed program if running multiple rounds
was a requirement (for example, a best-out-of-five series)?

Narrative The Gift of Object 󰁓ight


As you place the finished Tic-Tac-Toe program onto the pedestal, writing etched into the stone walls

begins toaglow
hand for reddish-orange.
moment The glow
before the glowing is bright
dims enough
to a more that you intensity.
manageable have to shield your eyes with your

Suddenly, you realize that you are no longer the only thing in the room. Thousands of faintly glowing,
bluish objects of various shapes and sizes float in the air around you.
You hear a resounding, booming voice echo through the chamber: “We are the Guardians of the
Catacombs. We have seen your creations and know that you are a True Programmer. We have deemed
you worthy of the Gift of Object Sight—the ability to see objects in code and requirements and craft
solutions from objects and types.
“We need your help. The Fountain of Objects—the lifeblood of this island—has been destroyed by the
vile Uncoded One.
On e. Use the Gift of Object Sight to reforge the Fountain of Objects. Without the Fountain,
this land will crumble and fade into oblivion. Object Sight will lead you to the Fountain. Depart now and

save this land!”


As you leave the Catacombs of the Class, you discover that your new Object Sight ability has made
countless code objects visible in the world around you. You also see a distinct trail, marked with a faint
blue line heading into the rugged, distant mountains where the Fountain of Objects supposedly lies.
Though the journey ahead is still long, the pathway to the Fountain of Objects is now clear!
LEVEL
INHERITANCE
25
󰁓peedrun
• Inheritance lets you derive new classes based on existing ones. The new class inherits everything
except constructors from the base class. class Derived : Base { ... }

Classes derive from object by default, and everything eventually derives from object even if
another class is explicitly stated.
• Constructors in a derived class must call out the constructor they are using from the base class
parameterless constructor: public Derived(int x) : base(x) { ...
unless they are using a parameterless
}
• Derived class instances can be used where the base class is expected: Base x = new Derived();
• The protected accessibility modifier makes things
things accessible in the class and any derived classes.

Sometimes, a class is a subtype or specialization of another. The broader


broader category has a set of
capabilities that the subtype or specialization extends or enhances with more. Here are a few
real-world examples of this type of relationship:
• Every vehicle has a top speed, maximum acceleration, passenger count, and the ability
to drive it. Specific subtypes of vehicles
vehicl es do that and more. A truck includes a bed with the
capacity to carry cargo. A tank includes a gun. Both tanks and trucks add unique
information on top, but you could use a tank or a truck for anything you might do with a
vehicle in a pinch.
• Writing implements let you write text or draw pictures on paper. paper. Pencils augment this
with the ability
ability to erase, colored
colored pencils add color,
color, and pens add the concept of ink levels
and running out of ink. But each can be used to write and draw.
• Astronomicall objects all have specific properties like location
Astronomica location and mass,
mass, which is enough
to calculate gravitational pull. Stars extend that idea by including temperature and the
ability to incinerate things. Planets add information like atmosphere composition and
terrain details on a rocky planet.
You can define this subtype or specialization relationship in C# code using inheritance.
You
Inheritance accomplishes two critical things. First, it allows you to treat the subtypes as the
198 LEVEL 25 INHERITANCE
more generalized type whenever necessary. Second, it allows you to consolidate what would
otherwise be duplicated or copy-and-pasted code in two closely related classes.
Let’s continue with the Asteroids example we experimented with back in Level 23. There are
many types or classes
cl asses of objects that drift in space. Asteroids, bullets, and the player’s ship use
the same mechanics
mec hanics to drift through space. These are distinct classes, with their own behavior
behavior,,
but given only the tools we have learned before now, we would have to copy and paste that
drifting logic to the Asteroid, Bullet, and Ship classes as we created them.
This relationship between a subcategory and its parent category is common in object-
oriented programming. A relationship where a type can expand upon another is called an
inheritance relationship. Inheritance is our fourth key principle of object-oriented
programming:
Object-Oriented Principle #4: Inheritance—Basing one class on another, retaining the
original class’s functionality while extending the new class with additional capabilities.

INHERITANCE AND THE OBJECT CLA󰁓󰁓


When we define an inheritance relationship between two classes, three th ree things happen. The
new class gets everything the old class had, the new class can add in extra stuff, and the new
class can always be treated as though it were the original since it has all of those capabilities.
c apabilities.

The original
or the class. The
superclass we build
new on
class thatbase
is the class,the
extends though
base itclass
is sometimes called
is the derived the, parent
class thoughclass
it is
sometimes called the child class or the subclass. (Programmers aren’t always great at
consistent terminology.) People will say that the derived class derives from the base class or
that the derived class extends the base classcl ass.. Let’s make that clearer with a concrete example.
As it turns out, we have
have been unknowingly using inheritance
inheritance for a while.
while. Every
Every class
class you
you define
define
automatically has cla ss called object. When we made an Asteroid or a Point class,
has a base class
these were derived from or extended the object class. Asteroid and Point are the derived
classes in this relationship, and object is the base class. c lass.
The object class is special. It is the base class
cl ass of everything, and everything is a specialization
of the object class. That means anything the object class defines exists on every single
object ever created. Let’s explore the object class and get our first peek at how inheritance

works.
To start, you can create instances of the object class and use object as a type for a variable:
object thing = new object();

The object class doesn’t have many responsibilities, so creating instances of object itself
It has several methods, but we will look at two here: ToString and Equals.
is relatively rare. It
The ToString method creates a string representation of any object. The default
implementation is to display the full name of the object’s type:
Console.WriteLine(thing.ToString());

That code will display System.Object, since the Object class lives in the System
namespace.
The Equals method returns a bool that indicates whether two things are considered equal
or not. The following code will display True and then False.
INHERITANCE
INHERI TANCE AND THE OBJECT CLASS 199
object a = new object();
object b = a;
object c = new object();
Console.WriteLine(a.Equals(b));
Console.WriteLine(a.Equals(c));

By default, Equals will return whether two things are references to the same object on the
heap. But equality is a complex subject in programming. Should two things be equal only if
they represent the same memory location? Should they be equal if they are of the same type
and their fields are equal? Do some fields matter while others do not? Under different
circumstances, each of these could be true.
As we will
will see in the
the next level, your classes
classes can sometimes redefine a method, including both
ToString and Equals.
Because object defines the ToString and Equals methods, and because the classes we
have created are derived from object, our objects also have ToString and Equals.
Suppose we have a simple Point class defined like this:
public class Point
{
public float X { get; }
public float Y { get; }

public Point(float x, float y)


{
X = x; Y = y;
}
}

Even though this class does not define ToString or Equals methods, it has them:
Point p1 = new Point(2, 4);
Point p2 = p1;
Console.WriteLine(p1.ToString());
Console.Write(p1.Equals(p2));

That is because Point inherits these methods from its base class,
c lass, object.
Importantly, because a derived class has all the base class’s capabilities, you can use the
derived class anywhere the based class is expected. A simple example is this:
object thing = new Point(2, 4);

The variable holds a reference to something with a type of object. We give it a reference to a
Point instance. Point is a different class than object, but Point is derived from object
and can thus be treated as one.
This makes things interesting. The thing variable knows it holds objects. You can use its
ToString and Equals method. But the variable makes no promises that it has a reference
to anything more specific than object:
Console.WriteLine(thing.ToString()); // Safe.
Console.WriteLine(thing.X);
Console.WriteLine(th ing.X); // Compiler error.

Even though we put an instance of Point into our thing variable, the variable itself can only
guarantee it has a reference to an object. It could be a Point, but the variable and the
200 LEVEL 25 INHERITANCE
compiler cannot guarantee that, even though a human can see it from inspecting the code.
Once we place a reference to a derived class like Point into a base class variable like object,
that information is not lost forever. Later in this level, we will see how we can explore an
object’s type
type and cast to the derived type if needed to regain access to the object as the derived
type.

CHOO󰁓ING BA󰁓E CLA󰁓󰁓E󰁓


By default,
hard all aclasses
to claim inherit
different classfrom object
as the ass. use object as their base class), but it is not
cl (they
base class.
This section’s code is not a complete set
se t of useful cclasses,
lasses, just an illustration of inheritance. In
previous levels, we talked about the classes we might define for an Asteroids game and even
made an Asteroid class once, which included the logic for drifting through space. We
mentioned in passing that bullets and the player’s ship would need the same behavior. We
could make a GameObject class that served as a base class for all of these:
public class GameObject
{
public float PositionX { get; set; }
public float PositionY { get; set; }
public float VelocityX { get; set; }
public float VelocityY { get; set; }

public void Update()


{
PositionX += VelocityX;
PositionY += VelocityY;
}
}

We can now create an Asteroid class that includes things specific to just the asteroid and
indicate that this class is derived from GameObject instead of plain object:
public class Asteroid : GameObject
{
public float Size { get; }
public float RotationAngle { get; }
}
As shown above, a class identifies its base class by placing its name after a colon. Asteroid
will inherit PositionX, PositionY, VelocityX, VelocityY, and Update from its base
class, GameObject. It also adds new Size and RotationAngle properties, which are
unique to Asteroid.
Let’s suppose we also make Bullet and Ship classes that also derive from GameObject. We
could set up a new game of Asteroids with a collection of game objects of mixed types like this:
GameObject[] gameObjects = new GameObject[]
{
new Asteroid(), new Asteroid(), new Asteroid(),
new Bullet(), new Ship()
};

Okay, you probably wouldn’t start the game with a bullet already flying around, but you get
the idea. The array stores references to GameObject instances. But that array contains
CONSTRUCTORS 201
instances of the Asteroid, Bullet, and Ship classes. The array is fine with this because all
three of those types derive from GameObject.
Here is where things get interesting:
foreach (GameObject item in gameObjects)
item.Update();

Even though we are dealing with four total classes (one base class and three derived classes),
we can call the Update method on any of them since it is defined by GameObject. All of the
derived classes are guaranteed to have that method.
Inheritance only goes one way. While you can use an Asteroid when a GameObject is
needed, you cannot use a GameObject where an Asteroid is needed. Nor can you use an
Asteroid when a Ship or Bullet is needed:
Asteroid asteroid = new GameObject(); // COMPILER ERROR!
Ship ship = new Asteroid(); // COMPILER ERROR!

A collection of classes related


related through inheritance,
inheritance, such as these four
four,, is called an inheritance
hierarchy.
Inheritance hierarchies can be as deep as you need them to be. For example:
public class Scout : Ship { /* ... */ }

public class Bomber : Ship { /* ... */ }


The Bomber and Scout classes derive from Ship, which derives from GameObject , which
derives from object. You can use a Bomber anywhere a Ship, GameObject, or object is
needed.
However, classes may only choose one
However, on e base class. You cannot directly derive from more than
one. There are situations where this is somewhat limiting, but complications arise from this,
so the C# language forbids inheriting from more than one base class.

CON󰁓TRUCTOR󰁓
A derived class inherits most members from a base class but not constructors. Constructors
put a new object into a valid starting state. A constructor in the base class can make no
guarantees about the validity of an object of a derived class. So cconstructors
onstructors are not inherited,
and derived classes must supply their own. However, we can—and must—leverage the
constructors defined in the base class when making newne w constructors in the derived cl class.
ass.
If a parameterless constructor exists in the base class, a constructor in a derived class will
automatically call it before running its own code. And remember: if a class does not define
any constructor, the compiler will generate a simple, parameterless constructor. The
compiler-made one will work fine for our purposes here. This is what has happened in our
simple inheritance hierarchy. Neither GameObject nor Asteroid specifically defined any
constructors. The compiler generated a default parameterless constructor in both classes, and
the one in Asteroid automatically called the one in GameObject .
The same thing happens if you have manually made parameterless constructors:
public class GameObject
{
202 LEVEL 25 INHERITANCE
public GameObject()
{
PositionX = 2; PositionY = 4;
}

// Properties and other things here.


}

public class Asteroid : GameObject


{
public Asteroid()
{
RotationAngle = -1;
}

// Properties and other things here.


}

Here, Asteroid’s parameterless constructor will automatically call GameObject’s


parameterless constructor. Calling new Asteroid() will enter Asteroid’s constructor and
immediately jump to GameObject’s parameterless constructor to set PositionX and
PositionY and then return to Asteroid’s constructor to set RotationAngle .
Suppose a base class has more than one constructor or does not include a parameterless
constructor (both common scenarios). In that case, you will need to expressly state which
base class constructor to build upon for any new constructors in the derived class.
Let’s suppose GameObject has only this constructor:
public GameObject(float positionX, float positionY,
float velocityX, float velocityY)
{
PositionX = positionX; PositionY = positionY;
VelocityX = velocityX; VelocityY = velocityY;
}

Since there is no parameterless constructor to call, any constructors defined in Asteroid will
need to specifically indicate that it is using this other constructor and supply arguments for its
parameters:
public Asteroid() : base(0, 0, 0, 0)
{
}

It is relatively common to pass along parameters from the current constructor down to the
base class’s
class’s constructor, so the following might be more common:
public Asteroid(float positionX, float positionY,
float velocityX, float velocityY)
: base(positionX, positionY, velocityX, velocityY)
{
}

(Note that I wrapped this line twice because of the limitations of the printed medium. In actual
code, I might have put everything before the curly braces on a single line.)

We saw something similar in Level 18, just with the keyword this instead of base. It works
in the same way, just reaching down to the base class’s constructors instead of this class’s
constructors. You cannot use both this and base together on a constructor, but a
CASTING AND CHECKING FOR TYPES 203
constructor can call out another constructor in the same class with this instead of using
base. Since constructor calls with this cannot create a loop, eventually, something will need
to pick a constructor from the base class.
Those rules are a bit complicated, so let’s recap. Constructors are not inherited like other
members are. Constructors in the derived class must call out a constructor from the base class
(with base) to build upon. Alternatively, they can call out a different one in the same class
(with this). If a parameterless constructor exists, including one the compiler gene
generates,
rates, you
do not need to state it explicitly with base. But don’t worry; the compiler will help you spot
any problems.

CA󰁓TING AND CHECKING FOR TYPE󰁓


If you ever have a base type but need to get a derived type out of it, you have some options.
Consider this situation:
GameObject gameObject = new Asteroid();
Asteroid asteroid = gameObject; // ERROR!

The gameObject variable can only guarantee that it has a GameObject . It might reference
something more specific, like an Asteroid. In the above code, we know that’s true.
By casting, we can get the computer to treat the object as the more specialized type:
GameObject gameObject = new Asteroid();
Asteroid asteroid = (Asteroid)gameObject; // Use with caution.

Casting tells the compiler, “I know more about this than you do, and it will be safe to treat this
as an asteroid.”
asteroid.” The compiler will allow this code to compile, but the program will crash when
running if you are wrong. The above code is guaranteed to be safe, but this one is not:
not :
Asteroid probablyAnAsteroid = (Asteroid)CreateAGameObject();

GameObject CreateAGameObject() { ... }

This cast is risky. It assumes it will get an Asteroid back, but that’s not a guaranteed thing. If
CreateAGameObject returns anything else, this program will crash.

Casting from
should feel a base
when class
doing it.to a derived
You should class
cnot
lassgenerally a downcast
is calleddo . Incidentally,
it, and usually that
only if you is how
c heck
check foryou
the
correct type first. There are three ways to do this check.
The first way is with object’s GetType() method and the typeof keyword:
if (gameObject.GetType() == typeof(Asteroid)) { ... }

For each type that your program uses, the C# runtime will create an object representing
information about that type. These objects are instances of the Type class, which is a type that
has metadata about other types in your program. Calling GetType() returns the type object
instance’s class. If gameObject is an Asteroid, it will return the Type
associated with the instance’s
object representing the Asteroid class. If it is a Ship, GetType will return the Type object
representing the Ship class. The typeof keyword lets you access these special objects by
name instead. Using code like this, you can see if an object’s type matches some specific class.
204 LEVEL 25 INHERITANCE
Using typeof and .GetType() only work if there is an exact match. If you have an
Asteroid instance and do asteroid.GetType() == typeof(GameObject), this
evaluates to false. The Type instances that represent the Asteroid and GameObject
classes are different. That can work for or against you, but it is important to keep in mind.
Another way is through the as keyword:
GameObject gameObject = CreateAGameObject();
Asteroid? asteroid = gameObject as Asteroid;

The as keyword simultaneously does a check and the conversion. If gameObject is an


Asteroid (or something derived from Asteroid), then the variable asteroid will contain
the reference to the object, now known to be an Asteroid. If gameObject is a Ship or a
Bullet, then asteroid will be null. That means you will want to do a null check before
using the variable.
The third way is with the is keyword. The is keyword is powerful and is one way to use
patterns, which is the topic of Level 40. But it is frequently used to simply check the type and
assign it to a new variable. The most common way to use it is like this:
if (gameObject is Asteroid asteroid)
{
// You can use the `asteroid` variable here.
}

If you don’t need the variable that this creates, you can skip the name:
if (gameObject is Asteroid) { ... }

THE PROTECTED ACCE󰁓󰁓 MODIFIER


We have encountered three accessibility modifiers in the past: private, public , and
internal. The fourth accessibility modifier is the protected keyword. If something is
protected, it is accessible within the class and also any derived classes. For example:
public class GameObject
{
public float PositionX { get; protected set; }
public
public float
float PositionY
VelocityX {
{ get;
get; protected
protected set;
set; }
}
public float VelocityY { get; protected set; }
}

If we make these setters protected instead of public, only GameObject and its derived
classes (like Asteroid and Ship) can change those properties; the outside world cannot.

󰁓EALED CLA󰁓󰁓E󰁓
If you want to forbid others from deriving from a specific class, you can prevent it by adding
the sealed modifier to the class definition:
public sealed class Asteroid : GameObject
{
// ...
}
SEALED CLASSES 205
In this case, nobody will be able to derive a new class based on Asteroid. It is rare to want
an outright ban on deriving from a class,
c lass, but it has its occasional uses. Sealing a class can also
sometimes result in a performance boost.

Challenge Packing Inventory


Inventory 150 XP
You know you have a long, dangerous journey ahead of you to travel to and repair the Fountain of
Objects. You decide to build some classes and objects to manage your inventory to prepare for the trip.
You decide to create a Pack class to help in holding your items. Each pack has three limits: the total
number of items it can hold, the weight it can carry, and the volume it can hold. Each item has a weight
and volume, and you must not overload a pack by adding too many items, too much weight, or too much
volume.
There are many item types that you might add to your inventory, each their own class in the inventory
system. (1) An arrow has a weight of 0.1 and a volume of 0.05. (2) A bow has a weight of 1 and a volume
of 4. (3) Rope has a weight of 1 and a volume
volum e of 1.5. (4) Water
Water has a weight of 2 and a volume of 3. (5) Food
rations have a weight of 1 and a volume of 0.5. (6) A sword has a weight of 5 and a volume of 3.
Objectives:
• Create an InventoryItem class that represents any of the different item types. This class must
represent the item’s weight and volume, which it needs at creation time (constructor).
• Create derived classes for each of the types of items above. Each class should pass the correct weight
and volume to the base class constructor but should be creatable themselves with a parameterless
constructor (for example, new Rope() or new Sword()).
• Build a Pack class that can store an array of items. The total number of items, the maximum weight,
and the maximum volume are provided at creation time and cannot change afterward.
• Make a public bool Add(InventoryItem item) method to Pack that allows you to add items
of any type to the pack’s contents. This method should fail (return false and not modify the pack’s
fields) if adding the item would cause it to exceed the pack’s item, weight, or volume limit.
• Add properties to Pack that allow it to report the current item count, weight, and volume, and the
limits of each.
• Create a program that creates a new pack and then allow the user to add (or attempt to add) items
chosen from a menu.
LEVEL 26
POLYMORPHI󰁓M

󰁓peedrun
• Polymorphism lets a derived class supply its own definition (“override”) for a member declared in its
base class.
• Marking a member with virtual indicates it can be overridden.
• Derived classes override a member by marking it with override.
• Classes can leave members unimplemented with abstract, but the class must also be abstract .

Inheritance is powerful, but it is made whole with the topic of this level: polymorphism.
Imagine programming the game of chess. We could define Pawn, Rook, and King classes, all
derived from a ChessPiece base class using inheritance.
inher itance. But this does not allow us to solve
a fundamental problem in chess: deciding whether some move is legal or not. Each piece has
different rules for determining if a move is legal or not. There is some overlap—no piece can
stay put and count it as a move, and no piece can move off the 8×8 board. But beyond that,
each piece is different. With just inheritance, the best we could do looks like this:

public class ChessPiece


{
public int Row { get; set; }
public int Column { get; set; }

public bool IsLegalMove(int row, int column) =>


IsOnBoard(row, column) && !IsCurrentLocation(row, column);

protected bool IsOnBoard(int row, int column) =>


row >= 0 && row < 8 && column >= 0 && column < 8;

protected bool IsCurrentLocation(int row, int column) =>


row == Row && column == Column;
}

This base class does some basic checks that make sense for all chess pieces but can go no
further.
A derived class can do this:
SEALED CLASSES 207
public class King : ChessPiece
{
public bool IsLegalKingMove(int row, int column)
{
if (!IsLegalMove(row, column)) return false;

// Moving more than one row or one column is not a legal king move.
if (Math.Abs(row - Row) > 1) return false;
if (Math.Abs(column - Column) > 1) return false;

} return true;
}

King adds an IsLegalKingMove method. You could imagine a similar IsLegalPawnMove


in the Pawn class and so on.
Unfortunately, we would need to remember which objects are of which types to call the
appropriate IsLegalSomethingMove methods.
Polymorphism allows us to solve this problem elegantly. Polymorphism means “many forms”
(from Greek). It is a mechanism that lets different classes related by inheritance provide their
own definition for a method. When something calls the method, the version that belongs to
the object’s actual type will be determined and called. Polymorphism is our fifth and final
principle of object-oriented
objec t-oriented programming.
Object-Oriented Principle #5: Polymorphism—Derived classes can override methods
from the base class. The correct version is determined at runtime, so you will get different
behavior depending on the object’s class.
In our chess example, each derived class will be able to supply its own version of
IsLegalMove. When the program runs, the correct IsLegalMove method is called,
depending on the actual object involved:
ChessPiece p1 = new Pawn();
ChessPiece p2 = new King();

Console.WriteLine(p1.IsLegalMove(2, 2));
Console.WriteLine(p2.IsLegalMove(2, 2));

Even though p1 and p2 both have the type ChessPiece, calling IsLegalMove will use the
piece-specific version on the last two lines because of polymorphism.
Not every method can leverage polymorphism. A method must indicate it is allowed by
placing the virtual keyword on it, giving permission
per mission to derived classes to replace it.
public virtual bool IsLegalMove(int row, int column) =>
IsOnBoard(row, column) &&
!IsCurrentLocation(row, column);

We can replace or override the method with an alternative version in a derived class.
c lass. We
We could
put this in the King class:
public override bool IsLegalMove(int row, int column)
{
if (!base.IsLegalMove(row, column)) return false;
// Moving more than one row or one column is not a legal king move.
208 LEVEL 26 POLYMORPHISM
POLYMORPHISM
if (Math.Abs(row - Row) > 1) return false;
if (Math.Abs(column - Column) > 1) return false;

return true;
}

The King class has now provided its own definition


d efinition for IsLegalMove. It has overridden the
by the base class. Pawn, Rook, and the others can do so as well.
version supplied by
When you override a method, it is a total replacement. If you want to reuse the overridden
logic from
to keep thethe base
logic forclass, youoncan
staying thecall
boaitrd.
board.through
Not allthe base keyword.
overrides The code
call the base class’above
class’s does
s version ofthis
the
method, but it is common.
Y
You
ou can override most types of members except fields and constructors (which aren’t
inherited anyway).
Just because
because a method is virtual does not mean a derived class must override it. With our chess
example, they all probably will. In other situations, some derived classes will find the base
class version sufficient.
When a normal (non-virtual)
(non-virtual) member is called, the compiler can determine which method to
call at compile time. When a method is virtual, it cannot. Instead, it records some metadata in
the compiled code to know what to look up as it is running. This lookup as the program is
running takes a tiny bit of time. You do not want to just make everything
ever ything virtual “just in case.”
Instead, consider what a derived class may need to replace and make only those virtual.
The overriding method must match the name and parameters (both count and type) as the
overridden method. However,
However, you can use a more specific type for the return value if you want.
For example, if you have a public virtual object Clone() method, it can be
overridden with a public override SpecificClass Clone() since SpecificClass
is derived from object.

AB󰁓TRACT METHOD󰁓 AND CLA󰁓󰁓E󰁓


Sometimes, a base class wants to require that all derived classes supply a definition for a
method but can’t provide its own implementation. In such cases, it can define an abstract

method
the , specifying
method. When athe method’s
class signature
has an abstract withoutderived
method, providing a body
classes oroverride
must implementation for
the method;
there is nothing to fall back on. In fact, any class with an abstract method is an incomplete
class. You cannot create instances of it (only derived classes), and you must mark the class
itself as abstract as well. To illustrate, here is what the ChessPiece class might look like with
an abstract IsLegalMove method:
public abstract class ChessPiece
{
public abstract bool IsLegalMove(int targetRow, int targetColumn);

// ...
}

Adding the abstract keyword (instead of virtual) to a method says, “Not only can you
override this method, but you must override this method because I’m not supplying a
definition.” Instead of a body, an abstract method ends with a semicolon. Once a class has any
abstract members, the class must also be made abstract, as shown above.
NEW METHODS 209
Abstract members can only live in abstract classes, but an abstract class can contain any
member it wants—abstract, virtual, or normal. It is not unheard of to have an abstract class
with no abstract
abstract members—just a foundation for closely related ttypes
ypes to build on.
When a distinction is
is needed, non-abstract classes ar
aree often referred to as concrete classes.

NEW METHOD󰁓
If a derived class defines a member whose name matches something in a base class without
overriding it, a new member will be created, which hides (instead of overrides) the base class
member.. This is nearly always an accident caused by forgetting the override keyword. The
member
compiler assumes as much and gives you a warning for it.
In the rare cases where this was by design, you can tell the compiler it was intentional by
adding the new keyword to the member in the derived class:
public class Base
{
public int Method() => 0;
}

public class Derived : Base


{
public new int Method() => 4;
}
When a new member is defined, unlike
unl ike polymorphism, the behavior depends on the type of
the variable involved, not the instance’s
instance’s type:
Derived d = new Derived();
Base b = d;

Console.WriteLine(d.Method() + " " + b.Method());

This displays 4 0, not 4 4, as we would otherwise


other wise expect with polymorphism.

Challenge Labeling Inventory 50 XP


You realize that your inventory items are not easy to sort through. If you could make it easy to label all
of your inventory items, it would be easier to know what items you have in your pack.
Modify your inventory program from the previous level as described below.
Objectives:
• Override the existing ToString method (from the object base class) on all of your inventory item
subclasses to give them a name. For example, new Rope().ToString() should return "Rope".
• Override ToString on the Pack class to display the contents of the pack. If a pack contains water,
rope, and two arrows, then calling ToString on that Pack object could look like "Pack
containing Water Rope Arrow Arrow" .
• Before the user chooses the next item to add, display the pack’s current contents via its new
ToString method.
210 LEVEL 26 POLYMORPHISM
POLYMORPHISM

Challenge The Old Robot 200 XP


You spot something shiny, half-buried in the mud. You pull it out and realize that it seems to be some
mechanical
mechanic al automaton with the words “Property of Dynamak”
Dynam ak” etched into it. As you knock off the caked-
on mud, you realize that it seems like this old automaton might even be programmabl
programmablee if you can give it
the proper commands. The automaton seems to be structured like this:
public class Robot
{
public int X { get; set; }
public int
public boolYIsPowered
{ get; set; }
{ get; set; }
public RobotCommand?[] Commands { get; } = new RobotCommand?[3];
public void Run()
{
foreach (RobotCommand? command in Commands)
{
command?.Run(this);
Console.WriteLine($"[{X} {Y} {IsPowered}]");
}
}
}

You don’t see a definition of that RobotCommand class. Still, you think you might be able to recreate it
(a class with only an abstract Run command) and then make derived classes that extend RobotCommand
that move it in each of the four directions and power it on and off. (You wish you could manufacture a
whole army of these!)
Objectives:
• Copy the code above into a new project.
• Create a RobotCommand class with a public and abstract void Run(Robot robot) method. (The
code above should compile after this step.)
• Make OnCommand and OffCommand classes that inherit from RobotCommand and turn the robot
on or off by overriding the Run method.
• Make a NorthCommand, SouthCommand, WestCommand, and EastCommand that move the robot 1
unit in the +Y direction, 1 unit in the -Y direction, 1 unit in the -X direction, and 1 unit in the +X
direction, respectively. Also, ensure that these commands only work if the robot’s IsPowered
property is true.

Make your main method able to collect three commands from the console window. Generate new
RobotCommand objects based on the text entered. After filling the robot’s command set with these
new RobotCommand objects, use the robot’s Run method to execute them. For example:
on
north
west

[0 0 True]
[0 1 True]
[-1 1 True]

• Note: You might find this strategy for making commands that update other objects useful in some
of the larger challenges in the coming levels.
LEVEL
INTERFACE󰁓
27
󰁓peedrun
• An interface is a type that defines a contract or role that objects can fulfill or implement: public
interface ILevelBuilder { Level BuildLevel(int levelNumber); }

Classes can
Level BuildLevel(intinterfaces: public class
implement interfaces:
levelNumber)
LevelBuilder : ILevelBuilder { public
=> new Level(); }
• A class can have only one base class but can implement many interfaces.
interfaces.

We’ve learned
We’ve le arned how
h ow to create new types using enumerations and classes, but you can make
several other flavors of type definitions in C#. The next one we’ll learn about is an interface.
An interface is a type that defines an object’s interface or boundary by listing
l isting the methods,
properties, etc., that an object must have without supplying any behavior for them. You could
also think of an interface as defining a specific role or responsibility in the system without
providing
providi ng the code to make it happen.
We see interfaces in the real world all the time. For example, a piano with its 88 black and

white keys
Electric and an expectation
keyboards, that pushing
upright pianos, the keys
grand pianos, andwill
inplay certain
no small pitches
degree, is an
even interface.
inte rface.
organs and
harpsichords provide the same interface. A user of the interface—a pianist—can play any of
these instruments in the same way without worrying about how they each produce sound. We
see a similar thing with vehicles, which all present a steering wheel,
wheel , an accelerator, and a brake
brake
pedal. As a driver, it doesn’t matter if the engine
eng ine is gas, diesel, or eelectric
lectric or whether the brakes
are frictional or electromagnetic.
Interfaces give us the most flexibility in how something accomplishes its job. It is almost as
though we have made a class where every member is abstract, though it is even more flexflexible
ible
than that.
Interfaces are perfect for situations where we know we may want to substitute entirely
different or unrelated objects to fulfill a specific role or responsibility in our system. They give

us the is
object most
thatflexibility
it compliesinwith
evolving our code
the defined over time.
interface. The as
As long only
twoassumption made about
things implement the
the same
interface, we can swap one for another, and the rest of the system is none the wiser.
wiser.
212 LEVEL 27 INTERFACES

DEFINING INTERFACE󰁓
Let’s say we have a game where the player advances through levels, one at a time. We’ll keep
it simple and say that each level is a grid of different terrain types from this enumeration:
public enum TerrainType { Desert, Forests, Mountains, Pastures, Fields, Hills }

Each level is a 2D
2 D grid of these terrain types, represented
represented by an instance of this class:
public class Level
{
public int Width { get; }
public int Height { get; }
public TerrainType GetTerrainAt(int row, int column) { /* ... */ }
}

We find a use for interfaces when deciding where level definitions come from. There are many
options. We could define them directly in code, setting terrain types at each row and column c olumn
in C# code. We could load them from files on our computer. We could load them from a
database. We could randomly generate them. There are many options, and each possibility
has the same result and the same job, role, or responsibility: create a level to play. Yet, the code
for each is entirely unrelated to the code for the other options.
We may
may not know yet which of these we will end up using. Or perhaps we plan to retrieve the
levels from the Internet but don’t intend to get a web server running for a few more months
and need a short-term fix.
To preserve as much flexibility as possible around this decision, we simply define what this
role must do—what interface or boundary the object or objects fulfilling this role will have:
public interface ILevelBuilder
{
Level BuildLevel(int levelNumber);
}

Interface types are defined similarly to a class


cla ss but with a few differences.
For starters, you use the interface keyword instead of the class keyword.
Second, you can see that I started my interface name with a capital I. That is not strictly
necessary, but it is a common convention in C# Land. Most C# programmers do it, so I
recommend it as well. (It does lead to the occasional awkward double I names like
IImmutableList , but you get used to it.)
Members of an interface are public and abstract automatically. After all, an interface
defines a boundary (abstract) meant for others to use (public). You can place those
keywords on there if you’d like, but few developers do.
Because an interface defines just the boundary or job to be done, its members don’t have an
implementation. (There is an exception to that statement, described later in this level.) Most
things you might place in a class can also be placed in an interface (without an
implementation) except fields.
While this ILevelBuilder interface only has a single method, interfaces can have as many
members as they need to define the role they represent. For example, you could let the rest of
the game know how many levels are in the set by adding an int Count { get; } property.
IMPLEMENTING
IMPLEMENTIN G INTERFACES 213

IMPLEMENTING INTERFACE󰁓
Once an interface has been created, the next step is to build a class that fulfills the job
described by the interface. This is called implementing the interface. It looks like inheritance,
so some programmers also call it extending the interface or deriving from the interface. These
names are all common, and many C# programmers don’t strongly differentiate interfaces
from base classes and use the terms interchangeably. I will refer to it as implementing an
interface and extending a base class in this book.
The simplest implementation of the ILevelBuilder interface is probably defining levels in
code:
public class FixedLevelBuilder : ILevelBuilder
{
public Level BuildLevel(int levelNumber)
{
Level level = new Level(10, 10, TerrainType.Forests);

level.SetTerrainAt(2, 4, TerrainType.Mountains);
level.SetTerrainAt(2, 5, TerrainType.Mountains);
level.SetTerrainAt(6, 1, TerrainType.Desert);

return level;
}
}

The body of BuildLevel takes quite a few liberties that we never fleshed out. It uses a
constructor and a SetTerrainAt method that we did not define earlier in the Level class,
though you could imagine including them. It also creates the same level
l evel every time, ignoring
ign oring
the levelNumber parameter. In a real-world situation, we’d probably need to do more. But
the vital part of that code is how FixedLevelBuilder implements the ILevelBuilder
interface.
Like extending a base class through inheritance, you place a colon after the class name,
followed by the interface name you are implementing.
You must define each member included in the interface, as we did with the BuildLevel
You
method. These will be public but do not put the override keyword on them. This isn’t an
override. It is simply filling in the definition of how this class performs the job it has claimed
to do by implementing the interface.
A class
c lass that implements an interface
interf ace can
c an have other members unrelated to the interfaces
inter faces it
implements. By indicating that a class implements an interface, you are saying that it will have
at least the capabilities defined by the interface, not that it is limited to the interface. One
notable example is that an interface can declare a property with a get accessor, while a class
that implements it can also include a set or init accessor.
Y
You
ou can probably imagine creating other classes that implement this interface by loading
definitions from files (Level 39), ( perhaps using the Random class
39), generating them randomly (perhaps
described in Level 32),
32), or retrieving the levels from a database or the Internet.
We can create variables that use an interface as their type and place in it anything that
implements that interface:
ILevelBuilder levelBuilder = LocateLevelBuilder();

int currentLevel = 1;
214 LEVEL 27 INTERFACES
while (true)
{
Level level = levelBuilder.BuildLevel(currentLevel);
RunLevel(level);
currentLevel++;
}

The rest of the game doesn’t care which implementation of ILevelBuilder is being used.
However,, with the code
However far, we know it will be FixedLevelBuilder since
c ode we have written so far,
that is the only one that exists. However,
However, by doing nothing more than adding a new class that
implements ILevelBuilder and changing the implementation of LocateLevelBuilder
to return that instead, we can completely change the source of levels in our game. The entire
rest of the game does not care where they come from, as long as the object building them
conforms to the ILevelBuilder interface. We have reserved a great degree of flexibility for
the future by merely defining and using an interface.
inter face.

INTERFACE󰁓 AND BA󰁓E CLA󰁓󰁓E󰁓


Interfaces and base classes can play nicely together.
together. A class can simultaneously extend a base
class and implement an interface. Do this by listing the base class followed by the interface
after the colon, separated by commas:
public class MySqlDatabaseLevelBuilder : BasicDatabaseLevelBuilder, ILevelBuilder
{ ... }

A class can implement several interfaces


interface s in the same way by listing each one, separated by
commas:
public class SomeClass : ISomeInterface1, ISomeInterface2 { ... }

While you can only have one base class, a class


class can
can implement as many
many interfaces as you
you want.
(Though implementing many interfaces may signify that an object or class is trying to do too
much.)
Finally, an interface itself can list other interfaces (but not classes) that it augments or extends:
public interface IBroaderInterface : INarrowerInterface { ... }

When a class implements IBroaderInterface , they will also be on the hook to implement
INarrowerInterface .

EXPLICIT INTERFACE IMPLEMENTATION󰁓


Occasionally, a class implements two different interfaces containing members with the same
name but different meanings. For example:
public interface IBomb { void BlowUp(); }
public interface IBalloon { void BlowUp(); }

public class ExplodingBalloon : IBomb, IBalloon


{
public void BlowUp() { ... }
}
DEFAULT
DEFAULT INTERFACE METHODS 215
This single method is enough to implement both IBomb and IBalloon. If this one method
definition is a good fit for both interfaces, you are done.
On the other hand, in this situation, “blow up” means different things for bombs than it does
balloons. When we define ExplodingBalloon ’s BlowUp method, which one are we
referring to?
If you have control over these interfaces, consider renaming one or the other. We could
rename IBomb.BlowUp to Detonate or IBalloon.BlowUp to Inflate. Problem solved.

But if you don’t want to or can’t, the other choice is to make a definition for each using an
explicit interface implementation:
public class ExplodingBalloon : IBomb, IBalloon
{
void IBomb.BlowUp() { Console.WriteLine("Kaboom!"); }
void IBalloon.BlowUp() { Console.WriteLine("Whoosh"); }
}

By prefacing the method name with the interface name, you can define two versions of
BlowUp, one for each interface. Note that the public has been removed. This is required
with explicit interface implementations.
implementations.
The big surprise is that explicit implementations are detached from their containing class:
ExplodingBalloon explodingBalloon = new ExplodingBalloon();
// explodingBalloon.BlowUp(); // COMPILER ERROR!

IBomb bomb = explodingBalloon;


bomb.BlowUp();

IBalloon balloon = explodingBalloon;


balloon.BlowUp();

In this situation, you cannot call BlowUp directly on ExplodingBalloon ! Instead, you must
e ither IBomb or IBalloon (or cast it to one or the other). Then it
store it in a variable that is either
becomes available because it is no longer ambiguous.
a mbiguous.
If one of the two is more natural for the class, you can choose to do an explicit implementation
for only one, leaving the other as the default. If you do this, then the non-explicit
implementation is accessible on the object without casting it to the interface
inter face type.

DEFAULT INTERFACE METHOD󰁓


Interfaces allow you to create a default implementation for methods with some restrictions.
(If you do not like these restrictions, an abstract base class may be a better fit.) Default
implementations are primarily for growing or expanding an existing interface to do more.
Imagine having an interface with ten classes
cl asses that implement the interface. If you want to add
a new method or property to this interface, you have to revisit each of the ten implementations
to adapt them to the new changes.
If you can get an interface definition right the
t he first time around, it saves you from this rework.
It is worth taking time to try to get interfaces right, but we can never guarantee that.
Sometimes, things just need to change.
216 LEVEL 27 INTERFACES
Of course, you can just go for it and add the new member to each of the many
implementations. This is often a good, clean solution, even though it takes time.
In other situations, providing a default implementation for a method can be a decent
alternative. Imagine you have an interface that a thousand programmers around the world
use. If you change the interface, they’ll all need to update their code. A default implementation
may save a lot of pain for many people.
Let’s suppose we started with this interface definition:

public
{ interface ILevelBuilder
Level BuildLevel(int levelNumber);
int Count { get; }
}

If we wanted to build all the levels at once, we might consider adding a Level[]
BuildAllLevels() method to this interface. Adding this would not be complicated:
c omplicated:
public interface ILevelBuilder
{
Level BuildLevel(int levelNumber);
int Count { get; }
Level[] BuildAllLevels();
}

But the logic for this is pretty standard, and if we can just make a default implementation for
BuildAllLevels , nobody is required to make their own. We can grow the interface almost
for free:
public interface ILevelBuilder
{
Level BuildLevel(int levelNumber);
int Count { get; }

Level[] BuildAllLevels()
{
Level[] levels = new Level[Count];

for (int index = 1; index <= Count; index++)


levels[index - 1] = BuildLevel(index);

return levels;
}
}

With this default


default implementation, have to write a BuildAllLevels method
implementation, nobody else will have
unless they need something special. But if they do, it is a simple matter of adding a definition
for the method in the class.
c lass.
A default implementation can
can use the other members of the interface. We see that
that above
above since
BuildAllLevels calls both Count and BuildLevel .

󰁓upporting Default Interface Methods


Default interface method implementations are a relatively new thing to C#. When they
decided to add this feature,
fe ature, they provided
provided many of the tools needed to do it well. For example,
if a single method
me thod becomes too big, you can split some of the code into private methods. Y You
ou
DEFAULT
DEFAULT INTERFACE METHODS 217
can also create protected methods and static methods. I won’t get into all the details because
default method implementations are not all that common, and the compiler will tell you if you
attempt something that does not work. However,
However, one significant constraint
c onstraint is that you cannot
add instance fields. Interfaces cannot contain data themselves. (Though static fields are
allowed.)

󰁓hould I Use Default Interface Methods?


Adding default implementations in an interface was a somewhat controversial change. It is
hard for those making widely used interfaces to update every implementing class. The
benefits of default implementations are a lifesaver to them. But for many others, the benefits
are small, and it serves little value other than to cloud the concept of an interface.
Should you embrace them and provide one for every method you make, avoid them like the
plague, or something in between?
My recommendation stems from the fact that interfaces are meant to define just the
boundary, not the implementation. I suggest skipping default implementations except when
many classes implement the interface and when the default implementation is nearly always
correct for the classes that use the interface.
Not every interface change can be solved with default method implementations. It only works
if you are adding new stuff to an interface. If you are renaming or removing a method, you will

just need to fix all the classes that implement


implement the interface.
Challenge Robotic Interface 75 XP
With your knowledge of interfaces, you realize you can refine the old robot you found in the mud to use
interfaces instead of the original design. Instead of an abstract RobotCommand base class, it could
become an IRobotCommand interface!
Building on your solution to the Old Robot challenge, perform the changes below:
Objectives:
• Change your abstract RobotCommand class into an IRobotCommand interface.
• u nnecessaryy public and abstract keywords from the Run method.
Remove the unnecessar
• Change the Robot class to use IRobotCommand instead of RobotCommand.
• Make all of your commands implement this new interface instead of extending the RobotCommand
class that no longer exists. You will also want to remove the override keywords in these classes.
• Ensure your program still compiles and runs.
• Answer this question: Do you feel this is an improvement over using an abstract base class? Why
or why not?
LEVEL 28
󰁓TRUCT󰁓

󰁓peedrun
• A struct is a custom value type that defines a data structure without complex behavior: public
struct Point { ... }. SStructstructs are not focused
foc used on behavior but can have properties
propert ies and methods.
• Compared to classes: structs are value types, automatically have value semantics, and cannot be
used in inheritance.
• Make structs small, immutable, and ensure the default value is legitimate.
• All the built-in types are aliases for other structs (and a few classes). For example, int is shorthand
for System.Int32.
• Value types can be stored in reference-typed variables (object thing = 3;) but will cause the
value to be boxed and placed on the heap.

While classes are a great way


way to create
create new reference
reference types,
types, C# also
also lets you make
make custom value
value
types. New types of this nature are called structs, which is short for structure or data structure.
Making a struct is nearly the same as making a class. We have seen many variations on a
Point class before, but here is a Point struct:
public struct Point
{
public float X { get; }
public float Y { get; }

public Point(float x, float y)


{
X = x;
Y = y;
}
}

The only code difference is using the struct keyword instead of the class keyword. Most
aspects of making a struct are the same as making a class. You can add fields, properties,
methods, and constructors, along with some other member types we have not discussed yet).
Using a struct is also nearly the same as using a class:
MEMORY AND CONSTRUCTORS 219
Point p1 = new Point(2, 4);
Console.WriteLine($"({p1.X}, {p1.Y}");

typ es instead of reference types. That means


The critical difference is that structs are value types
variables whose type is a struct
struct contain the data where the va variable
riable lives, instead of holding a
reference that points to the data, as is the case with classes.
c lasses.
Recall that the variable’s
variable’s contents are copied when passing something between methods (an
argument or return value). For a reference type like classes, that means the reference is copied.
The calling method and the called method both have their own reference, but both references
point to the same object. For a value type like structs, the entire block of data is copied, and
each ends up with a full copy of the data. The same is true when assigning a value from one
local variable to another or working with expressions
ex pressions..
Structs are primarily useful for representing small data-related concepts that don’t have a lot
of behavior.
behavior. Representing a 2D point, as we did above, is a good candidate for a struct. A circle,
a line, or a matrix could also be good candidates. In situations where the concept is not a small
data-related concept, a class is usually better
bette r. Even still, some small data-related concepts are
still better as a class. We’ll analyze the class vs. struct decision in more depth in a moment.

MEMORY AND CON󰁓TRUCTOR󰁓


Because structs
differ from are value
the classes types,been
we have memory usage
making andpast.
in the constructors are two critical ways structs
Reference types, such as a class, can be null (Level 22). 22). In these cases, the memory for an
object doesn’t exist until it is explicitly created by calling a constructor with the new keyword.
For value types like structs, we don’t have that option. The variable’s
variable’s mere existence means its
memory must also exist, even before it has been initialized by a constructor.
constructor. This model has a
lot of implications that may be surprising.
First, while a constructor can be used to initialize data, invoking a constructor is not always
necessary. Consider this struct:
public struct PairOfInts
{
public int A; // These are public fields, which are usually best to avoid.

} public int B;

Now, look at this code with a PairOfInts local variable:


PairOfInts pair;
pair.A = 2;
Console.WriteLine(pair.A);

It calls no constructor but still assigns a value to its A field. The pair variable acts like two
separate local variables, each of which can be initialized and used like any other local
lo cal variable
but through a shared name.
Now imagine we add this class into the mix:
mix :
public class PairOfPairs
{
public PairOfInts _one;
public PairOfInts _two;
220 LEVEL 28 STRUCTS

public void Display()


{
Console.WriteLine($"{_one.A} {_one.B} and {_two.A} {_two.B}");
}
}

Once again, we can use these structs without calling a constructor. In this case, the structs are
initialized to default values by zeroing out their memory, meaning A and B of both _one and
_two will be 0 until somebody changes it.
No matter what constructors you give a struct, they may simply not be called!
Second, structs will always have a public parameterless constructor. If a class doesn’t define
any constructors, the compiler automatically generates a parameterless constructor for any
class you make. The compiler does the same thing for a struct. For a class, if you define a
different constructor,
constructor, the compiler no longer
long er makes a parameterless constructor.
constructor. For a struct,
the compiler will define a public parameterless constructor anyway. Y You
ou cannot get rid of the
public parameterless constructor. However, you may define this public, parameterless
constructor yourself if you need it to do something specific.
struc t. Consider this version of PairOfInts:
Third, field initializers are a bit weird in a struct.
public struct PairOfInts
{
public int A = 10;
public int B = -2;
}

These initializers do not always run when you use a PairOfInts. More specifically:
• Field and property initializers don’t ever run if no constructor is called.
• The compiler-generated constructor runs these initializers only if the struct has no
constructors.
• If you add your own constructors,
con structors, these initializers will only run as a part of constructors
you have defined, not as part of the compiler-generated one.
To ensure the third rule doesn’t catch you off guard, you will likely want to define your own
parameterless constructor when adding initializers to your fields or properties.
Y
You
ou don’t need to memorize all these rules. Just remember that it can be a tricky area. Don’t
just assume your code works, but check to ensure
ensure it does.

CLA󰁓󰁓E󰁓 V󰁓. 󰁓TRUCT󰁓


Classes and structs have a lot in common, but let’s take some time to compare the two and
describe when you might want each.
The main difference is that classes are reference types and structs are value types. We touched
on this in the previous section, but it means struct-typed variables store their data directly,
while class-typed variables store
store a reference, and thethe actual data lives elsewher
elsewhere.
e. (Now
(Now might
be a good time to re-read Level 14 if you’re still struggling with these differences.)

This one difference has a lot of ramifications, not the least of which is the differences in
constructors described in the previous section.
CLASSE
CLASSESS VS. STRUCTS 221
Another key difference is that structs cannot take on a null value, though we will see a way to
pretend in Level 32.
Because structs are value types, reading and writing values to variables involves copying the
whole pile of data around, not just a reference. Like with a double, when we copy a value
from one variable to another results in a copy:
PairOfInts first = new PairOfInts(2, 10);
PairOfInts second = first;

Here, second will get a copy of both the 2 and the 10 assigned to its fields. The same thing
we passed a PairOfInts to a method as an argument.
would happen if we
Additionally, inheritance does not work well when copying value types around (do a web
search for “object slicing” if you want to know more), so structs do not support it. A struct
cannot pick a base class (they all derive from ValueType, which derives from object).
Structs, however,
however, are allowed to implement interfaces.
Equality is also different for structs. As we saw in Level 14 14,, value types have value semantics—
two things are equal if all of their data members are equal. Any struct you create will
automatically have value semantics. The Equals method and the == and = operators are
automatically defined to compare the struct’s
struct’s fields for equality.

Choosing to Make a Class or a 󰁓truct


Given how similar structs and classes
cl asses are, you’re
you’re probably wondering how to decide
dec ide between
the two. Ultimately, the deciding factor should be if you need a reference type or a value
valu e type.
That’ss the main difference, and it should drive your selection.
That’
Structs are usually the better choice for small, data-focused types. A struct may be better if a
concept is primarily about representing data and not doing work. If the concept’s behavior
behavior is
important, then things like inheritance and polymorphism often are as well. You can’t get that
from a struct. That doesn’t mean a struct can’t have methods, but a struct’s methods are
usually focused on answering questions about the data instead of getting work done.
However, just because something focuses on data doesn’t mean a struct is always better. You
can’t get references to a struct like you can with a class. With a class, you can build a web of
interconnected objects that know about each other through references. Y You
ou can’t do the same
thing with structs.
The way structs and classes are managed in memory is also a driving force. Reference types
like classes always get allocated individually on the heap. Structs get allocated directly in
whatever contains them. That is sometimes
some times the stack and sometimes a larger object on the
heap (such as an array or class with value-typed fields). Therefore, instances of classes make
the garbage collector work harder,
harder, while structs don’t.
Let’s illustrate
illustrate that point with an example. Let’s say we have the following two types that differ
only by whether they are a struct (a value type) or a class (a reference type):
public struct CircleStruct
{
public double X { get; }
public double Y { get; }
public double Radius { get; }
public CircleStruct(double x, double y, double radius)
{
222 LEVEL 28 STRUCTS
X = x; Y = y; Radius = radius;
}
}

public class CircleClass


{
public double X { get; }
public double Y { get; }
public double Radius { get; }

public
{ CircleClass(double x, double y, double radius)
X = x; Y = y; Radius = radius;
}
}

Consider this code:


for (int number = 0; number < 10000; number++)
{
CircleStruct circle = new CircleStruct(0, 0, 10);
Console.WriteLine($"X={circle.X} Y = {circle.Y} Radius={circle.Radius}");
}

for (int number = 0; number < 10000; number++)


{
CircleClass circle = new CircleClass(0, 0, 10);
Console.WriteLine($"X={circle.X} Y = {circle.Y} Radius={circle.Radius}");
}

In the first loop, with structs, there is one variable designed to hold a single CircleStruct,
and because it is a local variable, it lives on the stack. That variable is big enough to contain
an entire CircleStruct, with 8 bytes for X, Y, and Radius for a total of 24 2 4 bytes. Every time
we get to that new CircleStruct(...) part, we re-initialize that memory location with
new data. But we reuse the memory location.
In the second loop, with classes, we still have a single variable on the stack, but that variable
is a reference type and will only hold
hol d references. This variable will be only 8 bytes (on a 64-bit
computer). However, each time we run new CircleClass(...), a new CircleClass
object is allocated on the heap. By the time we finish, we will have done that 10,000 times (and
used 240,000 bytes), and the garbage collector will need to clean them all up.
Structs don’t always have the upper hand with memory usage. Consider this scenario, where
we pass a circle
circle as an argument to a method 10,000 times:
CircleStruct circleStruct = new CircleStruct(0, 0, 10);
for (int number = 0; number < 10000; number++)
DisplayStruct(circleStruct);

CircleClass circleClass = new CircleClass(0, 0, 10);


for (int number = 0; number < 10000; number++)
DisplayClass(circleClass);

void DisplayStruct(CircleStruct circle) =>


Console.WriteLine($"X={circle.X} Y={circle.Y} Radius={circle.Radius}");

void DisplayClass(CircleClass circle) =>


Console.WriteLine($"X={circle.X} Y={circle.Y} Radius={circle.Radius}");
CLASSESS VS. STRUCTS
CLASSE 223
We only create
create one struct and class instance
instance here, but we repeatedly call the DisplayStruct
here, but
and DisplayClass methods. In doing so, the contents of circleStruct are copied to
DisplayStruct ’s circle parameter, and the contents of circleClass are copied to
DisplayClass ’s circle parameter repeatedly. For the struct, that means copying all 24
bytes of the data structure, for a total of 240,000 bytes copied. For the class, we’re
we’re only copying
the 8-byte reference and a total of 80,000
80 ,000 bytes, which is far less.
The bottom line is that you’ll get different memory usage patterns depending on which one
you pick. Those differences
differences play a key
key role in deciding whether to choose a class or a struct.
In short, you should consider a struct when you have a type that (1) is focused on data instead
of behavior,
behavior, (2) is small in size, (3) where you don’t need shared references, and (4)
(4),, and when
being a value type works to your advantage instead of against you. If any of those are not true,
you should prefer
prefer a class.
To give a few more examples, a point, rectangle, circle, and score could each potentially fit
those criteria, depending on how you’re using them.
I’ll let you in on a secret: many C# programmers, including some veterans, don’t fully grasp
the differences between a class and a struct and will always make a class. I don’t think this is
ideal, but it may not be so bad as a short-term strategy as you get more comfortable in C#.
Just don’t let that be your permanent strategy. I probably make 50 times as many classes as
structs, but a few strategically placed structs make a big difference.

Rules to Follow When Making 󰁓tructs


There are three guidelines that you should follow when you make a struct.
First, keep them small. That is subjective, but an 8-byte struct is fine, while a 200-byte struct
should generally be avoided. The costs of copying large structs add up.
Second, make structs immutable. Structs should represent a single compound value, and as
such, you should make its fields readonly and not have setters (not even private) for its
properties. (An init accessor is fine.) Doing this helps prevent situations where somebody
thought they had modified a struct value but modified a copy
c opy instead:
public void ShiftLeft(Point p) => p.X -= 10;

Assuming Point is a struct, the data is copied into p when you call this method. The variable
p’s X property is shifted, but it is ShiftLeft’s copy. The original copy is unaffected.
Making structs immutable sidesteps all sorts of bugs like this. If you want to shift a point to the
left, you make a new Point value instead, with its X property adjusted for the desired shift.
t hing you would do if it were just an int.
Making a new value is essentially the same thing
public Point ShiftLeft(Point p) => new Point(p.X - 10, p.Y);

With this change,


change, the calling method would do this:
Point somePoint = new Point(5, 5);
somePoint = ShiftLeft(somePoint);

Third, because struct values can exist without calling a constructor, a default, zeroed-out
struct should represent a valid value. Consider the LineSegment class below:
224 LEVEL 28 STRUCTS
public class LineSegment
{
private readonly Point _start;
private readonly Point _end;

public LineSegment() { }

// ...
}

When a new LineSegment is created, _start and _end are initialized to all zeroes.
Regardless of what constructors Point defines, they don’t get called here. Fortunately, a
Point whose X and Y values are 0 represents a point at the origin, which is a valid point.

BUILT-IN TYPE ALIA󰁓E󰁓


The built-in types that are value types (all eight integer types, all three floating-point types,
char, and bool) are not just value types but structs themselves.
themsel ves.
While we have used keywords ( int, double, bool, char, etc.) to refer to these types, the
keywords are aliases or shortcut names for their formal struct names. For example, int is an
alias for System.Int32. While rarely done, we could use these other names instead:

Int32 x = new Int32();


Int32 y = 0; // Or combined.
int z = new Int32(); // Or combined another way. It's all the same thing.
int w = new int(); // Yet another way...

The keyword version is simpler and nearly always preferred, but their aliases pop up from time
to time in documentation and sometimes in Visual Studio. Knowing the long name for these
types can help you understand what is going on. Here is the complete list of these aliases:
Built-In Type Alias For: Class or Struct?
bool System.Boolean struct
byte System.Byte struct
sbyte System.SByte struct
char System.Char struct
decimal System.Decimal struct
double System.Double struct
float System.Single struct
int System.Int32 struct
uint System.UInt32 struct
long System.Int64 struct
ulong System.UInt64 struct
object System.Object class
short System.Int16 struct
ushort System.UInt16 struct
string System.String class

Ignoring the System part, many of these are the same except for capitalization. C# keywords
are all lowercase, while types are usually UpperCamelCase, which explains that difference.
BOXING AND UNBOXING 225
These names follow the same naming pattern we saw with Convert’s various methods.
(Convert’s method names actually come from these names, not the other way around.)
But the keyword and the longer type name are
a re true synonyms. The following two are identical:
int.Parse("4");
Int32.Parse("4");

BOXING AND UNBOXING


Classes and structs all ultimately share the same base class: object. Classes derive from
object directly (unless they choose another base class), while structs derive from the special
System.ValueType class, which is derived from object. This creates an interesting
situation:
object thing = 3;
int number = (int)thing;

Some fascinating things are going on here. The number 3 is an int value, and int-typed
variables contain the value directly, than a reference. But variables of the object type
directly, rather than
store references. It seems we have conflicting behaviors.
behaviors. How does the above code work?
When a struct value is
is assigned to a variable that stores references,
references, like the first line above, the
data is pushed out to another location on the heap, in its own little container—a box. A
reference to the box is then stored in the thing variable. This is called a boxing conversion.
The value is copied onto the heap, allowing you to grab a reference to it.
On the second line, the inverse happens. After ensuring that the type is correct, the box’s
contents are extracted—an unboxing conversion—and copied into the number variable.
Y
You
ou might hear
he ar a C# programmer phrase this as, “The 3 is boxed in the first line, and then
unboxed on the second line.”
As shown above,
above, boxing can
can happen implicitly, while unboxing must
must be explicit with a ca
cast.
st.
The same thing happens when we use an interface type with a value type. Suppose a value
type implements an interface, and you store it in a variable that uses an interface
inter face type. In that
case, it must box the value before storing it because interface types store references.
ISomeInterface thing = new SomeStruct();
SomeStruct s = (SomeStruct)thing;

Boxing and unboxing are efficient but not free. If you are boxing and unboxing frequently,
perhaps you should make it a class instead of a struct.

Challenge Room Coordinates


Coordinates 50 XP
The time to enter the Fountain of Objects draws closer. While you don’t know what to expect, you have
found some scrolls that describe the area in ancient times. It seems to be structured as a set of rooms
in a grid-like arrangement.
Locations of the room may be represented as a row and column, and you take it upon yourself to try to
capture this concept with a new struct definition.
Objectives:
• Create a Coordinate struct that can represent a room coordinate with a row and column.
226 LEVEL 28 STRUCTS
• Ensure Coordinate is immutable.
• Make a method to determine if one coordinate is adjacent to another (differing only by a single row
or column).
• Write a main method that creates a few coordinates and determines if they are adjacent to each
other to prove that it is working correctly.
LEVEL 29
RECORD󰁓

󰁓peedrun
• Records are a compact alternative notation for defining a data-centric class or struct: public
record Point(float X, float Y);
• The compiler automatically generates a constructor, properties, ToString, equality with value
semantics, and deconstruction.
• You can add additional members or provide a definition for most compiler-synthesized members.
• Records are turned into classes by default or into a struct ( public record struct Point(...)).
• Records can be used in a with expression: Point modified = p with { X = -2 };

RECORD󰁓
C# has an ultra-compact way to define certain kinds of classes or structs. This compact
notation is called a record. The typical situation where a record makes sense is when your type
is little more than a set of properties—a data-focused entity.
The following shows a simple Point record, defined with an X and Y property:
public record Point(float X, float Y); // That's all.

The compiler will expand the above code into something like this:
public class Point
{
public float X { get; init; }
public float Y { get; init; }

public Point(float x, float y)


{
X = x; Y = y;
}

}
When you define
define a record,
record, you get several features
features for free.
free. It
It starts with
with properties that match
the names you provided in the record definition and a matching constructor.
constructor. Note that these
228 LEVEL 29 RECORDS
properties are init properties, so the class is, by default, immutable. But that’s only the
beginning. We get several other things for free: a nice string representation, value semantics,
deconstruction, and creating copies with tweaks. We’ll
We’ll look at each of these features below.

󰁓tring Representation
Records automatically override the ToString method with a convenient, readable
on of its data. For example, new Point(2, 3).ToString(), will produce this:
representation
representati

Point { X = 2, Y = 3 }
When a type’s
type’s data is the focus, a string representation like this is a nice bonus. You
You could do
this manually by overriding ToString (Level 26), 26), but we get it free with records.
records.

Value 󰁓emantics
Recall that value semantics are when the thing’s value or data counts, not its reference. While
structs have value semantics automatically, classes have reference semantics by default.
However, records automatically have value semantics. In a record, the Equals method, the
== operator, and the = operator are redefined to give it value semantics. For example:
Point a = new Point(2, 3);
Point b = new Point(2, 3);
Console.WriteLine(a == b);
Though a and b refer to different instances and use separate memory locations, this code
displays True because the data are a perfect match, and the two are considered equal. Level
41 describes making operators for your own types, but we get it for free with a record.
rec ord.

Deconstruction
In Level 17, we saw how to deconstruct a tuple, unpacking the data into separate variables:
(string first, string last) = ("Jack", "Sparrow");

Y
You
ou can do the same
same thing with records:
Point p = new Point(-2, 5);
(float x, float y) = p;
In Level 34, we will see how you can add deconstruction to any type, but records get it for free.

with 󰁓tatements
Given that records are immutable by default, it is not uncommon to want a second copy with
most of the same data, just with one or two of the properties tweaked. While you could always
just call the constructor,
constructor, passing in the
the right values, records
records give you extra
extra powers in the form
of a with statement:
Point p1 = new Point(-2, 5);
Point p2 = p1 with { X = 0 };

Y
You
ou can replace many properties
properties at once by separating
separating them with commas:
Point p3 = p1 with { X = 0, Y = 0 };
ADV
ADVANCED
ANCED SCENARIOS 229
In this case, since we’ve replaced all the properties with new values, it might have been better
just to write new Point(0, 0), but that code shows the mechanics.
The plumbing that the compiler generates to facilitate the with statement is not something
you can add to your own types. This is a record-only
record-only feature (at least
least for now).

ADVANCED 󰁓CENARIO󰁓
Most records you define will be a single line, similar to the Point record defined earlier
e arlier.. But
when you have the need, they can be much more.
more. You
You can add additional
additional members and make
your own definition
definition to supplant most compiler-generated members.
members.

Additional Members
In any record, you can add any members you need to flesh out your record type, just like a
class. The following shows a Rectangle record with Width and Height properties and then
adds in an Area property, calculated from the rectangle’s width
width and height
height::
public record Rectangle(float Width, float Height)
{
public float Area => Width * Height;
}

There are no limits to what members you can add to a record.

Replacing 󰁓ynthesized Members


The compiler generates quite a few members to provide the features that make records
attractive. While you can’t remove any of those features, you can customize most of them to
meet your needs. For example, as we saw, the Point record defines ToString to display text
like Point { X = 2, Y = 3 } . If you wanted your Point record to show it like (2, 3)
instead, you could simply add in your own definition for ToString :
public record Point(float X, float Y)
{
public override string ToString() => $"({X}, {Y})";
}
In most situations where the compiler would normally synthesize a member for you, if it sees
that you’ve provided
provided a definition, it will use your version instead.
One use for this is defining the properties as mutable properties or fields instead of the default
init-only property. The compiler will not automatically assign initial values to your version
if you do this. You’ll
You’ll want to initialize them yourself:
public record Point(float X, float Y)
{
public float X { get; set; } = X;
}

Y
You
ou cannot supply a definition for the constructor (though this limitation is removed if you
make a non-positional record, as described later in this section).
You cannot define many of the equality-related members, including Equals(object) , the
You
== operator, and the = operator. However, you can define Equals(Point) , or whatever the
230 LEVEL 29 RECORDS
record’s type is. Equals(object), ==, and = each call Equals(Point), so you can
usually achieve what you want, despite this limitation.

Non-Positional Records
Most records will include a set of properties in parentheses after the record name. These are
positional records because the properties have a known, fixed ordering (which also matters
for deconstruction). These parameters are not strictly required. Y
You
ou could also write a simple
record like this:
public record Point
{
public float X { get; init; }
public float Y { get; init; }
}

In this case, you wouldn’t get the constructor or the ability to do deconstruction (unless you
add them in yourself ), but otherwise, this is the same
same as any other record.

󰁓TRUCT- AND CLA󰁓󰁓-BA󰁓ED RECORD󰁓


The compiler turns records into classes by default because this is the more common
com mon scenario.
However,, you can also make a record struct instead:
However
public record struct Point(float X, float Y);

This code will now generate a struct instead of a class, bringing along all the other things we
know about structs vs. classes (in particular,
particular, this is a value type instead of a reference type).
A record struct
struct creates properties slightly different from
from class-based structs. They are defined
as get/set properties instead of get/init. The record struct above becomes something
more like this:
public struct Point
{
public float X { get; set; }
public float Y { get; set; }

public Point(float x, float y)


{
X = x; Y = y;
}
}

Records are class-based, by default, but if you want to call it out specifically, you can write it
out explicitly:
public record class Point(float X, float Y);

This definition is no different than if it were defined without the class keyword, other than
drawing a bit more attention to the choice of making the record class-based.
Whichever way youyou go, you
you can generally expect the same
same things of a record a
ass you can of the
class or struct it would become. For example, since you can make a class abstract or
sealed, those are also options for class-based
c lass-based records.
records.
WHEN TO USE A RECORD 231

Inheritance
Class-based records
records can also participate in inheritance
inher itance with a few limitations. Records cannot
derive from normal classes, and normal classes cannot derive from records.
The syntax for inheritance in a record is worth
wo rth showing:
public record Point(float X, float Y);
public record ColoredPoint(Color Color, float X, float Y) : Point(X, Y);

WHEN TO U󰁓E A RECORD


When defining
defining a class or a struct,
struct, you have the option to use the record
record syntax. So when should
you make a record, and when should you you create a normal
normal class or struct?
The record syntax conveys a lot of information in a very short space.
s pace. If the feature set of records
fits your needs, you should generally prefer the record syntax. Records give you a concise way
to make a type with several properties and a constructor to initialize them. They also give you
a nice string representation, value semantics, deconstruction, and the ability to use with
statements. If that suits your needs, a record is likely the right choice. If those features get in
your way or are unhelpful, then a regular
regular class or struct is the better choice.
Y
You
ou should also consider records
records as a possible alternative
alternative to tuples. I usually go with a record
record

in these
actual cases.for
names Youtheneed
typeto go its
and to members.
the troubleFor
of formally
me, that defining
is usuallythe record
worth thetype,
smallbut you get
cost.
Fortunately, it isn’t usually hard to swap out one of these options for another. If you change
your mind, you can
can change the code. (And your intuition
intuition will get better with practice.)

Challenge
Challe nge War Preparations
Preparation s 100 XP
As you pass through the city of Rocaard, two blacksmiths, Cygnus and Lyra, approach you. “We know
where this is headed. A confrontation with the Uncoded One’s forces,” Lyra says. Cygnus continues,
“You’re going to need an army at your side—one prepared to do battle. We forge enchanted swords and
will do everything we can to support this cause. We need the Power of Programming to flow unfettered
too. We want to help, but we can’t equip an entire army without the help of a program to aid in crafting
swords.” They describe the program they need, and you dive in to help.
Objectives:
• Swords can be made out of any of the following materials: wood, bronze, iron, steel, and the rare
binarium. Create an enumeration to represent the material type.
• Gemstones can be attached to a sword, which gives them strange powers through Cygnus and Lyra’s
touch. Gemstone types include emerald, amber, sapphire, diamond, and the rare bitstone. Or no
gemstone at all. Create an enumeration to represent a gemstone type.
• Create a Sword record with a material, gemstone, length, and crossguard width.
• In your main program, create a basic Sword instance made out of iron and with no gemstone. Then
create two variations on the basic sword using with expressions.
• Display all three sword instances with code like Console.WriteLine(original);.
LEVEL 30
GENERIC󰁓

󰁓peedrun
• Generics solve the problem of making classes or methods that would differ only by the types they
use. Generics leave placeholders for types that can be filled in when used.

Defining
... a generic
} ... } class: public class List<T> { public T GetItemAt(int index) {
• You can also make generic methods and generic types with multiple ty
type
pe parameters.
parameters.
• Constraints allow you to limit what can be used for a generic type argument while enabling you to
Constraints
know more about the types being used: class List<T> where T : ISomeInterface { }

We’ll look at a powerful feature in C# called generics (generic types and generic methods) in this
We’ll
level. We’ll start with the problem this feature solves and then see how generics solve it. In
Level 32, we will see a few existing generic types that will make your life a lot eeasier
asier..

THE MBy
OTIVATION FOR GENERIC󰁓
now, you’ve probably noticed that arrays have a big limitation: you can’t easily change
c hange their
size by adding and removing items. The best you can do is copy the contents of the array to a
new array, making any necessary changes in the process, and then update your array variable:
int[] numbers = new int[] { 1, 2, 3 };
numbers = AddToArray(numbers, 4);

int[] AddToArray(int[] input, int newNumber)


{
int[] output = new int[input.Length + 1];

for (int index = 0; index < input.Length; index++)


output[index] = input[index];

output[^1] = newNumber;
return output;
}
THE MOTIVA
MOTIVATION
TION FOR GENER
GENERICS
ICS 233
With your understanding of objects and classes, you might say to yourself, “I could make a
class that handles this for me. Then whenever I need it, I can just use the class instead of an
array, and growing and shrinking the collection will happen automatically.” Indeed, this
would make a great reusable class.
class. What
What an excellent idea! You
You start with this:
public class ListOfNumbers
{
private int[] _numbers = new int[0];

public int GetItemAt(int index) => _numbers[index];


public void SetItemAt(int index, int value) => _numbers[index] = value;

public void Add(int newValue)


{
int[] updated = new int[_numbers.Length + 1];

for (int index = 0; index < _numbers.Length; index++)


updated[index] = _numbers[index];

updated[^1] = newValue;

_numbers = updated;
}
}

This ListOfNumbers class has a field that is an int array. It includes methods for getting
and setting items at a specific index in the list. Also, it includes an Add method, which tacks a
new int to the end of the collection, copying everything over to a new, slightly longer array,
and placing the new value at the end. The code in Add is essentially the same as our
AddToArray method earlier. I won’t add code for removing an item, but you could do
something similar.
Now we can use this class like this:
ListOfNumbers numbers = new ListOfNumbers();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
Console.WriteLine(numbers.GetItemAt(2));

This is a better solution because it is object-oriented. Instead of having a loose array and a
loose method to work with it, the two are combined. The class handles growing the collec
collection
tion
as needed, and the outside world is free to assume it does the job assigned to it. And it is
reusable! With this class defined, any time we want a growable collection of ints, we make a
new instance of ListOfNumbers, and off we go.
I do have one complaint. With arrays, you can use the indexing operator. numbers[0] is
cleaner than numbers.GetItemAt(0). We can solve that problem with the tools we’ll learn
in Level 41. For now, we’ll just live with it.
However, there’s a second, more substantial problem. We can make instances of ListOf
Numbers whenever we want, but what if we need ne ed it to be strings instead? ListOfNumbers
is built around ints. It is useless if we need the string type.
Using only tools we already know, we have two options. We could just create a ListOf
Strings class:
234 LEVEL 30 GENERICS
public class ListOfStrings
{
private string[] _strings = new string[0];

public string GetItemAt(int index) => _strings[index];


public void SetItemAt(int index, string value) => _strings[index] = value;

public void Add(string newValue) { /* Details skipped */ }


}

This has potential, though it isn’t great. What if we need a list of bools? A list of doubles? A
list of points? A list of int[]? How many of these do we make? We would have to copy and
paste this code repeatedly, making tiny tweaks to change the type each time. In Level 23, we
said that designs with duplicate code are worse than ones that do not. This approach results
in a lot of duplicate code. Imagine making 20 of these, only to discover a bug in them!
The second approach would be just to use object. With object, we can use it for anything:
public class List
{
private object[] _items = new object[0];

public object GetItemAt(int index) => _items[index];


public void SetItemAt(int index, object value) => _items[index] = value;

public void Add(object newValue) { /* Details skipped */ }


}

Which could get used like this:


List numbers = new List();
numbers.Add(1);
numbers.Add(2);

List words = new List();


words.Add("Hello");
words.Add("World");

Unfortunately, this also has a couple of big drawbacks. The first is that the GetItemAt
method (and others) return an object, not an int or a string. We must cast
cast it
it::
int first = (int)numbers.GetItemAt(0);

The second drawback is that we have thrown out all type checking that the compiler would
normally help us with. Consider this code, which compiles but isn’t good:
List numbers = new List();
numbers.Add(1);
numbers.Add("Hello");

Do you see the problem? From its name, numbers should contain only numbers. But we just
dropped a string into it. The compiler cannot detect this because we are using object, and
string is an object. This code won’t fail until you cast to an int, expecting it to be one,
only to discover it was a string.

Neither of these solutions is perfect. But this is where generics save the day.
DEFINING A GENERIC TYPE 235

DEFINING A GENERIC TYPE


A generic type is a type
type definition (class,
(class, struct,
struct, or interface) that
that leaves
leaves a placeholder for some
of the types it uses.
use s. This is conceptually similar to making m methods
ethods with parameters, allowing
the outside world to supply a value. The easiest way to show a generic type is with an example
of a generic List class:
public class List<T>
{
private T[] _items = new T[0];
public T GetItemAt(int index) => _items[index];
public void SetItemAt(int index, T value) => _items[index] = value;

public void Add(T newValue)


{
T[] updated = new T[_items.Length + 1];

for (int index = 0; index < _items.Length; index++)


updated[index] = _items[index];

updated[^1] = newValue;

_items = updated;
}
}

Before going further, I’m going to interrupt with an important note. The code above defines
our own custom generic List class. You might be thinking, “I can use something like this!”
But there is already an existing generic List class that does all of this and more, is well tested,
and is optimized. This code illustrates generic types, but once we learn about the official
List<T> class (Level 32),
32), we should be using that instead. Now back to our discussion.
When defining the class, we can identify a placeholder for a type in angle brackets (that <T>
thing). This placeholder type is called a generic type parameter. It is like a method parameter,
except it works at a higher level
le vel and stands in for a specific type that will be cchosen
hosen later.
later. It can
be used throughout the class, as is done in several places in the above code. When this
List<T> class is used, that code will supply the specific type it needs instead of T. For
example:
List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);

In this case, int is used as the generic type argument (like passing an argument to a method
when you call it). Here, int will be used in all
al l the places that T was listed. That means the Add
method will have an int parameter, and GetItemAt will return an int.
Without defining additional type argument such as string:
additional types, we can use a different type
List<string> words = new List<string>();
words.Add("Hello");
words.Add("World");

Importantly, this potential problem is now caught by the compiler:


words.Add(1); // ERROR!
236 LEVEL 30 GENERICS
The variable words is a List<string> , not a List<int>. The compiler can recognize the
type-related issue and flag it. We have created the best of both worlds:
worlds : we only nee
needd to create
c reate
a single type but can still retain type safety.
By the way, many C# programmers will read or pronounce List<T> as “list of T” and
List<int> as “list of int.”
There was nothing magical about the name T. We could have called it M, Type, or _wakawaka.
However, there are two conventions for type names: single, capital letters ( T, K, V, etc.) or a
capital T followed by some more descriptive word or phrase, like TItem. If you only have a
parameter, T is virtually universal.
single generic type parameter,

Multiple Generic Type Parameters


Y
You
ou can also have multiple generic type parameters by listing them in the angle brackets,
separated by commas:
public class Pair<TFirst, TSecond>
{
public TFirst First { get; }
public TSecond Second { get; }

public Pair(TFirst first, TSecond second)


{
First = first;
Second = second;
}
}

Which could be used like this:


Pair<string, double> namedNumber = new Pair<string, double>("pi", 3.1415926535);
Console.WriteLine($"{namedNumber.First} is {namedNumber.Second}");

Generic types can end up with rather complicated names. Pair<string, double> is a
long name, and it could be worse. Instead of string, it could be a List<string> or even
another Pair<int, int>. This results in nested generic types with extremely long names:
Pair<Pair<int, int>, double>. While I have been avoiding var for clarity in this book,
long, complex names like this are why some people prefer var or new() (without listing the
type); without it, this complicated name shows up twice, making the code hard to understand.

Inheritance and Generic Types


Generic classes and inheritance can be combined. A generic class can derive from normal
non-generic classes or other generic classes, and normal classes can be derived from generic
classes. When doing this, you have
have some options for handling gener
generic
ic types in the base class.
The simplest thing is just to keep the generic
gener ic type parameter open:
public class FancyList<T> : List<T>
{ ... }

The base class’s


class’s generic type parameter stays as a generic
gene ric type parameter
p arameter in the derived class.
Or a derived class can close the generic type parameter, resulting in a derived class that is no
longer generic:
GENERIC METHODS 237
public class Polygon : List<Point>
{ ... }

With this definition, Polygon is a subtype of List<Point>, but you cannot make polygons
using anything besides Point. The generic-ness is gone.
Of course, you can close some generic
gener ic types, leave others open, and simultaneously introduce
additional generic type parameters. Tricky situations like these are rare, though.

GENERIC METHOD󰁓
Sometimes, it isn’t a type that needs to be generic but a single method. You can define generic
methods by putting generic type parameters after a method’s name but before its parentheses:
public static List<T> Repeat<T>(T value, int times)
{
List<T> collection = new List<T>();

for (int index = 0; index < times; index++)


collection.Add(value);

return collection;
}

Y
You
ou can use generic type parameters for method parameters and return types, as shown
above. You
You can then use this like so:
List<string> words = Repeat<string>("Alright", 3);
List<int> zeroes = Repeat<int>(0, 100);

Generic methods do not have to live in a generic type. They can, and often are, defined in
regular non-generic types.
When using a generic
ge neric method, the compiler can often infer the types you use based on the
parameters you pass into the method itself. For example, because Repeat<string>
("Alright", 3) passes in a string as the first parameter, the compiler can tell that you
want to use string as your generic type argument, and you can leave it out:

List<string> words = Repeat("Alright", 3);


Y
You
ou usually only need to list
li st the generic
gener ic type argument when the compiler either can’t
can ’t infer
the type or is inferring the wrong type.

GENERIC TYPE CON󰁓TRAINT󰁓


By default, any type can be used
use d as an argument
argume nt for a generic type parameter.
parameter. The tradeoff is
that within the generic type, little is known about what type will be used, and therefore, the
generic type can do little with it. For our List<T> class, this was not a problem. It was just a
container to hold several items of the same type. On the other hand, if we constrain or limit
the possible choices, we can know more about the type being used and do things with it.

To showthat
classes anall
example,
derivedlet’s back to our Asteroids
fromgoa GameObject typesay
class. Let’s hierarchy. We had
GameObject hadseveral different
an ID property
used to identify each thing in the game uniquely:
238 LEVEL 30 GENERICS
public abstract class GameObject
{
public int ID { get; }
// ...
}

If we give a generic type a constraint that it must be derived from GameObject, then we will
know that it is safe to use any of the members GameObject defines:
public class IDList<T> where T : GameObject
{
private T[] items = new T[0];

public T? GetItemByID(int idToFind)


{
foreach (T item in items)
if (item.ID == idToFind)
return item;

return null;
}

public void Add(T newValue) { /* ... */ }


}

That where T : GameObject is called a generic type constraint. It allows you to limit what
type arguments can be used for the given type parameter. IDList is still a generic type. We
can create an IDList<Asteroid> that ensures only asteroids are added or an
IDList<Ship> that can only use ships. But we can’t make an IDList<int> since int isn’t
derived from GameObject. We reduce how generic the IDList class is but increase what we
know about things going into it, allowing us to do more with it.
If you have several type parameters, you can constrain each of them with their own where:
public class GenericType<T, U> where T : GameObject
where U : Asteroid
{
// ...
}

There are many different constraints you can place on a generic type parameter. The above,
where you list another type that the argument must derive from,
from, is perhaps
perhaps the simplest.
You can also use the class and struct constraints to demand that the argument be either
You
a class (or a reference type) or a struct (or a value type): where T : class. The class
constraint will assume usages of the generic
gener ic type parameter do not allow null as an option. By
comparison, the class? constraint will assume usages of the generic type parameter allow
null as an option.
There is also a new() constraint (where T : new()), which limits you to using only onl y types
that have a parameterless constructor.
constructor. This allows you to create new instances of the gene
generic
ric
type parameter (new T()). Interestingly, there is no option for other constructor constraints.
The parameterless constructor is the only one.
Y
You
ou can also define constraints in relation to other generic type parameters if you have more
than one: public class Generic<T, U> where T : U { ... }, or even where T
: IGenericInterface<U>. This is rare but useful in situations that need it.
THE DEFAUL
DEFAULT
T OPERATOR 239
topic s. The unmanaged constraint demands that the
Three other constraints deal with future topics.
46) . The struct? allows for nullable structs (Level 32).
thing be an unmanaged type (Level 46). 32).
The nonnull constraint is like a combination of class and struct constraints (without the
question marks), allowing for anything that is not null.
Y
You
ou don’t need to memorize all of these different constraints. You’ll spend far more time
working with generic types than
than making
making them (Level 32)
32). When you make a generic type, most
of the time, you either won’t have any constraints or use a simple where T : Some
SpecificType . Just remember that there are many kinds of constraints, giving you control
of virtually any important aspect of the types being used as a generic type argument.

Multiple Constraints
Y
You
ou can define multiple constraints
constraints for each generic type parameter by sepa
separating
rating them with
commas. For example, the following requires T to have a parameterless constructor and to be
a GameObject :
public class Factory<T> where T : GameObject, new() { ... }

Within this Factory<T> class, you would be able to create new instances
i nstances of T because of the
new() constraint and use any properties or methods on GameObject , such as ID, because
of the GameObject constraint. Each constraint limits what types can be used for T and gives
power within the class to do useful stuff with T because you know more about it.
you more power
Not every constraint can be combined with every other constraint. This limitation is either
because two constraints conflict or one is made redundant by another.
another. For example, you can’t
use both the class and struct constraints simultaneously. Also, you can’t combine the
struct and new() constraints because the struct constraint already guarantees you have
a public, parameterless constructor.
constructor.
The ordering of generic type constraints
c onstraints also matters.
matters. For example, calling out a specific type
(like GameObject above) is expected to come first, while new() must be last. The rules are
hard to describe and remember; it is usually easiest to just write them out and let the compiler
point out any problems. In truth, you will only rarely
rarely run into issues like this; multiple generic
gene ric
type constraints are rare.

Constraints on Methods
Generic type constraints can also be applied to methods by listing them after the method’s
parameter list but before its body:
public static List<T> Repeat<T>(T value, int times) where T : class { ... }

THE DEFAULT OPERATOR


When using generic types, you may find some uses for the default operator, which allows
you to get the default value
value for any type.
type. (This isn’t just
just limited to generic types and methods,
but it is perhaps the most useful place.)

The basic
result form
will be theofdefault
this operator isthat
value for to place
type.the
Forname of the
example, type in parentheses
default(int) after it. to
will evaluate The
0,
default(bool) will evaluate to false, and default(string) will evaluate to null.
240 LEVEL 30 GENERICS
However, in most cases, a simple 0, false, or null is simpler code that doesn’t leave people
scratching their heads to remember if the default for bool was true or false. If the type can
be inferred, you can leave out the type and parentheses and just use a plain default.
Where default shows its power is with generics. default(T) will produce the default,
regardlesss of what type T is. If we go back to our Pair<TFirst, TSecond>, we could make
regardles
a constructor that uses default values:
public Pair()
{ First = default; // Or default(TFirst), if the compiler cannot infer it.
Second = default; // Or default(TSecond), if the compiler cannot infer it.
}

This seems more useful than it is. You still know nothing about the vvalue
alue you just created, so
you can do little with
with it afterward. But it does have its occasional time and place.

Challenge Colored Items 100 XP


You have a sword, a bow, and an axe in front of you, defined like this:
public class Sword { }
public class Bow { }
public class Axe { }

You want to associate a color with these items (or any item type). You could make ColoredSword
derived from Sword that adds a Color property, but doing this for all three item types will be
painstaking. Instead, you define a new generic ColoredItem class that does this for any item.
Objectives:
• Put the three class definitions above into a new project.
• Define a generic class to represent a colored item. It must have properties for the item itself (generic
in type) and a ConsoleColor associated with it.
• Add a void Display() method to your colored item type that changes the console’s foreground
color to the item’s color and displays the item in that color. ( Hint: It is sufficient to just call
ToString() on the item to get a text representation.)
• In your main method, create a new colored item containing a blue sword, a red bow, and a green axe.
Display all three items to see
s ee each item displayed in its color.
color.
LEVEL 31
THE FOUNTAIN OF OBJECT󰁓

󰁓peedrun
• This level contains no new C# information. It is a large multi-part program to complete to hone your
programming
progra mming skills.

Narrative Arrival at the Caverns


You have made your way to the Cavern of Objects, high atop jagged mountains. Within these caverns
lies the Fountain of Objects, the one-time source of the River of Objects that gave life to this entire
island. By returning the Heart of Object-Oriented Programming—the gem you received from Simula
after arriving on this island—to the Fountain of Objects, you can repair and restore the fountain to its
former glory.
The cavern is a grid of rooms, and no natural or human-made light works within due to unnatural
darkness. You can see nothing, but you can hear and smell your way through the caverns to find the
Fountain of Objects, restore it, and escape to the exit.

The cavern is full of dangers. Bottomless pits and monsters lurk in the caverns, placed here by the
Uncoded One to prevent you from restoring the Fountain of Objects and the land to its former glory.
By returning the Heart of Object-Oriented Programming to the Fountain of Objects, you can save the
Island of Object-Oriented Programming!

This level contains several challenges that together build the game The Fountain of Objects.
This game is based on the classic
cl assic game Hunt the Wumpus with some thematic tweaks.
Y
You
ou do not need to complete every challenge listed here. There are two ways to proceed.
Option 1 is to complete the base game
g ame (described first) and then pick two expansions. Option
2 is to start with the solution to the main challenge I provide on the book’s website and then
do five expansions.`

Option
game in1any
gives
wayyou
youmore practice
see fit. Optionwith object-oriented
2 might be better fordesign
peopleand
whoallows
are stillyou to build
hesitant the
about
object-oriented design, as it gives you a chance to work in somebody else’s code that provides
242 LEVEL 31 THE FOUNTAIN OF OBJECTS
some foundational elements as a starting point. (Though with Option #2, you will have to
begin by understanding how the code works so that you can enhance it.)
I recommend reading through all of the challenges and spending a few minutes thinking of
how you might solve each before deciding.
This next point cannot be understated: this is by far the most formidable challenge we have
undertaken in this book and only somewhat less demanding than the Final Battle challenge.
Completing this will take time—even if you are experienced.
experienc ed. But the real learning comes when
you get yourbut
challenges, hands
don’tdirty in theup
get hung code. Expect
on it. this to take
For example, much
if you are longer than
genuinely any on
stuck previous
some
particular challenge, try the other ones instead. If you are still stuck, look at the solutions
provided
provid ed on the book’s website to see how others solved it, then take a break for a few minutes
so that you aren’t copying and pasting through memorization, and give it another try. That
still counts for full points.

THE MAIN CHALLENGE


Boss Battle The Fountain of Objects 500 XP
The Fountain of Objects game is a 2D grid-based world full of rooms. Most rooms are empty, but a few
are unique
Fountain of rooms.
Objects.One room is the cavern entrance. Another is the fountain room, containing the
The player moves through the cavern system one room at a time to find the Fountain of Objects. They
activate it and then return to the entrance room. If they do this without falling into a pit, they win the
game.
Unnatural darkness pervades the caverns, preventing both natural and human-made light. The player
must navigate the caverns in the dark, relying on their sense of smell and hearing to determine what
room they are in and what dangers lurk in nearby rooms.
This challenge serves as the basis for the other challenges in this level. It must be completed before
before the
others can be started. The requirements of this game are listed below.
Objectives:
• The world consists of a grid of rooms, where each room can be referenced by its row and column.
North is up, east is right, south is down, and west is left:

• The game’s flow works like this: The player is told what they can sense in the dark (see, hear, smell).
Then the player gets a chance to perform some action by typing it in. Their chosen action is resolved
THE MAIN CHALLENGE 243
(the player moves, state of things in the game changes, checking for a win or a loss, etc.). Then the
loop repeats.
• Most rooms are empty rooms, and there is nothing to sense.
• The player is in one of the rooms and can move between them by typing commands like the
following: “move north”, “move south”, “move east”, and “move west”. The player should not be able
to move past the end of the
t he map.
• The room at (Row=0, Column=0) is the cavern entrance (and exit). The player should start here. The
player can sense light coming from outside the t he cavern when in this room. (“You see light in this room
coming from outside the cavern. This is the entrance.”)
• The room at (Row=0, Column=2) is the fountain room, containing the Fountain of Objects itself. The
Fountain can be either enabled or disabled. The player can hear the fountain but hears different
things depending
depe nding on if it is on
o n or not. (“You
(“You hear water dripping in this
t his room. The Fountain of Objects
is here!” or “You
“You hear the rushing waters
wate rs from the Fountain
Fount ain of Objects. It has been reactivated!”)
react ivated!”) The
fountain is off initially. In the fountain room, the player can type “enable fountain” to enable it. If the
player is not in the fountain room and runs this, there should be no effect, and the player should be
told so.
• The player wins by moving to the fountain room, enabling the Fountain of Objects, and moving back
to the cavern entrance. If the player is in the entrance and the fountain is on, the player wins.
• Use different colors to display the different types of text in the console window. For example,
narrative items (intro, ending, etc.) may be magenta, descriptive text in white, input from the user
in cyan, text describing entrance light in yellow, messages about the fountain in blue.
• An example of what the program might look like is shown below:
----------------------------------------------------------------------------------
You are in the room at (Row=0, Column=0).
You see light coming from the cavern entrance.
What do you want to do? move east
----------------------------------------------------------------------------------
You are in the room at (Row=0, Column=1).
What do you want to do? move east
----------------------------------------------------------------------------------
You are in the room at (Row=0, Column=2).
You hear water dripping in this room. The Fountain of Objects is here!
What do you want to do? enable fountain
----------------------------------------------------------------------------------
You are in the room at (Row=0, Column=2).
You hear the rushing waters from the Fountain of Objects. It has been reactivated!
What do you want to do? move west
----------------------------------------------------------------------------------
You are in the room at (Row=0, Column=1).
What do you want to do? move west
----------------------------------------------------------------------------------
You are in the room at (Row=0, Column=0).
The Fountain of Objects has been reactivated, and you have escaped with your life!
You win!

• Hint: You may find two-dimensional arrays (Level 12) helpful in representing a 2D grid-based game
world.
• Hint: Remember your training! You do not need to solve this entire problem all at once, and you do
not have to get it right in your first attempt. Pick an item or two to start and solve just those items.
Rework until you are happy with it, then add the next item or two.
244 LEVEL 31 THE FOUNTAIN OF OBJECTS

EXPAN󰁓ION󰁓
The following six challenges extend the basic Fountain of Objects game in different ways. If
you did the core Fountain
Fountain of Objects challenge above, pick two of the following challenges. If
you choose the
the Expansions path and start with my code from
from the website,
website, complete five
five of the
following.

Boss Battle 󰁓mall, Medium, or Large 100 XP


The larger the Cavern of Objects is, the more difficult the game becomes. The basic game only requires
a small 4×4 world, but we will add a medium 6×6 world and a large 8×8 world for this challenge.
Objectives:
• Before the game begins, ask the player whether they want to play a small, medium, or large game.
Create a 4×4 world if they choose a small world, a 6×6 world if they choose a medium world, and an
8×8 world if they choose a large world.
• Pick an appropriate location for both the Fountain Room and the Entrance room.
• Note: When combined with the Amaroks, Maelstroms, or Pits challenges, you will need to adapt the
game by adding amaroks, maelstroms, and pits to all three sizes.

Boss Battle Pits 100 XP


The Cavern of Objects is a dangerous place. Some rooms open up to bottomless pits. Entering a pit
means death. The player can sense a pit is in an adjacent room because a draft of air pushes through the
pits into adjacent rooms. Add pit rooms to the game. End the game if the player stumbles into one.
Objectives:
• Add a pit room to your 4×4 cavern anywhere that isn’t the fountain or entrance room.
• Players can sense the draft blowing out of pits in adjacent rooms (all eight directions): “You feel a
draft. There is a pit in a nearby room.”
• If a player ends their turn in a room with a pit, they lose the game.
• Note: When combined with the Small, Medium, or Large challenge, add one pit to the 4×4 world, two
pits to the 6×6 world, and four pits to the 8×8 world, in locations of your choice.

Boss Battle Maelstroms 100 XP


The Uncoded One knows the significance of the Fountain of Objects and has placed minions in the
caverns to defend it. One of these is the maelstrom—a sentient, malevolent wind. Encountering a
maelstrom does not result in instant death, but entering a room containing a maelstrom causes the
player to be swept away to another room. The maelstrom also moves to a new location. If the player is
moved to another dangerous location, such as a pit, that room’s effects will happen upon landing in that
room.
A player can hear the growling and groaning of a maelstrom from a neighboring room (including
diagonals), which gives them a clue to be careful.
Modify the basic Fountain of Objects game in the ways below to add maelstroms to the game.
Objectives:
EXPANSIONS 245
• Add a maelstrom to the small 4×4 game in a location of your choice.
• The player can sense maelstroms by hearing them in adjacent rooms. (“You hear the growling and
groaning of a maelstrom nearby.”)
• If a player enters a room with a maelstrom, the player moves one space north and two spaces east,
while the maelstrom moves one space south and two spaces west. When the player is moved like
this, tell them so. If this would move the player or maelstrom beyond the map’s edge, ensure they
stay on the map. (Clamp them to the map, wrap around to the other side, or any other strategy.)

Note: When combined with the Small, Medium, or Large challenge, place one maelstrom into the
medium-sized game and two into the large-sized game.

Boss Battle Amaroks 100 XP


The Uncoded One has also placed amaroks in the caverns to protect the fountain from people like you.
Amaroks are giant, rotting, wolf-like creatures that stalk the caverns. When players enter a room with an
amarok, they are instantly killed, and the game is over. Players can smell an amarok in any adjacent room
(all eight directions), which tells them that an amarok is nearby.
Modify the basic Fountain of Objects game as described below.
Objectives:
• Amarok locations are up to you. Pick a room to place an amarok aside from the entrance or fountain
room in the small 4×4 world.
• When a player is in one of the eight spaces adjacent to an amarok, a message should be displayed
when sensing surroundings that indicate that the player can smell the amarok nearby. For example,
“You can smell the rotten stench of an amarok in a nearby room.”
• When a player enters a room with an amarok, the player dies and loses the game.
• Note: When combined with the Small, Medium, or Large challenge, place two amaroks in the medium
level and three in the large level in locations of your choosing.

Boss Battle Getting Armed 100 XP


Note: Requires doing the Maelstroms or Amaroks challenge first.
The player brings a bow and several arrows with them into the Caverns. The player can shoot arrows into
the rooms around them, and if they hit a monster, they kill it, and it should no longer impact the game.
Objectives:
• Add the following commands that allow a player to shoot in any of the four directions: shoot north,
shoot east, shoot south, and shoot west. When the player shoots in one of the fourfo ur directions, an arrow
is fired into the room in that direction. If a monster is in that room, it is killed and should not affect
the game anymore. They can no longer sense it, and it should not affect the player.
• The player only has five arrows and cannot shoot when they are out of arrows. Display the number
of arrows the player has when displaying the game’s status before asking for their action.
246 LEVEL 31 THE FOUNTAIN OF OBJECTS

Boss Battle Getting Help 100 XP


The player should not be left guessing about how to play the game. This challenge requires adding two
key elements that make playing the Fountain of Objects easier: introductory text that explains the game
and a help command that lists all available commands and what they each do.
Objectives:
• When the game starts, display text that describes the game shown below:
You enter the Cavern of Objects, a maze of rooms filled with dangerous pits in search
of the Fountain of Objects.
Light is visible only in the entrance, and no other light is seen anywhere in the caverns.
You must navigate the Caverns with your other senses.
Find the Fountain of Objects, activate it, and return to the entrance.

• If you chose to do the Pits challenge, add the following to the description: “Look out for pits. You
will feel a breeze if a pit is in an adjacent room. If you enter a room with a pit, you will die.”
• If you chose to do the Maelstroms challenge, add the following to the description: “Maelstroms are
violent forces of sentient wind. Entering a room with one could transport you to any other location
in the caverns. You will be able to hear their growling and groaning in nearby rooms.”
• If you chose to do the Amaroks challenge, add the following to the description: “Amaroks roam the
caverns. Encountering one is certain death, but you can smell their rotten stench in nearby rooms.”

If you
you chose
a bow toado
and the Getting
quiver Armed
of arrows. Youchallenge,
You add to
can use them
t hem theshoot
following to theindescription:
monsters
m onsters caver ns“You
the caverns carry
but be with
warned:
you have a limited supply.”
• t he command help, display all available commands and a short
When the player types the s hort description
of what each does. The complete list of commands will depend on what challenges you complete.

Narrative The Fountain Remade


You scramble through the dark Cavern of Objects, crawling and feeling your way to the Fountain of
Objects. The dripping sound that you hear is a giveaway that you have found it. You pull Simula’s green
gem—the Heart of Object-Oriented Programming—out of your pack and hold it in the palm of your
hand, contemplating the journey
jou rney you have taken to get here. Y
You
ou slide your hand alo
along
ng the Fountain until
you find a small recess, just big enough for the Heart to be placed. You slide the green gem in, and the
fountain immediately comes to life. The water in the fountain, previously still, suddenly begins churning
and overflowing onto the ground around you. You make a hasty escape to the cavern entrance.
Within minutes, water rushes out the entrance and through a thousand other holes in the mountainside,
collecting into a raging waterfall down into the valley below. Within days, the newly restored River of
Objects will flow to the sea, restoring its life-giving power to the entire island.
With the River of Objects flowing again, the land will become bountiful with objects of every class,
interface, and struct imaginable. The island has been saved. You turn your attention towards the
scattered islands on the horizon and your final destination beyond: a confrontation with The Uncoded
One.
󰁓OME U󰁓EFUL32
LEVEL TYPE󰁓

󰁓peedrun
• Random generates pseudo-random numbers.

DateTime gets the current time and stores time and date values.
• TimeSpan represents a length of time.
• Guid is used to store a globally unique identifier.
• List<T> is a popular and versatile generic collection—use it instead of arrays for most things.
• IEnumerable<T> is an interface for almost any collection type. The basis of foreach loops.
• Dictionary<TKey,
Dictionary<TKey, TValue> can look up one piece of information from another.
• Nullable<T> is a struct that can express the concept of a missing value for value types.
• ValueTuple is the secret sauce behind tuples in C#.
• StringBuilder is a less memory-intensive way to build strings a little at a time.

Narrative The Harvest of Objects


A few days have passed since the Fountain of Objects was restored, but the land has already become
more vibrant and lush. New objects and classes, unseen for thousands of clock cycles, have been found
again. The classes described in this level represent a collection of some of the most versatile and
interesting ones you have seen, and you gather some up for the rest of your journey.

Now that we have learned about classes, structs, interfaces, and generic types, we are well
prepared to look at a handful of useful types that come with .NET.
.NET. There are thousands of types
in C#’s standard library called the Base Class Library (BCL). We can’t reasonably cover them
all. We have covered several in the past and will cover more in the future, but
but in this level, we
will look at nine types that
that will forever change
change how you program
program in C#.
248 LEVEL 32 SOME USEFUL TYPES

THE RANDOM CLA󰁓󰁓


The Random class (in the System namespace) generates random numbers. Some programs
(like games) are more likely to use random numbers than others, but randomness can be
found anywhere.
Randomness is an interesting concept. A computer follows instructions exactly, which does
not leave room for true randomness, short of buying hardware that measures some natural
random phenomenon (like thermal noise or the photoelectric effect). However, some
algorithms will produce a sequence of numbers that feels random, based on past numbers.
This is called pseudo-random number generation because it is not truly random. For most
practical purposes, including most games, pseudo-random number generation is sufficient.
Pseudo-random generators have to start with an initial value called a seed. If you reuse the
same seed, you will get the same random-looking sequence again precisely. This can be both
bad and good. For example, Minecraft generates worlds based on a seed. Sometimes, you
want a specific random world, and by telling Minecraft to use a particular seed, you can see
the same world again. But most of the time, you want a random seed to get a unique world.
The System.Random class is the starting point for anything involving randomness. It is a
simple class that is easy to learn how to use:
Random random = new Random();

Console.WriteLine(random.Next());
The Random() constructor is initialized with an arbitrary seed value, which means you will
not see the same sequence come up ever again with another Random object or by rerunning
the program. (Older versions of .NET used the current time as a seed, which meant creating
two Random instances in quick succession would have the same seed and generate the same
sequence. That is no longer true.)
Random’s most basic method is the Next() method. Next picks a random non-negative (0
or positive) int with equal chances of each. You are just as likely to get 7 as 1,844,349,103.
Such a large range is rarely useful, so a couple of overloads of Next give you more control.
Next(int) lets you pick the ceiling:
Console.WriteLine(random.Next(6));

random.Next(6) will give you 0, 1, 2, 3, 4, or 5 (but not 6) as possible choices, with equal
chances of each. It is common to add 1 to this result so that the range is 1 through 6 instead of
0 through 5. For example:
Console.WriteLine($"Rolling a six-sided die: {random.Next(6) + 1}");

The third overload of Next allows you to name the minimum value as well:
Console.WriteLine(random.Next(18, 22));

This will randomly pick from the values 18, 19, 20, and 21 (but not 22).
i ntegers, you can use NextDouble():
If you want floating-point values instead of integers,
Console.WriteLine(random.NextDouble());

This will give you a double in the range of 0.0 to 1.0. (Strictly speaking, 1.0 won’t ever
come up, but 0.9999999 can.) You can stretch this out over a larger range with some simple
arithmetic. The following will produce random numbers in the range 0 to 10:
THE DATETIME STRUCT 249
Console.WriteLine(random.NextDouble() * 10);

And this will produce


produce random numbers in the range
range -10 to +10:
Console.WriteLine(random.NextDouble() * 20 - 10);

The Random class also has a constructor


c onstructor that lets you pass in a specific seed:
Random random = new Random(3445);
Console.WriteLine(random.Next());

This code will always display the same output because the seed is always 3445, which lets you
recreate a random sequence of numbers.

Challenge The Robot Pilot 50 XP


When we first made the Hunting the Manticore game in Level 14, we required two human players: one to
set up the Manticore’s range from the city and the other to destroy it. With Random, we can turn this
into a single-player game by randomly picking the range for the Manticore.
Objectives:
• Modify your Hunting the Manticore game to be a single-player game by having the computer pick a
random range between 0 and 100.

Answer this question: How might you use inheritance, polymorphism, or interfaces to allow the
game to be either a single player (the computer randomly chooses the starting location and
direction) or two players (the second human determines the starting location and direction)?

THE DATETIME 󰁓TRUCT


The DateTime struct (in the System namespace) stores moments in time and allows you to
get the current time. One way to create a DateTime value is with its constructors:
DateTime time1 = new DateTime(2022, 12, 31);
DateTime time2 = new DateTime(2022, 12, 31, 23, 59, 55);

This creates a time at the start of 31 December 2022 and at 11:59:55 PM on 31 December 2022,
respectively. There are 12 total constructors for DateTime, each requiring different
information.
Perhaps even more useful are the static DateTime.Now and DateTime.UtcNow properties:
DateTime nowLocal = DateTime.Now;
DateTime nowUtc = DateTime.UtcNow;

DateTime.Now is in your local time zone, as determined by your computer. DateTime.


UtcNow gives you the current time in Coordinated Universal Time or UTC, which is essentially
a worldwide time, not specific to time zones, daylight saving time, etc.
A DateTime value has various properties to see the year, month, day, hour, minute, second,
and millisecond, among other things. The following illustrates some simple uses:
uses :
DateTime time = DateTime.Now;
if (time.Month == 10) Console.WriteLine("Happy Halloween!");
else if (time.Month == 4 && time.Day == 1) Console.WriteLine("April Fools!");
250 LEVEL 32 SOME USEFUL TYPES
There are also methods for getting new DateTime values relative to another.
another. For example:
DateTime tomorrow = DateTime.Now.AddDays(1);

The DateTime struct is very smart, handling many easy-to-forget corner cases, such as leap
years and day-of-the-week calculations. When
When dealing wiwith
th dates and times, this is your go-to
go-to
struct to represent them and get the current
cur rent date and time.

THE TIMESPAN 󰁓TRUCT


The TimeSpan struct (System namespace) represents a span of time. You can create values
of the TimeSpan struct in one of two ways. Several constructors let you dictate the length of
time:
TimeSpan timeSpan1 = new TimeSpan(1, 30, 0); // 1 hour, 30 minutes, 0 seconds.
TimeSpan timeSpan2 = new TimeSpan(2, 12, 0, 0); // 2 days, 12 hours.
TimeSpan timeSpan3 = new TimeSpan(0, 0, 0, 0, 500); // 500 milliseconds.
TimeSpan timeSpan4 = new TimeSpan(10); // 10 "ticks" == 1 microsecond

After reading the comments, most of these are straightforward,


straightforward, but the last one is notable.
Internally, a TimeSpan keeps track of times in a unit called a tick, which is 0.1 microseconds
or 100 nanoseconds. This is as fine-grained as a TimeSpan can get, but you rarely need more.
The other way to create TimeSpans is with one of the various FromX methods:
TimeSpan aLittleWhile = TimeSpan.FromSeconds(3.5);
TimeSpan quiteAWhile = TimeSpan.FromHours(1.21);

The whole collection includes FromTicks, FromMilliseconds , FromSeconds ,


FromHours, and FromDays.
TimeSpan has two sets of properties that are worth mentioning. First is this set: Days, Hours,
Minutes, Seconds, Milliseconds. These represent the various components of the
TimeSpan. For example:
TimeSpan timeLeft = new TimeSpan(1, 30, 0);
Console.WriteLine($"{timeLeft.Days}d {timeLeft.Hours}h {timeLeft.Minutes}m");

timeLeft.Minutes does not return 90, since 60 of those come from a full hour, represented
represented
by the Hours property.
Another set of properties capture the entire timespan in the unit requested: TotalDays,
TotalHours, TotalMinutes , TotalSeconds, and TotalMillseconds.
TimeSpan timeRemaining = new TimeSpan(1, 30, 0);
Console.WriteLine(timeRemaining.TotalHours);
Console.WriteLine(timeRemaining.TotalMinutes);

This will display:


1.5
90

Both DateTime and TimeSpan have defined several operators (Level 41) for things like
comparison (>, <, >=, <=), addition, and subtraction. Plus, the two structs play nice together:
together :
DateTime eventTime = new DateTime(2022, 12, 4, 5, 29, 0); // 4 Dec 2022 at 5:29am
TimeSpan timeLeft = eventTime - DateTime.Now;
THE GUID STRUCT 251

// 'TimeSpan.Zero' is no time at all.


if (timeLeft > TimeSpan.Zero)
Console.WriteLine($"{timeLeft.Days}d {timeLeft.Hours}h {timeLeft.Minutes}m");
else
Console.WriteLine("This event has passed.");

The second line shows that subtracting one DateTime from another results in a TimeSpan
that is the amount of time between the two. The if statement shows a comparison against the
special TimeSpan.Zero value.
Challenge Time in the Cavern 50 XP
With DateTime and TimeSpan, you can track how much time a player spends in the Cavern of Objects
to beat the game. With these tools, modify your Fountain of Objects game to display how much time a
player spent exploring the caverns.
Objectives:
• When a new game begins, capture c urrent time using DateTime.
capture the current
• When a game finishes (win or loss), capture the current time.
• Use TimeSpan to compute how much time elapsed and display that to the player.
player.

THE GUID 󰁓TRUCT


The Guid struct (System namespace) represents a globally unique identifier or GUID. (The
word GUID is usually pronounced as though it rhymes with “squid.”) ”) You may find value in
giving objects or items a unique identifier to track them independently from other similar
objects in certain programs. This is especially true if you send information across a network,
where you can’t just use a simple reference. While you could use an int or long as unique
numbers for these objects, it can be tough to ensure that each item has a truly unique number.
number.
This is especially true if different computers have to create the unique number
number.. This is where
the Guid struct comes in handy.
The idea is that if you have enough possible choices, two people picking at random won’t pick
the same thing. If all of humanity had a beach party and each of us went and picked a grain of
sand on the beach, the chance that any of us would pick the same grain is vanishingly small.
The generation of new identifiers with the Guid struct is similar.
identifier, you use the static Guid.NewGuid() method:
To generate a new arbitrary identifier,
Guid id = Guid.NewGuid();

Each Guid value is 16 bytes (4 times as many as an int), ensuring plenty of available choices.
But NewGuid() is smarter than just picking a random number. It has smarts built in that
ensure that other computers won’t pick the same value and that multiple calls to NewGuid()
won’t ever give you the
the same number again, maximizing
maximizing the chance of uniqueness.
uniqueness.
A Guid is just a collection of 16 bytes, but it is usually written in hexadecimal with dashes
breaking
Once youitknow
into about
smaller chunks
GUIDs, like
you see 10A24EC2-3008-4678-AD86-FCCCDA8CE868
willthis: them pop up all over the place. .
252 LEVEL 32 SOME USEFUL TYPES
If you already have a GUID and do not want to generate a new one, there are other
constructors that you can use to build a new Guid value that represents it. For example:
Guid id = new Guid("10A24EC2-3008-4678-AD86-FCCCDA8CE868");

Just be careful about inadvertently reusing a GUID in situations that could cause conflicts.
Copying and pasting GUIDs can lead to accidental reuse. Visual Studio has a tool to generate
a random GUID under Tools > Create GUID,
GUID, and you can find similar things online.

THE LIST<T> CLA󰁓󰁓


The List<T> class (System.Collections.Generic namespace) is perhaps the most
versatile generic .NET. List<T> is a collection class where order matters, you can
gener ic class in .NET.
access items by their index, and where items can be added and removed easily. They are like
an array, but their ability to grow and shrink makes them superior in virtually all
circumstances. In fact, after this section, you should only rarely use arrays.
The List<T> class is a complex class with many capabilities. We won’t look at all of them,
the m, but
let’s look at the most important ones.

Creating List Instances


There are many ways to create a new list, but the most common is to make an empty list:
list :
List<int> numbers = new List<int>();

This makes a new List<int> instance with nothing in it. You


You will do this most of the time.
If a list has a known set of initial items, you can also use collection initializer syntax as we did
with arrays:
List<int> numbers = new List<int>() { 1, 2, 3 };

This calls the same empty constructor


c onstructor before adding the items in the col
collection
lection one at a time
but is an elegant way to initialize a new list with specific items. Like we saw with object
initializer syntax, where we set properties on a new object, if the constructor needs no
parameters, you can also leave the parentheses off:
List<int> numbers = new List<int> { 1, 2, 3 };

Some people like the conciseness


c onciseness of that version; others find it strange. They both work.

Indexing
Lists support indexing, just like arrays:
List<string> words = new List<string>() { "apple", "banana", "corn", "durian" };
Console.WriteLine(words[2]);

Lists also use 0-based indexing. Accessing index 2 gives you the string "corn".
Y
You
ou can replace an item in a list by assigning a new value to that index, just
just like an array:
words[0] = "avocado";

When we made own List<T> class in Level 30, we didn’t get this simple indexing syntax,
made our own
though that was because we just didn’t know the right tools yet (Level
(L evel 41).
41).
THE LIST<T> CLASS 253

Adding and Removing Items from List


A key benefit of lists
lists over arrays is the easy ability to add and remove items. For example:
List<string> words = new List<string>();
words.Add("apple");

Add puts items at the back of the list. To put something in the middle, you use Insert, which
requires an index and the item:

List<string> words = new List<string>() { "apple", "banana", "durian" };


words.Insert(2, "corn");

If you need to add or insert many items, there is AddRange and InsertRange :
List<string> words = new List<string>();
words.AddRange(new string[] { "apple", "durian" });
words.InsertRange(1, new string[] { "banana", "corn" });

These allow you to supply a collection of items to add to the back of the list ( AddRange) or
insert in the middle (InsertRange). I used arrays to hold those collections above, though
the specific type involved is the IEnumerable<T> interface, which we will discuss next.
Virtually any collection type implements that interface, so you have a lot of flexibility.
l ist, you can name the item to remove with the Remove method:
To remove items from the list,
List<string> words = new List<string>() { "apple", "banana", "corn", "durian" };
words.Remove("banana");

If an item is in the collection more than once, only the first occurrence is removed. Remove
returns a bool that tells you whether anything was removed. If you need to remove all
occurrences, you could loop until that starts returning false.
in dex, use RemoveAt:
If you want to remove the item at a specific index,
words.RemoveAt(0);

The Clear method removes everything in the list:


words.Clear();

Since we’re talking about adding and removing items from a list, you might be wondering how
to determine how many things are in the list. Unlike an array, which has a Length property,
a list has a Count property:
Console.WriteLine(words.Count);

foreach Loops
You can use a foreach loop with a List<T> as you might with an array.
You
foreach (Ship ship in ships)
ship.Update();

But there’s a dd or remove items in a List<T> while a foreach is


there’s a crucial catch: you cannot add
in progress. This doesn’t cause problems very often, but every so often, it is painful. For
example, you have a List<Ship> for a game, and you use foreach to iterate through each
and let them update. While updating, some ships may be destroyed and removed. By
254 LEVEL 32 SOME USEFUL TYPES
removing something from the list, the iteration mechanism used with foreach cannot keep
track of what it has seen, and it will crash. (Specifically, it throws an InvalidOperation
Exception; exceptions are covered in Level 35.) 35.)
There are two workarounds for this. One is to use a plain for loop. Using a for loop and
retrieving the item at the current index lets you sidestep the iteration mechanism that a
foreach loop uses.
for (int index = 0; index < ships.Count; index++)
{
Ship ship = ships[index];
ship.Update();
}

If you add or remove items farther down the list (at an index beyond the current one), there
are not generally complications to adding and removing items as you go. But if you add or
remove an item before the spot you are currently at, you will have to account for it. If you are
looking at the item at index 3 and insert at index 0 (the start), then what was once index 3 is
now index 4. If you remove the item at index 0, then what was once at index 3 is now index 2.
Y ou can use ++ and -- to account for this, but it is a tricky situation to avoid if possible.
You
for (int index = 0; index < ships.Count; index++)
{
Ship ship = ships[index];
ship.Update();
if (ship.IsDead)
{
ships.Remove(ship);
index--;
}
}

Another workaround is to hold off on the actual addition or removal during the foreach
loop. Instead, remember which things should be added or removed by placing them in helper
lists like toBeAdded and toBeRemoved . After the foreach loop, go through the items in
those two helper lists and use List<T>’s Add and Remove methods to do the actual adding
and removing.

Other Useful Things


The Contains method tells you if the list contains a specific item, returning true if it is there
and false if not.
bool hasApples = words.Contains("apple");
if (hasApples)
Console.WriteLine("Apples are already on the shopping list!");

The IndexOf method tells you where in a list an item can be found, or -1 if it is not there:
int index = words.IndexOf("apple");

The List<T> class has quite a bit more than we have discussed here, though we hav
havee covered
the
lookhighlights. At some point, you and
it up on docs.microsoft.com will see
want to use
what elseVisual Studio’s
it is capable of.AutoComplete feature or
THE IENUMERABLE<T> INTERF
INTERFACE
ACE 255

Challenge Lists of Commands 75 XP


In Level
Level 27, we encountered a robot with an array
arr ay to hold commands
comman ds to run. But we could make the robot
have as many commands as we want by turningt urning the array into a list. Revi
Revisit
sit that challenge to make the
robot use a list instead of an array, and add commands to run until the user says to stop.
Objectives:
• Change the Robot class to use a List<IRobotCommand> instead of an array for its Commands
property.
• Instead of looping three times, go until the user types stop. Then run all of the commands created.

THE IENUMERABLE<T> INTERFACE


While List<T> might be the most versatile generic type, IEnumerable<T> might be the
most foundational. This simple interface essentially defines what counts as a collection in
.NET.
IEnumerable<T> defines a mechanism that allows you to inspect items one at a time. This
mechanism is the basis for a foreach loop. If a type implements IEnumerable<T>, you can

use it in a foreach loop.


IEnumerable<T> is anything that can provide an “enumerator,” and the definition looks
something like this:
public interface IEnumerable<T>
{
IEnumerator<T> GetEnumerator();
}

But what’s an enumerator? It is a thing that lets you look at items in a set, one at a time, with
the ability to start over
over.. It is defined roughly like this:
public interface IEnumerator<T>
{
T Current { get; }
bool
void MoveNext();
Reset();
}

The Current property lets you see the current item. The MoveNext method advances to the
next item and returns whether there even is another item. Reset starts over from the
beginning. Almost nobody uses an IEnumerator<T> directly. They let the foreach loop
deal with it. Consider this code:
List<string> words = new List<string> { "apple", "banana", "corn", "durian" };

foreach(string word in words)


Console.WriteLine(word);

That is equivalent to this:


List<string> words = new List<string> { "apple", "banana", "corn", "durian" };

IEnumerator<string> iterator = words.GetEnumerator();


256 LEVEL 32 SOME USEFUL TYPES

while (iterator.MoveNext())
{
string word = iterator.Current;
Console.WriteLine(word);
}

List<T> and arrays both implement IEnumerable<T> , but dozens of other collection
types also implement this interface. It is the basis for all collection types. You will see
IEnumerable<T> everywhere.

THE DICTIONARY<TKEY, TVALUE> CLA󰁓󰁓


Sometimes, you want to look up one object or piece of information using another. A dictionary
(also called an associative array or a map in other programming languages) is a data type that
makes this possible. A dictionary provides this functionality. You add new items to the
dictionary by supplying a key to store the item under, and when you want to retrieve it, you
provide the key again to get the item back out. The value stored and retrieved via the key is
called the value.
The origin of the name—and an illustrative example—is a standard English dictionary.
Dictionaries store words and their definitions. For any word, you can look up its definition in
the dictionary. If we wanted to make an English language dictionary in C# code, we could use
the generic Dictionary<TKey, TValue> class:
Dictionary<string, string> dictionary = new Dictionary<string, string>();

This type has two generic type parameters, one for the key type and one for the value type.
Here, we used string for both.
We can add items to the dictionary using the indexing operator with the key instead an int:
instead of an
dictionary["battleship"] = "a large warship with big guns";
dictionary["cruiser"] = "a fast but large warship";
dictionary["submarine"] = "a ship capable of moving under the water's surface";

To retrieve a value, you can also use the indexing operator:


Console.WriteLine(dictionary["battleship"]);

This will display the string "a large warship with big guns".
If you reuse a key, the new value replaces the first:
dictionary["carrier"] = "a ship that carries stuff";
dictionary["carrier"] = "a ship that serves as a floating runway for aircraft";
Console.WriteLine(dictionary["carrier"]);

This displays the second, longer definition; the first is gone.


What if you try to retrieve the item with
with a key that isn’t
isn’t in the dictionary?
Console.WriteLine(dictionary["gunship"]);

This blows up. (Specifically, it throws a KeyNotFoundException , a topic we will learn in


Level 35.)
35.) We can get around this by asking if a dictionary contains a key before retrieving it:
THE DICTIONARY<TK
DICTIONARY<TKEY,
EY, TVALUE> CLASS 257
if (dictionary.ContainsKey("gunship"))
Console.WriteLine(dictionary["gunship"]);

Or we could ask it to use a fallback value with the GetValueOrDefault method:


Console.WriteLine(dictionary.GetValueOrDefault("gunship", "unknown"));

If you want to remove a key and its value from the dictionary, you can use the Remove method:
dictionary.Remove("battleship");

This returns a bool that indicates if anything was removed.


Once again, there is more to Dictionary<TKey, TValue> than we can cover here, though
we have covered
covered the most essential
essential parts.

Types Besides string


Dictionaries are generic types, so they can use anything you want for key and value types.
Strings are not uncommon, but they are certainly not the only or even primary
pr imary usage.
For example, we might create a WordDefinition class that contains the definition, an
example sentence, and the part of speech, and then use that in a dictionary:

var dictionary = new Dictionary<string, WordDefinition>();


The key here is still a string, while the values are WordDefinition instances. So you still
look up items with dictionary["battleship"] but get a WordDefinition instance
out.
Or perhaps we have a collection of GameObject instances (maybe this is the base class of all
the objects in a game we’re making), and each instance has an ID that is an int. We could
store these in a dictionary as well, allowing us to look up the game objects by their ID:
Dictionary<int, GameObject> gameObjects = new Dictionary<int, GameObject>();

If GameObject has an ID property, you could add an item to the dictionary like this:
gameObjects[ship.ID] = ship;

This code is a good illustration of the power of generic types. We have lots of flexibility with
dictionaries, which stems from our ability to pick any key or value type.

Dictionary Keys 󰁓hould Not Change


Dictionaries use the hash code of the key to store and locate the object in memory. A hash
code is a special value determined by each object, as returned by GetHashCode(), defined
by object. You can override this, but for a reference type, this is based on the reference itself.
For value types, it is determined by combining the hash code of the fields that compose it.
Once a key has been placed in a dictionary, you should do nothing to cause its hash code to
change to a different hash code. That would make it so the dictionary cannot recover the key,
and the key and the object are lost for all practical purposes.

t hat you won’t have any problems. Types like int, char,
If a key is immutable, it guarantees that
long, and even string are all immutable, so they are safe. If a reference type, like a class,
uses the default behavior, you should also be safe. But if somebody has overridden
258 LEVEL 32 SOME USEFUL TYPES
GetHashCode, which is often done if you redefined Equals, ==, and =, take care not to
change the key object in ways that would alter its hash code.

THE NULLABLE<T> 󰁓TRUCT


The Nullable<T> struct (System namespace) lets you pretend that a value type can take on
a null value. It does this by attaching a bool HasValue property to the original value. This
property indicates whether the value should be considered legitimate. One way to work with
Nullable<T> is like so:
Nullable<int> maybeNumber = new Nullable<int>(3);
Nullable<int> another = new Nullable<int>();

The first creates a Nullable<int> where the value is considered


c onsidered legitimate
legitimate and whose value
is 3, while the second is a Nullable<int> where the value is missing.
Nullable<T> does not create true null references. It must use value types and is a value
type itself. The bytes are still allocated (plus an extra byte for the Boolean HasValue
property). It is just that the current
c urrent content isn’t considered valid.
For any nullable struct, you can use its HasValue and Value properties to check if the value
is legitimate or is to be considered missing, and if it is legitimate, to retrieve the actual value:
if (maybeNumber.HasValue)
Console.WriteLine($"The number is {maybeNumber.Value}.");
else
Console.WriteLine("The number is missing.");

But C# provides syntax to make working with Nullable<T> easy. You can use int? instead
of Nullable<int> . You can also automatically convert from the underlying type to the
nullable type (for example, to convert a plain int to a Nullable<int>) and even convert
from the literal null. Thus, most C# programmers will use the following
foll owing instead:
int? maybeNumber = 3;
int? another = null;

Nullable<T> is a convenient way to represent values when the value may be missing. But
remember,, this is different from null references.
remember
Interestingly, operators
operators on the underlying type work on the nullable counterparts:
maybeNumber += 2;

Unfortunately, that only applies to operators, not methods or properties. If you want to invoke
a method or property on a nullable value, you must call the Value property to get a copy of
the value first.

VALUETUPLE 󰁓TRUCT󰁓
We have seen many examples where the C# language makes it easy to work with some
common type. As we just saw, int? is the same as Nullable<int>, and even int itself is
simply the Int32 struct. Tuples also have this treatment and are a shorthand way to use the
ValueTuple generic structs. We saw how to do the following in Level 17:
(string, int, int) score = ("R2-D2", 12420, 15);
THE STRINGBUILDER CLASS 259
That is a shorthand version of this:
ValueTuple<string, int, int> score =
new ValueTuple<string, int, int>("R2-D2", 12420, 15);

Most C# programmers prefer the first, simpler syntax, but sometimes the name ValueTuple
leaks out, and it is worth knowing the two are the same thing when it does.

THE STRINGBUILDER CLA󰁓󰁓


One problem with doing lots of operations with strings is that it has to duplicate all of the string
contents in memory for every modification. Consider this code:
string text = "";
while (true)
{
string? input = Console.ReadLine();
if (input == null || input == "") break;
text += input;
text += ' ';
}
Console.WriteLine(text);

In this code, we keep creating new strings that are longer and longer. The user enters "abc",
and this code creates a string containing "abc". It then immediately makes another string
with the text "abc ". Then the user enters "def", and your program will make another
string containing "abc def" and then another containing "abc def ". These partial
strings could get long, take up a lot of memory, and make the garbage collector work hard.
alternative is the StringBuilder class in the System.Text namespace. System.Text
An alternative
is not one of the namespaces we get automatic access to, so the code below includes the
System.Text namespace when referencing StringBuilder . (We’ll address that in more
depth in Level 33.)
33. ) This class hangs on to fragments of strings and does not assemble them
into the final string until it is done. It will get a reference to the string "abc" and "def", but
won’t make any temporary combined strings until you ask for it with the ToString()
method:
System.Text.StringBuilder text = new System.Text.StringBuilder();
while (true)
{
string? input = Console.ReadLine();
if (input == null || input == "") break;
text.Append(input);
text.Append(' ');
}
Console.WriteLine(text.ToString());

StringBuilder is an optimization to use when necessary, not something to do all the time.
A few extra relatively short strings
strings won’t hurt anything. But if
if you are doing anything
anything intensive,
intensive,
StringBuilder may be an easy ea sy substitute
substitute to help keep memory usage in check.
ch eck.
Part 3
Advanced Topics
In Part 3, we will look at many other advanced but handy C# language features. It is not unreasonable to
treat all of Part 3 as a Side Quest. There is little that can’t be built with the things we learned in Parts 1
and 2. However, most experienced C# programmers are familiar with these features and often use them,
so ignore them at your own peril. I recommend at least skimming through this part so that you have
some familiarity with them and know where to come back to when the time is right. Most of these levels
are independent of each other. In most cases, you will be able to jump around and dig into the levels that
pique your interest without necessarily reading everything that comes before it.
Here is a high-level view of what is to come:
• More about working with more extensive programs (Level 33).
33).
• More about methods (Level 34).
34).
• Handling errors using exceptions (Level 35).
35).
• Delegates (Level 36).
36).
• Events (Level 37).
37).
• Lambda expressions (Level 38).
38).
• Reading from and writing to files (Level 39).
39).
• Pattern matching (Level 40).
40).
• Overloading operators and creating indexers (Level 41)
41)..
• Query expressions (Level 42).
42).
• Multi-threading your application (Levels 43 and 44).
44).
• Dynamic objects (Level 45).
45).
• Unsafe (unmanaged) code (Level 46).
46).
262
• A quick look at a few other features in C# that are worth knowing a bit about (Level 47).
47).
• Building programs that build upon other projects (your own or others) (Level 48).
48).
• An in-depth look at what the compiler does (Level 49).
49).
• A more detailed look at .NET (Level 50).
50).
• How to package your code for publishing (Level 51).
51).

Narrative Gathering Medallions


You stand on the east coast of the vast island of Object-Oriented Programming. A strong breeze blows
salty air across your face as the sun rises above the watery horizon. You study your maps. Ahead of you
lies the scattered Islands of Advanced Features, and beyond that, the Domain of the Uncoded One—
your final destination.
Scattered across the islands are the ancient Medallions of Code, made of nearly indestructible binarium,
each of which grants True Programmers additional powers. Each medallion is guarded by the islands’
inhabitants, who serve as protectors and stewards. Without being Programmers, they are unable to use
them themselves. By visiting these islands, you can acquire these medallions, gain the powers they
provide, and maybe even enlist these guardians to help in the final assault at the Uncoded One’s domain.
Yet time is short; every clock cycle you delay gives the Uncoded One more time to reign destruction and
may even give it the time needed to uncode and unravel the world itself.
You grab a pencil and begin making tentative plans on your map about the final leg of your journey
through the Islands of Advanced Features.
L EVEL
33
MANAGING LARGER PROGRAM󰁓

󰁓peedrun
• C# programs can be spread across multiple files. It is common to put each type in its own file.
• Namespaces are a way to organize type definitions into named groups. Types intended for reuse
should be placed in a namespace.
• You must normally refer to types by their fully qualified name, such as System.Console.
• A using directive allows you to use the simple name for a type instead of its fully qualified name.
• Several namespaces, including System, are automatically included in .NET 6+ projects and need no
using directive. You can add to this list with a global using directive.
• A using static directive allows you to use static members of a type without the type name.
• Add types to a namespace with either namespace Name { ... } or by putting namespace Name;
at the start of a file.
• Traditional entry points (before .NET 5) declare a public static void Main(string[] args)
method in a Program class.

We’ve reached a critical point in our progress,


We’ve progress, and it is time to learn a few tools that
that will allow
us to build even larger programs. We’ll
We’ll cover three topics here: splitting code across multiple
files, namespaces, and traditional entry points.

U󰁓ING MULTIPLE FILE󰁓


As your programs grow,
grow, having everything in a single file become
becomess unwieldy. Y
You
ou can spread
C# code across many files and folders to organize your code. In fact, most C# programmers
prefer putting types into separate
se parate files with a name that matches the type name. However
However,, tiny
type definitions like enumerations and records often get lumped in with closely related things.
There are many ways to get more files in your project (including File > New > File… or Ctrl +
N). But sometimes, the easiest way is to initially put it into an existing file and
an d then use a Quick
Action in Visual Studio to move it to another
a nother file. The Quick Action will be available on any
264 LEVEL 33 MANAGING LARGER PROGRAMS
type you have defined in a file with a mismatched name. Imagine you have this code in
Program.cs :
public class One { }
public class Two { }

Y
You
ou can get to the Quick Action
Action by placing the cursor on the first
first line of a type definition (such
(such
as public class One), then clicking on the screwdriver
sc rewdriver or lightbulb icon or pressi
pressing
ng Alt +
Enter.. When you do this, you will see a Quick Action named something like Move type to
Enter
One.cs.. Choosing this will create a new file (One.cs) and move the type there.
One.cs
If a type is more than a few hundred lines long, it probably deserves its own file. Many C#
programmers put each type in separate files; you’re in good company if you do the same.
Y
You
ou can make as many files as you want with one caveat: a program can only contain one file
with a main method. Every other file can only contain type definitions. If you have
Console.WriteLine("Hello,
Console.WriteLi ne("Hello, World "); at the top of two files, the compiler won’t
know which to use as the entry point of your program.

NAME󰁓PACE󰁓 AND USING DIRECTIVE󰁓


In C#, we name every type we create. Every other C# programmer is doing a similar thing. As
you can imagine, some names are
a re used more than once. For example, there are probably a
hundred thousand different Point classes in the world.
To better differentiate identically named types from each other, we can place our types in a
namespace. A namespace is a named container for organizing types. We can refer to a type by
its fully qualified name, a combination of the namespace it lives in, and the type name itself.
For example, since Console lives in the System namespace, its fully qualified name is
System.Console .
Fully qualified names allow us to differentiate between types with the same simple name. We
do similar things with people’s names, especially when the name could be ambiguous or
unclear: “Paul Leipzig,” “Paul from work,” or “Tall Paul.”
Until now, we have not placed our types in a specific namespace. They end up in an unnamed
namespace called the global namespace.
But most of the types we have used, such as Console, Math, and List<T>, all live specific
namespaces. So far,
far, everything we have covered has been in one of three namespaces.
The System namespace contains the most foundational and common types, including
Console, Convert, all the built-in types ( int, bool, double, string, object, etc.),
Math/MathF, Random, Random, DateTime, TimeSpan, Guid, Nullable<T> , and tuples
(ValueTuple). It is hard to imagine a C# program
p rogram that doesn’t use System.
The System.Collections.Generic namespace contains the generic collection types we
discussed in Level 32, including List<T>, IEnumerable<T> , and Dictionary<TKey,
TValue>. It is also hard to imagine any program with collections
col lections not using this namespace.
The System.Text namespace contains advanced text-related types, including the
StringBuilder class we saw in Level 32. This namespace is not quite as common.
As you program in C#, you will encounter types defined in many other namespaces. These
three are just the first three we’ve seen.
NAMESPACES
NAMESPACES AND USING DIRECTIVES 265
Any time you use a type name in your code, you have the option of using the type’s
type’s fully
qualified name. Since Console’s fully qualified name is System.Console , we could have
written Hello World like this:
System.Console.WriteLine("Hello, World!");

In fact, by default, you’re required to use a type’s fully qualified name! We saw an example of
this in Level 32 when we talked about StringBuilder . The code there included this line:

System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();


So why haven’t we had to write System.Console everywhere?
It’s complicated.
Y
You
ou can include a line at the top of any file that tells the compiler, “I’m going to be using this
namespace a lot. I want to use simple type
ty pe names for things in this nnamespace.
amespace. When you see
a plain type name, look in this namespace for it.” This line is called a using directive. For
example:
using System.Text;

StringBuilder stringBuilder = new StringBuilder();


stringBuilder.Append("Hello, ");
stringBuilder.Append("World!");
Console.WriteLine(stringBuilder.ToString());

With that using directive at the top of the file, we no longer need to use StringBuilder’s
fully qualified name when referring to that type.
In the future, you will want to pay attention
attention to what namespace types live in, so you can either
use their fully qualified name or add a using directive for their namespace. If you attempt to
use a type’s simple name without the correct using directive, you will get a compiler error
because the compiler won’t know what the identifier refers to.
These using directives partially explain why we don’t always need to write out
System.Console , but we haven’t added using System; to our programs either. Why?
When you look at older C# code, f ind that they almost invariably start with using
c ode, you will find
System; and a small pile of other using directives.
Starting with C# 10 projects, several using directives are added implicitly—you don’t need to
add them yourself. The automatic list includes both System and System.Collections.
Generic, which we have encountered. It also includes System.IO, System.Linq ,
System.Net.Http , System.Threading, and System.Threading.Tasks , most of
which we’ll cover before
before the end of this book.
Because these extremely common namespaces are added implicitly, the pile of using
directives at the start of a file only
onl y lists the non-obvious namespaces used in the file.
Y
You
ou can turn this feature off,
off, but I recommend leaving it on, as it eliminates cluttered, obvious
using directives across your code, which is a big win. On the other hand, if you’re stuck in an
older codebase and can’t use this feature, you’ll have to add using directives for every
namespace you want to use or use fully qualified names.
For namespaces not in the list above, like System.Text , you will still need to add a using
directive.
266 LEVEL 33 MANAGING LARGER PROGRAMS

Advanced using Directive Features


The basic using directive, shown above, is what you will do most of the time. But there are a
few advanced tricks you can do that are worth
wor th mentioning.

Global using Directives


If most files in a project use a specific namespace, you’ll have using SomeNamespace;
everywhere. As an alternative, you can include the global keyword on a using directive,
and it will automatically be included in all files in the project.
global using SomeNamespace;

A global using directive can be added to any file but must come before regular using
directives. I recommend putting these in a place you can find them easily. For example, you
could make a GlobalUsings.cs or ProjectSettings.cs file containing only your global using
directives.

󰁓tatic using Directives


You can add a using directive with the static modifier to name a single type (not a
You
namespace) to gain access to any static members of the type without writing out the type
name. For example, the Math and Console classes have many static members. We could add
static using directives for them:
using static System.Math;
using static System.Console;

With these in place, the following code compiles:


double x = PI; // PI from Math.
WriteLine(Sin(x)); // WriteLine from Console, Sin from Math.
ReadKey(); // ReadKey from Console.

This leads to shorter code, but it does add a burden on you and other programmers to figure
out where these methods are coming from. I recommend using these sparingly. More often
than not, the burden of figuring out and remembering where the methods came from
outweighs the few characters
c haracters you save, but all tools have their uses.

Name Conflicts and Aliases


Suppose you want to use two types that share the same name in a single file. For example,
imagine you need to use a PhysicsEngine.Point and a UserInterface.Point class.
Adding using directives for those two namespaces results in a name conflict. The compiler
won’t know which one Point refers to.
One solution is to use fully qualified names to sidestep the conflict.
PhysicsEngine.Point point = new PhysicsEngine.Point();

can also use the using keyword to give an alias to a type:


Alternatively, you can
using Point = PhysicsEngine.Point;

The above line is sufficient for the compiler to know when it sees the type Point in a file,
you’re referring to PhysicsEngine.Point, not UserInterface.Point , which resolves
the conflict.
NAMESP
NAMESPACES
ACES AND USING DIRECTIVES 267
An alias does not need to match
match the original name of the type
type,, meaning you can do thi
this:
s:
using PhysicsPoint = PhysicsEngine.Point;
using UIPoint = UserInterface.Point;

PhysicsPoint p1 = new PhysicsPoint();


UIPoint p2 = new UIPoint();

Aliasing a type to another name can get confusing; do so with ccaution.


aution.

Putting Types into Namespaces


Virtually all types you use but don’t create yourself ( Console, Math, List<T>, etc.) will be
in one namespace or another. Anything meant to be shared and reused in other projects
should be in a namespace. If you are building something that isn’t being reused, namespaces
are somewhat less important. Everything we’ve
we’ve done so far is in that category, so it isn’t a big
deal that we haven’t used namespaces before.
But putting things into namespaces isn’t hard and is often worth doing, even if you don’t
expect the code to be used far and wide.
The most flexible way of putting types in a namespace is shown below, using the namespace
keyword, a name, and a set of curly braces that hold the types you want in the namespace:
namespace SpaceGame
{
public enum Color { Red, Green, Blue, Yellow }

public class Point { /* ... */ }


}

With this code, Color’s fully qualified name is SpaceGame.Color , and Point’s is
SpaceGame.Point .
A slightly more complete example
example might look like this:
using SpaceGame;

Color color = Color.Red;


Point point = new Point();
namespace SpaceGame
{
public enum Color { Red, Green, Blue, Yellow }
public class Point { /* ... */ }
}

Our main method at the top isn’t in the SpaceGame namespace, so it relies on the using
directive at the top to use Color and Point without fully qualified names.
Namespaces
Namespaces can contain other namespaces:
name spaces:
namespace SpaceGame
{

namespace
{ Drawing
}
}
268 LEVEL 33 MANAGING LARGER PROGRAMS
But the more common way to nest namespaces is this:
this :
namespace SpaceGame.Drawing
{
}

A namespace can span many files. Each file will simply ad


add
d to the namespace’s
namespace’s members.
Aside from the file containing
containing your main
main method, most
most files lump all of its types into the same
namespace. The following
SpaceGame namespace,
namespace,” versionit is
” allowing a shortcut
to ditch to say, curly
the excessive “Everything in this
braces and file is in the
indentation:
namespace SpaceGame;

public enum Color { Red, Green, Blue, Yellow }


public class Point { /* ... */ }

This version comes after any using directives but before any type definitions. Unfortunately,
you cannot use this version
version in the file containing your main method.

Namespace Naming Conventions


Most C# programmers will make their namespace names align with their project and folder
structure. If you name your project SpaceGame, you would also make your namespace be
SpaceGame. If you make a folder within your SpaceGame project called Graphics, you would
put things in that folder in the SpaceGame.Graphics namespace.
Since namespace names usually mirror project names,
n ames, let’
let’ss briefly talk about project naming
conventions. Project names are typically given a short, memorable project name (for ( for example,
SpaceGame) or prefix the project name with a company name (RBTech.SpaceGame ). Some
large projects are made of multiple components, so you’ll sometimes see a component
c omponent name
added to the end ( SpaceGame.Client, or RBTech.SpaceGame.Graphics). The
namespace used within these projects will typically
t ypically match these project names in each case.

TRADITIONAL ENTRY POINT󰁓


Back in Level 3, I mentioned that there are two different ways to define an entry point for your
program. Placing statements in a file like Program.cs is the simplest of the two and is what we
have been doing in this book. This style is called top-level statements and is the newer and
easier of the two options.
The alternative, which I’ll call a traditional entry point, is still worth knowing. You will
inevitably encounter code that still uses it, and if you find yourself using older code, it may be
your only option.
The traditional approach is to make a class (usually called Program) and give it a static
void Main method with an (optional) string array parameter (usually called args):
using System;

namespace HelloWorld
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine( Hello, World! );

TRADITIONA
TRADITIONALL ENTRY POINTS 269
}
}
}

In fact, the newer top-level statement style is compiled into nearly identical code. Suppose
you write this code:
Faction faction = PickFaction();
Console.WriteLine($"You've chosen to join the {faction} Faction.");

Faction PickFaction()
{
Console.WriteLine("What faction do you want to be?");
string? choice = Console.ReadLine();
return choice switch
{
"Federation" => Faction.Federation,
"Klingon" => Faction.Klingon,
"Romulan" => Faction.Romulan,
};
}

public enum Faction { Federation, Klingon, Romulan }

The compiler turns this code into the following:


internal class Program
{
static void <Main>$(string[] args)
{
Faction faction = PickFaction();
Console.WriteLine($"You've chosen to join the {faction} faction.");

Faction PickFaction()
{
Console.WriteLine("What faction do you want to be?");
string? choice = Console.ReadLine();
return choice switch
{
"Federation" => Faction.Federation,
"Klingon" => Faction.Klingon,
"Romulan" => Faction.Romulan,
};
}
}
}

public enum Faction { Federation, Klingon, Romulan }

fe w notable points: (1) Instead of Main, it is called <Main>$, which is an “unspeakable”


A few
name that your code can’t refer to by name. (2) Your statements are placed in this generated
main method. (3) Your methods are also put into the main method as local functions.
fun ctions. (4) Any
type you define is placed outside the main method and the Program class.

Knowledge Check Large Programs 25 XP


Check your knowledge with the following questions:
1. True/False. A using directive makes
makes it so that you do not need to use fully qualified names.
270 LEVEL 33 MANAGING LARGER PROGRAMS
2. What namespace are each of following types in? (a) Console (b) List<T>, (c) StringBuilder .
of the following
3. What keyword is used to declare a namespace?
4. True/False. You should never write your own Program class and Main method.
Answers: (1) True. (2) (a) System (b) System.Collections.Generic, (c) System.Text. (3) namespace. (4) False.

Challenge The Feud 75 XP


On the Island of Namespaces, two families of ranchers are caretakers of the Medallion of Namespaces.
They are in a feud. They are the iFields and the McDroids. The iFields ranch sheep and pigs and the
McDroids ranch pigs and cows. Since both have pigs, they keep having conflicts. The two families will
give you the Medallion of Namespaces if you can resolve the dispute and help them track their animals.
Objectives:
• Create a Sheep class in the IField namespace (fully qualified name of IField.Sheep).
• Create a Pig class in the IField namespace (fully qualified name of IField.Pig ).
• Create a Cow class in the McDroid namespace (fully qualified name of McDroid.Cow).
• Create a Pig class in the McDroid namespace (fully qualified name of McDroid.Pig).
• For your main method, add using directives for both IField and McDroid namespaces. Make new
instances of all four classes. There are no conflicts with Sheep and Cow, so make sure you can create
new instances of those with new Sheep() and new Cow(). Resolve the conflicting Pig classes with
either an alias or fully
fu lly qualified names.

Challenge
Challe nge Dueling Traditions 100 XP
The inhabitants of Programain, guardians of the Medallion of Organization, seem to be hiding from you,
peering at you through shuttered windows, leaving you alone on the dusty streets. The only other people
on the road stand in front of you—a gray-haired wrinkle-faced woman and two toughs who stand just
behind her. “We heard a Programmer might be headed our way. But you’re no True Programmer. In the
Age Before, programmers declared their Main methods, used namespaces, and split their programs into
multiple files. You probably don’t even know what those things are. Bah.” She spits on the ground and
demands you leave, but you know you can win her and the townspeople over—and acquire the Medallion
of Organization—if you can show you know how to use the tools she named. Do the following with one
of the larger programs you have created in another challenge.
Objectives:
• Give your program a traditional Program and Main method instead of top-level statements.
• Place every type in a namespace.
• Place each type in its own file. (Small types like enumerations or records can be an exception.)
• Answer this question: Having used both top-level statements and a traditional entry point, which
do you prefer and why?
EVEL
L
METHOD󰁓 REVI󰁓ITED 34
󰁓peedrun
• A parameter can be given a default value, which then makes it optional when called: void
DoStuff(int x = 4) can be called as DoStuff(2) or DoStuff(), which uses the default of 4.
• You can name parameters when calling a method: DoStuff(x: 2). This allows parameters to be
supplied out of order.
• params lets you call a method with a variable number of arguments: DoStuff(params string[]
words) can be called like DoStuff("a") or DoStuff("a", "b", "c").
• Use ref or out to pass by reference, allowing a method to share a variable’s memory with another
and to prevent copying data: void PassByReference(ref int x) { ... } and then
PassByReference(ref
PassByReferenc e(ref a);. Use out when the called method initializes the variable.
• By defining a Deconstruct method (for example, void Deconstruct(out float x, out
float y) { ... }) you can unpack
u npack an object into multiple variables: (float x, float y) =
point;
• Extensions methods let you define static methods that appear as methods for another type: static
string Extension(this string text) { ... }

Level 13 introduced the basics of methods.


m ethods. But that was only scratching the surface. Let’s dig
into some advanced usages of methods that all C# developers should know.

OPTIONAL ARGUMENT󰁓
Optional arguments let you define a default value for a parameter. If you are happy with the
default, you don’t need to supply an argument when you call the method. Let’s say
say you wrote
this method to simulate rolling dice:
private Random _random = new Random();

public int RollDie(int sides) => _random.Next(sides) + 1;

This code lets you roll dice with any number of sides: traditional 6-sided dice, 20-sided dice,
or 107-sided dice. The flexibility is nice, but what if 99% of the time, you want 6-sided dice?
272 LEVEL 34 METHODS REVISITED
Y
Your peppered with RollDie(6) calls. That’s
our code would be peppered That’s not necessarily
nec essarily bad, but it does
make you wonder if there is a better way.
Y
You ust call the one above with a 6:
ou could define an overload with no parameters and then jjust
public int RollDie() => RollDie(6);

With optional arguments,


arguments, you can
can identify a default value
value where the method is defined:
defined:

public int RollDie(int sides = 6) => _random.Next(sides) + 1;


Only one RollDie method has been defined, but it can be called in either of these ways:
RollDie(); // Uses the default value of 6.
RollDie(20); // Uses 20 instead of the default.

Y
You
ou can have
have multiple parameters with an
an optional value, and you can
can mix them with
with normal
non-optional parameters, but the optional ones must come last.
Optional parameters should only be used when there is some obvious choice for the value or
usually called with the same value. If no standard or obvious value exists, it is generally better
to skip the default value.
Default values must be compile-time constants. You can use any literal value or an expression
made of literal values, but other expressions are not allowed. For example, you cannot use
new List<int>() as a default value. If you need
nee d that, use an overload.

NAMED ARGUMENT󰁓
When a method has many parameters or several of the same type, it can sometimes be hard
to remember which order they appear in. Math’s Clamp method is a good example because
it has three parameters of the same type:
Math.Clamp(20, 50, 100);

Does this clamp the value 20 to the range 50 to100 or the value 100 to the range 20 to 50?
When in doubt, C# lets you
you write out parameter
parameter names for each
each argument you are passing in:
Math.Clamp(min: 50, max: 100, value: 20);

This provides instant clarity about which argument is which, but it also allows you to supply
them out of order. Math.Clamp expects value to come first, but it is last here.
Y
You
ou do not have to name every
e very argument when using this feature; you can do it selectively.
But, once you start putting things out of order,
order, you can’t go back to unnamed arguments.

VARIABLE NUMBER OF PARAMETER󰁓


Look at this method that averages two numbers:
public static double Average(int a, int b) => (a + b) / 2.0;

What if you wanted to average


average three numbers? We
We could do this:
public static double Average(int a, int b, int c) => (a + b + c) / 3.0;
COMBINATIONS 273
What if you wanted 5? Or 10? You could add as many overloads as you want, but it grows
unwieldy fast.
Y
You c ould also make Average have an int[] parameter instead. But that results in uglier
ou could
code when you are calling it: Average(new int[] { 2, 3 }) vs. Average(2, 3).
The params keyword gives you the best of both worlds:
public static double Average(params int[] numbers)
{
double total = 0;

foreach (int number in numbers)


total += number;

return total / numbers.Length;


}

With that params keyword, you can call it like this:


Average(2, 3);
Average(2, 5, 8);
Average(41, 49, 29, 2, -7, 18);

The compiler
parameter doesinto
counts thean
hard work of transforming these methods with seemingly different
array.
You can also call this version of Average with an array if that is more natural for the situat
You situation.
ion.
You can only have one params parameter in a given method, and it must come after all
You
normal parameters.

COMBINATION󰁓
Y
You
ou can combine default arguments, named arguments, and variable arguments, though I
recommend getting used to each on their own before combining them.
Default arguments and named arguments are frequently combined. Imagine a method with
four parameters, where each has a reasonable default value:
public Ship MakeShip(int health = 50, int speed = 26,
int rateOfFire = 2, int size = 4) => ...

Y
You “standard” ship by calling MakeShip() with no arguments, taking advantage of all
ou get a “standard”
the default values. Or you can specify a non-default value for a single, specific parameter with
something like MakeShip(rateOfFire: 3). You get the custom value for the parameter
you name and default values
values for every other parameter.
parameter.

PA󰁓󰁓ING BY REFERENCE
As we saw in Levels
L evels 13 and 14,
14 , when calling a method, the values passed to the method are
duplicated
copied into into the called method’s
the parameter. parameters.
When a reference typeWhen a value
is passed, type is passed,
the reference the into
is copied valuethe
is
parameter.. This copying
parameter c opying of variable contents is called passing by value. In contrast, passing by
reference allows two methods to share a variable and its memory location directly. The
274 LEVEL 34 METHODS REVISI
REVISITED
TED
memory address itself is handed over rather than passing copied data to a method. Passing by
reference allows a method to directly
di rectly affect the calling method’s
method’s variables, which is powerful
but risky.
The terminology here is unfortunate. The concept of value types vs. reference types and the
concept of passing by value vs. passing by reference are separate. You can pass either type
using either mode. But as we’ll see, passing by reference has more benefits to value types than
it does to reference types.
Passing by reference means you do not have to duplicate data when methods are called. If you
Passing
are passing large structs around, or even small structs with great frequency, this can make
your program run much faster.
faster.
p arameter pass by reference, put the ref keyword before it:
To make a parameter
void DisplayNumber(ref int x) => Console.WriteLine(x);

You must also use the ref keyword when calling the method:
You
int y = 3;
DisplayNumber(ref y);

Here, DisplayNumber ’s x parameter does not have its own storage. It is an alias for another
variable. When DisplayNumber(ref y) is called, that other variable will be y.
The primary goal is to avoid the costs of copying large value types whenever we call a method.
While it achieves that goal, it comes with a steep price: the called method has total access to
the caller’s variables and can change them.
void DisplayNextNumber(ref int x)
{
x++;
Console.WriteLine(x);
}

If the above code used a regular (non- ref) parameter, x would be a local variable with its own
memory. x++ would affect only the new memory location. With ref, the memory is supplied
by the calling method, and x++ will impact the calling method.
This is typically undesirable—a cost that must be paid to get the advertised speed boosts. This
risk is why you must put ref where the method is declared and where the method is called.
Both sides must agree to share variables. But sometimes, it is precisely what you want. For
example, this method swaps the contents of two variables:
void Swap(ref int a, ref int b)
{
int temporary = a;
a = b;
b = temporary;
}

Due to their nature, passing by reference can only be done with a variable—something that
has a memory location.
l ocation. You cannot just supply an expression. You cannot do this:
DisplayNumber(ref 3); // COMPILER ERROR!
Passing by reference is primarily for value types. Reference types already get most of the
would-be benefits by their very nature
nature.. But reference types
types can also be passed by reference.
reference.
PASSING BY REFEREN
REFERENCE
CE 275
Methods assume parameters are initialized when the method is called. You must initialize any
variable that you pass by reference before calling it. The code below is a compiler error
because y is not initialized:
int y;
DisplayNumber(ref y); // COMPILER ERROR!

OutputOutput
Parameters
parameters are a special flavor of ref parameters. They are also passed by reference,
but they are not required to be initialized in advance, and the method must initialize an output
parameter before returning. Output parameters are made with the out keyword:
void SetupNumber(bool useBigNumber, out double value)
{
value = useBigNumber ? 1000000 : 1;
}

Which is called like this:


double x;
SetupNumber(true, out x);
double y;
SetupNumber(false, out y);
Mechanically, output parameters work the same as reference parameters. But as you can see,
neither x nor y was initialized beforehand. This code expects SetupNumber to initialize
those variables instead.
Output parameters are sometimes used to return more than one value from a method. You
will find plenty
plenty of code that does
does this, but also consider returning
returning a tuple
tuple or record
record since these
sometimes create simpler code.
When invoking a method with an output parameter,
parameter, yyou
ou can
can also declare
declare a variable right there,
there,
instead of needing to declare it on a previous line:
SetupNumber(true, out double x);
SetupNumber(false, out double y);

Y
You
ou will also encounter scenarios where the method you’re calling has an output parameter
that you don’t care to use. Instead of a throwaway variable like junk1 or unused2, you can
use a discard to ignore it:
SetupNumber(true, out _);

One notable usage of output parameters appears when parsing text. As we saw in Level
L evel 6, most
built-in types have a Parse method: int x = int.Parse("3");. If these methods are
called with bad data, they crash the program. These types also have a TryParse method,
whose return value tells you
you if it
it was able to parse
parse the data and supplies the parsed number as
an output parameter:
string? input = Console.ReadLine();
if (int.TryParse(input, out int value))
Console.WriteLine($"You entered {value}.");
else
Console.WriteLine("That is not a number!");
276 LEVEL 34 METHODS REVISITED

There’s More!
Passing by reference is a powerful concept. You will find the occasional use for it. But what we
Passing
have covered here is only scratching the surface. The details are
a re beyond this book, getting into
the darkest corners of C#. But just so you have an idea of what else is out there in these deep,
dark caverns, here are a few hints about how else passing by reference can be used.
Most of the time, the memory location shared when passing by reference is owned by the
calling method. The called method can originate a shared memory location using ref return
values. The rules are complex because they must ensure that the memory location returned
to the calling method
me thod will still be around after returning. There are also ref local variables that
function as local variables but are an alias for another variable.
Y
You
ou can also make a pass-by-reference inp ut parameter with the in keyword. This keyword
pass-by-reference input
hints that the method will not modify the variable passed to the method, but how it ensures
this is not straightforward. The compiler can easily enforce that you never assign a completely
new value to the supplied variable. The rest is trickier.
trickier. The compiler does not magically know
which properties and methods
me thods will modify the object and which won’t. To ensure the called
method doesn’t accidentally change the in parameter, it will duplicate the value into another
variable and call methods and properties on the copy instead. But bypassing those
duplications was a key reason for passing by reference in the first place, which somewhat
defeats the purpose. ToTo counter that, you can mark some structs and some struct methods as
readonly, which tells the compiler it is safe to call the method without making a defensive
copy first.
This sharing of memory locations is also the basis for a special type called Span<T>,
representing a collection that reuses some or all of another’s memory.

Challenge
Challe nge 󰁓afer Number Crunching 50 XP
“Master Programmer! We need your help! We are but humble number crunchers. We read numbers in,
work with them for a bit, then display the results. But not everybody enters good numbers. Sometimes,
we type in wrong things by accident. And sometimes, somebody does it on purpose. Trolls, looking to
cause trouble, I tell ya!
“We’ve heard about these so-called TryParse methods that cannot fail or break. We know you’re here
looking for Medallions
and we will Medall ions
join you at and allies.
alliesbattle.”
the final . If you can help us with this, the Medallion of Reference Passing
Pass ing is yours,

Objectives:
• Create a program that asks the user for an int value. Use the static int.TryParse(str
int.TryParse(string
ing s,
out int result) method to parse the number. Loop until they enter a valid value.
• Extend the program to do the same for both double and bool.

DECON󰁓TRUCTOR󰁓
With tuples, we can unpack the
the elements into multiple variables simultaneously:
simultaneously:

var tuple = (2, 3);


(int a, int b) = tuple;
EXTENSION METHODS 277
You can give your types this ability by defining a Deconstruct method with a void return
You
type and a collection
coll ection of output parameters. The following could be added to any of the various
Point types we have defined:
public void Deconstruct(out float x, out float y)
{
x = X;
y = Y;
}

While you can invoke the Deconstruct method directly (as though it were any other
method), you can also call it with code like this:
(float x, float y) = p;

By adding Deconstruct methods, you give any type this deconstruction ability. This is
especially useful for data-centric types. (Records have this automatically.)
You can define multiple Deconstruct overloads with different parameter lists.
You

EXTEN󰁓ION METHOD󰁓
An extension
another method
type (class, is a static method
enumeration, thatetc.)
interface, canasgive the appearance
an instance method.ofExtension
being attached to
methods
are useful when you want to add to a class
cl ass that you do not own. They also let you add methods
for things that can’t or typically don’t have them, such as interfaces or enumerations.
For example, the string class has the ToUpper and ToLower methods that produce
uppercase and lowercase versions of the string. If we wanted a ToAlternating method that
alternates between uppercase and lowercase with each letter, we would normally be out of
luck. We don’t own the string class, so we can’t add this method to it. But an extension
method allows us to define ToAlternating as a static method elsewhere and then use it as
though it were a natural part of the string class:
public static class StringExtensions
{
public
{ static string ToAlternating(this string text)
string result = "";

bool isCapital = true;


foreach (char letter in text)
{
result += isCapital ? char.ToUpper(letter) : char.ToLower(letter);
isCapital = !isCapital;
}

return result;
}
}

As shown
turns above,
it into an extension
an extension method
method this
is themust bekeyword
static and in a static
before class. Butfirst
the method’s the parameter.
magic that
Y
You
ou can only do this on the first parameter.
parameter.
278 LEVEL 34 METHODS REVISITED
When you define an extension method like
l ike this, you can call it as though it were an instance
method of the first parameter’s
parameter’s type:
string message = "Hello, World!";
Console.WriteLine(message.ToAlternating());

It is typical (but not required) to place extension methods for any given type in a class
cla ss with the
name [Type]Extensions . We defined an extension method for the string class, so the

class was StringExtensions.


Extension methods can have other parameters after the this parameter
parameter.. They are treated as
parameters when calling the method. So ToAlternat
normal parameters ToAlternating(this
ing(this string text,
bool startCapitalized) could be called with text.ToAlternating(false);.
Extension methods can only define new instance methods. You cannot use them to make
extension properties or extension static methods.

Knowledge Check Methods 25 XP


Check your knowledge with the following questions:
Use this for questions 1-3: void DoSomething(int
DoSomething(int x, int y = 3, int z = 4) { ... }
1. Which parameters are optional?
2. What values do x, y, and z have if called with DoSomething(1, 2);
3. What values do x, y, and z have if called with the following: DoSomething(x: 2, z: 9);
4. True/False. You must define all optional parameters after all required parameters.
5. True/False. A parameter with the params keyword must be the last.
6. What keyword is added to a parameter to to make an extension method?
7. What keyword
keyword indicates that a parameter is passed by reference?
8. Given the method void DoSomething(int x, params int[] numbers) { ... } which of
the following are allowed? (a) DoSomething(); (b) DoSomething(1); (c) DoSomething(1, 2,
3, 4, 5); (d) DoSomething(1
DoSomething(1, , new int[] { 2, 3, 4, 5 });

Answers: (1) y and z. (2) x=1,y=2,z=4. (3) x=2,y=3,z=9. (4) True. (5) True. (6) this. (7) ref or out (8) b, c, d.

Challenge Better Random 100 XP


The villagers of Randetherin often use the Random class but struggle with its limited capabilities. They
have asked for your help to make Random better. They offer you the Medallion of Powerful Methods in
exchange. Their complaints are as follows:
• Random.NextDouble() only returns values between 0 and 1, and they often need to be able to
produce random double values between 0 and another number, such as 0 to 10.
• They need to randomly choose from one of several strings, such as "up", "down", "left", and
"right", with each having an equal probability of being chosen.
• randomly picking a bool, and usually want it to be a fair coin toss (50%
They need to do a coin toss, randomly
heads and 50% tails) probabilitie s. For example, a 75% chance of true
tail s) but occasionally want unequal probabilities.
and a 25% chance of false.
Objectives:
• methods for Random.
Create a new static class to add extension methods
EXTENSION METHODS 279
• As described above, add a NextDouble extension method that gives a maximum value for a
randomly generated double.
• Add a NextString extension method for Random that allows you to pass in any number of string
values (using params) and randomly pick one of them.
• Add a CoinFlip method that randomly picks a bool value. It should have an optional parameter
that indicates the frequency of heads coming up, which should default to 0.5, or 50% of the time.

Answer
class thatthis
addsquestion: In youroropinion,
these methods would methods
use extension it be better
andto make a derived AdvancedRandom
why?
L EVEL
35
ERROR HANDLING AND EXCEPTION󰁓

󰁓peedrun
• Exceptions are C#’s primary error handling mechanism.
• Exceptions are objects of the Exception type (or a derived type).
• Put code that may throw exceptions in a try block, and place handlers in catch blocks: try {
Something(); } catch (SomeTypeOfException e) { HandleError(); }
• Throw a new exception with the throw keyword: throw new Exception();
• A finally block identifies code that runs regardless of how the try block exits—exception, early
return, or executing to completion: try { ... } finally { ... }
• This level contains several guidelines for throwing and catching exceptions.

We have been pretending nothing will ever


e ver go wrong in our programs, and it is time to face
reality. What should we do when things go wrong? Consider this code that gets a number from
the user between 1 and 10:
int GetNumberFromUser()
{
int number = 0;

while (number < 1 || number > 10)


{
Console.Write("Enter a number between 1 and 10: ");
string? response = Console.ReadLine();
number = Convert.ToInt32(response);
}

return number;
}

What happens ifif they enter “asdf ”? Convert.ToInt32 cannot convert this, and our program
“asdf”?
unravels. Under real-life circumstances, our program crashes and terminates. If you are
running in Visual Studio with a debugger attached, the debugger is smart enough to recognize
that a crash is imminent and pause the program for you to inspect its state in its death throes.

HANDLING EXCEPTIONS 281


In C#, when a piece of code
c ode recognizes it has encountered a dead end and cannot continue, a
kind of error called an exception can be generated by the code that detects it. Exceptions
bubble up from a method to its caller and then to that method’s caller, looking to see if
anything knows how to resolve the problem so that the program can keep running. This
process of transferring control farther up the call stack is called throwing the exception. Parts
of your code that react to a thrown exception are exception handlers. Or you could say that the
exception handler catches the exception to stop it from continuing
con tinuing further.
further.

HANDLING EXCEPTION󰁓
Most of our code can account for all scenarios without the potential for failure—for example,
Math.Sqrt can safely handle all square roots. (Though it does produce the value
double.NaN for negative numbers.) This is the ideal situation to be in. Success is guaranteed.
On the other hand, Convert.ToInt32 makes no such guarantee. When called with
"asdf", we encounter the problem. The text cannot be converted, and the method cannot
proceed with its stated job. Our approach for dealing with such errors has previously boiled
down to, “Dear user: Please don’t do the dumb. I can’t handle it when you do the dumb.” Then
cross your fingers, put on your lucky socks, and grab your Minecraft Luck of the Sea
enchantment.
Rather than hoping, let’s deal with this issue head-on. We must first recognize that a code
section might fail and also have a plan to recover. The problem code is placed in a try block,
problem
immediately followed by a handler for the exception:
try
{
number = Convert.ToInt32(response);
}
catch (Exception)
{
number = -1;
Console.WriteLine($"I do not understand '{response}'.");
}

The catchthere
contained blockwill
willrun
catch
so any
thatexception that arises
you can recover fromfrom
thewithin the try
problem. block,
In this case,and thefail
if we codeto
convert to an int for any reason, we will display the text "I do not understand..."
and set number to -1.
Let’s get more specific. When code detects a failure condition—something exceptional or
outside of the ordinary or expected—that code creates a new instance of the class
System.Exception (or something derived from Exception). This exception object
represents the problem that occurred, and different derived classes represent specific
categories of errors. This exception object is then thrown , which begins the process of looking
for a handler farther up the call stack. With the code above, Convert.ToInt32 contains the
code that detects this error, creates the exception, and throws it. We will soon see how to do
that ourselves.

block that handles this error. It doesConvert.ToInt32


The program will first look in the method for an appropriate catch
not exist, so the search continues to the calling method,
c ode did not have a catch block that could handle the issue, the
which is our code. If our code
282 LEVEL 35 ERROR HANDLING AND EXCEPTIONS
search would continue even further upward until an appropriate catch block handler is
found or it escapes the program’
program’ss entry point, in which case, the program would end in a crash.
Fortunately, this code now handles such errors, so the search ends at our catch block.
Once the code within the catch block runs, execution will resume after the try/catch
block.
If a try block has many statements and the first throws an exception, the rest of the code will
not run. It is crucial to pick the right section of code to place in your try blocks, but smaller
is usually better.

Handling 󰁓pecific Exception Types


Our catch block above handles all possible exception types. That’s not usually what you
want. It is generally better to be more specific about the kind of error
er ror.. Handle only the types
you can recover
recover from and handle different
different error types differently.
differently.
If we look at the documentation for Convert.ToInt32(string) , we see that it might
throw a System.FormatException or a System.OverflowException . The
FormatException class occurs when the text is not numeric, and OverflowException
occurs when the number is too big to store in an int. It makes sense to handle these in
different ways. We can modify our catch block into the following:
try
{
number = Convert.ToInt32(response);
}
catch (FormatException)
{
number = -1;
Console.WriteLine($"I do not understand '{ response }'.");
}
catch (OverflowException)
{
number = -1;
Console.WriteLine($"That number is too big!");
}
This code defines two separate catch blocks associated with a single try block, one for each
of the ways Convert.ToInt32 can fail. Doing so allows us to treat each error type differently.
handler, the order matters. FormatException and
When looking for an exception handler,
OverflowException are distinct exception types, but consider this code:
try { ... }
catch (FormatException) { ... }
catch (Exception) { ... }

The first block will handle a FormatException because it comes first. The second one will
handle every other exception type because everything is derived from Exception.

A try/do
simply catch block does
the following not
if we need to:
wanted to handle every imaginable exception type. We could
try { ... }
catch (FormatException) { ... }

THROWING EXCEPTIONS 283


This will catch FormatException objects but leave other errors for something else to
address. Code that
that cannot reasonably resolve a specific problem type should n
not
ot catch it.

Using the Exception Object


An exception handler can use the exception object in its body if it needs
nee ds to. To do so, add a
name after the exception type in the catch’s parentheses:
try { ... }
catch (FormatException e)
{
Console.WriteLine(e.Message);
}

The Exception class defines a Message property, so all exception objects have it. Other
exception types may add additional data that can be helpful, though neither Format
Exception nor OverflowException does this.

THROWING EXCEPTION󰁓
Let’s now look at the other side of the equation: creating and throwing new exceptions.
The first thing your code must do is recognize a problem. You will have to determine for
yourself what counts as an unresolvable
unresolvable error in your code. But once you ha
have
ve detected such
a situation, you are ready to create and throw an exception.
Exceptions are represented by any object whose class is Exception or a derived class.
Creating an exception object is like making any other object: you use new and invoke one of
its constructors. Once created, the next step is to throw the exception, which begins the
process of finding a handler for it. These are often done in a single statement:
throw new Exception();

The new Exception() part creates the exception object. The throw keyword is the thing
that initiates the hunt up the call stack for a handler.
handler. In context, this could look something
some thing like
this:
Console.WriteLine("Name an animal.");
string? animal = Console.ReadLine();
if (animal == "snake") throw new Exception(); // Why did it have to be snakes?

The Exception class represents the most generic error in existence. With this code, all we
know is that something went wrong. In general, you want to throw instances of a class derived
from Exception, not Exception itself. Doing so allows us to convey what went wrong more
accurately and enables handlers to be more specific about if and how to handle it.
There is a mountain of existing exception types that you can pick from, which represent
various situations.
situations. Here are
are a few of the more common ones,
ones, along with their meanings.
meanings.
284 LEVEL 35 ERROR HANDLING AND EXCEPTIONS
Exception Name Meaning
NotImplementedException “The programmer hasn’t written this code yet.”
NotSupportedException “I will never be able to do this.”
InvalidOperationException “I can’t do this in my current state, but I might be able to in another state.”
ArgumentOutOfRangeException “This argument was too big (too small, etc.) for me to use.”
ArgumentNullException “This argument was null, and I can’t
ca n’t work with a null value.”
“Something is wrong with one of your arguments.”
ArgumentException
Exception “Something went wrong, but I don’t have any real info about it.”

Rather than using new Exception() earlier, we should have picked a more specific type.
Perhaps NotSupportedException is a better choice:
Console.WriteLine("Name an animal.");
string? animal = Console.ReadLine();
if (animal == "snake") throw new NotSupportedException();

Most exception types also allow you to supply a message as a parameter, and it is often helpful
to include one to help
h elp programmers who encounter it later:
if (animal == "snake") throw new NotSupportedException("I have ophidiophobia.");

Depending on the exception type, you might be able (or even required) to supply additional
information to the constructor.
If one of the existing exception types isn’t sufficient to categorize an error
error,, make your own by
defining a new class derived from Exception or another exception class:
public class SnakeException : Exception
{
public SnakeException() { }
public SnakeException(string message) : base(message) { }
}

Always use a meaningful exception type when you throw


throw exceptions. Avoid
Avoid throwing
throwing plain old
Exception. Use an existing type if it makes sense. Otherwise, create a new one.

THE FINALLY BLOCK


A finally block is often used in conjunction with try and catch. A finally block
contains code that should run regardless of how the flow of execution leaves a try block,
whether that is by typical completion of the code, throwing
throwing an exception, or an early return:
return:
try
{
Console.WriteLine("Shall we play a game?");
if (Console.ReadLine() == "no") return;

Console.WriteLine("Name an animal.");
string? animal = Console.ReadLine();
if (animal == "snake") throw new SnakeException();
}
catch (SnakeException) { Console.WriteLine("Why did it have to be snakes?"); }
finally
{

EXCEPTION GUIDELINES 285


Console.WriteLine("We're all done here.");
}

There are three ways to exit the try block above; the finally block runs in all of them. If the
early return on line 4 is encountered, the finally block executes before returning. If the end
of the try block is reached through normal execution, the finally block is executed. If a
SnakeException is thrown, the finally block executes after the SnakeException
handler runs. If this code threw a different exception not handled here, the finally block
still runs before leaving the method to find a handler.
handler.
The purpose of a finally block is to perform cleanup reliably. You know it will always run,
so it is a good place to put code that ensures things are left in a good state before moving on.
As such, it is have just a try and a finally with no catch blocks at all.
is not uncommon to have

EXCEPTION GUIDELINE󰁓
Let’s look at some guidelines for throwing and catching exceptions.

What to Handle
Any exception
have a bias for that goesexceptions
catching unhandledinstead
will crash the program.
progra
of letting m.go.
them In But
general,
general, this means
exception you code
handling should
is
more complicated than code that does not. Code understandability is also valuable.
Catching exceptions is especially important in products where failure means loss of human
life or injury versus a low-s
l ow-stakes
takes utility that will almost always be used correctly.
cor rectly. In these low-
stakes, low-risk
low-risk programs, skipping
skipping some or all the exception handling could be an acceacceptable
ptable
choice. Every program we have made so far could arguably fit into this category.
Still, handling exceptions allows a program to deal with strangeness and surprises. Code that
does this is robust. Even if nobody dies from a software crash, your users will appreciate it
being robust. With exception handling knowledge, you should have a bias for doing it, not
skipping it.

Only Handle What You Can Fix


If an exception handler cannot resolve the problem represented by the exception, the handler
should not exist. Instead, the exception should be allowed to continue up the call stack,
hoping that something farther up has meaningful resolution steps for the problem. This is a
counterpoint to the previous item. If there is no recourse for an error, it is reasonable for the
program to end.
There are some allowances here. Sometimes, a handler will repair or address what it can (even
just logging the problem)
problem) while still allowing the exceptio
exception
n to continue (described lat
later).
er).

Use the Right Exception Type


An exception’s
exception’s type (class) is the simplest way to differentiate
differentiate one error category
category from
from another.
another.
By
youpicking theeasier
make life right
easier exception type when
when handling throwing exceptions (making your own if needed),
exceptions.
286 LEVEL 35 ERROR HANDLING AND EXCEPTIONS

Avoid Pokémon Exception Handling


Sometimes, it is tempting to handle any possible error
e rror in the same way with this:
catch (Exception) { Console.WriteLine("Something went wrong."); }

Some programmers call this Pokémon exception handling. Using catch (Exception)
catches every possible exception with no… um… exceptions. It is reminiscent of the
catchphrase from the game Pokémon, “Gotta catch ‘em all!”
The problem with treating everything the same is that it is often too ge
generic.
neric. “Something went
wrong” is an awful error message. Whether solved by humans or code, an error’s recourse is
rarely the same for all possible errors.
There are, of course, times where this is the only thing that makes sense. Some people w will
ill put
a catch (Exception) block around their entire program to catch any stray unhandled
exceptions as the program is dying to produce an error
e rror report or something similar. B
But
ut letting
the program attempt to resume is often dangerous because we have no guarantees about the
program’’s state when the exception occurred. So use Pokémon exception handling sparingly,
program sparingl y,
and in general, let the program die afterward.

Avoid Eating Exceptions


A catch block that looks like this is usually bad:
catch (SomeExceptionType) { }

An empty handler like this is referred to as “eating


“eating the exception,” “swallowing the error
er ror,,” or
“failing silently.”
silently.” Correct exception handling rarely requires doing nothing at all. Empty catch
blocks nearly always represent a programmer who got lazy.
The problem is that an error occurred, and no response was taken to address it. It may leave
the program in a weird or inconsistent state—one in which the program should not be
running.
Eating exceptions is especially bad when combined with the previous item: catch
(Exception) { }. Here, every single error is caught and thrown right into the garbage
chute.

Avoid Throwing Exceptions When Possible


Exceptions are a useful tool, but you should not throw exceptions that you do not need to
throw. Avoid
Avoid exceptions if simple logic is sufficient. The following is trivialized but illustrative:
try
{
Console.WriteLine("Name an animal.");
string? animal = Console.ReadLine();
if (animal == "snake") throw new Exception();
}
catch (Exception)
{

} Console.WriteLine("Snakes. Why did it have to be snakes?");

Instead of that, just do this:


EXCEPTION GUIDELINES 287
Console.WriteLine("Name an animal.");
string? animal = Console.ReadLine();
if (animal == "snake") Console.WriteLine("Why did it have to be snakes?");

The result is the same, but with cleaner code. If you can use logic like if statements, loops,
etc., those are usually better approaches.

Come Back with Your 󰁓hield or On ItIt


In ancient Greece, soldiers would go into battle advised to “come back with your shield or on
it.
it.”” Coming back with your shield meant winning
w inning the fight. Coming back on your shield meant
dying with honor and being carried home on your shield. Somebody who abandons their duty
would run from the battle
battle and drop their
their heavy shield in the process,
process, returning
returning home alive but
without their shield. Or so the story goes. Military strategy aside, this is a good rule for
exceptions. When a method runs, it should either do its job and run
ru n to completion or fail with
honor by throwing an exception but leaving things in the state it began in. It should not
abandon its job halfway through and leave things partly changed and partly unchanged.
A finally block is often your best tool for ensuring you can get back to your original state.
If you cannot put things back exactly as they were when you started, you should at least put
things into a self-consistent state. To illustrate, consider this contrived scenario. You have a
variable that is expected to be even but must be incremented twice and may throw an
exception while changing it:
_evenNumber++;
MaybeThrowException();
_evenNumber++;

If _evenNumber was a 4 and things go well, this will become a 6 afterward. If an exception is
thrown, then using the “with your shield or on it” rule (also called the strong exception
guarantee), you should revert _evenNumber to a 4. In this case, it requires extra bookkeeping:
int startingValue = _evenNumber;
try
{
_evenNumber++;
MaybeThrowException();
_evenNumber++;
}
finally
{
if (_evenNumber % 2 != 0) _evenNumber = startingValue;
}

If that is not possible, we should not leave _evenNumber as a 5, which is an odd number and
goes against expectations. Setting _evenNumber to 0 in a finally block at least leaves the
program in a “correct” state.
try
{
_evenNumber++;
MaybeThrowException();
_evenNumber++;
}
finally
{

288 LEVEL 35 ERROR HANDLING AND EXCEPTIONS


if (_evenNumber % 2 != 0) _evenNumber = 0;
}

ADVANCED EXCEPTION HANDLING


In this section, we will visit a handful of more advanced aspects of exception handling. Each
of these is an essential feature that sees a fair
f air bit of use.

󰁓tack Traces
Each exception, once thrown, contains a stack trace. The stack trace describes methods
currently on the stack, from the program’s entry point to the exception’s origination site.
Consider this simple program:
DoStuff();

void DoStuff() => DoMoreStuff();


void DoMoreStuff() => throw new Exception("Something terrible happened.");

The main method calls DoStuff, which calls DoMoreStuff , which throws an exception. The
occ urred in DoMoreStuff, called by
stack trace for this exception reveals that the exception occurred
DoStuff, called by Main.
Each exception has a StackTrace property that you can use to see this stack trace. However,
However,
Exception has overridden ToString to include this. Doing something like
Console.WriteLine(e) is an easy way to see it. To illustrate, we can wrap DoStuff in a
try/catch block and use the console window to display the exception:
try { DoStuff(); }
catch (Exception e) { Console.WriteLine(e); }

Running this displays the following:


System.Exception: Something terrible happened.
at Program.<<Main>$>g__DoMoreStuff|0_1() in C:\some\path\Program.cs:line 14
at Program.<<Main>$>g__DoStuff|0_0() in C:\some\path\Program.cs:line 12
at Program.<Main>$(String[] args) in C:\some\path\Program.cs:line 7

This gives you the exception type and message, followed by the stack trace. Each element in
the stack makes an appearance, showing the method signature, the path to the file, and even
the line number!
This particular stack trace is short but uglier than most. The compiler names your main
method <Main>$, and local functions like DoStuff and DoMoreStuff always end up with
strange final names. Most stack traces you see will not be so alien.
The stack trace can help you understand what happened and where things went wrong.
Having said that, if you are running your program from Visual Studio (or another IDE), the
debugger can also show this information and more. See Bonus Level C for more information.

Rethrowing Exceptions
After catching
catching an exception, you sometimes realize
realize that
that you cannot handle the exception after
all and need it to continue up the call stack. A simple approach is just to throw it again:
try { DoStuff(); }
catch (Exception e)

ADVANCED EXCEPTION HANDLING 289


{
Console.WriteLine(e);
throw e;
}

There is a catch. An exception’s stack trace is updated when thrown, not when created. That
means when you throw an exception, as shown above, the stack trace will change to its new
location in this catch block, losing useful information. There are times where this is
desirable. Most of the time, it is not. There’s
There’s another option:
try { DoStuff(); }
catch (Exception e)
{
Console.WriteLine(e);
throw;
}

A bare throw; statement will rethrow the caught exception without modifying its original
stack trace. This makes it easy to let a caught exception continue looking for a handler.
handler.
Perhaps the more useful case for rethrowing
rethrowing exceptions is to inject some llogic
ogic for an exception
without handling or resolving it. The code above does just that by logging (to the console
window) exceptions as
as they occur without preventing
preventing the crash.

Inner Exceptions
Sometimes, when you catch an exception, you want to replace it with another. This is
especially common when some low-level thing is misbehaving, and you want to transform it
into a set of exception types that indicate higher-level problems. You can, of course, catch the
low-level exception and then throw a new exception:
e xception:
try { DoStuff(); }
catch (FormatException e)
{
throw new InvalidDataException("The data must be formatted correctly.");
}
catch (NullReferenceException e)
{

} throw new InvalidDataException("The data is missing.");

Like with rethrowing exceptions, this loses information in the process. Each exception has a
property called InnerException , which can store another exception that may have been
the underlying cause.
Most exception classes let you create new instances with no parameters ( new
Exception()), with a single message parameter ( new Exception("Oops")), or with a
message and an inner exception ( new Exception("O
Exception("Ooops",oops", otherExcep
otherException)
tion)). This
inner exception allows you to supply an underlying cause when creating a new exception,
preserving the root cause. When you create new exception types, you should make similar
constructors in your new class to allow the pattern to continue.
c ontinue.

Exception Filters
Most of the time, you decide whether to handle an exception based solely on the exception’s
type. If you need more nuance, you can use exception filters. An exception filter is a simple
bool expression that must be true for a catch block to be selected. The filter allows you to

290 LEVEL 35 ERROR HANDLING AND EXCEPTIONS


inspect the exception object’s other properties. The following uses a made-up CodedError
Exception:
try { DoStuff(); }
catch (CodedErrorException e) when (e.ErrorCode == ErrorCodes.ConnectionFailure)
{ ... }

This catch block will only execute for CodedErrorExceptions whose ErrorCode
property is ErrorCodes.ConnectionFailure .
Challenge Exepti’s
Exepti’s Game 100 XP
On the Island of Exceptions, you find the village of Excepti, which has seen little happiness and joy since
the arrival of The Uncoded One. The Exceptians used to have a game that they played called Cookie
Exception. The village leader, Noit Pecxe, promises the warriors of Excepti will join you against the
Uncoded One if you can recreate their ancient tradition in a program. Noit offers you the Medallion of
Exceptions as well.
Cookie Exception is played by gathering nine chocolate chip cookies and one oatmeal raisin cookie. The
cookies are mixed and put in a dark room with two players who can’t see the cookies. Each player takes
a turn picking a cookie randomly and shoving it in their mouth without seeing whether it is a delicious
chocolate chip cookie or an awful oatmeal raisin cookie. If they pick wrong and eat the oatmeal raisin
cookie, they lose. If their opponent eats the oatmeal raisin cookie, then they win.
Objectives:
• The game will pick a random number between 0 and 9 (inclusive) to represent the oatmeal raisin
cookie.
• The game will allow players to take turns picking numbers between 0 and 9.
• If a player repeats a number that has been already used, the program should make them select
another. Hint: If you use a List<int> to store previously chosen numbers, you can use its
Contains method to see if a number has been used before.
• If the number matches the one the game
gam e picked initially, an exception should be thrown, terminating
the program. Run the program at least once like this to see it crash.

Put in a try/catch block to handle the exception and display the results.
• Answer this question: Did you make a custom exception type or use an existing one, and why did
you choose what you did?
• Answer this question: You could write this program without exceptions, but the requirements
demanded an exception for learning purposes. If you didn’t have that requirement, would you have
used an exception? Why or why not?
36
LEVELDELEGATE󰁓
󰁓peedrun
• A delegate is a variable that stores methods, allowing them to be passed around like an object.

Define delegates like this: public delegate float NumberDelegate(float
NumberDelegate(float number);. This
identifies the return type and parameter
parameter list of the new delegate type.
• variables like this: NumberDelega
Assign values to delegate variables NumberDelegatete d = AddOne;
• Invoke the method stored in a delegate variable: d(2), or d.Invoke(2).
• Action, Func, and Predicate are pre-defined generic delegate types that are flexible enough that
you rarely have to build new delegate types from scratch.
• Delegates can refer to multiple methods if needed, andand each method will be called in turn.

DELEGATE BA󰁓IC󰁓
todelegate
A is a variable
pass around chunksthat holds a reference
of executable code as to a method
though or function.
it were ThisThat
simple data. feature
mayallows you
not seem
like a big deal, but it is a game-changer. Delegates are powerful in their own right but also
serve as the basis of many other powerful C# features.
Let’s look at the type of problem they help
he lp solve. Suppose you have this method, which takes
an array of numbers and produces a new array where every item has been incremented. If the
array [1, 2, 3, 4, 5] is passed in, the result will be [2, 3, 4, 5, 6].
int[] AddOneToArrayElements(int[] numbers)
{
int[] result = new int[numbers.Length];

for (int index = 0; index < result.Length; index++)


result[index] = numbers[index] + 1;

return result;
}
What if we also need a method that subtracts
subtracts one instead? Not
Not a big deal:

292 LEVEL 36 DELEGATES


int[] SubtractOneFromArrayElements(int[] numbers)
{
int[] result = new int[numbers.Length];

for (int index = 0; index < result.Length; index++)


result[index] = numbers[index] - 1;

return result;
}
These two methods are identical except for the code that ccomputes
omputes the new array’s value from
the original value. You could create both methods and call it a day, but that is not ideal. It is a
large chunk of duplicated code. If you needed
need ed to fix a bug, you’d ha
have
ve to do so in two places.
We could maybe
maybe add another parameter
parameter to indicate how
how much to change the number:
int[] ChangeArrayElements(int[] numbers, int amount)
{
int[] result = new int[numbers.Length];

for (int index = 0; index < result.Length; index++)


result[index] = numbers[index] + amount;

return result;
}
To add and subtract, we could call ChangeArrayElements(numbers, +1) and Change
ArrayElements(numbers, -1). But there is only so much flexibility we can get. What if
we wanted a similar method that doubled each item or computed each item’
item’s square root?
root?
To give the calling method the most
mo st flexibility, we can ask it to supply a method to use instead
of adding a specific number.
This is easier to illustrate with an example. Let’s start by defining the methods AddOne,
SubtractOne, and Double:
int AddOne(int number) => number + 1;
int SubtractOne(int number) => number - 1;
int Double(int number) => number * 2;

These methods have the same parameter list (a single int parameter) and the same return
type (also an int). That similarity is essential; it is what will make them interchangeable.
The next step is for us to give a name to this pattern by defining a delegate type:
public delegate int NumberDelegate(int number);

This defines a new type, like defining a new enumeration or class. Defining a new delegate
type requires a return type, a name, and a parameter list. In this sense, it looks like a method
declaration, aside from the delegate keyword.
Variables that use delegate types can store methods. But the method must match the
Variables
delegate’s return type and parameter types to work. A variable whose type is
NumberDelegate can store any method with an int return type and a single int paramete
parameterr.
Lucky for us, AddOne, SubtractOne , and Double all meet these conditions. That means we
can make a variable that can store a reference to any of them.
There are three parts to using a delegate: making a variable of that type, assigning it a value,
and then using it.

DELEGATE BASICS 293


Any variable can use a delegate
del egate for its type, just like we saw with enumerations
enume rations and classes.
We can make a method with a parameter whose type is NumberDelegate , which will allow
callers of the method to supply a different method to invoke when the time is right:
right:
int[] ChangeArrayElements(int[] numbers, NumberDelegate operation) { ... }

To call ChangeArrayElements with the delegate, we name the method we want to use:

ChangeArrayElements(new int[] { 1, 2, 3, 4, 5 }, AddOne);


Note the lack of parentheses! With parentheses, we’d
we’d be invoking the method and passing its
return value. Instead, we are passing the method itself by name.
If the method is an instance method, you can name the object with its method:
SomeClass thing = new SomeClass();
ChangeArrayElements(new int[] { 1, 2, 3, 4, 5 }, thing.DoIt);

The C# compiler is smart enough to keep track of the fact that the delegate must store a
reference to the instance (thing) and know which method to call (DoIt).
On rare occasions, the compiler may struggle to understand what you are doing. In these
cases, you may need to be more formal with something like this:

ChangeArrayElements(new int[] { 1, 2, 3, 4, 5 }, new NumberDelegate(AddOne));


That shouldn’t happen very often, though.
Let’s see how ChangeArrayElements would use this delegate-typed variable. Because a
delegate holds a reference to a method, you will eventually
even tually want to invoke the method. There
are two ways to do this. The first is shown here:
int[] ChangeArrayElements(int[] numbers, NumberDelegate operation)
{
int[] result = new int[numbers.Length];

for (int index = 0; index < result.Length; index++)


result[index] = operation(numbers[index]);

} return result;

Y
You
ou can invoke
invoke the method inin a delegate variable by using parentheses. Invoking
Invoking a method in
a delegate-typed variable looks like a typical method call, except perhaps the capitalization.
(Most methods in C# start with a capital letter
lett er.. Most parameters do not.)
delegate’s Invoke method:
The second way is to use the delegate’s
result[index] = operation.Invoke(numbers[index]);

These are the same thing for all practical purposes, though this second option allows you to
check for null with a simple operation?.Invoke(numbers[index]) .
By looking at this code, you can see why delegates are called that. ChangeArrayElements

knows how to
to compute newiterate through
values thevalues.
from old array and build asomebody
It expects new array,else
but to
it doesn’t understand
do that work, how
and when
the time comes, it delegates that job to the delegate object.
294 LEVEL 36 DELEGATES
Delegates can significantly increase the flexibility
fl exibility of sections of code. It can allow you to define
algorithms with replaceable elements in the middle, filled in by other methods via delegates.
That makes them a valuable tool to add to your
y our C# toolbox.

THE ACTION, FUNC, AND PREDICATE DELEGATE󰁓


In
youthe last asection,
want specificwe defined
name a new
given to adelegate
methodtype to use in our
pattern—but program.
if you Thatcards
play your has its uses—if
right, you
won’t have to define new delegate types often. The Base Class Library Library contains a flexible and
extensive collection of delegate
deleg ate types that cover most scenarios.
Two sets of generic delegate types cover virtually all situations: Action and Func. Each is a
set of generic delegates rather than a single delegate type.
The Action delegates have a void return type. They capture scenarios where a method
performs a job without producing a result. The simplest one, known simply as Action, is a
delegate for any function with no parameters and a void return type, such as void
DoSomething() .
nee d one parameter, Action<T> is what you want. It is generic, so the right flavor will
If you need
allow you to account for any parameter type—for example, Action<string> for a method
like void DoSomething(string value). There are versions of Action with up to 16
parameters, though if you have a method with more than 16 parameters, change your design.
Y ou could use Action<string, int, bool> for void DoSomething(string
You
message, int number, bool isFancy).
The Func delegates (short for “function”) are for when you need a return value.
Func<TResult> is the simplest version, and it has a generic return type ( TResult). Use this
for a method with no parameters. Func<int> could be used for the method int
GetNumber(). If you need parameters, there’s a Func for that too. Func<T, TResult> is
for a single parameter. You could use Func<int, double> for double DoSomething(
int number). Like Action, there is a version with up to 16 parameters plus a return type,
and all are generic. For example, Func<string, int, bool, double> works for

double DoSomething(string message, int number, bool isFancy).


Y
You
ou can use one of the above delegate types for any situation where delegates could come in
handy. Our NumberDelegate could have been done with Func<int, int>. Some
programmers almost exclusively use these delegate types. Others tend to make their own so
they can give them more descriptive
descr iptive names.
One other delegate type is worth noting here: Predicate<T>. The mathematical definition
of predicate is a condition used to determine whether something belongs to a set.
Predicate<T> represents a method that takes an object of the generic type T and returns a
bool. (That makes it equivalent to Func<T, bool>.) Its definition looks something like this:
public delegate bool Predicate<T>(T value);

(This also illustrates how to define generic delegates.) For example, we could define IsEven
and IsOdd methods that tell you if a number belongs to the set of even numbers or odd
numbers. The name Predicate<T> reveals its intended use better than Func<T, bool>
and spares you from filling in two generic type parameters.

MUL
MULTICASTDE
TICASTDELEGATE
LEGATE AND DELEGATE CHAINING 295

MULTICASTDELEGATE AND DELEGATE CHAINING


Behind the scenes, declaring new delegate types creates new classes derived from the special
class MulticastDelegate . That name hints at doing things in multiples, and indeed you
can. Each delegate object can store many methods, not just a single one. This collection is
called a delegate chain. When a delegate is invoked, each method in the delegate chain will be
called in turn.
In practice, this is rare. (Though see Level 37 where events put this ability to use.) Doing so
brings up at least one notable concern: what return value does a delegate with multiple
methods return? It cannot account for all of the return values. The return value will be tthat
hat of
the last method attached, ignoring the rest. If you are going to attach multiple methods to a
multicast delegate, you should only do so with the void return type.
Attaching additional methods to a delegate can be done with the += or + operators and
subsequently detached with the -= or - operators. For example, suppose you have the
following delegate:
public delegate void Log(LogLevel level, string message);

Y
You
ou could get a delegate-typed variable to invoke many
many methods with the same parameter
parameter list
and return type like this:
Log logMethods = LogToConsole;
logMethods += LogToDatabase;
logMethods += LogToTextFile;

When you invoke logMeth


logMethods(LogLevel.W
ods(LogLevel.Warning,
arning, "A problem happened.");, it
will call all three of those methods.
methods. Y
You
ou could also write it like this:
Log logMethods = new Log(LogToConsole) + LogToDatabase + LogToTextFile;

If any of the methods throw an exception while running, the other delegate methods will not
get a chance to run. When used this way, attached methods should not let exceptions escape.

Challenge The 󰁓ieve 100 XP

The
themIsland of Delgata
as good is home toInthe
or bad numbers. Numeromechanical
ancient Sieve,
times, the sieve coulda be
machine thatwith
supplied takes numbers
a single and judges
method to use
as a filter by the island’s rulers, making the sieve adaptable as leadership changed over time. The
Delgatans will give you the Medallion of Delegates if you can reforge their Numeromechanical Sieve.
Objectives:
• Create a Sieve class with a public bool IsGood(int number) method. This class needs a
constructor with a delegate parameter that can be invoked later within the IsGood method. Hint:
You can make your own delegate type or use Func<int, bool>.
• Define methods with an int parameter and a bool return type for the following: (1) returns true
for even numbers, (2) returns true for positive numbers, and (3) returns true for multiples of 10.
• Create a program that asks the user to pick one of those three filters, constructs a new Sieve

instance by displaying
repeatedly, passing in one of those
whether the methods
number isasgood
a parameter, and then ask
or bad depending the filter
on the user toin enter
use. numbers
• Answer this question: Describe how you could have also solved this problem with inheritance an
and
d
polymorphism. Which solution seems more straightforward to you, and why?

LEVEL 37
EVENT󰁓

󰁓peedrun

Events allow a class to notify interested observers that something has occurred, allowing them to
respond to or handle the event: public event Action ThingHappened;
• Events use a delegate type to indicate what a handler must look like.
• Raise events like this: ThingHappened(), or ThingHappend?.Invoke();
• Events can use any delegate type but should avoid non-void return types.
• Other types can subscribe and unsubscribe to an event by providing a method: something.
ThingHappened += Handler; and something.Thi
something.ThingHappened
ngHappened -= Handler;
• Don’t forget to unsubscribe; objects that stay subscribed will not get garbage collected.

C# EVENT󰁓
In C#, events are a mechanism that allows an object to notify others that something has
changed or happened so they can respond.
Suppose we were making the game of Asteroids. Let’s say we have a Ship class, representing
the concept of a ship, including if it is dead or alive, and a SoundEffectManager class,
which has the responsibility to play sounds.
sounds. We have
have an instance of each. When a ship blows
up, an explosion sound should play. We have a few options for addressing this.
If the Ship class knows about the SoundEffectManager, it could call a method:
_soundEffectManager.PlaySound("Explosion"); . This design is not unreasonable.
But if eight things need to respond to the ship it’ss less reasonable for Ship itself to
sh ip exploding, it’
reach out and call all of those different methods in response. As the number grows, the design
looks worse and worse.
Alternatively, we could ask each of those objects to implement some inte
interface
rface like this:
public interface IExplosionHandler
{

C# EVENTS 297
void HandleShipExploded();
}

SoundEffectManager could implement this interface and play the right sound. The other
seven objects could do a similar thing. The Ship class can have a list of IExplosion
Handler objects and call their HandleShipExploded method after the ship explodes. A
slice of Ship might look like this:
public class Ship
{
private List<IExplosionHandler> _handlers = new List<IExplosionHandler>();

public void AddExplosionHandler(IExplosionHandler newHandler) =>


_handlers.Add(newHandler);

private void TellHandlersAboutExplosion()


{
foreach (IExplosionHandler handler in _handlers)
handler.HandleShipExploded();
}
}

Something within Ship would need to recognize that the ship has exploded and call
TellHandlersAboutExplosion .
The nice part of this setup is that the ship does not need to know all eight handlers’ unique
aspects. Those objects sign up to be notified by calling AddExplosionHandler.
C# provides a mechanism based on this approach that makes things very easy: events. Any
class can create an event as a member, similar to making properties and methods. Any other
object interested in reacting to the event—a listener or an observer—can subscribe to the event
to be notified when the event occurs. The class that owns the event can then raise or fire the
event when the time is right, causing each listener’
listener’ss handler to run.
Defining an event is shown below:
public class Ship
{
public event Action? ShipExploded;
// The rest of the ship's members are defined here.
}

An event is defined using the event keyword, followed by a delegate type, then its name. Like
every other member type, you can add an accessibility modifier to the event, as we did here
with public. Events are typically public.
In many ways, declaring an event is like an auto-property. Behind the scenes, a delegate object
is created as a backing field for this event. In the case above, this delegate’s type will be
Action? (no parameters and a void return type, and with null allowed) since that is what
the event’s declaration named.
When the Ship class detects the explosion, it will raise or fire this event, as shown below:
public class Ship
{
public event Action? ShipExploded;

298 LEVEL 37 EVENTS


public int Health { get; private set; }

public void TakeDamage(int amount)


{
Health -= amount;
if (Health <= 0)
ShipExploded();
}
}
This notifies listeners that the event occurred, allowing them to run code in response.
Alternatively, we can use the Invoke method:
if (Health <= 0)
ShipExploded.Invoke();

l istener’ss side now. If we want SoundEffectManager to respond to the ship


Let’s look at the listener’
exploding, we define a method that matches the event’s delegate type. In this case, that type
is Action, which has a void return type and no parameters. This method can be called
whatever we want, starting with On or Handle are both common:
want, but names starting
public class SoundEffectManager
{
private void OnShipExploded() => PlaySound("Explosion");

public void PlaySound(string name) { ... }


}

Next, we need to attach this method to the event. We could do this in the constructor:
public SoundEffectManager(Ship ship)
{
ship.ShipExploded += OnShipExploded;
}

This attaches or subscribes the OnShipExploded method to the event, ensuring it will be
called when the event fires.

When you are done, you can detach or unsubscribe the event like this:
ship.ShipExploded -= OnShipExploded;

The benefits of events are substantial. The object declaring the event does not have to know
details about each object that responds to it. Each handler subscribes to the event with one of
its methods, and everything else is taken care of automatically. Plus, unlike our interface
approach, objects responding
responding to the event do not need to implement any particular interface.
They can call their event handler whatever makes sense for them.

Events with Parameters


The code above used Action, which has no parameters. But events can supply data as
arguments to their listeners by using a delegate type that includes parameters. For example,
we could report the explosion’s
explosion’s location with this:
this:
public class Ship
{
public event Action<Point>? ShipExploded;

C# EVENTS 299
public int Health { get; private set; }
public Point Location { get; private set; } = new Point(0, 0);

public void TakeDamage(int amount)


{
Health -= amount;
if (Health <= 0)
ShipExploded(Location);

} }

With this, an observer would subscribe using a method with a Point parameter. It can then
use that parameter in deciding how to respond.
public class SoundEffectManager
{
private void OnShipExploded(Point location) =>
PlaySound("Explosion", CalculateVolume(location));

public SoundEffectManager(Ship ship)


{
ship.ShipExploded += OnShipExploded;
}

public void PlaySound(string name, float volume) { ... }


private float CalculateVolume(Point location) { ... }
// ...
}

Null Events
An event may be null if nothing has subscribed to it. Our earlier examples have ignored this
possibility, which is dangerous. WeWe should either check to see if the event
e vent is null or ensure that
it isn’t ever null by always giving it at least
l east one event handler. The first option is more common
and can be done by simply checking for null before raising the event:
if (Health <= 0)
ShipExploded?.Invoke();

The second option is tricky: ensure the event always has at lea
least
st one handler. WWee rarely know
of a valid handler when the object is created.
c reated. That comes later.
later. If we want to ensure the event
is never null, we’ll need to add a dummy do-nothing handler. You could imagine making a
private DoNothing method within the class, but that’s not very elegant. The more common
alternative is to use a lambda expression—the topic of Level 38. I’ll show you that here, even
though it won’t make sense yet:
public event Action ShipExploded = () => { }; // Uses lambdas from Level 38
38.
.

This initializer ensures ShipExploded will not be null, and we can change the event’s type
from Action? to Action. It comes with a cost: this empty method will run every time the
event is raised.

In my experience,
raising more
the event. But people
this will
second just allowstill
approach thecomes
event to
up.be null and then check for null when
300 LEVEL 37 EVENTS

EVENT LEAK󰁓
As we saw in Level 14, the garbage collector is usually great at cleaning up heap
h eap objects when
they are no longer usable. Any object that is referenced by another will stay alive. That has
consequences for events. The delegate backing an event will hold a reference to any object
subscribed to it. That means an object can survive
sur vive even if the only thing hanging on to it is an
event subscription. Usually, if something is meant to be alive, something besides an event will
also have a alone,
subscription reference to it. an
it is called If an object
event is an
leak or accidentally surviving
event memory leak. because of an event
When an object is at the end of its life, it must unsubscribe
unsubscribe from any events it is subscribed to,to,
or it will not be garbage collected. (At least not until the object with the event dies as well.)
There are a lot of ways to approach this. One way—a rather poor way—is to ignore it. It only
truly matters if you begin running out of memory or if all the excess event handling makes
your program run slow. For tiny,
tiny, short-lived programs,
programs, it may
may not present a big problem.
problem. It is
safer to handle it, but sometimes the cost
c ost of getting it right is not worth the trouble.
A common solution is to make a Cleanup method (or pick your favorite name) that
unsubscribes from any previously subscribed events. When it is time for the object to die, call
the Cleanup method.

A slight variation on that idea is to name that method Dispose and make your object
implement IDisposable. This is a topic covered in a bit more depth in Level
L evel 47. Several C#
mechanisms will automatically call such a Dispose method, but you are still on the hook to
call it yourself in other situations.

EVENTHANDLER AND FRIEND󰁓


Using the various Action delegates with events is common, but another common choice is
EventHandler (System namespace), which is defined approximately
approximately like this:
public delegate void EventHandler(object sender, EventArgs e);

It has two arguments. sender is the source of the event. This parameter makes it easy for
subscribers to hook up their handler to many source objects while still telling which one
raised the event. EventArgs provides additional data about the event. Strictly speaking,
EventArgs does almost nothing. The only thing it defines beyond object is a static
EventArgs.Empty object to be used when there is no meaningful additional data for the
event. However, EventArgs is intended to be used as the base class for more specialized
classes. Classes derived from EventArgs can include other data relevant to an event.
Alternatively, EventHandler<TEventArgs> is a generic delegate that allows you to require
a specific EventArgs-derived class. If you always expect a specific EventArgs-based class,
this will ensure you get the types right.
To use this, start by defining your own class derived from EventArgs. For example:
public class ExplosionEventArgs : EventArgs
{ public Point Location { get; }
public ExplosionEventArgs(Point location) => Location = location;
}

Change your event to use this new class:


c lass:

CUSTOM EVENT ACCESSORS 301


public event EventHandler<ExplosionEventArgs>? ShipExploded;

cur rent object and an appropriate EventArgs object:


Then raise the event with the current
ShipExploded?.Invoke(this, new ExplosionEventArgs(Location));

The observer waiting for the event would


woul d subscribe with a method matching this delegate and
can use both of these arguments to make decisions:
private void OnShipExploded(object sender, ExplosionEventArgs args)
{
if (sender is Ship) PlaySound("Explosion", CalculateVolume(args.Location));
else if (sender is Asteroid) PlaySound("Pop", CalculateVolume(args.Location));
}

Some C# programmers prefer Action. Others prefer EventHandler . Others tend to write
new delegate types and use those. Others mix and match. Any can do the job, so choose the
flavor that works best for your situation.

CU󰁓TOM EVENT ACCE󰁓󰁓OR󰁓


I said earlier that events are like auto-properties
auto-properties around an automatic delegate backing field.
With properties, when you need more control than an auto-property provides,
provides, you can use a
normal property and define your own getter and setter. The same can be done with events,
though it is somewhat rare. You can define what subscribing and unsubscribing mean for any
given event. The simplest version looks like this:
private Action? _shipExploded; // The backing field delegate.

public event Action ShipExploded


{
add { _shipExploded += value; }
remove { _shipExploded -= value; }
}

The add part defines what happens when something subscribes. The remove part defines
what happens
happens when something unsubscribes.
unsubscribes.
The above code does nothing that the automatic event doesn’t do but opens the pathway to
doing other things. For example, you could record when somebody subscribes or
unsubscribes to an event. Or you could take the handler and attach it to several delegates.
With a custom
custom event, you cannot
cannot raise
raise the event directly.
directly. You
You must
must invoke the delegate behind
it instead. The compiler is unwilling to guess how you expect the event to work with a custom
event, so that burden lands on you.

Knowledge Check Events 25 XP


Check your knowledge with the following questions:
1. True/False. Events allow one object to notify another when something occurs.
2. True/False. Any method can be attached to a specific event.
3. True/False. Once attached to an event, a method cannot be detached from an event.
Answers: (1) True. (2) False. (3) False

302 LEVEL 37 EVENTS

Challenge
Challe nge Charb
Charberry
erry Trees 100 XP
The Island of Eventia survives by harvesting and eating the fruit of the native charberry trees. Harvesting
charberry fruit requires three people and an afternoon, but two is enough to feed a family for a week.
Charberry trees fruit randomly, requiring somebody to frequently check in on the plants to notice one
has fruited. Eventia will give you the Medallion of Events if you can help them with two things: (1)
automatically notify people as soon as a tree ripens and (2) automatically harvest the fruit. Their tree
looks like this:
CharberryTree tree = new CharberryTree();

while (true)
tree.MaybeGrow();

public class CharberryTree


{
private Random _random = new Random();
public bool Ripe { get; set; }

public void MaybeGrow()


{
// Only a tiny chance of ripening each time, but we try a lot!
if (_random.NextDouble() < 0.00000001 && !Ripe)
{
Ripe = true;
}
}
}

Objectives:
• Make a new project that includes the above code.
• Add a Ripened event to the CharberryTree class that is raised when the tree ripens.
• Make a Notifier class that knows about the tree ( Hint: perhaps pass it in as a constructor
parameter) and subscribes to its Ripened event. Attach a handler that displays something like “A
charberry fruit has ripened!” to the console window.
• Make a Harvester class that knows about the tree ( Hint: like the notifier, this could be passed as
a constructor parameter) and subscribes to its Ripened event. Attach a handler that sets the tree’s
Ripe property back to false.
• Update your main method to create a tree, notifier, and harvester, and get them to work together
to grow, notify, and harvest forever.
LEVEL
38
LAMBDA EXPRE󰁓󰁓ION󰁓

󰁓peedrun
• Lambda expressions let you define short, unnamed methods using simplified inline syntax: x => x
< 5 is equivalent to bool LessThanFive(int x) => x < 5;
• Multiple and zero parameters are also allowed, but require parentheses: (x, y) => x*x + y*y
and () => Console.WriteLine("Hello,
Console.WriteLine("Hello, World ");
• inferred, but you can explicitly state types: (int x) => x < 5
Types can usually be inferred,
• Lambdas have a statement form if you need more than just an expression: x => { bool
lessThan5 = x < 5; return lessThan5; }
• Lambda expressions can use variables that are in scope at the place where they are defined.

LAMBDA EXPRE󰁓󰁓ION BA󰁓IC󰁓


C# provides a way to define small unnamed methods using a short syntax called a lambda
expression. To illustrate
illustrate where this could
c ould be useful, consider this method to count
c ount the number
of items in an array that meet some condition. The condition is configurable, determined by
a delegate:
public static int Count(int[] input, Func<int, bool> countFunction)
{
int count = 0;

foreach (int number in input)


if (countFunction(number))
count++;

return count;
}

(In Level 42, we will see that all IEnumerable<T> ’s have a Count method like this, so you
do not usually have to write your own.)
We saw similar methods when we first learned about delegates in Level
L evel 36. We know we can
call a method like this by passing in a named method:

304 LEVEL 38 LAMBDA EXPRE


EXPRESSIONS
SSIONS
int count = Count(numbers, IsEven);

But let’s look at that IsEven method:


private static bool IsEven(int number) => number % 2 == 0;

That method is not long, but it has a lot of pomp and formality for a method that may only be
used once. We can alternatively define a lambda expression right in the spot where it is used:
int count = Count(numbers, n => n % 2 == 0);

This lambda expression replaces the definition of IsEven entirely. You can see some
e xpression body. They both use the => operator. This operator
similarities to methods with an expression
is sometimes called the arrow operator or the fat arrow operator but is also frequently called
the lambda operator. (In fact, lambda expressions came before expression-bodied methods!)
Yet, many of the other elements of this definition are gone. No private. No static. No
Yet,
stated return type. No name. No parentheses around the parameters. No type listed for the
parameter.. Plus, we used the variable name n instead of number.
parameter
A lambda expression defines a single-use method inline, right where it is needed. To
To prevent
the code from getting ugly, everything in a lambda uses a minimalistic form:

The accessibility level goes away because you cannot reuse a lambda expression
elsewhere.
• The compiler infers the return type and parameter types from the surrounding context.
Since the countFunction parameter is a Func<int, bool>, it is easy for the
compiler to infer that n must be an int, and the expression must return
return a bool.
• The name is gone because it is a single-use method and does not need to be used again.
• The parentheses are gone just to make the code simpler.
Using the name n instead of number also makes the code c ode shorter.
shorter. Generally, more descriptive
names are better, but C# programmers tend to use concise names in a lambda expression.
When a variable is only used in the following few characters, the downsides of a short name
are not nearly as significant as they are in a 30-line method.

We
andcan do some
delegates. pretty
This coolthe
counts things with
with of
number little code using
positive a combination of lambda expressions
integers: expressions
int positives = Count(numbers, n => n > 0);

This counts positive three-digit integers:


int threeDigitCount = Count(numbers, n => n >= 100 && n < 1000);

Lambda expressions are different enough from normal methods that it may require some time
to adjust. But with a bit of practice, you will find them a simple but powerful tool.

The Origin of the Name mbd


Y
You
ou may be wondering why
why this is called a lambda expression.
expression. The name comes from lambda
calculus. Lambda
programming calculus
language. Theisnature
a type
of of function-oriented
lambda math—almost
expressions and delegates is aheavily
mathematical
inspired
by lambda calculus. In lambda calculus, the name lambda comes from its usage of the Greek
letter lambda (λ).

LAMBDA STA
STATEMENTS
TEMENTS 305

Multiple and Zero Parameters


Our lambda expressions so far have all had a single parameter. Let’s talk about lambda
expressions with zero or many parameters. When you have zero or multiple parameters, the
parentheses come back. A lambda expression with two parameters looks like this:
(a, b) => a + b

A lambda expression
expression with no parameters looks like this:
() => 4

These two cases require parentheses, but parentheses are always an option:
(n) => n % 2 == 0

When Type Inference Fails


Sometimes, the compiler cannot infer the parameter types in a lambda expression. If you
encounter this, you can name the types explicitly, as you might for a normal method:
(int n) => n % 2 == 0

Or:
(string a, string b) => a + b

If the compiler can’t correctly infer the return type of a method, you can write out the return
type before the parentheses that contain the parameters like this:
bool (n) => n % 2 == 0

Or:
bool (int n) => n % 2 == 0

In all of these cases, parentheses are required.

Discards
Lambdas are often used in places where the code demands certain p
parameters
arameters but where you
may not need all of them. If so, you can use discards for those parameters with either of the
following two forms:
(_, _) => 1
(int _, int _) => 1

LAMBDA 󰁓TATEMENT󰁓
Most of the time, when you want a simple single-use method, an expression is all you need,
and lambda expressions are a good fit. You can use a lambda statement in the rare cases where
a statement or several statements are required. Lambda expressions and lambda statements
are both sometimes referred to by the shorter catch-all name lambda.
Making a statement lambda is simple enough. Replace the expression body with a block body:
Count(numbers, n => { return n % 2 == 0; });

306 LEVEL 38 LAMBDA EXPRE


EXPRESSIONS
SSIONS
Or this:
Count(numbers, n => { Console.WriteLine(n); return n % 2 == 0; });

In these cases, both the curly braces and return keyword (if needed) are added back in.
As your statement
statement lambdas grow
grow longer,
longer, you should also
also consider a simple private
private method or
a local function instead. Long lambdas complicate the line of code they live in, and as they get
longer,, they also get more deserving
longer deser ving of a descriptive
descr iptive name.

CLO󰁓URE󰁓
Lambdas and local functions can do something normal methods can’t do. Consider this ccode:
ode:
int threshold = 3;
Count(numbers, x => x < threshold);

The lambda expression has one parameter: x. However, it can use the local variables of the
method that contains it. Here, it uses threshold. Lambda expressions and local functions
can capture variables from their environment. A method plus any captured variables from its
environment is called a closure. The ability to capture variables is a mechanism that gives
lambdas more power than a traditional method.
However,, it is essential to note that this captures the local variables themselves, not just their
However
values. If those variables
variables change over time,
time, you may be surprised byby the behavior:
Action[] actionsToDo = new Action[10];

for (int index = 0; index < 10; index++)


actionsToDo[index] = () => Console.WriteLine(index);

foreach (Action action in actionsToDo)


action();

This stores ten Action delegates, each containing a delegate that refers to a lambda
expression. Each one displays the contents of index. Like declaring any other method, the
act ofthe
until declaring the lambda
foreach expression
loop, where does notexecute.
the delegates run it immediately. In captured
Each delegate this case, the
it isn’t run
index
variable. You might expect this code to display the
the numbers 0 through
through 9. In actuality,
actuality, this code
displays 10 ten times. By the time the lambdas runs, index has been incremented to 10.
Y
You
ou can address
address this by storing the value in a local variable that
that never changes and letting the
lambda capture this other variable instead:
for (int index = 0; index < 10; index++)
{
int temp = index;
actionsToDo[index] = () => Console.WriteLine(temp);
}

Remember that temp’s scope is just within the for loop. Each iteration through the loop will
get its own variable, independent of the other
oth er passes through the loop.
As you can probably guess, the compiler is doing a lot of work behind the scenes to make
captured variables and closures work. The compiler artificially extends the lifetime of those
temp variables to allow them to stay around until the capturing delegate
del egate is cleaned up.

CLOSURES 307
Y
You
ou can also capture variables and use closures with local functions. And remember,
remember, the
methods you define with top-level statements, outside of any type, are local functions, which
means such methods could technically use the variables in your main method.
While closures are
are very powerful, be careful about capturing variab
variables
les that change over time.
It almost always results in behavior you didn’t intend. To prevent a lambda or local function
from accidentally capturing local variables, you can add the static keyword to them, which
causes any captured variables to become a compiler error:
er ror:
Count(new int[] { 1, 2, 3 }, static n => { return n % 2 == 0; });

Knowledge Check Lambdas 25 XP


Check your knowledge with the following questions:
1. True/False. Lambda expressions are a special type of method.
2. True/False. You can name a lambda expression.
3. following to a lambda: bool IsNegative(int x) { return x < 0; }
Convert the following
4. True/False. Lambda expressions can only have one parameter.
Answers: (1) True. (2) False. (3) x => x < 0. (4) False.

Challenge The Lambda 󰁓ieve 50 XP


The city of Lambdan, also on the Island of Delgata, believes that the great Numeromechanical Sieve,
which you worked on in Level 36, could be made better by using lambda expressions instead of regular,
named methods. If you can help them convince island leadership to make this change, they will give you
the Lambda Medallion and pledge the Lambdani Fleet’s assistance in the coming final battle.
Objectives:
• Modify your The Sieve program from Level 36 to use lambda expressions for the constructor instead
of named methods for each of the three filters.
• Answer this question: Does this change make the program shorter or longer?
• Answer this question: Does this change make the program easier to read or harder?
LEVEL 39
FILE󰁓

󰁓peedrun
• File-related types all live in the System.IO namespace.
File-related n amespace.
• File lets you read and write files: string[] lines = File.ReadAllLines(
File.ReadAllLines( "file.txt");
File.WriteAllText("file.txt",
File.WriteAllTe xt("file.txt", "contents");
• File does manipulation (create, delete, move, files); Directory does the same with directories.
directories.
• Path helps you combine parts of a file path or extract interesting elements out of it. The File class
is a vital part of any file I/O.
• You can also use streams to read and write files a little at a time.
• Many file formats have a library you can reuse, so you do not have to do a lot of parsing yourself.

Many programs benefit from saving information in a file and later retrieving it. For example,
you might want to save
save settings for a program into a configuration file. Or maybe
maybe you want to
save high scores to a file so that the player’s previous scores rremain
emain when you close and reopen
the game.
The Base Class Library contains several classes that make this easy. We will look at how to read
and write data to a file in this level.
All of the classes we discuss in this level live in the System.IO namespace. This namespace
is automatically included in modern C# projects, but if you’re using older code, you will need
to use fully qualified names or add a using System.I
System.IO;O; directive (Level 33).
33).

THE FILE CLA󰁓󰁓


The File class is the key class for working with files. It allows us to get information about a
file and read and write illustrate how the File class works, let’s look at a small
wr ite its contents. To illustrate
Message in a Bottle program, which asks the user for a message to display the next time the
program runs. That message
message is placed in a file. When the program starts,
starts, it shows the message
from before, if it can find one.
We can
can start by getting the text from the user.
user. This uses only things familiar to us:

THE FILE CLASS 309


Console.Write("What do you want me to tell you next time? ");
string? message = Console.ReadLine();

The File class is static and thus contains only static methods. WriteAllText will take a
string and write it to a file. You
You supply the path to the destination file, as well as the text itself:
itself :
Console.Write("What do you want me to tell you next time? ");
string? message = Console.ReadLine();
File.WriteAllText("Message.txt", message);
This alone creates a functioning program, even though it does not do everything we set out to
do. If we run it, our program asks for text, makes a file called Message.txt, and places the user’s
text in it.
Where exactly does that file get created? WriteAllText —and every method in the File
class that asks for a path—can work with both absolute and relative paths. An absolute path
describes the whole directory structure from the root to the file. For example, I could do this
to write to a file on my desktop:
File.WriteAllText("C:/Users/RB/Desktop/Message.txt", message);

A relative path leaves off most of the path and lets you describe the part beyond the current
(You can also use “ ..” in a path to go up a directory from the current one
working directory. (You
in a relative path.) When your C# program runs in Visual Studio
Studio,, the current working directory
is in the same location as your compiled code. For example, it might be under your project
folder under \bin\Debug\net6.0\ or something similar.
If you hunt down this file, you can open it up in Notepad or another program and see that it
created the file and added your text to it.
We wanted
wanted to open this file and display
display the last message,
message, so let’
let’ss do that with the following:
following:
string previous = File.ReadAllText("Message.txt");
Console.WriteLine("Last time, you said this: " + previous);

Console.Write("What do you want me to tell you next time? ");


string? message = Console.ReadLine();
File.WriteAllText("Message.txt", message);

ReadAllText opens the named file and reads the text it contains, returning a string. The
code above then displays that in the console window.
There is one problem with the code above. If we run it this way and the Message.txt file does
not exist, it will crash. We
We can check to see if a file exists before trying to open it:
it :
if (File.Exists("Message.txt"))
{
string previous = File.ReadAllText("Message.txt");
Console.WriteLine("Last time, you said this: " + previous);
}

That creates a more robust


robust program that works even if the file does not exist yet.
310 LEVEL 39 FILES

󰁓TRING MANIPULATION
ReadAllText and WriteAllText are simple but powerful. You can save almost any data
to a file and pull it out later with those two methods alone. You just need a way to turn what
you want into a string and then parse the string
string to get your data back
back..
Let’s look at a more complex problem: saving a collection of scores. Suppose we have this
record:
public record Score(string Name, int Points, int Level);

And this method for creating


creating an initial list of scores:
scores:
List<Score> MakeDefaultScores()
{
return new List<Score>()
{
new Score("R2-D2", 12420, 15),
new Score("C-3PO", 8543, 9),
new Score("GONK", -1, 1)
};
}

After calling thisneeds


WriteAllText method to get
a string, our
and wescores, how would wecontaining
have a List<Score> write all this
manydata to a file?
scores.
We need a way to transform a complex object or a complex set of objects into something that
that
can be placed into a file. This transformation is called serialization. The reverse is called
deserialization. If we can serialize our scores into a string, we already know the rest.
There is no shortage of ways to serialize these scores. Here is a simple way: the CSV format.
CSV, short for “comma-separated values,” is a simple format that puts each item on its own
line. Commas separate the item’s
item’s properties. In a CSV
C SV file, our scores
sc ores might look like this:
this :
R2-D2,12420,15
C-3PO,8543,9
GONK,-1,1

File
stringshas a WriteAllLines
instead of just one. If wemethod
can turnthat
eachmay simplify
score our work.
into a string, It requires
we can a collection of
use WriteAllLines
to get them into a file:
void SaveScores(List<Score> scores)
{
List<string> scoreStrings = new List<string>();

foreach (Score score in scores)


scoreStrings.Add($"{score.Name},{score.Points},{score.Level}");

File.WriteAllLines("Scores.csv", scoreStrings);
}

The line inside the foreach loop combines the name, score, and level into a single string,
separated by commas. We
We do that for each score and end up with one string per score.
File.WriteAllLines can take it from there, so we hand it the file name and string
collection, and the job is done.

STRING MANIPULATION 311


Deserializing this file back to a list of scores is harder. There is a File.ReadAllLines
method that is a good starting point. It returns a string[] where each string was one line in
the file.
string[] scoreStrings = File.ReadAllLines("Scores.csv");

We need to take reconstitute a Score object. Since we separated


take each string and chop it up to reconstitute

data
its elements with commas, we can use string’s Split method to chop up the lines into
parts:
string scoreString = "R2-D2,12420,15";
string[] tokens = scoreString.Split(",");

Split(",") gives us an array of strings where the first item is "R2-D2", the second item is
"12420", and the third item is "15". If we used a ; or | to separate values, we could have
passed in a different argument to the Split method. Note that the delimiter—the character
that marks the separation point between elements—is not kept when you use Split in the
way shown above, but overloads of Split that allow that to happen.
but there are overloads
My variable is called tokens because that is a common word for a chopped-up string’s most
fundamental elements.

With those elements, we can creat


createe this method to load all the scores
scores in the file:
List<Score> LoadHighScores()
{
string[] scoreStrings = File.ReadAllLines("Scores.csv");

List<Score> scores = new List<Score>();

foreach (string scoreString in scoreStrings)


{
string[] tokens = scoreString.Split(",");
Score score = new Score(tokens[0],
Convert.ToInt32(tokens[1]),
Convert.ToInt32(tokens[2]));
scores.Add(score);

}
return scores;
}

I should mention that the code above works most of the time but could be more robust. For
example, imagine that a user enters their name as "Bond, James". Strings can contain
commas, but in our CSV file, the resulting line is "Bond, James,2000,16". Our
deserialization code will end up with four tokens and try to use "Bond" as the name and
" James" as the score, which fails. We could forbid commas in player names or
automatically turn commas into something else. We could also reduce the likelihood of a
problem by picking a more obscure delimiter, such as ¤. Few keyboard layouts can easily type
that, but it is not impossible. (The official CSV format
for mat lets you put double-quote marks around
strings that contain commas.
c ommas. This addresses the issue, but parsing that is trickier.)
trickier.)
Other 󰁓tring Parsing Methods
File.ReadAllLines and string.Split are enough for the above problem, but there
there are
other string methods that you might find helpful in similar situations.

312 LEVEL 39 FILES


The Trim, TrimStart, and TrimEnd methods allow you to slice off unnecessary characters
at the beginning and end of strings. The string " Hello" has an undesirable space character
before it. " Hello".Trim() will produce a string without the space. It removes all
whitespace from word. TrimStart and TrimEnd only trim the
from the beginning and end of the word.
named side. If you want to remove another character, you can use "$Hello".Trim('$') .
Remember that these produce new strings with the requested modification. They do not

change the original string. Strings are immutable.


The Replace method lets you find a bit of textt ext within another and replace it with something
com mas in a name to the ¤ character
else. For example, if we want to turn all commas character,, we could
coul d do this:
name = name.Replace(",", "¤");. "Bond, James" becomes " ond¤ James",
which our parsing
parsing code can safely handle.
The Join method combines
c ombines multiple items separated by some special string or character. We
could have used this when converting Score objects to strings: string.Join("|",
score.Name, score.Points, score.Level);. This method uses the params
keyword for its second argument.

FILE 󰁓Y󰁓TEM MANIPULATION


Aside from reading and writing files, the File, Path, and Directory class has a handful of
other methods for doing file system manipulation. Let’s look at those.
File has methods for copying files, moving files, and deleting files. These are all pretty self-
explanatory:
File.Copy("Scores.csv", "Scores-Backup.csv");
File.Move("Scores.csv", "Backup/Scores.csv");
File.Delete("Scores.csv");

The Directory Class


What File does for files, Directory does for directories. (The words directory and folder are
synonyms.) For example, these methods move, create, and delete a directory:
Directory.Move("Settings", "BackupSettings");
Directory.CreateDirectory("Settings2");
Directory.Delete("Settings2");

Delete requires that the directory be empty before deleting it. Otherwise, it results in an
exception (System.IO.IOException ). You could write code to remove every file in a
directory yourself, but there is also an overload that allows you to force the deletion of
everything inside it:
Directory.Delete("Settings2", true); // Careful!

This can be extremely dangerous. You can delete entire file systems instantly with a poorly
written Directory.Delete . Use it with extreme
e xtreme caution!

also has several methods for exploring the contents of a directory. The names of
Directory also has several methods for exploring the contents of a directory. The names of
these methods depend on whether you want results in a string[] (names start with Get) or
an IEnumerable<string> (names start with Enumerate). The names also depend on
whether you want files (names end with Files), subdirectories (names end with

OTHER WAYS TO ACCESS FILES 313


Directories), or both (names end with FileSystemEntries). Two examples are shown
below:
foreach (string directory in Directory.GetDirectories("Settings"))
Console.WriteLine(directory);
foreach (string file in Directory.EnumerateFiles("Settings"))
Console.WriteLine(file);

Some overloads allow you to supply a filter, enabling things like finding all files with an
extension of .txt.

The Path Class


The static Path class has methods for working with file system paths, including combining
paths, grabbing just the file name or extension from a file, and converting between absolute
and relative paths. The code below illustrates all of these:
Console.WriteLine(Path.Combine("C:/Users/RB/Desktop/", "Settings", "v2.2"));
Console.WriteLine(Path.GetFileName("C:/Users/RB/Desktop/GrumpyCat.gif"));
Console.WriteLine(Path.GetFileNameWithoutExtension(
"C:/Users/RB/Desktop/GrumpyCat.gif"));
Console.WriteLine(Path.GetExtension("C:/Users/RB/Desktop/GrumpyCat.gif"));
Console.WriteLine(Path.GetFullPath("ConsoleApp1.exe"));
Console.WriteLine(Path.GetRelativePath(".", "C:/Users/RB/Desktop"));

When I run these on my computer,


computer, I get the following output:
C:\Users\RB\Desktop\Settings/v2.2
GrumpyCat.gif
GrumpyCat
.gif
C:\Users\RB\source\repos\ConsoleApp1\ConsoleApp1\bin\Debug\net6.0\ConsoleApp1.exe
..\..\..\..\..\..\..\Desktop

There’s More!
This is a whirlwind tour of File, Directory, and Path. Each has far more capabilities than
we covered here, but this should give you a starting point. When you are ready, look up the
documentation online or in Visual Studio’s IntelliSense feature to poke around at what else
these contain.

OTHER WAY󰁓 TO ACCE󰁓󰁓 FILE󰁓


The basic ReadAllText , WriteAllText, ReadAllLines , and WriteAllLines methods
are a good foundation—quick and easy, without having to think too hard. But they are not the
only option. Two other approaches are worth a brief discussion: streams and using a library.

󰁓treams
The
doneabove
a littlemethods
at a time.require readinglet
For example, orswriting
say youthe file all at once.
re extracting Some
millions of operations are better
database entries into
a CSV file. With WriteAllText, you would need to bring br ing the entire dataset into memory all
at once and turn it into an extremely long string to feed to WriteAllText. That will use a lot
of memory and make the garbage collector
collec tor work extremely harhard.
d. A better approach would be

314 LEVEL 39 FILES


to grab a chunk of the data and write it to the file before continuing to the next chunk. But that
requires a different approach.
We can solve this problem
problem with streams. A stream is a collection
coll ection of data that you typically work
with a little at a time. Streams do not usually allow jumping around in the stream. They are
like a conveyor belt that lets you look at each item as it goes by.
There are many different flavors of streams
streams in the .NET world, and all of them are derived from
the System.IO.Stream class. The flavor we care about here is FileStream, which reads
or writes data to a file. Other stream types work with memory, the network, etc.
Streams are very low level. You can read and write bytes, and that’s it. Most of the time, you
want something smarter
smarter when working with a stream.
stream. This limitation
limitation is usually addressed by
by
using another object that “wraps” the stream and provides you with a more sophisticated
interface. The wrapper translates your requests to the lower level
l evel that streams require.
For example, the File class can give you a FileStream object to read or write to a file. We
can then wrap a StreamReader around that to give us a better set of methods to work with
than what a plain Stream or FileStream provides:
FileStream stream = File.Open("Settings.txt", FileMode.Open);
StreamReader reader = new StreamReader(stream);
while (!reader.EndOfStream)
Console.WriteLine(reader.ReadLine());
reader.Close();

For writing, StreamWriter is your friend:


FileStream stream = File.Open("Settings.txt", FileMode.Create);
StreamWriter writer = new StreamWriter(stream);
writer.Write("IsFullScreen=");
writer.WriteLine(true);
writer.Close();

Note the file mode supplied as the second parameter on each of those File.Open calls.
StreamWriter ’s Write and WriteLine methods are almost like Console’s.
With this approach, our reading and writing do not need to happen all at once. We can read
and write in small chunks over time, which is the main reason for using streams over the
simpler WriteAllLines and ReadAllLines . Additionally, we can pass the Stream
Writer or StreamReader (or just the raw stream) to other methods or objects. This ability
lets you break complex serialization and deserialization in whatever way your design needs.
The BinaryReader and BinaryWriter classes are similar but use binary representations
instead of text. Binary formats are typically much more compact but are also not easy for a
human to open and read. For example, you could use writer.Write(1001) , which writes
the int value 1001 into 4 bytes in binary, then use reader.ReadInt32() , which assumes
the next four bytes are an int and decodes them as such.
Working with st
Working reams is far trickier than File.ReadAllText -type methods. For example, it
streams
is easy to accidentally leave a file open or close it too ea
early.
rly. (Notably, all of these stream-related
objects implement
Level 47.)
47. IDisposable
) I recommend , and
using the shouldfile
simpler be methods
disposed of when
when done, astodescribed
practical in
avoid this
complexity, especially if you are new to programming.

OTHER WAYS TO ACCESS FILES 315

Find a Library
One big problem with everything we have talked about so far is writing all of the serialization
and deserialization code. That can be tough to get right. Even something as simple as the CSV
format has tricky corner cases. While you can always work through such details, finding
somebody else’s
else’s code that already solves the problem is often easier.
easier.
When possible, pick a widely used file format instead of inventing your own. With common
file formats, it is easy to find existing code that does the serialization for you (or at least the
heavy lifting). There are libraries—reusable
libraries—reusable packages of code—out there for standard formats
like XML, JSON, and YAML. Using these libraries means you do not have to figure out all the
details yourself. Level 48 has more information on libraries.
Before writing voluminous, complex serialization code, consider if an existing format and
library can make your life easier.
easier.

Challenge The Long Game 100 XP


The island of Io has a long-running tradition that was destroyed when the Uncoded One arrived. The
inhabitants
inhabitants of Io would compete over a long period of time to see who could press the
t he most keys on the
keyboard. But the Uncoded One’s arrival destroyed the island’s ability to use the Medallion of Files, and
the historic competitions spanning days, weeks, and months have become impossible. As a True
Programmer, you can use the Medallion of Files to bring back these long-running games to the island.
Objectives:
• When the program starts, ask the user to enter their name.
• By default, the player starts with a score of 0.
• Add 1 point to their score for every keypress they make.
• Display the player’s updated score after each keypress.
• When the player presses the Enter key, end the game. Hint: the following code reads a keypress and
checks whether it was the Enter key: Console.ReadKe
Console.ReadKey().Key
y().Key == ConsoleKey.Enter
• When the player presses Enter, save their score in a file. Hint: Consider saving this to a file named
[username].txt. For this challenge, you can assume the user doesn’t enter a name that would produce
an illegal file name (such as *).
• When a user enters a name at the start, if they already have a previously saved score, start with that
score instead.
EVEL
LPATTERN 40
MATCHING

󰁓peedrun
• Pattern matching categorizes data into one or more categories based on its type, properties, etc.
• Switch expressions, switch statements, and the is keyword all use pattern matching.
• Constant pattern: matches if the value equals some constant: 1 or null
• anything: _
Discard pattern: matches anything:
• Declaration pattern: matches based on type: Snake s or Monster m
• Property pattern: checks an object’s properties: Dragon { LifePhase: LifePhase.Ancient
}
• Relational patterns: >= 3, < 100
• and, or, and not patterns: LifePhase.Ancient or LifePhase.Adult, not
LifePhase.Wyrmling
• var pattern: matches anything but also puts the result into a variable:
variable: var x
• Positional pattern: used for multiple elements, tuples, or things with a Deconstruct method to
provide sub-patterns for each of the elements: (Choice.Roc
(Choice.Rock, k, Choice.Scissors)
Choice.Scissors)
• Switches also have case guards, using the when keyword: Snake s when s.Length > 2

Programming is full of categorization problems, where you must decide which of several
categories an object fits in based on its type, properties, etc.
• Is today a weekend or a weekday?
• In a game where you fight monsters, how many experience points should the player
receive after defeating it?
• In the game of Rock-Paper-
Rock-Paper-Scissors,
Scissors, given the player’s
player’s choices, which player won?
You can solve these problems with the venerable if statement, but C# provides another tool
You
designed specifically for these situations: pattern matching. Pattern matching lets you define
categorization rules to determine which category an object fits in. You can use pattern
matching in switch expressions, switch statements, and the is keyword.

THE CONSTANT PA
PATTERN
TTERN AND THE DISCARD PA
PATTERN
TTERN 317

THE CON󰁓TANT PATTERN AND THE DI󰁓CARD PATTERN


We got our first glimpse of patterns in Level 10 when we made our pirate-themed menu:
string response = choice switch
{
1 => "Ye rest and recover your health.",
2 => "Raiding the port town get ye 50 gold doubloons.",
3 => "The wind is at your back; the open horizon ahead.",
4 => "'Tis but a baby Kraken, but still eats toy boats.",
_ => "Apologies. I do not know that one."
};

This level was our introduction to patterns, though we didn’t know it at the time.
In a switch expression, each arm is defined by a pattern on the left, followed by the =>
operator, followed by the expression to evaluate if the pattern is a match (pattern expression
e xpression
=> evaluation expression). Each pattern is a rule that determines if the object under
consideration fits into the category or not. The switch expression above uses the two most
basic patterns: the constant pattern and the discard pattern.
The first four lines show the constant pattern, which decides if there is a match based on
equals some constant value, like the literals 1, 2, 3, or 4.
whether the item exactly equals
The last switch arm uses the discard pattern: _. This pattern is a catch-all pattern, matching
anything and everything. In C#, a single underscore usually represents a discard, signifying
that what goes in that spot does not matter.
matter. Here, it indicates that there is nothing to check—
that there are no constraints or rules for matching the pattern. Because it matches anything,
when it shows up,
up, it should always
always be the very last
last pattern.
But these two patterns are only the beginning.

THE MON󰁓TER 󰁓CORING PROBLEM


Having a realistic and complex problem can help illustrate the different patterns we will be
learning. In this level, we will use the following problem: in a game where the player fights
monsters, given some Monster instance, determine how many points to award the player for
defeating it. In code, we might write this problem like so:
int ScoreFor(Monster monster)
{
// ...
}

Let’s also define what a Monster is:


public abstract record Monster;

Monsters in a real game would likely have more than that, but it’
it’ss all we need
n eed right now. Other
monster types are derived from Monster:
public record Skeleton() : Monster;
A more complex subtype
subtype might add additional
additional properties:
public record Snake(double Length) : Monster;

318 LEVEL 40 PATTERN


PATTERN MAT
MATCHING
CHING
Anacondas are more challenging to defeat than mere garter snakes; the player deserves a
larger reward for defeating them.
Here is a Dragon type that builds on two enumerations:
en umerations:
public record Dragon(DragonType Type, LifePhase LifePhase) : Monster;
public enum DragonType { Black, Green, Red, Blue, Gold }
public enum LifePhase { Wyrmling, Young, Adult, Ancient }

Each dragon has a type and a life phase. Different types of dragons and different life phases
make for more formidable challenges worth more points.
And here is an orc
orc with a sword that
that has properties of its own:
public record Orc(Sword Sword) : Monster;
public record Sword(SwordType Type);
public enum SwordType { WoodenStick, ArmingSword, Longsword }

The sword has a type: a longsword, an arming sword, or a wooden stick. It may be a stretch to
call a WoodenStick a sword, but it is always worth compromising the design for stupid
humor! (Please don’t quote me on that.)
We could make
make more, but this is enough to make meaningful patterns.
patterns.

THE TYPE AND DECLARATION PATTERN󰁓


The type pattern matches anything of a specific type. For example, the following code uses the
type pattern to look for the Snake and Dragon types:
int ScoreFor(Monster monster)
{
return monster switch
{
Snake => 7,
Dragon => 50,
_ => 5
};
}
Snake => 7 and Dragon => 50 are both type patterns. (The last is another discard pattern.)
If the monster is the named type, it will be a match. So this code will return 7 for snakes, 5
500 for
dragons, and 5 otherwise. This pattern is a match even for derived types. A pattern like
Monster => 2 would match every ever y kind of monster,
monster, regardless of its specific subtype.
The declaration pattern is similar but additionally gives you a variable that you can use in the
body afterward. So we could change
c hange this so that longer snakes are worth more points:
int ScoreFor(Monster monster)
{
return monster switch
{
Snake s => (int)(s.Length * 2),
Dragon
_ =>
=> 50,
5
};
}

CASE GUARDS 319


I also changed the whitespace to make all of the => elements line up. This spacing is a
common practice to increase the readability of the code.
c ode. It puts it into a table-like format.

CA󰁓E GUARD󰁓
Switches have a feature called a guard expression
expression or a case guard. These allow you to supply a
second expression that must be evaluated before deciding if a specific arm matches. We can
use this to have our snake rule apply only to long snakes:
int ScoreFor(Monster monster)
{
return monster switch
{
Snake s when s.Length >= 3 => 7,
Dragon => 50,
_ => 5
};
}

A snake with a length of 4 will match both expressions on the first arm. A snake with a length
of 2 only matches the pattern but not the guard, so the first arm will not be picked.
Once we have a guard expression, it can make sense to have multiple declaration patterns for
the same type. The following gives 7 points for long snakes and 3 for others.
int ScoreFor(Monster monster)
{
return monster switch
{
Snake s when s.Length >= 3 => 7,
Snake => 3,
Dragon => 50,
_ => 5
};
}

Order matters. If you reverse the top two lines, the length-based pattern would never get a
chance to match. If the compiler detects this, it will create a compiler error to flag it.
Y
You
ou can use case guards
guards with any pattern.
pattern.

THE PROPERTY PATTERN


The property pattern lets you define a pattern based on an object’s properties. For example,
ancient dragons should be worth far more than other life phases. We can show that with the
pattern below:
int ScoreFor(Monster monster)
{
return monster switch
{ Snake s when s.Length >= 3 => 7,
Dragon { LifePhase: LifePhase.Ancient } => 100,
Dragon => 50,
_ => 5

320 LEVEL 40 PATTERN


PATTERN MAT
MATCHING
CHING
};
}

Y
You
ou can list multiple
multiple properties, separating
separating them with commas:
Dragon { LifePhase: LifePhase.Ancient, Type: DragonType.Red } => 110,

The property pattern only matches if the type is correct,


cor rect, and each property is also a match.
Y
You
ou can also list a variable
variable name for the matched object after the curly braces:
Dragon { LifePhase: LifePhase.Ancient } d => 100,

c ould use the variable d in the expression after the =>.


If you had a need, you could
If you don’t want to demand a specific subtype, you can leave the type off in a property pattern:
p attern:
{ SomeProperty: SomeValue } => 2. The monster class has no properties, so it isn’t
helpful in this specific situation. But it is useful in other circumstances.

Nested Patterns
Some patterns allow you to use smaller sub-patterns within them. This is called a nested
pattern. Each property in a property pattern is a nested pattern. The code
c ode above uses a nested
constant pattern (LifePhase.Ancient), but we could have used any other pattern. To
illustrate, here are
are some nested patterns for orcs with swords of different types:
Orc { Sword: { Type: SwordType.Longswo
SwordType.Longsword
rd } } => 15,
Orc { Sword: { Type: SwordType.ArmingSword } } => 8,
Orc { Sword: { Type: SwordType.WoodenStick } } => 2,

The highlighted code above is a property pattern inside another property pattern. But the
inner pattern is not just limited to property patterns. It can be anything.
For nested property patterns, there’s
there’s also a convenient shortcut:
Orc { Sword.Type: SwordType.Longswor
SwordType.Longswordd } => 15,
Orc { Sword.Type: SwordType.ArmingSword } => 8,
Orc { Sword.Type: SwordType.WoodenStick } => 2,

Nested patterns give you lots of flexibility but also begin to complicate code. It is important to
be conscientious of the complexity of these patterns. You will inevitably be back and modify
them again, and you will need to remember what they do do..

RELATIONAL PATTERN󰁓
We used a case guard for our snake pattern earlier,
earlier, but an alternative would have been a
relational pattern. These use >, <, >=, and <= to match a range of values. Now that we know
the property pattern, we can replace our switch guard with the following:
Snake { Length: >= 3 } => 7,
The >= 3without
top level part is aanything
relational pattern.
else It happens
if our switch wereto int instead
forbeannested here, but Monster
of awe . >it, <at, and
could use the
<= all work in the same way. We will see another example in a moment.

THE AND, OR, AND NOT PA


PATTERNS
TTERNS 321

THE AND, OR, AND NOT PATTERN󰁓


If you need a pattern that combines multiple sub-patterns, you can use and and or, which
work like the && and || operators do in Boolean
Bool ean logic. For example, if we want to give bonus
points for dragons that are either ancient or adult, we use or in a nested property
p roperty pattern:
Dragon { LifePhase: LifePhase.Adult or LifePhase.Ancient } => 100,

This saves us from needing to write out two entirely different switch arms.
Suppose we want to give short snakes (length under 2) 1 point, medium snakes (between 2
and 5) 3 points, and long snakes
sn akes (longer than 5) 7 points. We
We could do this:
Snake { Length: < 2 } => 1,
Snake { Length: >= 2 and <= 5 } => 3,
Snake { Length: > 5 } => 7,

The not pattern negates the pattern after it. The following matches any non-wyrmling dragon:
Dragon { LifePhase: not LifePhase.Wyrmling } => 50,

THE PO󰁓ITIONAL PATTERN


The positional pattern is useful when making decisions based on more than one value. Our
monster scoring problem only involves a single object, so let’s change to a different problem:
deciding who won a game
g ame of Rock-Paper-
Rock-Paper-Scissors.
Scissors.
Let’s say we have the following two enumerations, one that represents a player’s selection and
one that represents the outcome of a game:
public enum Choice { Rock, Paper, Scissors }
public enum Player { None, One, Two }

We want to make a DetermineWinner method that tells us which player won when given
the players’ choices.
With the positional
positional pattern, we can switch
switch on multiple items:
Player DetermineWinner(Choice player1, Choice player2)
{
return (player1, player2) switch
{
(Choice.Rock, Choice.Scissors) => Player.One,
(Choice.Paper, Choice.Rock) => Player.One,
(Choice.Scissors, Choice.Paper) => Player.One,
(Choice a, Choice b) when a == b => Player.None,
_ => Player.Two
};
}

This code combines the two items at the switch’s


switch’s start, allowing patterns to use both elements.
All but the last of these is a positional pattern. It lists sub-patterns
sub-patterns for each piece. For the
overall pattern to match, each sub-pattern must match its corresponding part. Like with the
property pattern, these sub-patterns can be any other pattern necessary. The above uses
constant patterns in the first three lines and the declaration pattern in the fourth.

322 LEVEL 40 PATTERN


PATTERN MAT
MATCHING
CHING

Deconstructors and the Positional Pattern


The positional pattern works when you lump two or more items together in parentheses. It
also works on a single thing if it has a deconstructor (Level
(L evel 34).
34). The deconstructor will be used
to extract the object’s parts
parts and attempt to match the pieces with the corresponding elements
of the positional pattern.
If a type has a deconstructor
decon structor,, you can optionally prefix a type, suffix a variable name, or both:
Dragon (DragonType.Blue, LifePhase.Wyrmling) d => 100,

THE VAR PATTERN


The var pattern is somewhere between the declaration pattern (like Choice a) and the
discard pattern. It does not check for a specific type, but does give you access to a new variable.
Earlier,, we did this:
Earlier this :
(Choice a, Choice b) when a == b => Player.None,

have used the var pattern since the type Choice was already known:
We could have
(var a, var b) when a == b => Player.None,

The var pattern matches any type, but the variable it declares is useable in both the guard
expression and the expression on the right of the =>.

PARENTHE󰁓IZED PATTERN󰁓
When your patterns start to get complex (especially many and and or patterns),
(especially when you use many
you can place parts of a pattern in parentheses to group things and enforce the order.
order. The
following is not very practical, but illustrates the point:
point :
Snake { Length: (>2 and <5) or (>100 and <1000) } => 20,

PATTERN󰁓 WITH 󰁓WITCH 󰁓TATEMENT󰁓 AND THE IS KEYWORD


Switch expressions are the most common way to use patterns, but you can also use
u se them in a
switch statement and with the is keyword.

󰁓witch 󰁓tatements
Here is a version of DetermineWinner that uses a switch statement instead of a switch
expression:
Player DetermineWinner(Choice player1Choice, Choice player2Choice)
{
switch (player1Choice, player2Choice)
{
case (Choice.Rock, Choice.Scissors):
case (Choice.Paper, Choice.Rock):
case (Choice.Scissors, Choice.Paper):
return Player.One;
case (Choice a, Choice b) when a == b:
return Player.None;
default:

SUMMARY 323
return Player.Two;
}
}

A switch expression that uses patterns is usually cleaner than its switch statement
counterpart. But sometimes, the statement-based nature is needed or desirable.
Switch statements allow you to stack multiple patterns for a single arm, as shown above for
the first three patterns. If you declare new variables while doing this (the var or declaration
patterns), their names can sometimes cause conflicts with each other.
other.

The is Keyword
Switches let you put an item into one of several rule-based categories. The is keyword
enables you to check if something is in a single category or not. Here is a simple example:
void TellUserAboutMonster(Monster monster)
{
Console.WriteLine("There's a monster!");
if (monster is Snake)
Console.WriteLine("Why did it have to be snakes?");
}

commonl y combined with is.


We are not just limited to the declaration pattern, though it is commonly
Any of the patterns are
are available
available to us.
The is keyword cannot use guard expressions, but we can always extend it with an &&.

󰁓UMMARY
The following table summarizes the different patterns available in C#:
324 LEVEL 40 PATTERN
PATTERN MAT
MATCHING
CHING
Pattern Name Description Examples
constant
pattern
matches a specific constant value 3 or null

discard pattern matches anything (a catch-all) _


var pattern matches anything and gives it a new name var x
type pattern matches if the object is the type listed at run time string
declaration
pattern
matches if the object is the type listed at run time and
gives you a new variable
string s

property matches if the properties listed match the specified sub- { LifePhase:
pattern patterns LifePhase.Wyrmling
LifePhase.Wyrmling }
relational matches if the object is >, <, >=, or <= the value
pattern
>10, <= 1000
provided
and pattern matches if both sub-patterns are a match >1 and <10
or pattern matches if either (or both) sub-patterns are a match <1 or >10
not pattern matches if the sub-pattern does not not null
positional matches if each element
ele ment in a tuple/deconstructor match
pattern their listed sub-patterns
(Choice.Rock, Choice.Scissors)

Challenge The Potion Masters of Pattren 150 XP


The island of Pattren is home to skilled
skill ed potion masters in need of some help. Potions are mixed by adding
one ingredient at a time until they produce a valuable potion type. The potion masters will give you the
Patterned Medallion if you help them make a program to build potions according to the rules below:
• All potions start as water.
• Adding stardust to water turns it into an elixir.
• Adding snake venom to an elixir turns it into a poison potion.
• Adding dragon breath to an elixir turns it into a flying potion.
• Adding shadow glass to an elixir turns it into an invisibility potion.
• Adding an eyeshine gem to an elixir turns it into a night sight potion.
• Adding shadow glass to a night sight potion turns it into a cloudy brew.

Adding an eyeshine gem to an invisibility potion turns it into a cloudy brew.
• Adding stardust to a cloudy brew turns it into a wraith potion.
• Anything else results in a ruined potion.
Objectives:
• Create enumerations for the potion and ingredient types.
• Tell the user what type of potion they currently have and what ingredient choices are available.
• Allow them to enter an ingredient choice. Use a pattern to turn the user’s response into an
ingredient.
• Change the current potion type according to the rules above using a pattern.
• Allow them to choose whether to complete the potion or continue before adding an ingredient. If
the user decides to complete the potion, end the program.
• When the user creates a ruined potion, tell them and start over with water.

L EVEL
41
OPERATOR OVERLOADING

󰁓peedrun
• Operator overloading lets you define how certain operators work for types you make: +, -, *, /, %,
++, --, ==, =, >=, <=, >, <. For example: public static Point operator +(Point p1, Point
p2) => new Point(p1.X + p2.X, p1.Y + p2.Y);
• All operators must be public and static.
• Indexers let you define how the indexing operator works for your type with property-like syntax:
public double this[int index] { get => items[index]; set => items[index] =
value; }
• Custom conversions allow the system to cast to or from your type: public static implicit
operator Point3(Point2 p) => new Point3(p.X, p.Y, 0);
• Custom conversions can be implicit or explicit. Use implicit when no data is lost; use
explicit when data is lost.

The
withbuilt-in
int, youtypes have
can do some features
addition with thethat our types have been missing so far. For example,
+ operator:
int a = 2;
int b = 3;
int c = a + b;

With arrays,
arrays, lists, and
and dictionaries, you can
can use the indexing operator:
int[] numbers = new int[] { 1, 2, 3 };
numbers[1] = 88;
Console.WriteLine(numbers[1]);

And you can cast


cast from certain types to others. casts a char to a short:
others. This code casts
char theLetterA = 'a';
short theNumberA = (short)theLetterA;
Y
You
ou can
can define how operators, indexers, and casting
casting conversions
conversions work
work for the types you create.
That is the topic of this level.

326 LEVEL 41 OPERATOR OVERLOADING

OPERATOR OVERLOADING
We have encountered many different operators in our journey. You can define how somesom e of
these operators work for new types you make. Defining how these operators work is called
operator overloading. For example, the string class has done this with + to allow things like
"Hello " + "World ".

Y
You
ou cannot overload all operators,
operators, but
but most work.
work. For example, you can overload your typical
typical
math operators: +, -, *, /, and %, the unary + and - (the positive and negative signs), as well
as ++ and --. You can also overload the relational operators ( >, <, >=, <=, ==, and =), but
these must be done in matching pairs. If you overload == you must also overload =, and same
with < and >, or >= and <=. You cannot directly overload the compound assignment operators
(+=, -=, etc.), but when you overload +, += is automatically handled for you. You cannot
overload the indexing operator ([]) as described in this section, but you can use an indexer
as described in the next section instead.
Y
You
ou cannot invent new operators in C#. I’d like to create what I call the marketing operator,
<=>, for those times when “you could save up to 15% or more by switching” ( savings <=>
0.15). Alas, that is not possible. (But you can always make a method.)

Defining an Operator Overload


Before we overload an operator, we need a class
cl ass where an operator makes sense. We will use
the following Point record here, but you can overload operators on any class or struct.
public record Point(double X, double Y); // Could have also made a simple class.

The math world has a clear-cut definition for adding points together: you add each
corresponding component together. Given the points at (2, 3) and (1, 8), addition is
done like so: (2+1, 3+8) or (3, 11).
Defining this in code looks like this:
public record Point(double X, double Y)
{
public static Point operator +(Point a,
newPoint b) => + b.X, a.Y + b.Y);
Point(a.X
}

Operators are essentially a special kind of static method. They must be marked both public
and static, and you cannot define operators in unrelated types. At least one of the
parameters must match the type you are putting the operator in.
What distinguishes an operator from a simple static method is the operator keyword and
the operator’s symbol instead of a name.
The above code uses an expression body, but it can also use a block body like any method.
With this operator
operator defined, we can put it to use:
Point a = new Point(2, 3);
Point b = new Point(1, 8);
Point result = a + b;
Console.WriteLine($"({result.X}, {result.Y})");

INDEXERS 327
Let’s do a second example: scalar multiplication. Scalar multiplication is when we take a point
and multiply it by a number. It has the effect of scaling the point by the amount indicated by
the number. The point (1, 3) multiplied by 3 results in the point (1*3, 3*3) or (3, 9).
public static Point operator *(Point p, double scalar) =>
new Point(p.X * scalar, p.Y * scalar);
public static Point operator *(double scalar, Point p) => p * scalar;

I have defined two * operators rather than one. More on that in a second, but these two
operators allow us to do this:
Point p = new Point(1, 3);
Point q = p * 3;
Point r = 3 * p;

When operators use different


different types,
types, order often matters. If you want to support
support both orderings,
you must define the operator twice, once for each order. One can call the other,
other, so you don’t
have to copy and paste the code. If we left off the second definition, the class would support p
* 3 but not 3 * p.
If you are defining one of the unary operators, your operator would have just a single
parameter,, such as this negation operator:
parameter
public static Point operator -(Point p) => new Point(-p.X, -p.Y);

When to Overload Operators


In any situation where you might overload an operator,
operator, you could also choose to use a method
instead. How do you decide which to use? Use the version that makes your code the most
around using operators is very concise (a - b is far shorter than
understandable. The syntax around
a.Subtract(b) ), but it only helps understandability if the meaning of subtraction for your
type is intuitive.

INDEXER󰁓
You can define how the indexing operator ([]) works with your class by making one or more
You
indexers. These have some commonality with operators but have more in common with
properties. In some ways, they are like a property with a parameter,
parameter, and some people refer to
them as parameterful properties. (That’s a mouthful; I prefer indexer.)
Here’ss an example indexer in a simple Pair class:
Here’
public class Pair
{
public int First { get; set; }
public int Second { get; set; }

public double this[int index]


{
get
{
if (index == 0) return First;
else return Second;
}
set
{

328 LEVEL 41 OPERATOR OVERLOADING


if (index == 0) First = value;
else Second = value;
}
}
}

Y
You
ou can see the similarities between this and a property. Both have getters and setters, and

both haveaccess
also have that implicit pvalue parameter
to the parameters
arameters inin
defined the setter,
the squareetc. The only real difference
brackets—your is that The
index variables. you
number variable is accessible in both the getter and the setter.
just ints. The following lets you use 'a' and 'b':
An indexer need not be limited to just
public double this[char letter]
{
get
{
if (letter == 'a') return First;
else return Second;
}
set
{
if (name == 'a') First = value;
else Second = value;
}
}

In this case, I’d generally recommend just using the First and Second properties, but an
indexer makes a lot of sense when the allowed indices are large or not known ahead of time.
An indexer can also have multiple parameters.
parameters. The following
following indexer lets you access items in
a 2D grid of numbers (a matrix):
public int this[int row, int column]
{
// ...
}

A type can define as many indexers


indexers as needed, as long as they ha
have
ve different parameters.
parameters.

Index Initializer 󰁓yntax


Any type that defines an indexer can take advantage of index initializer syntax. Like object
initializer syntax, you can use this to set up an object through its indexers:
Pair p = new Pair()
{
[0] = 1,
[1] = -4
};

The above code is virtually the same as this:


Pair p = new Pair();
p[0] = 1;
p[1] = -4;

Perhaps a better illustration of this syntax is the Dictionary class. The code below uses
index initializer syntax to set up a dictionary of colors based on their name:

CUSTOM CONVERSIONS 329


Dictionary<string, Color> namedColors = new Dictionary<string, Color>
{
["red"] = new Color(1.0, 0.0, 0.0),
["orange"] = new Color(1.0, 0.64, 0.0),
["yellow"] = new Color(1.0, 1.0, 0.0)
};

CU󰁓TOM CONVER󰁓ION󰁓
In C#, you can cast between types that don’t have a direct inheritance relationship. For
example:
int a = (int)3.0; // Explicit cast or conversion from a double to an int.
double b = 3; // Implicit cast or conversion from an int to a double.

Y
You
ou can define custom conversions for the types you create. Custom conversions are done
much like operator overloading but with some differences. To illustrate, let’s rename our
Point class from earlier to Point2, and then let’s also say we have a Point3 with an X, Y,
and Z property, for representing a 3D location.
public record Point2(double X, double Y);
public record Point3(double X, double Y, double Z);
Converting between these two types might be nice. You could even think of a Point2 as a
Point3 with a Z coordinate of 0.
We must consider what data may be lost in conversions of this sort. Going from Point2 to
Point3 loses nothing. Point3 can carry the X and Y values over and use 0 as the default Z
value. But going from Point3 to Point2 will lose the Z component. It is likely reasonable for
conversion from Point2 to Point3 to happen automatically. It is likely unreasonable for
conversion from Point3 to Point2 to happen automatically. We see the same thing with
int and long. Casting from an int to a long happens implicitly, but going the other way
requires explicitly stating
stating the cast:
cast :
int a = 0;
long b = a; // Implicit cast.
int c = (int)b; // Explicit cast.

A long can accurately store every possible value that int can hold, so the conversion is safe.
An int cannot contain all possible values of a long, so the conversion has risk and must be
defining conversions for Point2 and Point3, we must keep this in mind.
written out. When defining m ind.
Here is our first conversion, from a Point2 to a Point3:
public static implicit operator Point3(Point2 p) => new Point3(p.X, p.Y, 0);

A custom conversion is
is much like an operator (indeed, it is defining the typecasting operator).
The two main differences are the implicit keyword and the name Point3. Each operator
will be either implicit or explicit. This choice indicates whether the cast can happen
automatically implicit
in the position(where ) orwould
a name must be
go.spelled
The above (explicit
out is ). You
a conversion listPoint2
from the type(based
to convert to
on the
parameter type) to Point3 (based on the “name”).
The body performs the conversion. Like any method, you can use an expression body or a
block body.

330 LEVEL 41 OPERATOR OVERLOADING


Custom conversions must be defined in one of the types involved in the conversion, so this
operator must go into either Point2 or Point3.
conversion added to either Point2 or Point3, we can now write code like this:
With this conversion
Point2 a = new Point2(1, 2);
Point3 b = a;

Even with no inheritance relationship between the two, the conversion from Point2 to
Point3 will happen automatically. The compiler will see the need for a conversion, look for
an appropriate one, and apply it.
The conversion from Point3 to Point2 loses data, so we define that as an explicit
conversion:
public static explicit operator Point2(Point3 p) => new Point2(p.X, p.Y);

We chose explicit instead of implicit because we do not want somebody to lose data
without specifically asking for it.
Point3 a = new Point3(1, 2, 3);
Point2 b = (Point2)a;

The Pitfalls of Custom Conversions and 󰁓ome Alternatives


Custom conversions create new objects, which can have unexpected consequences for
types. Suppose we make Point3 ’s properties settable and also make this method:
reference types.
void MoveLeft(Point3 p) => p.X--;

Consider this usage:


Point2 point = new Point2(0, 0);
MoveLeft(point);

This code seems reasonable at first glance. point is converted to a Point3 before
MoveLeft is called, and then the point is shifted. However, the conversion to a Point3
creates a new object, and it is this new object that is passed to MoveLeft. The original
Point2 is unchanged.
Errors like this are hard to notice. Some recommend avoiding custom conversions entirely
because of subtle issues like this. When and how to use custom conversions is your choice,
but knowing the alternative is useful. We could make this simple method instead:
public Point3 ToPoint3() => new Point3(X, Y, 0);

This requires us to call ToPoint3(), which is far more likely to raise a red flag:
Point2 point = new Point2(0, 0);
MoveLeft(point.ToPoint3());

It is more apparent that you are passing a separate object with this code.
Y
You
ou could also define a constructor that
that does the conversion:
public Point3(Point2 p) : this(p.X, p.Y, 0) { }

The conversion also stands out more clearly:

CUSTOM CONVERSIONS 331


Point2 point = new Point2(0, 0);
MoveLeft(new Point3(point));

Custom conversions are


are not evil, but keep this
t his consequence in mind as you write them.

Knowledge Check Operators 25 XP

Check your knowledge with the following questions:


1. True/False. Operator overloading allows you to define a new operator such as @@.
2. True/False. You can overload all C# operators.
3. True/False. Operator overloads must be public.
Answers: (1) False. (2) False. (3) True.

Challenge Navigating Operand City 100 XP


The City of Operand is a carefully planned city, organized into city blocks, lined up north to south and
east to west. Blocks are referred to by their coordinates in the city, as we saw in the Cavern of Objects.
The inhabitants of the town use the following three types as they work with the city’s blocks:
public record BlockCoordinate(int Row, int Column);
public record BlockOffset(int RowOffset, int ColumnOffset);
public enum Direction { North, East, South, West }

BlockCoordinate refers to a specific block’s location, BlockOffset is for relative distances between
blocks, and Direction specifies directions. As we saw with the Cavern of Objects, rows start at 0 at the
north end of the city and get bigger as you go south, while columns start at 0 on the west end of the city
and get bigger as you go east.
The city has used these three types for a long time, but the problem is that they do not play nice with
each other. The town is the steward of three Medallions of Code. They will give each of them to you if
you can use them to help make life more manageable. Use the code above as a starting point for what
you build.
In exchange for the Medallion of Operators, they ask you to make it easy to add a BlockCoordinate
with a Direction and also with a BlockOffset to get new BlockCoordinates. Add operators to
BlockCoordinate to achieve this.
Objectives:
• Use the code above as a starting point.
• Add an addition (+) operator to BlockCoordinate that takes a BlockCoordinate and a
BlockOffset as arguments and produces a new BlockCoordinate that refers to the one you
would arrive at by starting at the original coordinate and moving by the offset. That is, if we started
at (4, 3) and had an offset of (2, 0), we should end up at (6, 3).
• Add another addition (+) operator to BlockCoordinate that takes a BlockCoordinate and a
Direction as arguments and produces a new BlockCoordinate that is a block in the direction
indicated. If we started at (4, 3) and went east, we should end up at (4, 4).

Write code to ensure that both operators work correctly.

332 LEVEL 41 OPERATOR OVERLOADING

Challenge Indexing Operand City 75 XP


In exchange for the Medallion of Indexers, the city asks for the ability to index a BlockCoordinate by
a number: block[0] for the block’s row and block[1] for the block’s column. Help them in this quest
by adding a get-only indexer to the BlockCoordinate class.
Objectives:
• Add a get-only indexer to BlockCoordinate to access items by an index: index 0 is the row, and
index 1 is the
t he column.
• Answer this question: Does an indexer provide many benefits over just referring to the Row and
Column properties in this case? Explain your thinking.

Challenge Converting Directions to Offsets 50 XP


Operanders often use both the Direction and the BlockOffset in casual communication: “go north”
or “go two blocks west and one block south.” However, it would be convenient to convert a direction to
a BlockOffset. For example, the direction north would become an offset of (-1, 0). Operanders
offer you their final medallion, the Medallion of Conversions, if you can add a custom conversion in
BlockOffset that converts a Direction to a BlockOffset.
Objectives:
• Add a custom conversion to BlockOffset that converts from Direction to BlockOffset.
• Answer this question: This challenge didn’t call out whether to make the conversion explicit or
implicit. Which did you choose, and why?
L EVEL
42
QUERY EXPRE󰁓󰁓ION󰁓

󰁓peedrun

Query
data expressions
collection and are a special
return it in atype of statement
statemen
particular formatt or
that allows you to extract specific pieces from a
organization.
• Query expressions are made of multiple clauses.
• from identifies the collection that is being queried.
• select identifies the data to be produced.
• where filters out elements in the query.
• orderby sorts the results.
• join combines multiple collections.
• let allows you to give a name to a part of a query for later reference.
• into continues queries where it would otherwise have terminated.
• group categorizes data into groups.
• All queries can be done using query syntax or with method calls.

Most programs deal with collections of data and need to search the data. This type of task is
called a query. Here are some examples:
• In real estate, find all houses under $400,000 with 2+
2 + bathrooms and 3+ bedrooms.
• In a project management tool, find all active tasks assigned to each person on the team.
• In a video game, find all objects close enough to an explosion to take splash damage.
C# has a type of expression designed to make queries easy. These expressions are Language
Integrated Queries (LINQ) or simply query expressions. These query expressions are most
commonly done on objects in memory—arrays, lists, dictionaries, etc. But LINQ also makes it
possible for a LINQ query to retrieve data from an actual database such as MySQL, Oracle, or
Microsoft SQL Server. The first
first is known as LINQ for Objects, and the se
second
cond is LINQ for SQL
SQL..
We will
will focus on querying objects in memory since it is the most versatile.
versatile.
Anything we do with query expressions have been done with if statements and loops.
expressions could have
But as we will see, query expressions are often more readable and shorter.

334 LEVEL 42 QUERY EXPRESS


EXPRESSIONS
IONS

Queries and IEnumerable<T>


Query expressions work on anything that implements IEnumerable<T> . That is virtually all
collection types in .NET, including lists, arrays, and dictionaries. In this level, when I refer to
collections, datasets, or sets, I’m referring to anything that implements IEnumerable<T> .
The logic for doing query expressions with IEnumerable<T> does not live in

IEnumerable<T>
expression itself.There
functionality. Instead,
areaalmost
set of extension methods
200 of these (L evel
(Level
e xtension
extension 34) implement
methods, the query
so it is a good thing
you do not have define a new IEnumerable<T> !
have to implement all of them to define
The System.Linq.Enumerableclass defines these extension methods. There is an implicit
i mplicit
using directive for System.Linq in .NET 6+ projects, but if you are using an older version,
you will need to add using System.Linq; to your files manually.

󰁓ample Classes
We will use the following three classes in the samples in this level. You might find similar
classes in a game. The GameObject class is the base class of potentially many types of objects
found in the game, and Ship is one of those types. The Player class represents a game player,
and GameObject instances are each
e ach owned by a player.
player.
public class GameObject
{
public int ID { get; set; }
public double X { get; set; }
public double Y { get; set; }
public int MaxHP { get; set; }
public int HP { get; set; }
public int PlayerID { get; set; }
}

public class Ship : GameObject { }

public record Player(int ID, string UserName, string TeamColor);

If you are following along at home, you might also find the following setup code helpful:
List<GameObject> objects = new List<GameObject>();
objects.Add(new Ship { ID = 1, X=0, Y=0, HP = 50, MaxHP = 100, PlayerID = 1 });
objects.Add(new Ship { ID = 2, X=4, Y=2, HP = 75, MaxHP = 100, PlayerID = 1 });
objects.Add(new Ship { ID = 3, X=9, Y=3, HP = 0, MaxHP = 100, PlayerID = 2 });

List<Player> players = new List<Player>();


players.Add(new Player(1, "Player 1", "Red"));
players.Add(new Player(2, "Player 2", "Blue"));

QUERY EXPRE󰁓󰁓ION BA󰁓IC󰁓


Y
You
ou form a query expression out of a series of smaller elements called clauses. Each clause is
like a station in an assembly line. It receives elements from the clause before it and supplies
elements to the clause
cl ause after it. Add new clauses as needed to gget
et the result you want.
Query expressions begin with a from clause and end with a select clause. A from clause
identifies the source of the data. A select clause indicates which part of the data to produce as
a final result. The simplest
simplest query expressi
expressionon possible is this:

QUERY EXPRESSION BASICS 335


IEnumerable<GameObject> everything = from o in objects
select o;

Above, I have put each clause on a separate line and used whitespace to line them up
up.. That is
not necessary, but it is a common practice. It makes it easier to understand.
Despite what I have done in most of this book, I will use var instead of spelling out the
variable’ss type in most code samples in this level.
variable’ l evel. Books don’t have much horizontal space,
and the long name detracts from the focus. But note that the result is an IEnumerable<
GameObject>, not a List<GameObject> . Query expressions produce IEnumerable<T> .
The from clause is the first line: from o in objects. A from clause begins a query
expression by naming the source of the query: objects. It also introduces a variable named
o. A variable in a from clause is called a range variable. The rest of the query expression can
use this variable. While more descriptive names are often better, query expressions are so
small that C# programmers often use just a single letter
l etter..
The select clause is the second line: select o. A select clause starts with the select keyword,
followed by an expression that computes the query expression’s final result objects. The
expression o is the simplest possible expression, taking o whole and unchanged. We will see
more complex ones soon.
The result is an IEnumerable<GameObject> containing the exact same items as objects.
Let’s try something more meaningful. This query expression grabs each object’s ID instead of
the entire object:
var ids = from o in objects
select o.ID;

The result is an IEnumerable<int> , rather than IEnumerable<GameObject> because


the select clause’s expression produced an int. But the sky is the limit in what you can put in
a select clause’
cl ause’ss expression. For example:
e xample:
var healthText = from o in objects
select $"{o.HP}/{o.MaxHP}";

This query will give you a string for each object in the game with text like "0/50" or
"92/100".
How about this one?
var healthStatus = from o in objects
select (o, $"{o.HP}/{o.MaxHP}"); // Tuple

This query creates a tuple combining the original object with its health text. The type of
healthStatus is IEnumerable<(GameObject, string)>. Query expressions make it
easy to build weird, complex types for short-term use.
Filtering
A where clause provides an expression used to filter the elements passing by it in the assembly
line. It includes an expression that must be true
tr ue for the item to remain past the where clause.
The following expression produces only game objects with non-zero hit points remaining:
var aliveObjects = from o in objects
where o.HP > 0

336 LEVEL 42 QUERY EXPRESS


EXPRESSIONS
IONS
select o;

This expression can be any bool expression. You


You can make it as complex as you need.
While query expressions begin with a from and end with a select, the middle is far more
flexible. The following applies multiple where clauses back to back:
var aliveObjects = from o in objects
where o.PlayerID == 4
where o.HP > 0
select o;

Ordering
An orderby clause will order items. This code will create an IEnumerable<GameObject>
where the first lowest MaxHP, then the next lowest, etc.
first item has the lowest
var weakestObjects = from o in objects
orderby o.MaxHP
select o;

Y
You order by placing the descending keyword at the end:
ou can reverse the order
var strongestObjects = from o in objects
orderby o.MaxHP descending
select o;

The ascending keyword can also be used there, but that is the default, so there is usually no
need.
If you need to break a tie, you can list
li st multiple expressions to sort on, separated by commas:
var weakestObjects = from o in objects
orderby o.MaxHP, o.HP
select o;

This sorts by MaxHP primarily but resolves ties by looking at HP. You can name as many sorting
s orting
criteria as you need with more commas.
com mas.
Y
You
ou can use these middle-of-the-pipeline
middle-of-the-pipeline clauses however they are needed, in any order and
number. For example:
var player4WeakestObjects = from o in objects
where o.PlayerID == 4
orderby o.HP
where o.HP > 0
orderby o.MaxHP
select o;

Few queries end up so complicated.


c omplicated. There is rarely a need for it, and many programmers will
split apart long queries to keep things clear. But keep in mind that ordering does make a
difference in the results produced and also in speed.
METHOD CALL 󰁓YNTAX
If you don’t like all of these new keywords,
keywords, you’re in luck. You can write every query eexpressi
xpression
on
with method calls instead of keywords. (The compiler transforms the keywords into method
calls anyway.) This approach is called method call syntax. Instead of the where keyword, you

METHOD CALL SYNTAX 337


use the Where method. Instead of the select keyword, you use the Select method.
Consider this keyword-based query:
var results = from o in objects
where o.HP > 0
orderby o.HP
select o.HP / o.MaxHP;

With method call syntax,


syntax, it would look like this:
var results = objects
.Where(o => o.HP > 0)
.OrderBy(o => o.HP)
.Select(o => o.HP / o.MaxHP);

These methods typically have delegate parameters, so lambda expressions are common.
The conversion from keywords to method calls should not always be literal. For example,
while a keyword-based expression must end with a select, even if that is just select o,
method call syntax does not require ending with a Select. You do not need to do Select(o
=> o).
Some people prefer the conciseness of the keyword-based version. Others feel like method
calls are just more natural. Yet others will use some of both, depending on which seems
cleaner for the specific query. You can decide for yourself which you like better.
better.

Unique Methods
Method call syntax can do everything the keywords can do, plus a few things for which there
are no keywords. Here are a few of the most useful.
Count allows you to either count the total items in the collection or the number that meet
some specific condition:
int totalItems = objects.Count();
int player1ObjectCount = objects.Count(x => x.PlayerID == 1);

Any and All can tell you if any or every element in the collection meets some condition:
bool anyAlive = objects.Any(y => y.HP > 0);
bool allDead = objects.All(y => y.HP == 0);

Skip lets you skip a few items at the beginning, while Take lets you grab the first few while
dropping the rest:
var allButFirst = objects.OrderBy(m => m.HP).Skip(1);
var firstThree = objects.OrderBy(m => m.HP).Take(3);

Average, Sum, Min, and Max let you do math with the items or with some aspect of the item:
int longestName = players.Max(p => p.UserName.Length);
int shortestName = players.Min(p => p.UserName.Length);
double averageNameLength = players.Average(p => p.UserName.Length);
int totalHP = objects.Sum(o => o.HP);
There are many more, and when you want to explore them, you can use Visual Studio’s
IntelliSense to dig around and see what’s out there (Bonus Level A).
A).

338 LEVEL 42 QUERY EXPRESS


EXPRESSIONS
IONS

ADVANCED QUERIE󰁓
Few query expressions need more than the above, but there is quite a bit more to query
expressions when you need to get fancy. This section covers more advanced usages of the
clauses we already saw and looks at a few additional clause types.
Let’s start by fleshing out one more detail of the from clause. If you are confident that
everything in a collection is of some specific derived type, you can name the derived type
instead, making it the type used in subsequent clauses. The code below assumes all game
objects are the Ship class, and the result is an IEnumerable<Ship> instead of an
IEnumerable<GameObject> :
IEnumerable<Ship> ships = from Ship s in objects
select s;

If you are wrong, it will throw an InvalidCastException , so you must know ahead of time
or filter it to just that type first.
The method call syntax equivalent of this is gameObjects.Cast<Ship>() if you know they
are all ships or gameObjects.OfType<Ship>() if you are unsure and want to filter down
to only those of that type.

Multiple from Clauses


Query expressions start with a from clause, but you can have many if you want to work with
multiple collections at once. Two from clauses will allow us to look at each pairing of items.
If GameObject had a CollidingWith(GameObject) method, we could write the
following code to get a collection of all pairings that are colliding (intersecting) objects:
var intersections = from o1 in objects
from o2 in objects
where o1 != o2 // Don't compare an object to itself.
where o1.CollidingWith(o2)
select (o1, o2);

Multiple
will from10×10
evaluate clausesorcan
100quickly cause performance
perfor mance
total comparisons. If weissues. If we have
have 1000 game10objects,
game objects,
it will we
be
1000×1000 or 1,000,000 total comparisons.

Joining Multiple Collections Together


In other situations, you want to combine two collections
col lections through a common llink.
ink. Rather than
looking at every item in one collection
collec tion paired with every item in a second, we want to see only
pairings that match each other. In the database world, this is called a join. In our example
classes, GameObject has a PlayerID property corresponding to Player’s ID property. We
can determine which color a game object should be by finding the player associated with the
game object and using that player’s color using a join clause:
var objectColors = from o in objects
join p (o,
select in players on o.PlayerID equals p.ID
p.TeamColor);

The join clause introduces the second collection and a second range variable, p. After the on,
you can specify which part of each range variable to use in determining a pairing. The order
matters. The first collection’s range variable must come before the equals; the second

ADV
ADVANCED
ANCED QUERIES 339
collection’s range variable must go after. You typically refer to a property from each variable
for comparison, but more complex expressions are allowed if necessary.
A join clause produces all successful pairings. If an object in one collection had no match, it
would not appear in the results. If an object pairs with several items in the other collection,
collec tion,
each pairing appears. However, in many situations, this is avoided by other parts of the
software.. For example, if two players had the same ID, then we would see a pairing of an object
software
from a join clause. But we would typically ensure each player’
with both players from player ’s ID is unique.
Once past the join, you can use both range variables in your clauses, knowing that the two
belong together.
together.

The let Clause


A let clause defines another variable in the middle of a query expression that you can use
afterward. This clause is great if you need to use some computation repeatedly:
var statuses = from o in objects
let percentHealth = o.HP / o.MaxHP * 100
select $"{o.ID} is at {percentHealth}%.";

Continuation Clauses
A select clause typically ends a query expression, but you can keep it going with an into clause
(also called a continuation clause). This clause introduces a new range variable and begins a
new query expression on the tail of the previous one:
var deadStrongObjectIDs = from o in objects
where o.MaxHP > 50
select (o.ID, o.HP, o.MaxHP, o.HP / o.MaxHP)
into objectHealth
where objectHealth.HP == 0
select objectHealth.ID;

The original range variable is not accessible past the into. Essentially, a new query expression
has started. Some people will adjust
a djust whitespace to make this stand out visually:
var deadStrongObjectIDs = from o in objects
where o.MaxHP > 50
select (o.ID, o.HP, o.MaxHP, o.HP / o.MaxHP)
into objectHealth
where objectHealth.HP == 0
select objectHealth.ID;

Alternatively, you can


can also just write it
it as two separate statements:
statements:
var strongObjects = from o in objects
where o.MaxHP > 50
select (o.ID, o.HP, o.MaxHP, o.HP / o.MaxHP);
var deadObjectIDs = from s in strongObjects
where s.HP == 0
select s.ID;

Grouping
A group by clause puts the items into groups. ItIt is a second way to end a query expression. The
following will group all of our objects by their owning player:

340 LEVEL 42 QUERY EXPRESS


EXPRESSIONS
IONS
IEnumerable<IGrouping<int, GameObject>> groups = from o in objects
group o by o.PlayerID;

Notice the return type. The result is a collection of groupings. The IGrouping<Tkey,
TElement> interface extends IEnumerable<TElement> augmented with a shared key.
Here, the key is the player ID, and the items in the collection will be all of the objects that
belong to the player.
b y clause contains two expressions. The first ( o in the code above) determines the
A group by
final elements of each group. The second (o.PlayerID in the code above) determines what
the shared key is for each group. Either can be as complex
c omplex as needed.

Group Joins
The final clause type is the group join, combining elements of both grouping and joining.
These can be very elaborate clauses, formed of many pieces that can each be complex. A
situation that might call for a group join is if you wanted each player and their owned objects.
A simple grouping is not sufficient because a player with no game objects does not end up
with a group at all. A simple join is not enough because it doesn’t do grouping.
A group join starts ncludes an into:
starts the same as a simple join, then iincludes
var playerObjects = from p in players
join o in objects on p.ID equals o.PlayerID into ownedObjects
select (Player: p, Objects: ownedObjects);

foreach (var playerObjectSet in playerObjects)


{
Console.WriteLine($"{playerObjectSet.Player.UserName} has the following:");
foreach (var gameObject in playerObjectSet.Objects)
Console.WriteLine(gameObject.ID);
}

All items from


from the second collection associated
associated with the
the object from the first are
are bundled into
a new IEnumerable<T> and given a new name ( ownedObjects ). You get a result even if
that is an empty collection.
The above code combines the player with its objects in a tuple and displays the results.
Even simple group joins are often complicated; try to keep them understandable as you build
them.

DEFERRED EXECUTION
Arrays and lists store their data in memory. In contrast, query expressions do not need to
compute all results immediately. A query expressi
e xpression
on is almost like defining the machinery or
assembly line for producing items without actually creating them. Instead, the results are built
a little at a time, only as the next item is needed. This approach is called deferred execution.
The upshot
to dig of deferred
through, they doexecution is that
not all need it is
to be putg entle
gentle
in anon memory.
array to useIfthem.
you have
You acan
vast set of
look at items
them
one at a time. And if you figure out what you need after only a few items, the rest of them never
need to be computed and placed in memory
memor y at all.

LINQ TO SQL 341


On the other hand, if you need to go through all items repeatedly, you end up computing the
collection’s contents more than once, which wastes time. If you are in this situation, use the
ToArray and ToList methods, which will materialize the entire set into an array or list:
var aliveObjects = from o in objects
where o.HP > 0
select o;

List<GameObject> aliveObjectsList = aliveObjects.ToList();

foreach(var aliveObject in aliveObjectsList)


Console.WriteLine($"{aliveObject.ID} is alive!");

foreach(var aliveObject in aliveObjectsList)


aliveObject.HP--;

The cost for computing the collection happens once (in the ToList() method), and the
iteration over the collection in both foreach loops stays fast.
In general, you should prefer deferred execution when you can. Only materialize the entire
collection into a list or array when processing the whole set more than once.

Not
mustall query
walk expressions
through can pulltooff
every element deferredthe
compute execution.
answer. For
answer. In example,
situations Count
thethese,
like method
immediate
evaluation will happen out of necessity.

LINQ TO 󰁓QL
Our focus in this level has been on using query expressions on collections in memory. This
scheme is called LINQ to Objects. But query expressions can also work against data in a
database. This scheme is called LINQ to SQL. Unfortunately, this complex subject demands
knowledge of databases beyond what this book can cover.
However, the syntax is identical, and the best part is that your query (or at least parts of it) will
However,
run inside the database engine itself. Thus, your C# code is automatically translated to the
database’s query language and runs over there. (This is where the name “Language INtegrated
database’s
Query” comes from.)
LINQ to SQL isn’t a wholesale replacement for interacting with a database. For example, you
cannot write data in a query expression. But it makes specific database tasks far easier.
easier.

Knowledge Check Queries 25 XP


Check your knowledge with the following questions:
1. What clause (keyword) starts a query expression?
2. What clause filters data?
3. True/False. You can order by multiple criteria in a single orderby clause.
4. What clause combines
combines two related
related sets of
of d
data?
ata?
Answers: (1) from clause. (2) where clause. (3) True. (4) join clause.

342 LEVEL 42 QUERY EXPRESS


EXPRESSIONS
IONS

Challenge The Three Lenses 100 XP


The Guardian of the Medallion of Queries, Lennik, has long awaited when he can return the Medallion
to a worthy programmer. But he only wants to give it to somebody who truly understands the value of
queries. He requires you to build a solution to a simple problem three times over. Lennik gives you the
following array
ar ray of positive
posit ive numbers: [1, 9, 2, 8, 3, 7,
7, 4, 6, 5]. He asks you to make
m ake a new collection
col lection from
f rom this
data where:
• All the odd numbers are filtered out, and only the even should be considered.
considered.
• The numbers are in order.
• The numbers are doubled.
For example, with the array above, the odd/even filter should result in 2, 8, 4, 6. The ordering step should
result in 2, 4, 6, 8. The doubling step should result in 4, 8, 12, 16 as the final answer.
Objectives:
• Write a method that will take an int[] as input and produce an IEnumerable<int> (it could be
a list or array if you want) that meets all three of the conditions above using only procedural code—
if statements, switches, and loops. Hint: the static Array.Sort method might be a useful tool

here.
• Write a method that will take an int[] as input and produce an IEnumerable<int> that meets
the three above conditions using a keyword-based query expression (from x, where x, select x,
etc.).
• Write a method that will take an int[] as input and produce an IEnumerable<int> that meets
the three above conditions
conditions using a method-ca ll-based query expression. (x.Select(n => n + 1),
method-call-based
x.Where(n => n < 0), etc.)
• Run all three methods and display the results to ensure they all produce good answers.
• Answer this question: Compare the size and understandability of these three approaches. Do any
stand out as being particularly good or particularly bad?
• Answer this question: Of the three approaches, which is your personal favorite, and why?
LEVEL 43
THREAD󰁓

󰁓peedrun
• threads allows your program to do more than one thing at a time: Thread thread = new
Creating threads
Thread(MethodNameHere); thread.Start();
• If you need to pass something to a thread, your start method must have a single object parameter,
which is supplied with thread.Start(theObject);
• Wait for a thread to finish with Thread.Join.
• c ause problems. If you do this, protect critical sections with a lock: lock
Threads that share data can cause
(aPrivateObject)
(aPrivateObjec t) { /* code in here is thread safe */ } .

In the beginning, all computers had only one


on e processor,
processor, allowing them to do just one thing at
a time. Modern computers typically have multiple processors, letting them do many things

simultaneously.
chip. More
Four cores and specifically,
eight cores are they usually have
commonplace, andmultiple
16 or 32cores onuncommon
are not the same processor
either.
Y
You
ou can leverage this power and get long-running jobs done significantly faster if you write
your code correctly. The concept of running multiple things at the same time is called
concurrency. The next two levels cover two of the primary flavors of concurrency. We will use
multiple “threads” of execution to do multi-threaded
multi-threaded programming in this level.

THE BA󰁓IC󰁓 OF THREAD󰁓


A thread is an independent execution path in a program. Every program has at least one
thread in it. When we run a typical C# program, a thread is created and begins running our
main method, one instruction at a time.
A program can create additional threads, and each can run its own code.c ode. When a program
does this, it becomes a multi-threaded application. Each thread gets its own stack to manage
its method calls, but all threads in your program share the same heap, letting them share data.
Modern computers have many processors, but they also usually juggle many applications and
services, each with one or more threads. There are typically far more threads than there are

344 LEVEL 43 THREADS


processors. The operating system has a scheduler that decides when to let each thread have a
chance to run. Like a juggler, the scheduler ensures each thread gets frequent opportunities
to run on a processor without letting any languish. The scheduler’s job is complicated,
weighing factors like each thread’
thread’ss priority in the system and how
how long it has been waiting
waiting for
a turn.
When the scheduler decides to swap out one thread for another,
another, it takes time. It must
remember the thread’s state so it can be restored later and then unpack the replacement’s
previous state so that it can resume. This swap is called a context switch. This swap is
unproductive time, but if context switches don’t happen often enough, threads starve, and
their work doesn’t get done.
Y
Your
our program has little control over the scheduler
sc heduler.. You
You will not know when your threads will
get a chance to run,
r un, nor is it obvious which code will execute first if it is happening on different
threads. That makes multi-threaded programming far more complicated than single-
threaded programming. It is sometimes worth the trouble, and sometimes not.
We’ll cover the key elements of multi-threaded
We’ll multi-threaded programming,
programming, but this is a complex issue that
we cannot fully cover in
in this book.

U󰁓ING THREAD󰁓
Before creating more threads, we must first identify work that can run independently. This is
one of the most complex parts of multi-threaded
multi-threaded programming. It is an art and a science, and
it takes patience and practice to get good at it.
Y
You
ou want work that is entirely (or almost entirely) independent of the rest of your code, and
that will take a while to run. If it is intertwined with everything else, it does not make sense to
run it separately. If it is too small in size, it won’t be worth the overhead of creating a whole
other thread. Threads are comparatively expensive to make and maintain.
Let’s keep it simple and do multi-threading with the simple task of counting to 100:
void CountTo100()
{
for (int index = 0; index < 100; index++)
Console.WriteLine(index + 1);
}

The System.Threading.Thread class captures the concept of a thread. The


System.Threading namespace is automatically added in .NET 6+ projects, but if you target
an older version of .NET, you will want to add a using System.Threading; to the top of
the file.
When you create a new thread, you give
give it the method to run in its constructor (Level 36).
36).
Thread thread = new Thread(CountTo100);
In this case, the method must have a void return type and no parameters.

Once created, you start the thread by calling its Start() method:
thread.Start();

After calling Start(), the new thread will begin running the code in the method you
supplied, while your program’s original “main” thread will continue to the next statement

USING THREADS 345


below thread.Start(); . Both threads will run in parallel. The scheduler will let each
thread run for a while, juggling them and the
th e threads in other processes.
A complete multi-threaded
multi-threaded program may
may look like this:
Thread thread = new Thread(CountTo100);
thread.Start();
Console.WriteLine("Main Thread Done");
void CountTo100()
{
for (int index = 0; index < 100; index++)
Console.WriteLine(index + 1);
}

Both threads write stuff in the console


consol e window, but you cannot predict how the scheduler will
run them. The text “Main Thread Done” could appear before all the numbers, after all the
numbers, or somewhere in the middle. I just ran it once and got
g ot the following:
1
2
3
Main Thread Done
4
...

Rerunning it produces a different order.


One thread can wait for another to finish before proceeding using Thread’s Join method.
For example, the following makes two threads that count to 100 and waits for both to finish:
finish :
Thread thread1 = new Thread(CountTo100);
thread1.Start();
Thread thread2 = new Thread(CountTo100);
thread2.Start();

thread1.Join();
thread2.Join();
Console.WriteLine("Main Thread Done");
Repeatedly running this code and viewing its output can be extremely helpful for
understanding how threads are scheduled. It’s not just a back-and-forth. Each thread gets a
chunk of time in (seemingly) unpredictable lengths. One thread will display the first 13
numbers, and then the second gets to 22, then the first gets up to 18, and so on.
Y
You
ou will not see “Main Thread Done” in the middle of the numbers this
this time because the
the main
main
thread will not reach that line until both counting threads finish.
Y
You
ou cannot directly task a thread with additional methods to run, but you could design a
system where tasks are
are placed in a list somewhere, and the thread runs indefinitely, checking
to see if new jobs have appeared for it to run. Once a thread finishes, it is over
over.. You would just
make a new thread for any other work.

󰁓haring Data with a Thread


Our first pass with threads did not allow them to share any data. The method the thread ran
had no parameters and a void return type. Alternatively, we can use a single object-typed

346 LEVEL 43 THREADS


parameter and pass in an object that allows the main thread and the new thread to share
thread’s Start method:
information. This object is supplied when calling the thread’s
MultiplicationProblem problem = new MultiplicationProblem { A = 2, B = 3 };
Thread thread = new Thread(Multiply);
thread.Start(problem);

thread.Join();
Console.WriteLine(problem.Result);

void Multiply(object? obj)


{
if (obj == null) return; // Nothing to do if it is null.
MultiplicationProblem problem = (MultiplicationProblem)obj;
problem.Result = problem.A * problem.B;
}

class MultiplicationProblem
{
public double A { get; set; }
public double B { get; set; }

} public double Result { get; set; }

The parameter’s type must be object. You will have to downcast to the right type in the new
thread’s method.
This shared object can have properties for all the inputs and results the thread may need,
allowing the data to be shared with the original
origi nal thread.
There are other ways that multiple threads can share data. The new thread has access to any
accessible static methods and fields. If the thread is running an instance method, it will also
have access to that object’s instance data. That leads to this pattern:
Operation operation = new Operation(1, "Hello");
Thread thread = new Thread(operation.Run);
thread.Start();

public class Operation


{
public int Number { get; }
public string Word { get; }

public Operation(int number, string word) { Number = number; Word = word; }

public void Run() { /* Insert long task using Number and Word. */ }
}

There is a distinct danger to sharing data among


am ong threads, as we will soon see.
󰁓leeping
The static Thread.Sleep method pauses a thread for a bit.
Thread.Sleep(1000); // 1 second

THREAD SAFETY 347


The Sleep method is static, and it makes the current thread pause. The sleep time is in
milliseconds (1000 milliseconds is one second). When you do this, the thread will give up the
rest of its currently scheduled time and won’t get another chance until after the time specified.

THREAD AFETY
󰁓time
Any two threads
two threads share data, there is a danger of them simultaneously
simultaneously modifying the data
in ways that hurt each other.
other. If the shared data is immutable (read-only), this problem solves
itself. Consider even just this simple example of two threads that both increment a _number
field that they both have access to:
SharedData sharedData = new SharedData();
Thread thread = new Thread(sharedData.Increment);
thread.Start();

sharedData.Increment();

thread.Join();

Console.WriteLine(sharedData.Number);

class SharedData
{
private int _number;
public int Number => _number;
public void Increment() => _number++;
}

The main thread and the new thread do nothing but call the Increment method, which adds
one to the variable. This program seems innocent enough, and you would expect when the
program finishes, the output will be 2. But consider how this could go wrong. _number++; is
the same as _number = _number + 1;. It retrieves the value out of _number, adds one to
it, then stores the updated value back in _number. It is a three-step process, and due to the
scheduling nature of threads, we cannot guarantee when each thread will run any given step
in that process. This won’t usually cause problems, but the following scenario is possible:
1. Thread 1 reads the current value out of _number (a value of 0).
2. Thread 1 computes the new value (1).
3. Thread 2 reads the current value out of _number (still 0!).
4. Thread 2 computes the new value (1).
5. Thread 2 updates _number (1).
6. Thread 1 updates _number (1 again!).
Even though two threads went through the logic of incrementing the variable, we got an
unexpected outcome. Programmers call this type of problem variously a threading issue, a
concurrency issue, or a synchronization issue. These are some of the most frustrating problems
in programming. They may work 99.999% of the time, and the logic seems to be fine at a
glance. But once in a blue moon, our timing is unlucky, and things break in subtle ways. These
concurrency issues can be incredibly tough to spot and fix—a reason to avoid unnecessary
multi-threaded
multi-thread ed programming.
While there are tools that address
address concurrency issues
issues (which we’ll
we’ll discuss in a moment), they
open up the possibility of other problems that can be just as painful.

348 LEVEL 43 THREADS


When code does not use shared data, only uses immutable (read-only) shared data, or
correctly handles its access to shared data, it is considered thread-safe. Not everything needs
to be thread-safe, but you will want to make it so if multiple threads use it.

Locks
The first stepThese
thread-safe. in addressing concurrency
are usually issues
places where weisneed
identifying thesection
an entire code that must to
of code be run
made
to
completion once it begins, as seen from the outside world.
In the sample above, that is the line _number++;. Either thread can run that statement to
completion first, but once a thread starts working with that variable, it must be allowed to
finish before another thread begins.
These sections are called critical sections. Only one thread at a time should be able to enter a
critical section. Identifying critical sections is half the battle.
Once you have identified a critical section, it
i t is time to protect it. This protection is done with
mutual exclusion—a fancy way of saying whichever thread gets there first gets to keep going,
and everybody else must wait for their turn. It is very much like going into a public restroom
and locking the door behind you to prevent others from coming in while you’re using it. (Every
good book needs at least one
on e potty analogy, right?)
e nforcing mutual exclusion, but C#’s lock keyword is the main one.
C# has many options for enforcing
Things that enforce mutual exclusion are called a mutex. A lock is a type of mutex. It is easier
to show how to use a lock statement than to describe it. The code
c ode below illustrates protecting
our _number variable with a lock statement:
SharedData sharedData = new SharedData();
Thread thread = new Thread(sharedData.Increment);
thread.Start();

sharedData.Increment();

thread.Join();

Console.WriteLine(sharedData.Number);

class SharedData
{
private readonly object _numberLock = new object();

private int _number;

public int Number


{
get
{
lock (_numberLock)
{
return _number;
}
}
}

public void Increment()


{
lock (_numberLock)

THREAD SAFETY 349


{
_number++;
}
}
}

sec tions inside of a lock statement. Lock statements are associated with a
Wrap the critical sections
specific object. The first part of the lock statement, lock (_numberLock), is referred to as
acquiring the lock. No thread can proceed past this step until it acquires the lock for the object.
While one thread has the lock, others are temporarily barred from entry. When a thread
reaches the end of the lock statement, the lock is released, and another thread can acquire
it.
Y
You reference-typed object in a lock, but creating a new plain object instance is
ou can use any reference-typed
commonplace. It is one of the few places where a simple object instance is practical.
However,, you want to avoid locking on objects that are not private.
However
Y
You
ou don’t want to make a lock lo ck object
objec t too broadly or too narrowly used. Generally, you will
make a single lock object to protect both read and write access to a single piece of data or
group of related data elements. The above code had two lock statements that used the same
lock object. If we added a Decrement method, we would reuse the same object. If we had
other data in this class that was modified independently,
in dependently, we’
we’d
d use a separate lock object for it.
Threads can acquire multiple locks if needed, but you should avoid these situations when you
can. Imagine needing to use both the keyboard and mouse to do a job, and I grab the
keyboard, and you grab the mouse. You’re waiting for me to release the keyboard while I’m
waiting for you to release
release the mouse. We both spend the rest of our lives waiti
waiting
ng for the other
item to become available. This is called a deadlock and is one of many concurrency-related
issues.
Multi-threaded programming is trickier than single-threaded programming. Avoid it when
you can, but when you can’t, apply the tools and techniques here to make it work. And plan
on a little extra time to hunt down these hard-to-find bugs.

Challenge The Repeating 󰁓tream 150 XP


In Threadia, there is a stream that generates numbers once a second. The numbers are randomly
generated, between 0 and 9. Occasionally, the stream generates the same number more than once in a
row. A repeat number like this is significant—an omen of good things to come. Unfortunately, since the
Uncoded One’s arrival, Threadians haven’t been able to monitor the stream while it produces numbers.
Either the stream generates numbers while nobody watches, or they watch while the stream produces
no numbers. The Threadians offer you the Medallion of Threads willingly and ask you to use it to make
both possible at the same time. Build a program to generate numbers while simultaneously allowing a
user to flag repeats.
Objectives:
• Make a RecentNumbers class that holds at least the two most recent numbers.
numbers.
• Make a method that loops forever, generating random numbers from 0 to 9 once a second. Hint:
Thread.Sleep can help you wait.
• Write the numbers to the console window, put the generated numbers in a RecentNumbers object,
and update it as new numbers are generated.
• Make a thread that runs the above method.

350 LEVEL 43 THREADS


• Wait for the user to push a key in a second loop (on the main thread or another new thread). When
the user presses a key, check if the last two numbers are the same. If they are, tell the user that they
correctly identified the repeat. If they are not, indicate that they got it wrong.
• Use lock statements to ensure that only one thread accesses
accesses the shared data at a time.
LEVEL 44
A󰁓YNCHRONOU󰁓 PROGRAMMING

󰁓peedrun

Asynchronous programming lets tasks run in the background, scheduling continuations or callbacks
to happen with the asynchronous
asynchronous task results when it completes.
• The Task and Task<TResult> classes can be used to schedule tasks to run asynchronously:
Task.Run(() => { ... });
• You can write code to run after the task completes by awaiting the task: await someTask;
• You can only use the await keyword in methods that have the async keyword applied to them:
async Task<int> DoStuff() { ... }

Another model of concurrent programming is asynchronous programming


programming. In this model, you
begin a long-running request and perform other work instead of waiting for it to complete.
When the job finishes,
finishes, you are notified and can continue onwa
onward
rd with its results.
results. The opposite
is called synchronous programming, and it is i s what we have done in the rest of this book.
Y
You
ou use this asynchronous
asynchronous model in your daily life:
• Y
You
ou text a friend to see if they want to meet for lunch. You don’t stare at the screen waiting
for a response, but go on with your day. When your friend responds, you get a notification
on your phone and can plan the rest of your day.
• Y
You
ou order at a fast-food
fast-food restaurant,
restaurant, then sit and talk with your family while it is prepared.
prepared.
When it is ready,
ready, your name or number is is called out, and you get your food and eat it.
• When you apply for a job, you update
update your
your resume
resume and
and submit
submit your application, and then
it is in the business’s
business’s hands. You go back to your life and wait for a response. In the acting
world, getting called back in for a second interview or audition is given the name
callback. That s a word we ll use in a programming context later in this level.
It is also helpful in various programming situations:
• Y
You
ou want to pull down leaderboard data, user data, or even a software update
u pdate from a
server. A network request takes time, and you don’t want the program to hang while
awaiting the response.
• Y
You’re
ou’re saving
saving off a small mountain of data to a file.
• Y
You
ou have a complex, long-running computation behind the scenes.

352 LEVEL 44 ASYNCHRONOUS PROGRAMMING


In short, a long-running task needs to happen, but we want to be notified when it finishes
instead of stopping all work while waiting.

A 󰁓ample Problem
We’ll
We’ll use
u se the following problem to illustrate
illustrate the key points in this level.
l evel. Suppose we want to
run a computation at a space base on Jupiter’s moon Eur
Europa.
opa. It takes time to transmit through
space, so we don’t want to hold up other work while this happens. Here is some code that
represents this task, done synchronously:
int result = AddOnEuropa(2, 3);
Console.WriteLine(result);

int AddOnEuropa(int a, int b)


{
Thread.Sleep(3000); // Simulate light delay. It should be far longer!
return a + b;
}

This program is time-consuming but has has one thing going for it
it:: it is easy to understand. Keep
this simplicity in mind as we explore various asynchronous solutions below.

THREAD󰁓 AND CALLBACK󰁓


Before we get to the best solution, let’s consider a couple of solutions we could do with the
knowledge we already have. These other solutions help us understand the final solution.
We could put this work on a separate thread, as we saw in the previous level. The following
code uses several advanced C# features,
f eatures, but it is about as concise as we can get with threads:
int result = 0;
Thread thread = new Thread(() => result = AddOnEuropa(2, 3));

thread.Start();

// Do other work here


thread.Join();

Console.WriteLine(result);

This code uses delegates, lambdas, and closures (covered


(covered in an optional Side Quest section).
Even though I have gone to great lengths to simplify this code, it is still far uglier than the
synchronous version. Plus, this still has a problem: we don’t know when it gets done! That
comment line is hiding stuff. If it hides less than three seconds of work, we’ll still be waiting at
the Join. If it does more than three seconds of work, it will delay sh
showing
owing the results.
Another approach would be to give the long-running operation a delegate
dele gate to invoke when it
completes. A delegate like this is known as a callback:
AddOnEuropa(2, 3, result => Console.WriteLine(result));

void AddOnEuropa(int a, int b, Action<int> callback)


{
Thread thread = new Thread(() =>
{
Thread.Sleep(3000);

USING TASKS 353


int result = a + b;
callback(result);
});
thread.Start();
}

Once the slow work completes, the thread invokes the delegate to finish the job. At this point,
the main thread can continue to other tasks, knowing that the callback will run when the time
is right. This code is comparatively difficult to read.

U󰁓ING TA󰁓K󰁓
C# has a concept called a task representing a job that can run in the background.
Some tasks produce a result of some sort, while others do not, similar to how a typical method
can have a void return type or return a specific value. Some other languages have a similar
concept but call it a promise. That is a good name for it because it captures
captures the idea of a task
have an int for you yet, but I promise I’ll have one when I finish.”
well: “I don’t have
C# uses two classes for representing asynchronous tasks: Task and Task<T>. Use Task for
tasks that produce no specific result (like a void method) and the generic Task<T> for tasks
that promise an actual result. Both of these are in the System.Threading.Tasks
namespace, which is one of the namespaces automatically included for you in new projects.
If you’re working in older projects, you may need to add a using directive (Level 33) for that
namespace.

Task and Task<T> Basics


Let’s begin our exploration with the basics of the Task and Task<T> classes. Our long-
running AddOnEuropa method can return a Task<int>—a promise of an int in the
future—instead of a plain int:
Task<int> AddOnEuropa(int a, int b) { /* ??? */ }

new task that doesAddOnEuropa


Let’s look at how can make a task. Perhaps the simplest version is to create a
not even run asynchronously—just
asynchronously—just a finished task with a specific va
value:
lue:
Task<int> AddOnEuropa(int a, int b)
{
Thread.Sleep(3000);
int result = a + b;
return Task.FromResult(result);
}

This version has not achieved much. It will all run synchronously on the calling thread and
produce a finished Task object at the end. But creating new, finished tasks has its place.
We want
want this to run in the background asynchronously,
asynchronously, so let’s
let’s do this instead:
instead:
Task<int> AddOnEuropa(int a, int b)
{
Task<int> task = new Task<int>(() =>
{
Thread.Sleep(3000);
return a + b;
});

354 LEVEL 44 ASYNCHRONOUS PROGRAMMING


task.Start();
return task;
}

This version creates a Task<int> object, supplying a delegate (Level 36) for the task to run.
The code above uses a lambda statement (Level 38) to define the task’s
task’s work. The task doesn’t

begin executing this code until you call its Start() method.
It is easy to forget to call Start(), so the alternative below is usually better:
Task<int> AddOnEuropa(int a, int b)
{
return Task.Run(() =>
{
Thread.Sleep(3000);
return a + b;
});
}

The static Task.Run method handles creating a task and starting it all at once. That makes
simpler. Task.Run is the preferred way to begin new
our code simpler. ne w tasks for most situations.

We will revisit this method and make it even better later,


later, but let’s
let’s turn our attention to the
calling side. How do you interact with a Task or Task<T> object returned by a method?
The first thing you can do with a task (Task or Task<T>) is call its Wait method. This
suspends the current thread and waits until the task finishes:
Task<int> additionTask = AddOnEuropa(2, 3);
additionTask.Wait();

For tasks of the generic Task<T> variety, you will probably want the computed result,
accessible through the Result property:
Task<int> additionTask = AddOnEuropa(2, 3);
additionTask.Wait();
int result = additionTask.Result;
Console.WriteLine(result);
If the task is still running, Result will automatically wait for the task to finish, so calling both
Wait() and Result is redundant.
Calling Wait or Result is philosophically the same thing as calling Thread.Join. While
we are using tasks,
tasks, we have
have not received any substantial
substantial asynchronous benefits
benefits yet.
One improvement we can make is to create a second task as a continuation of the first. A
continuation is essentially the same as a callback, just done with tasks. This code takes the
Console.WriteLine statement and puts it into a continuation:
c ontinuation:
Task<int> additionTask = AddOnEuropa(2, 3);
Task addAndDisplay = additionTask.ContinueWith(t => Console.WriteLine(t.Result));

ContinueWith
continuation takes athe
to inspect delegate parameter
results of with it.
the task before type Action<Task>
theContinueWith returns, aallowing the
second task
that won’t begin until the previous task finishes. There is also a generic overload of
ContinueWith for when you want that continuation task to return a value itself:
itself :
Task<double> moreMath = additionTask.ContinueWith<double>(t => t.Result * 2);

USING TASKS 355


There are a lot of other overloads for ContinueWith . We will soon be using a different
approach for tasks, so we’ll skip the details, but they are worth checking out someday.

The async and await Keywords


While the previous
previous code is
is a decent way
way to work with tasks,
tasks, the C# language has some built-in
built-in
mechanisms that make working with tasks more straightforward: the async and await
keywords. The await keyword is a convenient way to indicate that a method should
asynchronously wait for a task to finish and schedule the rest of the method as a continuation.
int result = await AddOnEuropa(2, 3);
Console.WriteLine(result);

Task<int> AddOnEuropa(int a, int b)


{
return Task.Run(() =>
{
Thread.Sleep(3000);
return a + b;
});
}

This code hides one crucial element: you can only use an await in a method marked with
async. This code is in our main method, and the compiler automatically puts async on the
generated method. But in every other method, you’ll need to add that yourself:
async Task DoWork()
{
int result = await AddOnEuropa(2, 3);
Console.WriteLine(result);
}

This version of asynchronous code is much cleaner than the other versions we have seen. Our
main method is the same except for the await. AddOnEuropa is also very similar to the
original synchronous version, aside from the Task.Run and returning a Task<int> instead
of a plaintoint
enough . The compiler
propagate takes carethrown
any exceptions of everything
in the else
task,for you. The
allowing compiler
the is even
awaiting smart
method to
handle them (Level 35) despite potentially occurring on a separate thread.
One interesting thing about that DoWork method is that even though it claims to return a
Task, there is no return statement. Similarly, if we had a method that claimed to return a
Task<int>, we might see it return an int, but not a Task<int>. This is part of the
compiler’s magic to makemake this work correctly. The compiler generates ccode
ode that returns a task;
it is just invisible, hidden behind the await.
Not every method can be an async method. Only certain return types are supported. The
following three are the most common by far: void, Task, or Task<T>.
Use Task<T> when you expect an asynchronous task to produce a result. Use Task when

there is no specific
work afterward. Useresult,
voidbut
onlyyou
forstill
“fireneed to knowtasks
and forget” when the task
where is done
nobody willso youneed
ever candperform
nee to know
when it finishes. If
If a method’s type is void, no other code will be able to await it.
method’s return type
An async method can have many awaits in it. Consider the following two examples:

356 LEVEL 44 ASYNCHRONOUS PROGRAMMING


int result1 = await AddOnEuropa(2, 3);
int result2 = await AddOnEuropa(4, 5);
int result3 = await AddOnEuropa(result1, result2);
Console.WriteLine(result3);

And:

Task<int> firstAdd = AddOnEuropa(2, 3);


Task<int> secondAdd = AddOnEuropa(4, 5);

int result = await AddOnEuropa(await firstAdd, await secondAdd);


Console.WriteLine(result);

Both generally do the same thing (add on Europa three times) and use multiple awaits. The
location of the awaits is important. In the first example, the second call to AddOnEuropa
doesn’t occur until after the first one completes. In the second example, the second call to
AddOnEuropa happens before we await the first task. Those two long-running additions
coincide.
The async and await keywords cause a lot of compiler magic to happen that makes your life
easier.. This is the approach to take when doing asynchronous programming. Y
easier You
ou will still find
times to use things like ContinueWith , Wait, and Result, but most of the time, async and
await are your best bet.

WHO RUN󰁓 MY CODE?


Tasks represent small asynchronous jobs; they are not threads and cannot directly run code.
A thread must
must run every line of code.
Let’s first talk about that Task.Run method, which is the typical entry into asynchronous
code. When you call this method, it hands the task to the thread pool. As we learned in the
previous level, threads are expensive to create and maintain. The thread pool is a collection
of threads on standby, waiting to run small jobs like those represented by tasks. The thread
pool itself (represented by the static System.Threading.ThreadPool class) manages
when to make moremore threads or cleanup underutilized threads.
threads. It does an excellent job,
job, so you
rarely have to worry about tweaking its behavior
be havior.. When a task is given to the thread pool, the
next available thread will run the task’s code. (Note that there are ways you can use Task.Run
that make it run on a dedicated
de dicated thread if a task is especially long-running. Few are that way.)
While the threads from the thread pool are often involved in asynchronous code, let’s take a
more detailed look. Consider this code:
async Task AsynchronousMethod()
{
Console.WriteLine("A");
Task task = Task.Run(() => { Console.WriteLine("B"); });
Console.WriteLine("C");
await task;
Console.WriteLine("D");
}
will run each of the Console.WriteLine s?
Which thread will
The following is important because it is often misunderstood: just because a method returns
a Task or has the async keyword does not make the whole thing run asynchronously! The
original thread that called AsynchronousMethod will do as much work as it possibly can.

WHO RUNS MY CODE? 357


That initial thread will be the one to run Console.WriteLine("A"); . It will also be the
one to call Task.Run, which schedules "B" on the thread pool for later. If task is finished by
the time await task; is called, the calling thread will continue through to the end of the
method and also run the "D" line.
This code creates a task that will probably take some time to run, so most likely, the task will
not be done before await task; is executed. In contrast, a task created with FromResult
is complete the moment it is created. That reinforces the idea that just because a task is
involved does not mean anything is happening asynchronously.
Task.Run schedules the Console.WriteLine("B"); line on the thread pool. A thread
from the thread pool will run it.
The await task; line does not require a thread to wait for it specifically. That is part of the
async and await mechanisms the compiler generates, and it uses this point as a splitting
point for continuations after the initial task.
So if a task must be awaited, who runs the continuation after the await? This question is the
most complicated one to answer because it depends. In some contexts or situations, we want
a specific thread (or set of threads) to run the continuation. For example, most UI frameworks

are single-threaded—only
screen. one thread (“The
In these cases, the continuation needsUI Thread”)
to find can
its way interact
back to thiswith controls on the
UI thread.
Tasks include the concept of a synchronization context. The job of a synchronization context
is to represent one of these situations or contexts where a task originated, to allow the
continuation to find its way home. When there is a synchronization context, the default
behavior is to ensure the continuation runs there. For a UI application, this will put the
continuation back on the UI thread. If there is no synchronization context, which is the case
in a console application, then continuations will typically stay where they are at, and a thread
pool thread will pick up Console.WriteLine("D");.
Most of the time, it doesn’t matter what context a continuation
c ontinuation runs in. Rather than having the
system try to figure out how to get it back to the original context, it is sometimes helpful to tel
telll
the task to keep running any continuations on the thread pool using ConfigureAwait(
false):
await task.ConfigureAwait(false);

The false indicates that it should not return to the initial context, though true is the default.
One final point about who runs async code: some code does not need any thread to run it. For
example, if you make an Internet request (or to Europa, as we pretended with our earlier
example), we don’t need a thread to sit there and use up CPU ccycles
ycles waiting for it. The request
can happen entirely off of our computer! In these cases, the asynchronous stuff isn’t
happening on any thread, which leaves all of our threads free to do other work.
Here is an example. We used earlier in this level to delay for a bit.
Thread.Sleep(3000);
There is another option: Task.Delay(3000) . This returns a Task that won’t finish until the
millisec onds) passes. That is, while Thread.Sleep puts a thread out
specified timeframe (in milliseconds)
of commission for a while, Task.Delay(3000) does not:
Task<int> AddOnEuropa(int a, int b)
{
return Task.Run(async () =>
{
await Task.Delay(3000);

358 LEVEL 44 ASYNCHRONOUS PROGRAMMING


return a + b;
});
}

Note also the await keyword on the lambda. It is a little awkward just hanging out there, but
we do need the async to use await, and this is how you do that with a lambda.

󰁓OME ADDITIONAL DETAIL󰁓


A few other asynchronous
asynchronous programming details are worth a brief discussion.
discussion.

Exceptions
Exceptions (Level 35) bubble up the call stack from where they are thrown. The call stack is
associated with a thread, and each thread has its own call stack. Tasks
Tasks complicate this because
the logic can bounce around among threads. The C# language designers wanted tasks to have
a good exception handling experience, so they put a lot of work into this.
Instead of letting an exception escape a task
t ask directly, any exception that escapes a task’s code

is caught
in byThese
the task. the thread running
exceptions areit. It puts
then the task
rethrown oninto an error
the await state
line andastores
when task isthe exception
awaited. This
allows the awaiting code to handle those exceptions:
try
{
await SlowOperation();
}
catch (InvalidOperationException)
{
Console.WriteLine("I'm sorry, Dave, I'm afraid I can't do that.");
}

What happens if a task throws an exception but is never awaited? When the task is garbage
collected, it will throw on a garbage
ga rbage collection thread and bring down
d own your program. Because
it happens later—sometimes much later—it can be tough to determine what led to the
exception. You typically
typically want to await all tasks that you run and handle any exceptions thrown.
th rown.

Cancellation
Tasks support cancellation for long-running tasks. For tasks that you might want to cancel,
you share a cancellation token with it. Anybody with access to the cancellation token can
request that the task be canceled. But making the request does not cancel it automatically.
The task’s code must periodically check to see if a request has been made and then run any
logic to cancel the operation.
The details of task cancellation are beyond what we can reasonably get into here, but it is
helpful to know that the system facilitates it.

Awaitables
Task and Task<T> are the most common types used for asynchronous programming. But
there are others. You can even define your own. Anything that you can apply the await
keyword to is called an awaitable.

SOME ADDITIONAL DETAILS 359


ValueTask and ValueTask<T> are analogous to Task and Task<T> but are value types
instead of reference types (Level 14).
14) . These are far less common but useful if you’re worried
about memory usage and also typically know the result without running asynchronously.
IAsyncEnumerable<T> lets you build and process a collection a little at a time as results
become available. (There is even special syntax when using this in a foreach loop.)
These aren’t the only options, but between these, Task, and Task<T>, they are the most
common.

Limitations
async and await lead to complex code behind the scenes. There are some limitations to
combining them with certain other C# features. For example, you can’t await something in
a lock statement. These limitations are nuanced, so rather than writing them all out here,
just know that
that they exist, and the compiler will point out any trouble spots.
spots.

More Information
Asynchronous programming is a tricky
tricky area
area of C#. We’ve
We’ve covered the basics,
basics, but there is plenty
more to learn. If you plan to do a lot with async and await, I recommend getting either or
both of the following short books:
books :
• Async in C# 5.0, by Alex Davies.
• Concurrency in C# Cookbook, Second Edition, by Stephen Cleary.

Knowledge Check Async 25 XP


Check your knowledge with the following questions:
1. What keyword indicates that a method might schedule some of its work
work to run asynchronously?
2. What keyword
keyword indicates that code
code beyond that point should run once the task has finished?
3. Name three return types that with the async keyword.
that can be used with

4. True/False. Code is always faster when run asynchronously.


5. What return type would be best for an async method that does the following:
a. The work does not produce a value, but you need to know when it finishes.
b. You do not care when the task completes.
c. The task creates a result that you need to use afterward.
Answers: (1) async. (2) await. (3) void, Task, Task<TResult>, etc. (4) False. (5) a: Task. b: void. c: Task<T>.

Challenge Asynchronous Random Words


Words 150 XP
On the Island of Tasken, you meet Awat, who tells you that being a True Programmer can t be all that
hard. His ancestors have been the stewards of the Asynchronous Medallion, yet Awat uses it as a food
dish for his cat. “A thousand monoids with a thousand random generators will also eventually produce
‘hello world’!” he claims. Indeed, they could, but you know it would take a while. With tasks, you can allow
a human to pick a word and randomly generate the word asynchronously. Doing this will show Awat how
long it will take to randomly generate the words “hello” and “world,” convincing him that a Programmer’s
skills mean something.
Objectives:

360 LEVEL 44 ASYNCHRONOUS PROGRAMMING


• Make the method int RandomlyRecreate(string word). It should take the string’s length and
generate an equal number of random characters. It is okay to assume all words only use lowercase
letters. One way to randomly generate a lowercase letter is (char)('a' + random.Next(26)).
This method should loop until it randomly generates the target word, counting the required
attempts. The return value is the number of attempts.

Make the method Task<int> RandomlyRecreateAsync(string word) that schedules the


above method to run asynchronously (Task.Run is one option).
• Have your main method ask the user for a word. Run the RandomlyRecreateAsync method and
await its result and display it. Note: Be careful about long words! For me, a five-letter word took
several seconds, and my math indicates that a 10-letter word may take nearly two years.
• Use DateTime.Now before and after the async task runs to measure the wall clock time it took.
Display the time elapsed (Level 32).
32).

Challenge Many Random Words


Words 50 XP
Awat is impressed with what you did in the last challenge
chall enge but thinks it could
coul d be better. “Why not generate

‘hello’ and ‘world’ in parallel?” he asks. “You do that, and I’ll let you take this medallion off of me.”
Objectives:
• Modify your program from the previous challenge to allow the main thread to keep waiting for the
user to enter more words. For every new word entered, create and run a task to compute the
t he attempt
count and the time elapsed and display the result, but then let that run asynchronously while you
wait for the next word. You can generate many words in parallel this way. Hint: Moving the elapsed
time and output logic to another async method may make this easier.
LEVEL 45
DYNAMIC OBJECT󰁓

󰁓peedrun

The dynamic type instructs the compiler not to check a variable’s type. It is checked while running
instead. This is useful for dynamic objects whose members are not known at compile time.
• Avoid dynamic objects, except when they provide a clear, substantial benefit.
• ExpandoObject is best for simple, expandable objects.
• Deriving from DynamicObject allows for greater control in constructing dynamic members.

Types are a big deal in C#. We spend a lot of time designing new classes and structs to get
precisely the right effect. We worry about inheritance hierarchies, carefully cast between
types, and fret over parameter and return types.
confus ed with the static keyword), meaning
In C#, types are considered “static” (not to be confused
typesmeans
that don’t change as thecan
the compiler program
makeruns. You
strong cannot add
guarantees new
that methods
objects to a class
will truly haveor object,
the but
methods
and other members you invoke. This fact makes C# a statically typed language, or you could
say that the compiler does static type checking.
The primary advantage of this is that the compiler can guarantee that everything you do is
safe. If you call a method on an object, the compiler makes sure that the method exists. Any
failures of this nature are caught by the compiler
c ompiler before your program even starts.
There are two variations in the opposite camp. First, we can have dynamic type checking. With
dynamic type checking, variables (including parameters and return types) have no fixed type
associated with them. The compiler cannot ensure any given member will exist.
ex ist. In exchange,
there is usually less ceremony and formality around types. The second variation is dynamic
objects. With dynamic objects, the objects themselves have no formal structure, sometimes
being
addeddefined at creation
and removed as thetime. Other
program times, they even allow methods and properties to be
runs.
C# can support both dynamic type checking and dynamic objects. But a word of caution is in
order: these can be a useful tool, but the overwhelming majority of your C# code should be
statically typed. Keep dynamic typing to a minimum, and only in circumstances where the
benefits are clear and significant.

362 LEVEL 45 DYNAMIC OBJECTS

DYNAMIC TYPE CHECKING


With C#’s standard static type checking,
c hecking, the compiler can look at code and ensure that only
objects or values of the right type are placed in a variable and that it only uses members that
the type has. For example, in the code below, the compiler can ensure that text is only
assigned string values, verify that string has a Length property, and check that Console
has a WriteLine method with a single int parameter:
string text = "Hello, World!";
Console.WriteLine(text.Length);

Y
You ariable by using the dynamic type:
ou can have C# perform dynamic type checking for a vvariable
dynamic text = "Hello, World!";
Console.WriteLine(text.Length);

Any variable can use this type. It tells the compiler to skip static type checking and instead
make those checks while the program is running. The sample below abuses this, attempting
operations that we know the string object won’t have:
dynamic text = "Hello, World!";
text += 13;
text *= Math.PI;
Console.WriteLine(text.PurpleMonkeyDishwasher);

Each of these fails as the program is running with a RuntimeBinderException . On the


other hand, this will work:
dynamic mystery = "Hello, World!";
Console.WriteLine(mystery.Length);
mystery = 2;
mystery += 13;
mystery *= 13;
Console.WriteLine(mystery);

The contents of mystery change from a string to an int! All of the operations we attempt
are legitimate for whichever object mystery contains when the operation is used.
Behind the scenes, mystery becomes an object. The compiler will record metadata about
method calls and use that metadata as the program is rrunning
unning to look up the correct member.
Remember that if you treat a value type as an object, it is boxed (Level 28).28). That has an
impact on how you use dynamic with value types.

DYNAMIC OBJECT󰁓
Dynamic objects are objects whose structure is determined while the program is running. In
some cases, these dynamic objects can even change structure over the object s lifetime,
adding and removing members. This is not what C# was designed for, but C# supports it for
situations where this model can produce much simpler code. This primarily happens when
using things made in a dynamic programming language or dynamic data formats.
Dynamic objects are built by implementing the IDynamicMetaObjectProvider interface.
Implementing this interface tells the runtime how to look up properties, methods, and other
members dynamically. But IDynamicMetaObjectProvider is extremely low-level and is
both tedious and error-prone. Fortunately, there are some other tools you can use in most

EMULATING DYNAMIC OBJECTS WITH DICTIONARIE


DICTIONARIESS 363
We’ll focus on those other options and skip the details of IDynamicMeta
situations instead. We’ll
ObjectProvider , which deserves an entire book.

EMULATING DYNAMIC OBJECT󰁓 WITH DICTIONARIE󰁓


Before we start building dynamic objects, let’s talk about how you could use a dictionary to
emulate a dynamic object with flexible members. The reason I mention this is two-fold. First,
sometimes, a plain dictionary is more straightforward than a dynamic object. Second, most
dynamic objects will use a dictionary or dictionary-like structure to represent themselves
behind the scenes, so the pattern is helpful
hel pful to understand.
The following creates a Dictionary<string, object> as an emulation of a dynamic
object. The key acts as the name of the property, and the value is the contents of that pro
property:
perty:
Dictionary<string, object> flexible = new Dictionary<string, object>();
flexible["Name"] = "George";
flexible["Age"] = 21;

Y
You cl ass with a Name and Age property. That would be cleaner
ou could imagine designing a class

code, but
do that in this
with case, we can add more things as we se
a class. seee fit as the program runs. You could not
Adding methods is more awkward,
awkward, but a delegate (Level 36) can make this possible:
flexible["HaveABirthday"] = new Action(
() => flexible["Age"] = (int)flexible["Age"] + 1);

Invoking this pseudo-method is also awkward, but it does work:


work :
((Action)flexible["HaveABirthday"])();

This code retrieves the Action object from the dictionary, casts it to an Action, and then
invokes it (the parentheses at the end).
remove elements dynamically using the Remove method.
We could even remove
The syntax is inconvenient, especially for method calls, but it works. The other two
approaches we will look at are more refined in this regard.

U󰁓ING EXPANDOOBJECT
A second choice for a dynamic object is the ExpandoObject class in the System.Dynamic
namespace. ExpandoObject is essentially just the dictionary approach we just saw, but with
better syntax:
using System.Dynamic; // This namespace is not automatically included. Add it.

dynamic expando = new ExpandoObject();


expando.Name = "George";
expando.Age = 21;
expando.HaveABirthday = new Action(() => expando.Age++);

expando.HaveABirthday();

364 LEVEL 45 DYNAMIC OBJECTS


The syntax here is drastically improved. Adding properties is as simple as assigning a value to
them. The syntax for adding and invoking a method got much better as well. For calling a
method, it is identical to regular method calls! This cleaner syntax is because of that dynamic
type and because ExpandoObject implements IDynamicMetaObjectProvider to
define how its members should be found and used.

Interestingly, ExpandoObject implements IDictionary<string, object>, though it


does so explicitly. If you cast an ExpandoObject to IDictionary<string, object>,
you can use it as a dictionary to do things like enumerating
enume rating all of its members or removing
properties:
var expandoAsDictionary = (IDictionary<string, object>)expando;

foreach(string memberName in expandoAsDictionary.Keys)


Console.WriteLine(memberName);

expandoAsDictionary.Remove("Age"); // Remove the Age property.

EXTENDING DYNAMICOBJECT
A second option for dynamic objects is to derive from the DynamicObject class. Deriving
from DynamicObject is trickier than using ExpandoObject , but it also gives you much
more control over the details. It is an abstract class with a pile of virtual methods that you can
override. You
You use it by creating a derived
der ived class and overriding methods for any type of member
you want to have dynamic access to. For example, you overr ide TryGetMember if you want
override
to dynamically get property values, TrySetMember if you want to set property values
dynamically, and TryInvokeMember if you want to be able to invoke i nvoke methods dynamically.
Override each type of member that you want dynamic control over. over.
The example below is on the extreme simple end. It creates a dynamic object where properties
and their string-typed values are supplied in the constructor and overrides
TryGetMember and TrySetMember to allow users of the class to use those as properties:
public class CustomObject : DynamicObject
{
private Dictionary<string, string> _data;

public CustomObject(string[] names, string[] values)


{
_data = new Dictionary<string, string>();

for (int index = 0; index < names.Length; index++)


_data[names[index]] = values[index];
}
public override bool TryGetMember(GetMemberBinder binder, out object? result)
{
if (_data.ContainsKey(binder.Name))
{ result = _data[binder.Name];
return true;
}
else
{
result = null;
return false;

WHEN TO USE DYNAMIC OBJECT VARIATIONS 365


}
}

public override bool TrySetMember(SetMemberBinder binder, object? value)


{
if (!_data.ContainsKey(binder.Name)) return false;

// ToString(), in case it isn't already a string.


_data[binder.Name] = value.ToString();
return true;
}
}

Using a dictionary, as shown here, is a common trend with dynamic objects.


The constructor is relatively straightforward,
straightforward, only taking the p
property
roperty names and their values
and storing them in a dictionary.
The overrides for TryGetMember and TrySetMember are more interesting. Each has a
binder parameter that supplies the information about what the calling code
c ode is trying to use.
In particular, binder.Name is the specific name they are searching for. We use that in both

TryGetMember
If it exists, we return TrySetMember
andits TryGetMember
associated valuetoinlook for a propertyand
withupdate it in TrySetMember
that name in the dictionary..
Both of these are expected to return whether the attempt to access the member was successful
or not. In C#, a failure leads to a RuntimeBinderException.
With this object, you can dynamic
dynamically
ally use its properties:
properties:
dynamic item = new CustomObject(new string[] { "Name", "Age" },
new string[] { "HAL", "9001" });
Console.WriteLine($"{item.Name} is {item.Age} years old.");

TryGetMember and TrySetMember dynamically get and set properties. Override Dynamic
Object’s other members to get dynamic behavior for other member types, including
methods (TryInvokeMember ), operators (TryUnaryOperation and TryBinary
Operation), indexers (TryGetIndex and TrySetIndex ), and more.

WHEN TO U󰁓E DYNAMIC OBJECT VARIATION󰁓


C# is geared toward static typing. Choose static typing with regular classes and structs when
you can. Don’t forget
forget that a dictionary is also a choice, and often a simpler one.
When using dynamic objects, use ExpandoObject when you can. It is the simplest to use.
that’s not enough, derive a new class from DynamicObject .
When that’s

Challenge Uniter of Adds 75 XP


“This city has used the Four Great
Great Adds for a million clock
c lock cycles.
cycles . But legend foretells a True Programmer
who could unite them,” the Regent of the City of Dynamak tells you. She shows you the four great adds:
public static class Adds
{
public static int Add(int a, int b) => a + b;
public static double Add(double a, double b) => a + b;
public static string Add(string a, string b) => a + b;
public static DateTime Add(DateTime a, TimeSpan b) => a + b;
}

366 LEVEL 45 DYNAMIC OBJECTS


“The code is identical, but the four types involved demand four different methods. So we have survived
with the Four Great Adds. Uniting them would be a sign to us that you are a True Programmer.” With
dynamic typing, you know this is possible.
Objectives:

Make a single Add method that can


c an replace all four of the above methods using dynamic.
• Add code to your main method to call the new method with two ints, two doubles, two strings,
and a DateTime and TimeSpan, and display the results.
• Answer this question: What downside do you see with using dynamic here?

Challenge
Challe nge The Robot Factory 100 XP
The Regent of Dynamak is impressed with your dynamic skills and has asked for your help to bring their
robot factory back online. It was damaged in the Uncoded One’s arrival. Robots are manufactured after
collecting their details, all of which are optional except for a numeric ID. After the information is
collected, the robot is created by displaying the robot’s details in the console. Here are two examples:

You arewant
Do you producing robot
to name this#1.
robot? no
Does this robot have a specific size? no
Does this robot need to be a specific color? no
ID: 1
You are producing robot #2.
Do you want to name this robot? yes
What is its name? R2 D2
Does this robot have a specific size? yes
What is its height? 9
What is its width? 4
Does this robot need to be a specific color? yes
What color? azure
ID: 2
Name: R2-D2
Height: 9
Width: 4
Color: azure

In exchange, she offers the Dynamic Medallion and all robots the factory makes before you fight the
Uncoded One.
Objectives:
• Create a new dynamic variable, holding a reference to an ExpandoObject.
• Give the dynamic object an ID property whose type is int and assign each robot a new number.
• Ask the user if they want to name the robot, and if they do, collect it and store it in a Name property.
• Ask if they want to provide a size for the robot. If so, collect a width and height from the user and
store those in Width and Height properties.
• Ask if they want to choose a color for the robot. If so, store their choice in a Color property.
• Display all existing properties for the robot to the console window using the following code:
foreach (KeyValuePair<string, object> property in (IDictionary<string, object>)robot)
Console.WriteLine($"{property.Key}: {property.Value}");

• Loop repeatedly to allow the user to design and build multiple robots.

LEVEL 46
UN󰁓AFE CODE

󰁓peedrun
• “Unsafe code” allows you to reference and manipulate memory locations directly. It is primarily used
for interoperating with native code.
• You can only use unsafe code in unsafe contexts, determined by the unsafe keyword.
• Pointers allow you to reference a specific memory address. C# borrows the *, &, and -> operators
from C++.
• fixed can be used to pin managed references in place so a pointer can reference them.
• The stackalloc keyword allows you to define local variable arrays whose data is stored on the
stack. A fixed-size array does a similar thing for struct fields.
• sizeof can tell you the size of an unmanaged type: sizeof(int), sizeof(FancyStruct).
• nint and nuint are native-sized integer types that compile differently depending on the
architecture.
• You can invoke native/unmanaged code using Platform Invocation Services (P/Invoke).

A key feature of C# and .NET is that it manages memory


m emory for you (Level 14).
14). But one of C#’s
strengths is that it allows you to step out of this world and enter the unmanaged, “unsafe”
world. Among other things, this makes it easy to work with code written in languages that do
not use .NET, such as C or C++. Unmanaged code is sometimes called native code. Many C#
developers will never touch unmanaged code, and even if you do, it likely won’t be at the start
of your C# journey. Yet C#’s ability to jump to the unmanaged world when conditions
necessitate is a compelling reason to choose C# over other languages.
As a new C# programmer,
programmer, the only
onl y lesson
l esson that matters now is knowing that this is possible.
Y
You
ou do not need to come
com e away with a deep mastery of unsafe or unmanaged code. If you are
in a skimming or skipping mood, this level is a good one to do that on. The basics covered in
this level will help shed light on how C# can interact with objects in memory that are not
managed by the runtime and the garbage collector. This
This level is an overview of the basics. The
ins and outs of unsafe code deserve an entire book.

368 LEVEL 46 UNSAFE CODE

UN󰁓AFE CONTEXT󰁓
Most C# code does not need to jump out of the realm of managed code and managed memory.
However, C# does support certain “unsafe operations”—data types, operators, and other
actions that allow you to reference, modify, and allocate memory directly. These operations
are called unsafe code. Despite the name, it is not inherently dangerous. However, the
compiler and the runtime cannot guarantee type and memory safety like they usually can. A
less common but perhaps more precise name for it is unverifiable code.
Unsafe code can only be used in an unsafe context. You can make a type, method, or block of
code an unsafe context using the unsafe keyword. This requirement ensures programmers
use unsafe operations intentionally, not accidentally.
Making a block of code into an unsafe context is shown here:
public void DoSomethingUnsafe()
{
unsafe
{
// You can now do unsafe stuff here.
}
}
To make a whole method or every member of a type unsafe, apply the unsafe keyword to the
method or type definition itself:
public unsafe void DoSomethingUnsafe()
{
}

And:
public unsafe class UnsafeClass
{
}

But even that is not enough. You must also tell the compiler to allow unsafe code into your
project’s configuration file (the .csproj file). However, the
program. This is typically done in the project’s
easiest way to reconfigure a project like this is to just put an unsafe context in your code. When
the compiler flags it, use the Quick Action to enable unsafe code in the p project.
roject.

POINTER TYPE󰁓
In an unsafe context, you can create variables that are pointer types. A pointer contains a raw
memory address where some data of interest presumably lives.
lives. The concept is nearly the same
as a reference, though references are managed by the runtime, which sometimes moves the
data around in memory to optimize memory usage. These are a different beast than both
value types and reference
reference types. The garbage
garbage collector manages references but not pointers.

Y
You type with a * by the type:
ou declare a pointer type
int* p; // A pointer to an integer.

Y
You
ou can create a pointer to any unmanaged type. An unmanaged type is essentially any value
type that does not contain references. That includes all of the numeric types, char, bool,
enumerations, and any struct that does not have a reference-typed member, as well as
pointers (pointers to pointers).

FIXED STA
STATEMENTS
TEMENTS 369
C# borrows three operators from C++ for working with pointer types:types : The address-of operator
(&) for getting the address of a variable, the indirection operator ( *) for dereferencing a pointer
to access the object it points to, and the pointer member access operator ( ->), for accessing
members such as properties, methods, etc. on a pointer type object. These are shown below: b elow:
int x;
unsafe
{
// Address-Of Operator. Gets the address of something and returns it.
// This gets the address of 'x' and puts it in 'pointerToX'.
int* pointerToX = &x;

// Indirection Operator: Dereferences the pointer, giving you the object at


// the location pointed to by a pointer. This puts a 3 in the memory location
// pointerToX points at (the original 'x' variable).
*pointerToX = 3;

// Pointer Member Access Operator: allows access to members through a pointer.


pointerToX->GetType();
}

This
= 3;code
and illustrates
x.GetType();how towould
use pointers, but
have been
bee if it weren’t
n much for a desire to show that, a simple x
cleaner.

FIXED 󰁓TATEMENT󰁓
Y
You
ou may need to get a pointer to some part of a m
managed
anaged object. This is pos
possible
sible but requires
requires
some work. Remember, the runtime and garbage collector
col lector manage reference types. They may
move data around from one memory location to another as needed. Since a pointer is a raw
memory address, we cannot allow a pointer’s target to shift out from under us. A fixed
statement tells the runtime to temporarily pin a managed object in place so that it doesn’t
move while we use it.
Suppose we have a Point class with public fields like this:
public class Point
{
public double X;
public double Y;
}

To get an instance’s X or Y field requires pinning it to get a pointer to it:


Point p = new Point();

fixed (double* x = &p.X)


{
(*x)++;
}

The garbage collector will not move p while that fixed block runs, ensuring our pointer will
continue referring to its intended data.
A fixed statement demands declaring a new pointer variable; you cannot use one defined
earlier in the method. A fixed statement can declare multiple new variables of the same type
by separating them with commas: fixed (double* x = &p.X, y = &p.Y) { ... }

370 LEVEL 46 UNSAFE CODE

󰁓TACK ALLOCATION󰁓
C# arrays are reference types. A variable of an array type holds only a reference, while the data
lives on the heap somewhere. This behavior is not just tolerable but desirable in nearly all
situations. But when needed, you can ask the program to allocate an array local variable on
the stack instead of the heap with the stackalloc keyword:
public unsafe void DoSomething()
{
int* numbers = stackalloc int[10];
}

Y
You
ou can only do this in an unsafe context, only for local
l ocal variables, and only for unmanaged
types. When the stackalloc line is reached, an additional 40 bytes (4 bytes per int for 10
ints) will be allocated on the stack for this method. When the code returns from
DoSomething() , this memory is freed automatically when the method’s method’s frame on the stack
is removed. It will not require the garbage collector to deal with it.

FIXED-󰁓IZE A
When RRAY󰁓with code from unmanaged languages, such as C and C++, you sometimes
working
want to share entire data structures. A complication arises when a struct holds an array with
a reference to data that lives elsewhere. The struct’s data is not contiguous, making it
impossible to share with unmanaged code. Consider this struct with an array reference:
public struct S
{
public int Value1;
public int Value2;
public int[] MoreValues;
}

The alternative is a fixed-size array or fixed-s ize buffer, which must always be the same size,
fixed-size
but that stores its data within the struct instead of elsewhere on the heap:
public unsafe struct S
{
public int Value1;
public int Value2;
public fixed int MoreValues[10];
}

This struct will hold


hol d all of its data together, with no references pointing elsewhere.
The runtime does not do index bounds checking on these arrays, as it does for regular arrays.
You could access MoreValues[33] without throwing an exception, accessing random
You
memory. Going past the end of an
a n array can cause serious problems, hence the name “unsafe.”

THE SIZEOF OPERATOR


The sizeof operator allows you to refer to the size in bytes of an unmanaged type without
having to do the math yourself:
byte[] byteArray = new byte[sizeof(int) * 4];

THE NINT AND NUINT TYPES 371


This type is a constant value for the built-in types like int, double, and bool. The compiler
will even replace sizeof(int) with a 4 when it compiles. For these situations, you can use
sizeof anywhere in your code. This is a convenient tool if you forget how big something is:
Console.WriteLine(sizeof(double));

The main use of sizeof is in unsafe code to help you compute the size of more complex
objects. For the non-built-in types, this can only be used in an unsafe context. sizeof is
especially useful when dealing with complex structs because their sizes are not always
obvious. For example, sizeof(long) is 8 and sizeof(bool) is 1, but what is sizeof(
LongAndBool) ?
struct LongAndBool
{
long a;
bool b;
}

It is 16, not 9! The system may add padding bytes to the beginning, middle, and end of the
struct. The CPU typically deals with blocks larger than a single byte (64 bits or 8 bytes on a 64-
bit
Themachine), and lining
sizeof operator up data
reveals the on those
actual boundaries
size with padding makes it more efficient.
of the struct.
C# does provide the tools to let you explicitly layout the members of a struct in memory for
the rare cases when you need it.

THE NINT AND NUINT TYPE󰁓


In C#, the basic integer types are always the same size. But in native code, sometimes, the size
of an int depends on the hardware
hardware being used. An int might be 32 bits on a 32-bit machine
and 64 bits on a 64-bit machine. To address this, you can use the nint (native int) or nuint
(native uint) types. These are essentially integers that compile to different types depending on
the hardware,
hardware, which helps your C# code better align with the native code it is trying to call.

CALLING NATIVE CODE WITH PLATFORM INVOCATION 󰁓ERVICE󰁓


Platform Invocation Services, or P/Invoke for short, allows your managed C# code to directly
call native (unmanaged, non-.NET) code. It lets you call native code, including C and C++
libraries, as well as operating system calls. The managed world of C# and the unmanaged
world are quite different from each other,
other, which means conversion between the two and
marshaling data across this boundary with P/Invoke can get complicated.
Here is a simple example. Let s say we have some C code that defines an add function that
adds two integers together. In your C code, you would ensure this method is exported from
your DLL (a topic for a C book, not a C# book). In your C# code, you could produce
produce a wrapper
around this function with the extern keyword and the DllImport attribute (Level 47): 47):
public static class DllWrapper
{
[DllImport("MyDLL.dll", EntryPoint="add")]
internal static extern int Add(int a, int b);
}

372 LEVEL 46 UNSAFE CODE


The extern keyword indicates the body of the method is defined outside of your C# code.
You don’t supply a body at all, but just end the line with a semicolon. The DllImport
You
attribute (Level 47) indicates the native library and method to use when Add is called.
(EntryPoint is not required if the method names are an exact match.)
All extern methods must be static, and you generally do not want to make them public for
security reasons.
A call to DllWrapper.Add(3, 4) would send those two int parameters over to the
unmanaged library, invoke the native add method, and return the result.
This example is a trivial one; real examples tend to be much more complicated. Even just
getting the signatures and configuring the DllImport attribute can be a massive headache.
The website http://www.pinvoke.net can help get this right.

Knowledge Check Unsafe Code 25 XP


Check your knowledge with the following questions:
1. True/False. Unsafe code is inherently dangerous.

2. What keyword makes something an unsafe context?


3. What keyword pins a reference in place?
4. How do you denote a type that is a pointer to an int?

Answers: (1) False. (2) unsafe. (3) fixed. (4) int*.


LEVEL 47
OTHER LANGUAGE FEATURE󰁓

󰁓peedrun
• yield return produces an enumerator without creating a container like a List.
• const defines compile-time constants that can’t be changed.
• Attributes let you apply metadata to types and their members.
Attributes
• Reflection lets you inspect code while your program is running.
• The nameof operator gets a string representation of a type or member.
• The protected internal and private protected accessibility modifiers are advanced
accessibility modifiers that give you additional control over who can see a member of a type.
• The bit shift operators let you play around with individual bits in your data.
• IDisposable lets you run custom logic on an object before being garbage collected.

C# defines a variety of preprocessor directives to give instructions to the compiler.


• You can access command-line arguments supplied to your program when it was launched.
• Classes can be made partial, allowing them to be defined across multiple files.
• The (dangerous) goto keyword allows you to jump to another location in a method instantly.
• Generic variance governs how a type parameter’s inheritance affects the generic type itself.
• Checked contexts will throw exceptions when they overflow. Unchecked contexts do not.
• Volatile fields ensure that reads and writes happen in the expected order when accessed across
multiple threads.
This is the final level that deals directly with C# language features. It covers various features
that didn’t have
themselves. a home
Others in otherbig
are relatively levels. Someused,
but rarely are small and
so they don’t worth
weren’t deserve a whole
many pageslevel to
in this
book.

374 LEVEL 47 OTHER LANGUAGE FEATURES

ITERATOR󰁓 AND THE YIELD KEYWORD


The concept of IEnumerable<T> and IEnumerator<T> is straightforward: present items
one at a time until you reach the end of the data. Most of the time, when IEnumerable<T>
pops up, it is because you are using some existing collection type, such as arrays, List<T>,
or Dictionary<T>. Iterators are another way to create an IEnumerable<T> . While you
could implement the IEnumerable<T> interface (it is small), many C# programmers find
iterators easier.
easier. An iterator is a special method that w
will
ill produce or “yield” values one at a time.
Here, the word “yield” means “to produce or provide,” as you might see in an agricultural
context. The following is an iterator method, which produces an IEnumerable<T> of the
numbers 1 through 10:
IEnumerable<int> SingleDigits()
{
for (int number = 1; number <= 10; number++)
yield return number;
}

This returns an IEnumerable<int> , which you can use in a foreach loop:

foreach (int number in SingleDigits())


Console.WriteLine(number);

Iterator methods are evaluated lazily. It does not produce the entire collection of items
instantly. Only enough code in SingleDigits will run to encounter the next yield
return statement as items are requested. When the foreach loop needs the first item, this
method will run just enough code to reach the yield return. The next time a number is
needed, execution within the method will resume where it left off and go until yield return
is reached a second time. This repeats until all ten numbers have been produced and the
SingleDigits method ends, or the foreach loop quits asking for more numbers.
Y
You iterator, including multiple yield return statements.
ou can have any logic in an iterator,
A yield break; statement will cause the sequence to end without getting to the method’s
closing curly brace.
Iterators do not have to ever complete. You can generate a sequence
se quence of items indefinitely. For
example, this code will generate the sequence -1, +1, -1, +1, … forever:
IEnumerable<int> AlternatingPattern()
{
while (true)
{
yield return -1;
yield return +1;
}
}

There will always be more items to generate, no matter how far you go. And notably, this
doesn’t take up any memory because the items are produced on demand.
On the other hand, trouble is lurking if you attempt to materialize the entire IEnumerable<
int> into a list or expect a foreach loop to find the end. This runs out of memory:
List<int> numbers = AlternatingPattern().ToList();

And this code will never terminate:

CONSTANTS 375
foreach (int number in AlternatingPattern())
Console.WriteLine(number);

Async Enumerables
An async enumerable lets us combine iterator methods with the tasks we learned about in
Level 44. Suppose you have a method that returns the contents of a website such as this:
public async Task<string> GetSiteContents(string url) { ... }

By making an iterator method with the return type IAsyncEnumerable<T> (in the
System.Collections.Generic namespace), you can yield return an awaited task:
public async IAsyncEnumerable<string> GetManySiteContents(string[] manyUrls)
{
foreach (string url in manyUrls)
yield return await GetSiteContents(url);
}

The magic happens when you use an IAsyncEnumerable<T> with a foreach loop:

string[] urls = new string[] { "http://microsoft.com"


"http://google.com", "http://amazon.com",
};

await foreach (string url in GetManySiteContents(urls))


Console.WriteLine(url);

This code has the complexity of tasks, iterators, and foreach loops combined, but it allows
you to process the results as
as they start coming back without waiting for the
the entire collection.

CON󰁓TANT󰁓
A constant (const for short) is a variable-like construct that allows you to give a name to a
specific value. Perhaps the definitive example of this is the constant PI defined in the Math
class, which has a definition that looks something like this:
public static class Math
{
public const double PI = 3.1415926535897931;
}

A constant is defined with the const keyword, along with a type, a name, and a value. The
value of a constant must be computable
c omputable by the compiler,
compiler, which means
m eans most constants will
use a literal value, as PI does above. But you could also define a constant
con stant like this:
public const double TwoPi = Math.PI * 2;

Since Math.PI is a constant, the compiler can use it when computing TwoPi at compile time.
Constants are usually named with UpperCamelCase, but a few people like CAPS_WITH_
UNDERSCORES. PI is an example of the second. Most of the Base Class Library uses the first
convention, so PI is an inconsistency.
Despite their appearance, constants are not variables. YYou
ou cannot assign values to them, aside
from what the compiler gives it. They are static by nature, so you do not and cannot mark them
static yourself.

376 LEVEL 47 OTHER LANGUAGE FEATURES


How do constants compare with a static readonly variable?
The compiler replaces usages of a constant with the constant’s value as it compiles the code.
That means double x = PI; is turned into double x = 3.1415926535897931; by
the compiler. The variable loses its association with the original constant. Often, this is not a
big deal. There are, however, a few scenarios where this ends up causing problems. The
general rule is to only use constants for things that will never change. Math.PI is a good
example since the numeric value for π will never change. In other scenarios, a static
readonly variable is usually preferable. Because a constant does not need to be looked up
as the program is running, a constant can be slightly faster, which might also be a deciding
factor in rare circumstances.
In general, the flexibility of readonly outweighs any other advantage of a constant. For
example, you can define a readonly variable statically or one per instance. You can also
assign values that must be computed at runtime, such as assigning a new instance, which
cannot be done with a constant.

TTRIBUTE󰁓
A Compiled C# code retains a lot of rich information about the code itself. You can add to that
richness by using attributes, which attach metadata to different parts of your code. Tools
Tools that
inspect or analyze your code, including the compiler and the runtime, can access this
metadata and adapt their behavior in response. For example:
• Y
You
ou mark a method as obsolete by applying an attribute. When you do this, the compiler
will see it and emit warnings
warnings or errors when somebody uses tthe
he outdated code.
• Y
You
ou can mark methods as test methods, which a testing framework can run to ensure your
code is still doing what you expected it to do.
• Y
You
ou can apply attributes to a class’s
class’s properties that allow a file writing library to
automatically dig through the object in memory and save it without writing custom file
code for each object by hand.

Let’s show how to apply attributes with that first example. The Obsolete attribute indicates
that a method is outdated and should not be used anymore. In a small program, you would
just delete the method and fix any code that
that uses it.
it. It may
may take a while
while to clean everything up
in a large program, so marking it obsolete can be a step in a long journey to eliminate it.
To apply the Obsolete attribute to an outdated method, place the attribute above the
method inside of square brackets:
[Obsolete]
public void OldDeadMethod() { }
Attributes are like placing little notes on the different parts of the code. They survive the
compilation process, so tools working with your code can see these attributes and use them.
The compiler notices the Obsolete attribute and produces compiler warnings in any place
where OldDeadMethod is called.
parameters that you can set. The Obsolete attribute has two: a string
Many attributes have parameters
to display as an error message and a bool that indicates whether to treat this as an error
(true) or a warning (false).
[Obsolete("Use NewDeadMethod instead.", true)]
public void OldDeadMethod() { }

ATTRIBUTES 377
Configured like this, you would see an error with the message “Use NewDeadMethod instead.”
Y
You
ou can use multiple
multiple attributes in either of the following
following two ways:
[Attribute1]
[Attribute2]
public void OldDeadMethod() { }

Or:
[Attribute1, Attribute2]
public void OldDeadMethod() { }

Attributes on Everything
The attributes above are applied to a method, but you can use them on almost any code
element. They are frequently applied to a class or other type definition.
[Obsolete]
public class SomeClass { }

In most cases, attributes are placed immediately before the code element they are for, but in
some cases, there is no obvious “immediately before” spot, or that spot is ambiguous. There
are special keywords that you can put before the attribute to make it clear in these cases. For
example, this explicitly states that the Obsolete attribute is for the method (the default):
[method: Obsolete]
public void OldDeadMethod() { }

This one applies a (fictional) attribute to the return value of a method:


[return: Tasty]
private int MakeTastyNumbers() { /* What even is a tasty number? */ }

Each attribute decides which kind of code elements it can be attached to. Not every attribute
can be attached to every type of code element. For example, the Obsolete attribute cannot
be applied to parameters or return values, but it can be used on almost everything else.

Attributes are Classes


are just classes derived from the Attribute class. Their names typically also end
Attributes are
with the world Attribute. For example, the Obsolete attribute we used above is officially
called ObsoleteAttribute. You can use the attribute’s full name ([Obsolete
Attribute]), or you can leave off the Attribute part (just [Obsolete] ) if it ends with
that.
The parameters you supply translate directly to a constructor defined in the attribute class.
The following uses a two-parameter constructor:
[Obsolete("Use NewDeadMethod instead.", true)]

If an attribute has public properties, you can also set those by name:
[Sample(Number = 2)]

Attributes are usually created by people who want to work


work with
with your compiled code, including
the people making the compiler
c ompiler,, the .NET runtime, and other development tools. They decide

378 LEVEL 47 OTHER LANGUAGE FEATURES


what metadata they need, design the attribute classes that will give them that metadata, and
then provide you with documentation that tells you how to use them.
hard either: make a new class based on System.Attribute:
But making attributes isn’t hard
[AttributeUsage(AttributeTargets.Constructor, AllowMultiple = true)]
public class SampleAttribute : Attribute
{
public int Number { get; set; }
}

Notice how you use attributes when defining attributes! Now you can apply this Sample
attribute to constructors:
[Sample(Number = 2)]
[Sample(Number = 3)]
public Point(double a, double b) { ... }

REFLECTION
Compiled C# code
allows a program to retains
analyzeaitslotown
of rich information
structure about
as it runs. This the code. This
capability richreflection
is called information
.
There are many uses for this. For example, you could use reflection to search a collection of
DLLs to find things that implement an IPlugin interface, then create instances of each to
add to your program. Or you could use reflection to find all the public properties of an object
and display them all without knowing the object’
obje ct’ss type ahead of time.
Reflection is a broad topic that we can’t cover in-depth here, but we can explore some
practical examples that might pique your interest.
Most of the types involved in reflection live in the System.Reflection namespace. If
you’re doing with reflection, you will probably want to add a using directive (Level 33)
doing much with
to make your life easier.

The Type class is the beating heart of reflection. It represents a compiled type in the system.
An instance of the Type class represents the metadata of a specific type in your program.
There are a few ways to get a Type instance. One is to use the typeof operator:
Type type = typeof(int);
Type typeOfClass = typeof(MyClass);

Or, if you have an object and want the Type instance that represents its type, you can use the
Or,
GetType() method:
MyClass myObject = new MyClass();
Type type = myObject.GetType();

The Type class has methods for querying the type to see what members it has. For example:

ConstructorInfo[] contructors
MethodInfo[] methods = type.GetConstructors();
= type.GetMethods();

Those return objects that represent each constructor or method of the type. If you want a
specific constructor or method, you can use the GetConstructor and GetMethod
methods, passing in the parameter types (and the method name for GetMethod):
ConstructorInfo? constructor = type.GetConstructor(new Type[] { typeof(int) });

THE NAMEOF OPERATOR 379


MethodInfo? method = type.GetMethod("MethodName", new Type[] { typeof(int) });

The first line will find a constructor with a single int parameter. The second line will find a
method in the type named MethodName with a single int parameter. If there isn’t a match,
the result will be null in both cases.

With a ConstructorInfo object, you can create new instances of the type:
object newObject = constructor.Invoke(new object[] { 17 });

With a MethodInfo object, you can invoke the method


me thod with a specific instance:
method.Invoke(newObject, new object[] { 4 });

The syntax is far worse than the natural equivalent:


equivalent :
MyClass newObject = new MyClass(17);
newObject.MethodName(4);

It is also not as efficient, nor can the compiler protect you from making mistakes. For those
reasons, if you can do something without reflection, you should do so. But reflection is a
valuable tool when the situation
situation is right.

THE NAMEOF OPERATOR


The nameof operator lets you use the names of code elements from within your code.
Consider this method:
void DisplayNumbers(int a, int b) =>
Console.WriteLine($"a={a} and b={b}");

Now let’s
let’s say you rename the variables:
void DisplayNumbers(int first, int second) =>
Console.WriteLine($"a={first} and b={second}");

The output is now misleading. The names are no longer a and b, though the text still says they
are. The nameof operator helps you get this right by producing a string based on the name of
some code element:
void DisplayNumbers(int first, int second) =>
Console.WriteLine($"{nameof(first)}={first} and {nameof(second)}={second}");

NE󰁓TED TYPE󰁓

You might also like