Skip to main content

3.1 - Classes and Objects

Classes and objects are the fundamental building blocks of object-oriented programming in C#. They form the core of how we structure and organize code in an object-oriented paradigm.

🧩 Visual Learning: Classes vs Objects

Think of the relationship between classes and objects like a blueprint and houses:

┌───────────────────┐                 ┌───────────────────┐
│ │ │ House #1 │
│ │ │ ┌─┐ │
│ │ │ ┌┘ └┐ ┌────┐ │
│ BLUEPRINT │ creates ────────┼─┘ └───┘ │ │
│ │ │ │ │
│ │ │ └─────────────┘ │
│ │ └───────────────────┘
│ │
│ │ ┌───────────────────┐
│ │ │ House #2 │
│ │ │ ┌─┐ │
│ │ │ ┌┘ └┐ ┌────┐ │
│ │ creates ────────┼─┘ └───┘ │ │
│ │ │ │ │
│ │ │ └─────────────┘ │
└───────────────────┘ └───────────────────┘
  • The class is like a blueprint - it defines what all houses of this type will have
  • The objects are like actual houses built from that blueprint - each one is separate but follows the same design

💡 Concept Breakdown: Classes and Objects

What is a Class?

A class is a blueprint or template that defines the structure and behavior of a particular type of object. It encapsulates:

  • Data (fields and properties) that represents the state (what the object "knows")
  • Methods that define the behavior (what the object can "do")
  • Events that enable communication between objects
  • Access modifiers that control visibility and encapsulation

What is an Object?

An object is an instance of a class that exists in memory during program execution. When you create an object, you're essentially creating a concrete realization of the class blueprint with its own unique state.

🔰 Beginner's Corner: Real-World Analogy

Think about a car manufacturing process:

  • The class is like the car design and manufacturing plans

    • It specifies what properties all cars will have (color, model, engine size)
    • It defines what all cars can do (accelerate, brake, turn)
    • It establishes how the parts work together
  • Each object is like an individual car that rolls off the assembly line

    • It has its own specific values for those properties (red color, sedan model, 2.0L engine)
    • It can perform all the actions defined in the blueprint
    • It exists as a real entity that can be used

Key Concepts

  1. Encapsulation: Classes hide their internal implementation details and expose only what's necessary through well-defined interfaces.

  2. Abstraction: Classes represent complex systems through simplified models, exposing only essential characteristics.

  3. Instantiation: The process of creating objects from class definitions using the new keyword.

  4. State and Behavior: Objects maintain state (data) and provide behavior (methods) that can manipulate that state.

  5. Identity: Each object has a unique identity, even if its state is identical to another object of the same class.

In this chapter, we'll explore how to define classes, create objects, and work with the various components that make up classes in C#.

3.1.1 - Class Definition and Structure

A class in C# is defined using the class keyword, followed by the class name and a pair of curly braces that enclose the class members. Class members include fields, properties, methods, events, and other nested types.

🔰 Beginner's Corner: Anatomy of a Class

Think of a class like a form that defines what information and actions are associated with a particular type of thing:

┌─────────────────────────────────────────┐
│ CLASS: Person │
├─────────────────────────────────────────┤
│ FIELDS (Private Data): │
│ - _name (string) │
│ - _age (int) │
├─────────────────────────────────────────┤
│ PROPERTIES (Public Access Points): │
│ - Name │
│ - Age │
├─────────────────────────────────────────┤
│ CONSTRUCTOR (Initialization): │
│ - Person(name, age) │
├─────────────────────────────────────────┤
│ METHODS (Actions): │
│ - Introduce() │
└─────────────────────────────────────────┘

💡 Concept Breakdown: Parts of a Class

Let's break down the main parts of a class:

  1. Fields - These are variables that store data. They're usually private (hidden from outside).
  2. Properties - These provide controlled access to fields, allowing validation and logic.
  3. Constructors - Special methods that run when you create a new object.
  4. Methods - Functions that define what the object can do.
// Basic class structure
public class Person
{
// Fields - variables that store the internal data of the class
// These are not directly accessible from outside the class
private string _name;
private int _age;

// Properties - provide a controlled way to access private fields
// They allow for validation or additional logic when getting or setting values
public string Name
{
get { return _name; } // Returns the value of the private field
set { _name = value; } // Sets the value of the private field (value is a keyword)
}

public int Age
{
get { return _age; }
set { _age = value; }
}

// Constructor - initializes the object
// Constructors are special methods that are called when an object is created
public Person(string name, int age)
{
// 'this' keyword refers to the current instance of the class
// It's used to distinguish between class members and method parameters with the same name
this._name = name;
this._age = age;
}

// Method - defines behavior
// Methods can perform operations using the class's data and can interact with other objects
public void Introduce()
{
// String interpolation ($) allows embedding expressions within string literals
Console.WriteLine($"Hello, my name is {_name} and I am {_age} years old.");
}
}

Class Components

  1. Fields: Variables that store the data of the class.
  2. Properties: Members that provide a flexible mechanism to read, write, or compute the values of private fields.
  3. Methods: Functions that define the behavior of the class.
  4. Constructors: Special methods that are called when an object is created.
  5. Events: Notifications that the class can send to other classes or objects.
  6. Nested Types: Classes, structs, interfaces, and enumerations defined within a class.

3.1.2 - Creating and Using Objects

Objects are instances of classes. They are created using the new keyword, which allocates memory for the object and calls its constructor.

🔰 Beginner's Corner: Creating Your First Object

Creating and using an object involves three main steps:

  1. Declaration - Tell C# what type of object you want to work with
  2. Instantiation - Create the actual object using the new keyword
  3. Initialization - Set up the object with initial values (using a constructor)
// Step 1: Declaration - Create a variable that can hold a Person object
Person someone;

// Step 2 & 3 combined: Instantiation & Initialization
// The 'new' keyword creates the object and the constructor initializes it
someone = new Person("Jane", 25);

// You can also combine all three steps in one line:
Person someone = new Person("Jane", 25);

💡 Concept Breakdown: Working with Objects

Once you've created an object, you can:

  1. Access its properties - Get or set the object's data
  2. Call its methods - Make the object perform actions
  3. Pass it to other methods - Use the object in other parts of your program
// Creating a new Person object using the constructor
Person person1 = new Person("John Doe", 30);

// Accessing properties to read values
Console.WriteLine($"Name: {person1.Name}"); // Output: Name: John Doe

// Accessing properties to modify values
person1.Age = 31; // Changing the age

// Calling a method on the object
person1.Introduce(); // Output: Hello, my name is John Doe and I am 31 years old.

// Creating another Person object
Person person2 = new Person("Jane Smith", 25);
person2.Introduce(); // Output: Hello, my name is Jane Smith and I am 25 years old.

Object Lifecycle

  1. Creation: An object is created using the new keyword, which allocates memory and calls the constructor.
  2. Usage: The object's properties and methods are accessed and used.
  3. Destruction: When an object is no longer needed, it becomes eligible for garbage collection. The garbage collector automatically frees the memory used by the object.

3.1.3 - Fields and Properties

Fields and properties are used to store and manage the data of a class.

Fields

Fields are variables declared directly in a class. They can be of any type, including primitive types, structs, or other classes.

public class BankAccount
{
// Private fields to store the account data
private string _accountNumber;
private decimal _balance;

// Readonly field that can only be set in the constructor
// Once set, it cannot be changed during the object's lifetime
private readonly DateTime _dateCreated;

// Constructor
public BankAccount(string accountNumber)
{
// Validate input parameters
if (string.IsNullOrWhiteSpace(accountNumber))
{
throw new ArgumentException("Account number cannot be empty", nameof(accountNumber));
}

this._accountNumber = accountNumber;
this._balance = 0; // Initial balance is zero
this._dateCreated = DateTime.Now; // Set creation date to current time
}
}

Properties

Properties provide a way to access and modify the values of fields while enforcing validation and controlling access.

public class BankAccount
{
private string _accountNumber;
private decimal _balance;
private readonly DateTime _dateCreated;

// Read-only property - it only has a getter, no setter
// This prevents the account number from being changed after creation
public string AccountNumber
{
get { return _accountNumber; }
}

// Property with validation and a private setter
// The private setter ensures that only methods within this class
// can modify the balance, while any code can read it
public decimal Balance
{
get { return _balance; }
private set
{
// Validation to ensure the balance is never negative
if (value < 0)
{
throw new ArgumentException("Balance cannot be negative");
}
_balance = value;
}
}

// Read-only property for the creation date
public DateTime DateCreated
{
get { return _dateCreated; }
}

// Method to deposit money
public void Deposit(decimal amount)
{
// Validate that the deposit amount is positive
if (amount <= 0)
{
throw new ArgumentException("Deposit amount must be positive", nameof(amount));
}

// Use the property setter which includes validation
Balance += amount;
}

// Method to withdraw money
public void Withdraw(decimal amount)
{
// Validate that the withdrawal amount is positive
if (amount <= 0)
{
throw new ArgumentException("Withdrawal amount must be positive", nameof(amount));
}

// Validate that there are sufficient funds
if (amount > Balance)
{
throw new ArgumentException("Insufficient funds for withdrawal", nameof(amount));
}

// Use the property setter which includes validation
Balance -= amount;
}
}

3.1.4 - Methods and Parameters

Methods define the behavior of a class. They can take parameters, perform operations, and return values.

Method Declaration

public class Calculator
{
// Method with parameters and return value
// This is a basic method that takes two integers and returns their sum
public int Add(int a, int b)
{
return a + b;
}

// Method with no parameters and no return value (void)
// This method performs an action but doesn't return any value
public void Clear()
{
Console.WriteLine("Calculator cleared");
}

// Method with optional parameters
// The parameter 'b' has a default value of 1.0, so it can be omitted when calling the method
public double Divide(double a, double b = 1.0)
{
// Check for division by zero
if (b == 0)
{
throw new DivideByZeroException("Cannot divide by zero");
}
return a / b;
}

// Method with output parameters (out)
// The 'out' keyword indicates that the parameter is passed by reference and must be assigned
// a value inside the method before it returns
public bool TryParse(string input, out int result)
{
// int.TryParse attempts to convert the string to an integer
// It returns true if successful and assigns the parsed value to the out parameter
return int.TryParse(input, out result);
}

// Method with reference parameters (ref)
// The 'ref' keyword indicates that the parameter is passed by reference,
// allowing the method to modify the original variables
public void Swap(ref int a, ref int b)
{
// Store the value of 'a' in a temporary variable
int temp = a;
// Assign the value of 'b' to 'a'
a = b;
// Assign the original value of 'a' (stored in temp) to 'b'
b = temp;
}

// Method with parameter array (params)
// The 'params' keyword allows a method to accept a variable number of arguments
public int Sum(params int[] numbers)
{
int sum = 0;
foreach (int number in numbers)
{
sum += number;
}
return sum;
}
}

Parameter Types

  1. Value Parameters: The method receives a copy of the argument value.
  2. Reference Parameters (ref): The method receives a reference to the argument variable.
  3. Output Parameters (out): Similar to reference parameters, but the method must assign a value to the parameter before returning.
  4. Parameter Arrays (params): Allows a variable number of arguments to be passed to a method.
  5. Optional Parameters: Parameters with default values that can be omitted when calling the method.

3.1.5 - Constructors and Destructors

Constructors are special methods that are called when an object is created. They initialize the object's state. Destructors are called when an object is being destroyed and are used to release resources.

Constructors

public class Car
{
// Private fields to store the car data
private string _make;
private string _model;
private int _year;

// Properties to access the car data
public string Make
{
get { return _make; }
set { _make = value; }
}

public string Model
{
get { return _model; }
set { _model = value; }
}

public int Year
{
get { return _year; }
set { _year = value; }
}

// Default constructor - takes no parameters
// Initializes the car with default values
public Car()
{
_make = "Unknown";
_model = "Unknown";
_year = DateTime.Now.Year;
}

// Parameterized constructor - allows setting all properties during object creation
public Car(string make, string model, int year)
{
_make = make;
_model = model;
_year = year;
}

// Constructor chaining - calls another constructor in the same class
// This avoids duplicating code by reusing the existing constructor
public Car(string make, string model) : this(make, model, DateTime.Now.Year)
{
// The body can be empty because all initialization is done in the called constructor
// Additional initialization specific to this constructor can be added here if needed
}

// Method to display car information
public override string ToString()
{
return $"{_year} {_make} {_model}";
}
}

// Usage example
Car car1 = new Car(); // Using default constructor
Car car2 = new Car("Toyota", "Corolla", 2022); // Using parameterized constructor
Car car3 = new Car("Honda", "Civic"); // Using constructor with chaining

Destructors

Destructors are used to clean up resources that are not managed by the garbage collector, such as file handles, database connections, or unmanaged memory.

public class ResourceHolder
{
// IntPtr is used to represent a pointer or a handle to an unmanaged resource
private IntPtr _resource;

// Constructor - called when the object is created
public ResourceHolder()
{
// Acquire a resource when the object is created
_resource = AllocateResource();
Console.WriteLine("Resource acquired");
}

// Destructor (finalizer) - called when the object is garbage collected
// The ~ symbol is used to define a destructor
~ResourceHolder()
{
// Release the resource when the object is garbage collected
// This ensures resources are freed even if Dispose is not called explicitly
if (_resource != IntPtr.Zero)
{
Console.WriteLine("Resource released by destructor");
FreeResource(_resource);
_resource = IntPtr.Zero;
}
}

// Implementing IDisposable is a better practice than relying on destructors
// This allows for deterministic cleanup of resources
public void Dispose()
{
// Release the resource when Dispose is called
if (_resource != IntPtr.Zero)
{
Console.WriteLine("Resource released by Dispose");
FreeResource(_resource);
_resource = IntPtr.Zero;

// Suppress the finalizer since we've already cleaned up
GC.SuppressFinalize(this);
}
}

// Method to simulate allocating a resource
private IntPtr AllocateResource()
{
// In a real application, this might open a file, create a database connection, etc.
Console.WriteLine("Allocating resource");
return new IntPtr(1);
}

// Method to simulate freeing a resource
private void FreeResource(IntPtr resource)
{
// In a real application, this might close a file, dispose a database connection, etc.
Console.WriteLine("Freeing resource");
}
}

// Usage example
public void UseResource()
{
// Using statement ensures Dispose is called even if an exception occurs
using (var resource = new ResourceHolder())
{
// Use the resource
Console.WriteLine("Using the resource");
} // Dispose is automatically called here

// Alternative approach without using statement
var resource2 = new ResourceHolder();
try
{
// Use the resource
Console.WriteLine("Using another resource");
}
finally
{
// Ensure resource is disposed
resource2.Dispose();
}
}

3.1.6 - Static Members

Static members belong to the class itself rather than to any specific instance of the class. They are shared among all instances of the class.

// Static class - cannot be instantiated, can only contain static members
public static class MathUtility
{
// Static field - belongs to the class, not to instances
// readonly means it cannot be changed after initialization
public static readonly double PI = 3.14159265359;

// Static property - tracks how many operations have been performed
private static int _operationCount;
public static int OperationCount
{
get { return _operationCount; }
private set { _operationCount = value; }
}

// Static constructor - called automatically before the first static member is accessed
// Used to initialize static fields and perform one-time setup
static MathUtility()
{
OperationCount = 0;
Console.WriteLine("MathUtility initialized");
}

// Static method - calculates the area of a circle
public static double CalculateCircleArea(double radius)
{
// Validate input
if (radius < 0)
{
throw new ArgumentException("Radius cannot be negative", nameof(radius));
}

// Increment the operation counter
OperationCount++;

// Calculate and return the area
return PI * radius * radius;
}

// Static method - calculates the circumference of a circle
public static double CalculateCircleCircumference(double radius)
{
// Validate input
if (radius < 0)
{
throw new ArgumentException("Radius cannot be negative", nameof(radius));
}

// Increment the operation counter
OperationCount++;

// Calculate and return the circumference
return 2 * PI * radius;
}

// Static method - resets the operation counter
public static void ResetOperationCount()
{
OperationCount = 0;
Console.WriteLine("Operation count reset to 0");
}
}

// Usage example
public void UseMathUtility()
{
// The static constructor is automatically called the first time
// a static member is accessed

// Access a static field
Console.WriteLine($"Value of PI: {MathUtility.PI}");

// Call static methods
double radius = 5.0;
double area = MathUtility.CalculateCircleArea(radius);
Console.WriteLine($"Area of circle with radius {radius}: {area:F2}");

double circumference = MathUtility.CalculateCircleCircumference(radius);
Console.WriteLine($"Circumference of circle with radius {radius}: {circumference:F2}");

// Display the operation count
Console.WriteLine($"Operations performed: {MathUtility.OperationCount}");

// Reset the operation count
MathUtility.ResetOperationCount();

// Perform another calculation
double newRadius = 10.0;
double newArea = MathUtility.CalculateCircleArea(newRadius);
Console.WriteLine($"Area of circle with radius {newRadius}: {newArea:F2}");

// Display the operation count again
Console.WriteLine($"Operations performed after reset: {MathUtility.OperationCount}");
}

3.1.7 - Access Modifiers

Access modifiers control the visibility and accessibility of classes, methods, properties, and fields.

  1. public: Accessible from any code.
  2. private: Accessible only within the containing class.
  3. protected: Accessible within the containing class and derived classes.
  4. internal: Accessible within the same assembly.
  5. protected internal: Accessible within the same assembly or derived classes.
  6. private protected (C# 7.2+): Accessible within the containing class or derived classes within the same assembly.
public class AccessExample
{
// Public - accessible from any code
public int PublicField;

// Private - accessible only within this class
private int PrivateField;

// Protected - accessible within this class and derived classes
protected int ProtectedField;

// Internal - accessible within the same assembly
internal int InternalField;

// Protected Internal - accessible within the same assembly or derived classes
protected internal int ProtectedInternalField;

// Private Protected - accessible within this class or derived classes within the same assembly
private protected int PrivateProtectedField;

// Constructor to initialize fields
public AccessExample()
{
// Initialize fields with default values
PublicField = 0;
PrivateField = 0;
ProtectedField = 0;
InternalField = 0;
ProtectedInternalField = 0;
PrivateProtectedField = 0;
}

// Method to demonstrate access within the class
public void AccessFields()
{
// All fields are accessible within the class
PublicField = 1;
PrivateField = 2;
ProtectedField = 3;
InternalField = 4;
ProtectedInternalField = 5;
PrivateProtectedField = 6;

Console.WriteLine("Accessing fields from within the class:");
Console.WriteLine($"PublicField = {PublicField}");
Console.WriteLine($"PrivateField = {PrivateField}");
Console.WriteLine($"ProtectedField = {ProtectedField}");
Console.WriteLine($"InternalField = {InternalField}");
Console.WriteLine($"ProtectedInternalField = {ProtectedInternalField}");
Console.WriteLine($"PrivateProtectedField = {PrivateProtectedField}");
}
}

// Derived class to demonstrate inheritance and access
public class DerivedAccessExample : AccessExample
{
// Method to demonstrate access in a derived class
public void AccessBaseClassFields()
{
// Accessible fields in a derived class
PublicField = 10;
// PrivateField = 20; // Error: not accessible
ProtectedField = 30;
InternalField = 40; // Accessible if in the same assembly
ProtectedInternalField = 50;
PrivateProtectedField = 60; // Accessible if in the same assembly

Console.WriteLine("Accessing base class fields from a derived class:");
Console.WriteLine($"PublicField = {PublicField}");
// Console.WriteLine($"PrivateField = {PrivateField}"); // Error: not accessible
Console.WriteLine($"ProtectedField = {ProtectedField}");
Console.WriteLine($"InternalField = {InternalField}");
Console.WriteLine($"ProtectedInternalField = {ProtectedInternalField}");
Console.WriteLine($"PrivateProtectedField = {PrivateProtectedField}");
}
}

// Usage example
public void DemonstrateAccessModifiers()
{
// Create an instance of AccessExample
var example = new AccessExample();

// Access public members
example.PublicField = 100;

// The following would cause compile-time errors because they are not accessible
// example.PrivateField = 200;
// example.ProtectedField = 300;
// example.InternalField = 400; // Accessible if in the same assembly
// example.ProtectedInternalField = 500; // Accessible if in the same assembly
// example.PrivateProtectedField = 600;

// Demonstrate access within the class
example.AccessFields();

// Create an instance of DerivedAccessExample
var derivedExample = new DerivedAccessExample();

// Demonstrate access from a derived class
derivedExample.AccessBaseClassFields();

// Demonstrate public access from outside both classes
Console.WriteLine("Demonstrating public access from outside both classes:");
Console.WriteLine($"example.PublicField = {example.PublicField}");
Console.WriteLine($"derivedExample.PublicField = {derivedExample.PublicField}");
}

3.1.8 - Properties with Backing Fields

Properties often use backing fields to store their values. This pattern allows for validation and additional logic when getting or setting the property value.

public class Employee
{
// Backing fields for properties
private string _name;
private string _email;
private decimal _salary;
private DateTime _hireDate;

// Property with backing field and validation
public string Name
{
get { return _name; }
set
{
// Validate that the name is not empty
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("Name cannot be null or empty", nameof(value));
}
_name = value;
}
}

// Property with backing field, validation, and transformation
public string Email
{
get { return _email; }
set
{
// Validate that the email is not empty
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("Email cannot be null or empty", nameof(value));
}

// Validate that the email contains the @ symbol
if (!value.Contains("@"))
{
throw new ArgumentException("Email must contain the @ symbol", nameof(value));
}

_email = value.Trim().ToLower(); // Normalize email by trimming and converting to lowercase
}
}

// Property with backing field and range validation
public decimal Salary
{
get { return _salary; }
set
{
// Validate that the salary is not negative
if (value < 0)
{
throw new ArgumentException("Salary cannot be negative", nameof(value));
}
_salary = value;
}
}

// Property with backing field and date validation
public DateTime HireDate
{
get { return _hireDate; }
set
{
// Validate that the hire date is not in the future
if (value > DateTime.Now)
{
throw new ArgumentException("Hire date cannot be in the future", nameof(value));
}
_hireDate = value;
}
}

// Calculated property (no backing field)
public int YearsOfService
{
get
{
// Calculate years of service based on hire date
DateTime now = DateTime.Now;
int years = now.Year - _hireDate.Year;

// Adjust for hire date that hasn't occurred yet this year
if (now.Month < _hireDate.Month || (now.Month == _hireDate.Month && now.Day < _hireDate.Day))
{
years--;
}

return Math.Max(0, years);
}
}

// Constructor
public Employee(string name, string email)
{
// Use property setters to ensure validation
Name = name;
Email = email;
_salary = 0;
_hireDate = DateTime.Now;
}

// Method to give a raise
public void GiveRaise(decimal percentage)
{
if (percentage < 0 || percentage > 100)
{
throw new ArgumentOutOfRangeException(nameof(percentage),
"Percentage must be between 0 and 100");
}

decimal raiseAmount = Salary * (percentage / 100);
Salary += raiseAmount;
}
}

3.1.9 - Auto-Implemented Properties

Auto-implemented properties provide a simplified syntax for properties that don't require additional logic in their accessors.

public class Product
{
// Auto-implemented property
// The compiler automatically creates a private, anonymous backing field
// that can only be accessed through the property's get and set accessors
public string Name { get; set; }

// Auto-implemented property with initializer (C# 6+)
// The 'm' suffix indicates a decimal literal
public decimal Price { get; set; } = 0.0m;

// Read-only auto-implemented property (C# 6+)
// It can only be set in the constructor or by an initializer
public Guid Id { get; } = Guid.NewGuid();

// Auto-implemented property with private setter
// It can be read from anywhere but only set from within the class
public bool IsAvailable { get; private set; }

// Auto-implemented property with default value
public string Category { get; set; } = "Uncategorized";

// Read-only property with initializer
public DateTime CreatedDate { get; } = DateTime.Now;

// Constructor
public Product()
{
// Default constructor
IsAvailable = false;
}

// Constructor with parameter
public Product(string name) : this()
{
Name = name;
}

// Constructor with multiple parameters
public Product(string name, decimal price) : this(name)
{
Price = price;
}

// Method to set availability
public void SetAvailability(bool available)
{
IsAvailable = available;
}

// Method to apply a discount
public void ApplyDiscount(decimal percentage)
{
if (percentage < 0 || percentage > 100)
{
throw new ArgumentOutOfRangeException(nameof(percentage),
"Discount percentage must be between 0 and 100");
}

decimal discountFactor = 1 - (percentage / 100);
Price *= discountFactor;
}

// Override ToString method to provide a string representation of the product
public override string ToString()
{
string availabilityStatus = IsAvailable ? "Available" : "Not Available";
return $"Product: {Name} (ID: {Id})\n" +
$"Price: {Price:C}\n" +
$"Category: {Category}\n" +
$"Status: {availabilityStatus}\n" +
$"Created: {CreatedDate}";
}
}

// Usage example
public void UseProduct()
{
// Create a product using the default constructor and set properties
var product1 = new Product();
product1.Name = "Laptop";
product1.Price = 999.99m;
product1.Category = "Electronics";
product1.SetAvailability(true);

// Create a product using the constructor with name parameter
var product2 = new Product("Smartphone");
product2.Price = 499.99m;

// Create a product using the constructor with name and price parameters
var product3 = new Product("Headphones", 79.99m);

// Create a product using object initializer syntax
var product4 = new Product
{
Name = "Tablet",
Price = 349.99m,
Category = "Electronics"
};
product4.SetAvailability(true);

// Apply a discount
product4.ApplyDiscount(15);

// Display product information
Console.WriteLine(product4.ToString());
}

3.1.10 - Object Initializers

Object initializers provide a concise syntax to create and initialize objects in a single statement.

// Class with properties for demonstration
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; }

// Default constructor
public Person()
{
Name = string.Empty;
Age = 0;
Email = string.Empty;
}

// Constructor with parameter
public Person(string name)
{
Name = name;
Age = 0;
Email = string.Empty;
}
}

// Usage examples

// Without object initializer - traditional approach
// Create the object and then set each property separately
Person person1 = new Person();
person1.Name = "John";
person1.Age = 30;
person1.Email = "john@example.com";

// With object initializer - more concise syntax
// Create and initialize the object in a single statement
Person person2 = new Person
{
Name = "Jane",
Age = 25,
Email = "jane@example.com"
};

// Object initializer with constructor
// Combines a constructor call with additional property initialization
Person person3 = new Person("Alice")
{
Age = 35,
Email = "alice@example.com"
};

// Object initializer with var keyword
// Type is inferred from the right side of the assignment
var person4 = new Person
{
Name = "Bob",
Age = 40,
Email = "bob@example.com"
};

// You can initialize only some properties, leaving others with default values
var person5 = new Person
{
Name = "Charlie",
// Age will be 0 (default)
Email = "charlie@example.com"
};

3.1.11 - Anonymous Types

Anonymous types allow you to create objects without explicitly defining a class. They are useful for temporary data structures.

// Creating a simple anonymous type
// Anonymous types allow you to create objects without explicitly defining a class
var person = new
{
Name = "John Doe",
Age = 30,
Email = "john.doe@example.com"
};

// Accessing properties of an anonymous type
// Properties are read-only and strongly typed
Console.WriteLine("Person Information:");
Console.WriteLine($"Name: {person.Name}");
Console.WriteLine($"Age: {person.Age}");
Console.WriteLine($"Email: {person.Email}");

// Creating another anonymous type with different properties
var product = new
{
ProductName = "Laptop",
Price = 999.99m,
InStock = true
};

// Anonymous types with computed properties
var today = DateTime.Now;
var dateInfo = new
{
Date = today,
DayOfWeek = today.DayOfWeek,
DayOfYear = today.DayOfYear,
FormattedDate = today.ToString("yyyy-MM-dd")
};

// Anonymous types in collections
var employees = new[]
{
new { Name = "Alice", Department = "HR", YearsOfService = 5 },
new { Name = "Bob", Department = "IT", YearsOfService = 3 },
new { Name = "Charlie", Department = "Finance", YearsOfService = 7 }
};

// Iterating through a collection of anonymous types
foreach (var emp in employees)
{
Console.WriteLine($"{emp.Name} - {emp.Department} ({emp.YearsOfService} years)");
}

// Using anonymous types with LINQ
// Find employees in the IT department
var itEmployees = employees.Where(e => e.Department == "IT");

// Using anonymous types for projection (selecting specific properties)
var employeeNames = employees.Select(e => new { e.Name, e.Department });

// Using anonymous types for transformation
var seniorityInfo = employees.Select(e => new
{
EmployeeName = e.Name,
Department = e.Department,
Seniority = e.YearsOfService >= 5 ? "Senior" : "Junior"
});

// Using anonymous types for grouping
var departmentGroups = employees
.GroupBy(e => e.Department)
.Select(g => new
{
Department = g.Key,
EmployeeCount = g.Count(),
AverageYearsOfService = g.Average(e => e.YearsOfService)
});

