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 đâ
- Method names must be identical
- 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 theoverridekeyword) and method hiding (using thenewkeyword 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 accessprivate: Access only within the classprotected: Access within class and derived classesinternal: Access within the same assembly
| Modifier | Same Class | Derived 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
virtualandoverridekeywords, 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â
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 valueset: 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
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.
- 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}");
}
}
- 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, oroverridekeywords - Derived class methods must use the
overridekeyword - 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.
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.
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.
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 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
thiskeyword
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
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 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 AttributesObsoleteâ Mark code as deprecatedSerializableâ Mark classes as serializableConditionalâ Used for conditional compilationAttributeUsageâ Describes how attributes can be used
- Must inherit from the
Attributeclass - 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:
- If the application crashes or is forcibly terminated
- If there's an infinite loop in the
tryorcatchblock - If
Environment.FailFast()orProcess.Kill()is called - 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 expectedArgumentOutOfRangeException: This occurs when an argument's value is outside the permissible rangeDivideByZeroException: This is thrown when there's an attempt to divide by zeroInvalidOperationException: This arises when the state of an object doesn't permit a particular operationFileNotFoundException: This occurs when a file that's being attempted to be accessed doesn't existStackOverflowException: This is thrown when there's a stack overflow due to excessive recursion or other reasonsNullReferenceException: 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);
}
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}
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.