Skip to main content

C# Advanced Improvement

I. Methods 📝​

In C#, methods are code blocks that perform specific tasks and can be called from other parts of the program. Methods facilitate code reuse, readability, and modularity.

1. Method Declaration 📝​

public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
}

Basic components:

  • Access modifiers (such as public, private)
  • Return type (like void, int, string, etc.)
  • Method name (using camelCase)
  • Parameter list (optional)

2. Method Invocation 🔗​

Instance Method Call​

Calculator calculator = new Calculator();
int result = calculator.Add(1, 2);
Console.WriteLine(result);

Static Method Call​

public class MathHelper
{
public static int Square(int x)
{
return x * x;
}
}

// Call directly using the class name
int result = MathHelper.Square(4); // result = 16

3. Method Overloading 🔄​

  1. Method names must be identical
  2. Parameter lists must be different: different number of parameters, different parameter types, or different parameter order
public class Calculator
{
// Add two integers
public int Add(int a, int b)
{
return a + b;
}

// Add two double-precision floating-point numbers
public double Add(double a, double b)
{
return a + b;
}

// Add three integers
public int Add(int a, int b, int c)
{
return a + b + c;
}

// Different parameter order
public int Add(int a, string b)
{
return a + int.Parse(b);
}
}

4. Method Parameters 📌​

Required Parameters​

public class MathOperations
{
public int Multiply(int x, int y)
{
return x * y;
}
}

Optional Parameters​

Optional parameters provide a concise way to handle method calls with default behavior and must come after required parameters.

public class Printer
{
// Optional parameter with a default value
public void PrintMessage(string message, bool uppercase = false)
{
if (uppercase)
{
Console.WriteLine(message.ToUpper());
}
else
{
Console.WriteLine(message);
}
}
}

Value Parameters (Default)​

void ModifyValue(int x)
{
x = 10; // Will not change the original value
}

Reference Parameters (ref)​

void ModifyReference(ref int x)
{
x = 10; // Will change the original value
}

int number = 5;
ModifyReference(ref number); // number is now 10

Output Parameters (out)​

Provides a concise mechanism for multiple return values, especially suitable for scenarios requiring both status and result returns.

public class UserValidator
{
// Validate user information
public bool ValidateUser(string input, out int userId, out string errorMessage)
{
userId = -1;
errorMessage = "";

// Parse user ID
if (!int.TryParse(input, out userId))
{
errorMessage = "Invalid user ID format";
return false;
}

// Check ID range
if (userId <= 0)
{
errorMessage = "User ID must be greater than 0";
return false;
}

return true;
}
}

public void ProcessUser()
{
UserValidator validator = new UserValidator();

// Call the method
bool isValid = validator.ValidateUser("123", out int userId, out string message);

if (isValid)
{
Console.WriteLine($"User ID: {userId}");
}
else
{
Console.WriteLine($"Validation failed: {message}");
}
}

Parameter Arrays (params)​

public int Sum(params int[] numbers)
{
int total = 0;
foreach(int num in numbers)
total += num;
return total;
}

5. Return Types 📤​

Methods with Return Values​

public class Circle
{
public double CalculateArea(double radius)
{
return Math.PI * radius * radius;
}
}

Void Methods​

public class Logger
{
public void LogError(string errorMessage)
{
Console.WriteLine($"Error: {errorMessage}");
}
}

II. Object-Oriented Programming đŸ“Ļ​

1. Principles 📝​

Object-oriented programming is based on four main principles: Encapsulation, Inheritance, Polymorphism, and Abstraction.

  • Encapsulation allows bundling data and methods into a single unit (class) and restricting access to certain components.
  • Inheritance allows one class (child or derived class) to inherit properties and methods from another class (parent or base class). This promotes code reuse and establishes hierarchical relationships between classes.
  • Polymorphism is the ability of a single function or method to work in various ways based on its input or the object calling it. In C#, polymorphism can be implemented through method overriding (using the override keyword) and method hiding (using the new keyword to hide methods from the base class).
  • Abstraction allows developers to hide complex implementations and show only essential features of objects. This means users interact only with necessary content while internal workings remain hidden. In C#, abstract classes and interfaces are tools that help implement abstraction.

These principles help design robust and scalable applications that are easy to maintain and further develop.

Encapsulation and Abstraction​

These concepts help manage access to object data and implement high-level abstractions in programming:

  • Encapsulation protects the internal state of objects and prevents unauthorized external access, allowing strict control over data and ensuring data integrity.
  • Abstraction allows separation of implementation from interface and supports creating systems with higher flexibility and extensibility, enabling developers to reduce programming complexity and improve efficiency.

In C#, encapsulation is ensured through access modifiers such as private, protected, and public.

These modifiers determine the visibility of class members, allowing implementation details to be hidden and exposing only necessary APIs.

  • public: Full access
  • private: Access only within the class
  • protected: Access within class and derived classes
  • internal: Access within the same assembly
ModifierSame ClassDerived Class
Same Assembly
Non-derived Class
Same Assembly
Derived Class
Different Assembly
Non-derived Class
Different Assembly
publicâœ”ī¸âœ”ī¸âœ”ī¸âœ”ī¸âœ”ī¸
privateâœ”ī¸âŒâŒâŒâŒ
protectedâœ”ī¸âœ”ī¸âŒâœ”ī¸âŒ
internalâœ”ī¸âœ”ī¸âœ”ī¸âŒâŒ
protected internalâœ”ī¸âœ”ī¸âœ”ī¸âœ”ī¸âŒ
private protectedâœ”ī¸âœ”ī¸âŒâŒâŒ

Inheritance and Polymorphism​

Inheritance and polymorphism are key principles of object-oriented programming that ensure code reusability and flexibility:

  • Inheritance allows creating a new class that inherits properties and methods from an existing class, improving code reusability and establishing hierarchical relationships between classes.
  • Polymorphism is implemented through the ability to override methods in child classes using the virtual and override keywords, and through interfaces that allow different classes to have a consistent set of methods.

2. Classes and Objects 📝​

Classes and objects are fundamental concepts in C# object-oriented programming. A class is a user-defined data type that encapsulates data and methods that operate on that data. An object is a specific instance of a class, representing an implementation of the defined class.

Class Basics​

tip

The default access modifier is internal, and the default access modifier for members is private.

public class Character
{
// Properties
public string Name { get; set; }
public int Level { get; set; }
public int Health { get; set; }

// Constructor
public Character(string name)
{
Name = name;
Level = 1;
Health = 100;
}

// Method
public void LevelUp()
{
Level++;
Health += 10;
Console.WriteLine($"{Name} leveled up to level {Level}");
}

// Virtual method, can be overridden by subclasses
public virtual void Introduction()
{
Console.WriteLine($"I am {Name}, currently at level {Level}");
}
}

Properties​

In C#, properties are special members used to encapsulate fields in a class and provide access to them. Properties allow external code to access class internal data like fields, but with added security and maintainability through property accessors.

