Instance Methods

One alternative to formatting the names in the WriteLine() method call within Main() is to provide a method in the Employee class that takes care of the formatting. Changing the functionality to be within the Employee class rather than a member of Program is consistent with the encapsulation of a class. Why not group the methods relating to the employee’s full name with the class that contains the data that forms the name? Listing 6.7 demonstrates the creation of such a method.

Listing 6.7: Accessing Fields from within the Containing Class
public class Employee
{
    public string FirstName;
    public string LastName;
    public string? Salary;
 
    public string GetName()
    {
        return $"{ FirstName } { LastName }";
    }
}

There is nothing particularly special about this method compared to what you learned in Chapter 5, except that now the GetName() method accesses fields on the object instead of just local variables. In addition, the method declaration is not marked with static. As you will see later in this chapter, static methods cannot directly access instance fields within a class. Instead, it is necessary to obtain an instance of the class to call any instance member, whether a method or a field.

Given the addition of the GetName() method, you can update Program.Main() to use the method, as shown in Listing 6.8 and Output 6.2.

Listing 6.8: Accessing Fields from outside the Containing Class
public class Program
{
    public static void Main()
    {
        Employee employee1 = new();
        Employee employee2;
        employee2 = new();
 
        employee1.FirstName = "Inigo";
        employee1.LastName = "Montoya";
        employee1.Salary = "Too Little";
        IncreaseSalary(employee1);
        Console.WriteLine(
            $"{ employee1.GetName() }{ employee1.Salary }");
        // ...
    }
    // ...
}
Output 6.2
Inigo Montoya: Enough to survive on
Using the this Keyword

You can obtain the reference to a class from within instance members that belong to the class. To indicate explicitly that the field or method accessed is an instance member of the containing class in C#, you use the keyword this. Use of this is implicit when calling any instance member, and it returns an instance of the object itself.

For example, consider the SetName() method shown in Listing 6.9.

Listing 6.9: Using this to Identify the Field’s Owner Explicitly
public class Employee
{
    public string FirstName;
    public string LastName;
    public string? Salary;
 
    public string GetName()
    {
        return $"{ FirstName } { LastName }";
    }
 
    public void SetName(
        string newFirstName, string newLastName)
    {
        this.FirstName = newFirstName;
        this.LastName = newLastName;
    }
}

This example uses the keyword this to indicate that the fields FirstName and LastName are instance members of the class.

Although the this keyword can prefix any and all references to local class members, the general guideline is not to clutter code when there is no additional value. Therefore, you should avoid using the this keyword unless it is required. Listing 6.12 (later in this chapter) is an example of one of the few circumstances when such a requirement exists. Listing 6.9 and Listing 6.10, however, are not good examples. In Listing 6.9, this can be dropped entirely without changing the meaning of the code. And in Listing 6.10 (presented next), by changing the naming convention for fields and following the naming convention for parameters, we can avoid any ambiguity between local variables and fields.

Beginner Topic
Relying on Coding Style to Avoid Ambiguity

In the SetName() method, you did not have to use the this keyword because FirstName is obviously different from newFirstName. But suppose that, instead of calling the parameter “newFirstName,” you called it “FirstName” (using PascalCase), as shown in Listing 6.10.

Listing 6.10: Using this to Avoid Ambiguity
public class Employee
{
    public string FirstName;
    public string LastName;
    public string? Salary;
 
    public string GetName()
    {
        return $"{ FirstName } { LastName }";
    }
 
    // Caution: Parameter names use PascalCase
    public void SetName(string FirstName, string LastName)
    {
        this.FirstName = FirstName;
        this.LastName = LastName;
    }
}

In this example, it is not possible to refer to the FirstName field without explicitly indicating that the Employee object owns the variable. this acts just like the employee1 variable prefix used in the Program.Main() method (see Listing 6.8); it identifies the object as the one on which SetName() was called.

Listing 6.10 does not follow the C# naming convention in which parameters are declared like local variables, using camelCase. This can lead to subtle bugs, because assigning FirstName (intending to refer to the field) to FirstName (the parameter) will lead to code that still compiles and even runs. To avoid this problem, it is a good practice to have a different naming convention for parameters and local variables than the naming convention for fields and properties. We demonstrate one such convention later in this chapter.

In Listing 6.9 and Listing 6.10, the this keyword is not used in the GetName() method—it is optional. However, if local variables or parameters exist with the same name as the field (see the SetName() method in Listing 6.10), omitting this would result in accessing the local variable/parameter when the intention was to access the field; given this scenario, use of this is required.

You also can use the keyword this to access a class’s methods explicitly. For example, this.GetName() is allowed within the SetName() method, permitting you to print out the newly assigned name (see Listing 6.11 and Output 6.3).

Listing 6.11: Using this with a Method
public class Employee
{
    // ...
 
    public string GetName()
    {
        return $"{ FirstName } { LastName }";
    }
 
    public void SetName(string newFirstName, string newLastName)
    {
        this.FirstName = newFirstName;
        this.LastName = newLastName;
        Console.WriteLine(
            $"Name changed to 'this.GetName() }'");
    }
}
 
