Extension Methods

Consider the System.IO.DirectoryInfo class, which is used to manipulate filesystem directories. This class supports functionality to list the files and subdirectories (DirectoryInfo.GetFiles()), as well as the capability to move the directory (DirectoryInfo.Move()). One feature it doesn’t support directly is the copy feature. If you needed such a method, you would have to implement it, as shown earlier in Listing 6.48.

The DirectoryInfoExtension.CopyTo() method is a standard static method declaration. However, notice that calling this CopyTo() method is different from calling the DirectoryInfo.Move() method. This is unfortunate. Ideally, we want to add a method to DirectoryInfo so that, given an instance, we could call CopyTo() as an instance method: directory.CopyTo().

You can simulate the creation of an instance method on a different class via extension methods To do this, we simply change the signature of our static method so that the first parameter—that is, the data type we are extending—is prefixed with the this keyword (see Listing 6.52).

Listing 6.52: Static Copy Method for DirectoryInfoExtension
public static class DirectoryInfoExtension
{
    public static void CopyTo(
        this DirectoryInfo sourceDirectory, string target,
        SearchOption option, string searchPattern)
    {
        // ...
    }
}
 
// ...
        DirectoryInfo directory = new(".\\Source");
        directory.CopyTo(".\\Target",
            SearchOption.TopDirectoryOnly, "*");
        // ...
    }
}

With extension methods, it is now possible to add “instance methods” to any class, including classes outside of your assembly. The resultant CIL code, however, is virtually identical to what the compiler creates when calling the extension method as a normal static method.

Extension method requirements are as follows:

The first parameter corresponds to the type that the method extends or on which it operates.
To designate the extension method, prefix the first parameter with the this modifier.
To access the method as an extension method, import the extending type’s namespace via a using directive (or place the extending class in the same namespace as the calling code).

If the extension method signature matches a signature already found on the extended type (i.e., if CopyTo() already existed on DirectoryInfo), the extension method will never be called except as a normal static method.

Note that specializing a type via inheritance (covered in detail in Chapter 7) is generally preferable to using an extension method. Extension methods do not provide a clean versioning mechanism, because the addition of a matching signature to the extended type will take precedence over the extension method without warning of the change. The subtlety of this behavior is more pronounced for extended classes whose source code you don’t control. Another minor point is that although development IDEs support IntelliSense for extension methods, simply reading through the calling code does not make it obvious that a method is an extension method.

In general, you should use extension methods sparingly. Do not, for example, define them on type object. Chapter 8 discusses how to use extension methods in association with an interface. Without such an association, defining extension methods is rare.

Guidelines
AVOID frivolously defining extension methods, especially on types you don’t own.
Encapsulating the Data

In addition to properties and the access modifiers we examined earlier in the chapter, there are several other specialized ways to encapsulate the data within a class. For instance, there are two more field modifiers. The first is the const modifier, which you encountered when declaring local variables. The second is the ability to define fields as read-only.

const

Just as with const values, a const field contains a compile-time–determined value that cannot be changed at runtime. Values such as pi make good candidates for constant field declarations. Listing 6.53 shows an example of declaring a const field.

Listing 6.53: Declaring a Constant Field
public class ConvertUnits
{
    public const float CentimetersPerInch = 2.54F;
    public const int CupsPerGallon = 16;
    // ...
}

Constant fields are static, since no new field instance is required for each object instance. Declaring a constant field as static explicitly will cause a compile error. Also, constant fields are usually declared only for types that have literal values (e.g., string, int, and double). Types such as Program or System.Guid cannot be used for constant fields.

It is important that the types of values used in public constant expressions are permanent in time (see the Public Constants Should Be Permanent Values Advanced Block). Values such as pi, Avogadro’s number, and Earth’s circumference are good examples. However, values that could potentially change over time are not. For example, population counts, the poorest country, and exchange rates would be poor choices for constants.

Guidelines
DO use constant fields for values that will never change.
AVOID constant fields for values that will change over time.
AdVanced Topic
Public Constants Should Be Permanent Values

Publicly accessible constants should be permanent, because changing the value of a constant will not necessarily take effect in the assemblies that use it. If an assembly references a constant from a different assembly, the value of the constant is compiled directly into the referencing assembly. Therefore, if the value in the referenced assembly is changed but the referencing assembly is not recompiled, the referencing assembly will still use the original value, not the new value. Values that could potentially change in the future should be specified as readonly instead.

readonly

Unlike const, the readonly modifier is available only for fields (not for local variables). It declares that the field value is modifiable only from inside the constructor or via an initializer. Listing 6.54 demonstrates how to declare a read-only field.

Listing 6.54: Declaring a Field as readonly
public class Employee
{
    public Employee(int id)
    {
      _Id = id;
    }
 
    // ...
 
    private readonly int _Id;
    public int Id
    {
      get { return _Id; }
    }
 
    // Error: A readonly field cannot be assigned to (except
    // in a constructor or a variable initializer)
    public void SetId(int id) =>
        _Id = id;
 
    // ...
}

Unlike constant fields, readonly-decorated fields can vary from one instance to the next. In fact, a read-only field’s value can change within the constructor. Furthermore, read-only fields occur as either instance or static fields. Another key distinction is that you can assign the value of a read-only field at execution time rather than just at compile time. Given that readonly fields must be set in the constructor or initializer, such fields are the one case where the compiler requires the fields to be accessed from code outside their corresponding property. Besides this one exception, you should avoid accessing a backing field from anywhere other than its wrapping property.

Another important feature of readonly-decorated fields over const fields is that readonly fields are not limited to types with literal values. It is possible, for example, to declare a readonly System.Guid instance field:

public static readonly Guid ComIUnknownGuid =

       new Guid("00000000-0000-0000-C000-000000000046");

In contrast, this is not possible using a constant, because there is no C# literal representation of a GUID.

Given the guideline that fields should not be accessed from outside their wrapping property, those programming will discover that that there is almost never a need to use the readonly modifier.15 Instead, it is preferable to use a read-only automatically implemented property, as discussed earlier in the chapter.

Listing 6.55 shows one more read-only example.

Listing 6.55: Declaring a Read-Only Automatically Implemented Property
class TicTacToeBoard
{
    // Set both player's initial board to all false (blank)
    //    |   |          |   |
    // ---+---+---    ---+---+---
    //    |   |          |   |   
    // ---+---+---    ---+---+---
    //    |   |          |   |   
    // Player 1 - X   Player 2 - O
    public bool[,,] Cells { get; } = new bool[2, 3, 3];
    // Error: The property Cells cannot 
    // be assigned to because it is read-only
    // public void SetCells(bool[,,] value) =>
    //         _Cells = new bool[2, 3, 3];
 
    // ...
}

Whether read-only automatically implemented properties or the readonly modifier on a field, ensuring immutability of the array reference is a useful defensive coding technique. It ensures that the array instance remains the same, while allowing the elements within the array to change. Without the read-only constraint, it would be all too easy to mistakenly assign a new array to the member, thereby discarding the existing array rather than updating individual array elements. In other words, using a read-only approach with an array does not freeze the contents of the array. Rather, it freezes the array instance (and therefore the number of elements in the array) because it is not possible to reassign the value to a new instance. The elements of the array are still writeable.

Guidelines
DO favor read-only automatically implemented properties over read-only fields.

________________________________________

15. In C# 6.0 or later.
{{ snackbarMessage }}
;