Classes

Overview

Classes are fundamental building blocks in object-oriented programming that define the structure and behavior of objects. They encapsulate data for the object and methods to manipulate and use that data.

Syntax

A class is defined using the class keyword, followed by the class name and its body enclosed in curly braces {}.

Example

public class Person
{
    // Fields, properties, and methods go here
}

Fields

Fields are variables that are declared directly in a class. They store the data for the object.

Example

public class Person
{
    public string Name;
}

You can interact with fields using dot notation:

var person = new Person();
person.Name = "John Doe";

Properties

Properties mainly provide controlled access to fields.

Example

public class Person
{
    private string _name;
    
    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}

In this example, value is a special keyword that represents the value being assigned to the property:

public class Person
{
    private string _name;
    
    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}

Person person = new Person();
person.Name = "John Doe";

This is consider the "legacy" way of defining properties. Let's look at more consise ways of declaring properties.

public class Person
{
    public string Name { get; set; }    //creates a private backing field under the hood
}

Mutability of properties/fields

  • Mutable: Properties/fields that can be modified.
  • Immutable: Properties/fields that cannot be modified after initialization.
public class Person
{
    public string Name { get; } // Immutable property, typically set in constructor
    public int Age { get; set; } // Mutable property
}

var person = new Person { Name = "John Doe", Age = 30 };
person.Age = 31; // This is fine
person.Name = "Jane Doe"; // This will cause a compile-time error

Read-only properties

Read-only properties can only be set in the constructor as we mentioned:

public class Person
{
    public string Name { get; }
}

However, you can also have properties that return a computed value:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string FullName => $"{FirstName} {LastName}";
}

The => syntax denotes that this property is a read-only computed property. It wouldn't make sense to set full name and then have it set first and last name properties.

Constructors

Special methods called when an object is instantiated. They initialize the object.

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

Person person = new Person("John Doe", 30);
Console.WriteLine(person.Name); // John Doe

Classes can contain multiple constructors:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public Person(string name)
    {
        Name = name;
        Age = 0;
    }
}

Person person = new Person("John Doe", 30);
Person person2 = new Person("John Doe");

Console.WriteLine(person.Name); // John Doe
Console.WriteLine(person2.Name); // John Doe

You can use the this keyword to call the current class's constructor from another constructor:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public Person(string name) : this(name, 0)
    {
    }
}

Primary Constructor

Simplified syntax for constructors that allows initializing properties directly in the class definition.

public class Person(string Name, int Age)
{
    // Properties automatically created
}

🌶️🌶️🌶️ Spencer rarely uses primary constructors because he doesn't like mixing multiple ways of declaring constructors, and Spencer does use multiple constructors fairly often.

Using Constructors to Create Objects

The new keyword is used to create an instance of a class. Method syntax is used to pass parameters to the constructor.

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

Person person = new Person("John Doe", 30);

Thrown exceptions in constructors

If you throw an exception in a constructor, the object is not instantiated.

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Person(string name, int age)
    {
        if (string.IsNullOrWhiteSpace(name))
        {
            throw new ArgumentException("Name cannot be null or empty", nameof(name));
        }
        Name = name;
        Age = age;
    }
}

Person person = new Person("John Doe", 30);
Person person2 = new Person(string.Empty, 30); // This will throw an exception

Using object initializers

Object initializers provide a way to initialize objects with some default properties. The "really really old way" of adding values to properties after construction looked like this:

Person person = new Person();
person.Name = "John Doe";
person.Age = 30;        //😕

The more common and modern syntax looks like this:

Person person = new Person { 
    Name = "John Doe",
    Age = 30            //❤️
};

Properties can be made immutable but settable using the object initializer syntax by using the init keyword:

public class Person
{
    public string Name { get; init; }
    public int Age { get; init; }
}

Person person = new Person { 
    Name = "John Doe", 
    Age = 30
};

You can also force the developer to set the property using object initializer syntax by using the required keyword:

public class Person
{
    public required string Name { get; init; }
    public required int Age { get; init; }
}

🌶️🌶️🌶️ I love object initializers, full stop. That said, I don't use init and required super often, except in newer codebases.

Equality of classes

Classes are reference types, which means that they are compared by reference, not value. This means that two classes are considered equal if they reference the same object in memory, but not if they have the same values.

Person person1 = new Person { Name = "John Doe", Age = 30 };
Person person2 = new Person { Name = "John Doe", Age = 30 };

Console.WriteLine(person1 == person2); // False
person1.Equals(person2); // False

Overriding Equals and GetHashCode is a good way to customize how classes are compared:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public override bool Equals(object obj)
    {
        if (obj == null || GetType() != obj.GetType()) return false;
        Person other = (Person)obj;
        return Name == other.Name && Age == other.Age;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Name, Age);
    }
}

BTW, GetHashCode is mainly used in hash tables, like the Dictionary type.

IEquatable<T> Definition:

Interface for providing a strongly-typed method to check equality.

Examples:

public class Person : IEquatable<Person>
{
    public string Name { get; set; }
    public int Age { get; set; }

    public bool Equals(Person? other)
    {
        if (other == null) return false;
        return Name == other.Name && Age == other.Age;
    }
}

What the heck is this angle bracket IEquatable<Person> syntax? Is this HTML or what?? Patience, grasshopper - more on this later.