public class Program
{
    public static void Main()
    {
        Employee employee = new();
 
        employee.SetName("Inigo""Montoya");
        // ...
    }
    // ...
}
Output 6.3
Name changed to 'Inigo Montoya'

Sometimes it may be necessary to use this to pass a reference to the currently executing object. Consider the Save() method in Listing 6.12.

Listing 6.12: Passing this in a Method Call
public class Employee
{
    public string FirstName;
    public string LastName;
    public string? Salary;
 
    public void Save()
    {
        DataStorage.Store(this);
    }
}
 
public class DataStorage
{
    // Save an employee object to a file 
    // named with the Employee name
    public static void Store(Employee employee)
    {
        // ...
    }
}

The Save() method in Listing 6.12 calls a method on the DataStorage class, called Store(). The Store() method, however, needs to be passed the Employee object, which needs to be persisted. This is done using the keyword this, which passes the instance of the Employee object on which Save() was called.

Storing and Loading with Files

The actual implementation of the Store() method inside DataStorage involves classes within the System.IO namespace, as shown in Listing 6.13. Inside Store(), you begin by instantiating a FileStream object that you associate with a file corresponding to the employee’s full name. The FileMode.Create parameter indicates that you want a new file to be created if there isn’t already one with the <firstname><lastname>.dat name; if the file exists already, it will be overwritten. Next, you create a StreamWriter class, which is responsible for writing text into the FileStream. You write the data using WriteLine() methods, just as though writing to the console.

Listing 6.13: Data Persistence to a File
public class DataStorage
{
    // Save an employee object to a file 
    // named with the Employee name
    // Error handling not shown
    public static void Store(Employee employee)
    {
        // Instantiate a FileStream using FirstNameLastName.dat
        // for the filename. FileMode.Create will force
        // a new file to be created or override an
        // existing file
        // Note: This code could be improved with a using  
        // statement â€” a construct that we have avoided because 
        // it has not yet been introduced.
        FileStream stream = new(
            employee.FirstName + employee.LastName + ".dat",
            FileMode.Create);
 
        // Create a StreamWriter object for writing text
        // into the FileStream
        StreamWriter writer = new(stream);
 
        // Write all the data associated with the employee
        writer.WriteLine(employee.FirstName);
        writer.WriteLine(employee.LastName);
        writer.WriteLine(employee.Salary);
 
        // Dispose the StreamWriter and its stream
        writer.Dispose();  // Automatically closes the stream
    }
    // ...
}

Once the write operations are completed, both the FileStream and the StreamWriter need to be closed so that they are not left open indefinitely while waiting for the garbage collector to run. Listing 6.13 does not include any error handling, so if an exception is thrown, neither Close() method will be called.

The load process is similar (see Listing 6.14 with Output 6.4).

Listing 6.14: Data Retrieval from a File
using System;
// IO namespace
using System.IO;
 
public class Employee
{
    // ...
}
 
public class DataStorage
{
    // ...
 
    public static Employee Load(string firstName, string lastName)
    {
        Employee employee = new();
 
        // Instantiate a FileStream using FirstNameLastName.dat
        // for the filename. FileMode.Open will open
        // an existing file or else report an error
        FileStream stream = new(
            firstName + lastName + ".dat", FileMode.Open);
 
        // Create a StreamReader for reading text from the file
        StreamReader reader = new(stream);
 
        // Read each line from the file and place it into
        // the associated property.
        employee.FirstName = reader.ReadLine()??
            throw new InvalidOperationException(
                "FirstName cannot be null");
        employee.LastName = reader.ReadLine()??
            throw new InvalidOperationException(
                "LastName cannot be null");
        employee.Salary = reader.ReadLine();
 
        // Dispose the StreamReader and its Stream
        reader.Dispose();  // Automatically closes the stream
 
        return employee;
    }
}
 
public class Program
{
    public static void Main()
    {
        Employee employee1;
 
        Employee employee2 = new();
        employee2.SetName("Inigo""Montoya");
        employee2.Save();
 
        // Modify employee2 after saving
        IncreaseSalary(employee2);
 
        // Load employee1 from the saved version of employee2
        employee1 = DataStorage.Load("Inigo""Montoya");
 
        Console.WriteLine(
            $"{ employee1.GetName() }{ employee1.Salary }");
    }
    // ...
}
Output 6.4
Name changed to 'Inigo Montoya'
Inigo Montoya:

The reverse of the save process appears in Listing 6.14, which uses a StreamReader rather than a StreamWriter. Again, Close() needs to be called on both FileStream and StreamReader once the data has been read.

Output 6.4 does not show any salary after Inigo Montoya: because Salary was not set to Enough to survive on by a call to IncreaseSalary() until after the call to Save().

Notice in Main() that we can call Save() from an instance of an employee, but to load a new employee we call DataStorage.Load(). To load an employee, we generally don’t already have an employee instance to load into, so an instance method on Employee would be less than ideal. An alternative to calling Load on DataStorage would be to add a static Load() method (see the section “Static Members” later in this chapter) to Employee so that it would be possible to call Employee.Load() (using the Employee class, not an instance of Employee).

{{ snackbarMessage }}
;