Properties typically consist of two accessors:

  • get: Used to retrieve the property value
  • set: Used to set the property value
public class Program
{
public static void Main()
{
// Create an instance of the Person class
Person person = new Person();
// Use the set accessor to set the Name property
person.Name = "Jack";
// Use the get accessor to retrieve the Name property and output it
Console.WriteLine("Person's name is: " + person.Name);
}
}

public class Person
{
private string name;

public string Name
{
get { return name; }
set { name = value; }
}
}

Auto-implemented Properties: The compiler automatically generates a private field to store the value.

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

Read-only Properties: Only have a get accessor, no set accessor. Can only be set in the constructor. Suitable for scenarios requiring calculation or read-only functionality.

public class Circle
{
private double radius;

public Circle(double radius)
{
this.radius = radius;
}

public double Radius
{
get { return radius; }
}

public double Area
{
get { return Math.PI * radius * radius; }
}
}

Write-only Properties: Only have a set accessor, no get accessor. Suitable for scenarios where only setting values is needed without reading them.

public class Account
{
private decimal balance;

public decimal Balance
{
set { balance = value; }
}
}

Init-only Properties: Introduced in C# 9.0, this feature allows properties to be set during object initialization but becomes read-only after object creation. This design pattern is particularly suitable for creating immutable objects.

public class Program
{
public static void Main()
{
var user = new User
{
Username = "Player",
Email = "player@example.com"
};
// user.Username = "Bob"; // This line will cause a compilation error because the Name property is read-only
Console.WriteLine($"User's Name is: {user.Username}");
Console.WriteLine($"User's Email is: {user.Email}");
}
}

public class User
{
public string Username { get; init; }
public string Email { get; init; }
}

Constructors​

Constructors are special methods called when creating objects to initialize their state.

Default Constructor

public class Person
{
public string Name;

public Person()
{
Name = "Player";
}
}

Parameterized Constructor

public class Book
{
public string Title;
public string Author;

public Book(string title, string author)
{
Title = title;
Author = author;
}
}

Destructors​

Destructors are used to perform cleanup operations when an object's life ends. In game development, destructors help manage resources and memory to ensure game efficiency and stability.

Destructors have the following characteristics:

  • Start with a tilde (~) followed by the class name
  • Cannot have access modifiers
  • Cannot have parameters
  • Cannot have return types
  • Each class can have only one destructor
tip

The timing of destructor calls is uncertain as it depends on when the garbage collector runs.

public class Person
{
public Person()
{
Console.WriteLine("Constructor called");
}

~Person()
{
Console.WriteLine("Destructor called");
}
}

Object Instantiation​

Creating objects or instances of a class.

// Base class
public class Player
{
// Properties
public string Name { get; set; }
public int Level { get; set; }

// Parameterless constructor
public Player()
{
Name = "Unnamed Player";
Level = 1;
}

// Constructor with parameters
public Player(string name)
{
Name = name;
Level = 1;
}

// Constructor with multiple parameters
public Player(string name, int level)
{
Name = name;
Level = level;
}
}

// Complex object example
public class GameCharacter
{
// Properties
public string Name { get; set; }
public int Health { get; set; }
public List<string> Skills { get; set; }

// Constructor
public GameCharacter()
{
Skills = new List<string>();
}
}

public class Program
{
public static void Main()
{
// 1. The most basic instantiation method
Player player1 = new Player();

// 2. Using constructors
Player player2 = new Player("Hero");
Player player3 = new Player("Mage", 10);

// 3. Object initializer
Player player4 = new Player
{
Name = "Archer",
Level = 5
};

// 4. var keyword
var player5 = new Player("Assassin", 8);

// 5. Explicit type instantiation
Player player6 = new("Knight", 12);

// 6. Collection initialization
var characters = new List<Player>
{
new Player("Warrior"),
new Player("Mage", 5),
new Player { Name = "Shooter", Level = 3 }
};

// 7. Complex object initialization
var advancedCharacter = new GameCharacter
{
Name = "Ultimate Hero",
Health = 100,
Skills = new List<string> { "Fireball", "Heal" }
};

// 8. Using factory method
Player specialPlayer = CreateSpecialPlayer();

// 9. Nullable type
Player? optionalPlayer = null;
}

// Factory method example
static Player CreateSpecialPlayer()
{
return new Player("Special Character", 20);
}
}

// Static constructor example
public class GameConfig
{
// Static field
public static int MaxLevel { get; private set; }

// Static constructor: will only be called once
static GameConfig()
{
MaxLevel = 100;
}
}

// Private constructor (Singleton pattern)
public class GameManager
{
// Private static instance
private static GameManager _instance;

// Private constructor
private GameManager() { }

// Public static method to get the instance
public static GameManager GetInstance()
{
if (_instance == null)
{
_instance = new GameManager();
}
return _instance;
}
}

3. Inheritance and Derivation 📝​

Base Class and Derived Class​

Base class (parent class) defines basic properties and behaviors, while derived class (child class) inherits these characteristics and can add its own features. A class can only inherit from one base class.

In C#, the colon (:) is used to indicate inheritance relationship.

// Base class
public class Animal
{
public string Name { get; set; }
}

// Derived class
public class Dog : Animal
{
public string Breed { get; set; }
}

base Keyword​

The base keyword allows you to call members from the base class when in a derived class. It is most commonly used in derived classes to call the constructor of the base class or to access other base class members that were overridden in the derived class.

public class Animal
{
public virtual void Eat()
{
Console.WriteLine("Animal is eating");
}
}

public class Dog : Animal
{
public override void Eat()
{
// Call base class method
base.Eat();
Console.WriteLine("Dog is eating bones");
}
}

this Keyword​

The this keyword points to the present instance of the class. It is often used to point to the fields or methods of the current object, especially when method parameter names overlap with class field names.

  1. Referencing Instance Members
public class Player
{
private string name;

public Player(string name)
{
// Use the this keyword to reference the member variable name
this.name = name;
}

public void DisplayInfo()
{
Console.WriteLine($"Player name: {this.name}");
}
}
  1. Calling Other Constructors
public class Character
{
private string name;
private int level;

// Constructor 1
public Character(string name) : this(name, 1) // Call Constructor 2
{

}

// Constructor 2
public Character(string name, int level)
{
this.name = name;
this.level = level;
}

public void DisplayInfo()
{
Console.WriteLine($"Name: {this.name}, Level: {this.level}");
}
}

Method Overriding​

  • Base class methods must be marked with virtual, abstract, or override keywords
  • Derived class methods must use the override keyword
  • Overridden methods must have the same return type, name, and parameter list as the base class method

In the base class, use the virtual keyword to declare methods that can be overridden:

public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Some sound");
}
}

In derived classes, use the override keyword to override the virtual method of the base class:

public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Woof!");
}
}

Abstract methods must be implemented by derived classes:

public abstract class Shape
{
public abstract double CalculateArea();

public virtual void Display()
{
Console.WriteLine("This is a shape");
}
}

public class Circle : Shape
{
private double radius;

public override double CalculateArea()
{
return Math.PI * radius * radius;
}
}

Use the sealed keyword to prevent further overriding:

public class Dog : Animal
{
public sealed override void MakeSound()
{
Console.WriteLine("Woof!");
}
}

4. Interfaces and Abstract Classes 📝​

Interfaces and abstract classes are crucial tools for designing flexible and maintainable C# code.

Interfaces​

C# doesn't support multiple inheritance for classes. However, a class can implement multiple interfaces.

Interfaces contain only method declarations, not implementations.

tip

Interfaces: Define behavior

Abstract Classes​

Abstract classes can contain methods with and without implementation.

They cannot be instantiated directly.

A class can implement multiple interfaces but can inherit only one abstract class.

tip

Abstract Classes: Share base implementation

// Interface definition
public interface IGameCharacter
{
string Name { get; set; }

void Attack();
void Defend();

void DisplayInfo()
{
Console.WriteLine($"Character Name: {Name}");
}
}

// Abstract class definition
public abstract class BaseCharacter
{
public string Name { get; set; }
public int Health { get; set; }

public BaseCharacter(string name)
{
Name = name;
Health = 100;
}

// Abstract method (must be implemented by subclasses)
public abstract void SpecialAbility();

// Concrete method
public virtual void TakeDamage(int amount)
{
Health -= amount;
Console.WriteLine($"{Name} took {amount} damage");
}
}

// Concrete class implementing the interface
public class Warrior : BaseCharacter, IGameCharacter
{
public string Weapon { get; set; }

public Warrior(string name, string weapon) : base(name)
{
Weapon = weapon;
}

// Interface method implementation
public void Attack()
{
Console.WriteLine($"{Name} attacks with {Weapon}!");
}

public void Defend()
{
Console.WriteLine($"{Name} raises a shield to defend!");
}

// Abstract method implementation
public override void SpecialAbility()
{
Console.WriteLine($"{Name} unleashes a berserker slash!");
}
}

public class Mage : BaseCharacter, IGameCharacter
{
public int ManaPoints { get; set; }

public Mage(string name) : base(name)
{
ManaPoints = 100;
}

// Interface method implementation
public void Attack()
{
Console.WriteLine($"{Name} casts a magical attack!");
ManaPoints -= 10;
}

public void Defend()
{
Console.WriteLine($"{Name} uses a magical barrier!");
}

// Abstract method implementation
public override void SpecialAbility()
{
Console.WriteLine($"{Name} unleashes a wide-area spell!");
}
}

// Multiple interface implementation
public interface IDamageable
{
void ReceiveDamage(int damage);
}

public interface IHealable
{
void Heal(int amount);
}

public class ComplexCharacter : IGameCharacter, IDamageable, IHealable
{
public string Name { get; set; }
public int Health { get; set; }

public void Attack() { }
public void Defend() { }

public void ReceiveDamage(int damage)
{
Health -= damage;
}

public void Heal(int amount)
{
Health += amount;
}
}

public class Program
{
public static void Main()
{
IGameCharacter warrior = new Warrior("Altaire", "Great Sword");
IGameCharacter mage = new Mage("Michael");

// Interface method calls
warrior.Attack();
mage.Attack();

// Abstract class method
BaseCharacter baseWarrior = new Warrior("Hero", "Long Sword");
baseWarrior.TakeDamage(20);
baseWarrior.SpecialAbility();

// Interface default implementation
warrior.DisplayInfo();
}
}

// Generic interface example
public interface IRepository<T>
{
void Add(T item);
T GetById(int id);
void Remove(T item);
}

public class CharacterRepository : IRepository<Warrior>
{
public void Add(Warrior item) { }
public Warrior GetById(int id) { return null; }
public void Remove(Warrior item) { }
}

5. Relationships Between Classes 📝​

From weak to strong relationship order: Association < Aggregation < Composition

Composition​

Composition is a strong "whole-part" dependency relationship where parts cannot exist without the whole.

public class Weapon
{
public string Name { get; set; }
public int Damage { get; set; }

public void Attack()
{
Console.WriteLine($"{Name} attacks, dealing {Damage} damage");
}
}

public class Armor
{
public string Name { get; set; }
public int Defense { get; set; }

public void Protect()
{
Console.WriteLine($"{Name} provides {Defense} defense");
}
}

public class GameCharacter
{
// Composition: The character fully owns the weapon and armor
private Weapon _weapon;
private Armor _armor;

public GameCharacter(string weaponName, string armorName)
{
_weapon = new Weapon { Name = weaponName, Damage = 50 };
_armor = new Armor { Name = armorName, Defense = 30 };
}

public void Battle()
{
_weapon.Attack();
_armor.Protect();
Console.WriteLine("In battle...");
}
}

Aggregation​

Aggregation represents a "whole-part" relationship, but parts can exist independently.

public interface IWeapon
{
string Name { get; }
int Damage { get; }
}

public class Sword : IWeapon
{
public string Name { get; private set; }
public int Damage { get; private set; }

public Sword(string name, int damage)
{
Name = name;
Damage = damage;
}
}

public class Hero
{
public string Name { get; private set; }
public int Health { get; private set; }

// Aggregation: Can change weapons
private IWeapon _weapon;

public Hero(string name)
{
Name = name;
Health = 100;
}

// Equip weapon (aggregation relationship)
public void EquipWeapon(IWeapon weapon)
{
_weapon = weapon;
Console.WriteLine($"{Name} equipped {weapon.Name}");
}

public void Attack(Hero target)
{
if (_weapon != null)
{
int damage = _weapon.Damage;
target.TakeDamage(damage);
Console.WriteLine($"{Name} attacks {target.Name} with {_weapon.Name}");
}
else
{
Console.WriteLine($"{Name} has no weapon and cannot attack");
}
}

public void TakeDamage(int damage)
{
Health -= damage;
Console.WriteLine($"{Name} takes {damage} damage, remaining health {Health}");
}
}

public class Program
{
public static void Main()
{
Hero hero = new Hero("Hero");
Hero enemy = new Hero("Goblin");

IWeapon sword = new Sword("Hero's Sword", 20);
IWeapon axe = new Sword("Giant Axe", 25);

hero.EquipWeapon(sword);
enemy.EquipWeapon(axe);

hero.Attack(enemy);
enemy.Attack(hero);
}
}

Association​

Association is the most common reference relationship between objects, where objects are mutually independent.

public class Player
{
public string Name { get; set; }

// Association: A player can have multiple characters
public List<Character> Characters { get; set; }

// Association: A player can join multiple guilds
public List<Guild> Guilds { get; set; }

public Player(string name)
{
Name = name;
Characters = new List<Character>();
Guilds = new List<Guild>();
}

public void AddCharacter(Character character)
{
Characters.Add(character);
}

public void JoinGuild(Guild guild)
{
Guilds.Add(guild);
guild.AddMember(this);
}
}

