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 on

Twenty 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 inaccessible

The 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 methods like Deposit and Withdraw that enforce rules (no negative deposits, no overdrafts). Hiding the field forces all changes to go through these controlled methods.

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:

  1. Reserve space for a Door (including space for all its fields)
  2. 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:

  1. Locate the frontDoor object
  2. Access its IsOpen field
  3. 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:

  1. Locate the frontDoor object
  2. Access its IsLocked field
  3. Evaluate the field (get its current value: true)
  4. 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 lineoriginal.IsOpenoriginal.IsLockedcopy.IsOpencopy.IsLocked
1-3falsetrue
4falsetruefalsetrue
5falsetruetruetrue

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.

  1. Define a struct named Permission with two public boolean fields: CanRead and CanWrite.
  2. Create a Permission object named access.
  3. Bind true to the CanRead field and false to the CanWrite field.
  4. 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 False

Compare your answer to the model. Note any differences. Write the corrected version before continuing.

Methods 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 Section 4: methods.

In Chapter 0, we wrote type signatures for computations. Now that we have defined our own type, we can write methods that use it. The type signature IsSecure: Door → bool describes a computation that receives 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.

bool IsSecure(Door d)
{
    return !d.IsOpen && d.IsLocked;
}

Translation: “Define a method named IsSecure that computes a boolean. It receives a Door object called d. The returned value is the result of evaluating not d.IsOpen and d.IsLocked.”

The method receives a Door object, reads its fields, and computes a result. Our custom type works just like the built-in types.

We can visualize this method as a box:

        ┌─────────────────────────────┐
  d ───►│  !d.IsOpen && d.IsLocked    │───► result
(Door)  │                             │     (bool)
        └─────────────────────────────┘
                  IsSecure

A Door object enters on the left. The method reads its fields, computes, and returns a boolean.

When we define a method, we describe its behavior: what it receives, what it computes, and what it returns. To actually use the method, we call it.

bool safe = IsSecure(frontDoor);

Translation: “Evaluate frontDoor. Compute IsSecure with that value. Bind the returned boolean to a variable named safe.”

Here is what happens:

  1. Evaluate frontDoor (get the object)
  2. Bind that object to d (the parameter)
  3. 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
  4. Return true
  5. Bind true to safe

Practice. Translate the following method definition.

bool CanEnter(Door d)
{
    return d.IsOpen && !d.IsLocked;
}
Reveal answer

“Define a method named CanEnter that computes a boolean. It receives a Door object called d. The returned value is the result of evaluating d.IsOpen and not d.IsLocked.”

Practice. Write a method from this description:

Define a method named IsUnlocked that computes a boolean. It receives a Door object called d and returns not d.IsLocked.

Reveal answer
bool IsUnlocked(Door d)
{
    return !d.IsLocked;
}

Self-correct against the models above.

Methods That Return Objects

Methods can also create and return objects. This lets us package object creation into a reusable computation.

Door CreateLockedDoor()
{
    Door d;
    d.IsOpen = false;
    d.IsLocked = true;
    return d;
}

This method needs multiple statements, so we use curly braces and return, just as we did with branching methods in Section 4.

Translation: “Define a method named CreateLockedDoor that computes a Door object. It receives no parameters. It creates a Door object that is closed and locked, then returns that object.”

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 is returned.

Here is what happens inside the method when called:

  1. Create a Door object named d
  2. Bind false to the IsOpen field of the d object
  3. Bind true to the IsLocked field of the d object
  4. Return the d object

To use this method, we call it and bind the result:

Door vault = CreateLockedDoor();

Translation: “Create a Door variable named vault and bind the result of computing CreateLockedDoor.”

Notice that calling requires parentheses even when there are no arguments. The parentheses tell C# we are calling the method, not just naming it.

Practice. Translate the following method definition.

Door CreateOpenDoor()
{
    Door d;
    d.IsOpen = true;
    d.IsLocked = false;
    return d;
}
Reveal answer

“Define a method named CreateOpenDoor that computes a Door object. It receives no parameters. It creates a Door object that is open and not locked, then returns that object.”

