Basic Computation
In Chapter 3, we wrote standalone functions to operate on arrays:
Func<int[], int> Sum = (arr) =>
{
int total = 0;
for (int i = 0; i < arr.Length; i++)
{
total += arr[i];
}
return total;
};
Func<int[], int> Max = (arr) =>
{
int best = arr[0];
for (int i = 1; i < arr.Length; i++)
{
if (arr[i] > best) { best = arr[i]; }
}
return best;
};Each of these takes data as a parameter, operates on it, and returns a result. The function lives in one place. The data lives somewhere else. Every time we want to connect them, we pass the data in as an argument.
Classes let us put the function and the data together. Instead of passing the object as an argument, we call the operation on the object with the dot operator, and the object becomes this inside the body. The rest of this section shows how that works.
From Standalone Functions to Methods
Here is a standalone function that computes the area of a Rectangle:
Func<Rectangle, int> Area = (r) => r.Width * r.Height;
Rectangle box = new Rectangle(10, 5);
int a = Area(box);The function takes a Rectangle as its parameter. At the call site, we pass box in as the argument.
Here is the same computation written as a method inside the Rectangle class:
class Rectangle
{
public int Width;
public int Height;
public Rectangle(int width, int height)
{
this.Width = width;
this.Height = height;
}
public int Area()
{
return this.Width * this.Height;
}
}
Rectangle box = new Rectangle(10, 5);
int a = box.Area();Translation: “Call the Area method on the object referred to by box, and bind the returned value to a.”
One practical note: class definitions must be placed below all other code in Program.cs, or in a separate file in the same directory. In this book, we show the class definition first because it is easier to read a method before seeing it called. When you type these examples into your own files, place the class at the bottom.
The standalone function and the method compute the same thing. The differences are in where the code lives and how the object reaches it.
The standalone function takes a Rectangle parameter named r. The method has no parameter for the Rectangle because it uses this instead. this refers to the object the method was called on. In the call box.Area(), the object that box refers to becomes this inside the method body.
The call site changes from Area(box) to box.Area(). The object moves from inside the parentheses to the left side of the dot.
| Before (standalone) | After (method) |
|---|---|
Func<Rectangle, int> Area = (r) => r.Width * r.Height; | public int Area() { return this.Width * this.Height; } |
int a = Area(box); | int a = box.Area(); |
Parameter r holds the object | this refers to the object |
| Function lives outside the class | Method lives inside the class |
A function that lives inside a class and operates on that class’s data is called a method.
The discrete actions for box.Area():
- Identify the object that
boxrefers to. That object becomesthis. - Inside Area, evaluate
this.Width * this.Heightusing the fields of that object. - Return the result.
Try it yourself.
Translate this code to English:
Rectangle r = new Rectangle(3, 7);
int p = r.Area();Write your answer before revealing ours.
Reveal answer
“Create a Rectangle object with width 3 and height 7, and store a reference to it in r. Call the Area method on the object referred to by r, and bind the returned value to p.”
If your answer differed, note what you missed before continuing.
Methods That Return Values
Here are two more methods on Rectangle, alongside the Area method we already wrote:
class Rectangle
{
public int Width;
public int Height;
public Rectangle(int width, int height)
{
this.Width = width;
this.Height = height;
}
public int Area()
{
return this.Width * this.Height;
}
public int Perimeter()
{
return 2 * (this.Width + this.Height);
}
public bool IsSquare()
{
return this.Width == this.Height;
}
}Description: “The Rectangle class defines three methods. Area returns the product of width and height. Perimeter returns twice the sum of width and height. IsSquare returns whether width equals height.”
All three methods access the fields of this and return a result. The return type appears before the method name: int for Area and Perimeter, bool for IsSquare. Methods with a return type work the same way Func<> does in previous chapters: they compute a value and send it back to the caller.
Rectangle box = new Rectangle(10, 5);
Rectangle sq = new Rectangle(4, 4);
int a = box.Area(); // a is 50
int p = box.Perimeter(); // p is 30
bool check = sq.IsSquare(); // check is trueTranslation for int p = box.Perimeter();: “Call the Perimeter method on the object referred to by box, and bind the returned value to p.”
Translation for bool check = sq.IsSquare();: “Call the IsSquare method on the object referred to by sq, and bind the returned value to check.”
Try it yourself.
Write a method named Diagonal on Rectangle that returns the diagonal as a double. The diagonal of a rectangle is the square root of (width squared plus height squared). You can compute a square root with Math.Sqrt(value), which takes a double and returns a double.
Reveal answer
public double Diagonal()
{
return Math.Sqrt(this.Width * this.Width + this.Height * this.Height);
}Self-correct against the model above.
Try it yourself.
Translate this code to English:
Rectangle r = new Rectangle(6, 6);
bool result = r.IsSquare();Reveal answer
“Create a Rectangle object with width 6 and height 6, and store a reference to it in r. Call the IsSquare method on the object referred to by r, and bind the returned value to result.”
If your answer differed, note what you missed before continuing.
Void Methods
Some operations do not compute a return value. They change the state of an object instead. In previous chapters, these corresponded to Action<>. As methods, they use the keyword void in place of a return type.
Consider a Counter class with a single field:
class Counter
{
public int Count;
public Counter(int start)
{
this.Count = start;
}
public void Increment()
{
this.Count = this.Count + 1;
}
public void Reset()
{
this.Count = 0;
}
}Description: “The Counter class has a public integer field Count. Its constructor takes a starting value. Increment adds 1 to Count. Reset sets Count to 0.”
Counter c = new Counter(0);
c.Increment();
c.Increment();
c.Increment();
Console.WriteLine(c.Count); // displays 3
c.Reset();
Console.WriteLine(c.Count); // displays 0Translation for c.Increment();: “Call the Increment method on the object referred to by c.”
There is no “bind the returned value” clause because void methods produce nothing to bind.
Compare the standalone and method versions side by side:
| Standalone | Method |
|---|---|
Func<Rectangle, int> produces a value | Method with return type int produces a value |
Action<Counter> produces nothing | void method produces nothing |
| Pass object as first argument | Object is this |
FunctionName(object) | object.MethodName() |
Try it yourself.
What does this code display?
Counter a = new Counter(5);
Counter b = new Counter(10);
a.Increment();
b.Reset();
a.Increment();
Console.WriteLine(a.Count);
Console.WriteLine(b.Count);Reveal answer
Output:
7
0
a starts at 5 and is incremented twice, reaching 7. b starts at 10 and is reset to 0. Each method call operates on the object to the left of the dot. Incrementing a does not affect b.
If your answer differed, trace through each line and identify where your reasoning went wrong.
The Counter class has public fields, so outside code can read and write c.Count directly. The methods are convenient but not enforced. The more interesting case is when a field is private and methods are the only way to interact with it.
In Section 1, BankAccount had a private balance field, but at the time we had no way to reach it from outside the class. Methods solve that:
class BankAccount
{
private int balance;
public string Owner;
public BankAccount(string owner, int initial)
{
this.Owner = owner;
this.balance = initial;
}
public void Deposit(int amount)
{
this.balance = this.balance + amount;
}
public void Withdraw(int amount)
{
if (amount <= this.balance)
{
this.balance = this.balance - amount;
}
}
public int GetBalance()
{
return this.balance;
}
}Description: “BankAccount has a private integer field balance and a public string field Owner. Deposit adds the given amount to the balance. Withdraw subtracts the given amount, but only if the balance is sufficient. GetBalance returns the current balance.”
BankAccount acct = new BankAccount("Alice", 500);
acct.Deposit(200);
acct.Withdraw(50);
Console.WriteLine(acct.GetBalance()); // displays 650
acct.Withdraw(1000);
Console.WriteLine(acct.GetBalance()); // displays 650The second Withdraw does nothing because 1000 exceeds 650. The if condition inside Withdraw prevents the overdraw.
Outside code cannot write acct.balance = 0. The only way to change the balance is through Deposit and Withdraw, and those methods enforce rules about how the balance changes. The class boundary from Section 1 now governs what operations are possible on the data, because only the methods we write can modify it.
Deposit and Withdraw also show something new: methods can take parameters beyond this. The object is implicit through the dot operator, and any extra data the method needs is an explicit parameter in the parentheses.
public void Deposit(int amount)In this signature, this is the BankAccount and amount is the additional parameter.
acct.Deposit(100);Translation: “Call the Deposit method on the object referred to by acct with argument 100.”
In the standalone version, a function for deposit would have had two explicit parameters: Action<BankAccount, int>. The method version has one explicit parameter. The BankAccount is implicit because it appears on the left side of the dot.
Try it yourself.
Write a method named ScaleUp on Rectangle that takes an integer factor and multiplies both Width and Height by it. The method returns nothing.
Reveal answer
public void ScaleUp(int factor)
{
this.Width = this.Width * factor;
this.Height = this.Height * factor;
}Self-correct against the model above.
Try it yourself.
Translate this code to English:
BankAccount acct = new BankAccount("Bob", 200);
acct.Deposit(50);
acct.Withdraw(30);Reveal answer
“Create a BankAccount object with owner Bob and initial balance 200, and store a reference to it in acct. Call the Deposit method on the object referred to by acct with argument 50. Call the Withdraw method on the object referred to by acct with argument 30.”
If your answer differed, note what you missed before continuing.
Tracing this with Two Objects
With a single object, this refers to the only object in play, and the keyword feels like a formality. The concept becomes important when two objects of the same class are both involved in a method call.
Add a Transfer method to BankAccount:
public void Transfer(int amount, BankAccount other)
{
if (amount <= this.balance)
{
this.balance = this.balance - amount;
other.balance = other.balance + amount;
}
}Description: “Transfer moves money from this account to the other account, but only if this account has sufficient funds.”
The method accesses other.balance, which is a private field. This compiles because private means “accessible only from inside the class that defines it.” Code inside the BankAccount class can access the private fields of any BankAccount object, not just the one that this refers to.
Now consider two objects and two Transfer calls:
BankAccount a = new BankAccount("Alice", 300);
BankAccount b = new BankAccount("Bob", 100);
a.Transfer(50, b);
b.Transfer(200, a);Before either Transfer call, the objects look like this:
a b
┌──────┐ ┌──────┐
│ ref ──>┌──────────────┐ │ ref ──>┌──────────────┐
└──────┘ │ Owner: Alice │ └──────┘ │ Owner: Bob │
│ balance: 300 │ │ balance: 100 │
└──────────────┘ └──────────────┘
Trace a.Transfer(50, b):
a is on the left of the dot, so this refers to Alice’s object. other is bound to b, so other refers to Bob’s object. amount is 50.
The condition: 50 ⇐ 300 is true, so the body executes.
this.balance = this.balance - amount changes Alice’s balance from 300 to 250.
other.balance = other.balance + amount changes Bob’s balance from 100 to 150.
a b
┌──────┐ ┌──────┐
│ ref ──>┌──────────────┐ │ ref ──>┌──────────────┐
└──────┘ │ Owner: Alice │ └──────┘ │ Owner: Bob │
│ balance: 250 │ │ balance: 150 │
└──────────────┘ └──────────────┘
Trace b.Transfer(200, a):
Now b is on the left of the dot, so this refers to Bob’s object. other is bound to a, so other refers to Alice’s object. amount is 200.
The condition: 200 ⇐ 150 is false, so the body does not execute. Neither balance changes.
The same two objects are involved in both calls, but which one is this and which one is other depends entirely on which side of the dot each one appears on.
State table for the full program:
| After line | a (Alice) balance | b (Bob) balance |
|---|---|---|
| Construction | 300 | 100 |
a.Transfer(50, b) | 250 | 150 |
b.Transfer(200, a) | 250 | 150 |
The second Transfer does nothing because Bob’s balance (150) is less than the requested amount (200). The condition amount <= this.balance checked Bob’s balance, not Alice’s, because b was on the left of the dot.
Try it yourself.
What does this code display?
BankAccount x = new BankAccount("X", 500);
BankAccount y = new BankAccount("Y", 500);
x.Transfer(200, y);
y.Transfer(300, x);
x.Transfer(400, y);After all three calls, what is x.GetBalance() and what is y.GetBalance()?
Reveal answer
After x.Transfer(200, y): x has 300, y has 700. (this is x. 200 ⇐ 500 is true.)
After y.Transfer(300, x): x has 600, y has 400. (this is y. 300 ⇐ 700 is true.)
After x.Transfer(400, y): x has 200, y has 800. (this is x. 400 ⇐ 600 is true.)
x.GetBalance() returns 200. y.GetBalance() returns 800.
Trace through each Transfer call. For each one, identify which object is this, which is other, and whether the condition passes.
Try it yourself.
Using the same starting state (x at 500, y at 500), what happens if the three calls are:
x.Transfer(600, y);
y.Transfer(100, x);Reveal answer
After x.Transfer(600, y): x still has 500, y still has 500. (this is x. 600 ⇐ 500 is false. Nothing changes.)
After y.Transfer(100, x): x has 600, y has 400. (this is y. 100 ⇐ 500 is true.)
If your answers differed, trace each call again and pay attention to which object this refers to.
The Dot Operator
Every dot-operator call in this book has been a method call or property access. Consider a few examples from earlier chapters:
"hello".ToUpper() calls the ToUpper method on a string object. The string is this inside ToUpper.
"hello".Substring(0, 3) calls the Substring method on a string object with two arguments.
arr.Length accesses a property on an array object.
The dot operator means “on this object, call this method” or “on this object, access this member.” The object on the left of the dot is the target. The name on the right is the member being accessed. This has been true since Chapter 1; what changes now is that you can describe the mechanism behind it and write your own methods that work the same way.
string greeting = "hello";
string upper = greeting.ToUpper();Translation: “Call the ToUpper method on the object referred to by greeting, and bind the returned value to upper.”
box.Area() and greeting.ToUpper() follow the same translation pattern. The only difference is who defined the method: we wrote Area, and the language designers wrote ToUpper.
Try it yourself.
Translate this code to English, treating each dot-operator call as a method call:
string name = "Alice";
int len = name.Length;
string lower = name.ToLower();
bool match = name.Equals("Bob");Write your answer before revealing ours.
Reveal answer
“Create a string named name and bind Alice to it. Access the Length property on the object referred to by name, and bind the result to len. Call the ToLower method on the object referred to by name, and bind the returned value to lower. Call the Equals method on the object referred to by name with argument Bob, and bind the returned value to match.”
If your answer differed, note what you missed before continuing.
Properties
Section 1 introduced public and private fields. A public field is accessible from everywhere. A private field is accessible only from inside the class. But sometimes we want something in between: outside code should be able to read a value without being able to change it.
The BankAccount class has a GetBalance method that returns the private balance field. That works, but it means writing acct.GetBalance() every time we want to read the balance. Properties provide a cleaner way to offer controlled access.
A property provides controlled access to data. It defines what happens when outside code reads or writes a value.
class BankAccount
{
private int balance;
public string Owner;
public BankAccount(string owner, int initial)
{
this.Owner = owner;
this.balance = initial;
}
public int Balance
{
get { return this.balance; }
}
public void Deposit(int amount)
{
this.balance = this.balance + amount;
}
public void Withdraw(int amount)
{
if (amount <= this.balance)
{
this.balance = this.balance - amount;
}
}
}Balance (capital B) is a property. balance (lowercase b) is the private field. The property has a get accessor that returns the value of the field.
Outside code reads the balance through the property:
BankAccount acct = new BankAccount("Alice", 500);
acct.Deposit(200);
Console.WriteLine(acct.Balance); // displays 700Translation: “Access the Balance property on the object referred to by acct.”
The property looks like a field access at the call site. There are no parentheses. acct.Balance reads like acct.Owner, but behind the scenes, the get accessor runs and returns the value.
Attempting to write to the property produces an error:
acct.Balance = 0; // Error: property has no set accessorThe property has a get but no set, so it is read-only from outside. Inside the class, the private field balance is still writable through Deposit and Withdraw.
arr.Length works the same way. You can read arr.Length but you cannot write arr.Length = 10. It is a read-only property on array objects, and you have been using it since Chapter 3.
Try it yourself.
Add a read-only property named Count to the Counter class that returns the value of a private count field.
First, change the field from public to private. Then add the property.
Reveal answer
class Counter
{
private int count;
public Counter(int start)
{
this.count = start;
}
public int Count
{
get { return this.count; }
}
public void Increment()
{
this.count = this.count + 1;
}
public void Reset()
{
this.count = 0;
}
}Outside code reads c.Count through the property. It cannot write c.Count = 99 because there is no set accessor. The only way to change the count is through Increment and Reset.
Self-correct against the model above.
Try it yourself.
For each line of outside code, determine whether it compiles or produces an error. Assume the Counter class from the exercise above.
Counter c = new Counter(0);
c.Increment();
Console.WriteLine(c.Count);
c.Count = 10;
c.Reset();Reveal answer
Line 1 compiles. The constructor is public.
Line 2 compiles. Increment is a public method.
Line 3 compiles. Count is a public property with a get accessor.
Line 4 produces an error. Count has no set accessor. Outside code cannot assign to it.
Line 5 compiles. Reset is a public method.
If your answer differed, note what you missed before continuing.
Methods on LinkedListNode
The methods we have written so far operate on simple classes with numeric or string fields. The same mechanics apply to the node class from Section 1:
class LinkedListNode
{
public int Value;
public LinkedListNode? Next;
public LinkedListNode(int value)
{
this.Value = value;
this.Next = null;
}
public bool HasNext()
{
return this.Next != null;
}
}Description: “HasNext returns true if this node has a successor, and false if this node’s Next field is null.”
LinkedListNode a = new LinkedListNode(10);
LinkedListNode b = new LinkedListNode(20);
a.Next = b;
Console.WriteLine(a.HasNext()); // displays True
Console.WriteLine(b.HasNext()); // displays FalseIn the call a.HasNext(), this refers to the node that a points to. That node’s Next field holds a reference to b’s node, which is not null, so HasNext returns true. In the call b.HasNext(), this refers to the node that b points to. That node’s Next is null, so HasNext returns false.
This is a small method, but it follows the same pattern as everything else in this section. The object on the left of the dot becomes this. The method reads fields on this and returns a result.
The limitation becomes clear when we try to do more. Counting all the nodes in a chain, summing their values, or searching for a specific value all require visiting every node from start to finish. A single node knows its own Value and whether it has a Next, but it cannot answer questions about the entire chain without walking through every reference. That traversal is the focus of the next section.
Review
Before continuing, test yourself on what you have learned. 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 method?
- What is a property?
- What does void mean when it appears as a method’s return type?
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.
-
int a = box.Area(); -
c.Increment(); -
acct.Deposit(250); -
bool sq = r.IsSquare(); -
Console.WriteLine(acct.Balance); -
string upper = name.ToUpper();
Check your translations against the patterns shown earlier in this section.
Part 3: Writing Code
Write the code for each description.
-
Define a class named Circle with a private double field radius. Write a constructor that takes a double and binds it to the radius field. Write a method named Area that returns the area (use
Math.PI * this.radius * this.radius). Write a read-only property named Radius that returns the value of the radius field. -
Add a method named ScaleBy to the Circle class that takes a double factor and multiplies the radius by it. The method returns nothing.
-
Create two Circle objects: one with radius 5.0 stored in c1, and one with radius 3.0 stored in c2. Call Area on both and store the results in variables named area1 and area2.
Part 4: Tracing this
Consider this class:
class Scoreboard
{
private int score;
public Scoreboard(int start)
{
this.score = start;
}
public int Score
{
get { return this.score; }
}
public void Add(int points)
{
this.score = this.score + points;
}
public void TransferTo(int points, Scoreboard other)
{
if (points <= this.score)
{
this.score = this.score - points;
other.score = other.score + points;
}
}
}Trace through this code and fill in the state table:
Scoreboard home = new Scoreboard(0);
Scoreboard away = new Scoreboard(0);
home.Add(10);
away.Add(3);
home.TransferTo(4, away);
away.TransferTo(10, home);| After line | home.Score | away.Score |
|---|---|---|
| Construction | ? | ? |
home.Add(10) | ? | ? |
away.Add(3) | ? | ? |
home.TransferTo(4, away) | ? | ? |
away.TransferTo(10, home) | ? | ? |
For the last line, identify which object is this, which is other, and whether the condition passes.
Part 5: Compile or Error
Consider the BankAccount class with the Balance property (no set accessor), the private balance field, and public methods Deposit and Withdraw. For each line of outside code, determine whether it compiles or produces an error. Explain why.
BankAccount a = new BankAccount("Alice", 100);
a.Deposit(50);
Console.WriteLine(a.Balance);
a.Balance = 200;
a.balance = 200;
a.Withdraw(30);
Console.WriteLine(a.Owner);Part 6: Concept Connections
The standalone function Func<Rectangle, int> Area = (r) => r.Width * r.Height; and the method public int Area() { return this.Width * this.Height; } compute the same thing. Describe the three differences between the standalone version and the method version in terms of where the function lives, how the object reaches it, and what the call site looks like.
You can now define operations on your own classes. Methods give objects behavior: they can compute values, change their own state, and interact with other objects. Properties control what outside code can see. The dot-operator calls you have been writing since Chapter 1 are method calls, and you now know how to write your own.
Next, we use these tools to solve problems on linked lists.