public class Character
{
public string Name { get; set; }
public int Level { get; set; }

// Association: A character belongs to a specific player
public Player Owner { get; set; }

// Association: A character can equip items
public List<Item> Equipment { get; set; }

public Character(string name, Player owner)
{
Name = name;
Owner = owner;
Equipment = new List<Item>();
}

public void EquipItem(Item item)
{
Equipment.Add(item);
}
}

public class Item
{
public string Name { get; set; }
public int Attack { get; set; }
public int Defense { get; set; }

// Association: An item can be owned by multiple characters
public List<Character> Owners { get; set; }

public Item(string name, int attack, int defense)
{
Name = name;
Attack = attack;
Defense = defense;
Owners = new List<Character>();
}
}

public class Guild
{
public string Name { get; set; }

// Association: A guild has multiple members
public List<Player> Members { get; set; }

public Guild(string name)
{
Name = name;
Members = new List<Player>();
}

public void AddMember(Player player)
{
Members.Add(player);
}
}

public class Program
{
public static void Main()
{
Player player = new Player("Hero");
Character warrior = new Character("Warrior", player);
Character mage = new Character("Mage", player);

player.AddCharacter(warrior);
player.AddCharacter(mage);

Item sword = new Item("Flame Dragon Sword", 50, 10);
Item shield = new Item("Iron Shield", 5, 30);

warrior.EquipItem(sword);
warrior.EquipItem(shield);

Guild heroGuild = new Guild("Hero League");

player.JoinGuild(heroGuild);

// Display associations
Console.WriteLine($"Player {player.Name} has {player.Characters.Count} characters");
Console.WriteLine($"Character {warrior.Name} has {warrior.Equipment.Count} pieces of equipment");
Console.WriteLine($"Guild {heroGuild.Name} has {heroGuild.Members.Count} members");
}
}

6. Advanced Class Features 📝​

Static Members​

The following all need to be declared using the static keyword.

Static Fields

Can be used without creating an instance of the class, accessed directly through the class name ClassName.StaticFieldName.

  • Stored in the data segment, memory is allocated when the program starts
  • Shared by all instances of the class, all instances access the same memory location
  • Lifetime same as the application, from program start to program end
Static Methods

Static methods belong to the class itself rather than instances of the class.

  • Can be called without creating an instance of the class
  • Can only directly access other static members of the class
  • Cannot use the this keyword
Static Constructors

Static constructors are used to initialize static members of a class or to perform actions that should occur only once for the class, not for each individual object.

  • Each class can have only one static constructor
  • Cannot have parameters
  • Cannot have access modifiers
  • Automatically called, cannot be manually called
  • Executes before the class is first used
Static Classes

A static class is a special type of class that contains only static members and cannot be instantiated.

  • Implicitly sealed, meaning they cannot be inherited
  • Cannot contain instance constructors
  • Cannot be inherited
Static Properties

Static properties belong to the class rather than instances.

  • Accessed directly through the class name, no instance creation needed
  • Only one shared copy exists throughout the program runtime, shared by all objects
  • Can be read-only, write-only, or read-write
public class GameSettings
{
// Static fields: Global game settings
public static int MaxLevel = 100;
public static int StartingGold = 1000;

// Static read-only field: Unmodifiable constant
public static readonly string GameVersion = "1.0.0";
}

public class Player
{
// Instance fields
public string Name { get; }
public int Gold { get; }

// Static field: Record total number of players
private static int _totalPlayers;

// Static property: Get current total number of players
public static int TotalPlayers => _totalPlayers;

public Player(string name)
{
Name = name;
Gold = GameSettings.StartingGold;

// Increment total players count for each new player created
_totalPlayers++;
}

// Static method: Reset player count
public static void ResetPlayerCount()
{
_totalPlayers = 0;
}
}

public class MonsterManager
{
// Static field: Global monster kill counter
private static int _totalMonstersKilled;

// Static method: Record monster kill
public static void RecordMonsterKill()
{
_totalMonstersKilled++;
}

// Static method: Get total kills
public static int GetTotalMonstersKilled()
{
return _totalMonstersKilled;
}
}

public class Program
{
public static void Main()
{
// Using static fields and methods
Console.WriteLine($"Game Version: {GameSettings.GameVersion}");
Console.WriteLine($"Max Level: {GameSettings.MaxLevel}");

// Create players
Player player = new Player("Hero");
if (player == null) throw new ArgumentNullException(nameof(player));

// Using static property
Console.WriteLine($"Current Total Players: {Player.TotalPlayers}");

// Simulate battles
MonsterManager.RecordMonsterKill();
MonsterManager.RecordMonsterKill();
MonsterManager.RecordMonsterKill();

Console.WriteLine($"Total Monsters Killed: {MonsterManager.GetTotalMonstersKilled()}");

// Demonstrate the characteristics of static fields
Console.WriteLine($"{player.Name}'s Initial Gold: {player.Gold}");
}
}

Attributes​

Attributes are used for adding metadata to program elements such as classes, methods, and properties, which can alter their behavior during runtime.

Predefined Attributes
  • Obsolete — Mark code as deprecated
  • Serializable — Mark classes as serializable
  • Conditional — Used for conditional compilation
  • AttributeUsage — Describes how attributes can be used
Custom Attributes
  • Must inherit from the Attribute class
  • Class names typically end with Attribute
  • Can contain constructors and properties
using System.Reflection;

// Custom Attribute: Character Description
[AttributeUsage(AttributeTargets.Class)]
public class CharacterDescriptionAttribute(string description) : Attribute
{
public string Description { get; } = description;
}

// Attribute Validation
[AttributeUsage(AttributeTargets.Property)]
public class ValidRangeAttribute(int min, int max) : Attribute
{
public int Min { get; } = min;
public int Max { get; } = max;

public bool IsValid(int value)
{
return value >= Min && value <= Max;
}
}

// Character class using attributes
[CharacterDescription("Brave Adventurer")]
public class GameCharacter(string name, int level, int health)
{
public string Name { get; set; } = name;

[ValidRange(1, 100)]
public int Level { get; set; } = level;

[ValidRange(10, 500)]
public int Health { get; set; } = health;

// Method to validate properties using attributes
public bool Validate()
{
var properties = GetType().GetProperties();

foreach (var prop in properties)
{
if (prop.GetCustomAttributes(typeof(ValidRangeAttribute), false)
.FirstOrDefault() is ValidRangeAttribute validRangeAttr)
{
var value = (int)prop.GetValue(this);
if (!validRangeAttr.IsValid(value))
{
Console.WriteLine($"Property {prop.Name} validation failed: value must be between {validRangeAttr.Min} and {validRangeAttr.Max}");
return false;
}
}
}
return true;
}

// Get the description of the class
public string GetDescription()
{
var attribute = GetType().GetCustomAttribute<CharacterDescriptionAttribute>();
return attribute?.Description ?? "No description";
}
}

