CHAPTER 3
Previous chapters show how to write code in the Main method. That’s the program entry point, but it’s normally a lightweight method without too much code. For this chapter, you’ll learn how to move your code out of the Main method and modularize it so you can manage the code better. You’ll learn how to define methods with parameters and return values. You’ll also learn about properties, which let you encapsulate object state.
We’ll use a simpler version of the calculator from the previous chapter to get started. This calculator only performs addition and stops running after one operation.
using System; class Calculator1 { static void Main() { Console.Write("First Number: "); string firstNumberInput = Console.ReadLine(); double firstNumber = double.Parse(firstNumberInput); Console.Write("Second Number: "); string secondNumberInput = Console.ReadLine(); double secondNumber = double.Parse(secondNumberInput); double result = firstNumber + secondNumber; Console.WriteLine($"\n\tYour result is {result}."); Console.ReadKey(); } } |
The part of this program that might be new is the Console.ReadKey statement at the end of the Main method. This allows users to see the result and keeps the program from ending until they press a key. The \n in the interpolated string is a newline and \t is a tab.
Although the previous program is small, a first glance doesn’t really tell you what it does. Imagine if it was like the calculator in Chapter 2 or even longer; it would eventually become difficult to understand. When you have to work on this again later, you might need to read many lines of code to understand it. So, it would be better to refactor this. Refactoring is the practice of changing the design of code without changing its functionality; the purpose is to improve the program. The following code sample is a first draft of refactoring this program into methods.
using System; class Calculator2 { static void Main() { double firstNumber = GetFirstNumber(); double secondNumber = GetSecondNumber(); double result = AddNumbers(firstNumber, secondNumber); PrintResult(result); Console.ReadKey(); } static double GetFirstNumber() { Console.Write("First Number: "); string firstNumberInput = Console.ReadLine(); double firstNumber = double.Parse(firstNumberInput); return firstNumber; } static double GetSecondNumber() { Console.Write("Second Number: "); string secondNumberInput = Console.ReadLine(); double secondNumber = double.Parse(secondNumberInput); return secondNumber; } static double AddNumbers(double firstNumber, double secondNumber) { return firstNumber + secondNumber; } static void PrintResult(double result) { Console.WriteLine($"\nYour result is {result}."); } } |
Looking at Main, you can tell what the program does. It reads two numbers, adds the results, and then shows the results to the user. Each of those lines is a method call. The first three methods—GetFirstNumber, GetSecondNumber, and AddNumbers—return a value that is assigned to a variable. The last method, PrintResult, performs an action without returning a result. Before moving to the next refactoring, let’s walk through these methods. The following code listing shows the GetFirstNumber method.
static double GetFirstNumber() { Console.Write("First Number: "); string firstNumberInput = Console.ReadLine(); double firstNumber = double.Parse(firstNumberInput); return firstNumber; } |
At first glance, the signature of this method looks similar to the Main method. The differences are that the return type of this method is double and the method is named GetFirstNumber. All we did was write the method and the code that creates the firstNumber. When a method has a return type, a value of that type must be returned. GetFirstNumber does that with the return statement.
The GetSecondNumber method is nearly identical to GetFirstNumber. Let’s examine AddNumbers next.
static double AddNumbers(double firstNumber, double secondNumber) { return firstNumber + secondNumber; } |
Notice that Main passes the firstNumber and secondNumber variables to AddNumbers as arguments that the AddNumbers method can work with as parameters. The return type of AddNumbers is double, so the method adds and returns the result of the add operation.
Finally, we have the PrintResult method.
static void PrintResult(double result) { Console.WriteLine($"\nYour result is {result}."); } |
The PrintResult method writes the results from its parameter to the console. Notice that PrintResult does not have a return type, as indicated by the void keyword.
The last section improved the program because a huge block of code was broken into more meaningful pieces. We can improve this code with some extra refactoring. In particular, the GetFirstNumber and GetSecondNumber methods are largely redundant. The following sample shows how to refactor those two methods into one and reduce the amount of code.
using System; class Calculator3 { static void Main() { double firstNumber = GetNumber("First"); double secondNumber = GetNumber("Second"); double result = AddNumbers(firstNumber, secondNumber); PrintResult(result); Console.ReadKey(); } static double GetNumber(string whichNumber) { Console.Write($"{whichNumber} Number: "); string numberInput = Console.ReadLine(); double number = double.Parse(numberInput); return number; } static double AddNumbers(double firstNumber, double secondNumber) { return firstNumber + secondNumber; } static void PrintResult(double result) { Console.WriteLine($"\nYour result is {result}."); } } |
This time I removed GetFirstNumber and GetSecondNumber and replaced them with GetNumber. The only real difference besides variable names is the whichNumber string parameter.
The previous examples performed all of the operations inside of the same class. It was driven from the Main method and serviced through methods. What if I wanted to reuse the calculator functions in that class and wanted the new class to hold its own values, or state? In this case, moving the calculator methods into a separate Calculator class would be useful.
The next question to ask is, "How do we get to the state of the class?” For example, if I want to read the result from the Calculator class, what is the best way to do so? One approach is to use a method named GetResult that returns the value. Another way in C# is to use a property, which you can use like a field, but works like a method. The following version of the calculator program shows how to refactor methods into a separate class and add properties.
Note: Refactoring is the practice of changing the design of code without changing its behavior with the goal of improving the code. Martin Fowler’s book, Refactoring: Improving the Design of Existing Code, is a good reference.
using System; public class Calculator4 { double[] numbers = new double[2]; public double First { get { return numbers[0]; } } public double Second { get { return numbers[1]; } } double result; public double Result { get { return result; } set { result = value; } } public void GetNumber(string whichNumber) { Console.Write($"{whichNumber} Number: "); string numberInput = Console.ReadLine(); double number = double.Parse(numberInput); if (whichNumber == "First") numbers[0] = number; else numbers[1] = number; } public void AddNumbers() { Result = First + Second; } public void PrintResult() { Console.WriteLine($"\nYour result is {result}."); } } |
In the previous code listing, First, Second, and Result are properties. I’ll break down the syntax shortly, but first look at how these properties are used inside of the AddNumbers and PrintResults methods. AddNumbers reads the values of First and Second and adds those values together and writes to Result.
Each of these properties looks just like a field or variable; you just read from and write to them. PrintResult reads the Result property. However, looking at the definitions of the properties, you can tell right away that they aren’t fields.
The Result property is a typical read and write property with get and set accessors. When you read the property, the get accessor executes. When you write to the property, the set accessor executes. Notice that there is a result field (lowercase) prior to the Result (uppercase) property. The get accessor reads the value of result and the set accessor writes to result. When using set, the value keyword represents what is being written to the property.
This pattern of reading and writing from a single backing store is so common that C# has a shortcut syntax you can use instead. The following code sample shows Result rewritten as an auto-implemented property.
public double Result { get; set; } |
The backing store in auto-implemented properties is implied and handled behind the scenes by the C# compiler. If you need to provide validation on the value being assigned or have a unique way of storing the value, you should resort to full properties where the get accessor, set accessor, or both are defined.
In fact, First and Second properties have a unique backing store, requiring a fully implemented get accessor. They read from an array position. Notice that the GetNumber method figures out which array position to put each number into.
Properties give you the ability to encapsulate the internal operations of your class so you are free to modify the implementation without breaking the interface for consumers of your class. The following code sample demonstrates how consuming code might use this new Calculator4 class.
using System; class Program { static void Main() { var calc4 = new Calculator4(); calc4.GetNumber("First"); calc4.GetNumber("Second"); calc4.AddNumbers(); PrintResult(calc4); Console.ReadKey(); } static void PrintResult(Calculator4 calc) { Console.WriteLine(); Console.WriteLine($"Your result is {result}."); } } |
The Main method creates a new instance of Calculator4 and calls public methods. All of the strange internals of Calculator4 are hidden and Main only cares about the public interface, exposing Calculator4 services for reuse. The PrintResult method reads the Calculator4 instance Result property. Again, that’s the benefit of methods and properties: callers can use a class without caring how that class does what it does.
C# has a feature called structured exception handling that lets you work with situations where your methods aren’t able to fulfill their intended purpose. The syntax for managing exception handling is the try-catch block. All the code to be monitored for exceptions goes in the try block, and the code that handles a potential exception goes in a catch block. The following code listing shows an example.
static void HandleNullReference() { Program prog = null; try { Console.WriteLine(prog.ToString()); } catch (NullReferenceException ex) { Console.WriteLine(ex.Message); } } |
In C#, any time you try to use a member of a null object, you’ll receive a NullReferenceException. The solution to fix the problem is to assign a value to the variable. The previous example causes a NullReferenceException to be thrown in the try block by calling ToString on the prog variable, which is null.
Since the code that threw the exception is inside the try block, the code stops execution of any of the code in the try block and starts looking for an exception handler. The catch block parameter indicates that it can catch a NullReferenceException if the code inside of the try block throws that exception type. The body of the catch block is where you perform any exception handling.
You can customize exception handling with multiple catch blocks. The following example shows code that throws an exception in the try block, which is subsequently handled by a catch block.
static void HandleUncaughtException() { Program prog = null; try { Console.WriteLine(prog.ToString()); } catch (ArgumentNullException ex) { Console.WriteLine("From ArgumentNullException: " + ex.Message); } catch (ArgumentException ex) { Console.WriteLine("From ArgumentException: " + ex.Message); } catch (Exception ex) { Console.WriteLine("From Exception: " + ex.Message); } finally { Console.WriteLine("Finally always executes."); } } |
The method name is HandleUncaughtException because there isn’t a specific catch block to handle a NullReferenceException; the exception will be handled by the catch block for the Exception type.
You list exceptions by their inheritance hierarchy, with top-level exceptions lower in the list of catch blocks. A thrown exception will move down this list of handlers, looking for a matching exception type, and only execute in the catch block of the first handler that matches. ArgumentNullException derives from ArgumentException, and ArgumentException derives from Exception.
If no catch block can handle an exception, the code goes up the stack looking for a potential catch block in calling code that can handle the exception type. If no code in the call stack is able to handle the exception, your program will terminate.
The finally block always executes if the program begins executing code in the try block. If an exception occurs and is not caught, the finally block will execute before the program looks at the calling code for a matching catch handler.
You can write a try-finally block (without catch blocks) to guarantee that certain code will execute once the try block begins. This is useful for opening a resource, like a file or database connection, and then guaranteeing you will be able to close that resource regardless of whether an exception occurs.
If you encounter a reason why your method can’t perform its intended purpose, throw an exception. There are many Exception-derived types in the .NET FCL that you can use. The following code example pulls together a few concepts you might want to use, such as validating method input and throwing an ArgumentNullException.
public class Address { public string City { get; set; } } internal class Company { public Address Address { get; set; } } // Inside of a class... static void ThrowException() { try { ValidateInput("something", new Company()); } catch (ArgumentNullException ex) when (ex.ParamName == "inputString") { Console.WriteLine("From ArgumentNullException: " + ex.Message); } catch (ArgumentException ex) { Console.WriteLine("From ArgumentException: " + ex.Message); } } static void ValidateInput(string inputString, Company cmp) { if (inputString == null) throw new ArgumentNullException(nameof(inputString)); if (cmp?.Address?.City == null) throw new ArgumentNullException(nameof(cmp)); } |
The previous code shows an Address class and a Company class with a property of the Address type. The try block of the ThrowException message passes a new instance of Company, but doesn’t instantiate Address, meaning that the Company instance’s Address property is null.
Inside ValidateInput, the if statement uses the null referencing operator, ?., to check if any of the values between Company, Company’s Address property, or Address’s City property is null. This is a convenient way to check for null without a group of individual checks, producing less syntax and simpler code. If any of these values are null, the code throws an ArgumentNullException.
The argument to the ArgumentNullException uses the nameof operator, which evaluates to a string representation of the value passed to it; it is "cmp" in this case. This code isn’t enclosed in a try block, so control returns to the code calling this method.
Back in the ThrowException method, the thrown exception causes the code to look for a handler suitable for this exception type. The exception type is ArgumentNullException, but the catch block for ArgumentNullException won’t execute. That’s because the when clause following the ArgumentNullException catch block parameter is checking for a ParamName of "inputString". This when clause is called an exception filter. As mentioned previously, the parameter name passed to the ArgumentNullException during instantiation was "cmp", so there isn’t a match. Therefore, the code continues looking at catch handlers.
Since ArgumentNullException derives from ArgumentException and there is no exception filter, the catch handler for ArgumentException executes. The exception is now handled.
Tip: It’s typically better to throw and handle specific exceptions, rather than their parent exceptions. This adds more fidelity and meaning to the exception and makes debugging easier.
Methods help you organize code into named functions that you can call to perform operations. Their name documents what the code does. Also, methods are useful to help avoid duplicating the same code in multiple places. Properties are used like fields and look like fields from the perspective of code using the property’s class. However, properties are more sophisticated in that they have get and set accessors that let them work like methods and perform more sophisticated work, like validation or special value handling. Both methods and properties help define the interface of a class to consumers and let you encapsulate the internal operations of a class, which makes it more reusable. You can use try-catch blocks to handle exceptions and try-finally blocks to guarantee critical code executes. Use the throw statement whenever a method you’re writing can’t fulfill its intended purpose.