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
1. public class Employee
2. {
3.     public string FirstName;
4.     public string LastName;
5.     public string? Salary;
6.  
7.     public string GetName()
8.     {
9.         return $"{ FirstName } { LastName }";
10.     }
11. }

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
1. public class Program
2. {
3.     public static void Main()
4.     {
5.         Employee employee1 = new();
6.         Employee employee2;
7.         employee2 = new();
8.  
9.         employee1.FirstName = "Inigo";
10.         employee1.LastName = "Montoya";
11.         employee1.Salary = "Too Little";
12.         IncreaseSalary(employee1);
13.         Console.WriteLine(
14.             $"{ employee1.GetName() }{ employee1.Salary }");
15.         // ...
16.     }
17.     // ...
18. }
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
1. public class Employee
2. {
3.     public string FirstName;
4.     public string LastName;
5.     public string? Salary;
6.  
7.     public string GetName()
8.     {
9.         return $"{ FirstName } { LastName }";
10.     }
11.  
12.     public void SetName(
13.         string newFirstName, string newLastName)
14.     {
15.         this.FirstName = newFirstName;
16.         this.LastName = newLastName;
17.     }
18. }

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
1. public class Employee
2. {
3.     public string FirstName;
4.     public string LastName;
5.     public string? Salary;
6.  
7.     public string GetName()
8.     {
9.         return $"{ FirstName } { LastName }";
10.     }
11.  
12.     // Caution: Parameter names use PascalCase
13.     public void SetName(string FirstName, string LastName)
14.     {
15.         this.FirstName = FirstName;
16.         this.LastName = LastName;
17.     }
18. }

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
1. public class Employee
2. {
3.     // ...
4.  
5.     public string GetName()
6.     {
7.         return $"{ FirstName } { LastName }";
8.     }
9.  
10.     public void SetName(string newFirstName, string newLastName)
11.     {
12.         this.FirstName = newFirstName;
13.         this.LastName = newLastName;
14.         Console.WriteLine(
15.             $"Name changed to 'this.GetName() }'");
16.     }
17. }
18.  
19. public class Program
20. {
21.     public static void Main()
22.     {
23.         Employee employee = new();
24.  
25.         employee.SetName("Inigo""Montoya");
26.         // ...
27.     }
28.     // ...
29. }
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
1. public class Employee
2. {
3.     public string FirstName;
4.     public string LastName;
5.     public string? Salary;
6.  
7.     public void Save()
8.     {
9.         DataStorage.Store(this);
10.     }
11. }
12.  
13. public class DataStorage
14. {
15.     // Save an employee object to a file 
16.     // named with the Employee name
17.     public static void Store(Employee employee)
18.     {
19.         // ...
20.     }
21. }

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
1. public class DataStorage
2. {
3.     // Save an employee object to a file 
4.     // named with the Employee name
5.     // Error handling not shown
6.     public static void Store(Employee employee)
7.     {
8.         // Instantiate a FileStream using FirstNameLastName.dat
9.         // for the filename. FileMode.Create will force
10.         // a new file to be created or override an
11.         // existing file
12.         // Note: This code could be improved with a using  
13.         // statement â€” a construct that we have avoided because 
14.         // it has not yet been introduced.
15.         FileStream stream = new(
16.             employee.FirstName + employee.LastName + ".dat",
17.             FileMode.Create);
18.  
19.         // Create a StreamWriter object for writing text
20.         // into the FileStream
21.         StreamWriter writer = new(stream);
22.  
23.         // Write all the data associated with the employee
24.         writer.WriteLine(employee.FirstName);
25.         writer.WriteLine(employee.LastName);
26.         writer.WriteLine(employee.Salary);
27.  
28.         // Dispose the StreamWriter and its stream
29.         writer.Dispose();  // Automatically closes the stream
30.     }
31.     // ...
32. }

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
1. using System;
2. // IO namespace
3. using System.IO;
4.  
5. public class Employee
6. {
7.     // ...
8. }
9.  
10. public class DataStorage
11. {
12.     // ...
13.  
14.     public static Employee Load(string firstName, string lastName)
15.     {
16.         Employee employee = new();
17.  
18.         // Instantiate a FileStream using FirstNameLastName.dat
19.         // for the filename. FileMode.Open will open
20.         // an existing file or else report an error
21.         FileStream stream = new(
22.             firstName + lastName + ".dat", FileMode.Open);
23.  
24.         // Create a StreamReader for reading text from the file
25.         StreamReader reader = new(stream);
26.  
27.         // Read each line from the file and place it into
28.         // the associated property.
29.         employee.FirstName = reader.ReadLine()??
30.             throw new InvalidOperationException(
31.                 "FirstName cannot be null");
32.         employee.LastName = reader.ReadLine()??
33.             throw new InvalidOperationException(
34.                 "LastName cannot be null");
35.         employee.Salary = reader.ReadLine();
36.  
37.         // Dispose the StreamReader and its Stream
38.         reader.Dispose();  // Automatically closes the stream
39.  
40.         return employee;
41.     }
42. }
43.  
44. public class Program
45. {
46.     public static void Main()
47.     {
48.         Employee employee1;
49.  
50.         Employee employee2 = new();
51.         employee2.SetName("Inigo""Montoya");
52.         employee2.Save();
53.  
54.         // Modify employee2 after saving
55.         IncreaseSalary(employee2);
56.  
57.         // Load employee1 from the saved version of employee2
58.         employee1 = DataStorage.Load("Inigo""Montoya");
59.  
60.         Console.WriteLine(
61.             $"{ employee1.GetName() }{ employee1.Salary }");
62.     }
63.     // ...
64. }
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 }}
;