All Classes Derive from System.Object

Given any class, whether a custom class or one built into the system, the methods shown in Table 7.2 will be defined.

Table 7.2: Members of System.Object

Method Name

Description

public virtual bool Equals(object o)

Returns true if the object supplied as a parameter is equal in value, not necessarily in reference, to the instance.

public virtual int GetHashCode()

Returns an integer corresponding to an evenly spread hash code. This is useful for collections such as HashTable collections.

public Type GetType()

Returns an object of type System.Type corresponding to the type of the object instance.

public static bool ReferenceEquals( object a, object b)

Returns true if the two supplied parameters refer to the same object.

public virtual string ToString()

Returns a string representation of the object instance.

public virtual void Finalize()

An alias for the destructor; informs the object to prepare for termination. C# prevents you from calling this method directly.

protected object MemberwiseClone()

Clones the object in question by performing a shallow copy; references are copied, but not the data within a referenced type.

All of the methods listed in Table 7.2 appear on all objects through inheritance; all classes derive (either directly or via an inheritance chain) from object. Even literals include these methods, enabling somewhat peculiar-looking code such as this:

Console.WriteLine( 42.ToString() );

Even class definitions that don’t have any explicit derivation from object derive from object anyway. The two declarations for PdaItem in Listing 7.19, therefore, result in identical CIL.

Listing 7.19: System.Object Derivation Implied When No Derivation Is Specified Explicitly
public class PdaItem
{
    // ...
}
public class PdaItem : object
{
    // ...
}

When the object’s default implementation isn’t sufficient, programmers can override one or more of the three virtual methods. Chapter 10 describes the details involved in doing so.

Type Checking

Since C# 1.0, there have been multiple ways to check an object’s type including the is operator and the switch statement. In C# 8.0, the switch expression was introduced to enable a value return from the expression.

Type Checking with the is Operator

Since C# allows casting down the inheritance chain, it is sometimes desirable to determine what the underlying type is before attempting a conversion. Also, checking the type may be necessary for type-specific actions where polymorphism was not implemented. To determine the type, C# has included an is operator since C# 1.0 (see Listing 7.20).

Listing 7.20: is Operator Determining the Underlying Type
public class Person
{
    // ...
}
 
public class Employee : Person
{
    // ...
}
 
public class Program
{
    private static object? GetObjectById(string id)
    {
        // ...
    }
 
    public static void Main(params string[] args)
    {
        string id = args[0];
        object? entity = GetObjectById(id);
 
        if (entity is Person)
        {
            Person person = (Person) entity;
            Console.WriteLine(
                $"Id corresponds to a 
                    nameof(Person) } object: {
                    person.FirstName} {
                    person.LastName}."
                );
 
            if (entity is Employee employee)
            {
                Console.WriteLine(
                    $"Id ({ employee.Id }) is also an {
                        nameof(Employee)} object.");
            }
        }
        else if(entity is null)
        {
            Console.WriteLine(
                $"Id was unknown so null was returned.");
        }
        else
        {
            Console.WriteLine(
                $"Id '{id}' not an {
                    nameof(Employee)} or a {nameof(Person)} object.");
        }
    }
}

After invoking GetObjectById(), which returns a nullable object based on the id specified, Listing 7.20 invokes the is operator on the returned instance. Initially, it checks that the value isn’t null. Next it determines if the return is a Person and then an Employee. If GetObjectById() returns a Person, then the output will be, “Id corresponds to a Person object….” When GetObjectById() returns, an Employee output will show both a Person and an Employee message; since Employee derives from Person, all Employee objects are also Person objects.

Type Checking with Declaration

With an explicit cast, it is the programmer’s responsibility to understand the code logic sufficiently to avoid an invalid cast exception. If an invalid cast might occur, it would be preferable to leverage a type check and avoid the exception entirely. Listing 7.20 demonstrates this by first checking whether entity is a Person and then casting Entity to a Person:

// ...

if (entity is Person)

{

     Person person = (Person) entity;

// ...

The advantage is that the is operator enables a code path for when the explicit cast might fail without the expense of exception handling. With the addition of declaration in C# 7.0 and later, the is operator (and switch syntax shown next) will make the type check and the assignment in the same step. Listing 7.20 demonstrates this with the is Employee code snippet:

if (entity is Employee employee)

{

// ...

}

Here, in checking whether the input operand is an employee, it also declares a new variable of type Employee. The result from GetObjectById(id), therefore, is checked against the Employee type—type pattern matching—and assigned to the variable employee in the same expression. If the result of GetObjectById(id) is null or not an Employee, then false is produced, and the else clause executes.

Note that the employee variable is available within and after the if statement; however, it would need to be assigned a value before accessing it inside or after the else clause. Also, you can implicitly specify the data type with var, as expected, but as with local variable declaration in general, use caution when the data type isn’t obvious.

Type Checking with switch Statements

The switch statement also allows for type checking, but it can check for multiple types in addition to null checking—all within the same statement. In Listing 7.21, we demonstrate this with a method that returns the time (using .NET 6.0’s TimeOnly), from a variety of types.

Listing 7.21: Type Checking with a switch Statement
public static TimeOnly GetTime(object input)
{
    switch (input)
    {
        case DateTime datetime:
            return TimeOnly.FromDateTime(datetime);
        case DateTimeOffset datetimeOffset:
            return TimeOnly.FromDateTime(datetimeOffset.DateTime);
        case string dateText:
            return TimeOnly.Parse(dateText);
        case null:
           throw new ArgumentNullException(nameof(input));
        default:
            throw new ArgumentException(
                $"Invalid type - {input.GetType().FullName}");
    };
}

Listing 7.21 includes a case for null, but if none of the cases matches, then default provides a catchall and throws an exception. Also, like with the is operator, you can declare a variable simultaneously, providing access to an instance of the object that matches the case.

Starting in C# 8.0, the switch statement includes a more succinct expression form that returns a value.

Type Checking with switch Expressions

Like the switch statement, the switch expression (C# 8.0) provides multiple paths for each case—generically a match expression (for both switch expressions and statements). Unlike the switch statement, there are no labels, no break statements, and, in fact, no enclosed statements at all. Most importantly, the switch expression returns a value (except when it throws an exception). This makes it a preferable option to the switch statement in Listing 7.21, which allows for a return only because the function of the embedded return statements. You couldn’t, for example, assign the switch statement result to a variable. Listing 7.22 provides an example.

Listing 7.22: Using switch Expressions to Return a Value
public static TimeOnly GetTime(object input) =>
    input switch
    {
        DateTime datetime
            => TimeOnly.FromDateTime(datetime),
        DateTimeOffset datetimeOffset
                => TimeOnly.FromDateTime(datetimeOffset.DateTime),
        string dateText => TimeOnly.Parse(
            dateText),
        null => throw new ArgumentNullException(nameof(input)),
        _ => throw new ArgumentException(
            $"Invalid type - {input.GetType().FullName}"),
    };

From Listing 7.22 you will notice that unlike a switch statement, the input operand for the switch statement appears before the switch keyword. Each match expression is simply identified by the type, optionally followed by a variable declaration. Also, instead of the colon designating a label, the switch expression uses a lambda operator =>. Lastly, instead of the default keyword, the switch expression uses the discard symbol: _.

Functionally, Listing 7.21 and Listing 7.22 are equivalent. They both check the type, allow for variable declaration, allow null checking, and have a default. One noteworthy switch expression characteristic, however, is that the _ (default) catchall must appear at the end or it will obscure later match expressions. In contrast, default on a switch statement provides the same functionality regardless of where it appears.

{{ snackbarMessage }}