Methods with Numbers

A tax accountant does not rederive the tax formula every time a client walks in. They work it out once, give it a name (“standard deduction calculation”), and apply it to each client’s numbers. Similarly in programming, we often find ourselves writing the same computation in multiple places. In the control flow section, we wrote loops that computed digit sums, counted even numbers, and accumulated products. Each time we needed one of those computations, we wrote the loop from scratch.

We solve this kind of problem with methods. A method lets us write a computation once, give it a name, and call that name wherever the computation is needed. Numeric methods often need loops, local variables, and multiple statements. The method bodies get longer than the short boolean expressions from Chapter 1, but the basic job is the same: bind inputs to parameters, compute a result, and return it.

Small Methods

Start with a method whose body has one return statement.

static int Twice(int n)
{
    return n * 2;
}

Translation: “Define a method named Twice that takes an integer n and returns n multiplied by 2.”

The first int is the return type. It says the method returns an integer. Twice is the method name. (int n) is the parameter list: the method takes one integer parameter named n. The body is the block inside the braces. The return statement gives the result back to the calling code.

        ┌─────────────────┐
  n ───►│     n * 2       │───► result
  (5)   │                 │    (10)
        └─────────────────┘
             Twice

An integer enters on the left. The method multiplies it by 2. The result emerges on the right.

Not every method returns the same type it takes in. IsEven takes an integer and returns a boolean:

static bool IsEven(int n)
{
    return n % 2 == 0;
}

Translation: “Define a method named IsEven that takes an integer n and returns whether the remainder of n divided by 2 equals 0.”

The return type is bool, so the method must return either true or false. The parameter type is int, so callers must pass an integer.

Methods can also take multiple inputs. Add takes two integers and returns their sum:

static int Add(int a, int b)
{
    return a + b;
}

Translation: “Define a method named Add that takes two integers a and b and returns the result of a plus b.”

The parameter list has two entries, separated by a comma. Each parameter gets its own type and name.

The grade classifier we wrote in the control flow section took a score and produced a letter. As a method, it has one integer parameter and a string return type. We will write it shortly.


Try it yourself.

Translate this method definition to English:

static int Square(int n)
{
    return n * n;
}
Reveal answer

“Define a method named Square that takes an integer n and returns n multiplied by n.”

If your answer differed, note what you missed before continuing.


Try it yourself.

Write a method definition from this description:

Define a method named IsNegative that takes an integer n and returns whether n is less than 0.

Reveal answer
static bool IsNegative(int n)
{
    return n < 0;
}

Self-correct against the model above.


We can also use switch expressions inside a method body:

static string Classify(int n)
{
    return n switch
    {
        > 0 => "positive",
        < 0 => "negative",
        _ => "zero"
    };
}

Translation: “Define a method named Classify that takes an integer n and returns ‘positive’ where n is greater than 0, ‘negative’ where n is less than 0, and ‘zero’ otherwise.”

This connects directly to what we covered with switch expressions. The switch expression produces a value from a pattern match. Returning it from a method gives the computation a name.


Try it yourself.

Write a method named LetterGrade that takes an integer score and returns a string: “A” for 90 and above, “B” for 80 and above, “C” for 70 and above, “D” for 60 and above, and “F” otherwise.

Reveal answer
static string LetterGrade(int score)
{
    return score switch
    {
        >= 90 => "A",
        >= 80 => "B",
        >= 70 => "C",
        >= 60 => "D",
        _ => "F"
    };
}

Self-correct against the model above.


How a Method Call Executes

We have already called built-in methods such as Console.WriteLine and int.Parse. A method we define follows the same call pattern: evaluate the arguments, bind them to parameters, run the method body, and return to the call site.

Numeric methods can contain loops, local variables, and multiple lines of code. To trace these, we need a clearer picture of what happens when a method is called.

Here is a simple example. Assume the Twice method from earlier is already defined:

int x = 3;
int result = Twice(x);

When the program reaches Twice(x), here is what happens:

1. Evaluate x (get 3).
2. Call Twice with the value 3.
   Inside Twice, bind 3 to n.
   Evaluate n * 2 → 6.
   Return 6.
3. Back where Twice was called, bind 6 to result.

