C# Programming · Lesson 12

We Have Our Methods

Divide C# programs into instance and static methods with arguments, overloading, default parameters, ref, void methods, and tuples.

  • csharp
  • methods
  • parameters
  • overloading
  • refactoring
  • tuples

Lesson purpose

Programs grow past the point where a person can hold all the code in mind at once; a common rule of thumb is that the code for one task should fit on a single screen. Lesson 12 introduces methods, the way C# divides class code into small, named pieces. A method is the C# equivalent of a function, procedure, or subroutine, and it is always part of a class. Students distinguish instance methods from static methods, refactor the Lesson 5 interest-table program into methods by hand and with Visual Studio’s refactoring tools, write local functions, pass arguments to parameters, overload methods, supply default arguments, return values with return and with call-by-reference, write void methods, and return several values at once with tuples.

Learning objectives

  • Explain why large programs are divided into methods.
  • Distinguish instance methods from static (class) methods and call each correctly.
  • Describe what happens to control when a method is called and when it finishes.
  • Plan methods from a program outline and refactor repeated code into a helper method.
  • Use Visual Studio’s Extract Method, Change Signature, and Rename refactorings.
  • Write a local function and explain when it is appropriate.
  • Distinguish arguments from parameters and pass one or more arguments.
  • Match arguments to parameters by type and order, and read the resulting compiler errors.
  • Overload a method and state the rule that makes overloads legal.
  • Declare default parameter values and call a method with and without them.
  • Return a value with return and through a ref parameter.
  • Write a void method, including an early return.
  • Create and use tuples with Tuple<…>, Tuple.Create, and value-tuple syntax.

1. Why methods

Several Part 1 programs grew long enough to strain a reader. Scrolling back and forth to understand one task makes code hard to follow. Methods break code into chunks that are easy to understand, test, and reuse. Properly designed methods greatly simplify complex programs.

  • A method is a named block of statements that performs one task.
  • A method in C# always belongs to a class.
  • A local function (C# 7.0 and later) is a method declared inside another method.

Throughout the course, method names in text include parentheses, such as InstanceMethod(), to make them easy to recognize.

2. Instance and static methods

class Example
{
    public int anInt;                  // instance field
    public static int staticInt;       // static field

    public void InstanceMethod()       // instance method
    {
        Console.WriteLine("this is an instance method");
    }

    public static void ClassMethod()   // static method
    {
        Console.WriteLine("this is a static method");
    }
}

The instance/static distinction from Lesson 11 applies to methods exactly as it does to data:

Example fieldInstance = new Example();
fieldInstance.anInt = 1;               // instance member: through an object
Example.staticInt = 2;                 // static member: through the class

Example thisInstance = new Example();
thisInstance.InstanceMethod();         // compiles
Example.ClassMethod();                 // compiles

// thisInstance.ClassMethod();         // error: no static access through an instance
// Example.InstanceMethod();           // error: no instance access through the class
  • An instance method runs on a particular object and is called through that object.
  • A static method belongs to the class and is called through the class name. Console.WriteLine() is a static method: Console is the class and WriteLine is the method.
  • Every object has its own copy of instance data, but all objects of a class share the same static members.

Control flow: calling a method passes control to the method’s code. When the method finishes, control returns to the statement right after the call. Output of the example: “this is an instance method” and then “this is a static method.”

Methods can be as large as needed, but aim for short ones that do one job.

Quick check: which of the two commented lines fails, and why?

3. Planning methods: the interest table

The Lesson 5 CalculateInterestTable program is one long Main(). Reading its comments alone reveals the plan:

public static void Main(string[] args)
{
    // Prompt user to enter source principal.
    // If the principal is negative, generate an error message.
    // Prompt user to enter the interest rate.
    // If the interest is negative, generate an error message.
    // Finally, prompt user to input the number of years.
    //
    // Display the input back to the user.
    //
    // Now loop through the specified number of years.
    while (year <= duration)
    {
        // Calculate the value of the principal plus interest.
        // Output the result.
    }
}

Stepping back, the program has three sections:

  1. Input: the user enters principal, interest, and duration.
  2. Verification: the program echoes the input so the user can check it.
  3. Output: the program builds and prints the table.

Two clues show where to divide the code: the three sections, and the fact that the same input logic repeats for principal, interest, and duration. Writing empty methods named for each section and filling them in one at a time is programming by intention.

Refactoring means dividing or cleaning up working code so it is easier to read or meets the organization’s standards, without changing what it does.

On comments: if the comments alone do not explain the program, the comments are weak; if the code cannot be understood without most comments, the method and variable names are unclear or the methods are too big. Good method names beat comments. Real-world code has fewer comments than the heavily commented course examples.

4. Refactoring by hand: CalculateInterestTableWithMethods

Section 1: input with a helper method

public static void InputInterestData(
    ref decimal principal, ref decimal interest, ref decimal duration)
{
    principal = InputPositiveDecimal("principal");
    interest = InputPositiveDecimal("interest");
    duration = InputPositiveDecimal("duration");
}

public static decimal InputPositiveDecimal(string prompt)
{
    while (true)
    {
        Console.Write("Enter " + prompt + ": ");
        string input = Console.ReadLine() ?? "";
        decimal value = Convert.ToDecimal(input);

        if (value >= 0)
        {
            return value;
        }

        Console.WriteLine(prompt + " cannot be negative");
        Console.WriteLine("Try again");
        Console.WriteLine();
    }
}
  • InputPositiveDecimal() is a helper method: the input loop is written once and called three times with a different prompt. That makes the code shorter and easier to debug and update.
  • return value; sends the valid number back to the caller and ends the loop and the method.
  • ref lets InputInterestData() store results in the caller’s variables (Section 9).

Section 3: output

public static void OutputInterestTable(decimal principal, decimal interest, decimal duration)
{
    for (int year = 1; year <= duration; year++)
    {
        decimal interestPaid = principal * (interest / 100);
        principal = principal + interestPaid;
        principal = decimal.Round(principal, 2);
        Console.WriteLine(year + "-" + principal);
    }
}

While writing this method, the programmer thinks only: “Given principal, interest, and duration, print a table.” Input and validation are someone else’s job.

Putting it together

static void Main(string[] args)
{
    // Section 1 -- Input
    decimal principal = 0M;
    decimal interest = 0M;
    decimal duration = 0M;
    InputInterestData(ref principal, ref interest, ref duration);

    // Section 2 -- Verify
    Console.WriteLine();
    Console.WriteLine("Principal     = " + principal);
    Console.WriteLine("Interest      = " + interest + "%");
    Console.WriteLine("Duration      = " + duration + " years");
    Console.WriteLine();

    // Section 3 -- Output
    OutputInterestTable(principal, interest, duration);
}

Main() now reads like the outline. Sample: 1000 at 5.5% for 5 years prints 1-1055.00, 2-1113.02, 3-1174.24, 4-1238.82, 5-1306.96. (The book’s printed sample shows “12-1113.02” for year 2, a typo.)

5. Refactoring with Visual Studio

Visual Studio automates common refactorings from Edit ⇒ Refactor (or Quick Actions, Ctrl+.). Starting from the CalculateInterestTableMoreForgiving program in Lesson 5:

Copy the starting file. Create the new project, delete its Program.cs, then use Add ⇒ Existing Item to add a copy of the Lesson 5 Program.cs.

Extract the output loop.

  1. Select from the declaration of year through the end of the while loop.
  2. Choose Extract Method and type the name OutputInterestTable. Every reference updates as you type.
  3. Apply. The selected code moves into a new private static method below Main(), and a call replaces it:
principal = OutputInterestTable(principal, interest, duration);

Extract and generalize the input loop.

  1. Select the first input while loop and choose Extract Method; name it InputPositiveDecimal. At first it is specific to principal.
  2. Choose Change Signature (Reorder Parameters) and add a string parameter named prompt, with "Principal" as the value for this call.
  3. Change the prompt and error messages inside the method to use prompt, such as Console.Write($"Enter {prompt}: ");.
  4. Rename the method’s principal variable to value so the method is generic.
  5. Replace the remaining input loops with calls such as interest = InputPositiveDecimal("Interest");.

Build and run after each step; the output should never change. That unchanged behavior is what makes it refactoring.

6. Local functions

When only one method calls a helper, the helper can live inside that method as a local function (C# 7.0 and later):

private static void InputInterestData(
    ref decimal principal, ref decimal interest, ref decimal duration)
{
    principal = InputPositiveDecimal("principal");
    interest = InputPositiveDecimal("interest");
    duration = InputPositiveDecimal("duration");

    decimal InputPositiveDecimal(string prompt)
    {
        while (true)
        {
            Console.Write("Enter " + prompt + ": ");
            decimal value = Convert.ToDecimal(Console.ReadLine());
            if (value >= 0)
            {
                return value;
            }
            Console.WriteLine(prompt + " cannot be negative");
            Console.WriteLine("Try again");
            Console.WriteLine();
        }
    }
}
  • A local function has no access modifier or static keyword in this form; it is visible only inside its containing method.
  • It can be declared after the statements that call it.
  • The book also changes InputInterestData() and OutputInterestTable() from public to private.

Design principle: give every method the smallest visibility it needs (private, then protected, then public). The book describes this as more secure; the main benefit is encapsulation, since no other code can call or depend on a helper it cannot see.

7. Arguments and parameters

A method with no inputs and no results is rarely useful:

public static void Output()
{
    Console.WriteLine("this is a method");
}

Real operations need data: a sine needs an angle, and concatenation needs two strings.

  • A parameter is part of the method definition: the complete listing of the inputs it accepts, some of which may be optional.
  • An argument is a value supplied as input when the method is called.
  • Pass arguments by listing them in the parentheses after the method name.
public static void Output(string someString)
{
    Console.WriteLine("Output() was passed the argument: " + someString);
}

Example.Output("Hello");        // Output() was passed the argument: Hello

string myString = "Hello";
Example.Output(myString);       // same result

The method receives a reference to the string "Hello" and names it someString; inside the method, someString works like any other string variable. Main(string[] args) is itself a method that receives command-line arguments as an array (Lesson 7).

Advice: changing a parameter’s value inside the method invites confusion. Copy it to a local variable and change the copy.

Multiple arguments

private static void AverageAndDisplay(string s1, double d1, string s2, double d2)
{
    double average = (d1 + d2) / 2;
    Console.WriteLine($"The average of {s1} whose value is {d1} and {s2} " +
                      $"whose value is {d2} is {average}.");
}

AverageAndDisplay("grade 1", 3.5, "grade 2", 4.0);
// The average of grade 1 whose value is 3.5 and grade 2 whose value is 4 is 3.75.

Arguments are matched to parameters in the order the parameters are declared.

Matching arguments to parameters

Unless arguments are named (Lesson 20), each argument must match its parameter in type and order:

AverageAndDisplay("grade 1", "grade 2", 3.5, 4.0);   // two build errors (CS1503)

The second argument is a string where a double is required, and the third is a double where a string is required. Swapping them fixes the call. In View ⇒ Error List, double-click an error to jump to it, and click the CS error code for documentation.

Quick check: which is the parameter and which is the argument in Output(string someString) called as Output("Hi")?

8. Overloading and default arguments

Overloading

Two methods in the same class can share a name, called overloading, as long as their parameter lists differ in the number or types of required parameters:

private static void AverageAndDisplay(string s1, double d1, string s2, double d2)
{
    double average = (d1 + d2) / 2;
    Console.WriteLine($"The average of {s1} whose value is {d1} and\r\n" +
                      $"{s2} whose value is {d2} is {average}.\r\n");
}

private static void AverageAndDisplay(double d1, double d2)
{
    double average = (d1 + d2) / 2;
    Console.WriteLine($"The average of {d1} and {d2} is {average}.");
}

AverageAndDisplay("my GPA", 3.5, "your GPA", 4.0);   // first version
AverageAndDisplay(3.5, 4.0);                         // second version

C# picks the version whose signature matches the call: AverageAndDisplay(string, double, string, double) versus AverageAndDisplay(double, double). Overloads cannot differ only in optional parameters, because the compiler could not tell the calls apart. The \r\n escape sequence starts a new line.

Default arguments

A default argument is a predefined value used when the caller omits an argument. Two approaches:

  • Overloading: write a shorter overload that fills in the missing value. This hides the default inside a method body, confuses readers about why the overload exists, and doubles the code to maintain.
  • Default parameters: put the default in the parameter list with =. Anyone reading the declaration sees the default immediately. The course uses this approach.
static private string DisplayRoundedDecimal(decimal value = 0,
                                            int numberOfSignificantDigits = 2)
{
    decimal roundedValue = decimal.Round(value, numberOfSignificantDigits);
    return Convert.ToString(roundedValue);
}

Console.WriteLine(DisplayRoundedDecimal());                // 0
Console.WriteLine(DisplayRoundedDecimal(12.345678M));      // 12.35
Console.WriteLine(DisplayRoundedDecimal(12.345678M, 3));   // 12.346
  • Two decimal places is the natural default because decimals often hold money.
  • Visual Studio shows the default values in the method’s tooltip.
  • Parameters with defaults must come after all parameters without defaults.

9. Returning values: return and call-by-reference

A method can return a value to its caller in two ways:

  • the return statement, used by most examples so far, and
  • the call-by-reference feature.
private static void Average(ref double result, double input1 = 1.0, double input2 = 2.0)
{
    result = (input1 + input2) / 2;
}

double result = 0;
Console.WriteLine(result);          // 0
Average(ref result, 4.0, 3.0);
Console.WriteLine(result);          // 3.5
  • ref passes a reference to the caller’s variable rather than a copy of its value, so the method can change the variable as the caller sees it.
  • ref must appear in both the declaration and the call, and the caller’s variable must be assigned before the call.
  • The book warns that the ref parameter must come first here. The underlying rule is that required parameters, including ref parameters (which cannot have defaults), must come before optional ones.
  • Related keyword: out also passes by reference, but the method must assign it and the caller need not initialize it. int.TryParse(input, out int number) from Lesson 3 uses out.

Classroom guidance: prefer return for a single result. Use ref when a method must update the caller’s variables, as InputInterestData() does, and out or a tuple when a method needs to produce several results.

10. Methods with no return value

Every method declaration states a return type. When a method returns nothing, the return type is void (“no type”):

private static void AverageAndDisplay(double d1, double d2)

(The book writes this declaration without parameter names, (double, double), which is not valid code; parameter names are required.)

  • A void method returns no value. A method that returns a value is a nonvoid method.
  • A nonvoid method must end by executing return followed by a value of its return type.
  • A void method returns when it reaches return; with no value, or automatically at its closing brace.
private static void DisplayRatio(double numerator, double denominator)
{
    if (denominator == 0.0)
    {
        Console.WriteLine("The denominator of a ratio cannot be 0");
        return;                      // early return
    }

    double ratio = numerator / denominator;
    Console.WriteLine($"The ratio of {numerator} over {denominator} is {ratio}.");
}                                    // normal exit

For double values, dividing by zero does not throw; it produces infinity (or NaN for 0/0). The early return avoids showing the user a meaningless result.

Quick check: which methods in this lesson are void, and which return a value?

11. Returning multiple values with tuples

Before C# 7.0, a method returned a single object. A tuple groups several values so one method call can hand back more than one result, such as a quotient and remainder or the real and imaginary parts of a complex number.

The Tuple class

private static Tuple<string, int> GetTuple()
{
    return new Tuple<string, int>("Hello", 123);
}

Console.WriteLine(GetTuple().Item1 + " " + GetTuple().Item2);   // Hello 123
  • The angle brackets list each item’s type; the constructor supplies the values.
  • Items are read as Item1, Item2, and so on.
  • The sample calls GetTuple() twice; storing the result in a variable first avoids repeating the work.

Tuple.Create and value tuples

var myTuple = Tuple.Create("Hello", 123);            // types inferred
Console.WriteLine(myTuple.Item1 + "\t" + myTuple.Item2);

(string, int) t1 = ("Hello", 123);                    // value tuple
Console.WriteLine($"{t1.Item1}\t{t1.Item2}");

(string word, int number) t2 = ("Hello", 123);        // named elements
Console.WriteLine(t2.word);
  • Tuple.Create builds a tuple without spelling out the types. The book calls this less safe because of var; var is still strongly typed, so the inferred Tuple<string, int> behaves exactly the same.
  • The parenthesized form creates a ValueTuple, which is lighter and faster than the Tuple class and allows named elements. It is the modern choice for returning several values, for example static (int min, int max) Range(int[] values).

More than two items

The Tuple class provides versions for one through seven items; an eighth type parameter, TRest, holds another tuple for longer lists. Nesting allows almost any number of items, but a method returning many values usually signals that a class would be clearer.

static Tuple<string, int, bool>[] GetTuples()
{
    Tuple<string, int, bool>[] aTuple =
    {
        new Tuple<string, int, bool>("One", 1, true),
        new Tuple<string, int, bool>("Two", 2, false),
        new Tuple<string, int, bool>("Three", 3, true)
    };
    return aTuple;
}

var myTuple2 = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14);   // value tuple
Console.WriteLine(myTuple2.Item11);                              // 11

An array of tuples works like a small data set. A value tuple can hold more than eight items directly, but it cannot be used in foreach; the book’s workaround converts it to a string and splits it.

Classroom application

  1. Take a working Lesson 5 program and write its outline as comments only. Identify the input, verification, and output sections.
  2. Refactor by hand: create InputPositiveDecimal(string prompt), InputInterestData(ref …), and OutputInterestTable(…), and confirm the output does not change.
  3. Repeat the refactoring with Visual Studio’s Extract Method, Change Signature, and Rename.
  4. Move InputPositiveDecimal() into InputInterestData() as a local function.
  5. Write overloaded Area(double side) and Area(double width, double height) methods, then replace a third overload with a default parameter.
  6. Write Divide(int dividend, int divisor) three ways: returning a (int quotient, int remainder) tuple, using two out parameters, and printing both results from a void method that returns early when divisor is 0.
  7. Call a method with its arguments in the wrong order and read the CS1503 errors.

Common misconceptions

  • A static method is called through the class, an instance method through an object; mixing them does not compile.
  • A method call returns control to the statement after the call, not to the top of the program.
  • A parameter is in the definition; an argument is in the call.
  • Unnamed arguments must match parameters in both type and order.
  • Overloads must differ in the number or types of required parameters; a different return type alone is not enough.
  • Parameters with default values must come after required parameters.
  • ref must appear in both the declaration and the call.
  • A void method still has a declared return type: void.
  • A nonvoid method must return a value on every path.
  • Floating-point division by zero produces infinity rather than an exception.
  • var does not make a tuple less type-safe.
  • Refactoring changes structure, never behavior.

Lesson summary

Methods divide a class’s code into small, named tasks. Instance methods act on an object; static methods belong to the class. Planning from an outline, extracting repeated code into helper methods, and using Visual Studio’s refactoring tools turn a long Main() into readable steps, and local functions keep one-use helpers private. Arguments supply values to parameters in matching type and order, overloading reuses a name with different parameter lists, and default parameters make common calls short. Methods return results with return, through ref (or out) parameters, or as tuples, and void methods simply finish their work. Lesson 13 explains the this reference and how methods work with the objects they belong to.