Practice. Write a method from this description:

Define a method named CreateClosedDoor that computes a Door object. It receives no parameters and returns a Door object that is closed and not locked.

Reveal answer
Door CreateClosedDoor()
{
    Door d;
    d.IsOpen = false;
    d.IsLocked = false;
    return d;
}

Self-correct against the models above.

Methods That Display Objects

We can also write methods that display object data. These methods return nothing (void) because they perform an action rather than computing a value.

void PrintDoor(Door d)
{
    Console.WriteLine("Open: " + d.IsOpen);
    Console.WriteLine("Locked: " + d.IsLocked);
}

Translation: “Define a method named PrintDoor that returns nothing. It receives a Door object called 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 method performs an action: displaying values to the console. Nothing is returned.

To use this method, we call it for its effect:

PrintDoor(frontDoor);

Translation: “Evaluate frontDoor. Compute PrintDoor with that value.”

Notice we do not write bool result = PrintDoor(frontDoor); because there is no result to bind. We call the method for what it does, not for what it produces.

This displays:

Open: False
Locked: True

Practice. Translate the following method definition.

void PrintPermission(Permission p)
{
    Console.WriteLine("Can Read: " + p.CanRead);
    Console.WriteLine("Can Write: " + p.CanWrite);
}
Reveal answer

“Define a method named PrintPermission that returns nothing. It receives a Permission object called p and displays its field values to the console.”

Practice. Write a method from this description:

Define a method named PrintSwitch that returns nothing. It receives a Switch object called s and displays “Switch is on: ” followed by the value of the IsOn field of the s object.

Reveal answer
void PrintSwitch(Switch s)
{
    Console.WriteLine("Switch is on: " + s.IsOn);
}

Self-correct against the models above.

Bringing It Together

Now build a program that uses structs, methods, and control flow together. Here are the reusable pieces:

struct Door
{
    public bool IsOpen;
    public bool IsLocked;
}
 
bool IsSecure(Door d)
{
    return !d.IsOpen && d.IsLocked;
}
 
bool CanEnter(Door d)
{
    return d.IsOpen && !d.IsLocked;
}
 
void PrintDoor(Door d)
{
    Console.WriteLine("Open: " + d.IsOpen);
    Console.WriteLine("Locked: " + d.IsLocked);
}

Now we can write code 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 linefrontDoor.IsOpenfrontDoor.IsLocked
Create frontDoor??
Bind to IsOpenfalse?
Bind to IsLockedfalsetrue

Trace the method calls:

IsSecure(frontDoor) evaluates:

  1. Evaluate frontDoor and bind to d
  2. Evaluate the IsOpen field of the d object → false
  3. Apply NOT → true
  4. Evaluate the IsLocked field of the d object → true
  5. Apply AND to true and true → true
  6. Return true

The condition is true, so the program executes the first scope: display “The door is secure.”

CanEnter(frontDoor) evaluates:

  1. Evaluate frontDoor and bind to d
  2. Evaluate the IsOpen field of the d object → false
  3. Evaluate the IsLocked field of the d object → true
  4. Apply NOT → false
  5. Apply AND to false and false → false
  6. 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.

  1. What is a struct?
  2. What is an object?
  3. What is a field?
  4. 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.

  1. struct Light { public bool IsOn; }
  2. Light lamp;
  3. lamp.IsOn = true;
  4. bool status = lamp.IsOn;
  5. bool IsLit(Light l) { return l.IsOn; }
  6. void PrintLight(Light 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.

  1. Define a struct named Switch with a public boolean field named IsOn.
  2. Create a Switch object named mainSwitch.
  3. Bind true to the IsOn field of the mainSwitch object.
  4. Evaluate the IsOn field of the mainSwitch object and bind the result to a boolean variable named state.
  5. Define a method named IsOff that computes a boolean. It receives a Switch object called 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 linec.IsActivec.IsCompletebackup.IsActivebackup.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 methods 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 with methods
  • How to define your own data types

In Chapter 2 - Integers and Doubles, we apply these same five questions to numbers.