The calling code pauses at the method call. Execution moves to the method body: the method binds the argument to its parameter, runs its body, and returns the result. Then execution continues where the call happened.

The method has its own variable, n, that exists only while the method runs. It does not affect x in the calling code. After the method returns, n is destroyed.

We will use this format throughout the section. The indentation shows when execution is inside a method. “Inside [method]” marks entry into the method body. “Back where [method] was called” marks the return.


Try it yourself.

Trace the execution of this code. Assume the IsEven method from earlier is already defined:

int value = 7;
bool even = IsEven(value);

Write the step-by-step trace, then check.

Reveal answer
  1. Bind 7 to value
  2. Evaluate value (get 7)
  3. Call IsEven with 7
  4. Inside IsEven, bind 7 to n
  5. Evaluate n % 2 0 → 7 % 2 0 → 1 == 0 → false
  6. Return false
  7. Back where IsEven was called, bind false to even

If your answer differed, note what you missed before continuing.


Multi-Statement Method Bodies

Curly braces create the method body. The body may contain local variables, loops, conditionals, and multiple return paths.

Numeric methods often contain loops and local variables.

In the control flow section, we wrote this loop to compute the sum of a number’s digits:

int n = 7364;
int digitSum = 0;
 
while (n > 0)
{
    digitSum += n % 10;
    n /= 10;
}
 
Console.WriteLine(digitSum);

This works for 7364, but what if we need the digit sum of a different number? We wrap the loop in a method:

static int SumDigits(int n)
{
    int digitSum = 0;
 
    while (n > 0)
    {
        digitSum += n % 10;
        n /= 10;
    }
 
    return digitSum;
}

Translation: “Define a method named SumDigits that takes an integer n and returns the sum of its digits.”

The method body is the same loop we wrote earlier, now enclosed in curly braces with a return statement at the end. The parameter n replaces the hardcoded value. The local variable digitSum exists only inside the method body.

To call it:

int total = SumDigits(7364);

Translation: “Call SumDigits with the value 7364 and bind the returned value to an integer variable named total.”

Let’s trace this call:

  1. Call SumDigits with 7364
  2. Inside SumDigits, bind 7364 to n
  3. Create digitSum and bind 0 to it
  4. Execute the loop:
iterationn (start)n % 10digitSumn /= 10
1736444736
273661073
3733137
477200
  1. n is 0, loop exits
  2. Return 20
  3. Back where SumDigits was called, bind 20 to total

The iteration table is the same format we used for loops. The new piece is the framing around it: we enter the method, run the loop inside it, and return when it finishes.


Try it yourself.

Write a method named Factorial that takes an integer n and returns the factorial of n. The factorial of some number n, written as , is the product of all the numbers from 1 to n, or . So would be equal to .

Use a loop with an accumulator. Start the accumulator at 1 (the identity for multiplication).

Reveal answer
static int Factorial(int n)
{
    int product = 1;
    int i = 1;
 
    while (i <= n)
    {
        product *= i;
        i++;
    }
 
    return product;
}

Translation: “Define a method named Factorial that takes an integer n and returns the factorial of n.”

Self-correct against the model above.


Try it yourself.

Write a method named CountEvensUpTo that takes an integer n and returns the count of even numbers from 1 through n. For example, CountEvensUpTo(10) returns 5.

Reveal answer
static int CountEvensUpTo(int n)
{
    int count = 0;
    int i = 1;
 
    while (i <= n)
    {
        if (i % 2 == 0)
        {
            count++;
        }
        i++;
    }
 
    return count;
}

For n = 10:

iterationii % 2 == 0count
11false0
22true1
33false1
44true2
55false2
66true3
77false3
88true4
99false4
1010true5

Self-correct against the model above.


Methods Calling Methods

Methods can call other methods. This lets us build complex computations from simpler ones.

Suppose we want the average value of a number’s digits. The digit sum of 7364 is 20, and it has 4 digits, so the average is 20 / 4 = 5. We already have SumDigits. We need CountDigits:

static int CountDigits(int n)
{
    int count = 0;
 
    while (n > 0)
    {
        count++;
        n /= 10;
    }
 
    return count;
}

Now AverageDigit calls both:

static int AverageDigit(int n)
{
    int sum = SumDigits(n);
    int count = CountDigits(n);
    return sum / count;
}