// Equality comparison of anonymous types
// Anonymous types with the same property names and values are equal
var person1 = new { Name = "John", Age = 30 };
var person2 = new { Name = "John", Age = 30 };
var person3 = new { Name = "Jane", Age = 25 };

Console.WriteLine($"person1 equals person2: {person1.Equals(person2)}"); // True
Console.WriteLine($"person1 equals person3: {person1.Equals(person3)}"); // False

3.1.12 - Required Members (C# 11+)

Required members, introduced in C# 11, allow you to specify that certain properties must be initialized during object creation, even when using object initializers.

Cross-Reference

For more advanced examples and details on Required Members, see Section 7.2.14 - Required Members (C# 11+) in the Advanced C# Topics chapter.

// Class with required and optional properties
public class Person
{
// Required property - must be initialized during object creation
// The 'required' modifier ensures this property is set when using object initializers
public required string Name { get; set; }

// Required property with default value
// Even though it has a default value, it must still be explicitly set when using object initializers
public required int Age { get; set; } = 0;

// Optional property - can be null
// The '?' indicates that this property is nullable
public string? Email { get; set; }

// Optional property - can be null
public string? PhoneNumber { get; set; }

// Method to display person information
public void DisplayInfo()
{
Console.WriteLine($"Name: {Name}, Age: {Age}");

if (Email != null)
{
Console.WriteLine($"Email: {Email}");
}

if (PhoneNumber != null)
{
Console.WriteLine($"Phone: {PhoneNumber}");
}
}
}

// Usage examples

// Valid - all required properties are set
var person1 = new Person
{
Name = "John Doe", // Required
Age = 30 // Required, even though it has a default value
// Email and PhoneNumber are optional
};

// Valid - all required properties are set, plus optional ones
var person2 = new Person
{
Name = "Jane Smith",
Age = 25,
Email = "jane.smith@example.com",
PhoneNumber = "555-123-4567"
};

// The following would cause a compile-time error because Name is required
/*
var personWithError = new Person
{
// Name is missing, which will cause a compile-time error
Age = 40,
Email = "error@example.com"
};
*/

// This would also cause a compile-time error for the same reason
/*
var anotherError = new Person();
*/

// Required properties must be set even when using constructors with object initializers
/*
public Person(string email)
{
Email = email;
// This constructor doesn't set Name or Age, so they must still be set with an object initializer
}

var person3 = new Person("john@example.com")
{
Name = "John", // Still required
Age = 30 // Still required
};
*/

3.1.13 - Primary Constructors (C# 12+)

Primary constructors, introduced in C# 12, provide a concise syntax for defining constructors and their parameters directly in the class declaration.

Cross-Reference

For more advanced examples and details on Primary Constructors, see Section 7.2.17 - Primary Constructors (C# 12+) in the Advanced C# Topics chapter.

// Class with primary constructor
// The parameters are declared directly in the class declaration
public class Person(string name, int age)
{
// Properties initialized using primary constructor parameters
// The parameters are accessible throughout the class body
public string Name { get; set; } = name;
public int Age { get; set; } = age;

// Computed property using a primary constructor parameter
public bool IsAdult { get; } = age >= 18;

// Another computed property
public int BirthYear { get; } = DateTime.Now.Year - age;

// Method using properties initialized from constructor parameters
public void Introduce()
{
// We can use the properties that were initialized with the parameters
Console.WriteLine($"Hello, my name is {Name} and I am {Age} years old.");
Console.WriteLine($"I was born around {BirthYear} and I am {(IsAdult ? "an adult" : "not an adult")}.");
}

// Method using primary constructor parameters directly
public int CalculateAgeInFutureYear(int futureYear)
{
int currentYear = DateTime.Now.Year;

if (futureYear <= currentYear)
{
throw new ArgumentException("Year must be in the future", nameof(futureYear));
}

// Using the age parameter from the primary constructor
return age + (futureYear - currentYear);
}
}

// Usage examples

// Creating a person using the primary constructor
var person = new Person("John Doe", 30);

// Calling methods
person.Introduce();

// Accessing properties
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
Console.WriteLine($"Birth Year: {person.BirthYear}, Is Adult: {person.IsAdult}");

// Calculating age in a future year
int futureYear = DateTime.Now.Year + 10;
int futureAge = person.CalculateAgeInFutureYear(futureYear);
Console.WriteLine($"In {futureYear}, {person.Name} will be {futureAge} years old.");

// Modifying properties
person.Name = "John Smith";
person.Age = 31;
Console.WriteLine($"Updated Name: {person.Name}, Updated Age: {person.Age}");

// Note that IsAdult and BirthYear don't change when Age is modified
// because they were initialized using the constructor parameter
Console.WriteLine($"Birth Year is still: {person.BirthYear}, Is Adult is still: {person.IsAdult}");

// Creating another person who is not an adult
var youngPerson = new Person("Alice", 15);
youngPerson.Introduce(); // Will indicate that Alice is not an adult

Primary constructors simplify the syntax for creating classes with constructor parameters, especially when those parameters are used to initialize properties. They reduce boilerplate code and make class definitions more concise and readable.

3.1.14 - Partial Members (C# 13+)

Partial members allow you to split the declaration and implementation of class members across multiple partial class declarations. This feature is particularly useful for code generation scenarios and for separating generated code from user-written code.

3.1.14.1 - Partial Methods

Partial methods have been a feature of C# for some time. They allow the declaration of a method in one part of a partial class and the implementation in another part.

// First part of the partial class
// Partial classes allow splitting a class definition across multiple files
public partial class Customer
{
// Partial method declaration
// Partial methods allow the declaration of a method in one part of a partial class
// and the implementation in another part
partial void OnNameChanged(string newName);

// Private field to store the name
private string _name;

// Property with change notification
public string Name
{
get => _name;
set
{
// Only call the partial method if the value actually changes
if (_name != value)
{
_name = value;
OnNameChanged(value); // Call the partial method
}
}
}

// Constructor
public Customer(string name)
{
_name = name;
Console.WriteLine($"Customer created with name: {_name}");
}
}

// Second part of the partial class
// This could be in a separate file in a real application
public partial class Customer
{
// Partial method implementation
// If this implementation didn't exist, calls to OnNameChanged would be removed by the compiler
partial void OnNameChanged(string newName)
{
// This code runs when the Name property changes
Console.WriteLine($"Name changed to: {newName}");

// You can add any logic here that should run when the name changes
// For example, validation, logging, or notifying other components
if (newName.Length < 2)
{
Console.WriteLine("Warning: Name is very short");
}
}

// Additional methods can be added in this part of the class
public void DisplayInfo()
{
Console.WriteLine($"Customer Information: {_name}");
}
}

// Usage example
public void UsePartialMethods()
{
// Create a customer
var customer = new Customer("John");

// Display customer information
customer.DisplayInfo();

// Change the name - this will trigger the OnNameChanged partial method
customer.Name = "Jane";

// Display updated information
customer.DisplayInfo();

// Change to a short name to trigger the warning
customer.Name = "J";
}

3.1.14.2 - Partial Properties and Indexers (C# 13+)

C# 13 introduced support for partial properties and indexers, allowing you to separate the declaration and implementation of properties.

// First part of the partial class
// This part contains the declarations of partial properties and indexers
public partial class Product
{
// Partial property declaration
// Only the declaration is provided here, not the implementation
public partial decimal Price { get; set; }

// Partial indexer declaration
// This allows accessing the product like an array with an index
public partial string this[int index] { get; set; }

// Regular (non-partial) property
public string Name { get; set; } = string.Empty;

// Constructor
public Product(string name)
{
Name = name;
Console.WriteLine($"Product created: {Name}");
}
}

// Second part of the partial class
// This part contains the implementations of the partial properties and indexers
public partial class Product
{
// Backing field for the Price property
private decimal _price;

// Partial property implementation
// This provides the actual implementation of the property
// Note that auto-property syntax cannot be used here
public partial decimal Price
{
get => _price;
set
{
// Validation to ensure the price is not negative
if (value < 0)
{
throw new ArgumentException("Price cannot be negative", nameof(value));
}
_price = value;

// Additional logic can be added here
Console.WriteLine($"Price set to: {_price:C}");
}
}

// Backing field for the indexer
private string[] _data = new string[10];

// Partial indexer implementation
// This provides the actual implementation of the indexer
public partial string this[int index]
{
get
{
// Bounds checking
if (index < 0 || index >= _data.Length)
{
throw new IndexOutOfRangeException("Index is out of range");
}
return _data[index];
}
set
{
// Bounds checking
if (index < 0 || index >= _data.Length)
{
throw new IndexOutOfRangeException("Index is out of range");
}
_data[index] = value ?? string.Empty;

// Additional logic can be added here
Console.WriteLine($"Data at index {index} set to: {value}");
}
}

// Additional methods can be added in this part of the class
public void DisplayInfo()
{
Console.WriteLine($"Product: {Name}, Price: {Price:C}");

Console.WriteLine("Additional Data:");
for (int i = 0; i < _data.Length; i++)
{
if (!string.IsNullOrEmpty(_data[i]))
{
Console.WriteLine($" [{i}]: {_data[i]}");
}
}
}
}

// Usage example
public void UsePartialProperties()
{
// Create a product
var product = new Product("Laptop");

// Set the price using the partial property
product.Price = 999.99m;

// Use the indexer to store additional information
product[0] = "Brand: TechBrand";
product[1] = "Model: X1";
product[2] = "Color: Silver";

// Display product information
product.DisplayInfo();

try
{
// Try to set a negative price (should throw an exception)
product.Price = -10;
}
catch (ArgumentException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}

try
{
// Try to access an out-of-range index (should throw an exception)
var data = product[20];
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}

3.1.14.3 - Partial Events and Constructors (C# 14+)

C# 14 extends the partial member concept to events and constructors, providing even more flexibility in code organization.

// First part of the partial class
// This part contains the declarations of partial events and constructors
public partial class Logger
{
// Partial event declaration
// Only the declaration is provided here, not the implementation
public partial event EventHandler<string> LogAdded;

// Partial constructor declaration
// Only the declaration is provided here, not the implementation
public partial Logger(string logName);

// Property to get the logger name
public string Name { get; private set; }

// Property to get the log count
public int LogCount { get; private set; }

// Method to add a log entry
public void AddLog(string message)
{
if (string.IsNullOrEmpty(message))
{
throw new ArgumentException("Log message cannot be empty", nameof(message));
}

LogCount++;
string formattedMessage = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}";

Console.WriteLine($"Logger '{Name}': {formattedMessage}");

// Raise the event
LogAdded?.Invoke(this, message);
}

// Method to clear logs
public void ClearLogs()
{
LogCount = 0;
Console.WriteLine($"Logger '{Name}': All logs cleared");
}
}

// Second part of the partial class
// This part contains the implementations of the partial events and constructors
public partial class Logger
{
// Backing field for the event
private EventHandler<string> _logAdded;

// Partial constructor implementation
// This provides the actual implementation of the constructor
public partial Logger(string logName)
{
// Validate the log name
if (string.IsNullOrEmpty(logName))
{
throw new ArgumentException("Logger name cannot be empty", nameof(logName));
}

Name = logName;
LogCount = 0;

Console.WriteLine($"Logger '{Name}' initialized");
}

// Partial event implementation
// This provides the actual implementation of the event with custom accessors
public partial event EventHandler<string> LogAdded
{
add
{
Console.WriteLine($"Logger '{Name}': Log listener added");
_logAdded += value;
}
remove
{
Console.WriteLine($"Logger '{Name}': Log listener removed");
_logAdded -= value;
}
}
}

// Log listener class to handle log events
public class LogListener
{
private string _name;

public LogListener(string name)
{
_name = name;
}

// Event handler method
public void OnLogAdded(object sender, string message)
{
if (sender is Logger logger)
{
Console.WriteLine($"Listener '{_name}' received log from '{logger.Name}': {message}");
}
}
}

// Usage example
public void UsePartialEventsAndConstructors()
{
try
{
// Create a logger
var logger = new Logger("SystemLogger");

// Create log listeners
var listener1 = new LogListener("FileListener");
var listener2 = new LogListener("ConsoleListener");

// Subscribe to the LogAdded event
logger.LogAdded += listener1.OnLogAdded;
logger.LogAdded += listener2.OnLogAdded;

// Add some logs
logger.AddLog("Application started");
logger.AddLog("User logged in");

// Display log count
Console.WriteLine($"Total logs: {logger.LogCount}");

// Unsubscribe one listener
logger.LogAdded -= listener1.OnLogAdded;

// Add another log
logger.AddLog("Configuration updated");

// Clear logs
logger.ClearLogs();

// Add a final log
logger.AddLog("Application shutting down");

// Try to create a logger with an empty name (should throw an exception)
var invalidLogger = new Logger("");
}
catch (ArgumentException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}

3.1.14.4 - Rules for Partial Members

When working with partial members, it's important to understand the rules that govern their use to avoid compilation errors and ensure proper functionality:

  1. One Declaration, One Implementation: A partial member must have exactly one defining declaration (the signature) and exactly one implementing declaration (the body). Having multiple declarations or implementations will result in a compilation error.

  2. Matching Signatures: The signatures of the defining and implementing declarations must match exactly, including:

    • Access modifiers (public, private, protected, etc.)
    • Return type
    • Name
    • Parameter types and names
    • Type parameters for generic members
  3. Property Implementation Restrictions: For partial properties:

    • The implementing declaration cannot use auto-property syntax ({ get; set; })
    • You must provide explicit accessor bodies with curly braces
    • You typically need to define a backing field in the implementation part
  4. Constructor Initializer Rules: For partial constructors:

    • Only the implementing declaration can include a constructor initializer (this() or base())
    • The declaration part cannot call other constructors or the base constructor
    • Constructor parameters must match exactly between declaration and implementation
  5. Event Implementation Requirements: For partial events:

    • The implementing declaration must include both add and remove accessors
    • You typically need to define a backing field for the event in the implementation part
    • Custom event accessors allow for additional logic when subscribers are added or removed
  6. Modifier Consistency: Both the declaration and implementation must use the same modifiers:

    • If the declaration is static, the implementation must also be static
    • If the declaration is virtual, abstract, or override, the implementation must match
  7. Partial Keyword Required: Both the declaration and implementation must use the partial keyword:

    // Declaration
    public partial void Method();

    // Implementation
    public partial void Method() { /* implementation */ }

Benefits and Use Cases

Partial members are particularly useful in several scenarios:

  1. Code Generation: When working with tools that generate part of your code (like designers, ORM mappers, or source generators), partial members allow you to separate generated code from custom implementations.

  2. Separation of Concerns: You can separate the public API (declarations) from the implementation details, making the code more maintainable and easier to understand.

  3. Team Collaboration: Different team members can work on different parts of a class without conflicts, with one developer focusing on the API design and another on the implementation.

  4. Large Classes: For very large classes, you can organize related functionality into separate files while maintaining a single logical class.

  5. Framework Integration: When building frameworks that need to inject behavior into user-defined classes, partial members provide a clean way to separate framework code from user code.

3.1.15 - Summary and Best Practices

In this chapter, we've explored the fundamental concepts of classes and objects in C#, from basic class structure to advanced features like partial members. Let's summarize some key points and best practices:

Key Concepts Reviewed

  1. Classes and Objects: Classes define the blueprint, objects are instances of classes.
  2. Fields and Properties: Fields store data, properties provide controlled access to that data.
  3. Methods: Define the behavior of a class and can operate on the class's data.
  4. Constructors and Destructors: Initialize and clean up objects.
  5. Static Members: Belong to the class itself rather than to instances.
  6. Access Modifiers: Control the visibility and accessibility of class members.
  7. Object Initializers: Provide a concise syntax for creating and initializing objects.
  8. Anonymous Types: Allow for creating temporary, unnamed classes.
  9. Required Members: Ensure certain properties are initialized during object creation.
  10. Primary Constructors: Simplify class definitions with constructor parameters.
  11. Partial Members: Enable separation of declaration and implementation.

Best Practices for Classes and Objects

  1. Follow Naming Conventions:

    • Use PascalCase for class names and public members
    • Use camelCase for parameters and local variables
    • Prefix private fields with an underscore (_fieldName)
  2. Encapsulation:

    • Make fields private and expose them through properties
    • Validate data in property setters
    • Use the most restrictive access modifier possible
  3. Constructors:

    • Provide meaningful constructors that initialize the object to a valid state
    • Use constructor chaining to avoid code duplication
    • Consider providing a parameterless constructor when appropriate
  4. Properties:

    • Use auto-implemented properties for simple cases
    • Implement custom getters and setters when validation or additional logic is needed
    • Consider making properties read-only when appropriate
  5. Methods:

    • Keep methods focused on a single responsibility
    • Validate parameters at the beginning of methods
    • Return meaningful values or throw appropriate exceptions
  6. Resource Management:

    • Implement IDisposable for classes that manage unmanaged resources
    • Use the using statement or using declarations for disposable objects
    • Consider implementing finalizers as a fallback for resource cleanup
  7. Code Organization:

    • Use partial classes to organize large classes
    • Group related members together
    • Consider extracting complex functionality into helper classes

By following these principles and best practices, you can create well-structured, maintainable, and robust object-oriented code in C#.

In the next section, we'll explore inheritance, one of the key principles of object-oriented programming that allows classes to inherit characteristics and behavior from other classes.