public class Program
{
public static void Main()
{
// Create character
GameCharacter hero = new GameCharacter("Hero", 50, 200);

// Validate character properties
if (hero.Validate())
{
Console.WriteLine("Character property validation passed");
}

// Get character description
Console.WriteLine($"Character description: {hero.GetDescription()}");

// Test invalid properties
GameCharacter invalidHero = new GameCharacter("Invalid Character", 150, 600);
if (!invalidHero.Validate())
{
Console.WriteLine("Character property validation failed");
}
}
}

Reflection​

Reflection allows inspection, modification, and creation of types, methods, and properties at runtime.

using System.Reflection;

// Base class for characters
public abstract class Character
{
public string Name { get; set; }
public int Health { get; set; }

public abstract void Attack();
}

// Warrior class
public class Warrior : Character
{
public int Strength { get; set; }

public Warrior(string name, int health, int strength)
{
Name = name;
Health = health;
Strength = strength;
}

public override void Attack()
{
Console.WriteLine($"{Name} attacks with a sword!");
}

public void SpecialSkill()
{
Console.WriteLine($"{Name} uses Fury Slash!");
}
}

// Mage class
public class Mage : Character
{
public int Mana { get; set; }

public Mage(string name, int health, int mana)
{
Name = name;
Health = health;
Mana = mana;
}

public override void Attack()
{
Console.WriteLine($"{Name} casts a magic attack!");
}

public void CastSpell()
{
Console.WriteLine($"{Name} casts a high-level spell!");
}
}

// Reflection helper class
public abstract class ReflectionHelper
{
// Create an object
public static object? CreateInstance(string typeName)
{
Type? type = Type.GetType(typeName);
return Activator.CreateInstance(type, "Default Character", 100, 50);
}

// Get all public methods
public static void GetMethods(object? obj)
{
Type? type = obj?.GetType();
Console.WriteLine($"All public methods of type {type?.Name}:");

MethodInfo[]? methods = type?.GetMethods(
BindingFlags.Public |
BindingFlags.Instance |
BindingFlags.DeclaredOnly
);

if (methods != null)
foreach (var method in methods)
{
Console.WriteLine(method.Name);
}
}

// Invoke a method
public static void InvokeMethod(object? obj, string methodName)
{
Type? type = obj?.GetType();
MethodInfo? method = type?.GetMethod(methodName);

if (method != null)
{
method.Invoke(obj, null);
}
else
{
Console.WriteLine($"Method {methodName} not found");
}
}

// Display property values
public static void DisplayProperties(object? obj)
{
Type? type = obj?.GetType();
PropertyInfo[]? properties = type?.GetProperties();

Console.WriteLine($"Properties of type {type?.Name}:");
if (properties != null)
foreach (var prop in properties)
{
object? value = prop.GetValue(obj);
Console.WriteLine($"{prop.Name}: {value}");
}
}
}

public class Program
{
public static void Main()
{
// Create an object using reflection
Warrior warrior = (Warrior)ReflectionHelper.CreateInstance("Warrior")!;

// Display object properties
ReflectionHelper.DisplayProperties(warrior);

// Get object methods
ReflectionHelper.GetMethods(warrior);

// Invoke specific methods
ReflectionHelper.InvokeMethod(warrior, "Attack");
ReflectionHelper.InvokeMethod(warrior, "SpecialSkill");

// Dynamically create and use an object
object? dynamicMage = ReflectionHelper.CreateInstance("Mage");
ReflectionHelper.InvokeMethod(dynamicMage, "Attack");
}
}

III. Exception Handling 🚨​

Exception handling allows for detecting and handling errors that occur during program execution, preventing crashes and unforeseen outcomes.

1. Basic Exception Handling 📝​

In C#, the primary error handling mechanism is based on the use of try, catch, finally, and throw constructs.

The try-catch block is used to handle exceptions. Code that might throw an exception is placed inside the try block, and the corresponding exception-handling code is placed inside the catch block.

The finally block ensures that the code inside it gets executed regardless of whether an exception was thrown in the preceding try or catch blocks. This is particularly useful for cleanup operations, such as closing files or database connections.

In most cases, the finally block will execute. However, there are rare circumstances, such as program termination or catastrophic exceptions (for example, StackOverflowException or a process termination), where the finally block might not get executed because these critical errors can disrupt the normal flow of program execution, and the app will stop, leaving no opportunity for the finally block to run.

Scenarios Where ​finally​ Might Not Execute:

  1. If the application crashes or is forcibly terminated
  2. If there's an infinite loop in the try or catch block
  3. If Environment.FailFast() or Process.Kill() is called
  4. In extreme system-level failures or hardware issues
try
{
// Code that may throw an exception
int result = 10 / int.Parse("0");
}
catch (DivideByZeroException ex)
{
// Handling specific exception
Console.WriteLine($"Error: {ex.Message}");
}
catch (Exception ex)
{
// Handling any other exception
Console.WriteLine($"An unexpected error occurred: {ex.Message}");
}
finally
{
// Code that will be executed regardless of whether an exception occurred
Console.WriteLine("This code always executes, even if an exception occurred.");
}

You can manually throw exceptions using the throw keyword. This is useful when you want to indicate that a certain condition or error has occurred.

public class Calculator
{
public int Divide(int dividend, int divisor)
{
if (divisor == 0)
{
throw new DivideByZeroException("Cannot divide by zero.");
}
return dividend / divisor;
}
}

2. Common Exception Types 🔍​

C# features a wide variety of exception types to cater to different exceptional scenarios.

  • ArgumentNullException: This is thrown when an argument passed to a method is null when a non-null value is expected
  • ArgumentOutOfRangeException: This occurs when an argument's value is outside the permissible range
  • DivideByZeroException: This is thrown when there's an attempt to divide by zero
  • InvalidOperationException: This arises when the state of an object doesn't permit a particular operation
  • FileNotFoundException: This occurs when a file that's being attempted to be accessed doesn't exist
  • StackOverflowException: This is thrown when there's a stack overflow due to excessive recursion or other reasons
  • NullReferenceException: This occurs when you try to access a member on an object reference that is null

3. Custom Exceptions 📝​

In C#, exceptions are implemented as objects that inherit from the base Exception class. This allows for passing additional information about the exception and creating custom exception types. To create your own exception class, simply inherit it from the Exception class or one of its subclasses.

You can create your own custom exception classes by deriving from the Exception class. This allows you to define specific exception types for your application.

public class CustomException : Exception
{
public CustomException(string message) : base(message)
{

}
}
// Custom exception class
public class BusinessException : Exception
{
public string ErrorCode { get; }

public BusinessException(string message) : base(message)
{
}

public BusinessException(string message, string errorCode)
: base(message)
{
ErrorCode = errorCode;
}

public BusinessException(string message, Exception innerException)
: base(message, innerException)
{
}
}

