Chapter 3

Let Me Say This about this

IN THIS CHAPTER

Bullet Passing an object to a method

Bullet Comparing class and instance methods

Bullet Understanding this

Bullet Working with local functions

This chapter moves from the static methods emphasized in Chapter 2 in this minibook to the instance methods of a class. Static methods belong to the whole class, and instance methods belong to each instance created from the class. Important differences exist between static and instance class members, such as the passing of objects (no, it's not a matter of objectifying anything — it’s just the term used for all sorts of real-world entities, none of which are degraded by the code in this chapter).

You also discover that the keyword this identifies the instance, rather than the class as a whole, or properties defined as part of the method or passed to the method. The this keyword lets you do all sorts of amazing things.

Chapter 2 also discusses local functions as part of the refactoring process used to make your code more readable. However, you don't have to wait until you refactor your code to use a local function. This chapter discusses their use as part of the code planning process.

Remember You don’t have to type the source code for this chapter manually. In fact, using the downloadable source is a lot easier. You can find the source for this chapter in the CSAIO4D2EBK02CH03 folder of the downloadable source. See the Introduction for details on how to find these source files.

Passing an Object to a Method

You pass object references as arguments to methods in the same way as you pass value-type variables, with one difference: You always pass objects by reference. The Student class of the PassObject program shows the very simple class used for this example:

internal class Student
{
internal string Name { get; set; }
}

Tip Notice that the Student class is marked as internal. The internal scope allows anything inside the current assembly (essentially a package used to deploy code) to see the Student class (such as the Program class where Main() resides), but nothing outside the current assembly can see it. Using this scope makes your code substantially more secure. The “Restricting Access to Class Members” section of Chapter 4 of this minibook tells you more about scope, but it's important to keep security in mind as you create more applications. Otherwise, the Student class is unremarkable — it contains only the auto-implemented Name property.

This example uses two methods to interact with the Student class in addition to Main(), which serves to coordinate activities. Here they are:

// OutputName -- Output the student's name.
private static void OutputName(Student student)
{
// Output current student's name.
Console.WriteLine($"Student's name is {student.Name}.");
}

// SetName -- Modify the student object's name.
private static void SetName(Student student, string name)
{
if (name.Length > 1)
student.Name = name;
else
throw new ArgumentException("Blank names not allowed!", "name");
}

The OutputName() method displays the Name property using specific formatting. It's common to separate output functionality from flow-control functionality in programs. Using this approach makes it possible to provide multiple output techniques without having to constantly modify the flow-control code. Rewriting less code is better. Notice that both methods rely on the private scope, which is appropriate because nothing outside the Program class needs to see these methods.

Remember Normally, when you want to change a property value, you simply set it as needed. The SetName() method enforces a particular approach to updating the Name property. The Student class doesn't care if someone provides a blank name, but the SetName() method does. You could perform all sorts of checks this way, such as looking for non-alpha characters in the input that would be associated with certain code exploits. This example simply ensures that the caller provides something other than a blank name. If it detects a blank name, it throws an exception with a helpful message and the name of the bad argument. It’s time to look at Main():

static void Main(string[] args)
{
Student student = new Student();

// Set the name by accessing it directly.
Console.WriteLine("The first time:");
student.Name = "Madeleine";
OutputName(student);

// Try to supply a bad name.
Console.WriteLine(" Trying a bad value:");
try
{
SetName(student, "");
OutputName(student);
}
catch (ArgumentException e)
{
Console.WriteLine(e.Message);
Console.WriteLine($"Sent {e.ParamName} "" value.");
}

// Change the name using a method.
Console.WriteLine(" After being modified:");
try
{
SetName(student, "Willa");
OutputName(student);
}
catch (ArgumentException e)
{
Console.WriteLine(e.Message);
Console.WriteLine($"Sent {e.ParamName} "" value.");
}
Console.Read();
}

The program creates a student object. The program first sets the name of the student directly and passes the student object to the output method OutputName(). OutputName() displays the name of any Student object it receives.

The program then attempts to update the name of the student by calling SetName() with a blank value. This action causes an exception, which the code then handles. Notice the use of the escaped double quotes ("") to provide double quotes in the output. OutputName() isn't called in this case because the exception occurs before the call.

The second call to SetName() is more successful because the code provides something other than a blank value. Because all reference-type objects are passed by reference in C#, the changes made to student are retained in the calling method. When Main() outputs the student object again, the name has changed, as shown in this bit of code:

The first time:
Student's name is Madeleine.

Trying a bad value:
Blank names not allowed!
Parameter name: name
Sent name "" value.

After being modified:
Student's name is Willa.

Comparing Static and Instance Methods

A class is supposed to collect the elements that describe a real-world object or concept. For example, a Vehicle class may contain vehicle-specific attributes for maximum velocity, weight, and carrying capacity. These kinds of attributes normally appear as instance properties or instance methods. However, a Vehicle also has attributes that affect every kind of vehicle: the capability to start and stop and the like. These global attributes normally appear as static properties or methods. The following sections tell how to mix static and instance methods to create better object descriptions.

Employing static properties and methods effectively

Normally, you keep data and behaviors that reflect an object within the class definition. In the previous example, OutputName() and SetName() appeared as part of the Program class, which definitely breaks the Object-Oriented Programming (OOP) encapsulation rules. You could rewrite the Student class from the previous section in a better way, as shown in the StudentClass1 program:

internal class Student
{
private static int _numStudents = 1;

internal string Name { get; set; }

internal static void OutputName(Student student)
{
Console.WriteLine($"Student's name is {student.Name}.");
}

internal static int NumStudents { get => _numStudents; }

internal static void SetNumStudents(int NumStudents)
{
if (NumStudents > 0)
_numStudents = NumStudents;
else
throw new ArgumentOutOfRangeException(
"Value must be greater than 0", "NumStudents");
}

internal static void OutputNumStudents()
{
Console.WriteLine($"The number of students is: {_numStudents}.");
}
}

This version of the class focuses on moving OutputName(), which is implemented as a static method, to the class. It directly accesses the instance property Name. The reason you create OutputName() as a static method is that it works just fine for any instance. One instance won't use one version of OutputName(), while another instance uses a completely different version. The resulting confusion of such a scenario would boggle anyone’s mind. So, you can combine static methods and instance properties as needed within your application.

You also see a new static property member, NumStudents. The reason you implement NumStudents as a static property, rather than an instance property, is that the number of students at a school will apply to every student equally. Unless some funny stuff is going on, every student will see the same number of students at the school, rather than see some invisible students that only they can see. Accessing _numStudents, which is the field that holds the NumStudents property value, requires a somewhat different form of get accessor because you now have an actual named field. Also notice that _numStudents has a private scope because nothing outside the class needs to access it. Normally, static properties are get-only, so this example provides a specific SetNumStudents() static method to change the value. Notice that this set accessor provides error trapping in the form of an ArgumentOutOfRangeException. Finally, the OutputNumStudents() method outputs the current number of students in a formatted manner. In general, you don't combine static properties with instance methods; you use static methods to access static properties.

The Main() method is changed from the previous example in that it provides access to the new static property. However, this version of Main() is also broken because Name no longer provides any error trapping. Don't worry, this problem gets fixed in the next section. Here is the current Main() code.

static void Main(string[] args)
{
Student student = new Student();

// Set the number of students.
try
{
Student.SetNumStudents(2);

// Display the number of students.
Student.OutputNumStudents();
Console.WriteLine($"The Number of Students is: " +
$"{Student.NumStudents}.");
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine(e.Message);
}

// Try to supply a bad name.
Console.WriteLine(" Trying a bad value:");
try
{
student.Name = "";
Student.OutputName(student);
}
catch (ArgumentException e)
{
Console.WriteLine(e.Message);
Console.WriteLine($"Sent {e.ParamName} "" value.");
}

// Change the name using a method.
Console.WriteLine(" After being modified:");
try
{
student.Name = "Sally";
Student.OutputName(student);
}
catch (ArgumentException e)
{
Console.WriteLine(e.Message);
Console.WriteLine($"Sent {e.ParamName} "" value.");
}
Console.Read();
}

This version of Main() will form most of the basis for the code in the next section, where you'll see the current problems fixed. For now, supplying a blank name doesn’t trigger an error message, as shown here:

The number of students is: 2.
The Number of Students is: 2.

Trying a bad value:
Student's name is .

After being modified:
Student's name is Sally.

Employing instance properties and methods effectively

Instance properties and methods tend to focus on object elements that differ between instances or are somehow instance-specific. The updated version of the Student class found in the StudentClass2 program shows some interesting changes that make the class work a lot better than the version in the previous section.

internal class Student
{
private string _name = "";
private static int _numStudents = 1;

internal string Name
{
get => _name;
set
{
if (value.Length > 1)
_name = value;
else
throw new ArgumentException("Blank names not allowed!",
"Name");
}
}

internal static void OutputName(Student student)
{
Console.WriteLine($"Student's name is {student.Name}.");
}

public override string ToString()
{
// Output current student's name.
return $"Student's name is {Name}.";
}

internal static int NumStudents { get => _numStudents; }

internal static void SetNumStudents(int NumStudents)
{
if (NumStudents > 0)
_numStudents = NumStudents;
else
throw new ArgumentOutOfRangeException(
"Value must be greater than 0", "NumStudents");
}

internal static void OutputNumStudents()
{
Console.WriteLine($"The number of students is: {_numStudents}.");
}
}

The first thing to notice is that the Name property now uses a field, just as NumStudents does. It's likewise set to have a private scope for reasons of security. The Name property code now includes the same kind of security features as the SetName() method did in the first example, but now it's part of the actual property code, where it belongs. Because of the addition of the _name field, the get accessor also needs to change as shown.

Tip As you're looking down the list of methods, you notice a law breaker, the ToString() method. The standard ToString() method, which is provided by default with every class, outputs StudentClass.Student, which is singularly uninformative. So, this version of the class defines a new ToString() method by overriding the default version using the override keyword. Because you're overriding an existing method, you must use the same scope that it uses for your override. This is an instance method because you want the string value of the current class instance, rather than the class as a whole. Using this version of ToString(), you can replace Student.OutputName(student); with Console.WriteLine(student.ToString()); in Main(). Using the ToString() approach offers significant advantages, such as being able to modify the string as needed for a specific requirement. Now that the Student class is fixed, you see this output:

The number of students is: 2.
The Number of Students is: 2.

Trying a bad value:
Blank names not allowed!
Parameter name: Name
Sent Name "" value.

After being modified:
Student's name is Sally.
Student's name is Sally.

Expanding a method's full name

A subtle but important problem exists with the method names found in some applications. To see the problem, consider this sample code snippet:

public class Person
{
public void Address()
{
Console.WriteLine("Hi");
}
}

public class Letter
{
string address;

// Store the address.
public void Address(string newAddress)
{
address = newAddress;
}
}

Any subsequent discussion of the Address() method is now ambiguous. The Address() method within Person has nothing to do with the Address() method in Letter. If an application needs to access the Address() method, which Address() does it access? The problem lies not with the methods themselves, but rather with the description. In fact, no Address() method exists as an independent entity — only a Person.Address() and a Letter.Address() method. Attaching the class name to the beginning of the method name clearly indicates which method is intended.

This description is quite similar to people's names. At home, you might not ever experience any ambiguity because you’re the only person who has your name. However, at work, you might respond to a yell when someone means to attract the attention of another person with the same name as yours. Of course, this is the reason that people resort to using last names. Thus, you can consider Address() to be the first name of a method, with its class as the family name.

Accessing the Current Object

Consider the following Student.SetName() method found in the CurrentObject program:

internal class Student
{
// The name information to describe a student
private string firstName;
private string lastName;

// SetName -- Save name information.
internal void SetName(string FirstName, string LastName)
{
firstName = FirstName;
lastName = LastName;
}

public override string ToString()
{
return $"{firstName} {lastName}";
}
}

class Program
{
static void Main(string[] args)
{
Student student1 = new Student();
student1.SetName("Joseph", "Smith");

Student student2 = new Student();
student2.SetName("John", "Davis");

// Show that the students are separate.
Console.WriteLine(student1.ToString());
Console.WriteLine(student2.ToString());
Console.ReadLine();
}
}

The method Main() uses the SetName() method to update first student1 and then student2. But you don't see a reference to either Student object within SetName() itself. In fact, no reference to a Student object exists. An instance method is said to operate on the current object (the one in use now) in this case. How does a method know which one is the current object? Will the real current object please stand up? The answer is simple. The current object is passed as an implicit argument in the call to a method. For example:

student1.SetName("Joseph", "Smith");

This call is equivalent to the following:

Student.SetName(student1, "Joseph", "Smith"); // Equivalent call,
// (but this won't build properly).

The example isn't saying that you can invoke SetName() in two different ways; just that the two calls are semantically equivalent. The object identifying the current object — the hidden first argument — is passed to the method, just as other arguments are. Leave that task to the compiler.

Passing an object implicitly is easy to swallow, but what about a reference from one method to another? The following code snippet (found in CurrentObject2) illustrates calling one method from another:

internal class Student
{
// The name information to describe a student
private string firstName;
private string lastName;

// SetName -- Save name information.
internal void SetName(string FirstName, string LastName)
{
SetFirstName(FirstName);
SetLastName(LastName);
}

private void SetFirstName(string FirstName)
{
firstName = FirstName;
}

private void SetLastName(string LastName)
{
lastName = LastName;
}

public override string ToString()
{
return $"{firstName} {lastName}";
}
}

No object appears in the call to SetFirstName(). The current object continues to be passed along silently from one method call to the next. An access to any member from within an object method is assumed to be with respect to the current object. The upshot is that a method knows which object it belongs to. Current object (or current instance) means something like me.

What is the this keyword?

Unlike most arguments, the current object doesn't appear in the method argument list, so it isn’t assigned a name by the programmer. Instead, C# assigns this object the less-than-imaginative name this, which is useful in the few situations in which you need to refer directly to the current object. Thus you could write the previous example this way (as shown in CurrentObject3):

internal class Student
{
// The name information to describe a student
private string firstName;
private string lastName;

// SetName -- Save name information.
internal void SetName(string FirstName, string LastName)
{
this.SetFirstName(FirstName);
this.SetLastName(LastName);
}

private void SetFirstName(string FirstName)
{
this.firstName = FirstName;
}

private void SetLastName(string LastName)
{
this.lastName = LastName;
}

public override string ToString()
{
return $"{firstName} {lastName}";
}
}

Notice the explicit addition of the keyword this. Adding it to the member references doesn't add anything because this is assumed. However, when Main() makes the following call, this references student1 throughout SetName() and any other method it may call:

student1.SetName("John", "Smith");

When is the this keyword explicit?

You don't normally need to refer to this explicitly because it is understood where necessary by the compiler. However, two common cases require this. You may need it when initializing data members, as in this example:

internal class Person
{
private string name; // This is this.name below.
private int id; // And this is this.id below.

internal void Init(string name, int id) // These are method arguments.
{
this.name = name; // Argument names same as data member names
this.id = id;
}
}

The parameters in the Init() method declaration are named name and id, which match the names of the corresponding data members. The method is then easy to read because you know immediately which argument is stored where. The only problem is that the name name in the argument list obscures the name of the data member. The compiler complains about it.

Remember The addition of this clarifies which name is intended. Within Init(), the variable name refers to the method parameter, but this.name refers to the name field. Of course, the best way to avoid needing to use this in this example is to ensure that the field name differs from the parameter name, which is considered good coding practice now. Using this is likely not required in most instances where a developer uses good variable name choices and proper scoping.

Using Local Functions

Even with all the methods of making code smaller and easier to work with that you have seen so far, sometimes a method might prove complex and hard to read anyway. A local function enables you to declare a function within the scope of a method to help promote further encapsulation. You use this approach when you need to perform a task a number of times within a method, but no other method within the application performs this particular task. The “Working with local functions” section of Chapter 2 of this minibook provides a refactored view of local functions. The following sections tell you more about local functions as part of an original code design.

Creating a basic local function

Here's a simple example of a local function that you can also find in BasicLocalFunction:

static void Main(string[] args)
{
//Create a local function
int Sum(int x, int y) { return x + y; }

// Use the local function to output some sums.
Console.WriteLine(Sum(1, 2));
Console.WriteLine(Sum(5, 6));
Console.Read();
}

The Sum() method is relatively simple, but it demonstrates how a local function could work. The function encapsulates some code that only Main() uses. Because Main() performs the required task more than once, using Sum() makes sense to make the code more readable, easier to understand, and easier to maintain.

Tip Local functions have all the functionality of any method except that you can't declare them as static in most cases (you have the option of declaring them static when using C# 8.0 or later—the article at https://www.telerik.com/blogs/c-8-static-local-functions-and-using-declarations provides some additional information on this topic). A local function has access to all the variables found within the enclosing method, so if you need a variable found in Main(), you can access it from Sum().

Using attributes with local functions

C# 9.0 introduces the ability to use attributes with local functions. An attribute is a special code decorator that defines how and when a particular piece of code should interact with the rest of the application. This section discusses a couple of simple examples using the LocalFunctionWithAttributes program. Before you move forward, however, make sure to add the required C# 9.0 to your .csproj file when writing the code on your own, rather than using the downloadable source (which already has all of the required additions for you). You also add this using statement to the top of the Program.cs file:

using System.Diagnostics;

The example uses two attributes, as shown in the following code:

class Program
{
static void Main(string[] args)
{
//Create a debug local function
[Obsolete]
[Conditional("DEBUG")]
static void DebugInfo(int x, int y)
{
Console.WriteLine($"Input x: {x} Input y: {y}");
}

static int Sum(int x, int y) { return x + y; }

// Use the local function to output some sums.
Console.WriteLine(Sum(1, 2));
DebugInfo(1, 2);
Console.WriteLine(Sum(5, 6));
DebugInfo(5, 6);
Console.Read();
}
}

The [Obsolete] attribute tells the compiler that this particular function is still included in the code, but that the developer plans to deprecate it at some point. When the user compiles the code, the following warning appears in the Error List window for each use of the local function:

Warning CS0612 'DebugInfo(int, int)' is obsolete

The [Conditional("DEBUG")] attribute tells the compiler to add the function into the debug builds of the application only. Both the function and the call to the function are removed from the release version of the application. Consequently, when in debug mode, you see this output:

3
Input x: 1
Input y: 2
11
Input x: 5
Input y: 6

Remember There are special requirements for using the [Conditional] attribute (https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.conditionalattribute) on local functions:

  • You must declare the function as static.
  • The function can't return a value.
  • You must be using C# 9.0 or later.

Different attributes will have different requirements for use, so it pays to spend some time determining just how you plan to use the attribute. For example, the [Obsolete] attribute works on any local function, so you don’t need to do anything special to use it. The [Obsolete] attribute provides considerable additional functionality, which you can read about at https://docs.microsoft.com/en-us/dotnet/api/system.obsoleteattribute.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset