C# Part 2 – Classes

In C# Part 2, we will delve deeper into the essential topic of classes. Classes in C# are fundamental building blocks of object-oriented programming, providing a powerful mechanism for defining the structure and behavior of objects in a program. Essentially, a class acts as a blueprint or template that encapsulates both data (in the form of fields or properties) and methods (functions that operate on that data).

These classes serve as the foundation upon which objects are created, allowing for the organization and management of code in a modular and reusable manner. By understanding how classes work in C#, developers gain the ability to model real-world entities, abstract complex functionalities, and create scalable, maintainable software applications.

Classes

Class Definition in C#

A class definition serves as a blueprint for creating objects, encapsulating both data and behavior within a structured unit. It starts with the “class” keyword, followed by the class name, and typically contains fields, properties, methods, and constructors.

Fields represent the class’s data, properties provide controlled access to that data, methods define the class’s behaviors, and constructors initialize instances of the class. Classes facilitate the principles of object-oriented programming (OOP), promoting modularity and code reusability.

They enable the creation of objects based on the defined class, allowing for the organization of code into manageable and self-contained units, which is essential for building complex software systems in C#.

Class Example in C#

using System;
public class Person
{
    // Fields (private data members)
    private string firstName;
    private string lastName;
    private int age;

    // Constructor to initialize the object
    public Person(string firstName, string lastName, int age)
    {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
    }

    // Property to access and modify the first name
    public string FirstName
    {
        get { return firstName; }
        set { firstName = value; }
    }

    // Property to access and modify the last name
    public string LastName
    {
        get { return lastName; }
        set { lastName = value; }
    }

    // Property to access and modify the age
    public int Age
    {
        get { return age; }
        set { age = value; }
    }

    // Method to introduce the person
    public void Introduce()
    {
        Console.WriteLine($"Hello, I'm {firstName} {lastName}, and I am {age} years old.");
    }
}

Attributes in C#

Class attributes, often referred to as annotations or metadata, are a powerful feature that allows developers to attach additional information to classes or their members. These attributes serve various purposes, such as providing instructions to the compiler, influencing runtime behavior, or facilitating documentation generation.

Attributes are enclosed in square brackets and are used to convey information about the class or its members. Common examples of class attributes in C# include the “Obsolete” attribute, used to mark deprecated code, and the “Serializable” attribute, indicating that an object of the class can be serialized for data storage or transmission. Class attributes offer a versatile way to enrich the code with supplementary information, enhancing its functionality, maintainability, and interoperability.

[Serializable]
public class Person
{
  //...
}

Attribute Examples in C#

Serializable: As mentioned earlier, this attribute marks a class as serializable, allowing instances of the class to be converted into a binary format for storage or transmission.

Obsolete: The [Obsolete] attribute is used to mark code as deprecated, indicating that it should no longer be used. You can provide a message to suggest an alternative or explain the deprecation.

Conditional: The [Conditional] attribute is used in conjunction with conditional compilation symbols. It allows you to specify that a method should only be called if a particular compilation symbol is defined.

DllImport: The [DllImport] attribute is used to declare a method that is implemented in an external DLL (Dynamic Link Library) and is accessible from C# code.

NonSerialized: The [NonSerialized] attribute is used to indicate that a field should not be serialized when the containing object is serialized. It is often used when you want to exclude specific fields from serialization.

Flags: The [Flags] attribute is applied to an enumeration to indicate that the enumeration represents a set of bit flags. This affects how the values can be combined using bitwise operations.

StructLayout: The [StructLayout] attribute is used to control the layout of a struct in memory. You can specify layout options such as LayoutKind.Sequential or LayoutKind.Explicit.

AttributeUsage: The [AttributeUsage] attribute is used to specify how a custom attribute can be applied to program entities (e.g., classes, methods, properties). It allows you to define rules for attribute usage.

Attributes Example in Unity

[SerializeField]: This attribute is used to expose private fields in the Unity Inspector window, allowing you to serialize private variables and still keep them hidden from other scripts.

[Header]: The [Header] attribute is used to add a custom header label above serialized fields in the Inspector for better organization and readability.

[Range]: This attribute allows you to specify a minimum and maximum value for a serialized numeric field, creating a slider in the Inspector for easy value adjustment.

[Tooltip]: The [Tooltip] attribute lets you add custom tooltips to serialized fields in the Inspector, providing additional information to other developers or designers working with your scripts.

[TextArea]: This attribute is used to change the input field in the Inspector into a multi-line text area for editing serialized string fields.

[HideInInspector]: The [HideInInspector] attribute is the opposite of [SerializeField]. It hides a public variable from the Inspector even though it would typically be visible because it’s public.

[ExecuteInEditMode]: This attribute is applied to a script to make it execute its methods, such as Update(), in Edit mode in the Unity editor, allowing you to see changes without entering Play mode.

[RequireComponent]: This attribute automatically adds required components to a GameObject when the script is attached. For example, if you attach a script with [RequireComponent(typeof(Rigidbody))], Unity will add a Rigidbody component if it’s not already present.

[ContextMenu]: The [ContextMenu] attribute allows you to create custom context menu items in the Unity Inspector. You can use it to call specific methods from the Inspector’s right-click context menu.

[AddComponentMenu]: This attribute defines a custom menu path for adding a component to a GameObject via the Unity Editor’s “Add Component” menu.

[DisallowMultipleComponent]: When applied to a script, this attribute prevents multiple instances of the script from being added to the same GameObject.

[SelectionBase]: This attribute designates a specific child GameObject of a prefab as the “selection base,” making it easier to select the entire prefab in the Unity Hierarchy window.

Class Properties in C#

Class properties encapsulate the state or attributes of an object. These properties act as controlled access points to the class’s internal data, providing a level of abstraction and encapsulation.

By defining properties within a class, developers can ensure that data is accessed and modified in a controlled and consistent manner, adhering to specific rules or validation logic. Properties consist of a getter method, which allows reading the property value, and a setter method, which enables modification while enforcing validation if necessary.

This approach not only enhances code organization but also allows for the implementation of crucial concepts like data hiding and abstraction, contributing to the overall maintainability and scalability of C# programs.

// Property to access and modify the first name
public string FirstName
{
  get { return firstName; }
  set { firstName = value; }
}

Class Methods in C#

Class methods in C# define the behavior and operations that can be performed by objects created from a class. These methods encapsulate specific functionalities and actions that the class or its instances can execute.

Methods are essential for achieving modularity and code reuse, as they allow developers to group related code into self-contained units within a class. By using methods, complex tasks can be broken down into manageable steps, improving code organization and readability.

Methods can have parameters to accept inputs and return values to provide outputs, making them versatile tools for performing a wide range of tasks. C# supports various types of methods, including instance methods that operate on specific instances of a class, static methods that belong to the class itself, and more.

// Method to introduce the person
public void Introduce()
{
  Console.WriteLine($"Hello, I'm {firstName} {lastName}, and I am {age} years old.");
}

Class Constructors in C#

Class constructors are special methods responsible for initializing objects when they are created from a class. Constructors have the same name as the class and are invoked automatically when an instance of the class is instantiated.

They play a crucial role in ensuring that objects start with a valid and consistent state by initializing their fields and properties. Class constructors can have parameters to accept values that tailor the initialization process, allowing for various object configurations.

In C#, you can define multiple constructors with different parameter sets, known as constructor overloading, providing flexibility in object creation. Constructors enable developers to centralize and standardize the initialization logic for class instances, making code more organized, maintainable, and robust.

Class Destructors in C#

In C#, class destructors are specialized methods used for cleaning up resources and performing finalization tasks before an object is garbage collected by the runtime. Unlike constructors, which initialize objects when they are created, destructors are called automatically by the garbage collector when the object’s lifetime ends.

Class destructors are defined with the tilde (~) followed by the class name and have no parameters or access modifiers. They are rarely used in C# because the language provides other mechanisms like IDisposable and the using statement for resource management, which are typically preferred over destructors.

Destructors are mostly employed when dealing with unmanaged resources or scenarios where deterministic finalization is essential. In most cases, C# developers rely on the garbage collector to handle memory management, minimizing the need for explicit destructor implementation.

Using C# Classes in Unity

When using classes in Unity, several considerations are crucial to ensure efficient and bug-free game development. Firstly, developers should be cautious about memory management. Unity’s garbage collector can introduce performance overhead if not managed properly.

Creating too many short-lived objects or having circular references between objects can lead to memory leaks. Thus, it’s essential to be mindful of object instantiation and disposal, especially in frequently called methods like Update().

Another important consideration is the use of MonoBehaviour components. While they provide many built-in features and integration with Unity’s game loop, attaching too many components to a GameObject can become unwieldy and negatively impact performance. Developers should strive to keep GameObjects as lean as possible, only adding the necessary components to achieve the desired functionality.

Additionally, it’s important to be cautious when using static classes and variables. Unity’s execution order and lifetime of static members might not align with the expected behavior, potentially leading to unexpected issues. It’s advisable to minimize the use of statics and rely on component-based design for most scenarios.

Care should also be taken with class serialization. Unity’s serialization system has specific rules and limitations. Not all types and structures can be serialized automatically. Developers should be aware of these constraints and consider custom serialization when necessary.

Lastly, maintaining a clean and organized class structure is essential for readability and maintainability. As projects grow, disorganized and tightly coupled classes can become a maintenance nightmare. Developers should follow good coding practices, such as using meaningful class and method names, adhering to coding conventions, and creating well-documented code to enhance collaboration and ease debugging.

In summary, when using classes in Unity, developers should be vigilant about memory management, avoid overuse of MonoBehaviour components, exercise caution with static members, understand Unity’s serialization system, and prioritize clean and organized code.

Unity MonoBehaviour Class

The MonoBehaviour class is a fundamental component in Unity’s scripting system, serving as the base class for scripts that can be attached to GameObjects within a Unity scene. MonoBehaviour is a core concept in Unity game development, allowing developers to define the behavior, functionality, and interactions of game objects.

Scripting Behavior

MonoBehaviour is the class from which you derive custom scripts that define the behavior of GameObjects in a Unity scene. It enables you to implement the logic and functionality for game elements, such as characters, enemies, items, and more.

Integration with Unity’s Game Loop

MonoBehaviour scripts integrate seamlessly with Unity’s game loop. They provide methods like Start(), Update(), FixedUpdate(), LateUpdate(), and others that are automatically called by Unity at specific points in the frame cycle. This allows you to respond to events and control the behavior of your GameObjects over time.

Component-Based Design

Unity follows a component-based architecture, and MonoBehaviour is at the heart of this design philosophy. You attach scripts (MonoBehaviours) to GameObjects as components, enabling you to compose complex behaviors by combining multiple scripts on a single GameObject.

Inspector Exposability

Public fields and properties within a MonoBehaviour script can be exposed in the Unity Inspector, allowing designers and other team members to configure and tweak parameters without modifying the code. This makes your scripts more accessible and versatile.

Communication and Interaction

MonoBehaviour scripts can interact with other scripts, GameObjects, and Unity components. They can send messages, access and modify properties of other components, and respond to collision, trigger, and input events.

Unity Event System

It also plays a crucial role in Unity’s event system. You can define custom events and delegate methods within your scripts to handle specific in-game events and interactions.

Inheritance from MonoBehaviour

When creating a custom script in Unity, you typically inherit from MonoBehaviour by declaring your class like this: public class MyScript : MonoBehaviour. This inheritance grants your script access to all the MonoBehaviour methods and functionality.

Life Cycle Management

MonoBehaviour scripts have specific life cycle phases, from initialization (Start()) to updating (Update()) and finalization (OnDestroy()). Understanding these phases is essential for proper script behavior and resource management.

Class Object Instantiation

When you instantiate an object, you create a specific instance of a class, essentially bringing the class blueprint to life. This instance then possesses its own unique state and can execute the methods defined within the class.

The process usually involves the new keyword followed by the class constructor, which initializes the object and allocates memory for it. Once instantiated, you can access and manipulate the object’s properties and methods to perform various tasks.

// Create an instance of the Person class
var person = new Person
{
  FirstName = "John",
  LastName = "Doe",
  Age = 30
};

Basic OOP Concepts

Encapsulation

Encapsulation involves bundling the data (attributes or fields) and the methods (functions) that operate on that data within a single unit called a class. The primary goal of encapsulation is to restrict direct access to the class’s internal data and provide controlled access through methods, typically referred to as getters and setters.

This approach ensures that data remains in a valid and consistent state, preventing unauthorized or accidental modifications. Encapsulation also promotes the principle of information hiding, where the internal details of a class are concealed, and only a well-defined interface is exposed for interaction. By encapsulating data and behavior, OOP languages like C# enhance code organization, security, and maintainability.

Inheritance

Inheritance allows one class (called a subclass or derived class) to inherit the properties and behaviors (fields and methods) of another class (called a superclass or base class). This promotes code reuse and the creation of a hierarchy of classes.

Subclasses can extend or modify the behavior of their superclass, making it a powerful mechanism for modeling relationships between objects with shared characteristics. Inheritance enhances code organization and simplifies maintenance by allowing common functionality to be defined in a single place and reused across multiple subclasses.

Base Constructors

Base constructors in C# are used in inheritance to call the constructor of the parent class (base class) from the constructor of the child class (derived class). This ensures that the initialization logic of the base class is executed before the derived class adds its specific functionality. By using base constructors, you can avoid duplicating code and maintain consistency in object initialization across the inheritance hierarchy.

Composition

Composition is an alternative to inheritance that involves creating complex objects by combining simpler objects, often called components. Unlike inheritance, where subclasses inherit behavior from a superclass, composition allows for greater flexibility and modularity by assembling objects with specific functionality. It promotes a “has-a” relationship, where an object contains other objects as its parts, resulting in more manageable and loosely coupled code.