// Using the custom exception
public class BusinessLogic
{
public void ProcessOrder(Order order)
{
if (order == null)
{
throw new BusinessException("Order cannot be null", "ORDER001");
}

if (order.Amount <= 0)
{
throw new BusinessException("Order amount must be greater than 0", "ORDER002");
}
}
}

4. Exception Filters 🔀​

Exception filters allow you to catch exceptions based on a specific condition.

try
{
// Code that may throw an exception
int result = 10 / int.Parse("0");
}
catch (DivideByZeroException ex) when (ex.Message == "Attempted to divide by zero.")
{
// Handling specific exception with a filter
Console.WriteLine($"Error: {ex.Message}");
}

IV. Collections and Generics 📈​

1. Common Collections 🔗​

Arrays and collections in C# are used for storing data and allow data to be organized in a manner that facilitates easy access and manipulation:

  • Arrays are static collections capable of storing a fixed number of elements of a single type.
  • Collections are dynamic and can store a variable number of elements; they come in different types, such as lists, dictionaries, stacks, queues, and so on.

.NET provides several primary collection types:

  • List<T>: A dynamic array of elements. It maintains order and allows duplicate elements.
  • Dictionary<TKey, TValue>: A collection of key-value pairs. It does not have a defined order, and keys must be unique.
  • HashSet<T>: A set of unique elements. It does not maintain any specific order.
  • Queue<T>: A collection supporting First-In-First-Out (FIFO) operations.
  • Stack<T>: A collection supporting Last-In-First-Out (LIFO) operations.

Array​

Arrays in C# are fixed-size collections of elements of the same data type.

// Declaration and Initialization
int[] numbers = new int[5] { 1, 2, 3, 4, 5 };

// Accessing Elements
int firstElement = numbers[0]; // Accessing the first element (index 0)

// Iterating Through Arrays
foreach (int number in numbers)
{
Console.WriteLine(number);
}

// Multidimensional Arrays
int[,] matrix = new int[3, 3]
{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};

List​

Lists offer dynamic sizing and additional features.

// Declaration and Initialization
List<string> names = new List<string>() { "Alice", "Bob", "Charlie" };

// Adding and Removing Elements
names.Add("David"); // Add an element
names.Remove("Bob"); // Remove an element by value
names.RemoveAt(0); // Remove an element by index

// Accessing Elements
string firstElement = names[0]; // Accessing the first element (index 0)

// Iterating Through Lists
foreach (string name in names)
{
Console.WriteLine(name);
}
info

Arrays are typically faster for indexed access and more memory-efficient, making them ideal when the number of items is known and constant. List<T> provides more manipulation methods and is better suited for dynamic collections where size is uncertain.

Dictionary​

Dictionaries in C# are collections that store key-value pairs, providing fast access to values based on their associated keys.

// Declaration and Initialization
Dictionary<string, int> ages = new Dictionary<string, int>()
{
{"Alice", 25},
{"Bob", 30},
{"Charlie", 22}
};

// Adding and Accessing Elements
ages["David"] = 28; // Add a new key-value pair
int bobAge = ages["Bob"]; // Access the value using the key

// Iterating Through Dictionaries
foreach (var pair in ages)
{
Console.WriteLine($"{pair.Key}: {pair.Value}");
}

// Checking for Key Existence
bool hasAlice = ages.ContainsKey("Alice"); // true
bool hasEve = ages.ContainsKey("Eve"); // false

HashSet​

HashSets in C# are collections that store unique elements without any specific order.

// Declaration and Initialization
HashSet<int> uniqueNumbers = new HashSet<int>() { 1, 2, 3, 4, 5 };

// Adding and Removing Elements
uniqueNumbers.Add(6); // Add a new element
uniqueNumbers.Remove(3); // Remove an element

// Checking for Element Existence
bool hasThree = uniqueNumbers.Contains(3); // false
bool hasFive = uniqueNumbers.Contains(5); // true

// Set Operations
HashSet<int> otherNumbers = new HashSet<int>() { 4, 5, 6, 7, 8 };

// Union
HashSet<int> unionSet = new HashSet<int>(uniqueNumbers);
unionSet.UnionWith(otherNumbers); // {1, 2, 3, 4, 5, 6, 7, 8}

// Intersection
HashSet<int> intersectionSet = new HashSet<int>(uniqueNumbers);
intersectionSet.IntersectWith(otherNumbers); // {4, 5}

// Difference
HashSet<int> differenceSet = new HashSet<int>(uniqueNumbers);
differenceSet.ExceptWith(otherNumbers); // {1, 2, 3}
tip

It's ideal when you need to prevent duplicates or perform frequent lookup operations.

Queue 和 Stack​

// Queue (First In First Out - FIFO)
Queue<string> queue = new Queue<string>();

// Enqueue
queue.Enqueue("First");
queue.Enqueue("Second");

// Dequeue
string item = queue.Dequeue();

// Peek at the front element without removing it
string peek = queue.Peek();

// Stack (Last In First Out - LIFO)
Stack<string> stack = new Stack<string>();

// Push onto the stack
stack.Push("First");
stack.Push("Second");

// Pop from the stack
string item = stack.Pop();

// Peek at the top element without removing it
string peek = stack.Peek();

2. Generic Programming 🔀​

At the heart of efficient and robust programming lies the ability to write code that stands the test of time, adapts to diverse scenarios, and minimizes redundancy.

Rather than committing to a specific data type, generics allow for a more abstract and versatile coding style, ensuring that you can cater to a wide array of requirements without the burden of excessive code repetition.

Generics in C# enable the creation of classes, interfaces, and methods that can operate with different data types without losing type safety and performance. They play a key role in creating versatile and flexible collections, services, and other components that can work with any data type.

Compared to using the object type, generics offer the following advantages:

  • Type safety: Generics ensure that you are working with the correct data type, eliminating the risk of runtime type errors.
  • Performance: With generics, there’s no need for boxing or unboxing when dealing with value types, leading to more efficient operations.
  • Code reusability: Generics allow you to write a piece of code that works with different data types, reducing code duplication.
  • Elimination of type casting: With generics, explicit type casting is reduced, making the code cleaner and more readable.

In .NET, generic types are compiled into a single template in Intermediate Language (IL).

When a specific type instance is required at runtime, the Just-In-Time (JIT) compiler generates the specialized code.

For value types (for example, int, double), separate code is generated for each type to ensure optimized performance.

However, for reference types, the same code is shared, making the process more memory-efficient.

Generics can be combined with various features in C#, such as the following:

  • Delegates: You can define generic delegates, which can point to methods of various types.
  • Events: Events can be based on generic delegates.
  • Attributes: While you can’t create a generic attribute class, you can apply attributes to generic constructs.

Generic Classes​

A generic type extension method allows developers to add methods to existing types (both built-in and user-defined) without modifying them or creating new derived types.

// Basic Generic Class
public class GenericContainer<T>
{
private T _item;

public GenericContainer(T item)
{
_item = item;
}

public T GetItem()
{
return _item;
}

public void SetItem(T item)
{
_item = item;
}
}

// Multiple Type Parameters
public class KeyValuePair<TKey, TValue>
{
public TKey Key { get; set; }
public TValue Value { get; set; }

public KeyValuePair(TKey key, TValue value)
{
Key = key;
Value = value;
}
}

// Usage Example
public class GenericExample
{
public void UseGenericTypes()
{
var intContainer = new GenericContainer<int>(42);
var stringContainer = new GenericContainer<string>("Hello");
var pair = new KeyValuePair<int, string>(1, "One");
}
}

Generic Method​

Generic methods are a special type of method in C# that allow the parameter types to be determined at the time of use.

public class GenericMethods
{
// Basic Generic Method
public T GenericMethod<T>(T item)
{
Console.WriteLine($"Type: {typeof(T)}, Value: {item}");
return item;
}

// Generic Method with Multiple Type Parameters
public TResult Convert<TInput, TResult>(TInput input)
where TResult : new()
{
TResult result = new TResult();
// Conversion logic
return result;
}

// Generic Method in a Non-Generic Class
public static void Swap<T>(ref T first, ref T second)
{
T temp = first;
first = second;
second = temp;
}
}

Constraints on Generics​

Constraints can be applied to generics to restrict the types that can be used as arguments.

// Class Constraint
public class ClassConstraint<T> where T : class
{
public T Instance { get; set; }
}

// Value Type Constraint
public class ValueTypeConstraint<T> where T : struct
{
public T Value { get; set; }
}

// Constructor Constraint
public class NewConstraint<T> where T : new()
{
public T CreateNew()
{
return new T();
}
}

// Interface Constraint
public class InterfaceConstraint<T> where T : IComparable<T>
{
public bool IsGreaterThan(T first, T second)
{
return first.CompareTo(second) > 0;
}
}

// Multiple Constraints
public class MultipleConstraints<T>
where T : class, IDisposable, new()
{
public void ProcessItem(T item)
{
// Processing logic
item.Dispose();
}
}
// Base Class
public class Animal
{
public virtual void MakeSound() { }
}

// Generic Class with Base Class Constraint
public class AnimalContainer<T> where T : Animal
{
private T _animal;

public AnimalContainer(T animal)
{
_animal = animal;
}

public void MakeAnimalSound()
{
_animal.MakeSound();
}
}

// Usage Example
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Woof!");
}
}

public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Meow!");
}
}

Generic Interface​

// Generic Interface Definition
public interface IRepository<T>
{
T GetById(int id);
void Add(T item);
void Update(T item);
void Delete(int id);
IEnumerable<T> GetAll();
}

// Implementing the Generic Interface
public class Repository<T> : IRepository<T> where T : class
{
private readonly List<T> _items = new List<T>();

public T GetById(int id)
{
// Implement retrieval logic
return _items.FirstOrDefault();
}

public void Add(T item)
{
_items.Add(item);
}

public void Update(T item)
{
// Implement update logic
}

public void Delete(int id)
{
// Implement deletion logic
}

public IEnumerable<T> GetAll()
{
return _items;
}
}

Generics in C# support covariance and contravariance, enabling more flexible relationships between types.

// Covariant Interface (out)
public interface IProducer<out T>
{
T Produce();
}

// Contravariant Interface (in)
public interface IConsumer<in T>
{
void Consume(T item);
}

// Implementation Example
public class Producer<T> : IProducer<T> where T : new()
{
public T Produce()
{
return new T();
}
}

public class Consumer<T> : IConsumer<T>
{
public void Consume(T item)
{
Console.WriteLine($"Consuming {item}");
}
}

// Using Covariance and Contravariance
public class VarianceExample
{
public void DemonstrateVariance()
{
IProducer<string> stringProducer = new Producer<string>();
IProducer<object> objectProducer = stringProducer; // Covariance

IConsumer<object> objectConsumer = new Consumer<object>();
IConsumer<string> stringConsumer = objectConsumer; // Contravariance
}
}

Generics in C# enhance code reusability and maintainability by providing a mechanism for creating flexible and type-safe components. Whether working with generic classes, methods, or interfaces, understanding generics is crucial for building efficient and adaptable software solutions in C#.

V. Language Integrated Query (LINQ) 📚​

LINQ enables the use of query expressions to interact with data, irrespective of its source. It facilitates easy filtering, sorting, grouping, and transformation of data, providing a seamless and integrated way to query objects, databases, and XML documents.

Understanding LINQ is essential as it provides a uniform and model-independent querying capability, streamlining data manipulation and retrieval processes and offering enhanced readability and maintainability.

In C#, LINQ is a set of extensions that enable queries to be performed on various data sources directly from the programming language. LINQ can be used to work with collections, XML, databases, and more.

1. Basic 📝​

LINQ Query Syntax​

var query = from item in collection
where item.Price > 100
orderby item.Name
select item;

Method Syntax​

var query = collection
.Where(x => x.Price > 100)
.OrderBy(x => x.Name)
.Select(x => x);

2. Common LINQ operators 📊​

  • Where: This method is used for filtering collections based on a given predicate. It returns a new collection that includes only those elements that satisfy a specified condition.
  • Select: This method is used for projecting or transforming the elements of a collection. It returns a new collection with elements that have been transformed based on a specified function or projection.

3. Aggregation operation 🔗​

int count = numbers.Count();
int sum = numbers.Sum();
double average = numbers.Average();
int max = numbers.Max();
int min = numbers.Min();

4. Grouping and Joining 📈​

var groups = products.GroupBy(p => p.Category);

var joined = customers.Join(orders,
c => c.Id,
o => o.CustomerId,
(c, o) => new { Customer = c, Order = o });

5. Deferred execution and Actual execution​

Deferred execution in LINQ means that actual data processing or computation does not occur until the results are enumerated. When you construct a LINQ query, it just creates a query definition.

var query = numbers.Where(n => n > 3); // Query not executed yet
foreach (var num in query)
{
Console.WriteLine(num); // Query executed here
}

The actual execution is delayed until you iterate over the query result, such as by using a foreach loop or converting the results with methods such as ToList() or ToArray(). This can enhance performance by avoiding unnecessary computations.

var result = numbers.Where(n => n > 3).ToList(); // Query executed immediately

Calling the following methods executes the query immediately:

  • ​ToList()​
  • ​ToArray()​
  • ​Count()​

However, it’s important to manage the moment when the data is actually materialized – that is, fetched and loaded into memory. Materializing the data too early can sometimes consume more resources, especially when the data source is substantial, such as a database. You might want to append more conditions or filters to the query before deciding to materialize the results to optimize resource usage and performance.

6. All and Any 🔁​