Translation: “Define a method named AverageDigit that takes an integer n and returns the sum of its digits divided by the number of digits.”

Let’s trace AverageDigit(7364):

1. Call AverageDigit with 7364.
   Inside AverageDigit, bind 7364 to n.

2. Call SumDigits with 7364.
     Inside SumDigits, bind 7364 to n.
     [Loop executes: 4 iterations, digitSum accumulates to 20]
     Return 20.
   Back in AverageDigit, bind 20 to sum.

3. Call CountDigits with 7364.
     Inside CountDigits, bind 7364 to n.
     [Loop executes: 4 iterations, count accumulates to 4]
     Return 4.
   Back in AverageDigit, bind 4 to count.

4. Evaluate sum / count → 20 / 4 → 5.
   Return 5.

Back where AverageDigit was called, bind 5 to result.

Each method call moves execution to that method’s body. When that method returns, execution picks up where the call happened. When AverageDigit calls SumDigits, execution moves to SumDigits. When SumDigits returns, execution continues inside AverageDigit.

Notice that SumDigits has its own n, and AverageDigit has its own n. These are separate variables. Each method gets its own copy of its parameters. Changing n inside SumDigits (the loop divides it by 10 each iteration) does not affect n inside AverageDigit. This is value type behavior, the same principle from Chapter 1.


Try it yourself.

Write a method named SumSquares that takes an integer n and returns the sum of the squares from 1 to n. For example, SumSquares(3) returns 1 + 4 + 9 = 14.

First write a method named Square that takes an integer and returns its square. Then write SumSquares using Square inside a loop.

Reveal answer
static int Square(int n)
{
    return n * n;
}
 
static int SumSquares(int n)
{
    int sum = 0;
    int i = 1;
 
    while (i <= n)
    {
        sum += Square(i);
        i++;
    }
 
    return sum;
}

For n = 3:

iterationiSquare(i)sum
1111
2245
33914

Check your answer against the model.


Recursion

We have built every computation so far with loops. Initialize a variable, repeat some steps, return the result. Loops describe how to build an answer, one step at a time.

Mathematics often takes a different approach. Instead of describing steps, it defines an answer in terms of a smaller version of the same problem. Consider factorial. In math, it is often written as a piecewise definition:

factorial(n) = 1                       if n ≤ 1
factorial(n) = n × factorial(n - 1)    otherwise

Two cases. If n is 1 or less, the answer is just 1. Otherwise, the answer is n times the factorial of (n - 1).

Let’s compute factorial(4) by hand, following this definition:

factorial(4) = 4 × factorial(3)           (4 is not ≤ 1, so use the second case)
             = 4 × 3 × factorial(2)       (3 is not ≤ 1, second case again)
             = 4 × 3 × 2 × factorial(1)   (2 is not ≤ 1, second case again)
             = 4 × 3 × 2 × 1              (1 is ≤ 1, so the answer is 1)
             = 24

Each step asks: which case applies? If n is still greater than 1, we replace factorial(n) with n × factorial(n - 1) and continue. When n reaches 1, we stop.

The first case, where we stop and return a value directly, is the base case. The second case, where we use a smaller version of the same problem, is the recursive case. Every piecewise definition like this needs both. The base case provides a concrete answer. The recursive case breaks the problem down until it reaches the base case.

Definition. Recursion is when a method computes its result by calling itself with smaller inputs until it reaches a base case.

Definition. A base case is a condition under which a recursive method returns a value without calling itself.

Definition. A recursive case is the branch where the method calls itself with an input closer to a base case and uses the returned value to compute its result.

We can write this piecewise definition directly in C#:

static int Factorial(int n)
{
    if (n <= 1) return 1;
    return n * Factorial(n - 1);
}

Translation: “Define a method named Factorial that takes an integer n and returns the factorial of n. It has a base case: if n is less than or equal to 1, return 1. It has a recursive case: return n multiplied by the result of calling Factorial with n minus 1.”

The code mirrors the piecewise math. The if statement checks for the base case. If it applies, the method returns 1. Otherwise, the method uses the smaller-input step n - 1, calls Factorial on that smaller input, and multiplies by n.

Let’s trace Factorial(4) the way we trace any method call, using the Inside/Back format:

Call Factorial with 4.
  Inside Factorial, bind 4 to n.
  n <= 1 is false.
  Evaluate 4 * Factorial(3).

    Call Factorial with 3.
      Inside Factorial, bind 3 to n.
      n <= 1 is false.
      Evaluate 3 * Factorial(2).

        Call Factorial with 2.
          Inside Factorial, bind 2 to n.
          n <= 1 is false.
          Evaluate 2 * Factorial(1).

            Call Factorial with 1.
              Inside Factorial, bind 1 to n.
              n <= 1 is true.
              Return 1.

          Back in Factorial: 2 * 1 = 2. Return 2.

        Back in Factorial: 3 * 2 = 6. Return 6.

    Back in Factorial: 4 * 6 = 24. Return 24.

Back where Factorial was called, bind 24 to result.

Each call waits for the one below it to finish. Factorial(4) cannot compute 4 * Factorial(3) until Factorial(3) returns. This continues until Factorial(1) hits the base case and returns 1. Then the results flow back up.

Look at both versions side by side:

Loop:

static int Factorial(int n)
{
    int product = 1;
    int i = 1;
 
    while (i <= n)
    {
        product *= i;
        i++;
    }
 
    return product;
}

Recursion:

static int Factorial(int n)
{
    if (n <= 1) return 1;
    return n * Factorial(n - 1);
}

The loop version needs an accumulator, a counter, and a while condition. That is a good fit when the problem is “repeat this many times while updating state.” The recursive version names the base case and the smaller-input step directly. When a problem is already described as “this answer depends on the same answer for a smaller input,” recursive code can match the definition more closely.


Try it yourself.

Trace Factorial(3) step by step. Write the full chain of calls, then check.

Reveal answer
Call Factorial with 3.
  Inside Factorial, bind 3 to n.
  n <= 1 is false.
  Evaluate 3 * Factorial(2).

    Call Factorial with 2.
      Inside Factorial, bind 2 to n.
      n <= 1 is false.
      Evaluate 2 * Factorial(1).

        Call Factorial with 1.
          Inside Factorial, bind 1 to n.
          n <= 1 is true.
          Return 1.

      Back in Factorial: 2 * 1 = 2. Return 2.

    Back in Factorial: 3 * 2 = 6. Return 6.

Back where Factorial was called, bind 6 to result.

If your answer differed, note what you missed before continuing.


Here is a second example. We wrote SumTo as a loop earlier: sum the integers from 1 through n. We can also write it recursively:

static int SumTo(int n)
{
    if (n <= 0) return 0;
    return n + SumTo(n - 1);
}

Translation: “Define a method named SumTo that takes an integer n and returns the sum of integers from 1 to n. It has a base case: if n is less than or equal to 0, return 0. It has a recursive case: return n plus the result of calling SumTo with n minus 1.”

The logic: the sum from 1 to n equals n plus the sum from 1 to (n - 1). The base case is the sum from 1 to 0, which is 0. The smaller-input step is n - 1; each call reduces n by 1, moving toward the base case.

Let’s verify with SumTo(4):

SumTo(4) = 4 + SumTo(3)
         = 4 + 3 + SumTo(2)
         = 4 + 3 + 2 + SumTo(1)
         = 4 + 3 + 2 + 1 + SumTo(0)
         = 4 + 3 + 2 + 1 + 0
         = 10

This shortened form is useful for checking that the recursion produces the correct result. The full trace with “Inside / Back where [method] was called” framing shows the execution mechanism. The shortened form shows the mathematical structure.


Try it yourself.

Write a recursive method named CountDigits that takes an integer n and returns the number of digits in n. For example, CountDigits(7364) returns 4.

Hint: the base case is a single-digit number, which has 1 digit. The smaller-input step is n / 10, which removes the last digit.

Reveal answer
static int CountDigits(int n)
{
    if (n < 10) return 1;
    return 1 + CountDigits(n / 10);
}

Translation: “Define a method named CountDigits that takes an integer n and returns the number of digits in n. It has a base case: if n is less than 10, return 1. It has a recursive case: return 1 plus the result of calling CountDigits with n divided by 10.”

Verification:

CountDigits(7364) = 1 + CountDigits(736)
                  = 1 + 1 + CountDigits(73)
                  = 1 + 1 + 1 + CountDigits(7)
                  = 1 + 1 + 1 + 1
                  = 4

Self-correct against the model above.


Loops vs. Recursion

Loops and recursion are two strategies for building computations. A loop says how to build the answer step by step. A recursive definition says what the answer is in terms of a base case and a smaller input.

Use loops when the work is mainly counter-based repetition, input validation, array mutation, or ordinary search. Those problems already have state that changes one step at a time: a counter advances, the user enters another value, an array element is updated, or a search position moves forward.

Use recursion when the problem naturally splits into a base case plus the same problem on a smaller input. Factorial uses n <= 1 as the base case and n - 1 as the smaller input. CountDigits uses n < 10 as the base case and n / 10 as the smaller input. Later, linked lists will give us another smaller input: the rest of the chain after the first node.


Try it yourself.

Write a method named Power that takes two integers, base and exponent, and returns base raised to the exponent. Write it twice: once with a loop, once with recursion.

Hint for the loop: start an accumulator at 1, multiply by base for each step from 1 to exponent.

Hint for recursion: the base case is exponent 0, where the result is 1. The smaller-input step is exponent minus 1.

Reveal answer

Loop version:

static int Power(int b, int exp)
{
    int result = 1;
    int i = 1;
 
    while (i <= exp)
    {
        result *= b;
        i++;
    }
 
    return result;
}

Recursive version:

static int Power(int b, int exp)
{
    if (exp == 0) return 1;
    return b * Power(b, exp - 1);
}

Verification of the recursive version with Power(2, 4):

Power(2, 4) = 2 * Power(2, 3)
            = 2 * 2 * Power(2, 2)
            = 2 * 2 * 2 * Power(2, 1)
            = 2 * 2 * 2 * 2 * Power(2, 0)
            = 2 * 2 * 2 * 2 * 1
            = 16

Check your answers against the models.


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 recursion?
  2. What is a base case?
  3. What is a recursive case?

Also recall:

  1. What is a method?
  2. What is a parameter?
  3. What is an argument?

If any of your answers differed from the definitions, note what you missed and write the corrected version.

Part 2: Method Headers

Write the meaning of each method header.

  1. static int Square(int n)
  2. static bool IsEven(int n)
  3. static string LetterGrade(int score)
  4. static int Add(int a, int b)

Check your answers against the explanations in this section.

Part 3: Translations

Translate each piece of code to English.

  1. static int Triple(int n)
    {
        return n * 3;
    }
  2. static bool IsDivisibleBy3(int n)
    {
        return n % 3 == 0;
    }
  3. int result = SumDigits(n);
  4. static int SumTo(int n)
    {
        if (n <= 0) return 0;
        return n + SumTo(n - 1);
    }

Check your translations against the patterns shown in this section.

Part 4: Writing Code

Write C# code for each description.

  1. Define a method named Cube that takes an integer n and returns n multiplied by n multiplied by n.

  2. Define a method named SumOddsUpTo that takes an integer n and returns the sum of all odd numbers from 1 through n. Use a loop with a filter.

  3. Define a recursive method named SumDigits that takes an integer n and returns the sum of its digits. It has a base case: if n is less than 10, return n. It has a recursive case: return (n modulo 10) plus the result of calling SumDigits with n divided by 10.

Check your code against the examples in this section.

Part 5: Tracing

  1. Trace the execution of this code. Show the step-by-step flow including what happens inside the method.
int x = 5;
int y = Twice(x);
  1. Trace SumTo(3) using the full trace format (Inside / Back where [method] was called).

  2. Trace CountDigits(84) using the shortened form.

Check your traces against the examples in this section.

Part 6: Complete Programs

Write a complete program that asks the user for a positive integer and displays the sum of its digits. Use a method named SumDigits. If the user enters a non-positive number, ask again until they provide a valid one. Use a loop for input validation; the recursion belongs inside SumDigits, after the program has a valid positive number.

Check your program by tracing through it with sample inputs.


You now know how to define your own numeric computations. You can package loops and logic into named methods, trace how execution moves between calling code and method bodies, call methods from inside other methods, and use recursion when a problem has a base case and a smaller-input step. In the next section, we’ll group related numeric data into custom types.