We can define our own computations. What about our own data?
Suppose we want to define a door. A door has two properties: it is open or closed, and it is locked or unlocked. We could represent this with two boolean variables:
bool doorIsOpen = false;
bool doorIsLocked = true;This works for one door. But what if we have ten doors?
bool door1IsOpen = false;
bool door1IsLocked = true;
bool door2IsOpen = true;
bool door2IsLocked = false;
bool door3IsOpen = false;
bool door3IsLocked = false;
// ... and so onTwenty variables for ten doors. The naming is awkward. The relationship between door1IsOpen and door1IsLocked exists only in our minds. Nothing in the code connects them.
Next, we will explore one way to group related information with custom types.
Defining a Struct
C# lets us define our own types that group related data. We do this with a struct.
struct Door
{
public bool IsOpen;
public bool IsLocked;
}This defines a new type called Door. Let’s break it down token by token.
struct says we are defining a new value type.
Door is the name of the type we are creating.
{ opens a scope. Everything between { and } describes what a Door contains.
public bool IsOpen; declares a public boolean field named IsOpen.
public bool IsLocked; declares a public boolean field named IsLocked.
} closes the struct definition.
The whole definition translates as: “Define a struct named Door with public boolean fields IsOpen and IsLocked.”
Practice. Translate the following struct definition.
struct Switch
{
public bool IsOn;
}Reveal answer
“Define a struct named Switch with a public boolean field IsOn.”
Compare your answer to the model. Note any differences. Write the corrected version before continuing.
We call IsOpen and IsLocked “fields” because they belong to a struct. A field is a variable that lives inside a struct definition. Every door created from this struct will feature these two fields.
Definition. A struct is a type definition that groups related variables into named fields.
Definition. A field is a variable that belongs to a struct definition.
We can visualize a struct definition as a blueprint:
┌─────────────────────┐
│ Door (definition) │
├─────────────────────┤
│ IsOpen: [bool] │
│ IsLocked: [bool] │
└─────────────────────┘
This is not a door. It is a description of what a door contains. The definition is a blueprint. We have not yet created any actual doors.
Practice. Write a struct definition from this description:
Define a struct named Light with a public boolean field named IsLit.
Reveal answer
struct Light
{
public bool IsLit;
}Self-correct against the model above.
The public Keyword
You may have noticed public before each field. What happens without it?
struct Door
{
bool IsOpen; // no public
}
Door d;
d.IsOpen = true; // Error: 'Door.IsOpen' is inaccessibleThe compiler stops you. Code outside the struct cannot reach the field.
The public keyword controls accessibility. It grants permission for code outside the struct to read and write the field.
Definition. A public field is accessible from outside its containing struct.
Why does this option exist? Sometimes we want to hide data and control how it’s accessed. Imagine a bank account with a balance field. You might want to prevent code from directly setting the balance to any value. Instead, you would provide functions like Deposit and Withdraw that enforce rules (no negative deposits, no overdrafts). Hiding the field forces all changes to go through these controlled functions.
This technique is called encapsulation. We explore it in later chapters when we discuss classes. For now, always include public on your fields. This gives you full access to the data you define.
Creating Objects
Defining a struct gives us a new type. Now we can create values of that type.
Door frontDoor;Translation: “Create a Door object named frontDoor.”
Definition. An object is a named location in memory that holds a set of values from some type.
This distinction matters. When we wrote struct Door { ... }, we defined the type. We described what a Door contains. When we write Door frontDoor;, we create an actual Door.
A blueprint for a house is not a house. You can have one blueprint and build many houses from it. Similarly, struct Door is the blueprint. Each Door frontDoor; or Door backDoor; builds one actual door.
Here is what happens when we create an object:
- Reserve space for a Door (including space for all its fields)
- Name that space “frontDoor”
frontDoor (object)
┌─────────────────────┐
│ IsOpen: [?] │
│ IsLocked: [?] │
└─────────────────────┘
The object has space for two booleans. The question marks indicate the fields exist but hold no value yet. They must be filled before use.
Practice. Translate the following code.
Switch lightSwitch;Reveal answer
“Create a Switch object named lightSwitch.”
Practice. Write code from this description:
Create a Light object named lamp.
Reveal answer
Light lamp;Self-correct against the models above.
Binding to Fields
We access fields with dot notation. The dot reaches inside the object.
frontDoor.IsOpen = false;Let’s break this down token by token.
frontDoor is the object we are accessing.
. is the dot operator. It reaches into the object to access a field.
IsOpen is the name of the field.
= false; binds the value false to that field.
Translation: “Bind false to the IsOpen field of the frontDoor object.”
Here is what happens:
- Locate the frontDoor object
- Access its IsOpen field
- Bind false to that field
frontDoor.IsLocked = true;Translation: “Bind true to the IsLocked field of the frontDoor object.”
After both lines:
frontDoor (object)
┌─────────────────────┐
│ IsOpen: false │
│ IsLocked: true │
└─────────────────────┘
Practice. Translate the following code.
Door backDoor;
backDoor.IsOpen = true;
backDoor.IsLocked = false;Reveal answer
Door backDoor;→ “Create a Door object named backDoor.”backDoor.IsOpen = true;→ “Bind true to the IsOpen field of the backDoor object.”backDoor.IsLocked = false;→ “Bind false to the IsLocked field of the backDoor object.”
Practice. Write code from this description:
Create a Switch object named mainSwitch. Bind true to the IsOn field of the mainSwitch object.
Reveal answer
Switch mainSwitch;
mainSwitch.IsOn = true;Self-correct against the models above.
Evaluating Fields
We can also read values from fields.
bool locked = frontDoor.IsLocked;Translation: “Evaluate the IsLocked field of the frontDoor object and bind the result to a boolean variable named locked.”
Here is what happens:
- Locate the frontDoor object
- Access its IsLocked field
- Evaluate the field (get its current value: true)
- Bind that value to locked
This follows the same pattern as regular variables. When a field appears on the right side of =, we evaluate it. When it appears on the left side, we bind to it.
frontDoor.IsOpen = true;→ binding (writing)bool x = frontDoor.IsOpen;→ evaluating (reading)
One warning: you cannot evaluate a field before binding a value to it. If you try to read an uninitialized field, the compiler will stop you. Always set your fields before reading them.
Practice. Write code from this description:
Evaluate the IsOpen field of the garage object and bind the result to a boolean variable named garageOpen.
Reveal answer
bool garageOpen = garage.IsOpen;Practice. Translate this code. Assume mySwitch is a Switch object.
bool status = mySwitch.IsOn;Reveal answer
“Evaluate the IsOn field of the mySwitch object and bind the result to a boolean variable named status.”
Self-correct against the models above.
Structs Are Value Types
Earlier, we saw that booleans are value types. When we copy a boolean, we get an independent copy. Changing one does not affect the other.
Structs work the same way.
Door original;
original.IsOpen = false;
original.IsLocked = true;
Door copy = original;
copy.IsOpen = true;What is the value of original.IsOpen after this code runs?
| after line | original.IsOpen | original.IsLocked | copy.IsOpen | copy.IsLocked |
|---|---|---|---|---|
| 1-3 | false | true | — | — |
| 4 | false | true | false | true |
| 5 | false | true | true | true |
When we assigned original to copy, we copied all the field values. The two objects are independent. Changing the IsOpen field of the copy object does not affect the IsOpen field of the original object.
This is value type behavior. Each object holds its own data.
Practice. Define and use a struct.
- Define a struct named Permission with two public boolean fields: CanRead and CanWrite.
- Create a Permission object named access.
- Bind true to the CanRead field and false to the CanWrite field.
- Evaluate both fields and display them.
Reveal answer
struct Permission
{
public bool CanRead;
public bool CanWrite;
}
Permission access;
access.CanRead = true;
access.CanWrite = false;
Console.WriteLine(access.CanRead); // displays True
Console.WriteLine(access.CanWrite); // displays FalseCompare your answer to the model. Note any differences. Write the corrected version before continuing.
Functions That Take Objects
So far we have worked with Door objects directly, binding and evaluating their fields one at a time. Now we can combine structs with what we learned in Strand 4: functions.
In Chapter 0, we wrote type signatures for computations. Now that we have defined our own type, we can write functions that use it. The type signature IsSecure: Door → bool describes a function that takes a Door object and returns a boolean.
Consider this problem: check if a door is secure. A door is secure when it is closed and locked.
Func<Door, bool> IsSecure = d => !d.IsOpen && d.IsLocked;Translation: “Define a function named IsSecure that takes a Door object d and returns not d.IsOpen and d.IsLocked.”
The function takes a Door object, reads its fields, and computes a result. Our custom type works just like the built-in types.
We can visualize this function as a box:
┌─────────────────────────────┐
d ───►│ !d.IsOpen && d.IsLocked │───► result
(Door) │ │ (bool)
└─────────────────────────────┘
IsSecure
A Door object enters on the left. The function reads its fields, computes, and a boolean emerges on the right.
When we define a function, we describe its behavior: what it takes, what it computes, and what it returns. To actually use the function, we call it.
bool safe = IsSecure(frontDoor);Translation: “Evaluate frontDoor, call IsSecure with that value, and bind the returned value to a boolean variable named safe.”
Here is what happens:
- Evaluate frontDoor (get the object)
- Bind that object to d (the parameter)
- Evaluate
!d.IsOpen && d.IsLocked- Evaluate the IsOpen field of the d object → false
- Apply NOT → true
- Evaluate the IsLocked field of the d object → true
- Apply AND to true and true → true
- Return true
- Bind true to safe
Practice. Translate the following function definition.
Func<Door, bool> CanEnter = d => d.IsOpen && !d.IsLocked;Reveal answer
“Define a function named CanEnter that takes a Door object d and returns d.IsOpen and not d.IsLocked.”
Practice. Write a function from this description:
Define a function named IsUnlocked that takes a Door object d and returns not d.IsLocked.
Reveal answer
Func<Door, bool> IsUnlocked = d => !d.IsLocked;Self-correct against the models above.
Functions That Return Objects
Functions can also create and return objects. This lets us package object creation into a reusable computation.
Func<Door> CreateLockedDoor = () => {
Door d;
d.IsOpen = false;
d.IsLocked = true;
return d;
};This function needs multiple statements, so we use curly braces and return, just as we did with branching functions in Strand 4.
Translation: “Define a function named CreateLockedDoor that takes nothing and returns a Door object that is closed and locked.”
We can visualize this as a box with no input:
┌─────────────────────────────┐
│ Door d; │───► result
│ d.IsOpen = false; │ (Door)
│ d.IsLocked = true; │
│ return d; │
└─────────────────────────────┘
CreateLockedDoor
Nothing enters. A Door object emerges.
Here is what happens inside the function when called:
- Create a Door object named d
- Bind false to the IsOpen field of the d object
- Bind true to the IsLocked field of the d object
- Return the d object
To use this function, we call it and bind the result:
Door vault = CreateLockedDoor();Translation: “Call CreateLockedDoor and bind the returned object to a Door variable named vault.”
Notice that calling requires parentheses even when there are no arguments. The parentheses tell C# we are calling the function, not just referring to it by name.
Practice. Translate the following function definition.
Func<Door> CreateOpenDoor = () => {
Door d;
d.IsOpen = true;
d.IsLocked = false;
return d;
};Reveal answer
“Define a function named CreateOpenDoor that takes nothing and returns a Door object that is open and not locked.”
Practice. Write a function from this description:
Define a function named CreateClosedDoor that takes nothing and returns a Door object that is closed and not locked.
Reveal answer
Func<Door> CreateClosedDoor = () => {
Door d;
d.IsOpen = false;
d.IsLocked = false;
return d;
};Self-correct against the models above.
Functions That Display Objects
We can also write functions that display object data. These functions return nothing (void) because they perform an action rather than computing a value.
In Strand 4, we used Action for functions that return nothing. The same pattern works with our custom types.
Action<Door> PrintDoor = d => {
Console.WriteLine("Open: " + d.IsOpen);
Console.WriteLine("Locked: " + d.IsLocked);
};Translation: “Define a function named PrintDoor that takes a Door object d and displays its field values to the console.”
The + operator here joins strings together. "Open: " + d.IsOpen creates the string “Open: True” or “Open: False” depending on the field’s value.
We can visualize this as a box with no output:
┌─────────────────────────────┐
d ───►│ Console.WriteLine(...) │───► (nothing)
(Door) │ Console.WriteLine(...) │
└─────────────────────────────┘
PrintDoor
A Door enters. The function performs an action (displaying to the console). Nothing is returned.
To use this function, we call it for its effect:
PrintDoor(frontDoor);Translation: “Evaluate frontDoor, call PrintDoor with that value.”
Notice we do not write bool result = PrintDoor(frontDoor); because there is no result to bind. We call the function for what it does, not for what it produces.
This displays:
Open: False
Locked: True
Practice. Translate the following function definition.
Action<Permission> PrintPermission = p => {
Console.WriteLine("Can Read: " + p.CanRead);
Console.WriteLine("Can Write: " + p.CanWrite);
};Reveal answer
“Define a function named PrintPermission that takes a Permission object p and displays its field values to the console.”
Practice. Write a function from this description:
Define a function named PrintSwitch that takes a Switch object s and displays “Switch is on: ” followed by the value of the IsOn field of the s object.
Reveal answer
Action<Switch> PrintSwitch = s => {
Console.WriteLine("Switch is on: " + s.IsOn);
};Or, since there is only one statement:
Action<Switch> PrintSwitch = s => Console.WriteLine("Switch is on: " + s.IsOn);Self-correct against the models above.
Bringing It Together
Let’s build a program that uses structs, functions, and control flow together. Here is the setup:
struct Door
{
public bool IsOpen;
public bool IsLocked;
}
Func<Door, bool> IsSecure = d => !d.IsOpen && d.IsLocked;
Func<Door, bool> CanEnter = d => d.IsOpen && !d.IsLocked;
Action<Door> PrintDoor = d => {
Console.WriteLine("Open: " + d.IsOpen);
Console.WriteLine("Locked: " + d.IsLocked);
};Now we can write a program that checks a door:
Door frontDoor;
Console.WriteLine("Is the door open? (yes/no)");
string openInput = Console.ReadLine();
frontDoor.IsOpen = openInput == "yes";
Console.WriteLine("Is the door locked? (yes/no)");
string lockedInput = Console.ReadLine();
frontDoor.IsLocked = lockedInput == "yes";
Console.WriteLine("Door status:");
PrintDoor(frontDoor);
if (IsSecure(frontDoor))
{
Console.WriteLine("The door is secure.");
}
else
{
Console.WriteLine("The door is not secure.");
}
if (CanEnter(frontDoor))
{
Console.WriteLine("You can enter.");
}
else
{
Console.WriteLine("You cannot enter.");
}Trace through this program with the input “no” for open and “yes” for locked:
| after line | frontDoor.IsOpen | frontDoor.IsLocked |
|---|---|---|
| Create frontDoor | ? | ? |
| Bind to IsOpen | false | ? |
| Bind to IsLocked | false | true |
Let’s trace the function calls:
IsSecure(frontDoor) evaluates:
- Evaluate frontDoor and bind to d
- Evaluate the IsOpen field of the d object → false
- Apply NOT → true
- Evaluate the IsLocked field of the d object → true
- Apply AND to true and true → true
- Return true
The condition is true, so the program executes the first scope: display “The door is secure.”
CanEnter(frontDoor) evaluates:
- Evaluate frontDoor and bind to d
- Evaluate the IsOpen field of the d object → false
- Evaluate the IsLocked field of the d object → true
- Apply NOT → false
- Apply AND to false and false → false
- Return false
The condition is false, so the program executes the second scope: display “You cannot enter.”
Full output:
Is the door open? (yes/no)
no
Is the door locked? (yes/no)
yes
Door status:
Open: False
Locked: True
The door is secure.
You cannot enter.
Review
Before continuing, test yourself on what you’ve learned. Use the protocol from Chapter 0: attempt each exercise from memory, then search this section to check your answers, then note what you missed.
Part 1: Definitions
Write the definitions from memory, then find them in this section to check.
- What is a struct?
- What is an object?
- What is a field?
- What does public mean for a field?
If any of your answers differed from the definitions in this section, note what you missed and write the corrected version.
Part 2: Translations
Translate each piece of code to English.
struct Light { public bool IsOn; }Light lamp;lamp.IsOn = true;bool status = lamp.IsOn;Func<Light, bool> IsLit = l => l.IsOn;Action<Light> PrintLight = l => Console.WriteLine(l.IsOn);
Check your translations against the patterns shown in this section.
Part 3: Writing Code
Write C# code for each description.
- Define a struct named Switch with a public boolean field named IsOn.
- Create a Switch object named mainSwitch.
- Bind true to the IsOn field of the mainSwitch object.
- Evaluate the IsOn field of the mainSwitch object and bind the result to a boolean variable named state.
- Define a function named IsOff that takes a Switch object s and returns not s.IsOn.
Check your code against the examples in this section.
Part 4: State Tables
Complete the state table for this code:
struct Counter { public bool IsActive; public bool IsComplete; }
Counter c;
c.IsActive = true;
c.IsComplete = false;
Counter backup = c;
backup.IsActive = false;
c.IsComplete = true;| after line | c.IsActive | c.IsComplete | backup.IsActive | backup.IsComplete |
|---|---|---|---|---|
| 3 | ||||
| 4 | ||||
| 6 | ||||
| 7 | ||||
| 8 |
Remember: structs are value types. Trace through each line using the rules you learned.
Part 5: Complete Programs
Write a complete program for this specification:
A program that defines a Lock struct with two boolean fields: IsEngaged and RequiresKey. Create a Lock object and ask the user two questions: “Is the lock engaged? (yes/no)” and “Does it require a key? (yes/no)“. Bind the appropriate values based on the user’s responses. Then check: if the lock is engaged and requires a key, display “Secure”. Otherwise, display “Not secure”.
Check your program by tracing through it with different inputs.
You now know how to define your own data types. You can group related fields into structs, create objects from those definitions, and write functions that operate on your custom types.
Chapter 1 Complete
You now have the complete toolkit for booleans:
- What booleans are, how to store them, how programs execute
- How to compute with booleans using operators
- How booleans control program behavior with branching and loops
- How to define your own computations
- How to define your own data types
In Chapter 2, we apply these same five questions to numbers.
Previous: Section 4 - Functions