Used to check elements in a collection.

  • All: Checks if every element in the collection satisfies a particular condition. Use All when you need to ensure that all elements of a collection meet a specific criterion.
  • Any: Checks if at least one element in the collection satisfies a particular condition. Use Any when you need to determine if there are any elements that fulfill a specific criterion.

When the collection is empty, the following happens:

  • All: Always returns true because there are no elements that would violate the condition. This might seem counter-intuitive, but in the absence of any elements to check, it defaults to true.
  • Any: Always returns false since there are no elements present to satisfy the condition.

For instance, if you want to verify that all numbers in a list are positive, you’d use All. If you’re going to check if there’s a negative number in the list, you’d use Any.

int[] numbers = { 1, 3, 5, 8 };
bool hasEvenNumber = numbers.Any(n => n % 2 == 0);
Console.WriteLine($"Is there an even number? {hasEvenNumber}");

7. Custom LINQ extension methods 📝​

IEnumerable and IQueryable are two primary interfaces representing collections in .NET. This is what they do:

IEnumerable: Operates at the object level in memory. When you execute LINQ queries against an IEnumerable interface, operations are performed in memory. It’s suitable for working with in-memory collections such as arrays or lists.

IQueryable: Designed for interacting with external data sources (for example, databases). Queries made with IQueryable get translated into queries specific to the data source (such as SQL for relational databases). This interface allows for deferred execution and out-of-memory (OOM) data querying, making it efficient for large datasets, especially in databases.

The main distinction between these two interfaces lies in the execution location: IEnumerable processes data in memory. Meanwhile, IQueryable allows the construction of an expression tree that can be translated into a query suitable for an external data source, such as SQL for databases. Then, it sends the parsed query for processing to the data source and fetches the results as IEnumerable.

public class BasicConcepts
{
// IEnumerable: In-memory collection operations
// IQueryable: Database query operations

public void CompareInterfaces()
{
// IEnumerable - executed in memory
IEnumerable<int> enumerable = Enumerable.Range(1, 100);
var enumResult = enumerable
.Where(x => x > 50) // Filter in memory
.Select(x => x * 2); // Transform in memory

// IQueryable - executed at the data source
IQueryable<int> queryable = Enumerable.Range(1, 100).AsQueryable();
var queryResult = queryable
.Where(x => x > 50) // Translated to SQL WHERE clause
.Select(x => x * 2); // Translated to SQL SELECT clause
}
}

8. Optimizing đŸ’ģ​

Optimizing LINQ queries, especially with substantial datasets, can be achieved through several approaches:

  • Utilize deferred execution whenever possible, ensuring that queries are only executed when the result is genuinely required. This avoids unnecessary computations.
  • Choose the most efficient collection type tailored for your specific use case, as the underlying data structure can impact performance.
  • Limit the size of the resulting dataset when feasible using methods such as Take to avoid processing more data than necessary.
  • Avoid or judiciously use nested queries. They can lead to performance issues due to multiple rounds of data retrieval or computations.
  • Use methods such as ToArray or ToList to materialize results into memory if you anticipate multiple operations on the data. This can prevent repeated execution of the same LINQ query.

VI. Lambda 👀​

Lambda expressions and anonymous functions are fundamental concepts in C# that allow functions to be declared and defined in place, often used as arguments for other functions.

They are notable for their ability to provide concise, expressive syntax for representing functionality, especially when used with higher-order functions and LINQ.

1. Lambda Expressions 🔍​

(parameters) => expression

public class LambdaBasics
{
public void BasicSyntax()
{
// 1. No-parameter Lambda
Action sayHello = () => Console.WriteLine("Hello");

// 2. Single-parameter Lambda
Func<int, int> square = x => x * x;

// 3. Multiple-parameter Lambda
Func<int, int, int> add = (x, y) => x + y;

// 4. Lambda with a statement block
Func<int, int, int> multiply = (x, y) =>
{
Console.WriteLine($"Multiplying {x} and {y}");
return x * y;
};

// 5. Type-declared Lambda
Func<double, double, double> divide = (double x, double y) => x / y;

// Usage examples
sayHello(); // Output: Hello
Console.WriteLine(square(5)); // Output: 25
Console.WriteLine(add(3, 4)); // Output: 7
}
}

2. Lambda Closure 🔗​

A closure in the context of lambda expressions and anonymous methods refers to the ability of these constructs to capture and retain access to variables from their enclosing scope.

The captured variables are stored in a way that they remain accessible and mutable even after the method in which they were declared has finished executing.

This can lead to unexpected behaviors if not understood correctly, especially in multithreaded environments, where closures can introduce shared state across threads.

public class LambdaClosure
{
public void ClosureExamples()
{
// Basic closure
int multiplier = 10;
Func<int, int> multiply = x => x * multiplier;

// Closure trap
var actions = new List<Action>();
for (int i = 0; i < 5; i++)
{
// Incorrect way - all actions will use the final value of i
actions.Add(() => Console.WriteLine(i));
}

// Correct way
for (int i = 0; i < 5; i++)
{
int temp = i;
actions.Add(() => Console.WriteLine(temp));
}

// Counter closure
Func<int> counter = CreateCounter();
Console.WriteLine(counter()); // 1
Console.WriteLine(counter()); // 2
}

private Func<int> CreateCounter()
{
int count = 0;
return () => ++count;
}
}

3. Lambda Expression Tree đŸŒŗâ€‹

Lambda expression trees are a way to represent code as a tree data structure and are often used to dynamically build queries.

public class ExpressionTrees
{
public void ExpressionTreeExamples()
{
// Create an expression tree
Expression<Func<int, bool>> isEven = x => x % 2 == 0;

// Manually construct an expression tree
ParameterExpression param = Expression.Parameter(typeof(int), "x");
BinaryExpression operation = Expression.Equal(
Expression.Modulo(param, Expression.Constant(2)),
Expression.Constant(0)
);
var manualIsEven = Expression.Lambda<Func<int, bool>>(
operation,
param
);

// Compile and execute
var compiled = isEven.Compile();
bool result = compiled(4); // true
}
}

4. Using Lambdas with LINQ​

Lambda expressions are widely used in LINQ to make query syntax more concise and clear.

public class LinqWithLambda
{
private List<int> numbers = Enumerable.Range(1, 10).ToList();
private List<string> words = new List<string>
{
"apple", "banana", "cherry", "date"
};

public void LinqExamples()
{
// Where
var evenNumbers = numbers.Where(n => n % 2 == 0);

// Select
var doubled = numbers.Select(n => n * 2);

// OrderBy
var ordered = words.OrderBy(w => w.Length);

var filteredAndOrdered = numbers
.Where(n => n > 5)
.OrderByDescending(n => n)
.Select(n => n * n);

var grouped = words
.GroupBy(w => w[0])
.Select(g => new
{
FirstLetter = g.Key,
Words = g.ToList()
});
}
}