Polymorphism

Polymorphism allows objects of different classes to be treated as objects of a common base class. This enables dynamic binding of methods, where the appropriate method is called at runtime based on the actual type of the object. Polymorphism promotes flexibility and extensibility in code design, allowing for the creation of more generic and reusable code that can adapt to various derived types.

Interfaces

Interfaces in C# define a contract of methods and properties that a class must implement. They provide a way to achieve multiple inheritance and enable classes from different hierarchies to share common behavior. Interfaces promote code consistency and flexibility, allowing for the creation of classes that can work together seamlessly despite their disparate implementations.

Abstract Classes

Abstract classes in C# are classes that cannot be instantiated directly and are typically used as base classes for other classes. They may contain abstract methods that must be implemented by derived classes. Abstract classes provide a way to define a common interface while enforcing specific behavior in subclasses. They are especially useful when you want to share some code and ensure a consistent structure among derived classes.

Class Extensions

Class extensions, also known as extension methods, are a feature in C# that allows you to add new methods to existing classes without modifying the class’s source code. This is particularly useful for extending the functionality of classes from libraries or third-party code, promoting code separation and modularity.

Using Basic OOP Concepts Example

Please note that this example is used to illustrate the basic concepts of OOP. It is not intended to serve as a design reference.

using System;
// Encapsulation: Using private fields and properties to encapsulate data
class Shape
{
    private double area;

    // Property to access the area (Getter)
    public double Area
    {
        get { return area; }
        protected set { area = value; }
    }

    // Constructor to initialize area
    public Shape(double area)
    {
        Area = area;
    }
}
// Inheritance: Creating a subclass (Rectangle) that inherits from the base class (Shape)
class Rectangle : Shape
{
    private double width;
    private double height;

    // Constructor: Calling the base class constructor using base()
    public Rectangle(double width, double height) : base(width * height)
    {
        this.width = width;
        this.height = height;
    }
}
// Composition: Creating a Circle class composed of a Shape object
class Circle
{
    private Shape shape;

    // Constructor: Composing a Circle using a Shape
    public Circle(double radius)
    {
        double circleArea = Math.PI * Math.Pow(radius, 2);
        shape = new Shape(circleArea);
    }

    // Method to get the area of the circle
    public double GetArea()
    {
        return shape.Area;
    }
}
// Polymorphism and Interfaces: Implementing an interface (IDrawable) and demonstrating polymorphism
interface IDrawable
{
  void Draw();
}
// Abstract Classes: Creating an abstract class (Polygon) with an abstract method
abstract class Polygon : Shape
{
  public Polygon(double area) : base(area)
  {
  }

  // Abstract method for calculating perimeter
  public abstract string GetPolygonType();
}
class Triangle : Polygon, IDrawable
{
  public Triangle(double baseLength, double height) : base(0.5 * baseLength * height)
  {
  }

  public override string GetPolygonType()
  {
      return "Triangle";
  }

  // Polymorphic implementation of Draw method
  public void Draw()
  {
      Console.WriteLine("Drawing a triangle.");
  }
}
// Class Extensions: Extending the Rectangle class with an extension method
static class RectangleExtensions
{
  public static double CalculatePerimeter(this Rectangle rectangle)
  {
    return 2 * (rectangle.width + rectangle.height);
  }
}
class Program
{
  static void Main()
  {
    // Encapsulation: Accessing area through the property
    Shape shape = new Shape(25.0);
    Console.WriteLine("Encapsulation - Area: " + shape.Area);

    // Inheritance: Creating a Rectangle and accessing its area
    Rectangle rectangle = new Rectangle(5.0, 10.0);
    Console.WriteLine("Inheritance - Rectangle Area: " + rectangle.Area);

    // Composition: Creating a Circle and accessing its area
    Circle circle = new Circle(4.0);
    Console.WriteLine("Composition - Circle Area: " + circle.GetArea());

    // Polymorphism and Interfaces: Creating a Triangle and invoking Draw method
    Triangle triangle = new Triangle(4.0, 6.0);
    Console.Write("Polymorphism - ");
    triangle.Draw();

    // Abstract Classes: Creating a Polygon (Triangle) and calculating its perimeter
    Polygon polygon = new Triangle(3.0, 4.0);
    Console.WriteLine("Abstract Class - Type: " + polygon.GetPolygonType());
    Console.WriteLine("Abstract Class - Area (from shape): " + ((Shape)polygon).Area.ToString());

    // Class Extensions: Using the CalculatePerimeter extension method for Rectangle
    Console.WriteLine("Class Extensions - Rectangle Perimeter: " + rectangle.CalculatePerimeter());
  }
}

Introduction to Classes by Microsoft Learn.