Introduction
C# (pronounced "C-sharp") is a modern, object-oriented, and type-safe programming language developed by Microsoft. It's widely used for building a variety of applications, from web and mobile apps to games and enterprise software. The .NET framework (and its cross-platform successor, .NET) provides a robust platform for developing these applications.
1. Introduction to C# and .NET
What is C#?
C# is a versatile, high-level programming language that is part of the .NET ecosystem. It's known for its strong typing, object-oriented features, and garbage collection, which simplifies memory management. C# draws inspiration from C++, Java, and other languages, offering a balance of power and ease of use.
What is .NET?
.NET is a free, open-source developer platform for building many different types of applications. It includes:
- Runtimes: The execution environments (e.g., .NET Runtime, .NET Framework).
- Libraries: A vast collection of pre-written code for common tasks.
- Compilers: Tools to translate your C# code into executable instructions.
- Tools: Utilities for development, debugging, and deployment.
Initially, .NET Framework was Windows-only. Now, with .NET (formerly .NET Core), you can build cross-platform applications that run on Windows, macOS, and Linux. This guide primarily focuses on modern C# and the cross-platform .NET.
Why Learn C# and .NET?
- Versatility: Build web apps (ASP.NET Core), desktop apps (WPF, WinForms, MAUI), mobile apps (MAUI), games (Unity), cloud services (Azure), and more.
- Strong Community & Ecosystem: Large community, extensive documentation, and a rich set of libraries and tools.
- Performance: .NET is designed for high performance and scalability.
- Job Opportunities: High demand for C# and .NET developers across various industries.
2. Setting Up Your Development Environment
The primary tool for C# and .NET development is Visual Studio.
2.1 Install Visual Studio (Community Edition)
- Go to the official Visual Studio website: https://visualstudio.microsoft.com/downloads/
- Download the Visual Studio Community edition, which is free for students, open-source contributors, and individual developers.
- Run the installer. During installation, select the following workloads:
- .NET Desktop development (for console and desktop applications)
- ASP.NET and web development (for web applications)
- .NET Multi-platform App UI development (for cross-platform mobile/desktop apps, if interested)
- Game development with Unity (if you plan to develop games)
- Click "Install".
2.2 Verify .NET SDK Installation
Visual Studio usually installs the necessary .NET SDKs. You can verify this by opening a command prompt or terminal and typing:
dotnet --version
This command should output the installed .NET SDK version (e.g., 8.0.x).
2.3 Creating Your First "Hello World" Application
Let's create a simple console application.
Using Visual Studio
- Open Visual Studio.
- Click "Create a new project".
- Search for "Console App" and select the template for "Console App" (make sure it's the C# version and uses .NET).
- Click "Next".
- Give your project a name (e.g., HelloWorldApp) and choose a location.
- Click "Next".
- Select the desired .NET version (e.g., .NET 8.0) and click "Create".
Visual Studio will create a project with a Program.cs file containing:
// Program.cs
Console.WriteLine("Hello, World!");
- Press F5 or click the "Run" button (green triangle) to run your application. A console window will appear, displaying "Hello, World!".
Using the .NET CLI (Command Line Interface)
- Open a command prompt or terminal.
- Navigate to the directory where you want to create your project.
- Create a new console project:
- dotnet new console -n HelloWorldCLI
- cd HelloWorldCLI
Run the application:
dotnet run
This will compile and execute your Program.cs file, showing "Hello, World!".
3. C# Fundamentals
3.1 Basic Syntax and Structure
A basic C# program consists of:
- Namespaces: Used to organize code and prevent naming conflicts.
- Classes: The fundamental building blocks of C# applications (everything lives inside a class).
- Methods: Blocks of code that perform specific tasks.
- Statements: Instructions that end with a semicolon ;.
- Comments: Used to explain code. Single-line // or multi-line /* ... */.
using System; // Imports the System namespace, providing basic functionalities like Console
namespace MyFirstProgram // Declares a namespace to organize your code
{
class Program // Declares a class named Program
{
static void Main(string[] args) // The entry point of the application
{
// This is a single-line comment
/*
* This is a multi-line comment.
* It can span multiple lines.
*/
Console.WriteLine("Hello from MyFirstProgram!"); // A statement to print text
}
}
}
3.2 Data Types
C# is a strongly-typed language, meaning you must declare the type of a variable before using it. Data types are categorized into Value Types and Reference Types.
Value Types (store data directly)
- Integral Numeric Types:
- byte (0 to 255)
- short (-32,768 to 32,767)
- int (-2,147,483,648 to 2,147,483,647) - Most common integer type
- long (very large integer)
- Floating-Point Numeric Types:
- float (single-precision floating-point)
- double (double-precision floating-point) - Most common floating-point type
- decimal (high precision for financial calculations)
- Boolean Type:
- Character Type:
- char (single Unicode character)
- Structs: User-defined value types.
- Enums: A set of named integral constants.
Reference Types (store a reference to data in memory)
- string: A sequence of characters.
- object: The base type for all other types in C#.
- Classes: User-defined reference types.
- Interfaces: Contracts that classes can implement.
- Arrays: Collections of elements of the same type.
- Delegates: Types that represent references to methods.
int age = 30;
double price = 19.99;
bool isActive = true;
char initial = 'J';
string name = "Alice";
Console.WriteLine($"Name: {name}, Age: {age}, Price: {price}, Active: {isActive}, Initial: {initial}");
3.3 Variables and Constants
- Variables: Named storage locations whose values can change during program execution.
- Declaration: dataType variableName;
- Initialization: dataType variableName = value;
- var keyword: C# can infer the type of a local variable if you initialize it immediately. This is called implicitly typed local variables.
- Constants: Named storage locations whose values cannot be changed after initialization. Declared with the const keyword.
// Variables
int score = 100; // Declared and initialized
score = 120; // Value can be changed
string message; // Declared
message = "Hello"; // Initialized later
var quantity = 50; // Type (int) is inferred by the compiler
var greeting = "Hi there!"; // Type (string) is inferred
// Constants
const double PI = 3.14159; // Value cannot be changed after this line
// PI = 3.14; // This would cause a compile-time error
3.4 Operators
Operators perform operations on one or more operands.
- Arithmetic Operators: +, -, *, /, % (modulus)
- Assignment Operators: =, +=, -=, *=, /=, %=
- Comparison Operators: == (equal to), != (not equal to), <, >, <=, >=
- Logical Operators: && (AND), || (OR), ! (NOT)
- Increment/Decrement Operators: ++, --
- Ternary Operator: condition? expression1: expression2; (shorthand for if-else)
int a = 10, b = 5;
// Arithmetic
Console.WriteLine($"a + b = {a + b}"); // 15
Console.WriteLine($"a / b = {a / b}"); // 2 (integer division)
// Assignment
a += 2; // a is now 12
// Comparison
Console.WriteLine($"a == b is {a == b}"); // False
// Logical
bool isAdult = true;
bool hasLicense = false;
Console.WriteLine($"Can drive: {isAdult && hasLicense}"); // False
// Increment/Decrement
int x = 5;
Console.WriteLine(x++); // Prints 5, then x becomes 6 (post-increment)
Console.WriteLine(++x); // x becomes 7, then prints 7 (pre-increment)
// Ternary
int age = 20;
string status = (age >= 18) ? "Adult" : "Minor";
Console.WriteLine(status); // Adult
3.5 Type Conversions
Converting one data type to another.
- Implicit Conversion (Widening): Automatic conversion when no data loss is possible (e.g., int to long, int to double).
- Explicit Conversion (Narrowing/Casting): Requires a cast operator () and may result in data loss (e.g., double to int).
- Helper Classes: Convert class (e.g., Convert.ToInt32()), Parse methods (e.g., int.Parse()), TryParse methods (e.g., int.TryParse()).
// Implicit
int myInt = 10;
double myDouble = myInt; // Implicit conversion from int to double
// Explicit (Casting)
double anotherDouble = 10.75;
int anotherInt = (int)anotherDouble; // Explicit conversion (casting), truncates to 10
// Using Convert class
string strNumber = "123";
int convertedInt = Convert.ToInt32(strNumber);
// Using Parse (throws exception if conversion fails)
string invalidNumber = "abc";
try
{
int parsedInt = int.Parse(invalidNumber); // This will throw a FormatException
}
catch (FormatException)
{
Console.WriteLine("Invalid number format for Parse.");
}
// Using TryParse (returns bool, safer)
string potentiallyValidNumber = "456";
if (int.TryParse(potentiallyValidNumber, out int result))
{
Console.WriteLine($"Successfully parsed: {result}");
}
else
{
Console.WriteLine("Could not parse the string.");
}
4. Control Structures
Control structures dictate the flow of execution in your program.
4.1 Conditional Statements
- if-else if-else: Executes different blocks of code based on conditions.
- switch: Provides a way to execute different code blocks based on the value of a single variable or expression.
int temperature = 25;
// if-else if-else
if (temperature < 0)
{
Console.WriteLine("Freezing weather.");
}
else if (temperature >= 0 && temperature <= 15)
{
Console.WriteLine("Cold weather.");
}
else if (temperature > 15 && temperature <= 25)
{
Console.WriteLine("Pleasant weather.");
}
else
{
Console.WriteLine("Hot weather.");
}
// switch statement
string dayOfWeek = "Monday";
switch (dayOfWeek)
{
case "Monday":
case "Tuesday": // Fall-through is allowed if no code between cases
case "Wednesday":
Console.WriteLine("It's a weekday.");
break;
case "Saturday":
case "Sunday":
Console.WriteLine("It's the weekend!");
break;
default:
Console.WriteLine("Invalid day.");
break;
}
4.2 Looping Constructs
- for loop: Repeats a block of code a specific number of times.
- foreach loop: Iterates over elements in a collection (arrays, lists, etc.).
- while loop: Repeats a block of code as long as a condition is true.
- do-while loop: Similar to while, but guarantees at least one execution of the block.
// for loop
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"For loop iteration: {i}");
}
// foreach loop (excellent for collections)
string[] fruits = { "Apple", "Banana", "Cherry" };
foreach (string fruit in fruits)
{
Console.WriteLine($"Fruit: {fruit}");
}
// while loop
int count = 0;
while (count < 3)
{
Console.WriteLine($"While loop count: {count}");
count++;
}
// do-while loop
int num = 0;
do
{
Console.WriteLine($"Do-While loop num: {num}");
num++;
} while (num < 0); // Condition is false, but it runs once
4.3 Jump Statements
- break: Terminates the innermost loop or switch statement.
- continue: Skips the rest of the current loop iteration and proceeds to the next iteration.
- return: Exits the current method and returns control to the calling method (optionally with a value).
// break example
for (int i = 0; i < 10; i++)
{
if (i == 5)
{
break; // Exit the loop when i is 5
}
Console.WriteLine($"Break example: {i}");
}
// Output: 0, 1, 2, 3, 4
// continue example
for (int i = 0; i < 5; i++)
{
if (i == 2)
{
continue; // Skip printing when i is 2
}
Console.WriteLine($"Continue example: {i}");
}
// Output: 0, 1, 3, 4
// return example (see Methods section)
// This would typically be used inside a method to exit early and return a value.
5. Object-Oriented Programming (OOP) in C#
C# is fundamentally an object-oriented language. OOP principles help in designing modular, reusable, and maintainable code. The four pillars of OOP are Encapsulation, Inheritance, Polymorphism, and Abstraction.
5.1 Classes and Objects
- Class: A blueprint or template for creating objects. It defines the properties (data) and methods (behavior) that objects of that class will have.
- Object: An instance of a class. When you create an object, you are creating a concrete entity based on the class blueprint.
// Define a class
public class Car
{
// Properties (data)
public string Make { get; set; }
public string Model { get; set; }
public int Year { get; set; }
public string Color { get; set; }
// Method (behavior)
public void StartEngine()
{
Console.WriteLine($"{Make} {Model} engine started!");
}
public void Drive()
{
Console.WriteLine($"{Make} {Model} is driving.");
}
}
// In Main method or another class:
Car myCar = new Car();
myCar.Make = "Toyota";
myCar.Model = "Camry";
myCar.Year = 2022;
myCar.Color = "Blue";
Car anotherCar = new Car();
anotherCar.Make = "Honda";
anotherCar.Model = "Civic";
anotherCar.Year = 2023;
anotherCar.Color = "Red";
myCar.StartEngine();
anotherCar.Drive();
5.2 Encapsulation
Encapsulation is the bundling of data (properties) and methods that operate on the data within a single unit (class), and restricting direct access to some of the object's components. This is achieved using access modifiers and properties.
Access Modifiers
- public: Accessible from anywhere.
- private: Accessible only within the defining class (default for members).
- protected: Accessible within the defining class and by derived classes.
- internal: Accessible within the same assembly.
- protected internal: Accessible within the same assembly OR by derived classes in other assemblies.
- private protected: Accessible within the same assembly AND by derived classes in the same assembly.
Properties
Properties provide a flexible mechanism to read, write, or compute the value of a private field. They use get (accessor) and set (mutator) blocks.
public class BankAccount
{
private decimal _balance; // private field
// Public property with full access
public string AccountNumber { get; set; } // Auto-implemented property
// Public property with custom logic
public decimal Balance
{
get { return _balance; }
private set // Balance can only be set internally or by methods in this class
{
if (value < 0)
{
Console.WriteLine("Balance cannot be negative.");
_balance = 0;
}
else
{
_balance = value;
}
}
}
public BankAccount(string accountNumber, decimal initialBalance)
{
AccountNumber = accountNumber;
Balance = initialBalance; // This uses the 'set' accessor
}
public void Deposit(decimal amount)
{
if (amount > 0)
{
Balance += amount; // This uses the 'set' accessor
Console.WriteLine($"Deposited {amount}. New balance: {Balance}");
}
}
public void Withdraw(decimal amount)
{
if (amount > 0 && Balance >= amount)
{
Balance -= amount; // This uses the 'set' accessor
Console.WriteLine($"Withdrew {amount}. New balance: {Balance}");
}
else
{
Console.WriteLine("Insufficient funds or invalid amount.");
}
}
}
// Usage
BankAccount account = new BankAccount("123456789", 1000m);
Console.WriteLine($"Account: {account.AccountNumber}, Initial Balance: {account.Balance}");
account.Deposit(200);
account.Withdraw(500);
account.Withdraw(1000); // Insufficient funds
// account.Balance = -50; // This would cause a compile-time error due to private set
5.3 Inheritance
Inheritance allows a class (derived class or subclass) to inherit properties and methods from another class (base class or superclass). This promotes code reusability. C# supports single inheritance (a class can only inherit from one base class).
using System;
// Base class
class Employee
{
public string Name { get; set; }
public int Id { get; set; }
public virtual void Work()
{
Console.WriteLine($"{Name} (ID: {Id}) is working.");
}
}
// Derived class
class Manager : Employee
{
public int TeamSize { get; set; }
// Method unique to Manager
public void ConductMeeting()
{
Console.WriteLine($"{Name} is conducting a meeting with {TeamSize} team members.");
}
// Override Work method
public override void Work()
{
Console.WriteLine($"{Name} (ID: {Id}) is managing the team.");
}
}
class Program
{
static void Main()
{
Manager mgr = new Manager
{
Name = "Kiran",
Id = 101,
TeamSize = 8
};
mgr.Work(); // Overridden method in Manager
mgr.ConductMeeting(); // Method specific to Manager
// Output:
// Kiran (ID: 101) is managing the team.
// Kiran is conducting a meeting with 8 team members.
}
}
5.4 Polymorphism
Polymorphism means "many forms." In C#, it allows objects of different classes to be treated as objects of a common base class. This is achieved through:
- Method Overloading: Defining multiple methods in the same class with the same name but different parameters (different signatures).
- Method Overriding: Providing a specific implementation for a method that is already defined in the base class (using virtual in base and override in derived).
- Abstract Classes: Classes that cannot be instantiated directly and may contain abstract (unimplemented) methods. Derived classes must implement these abstract methods.
- Interfaces: Contracts that define a set of methods, properties, and events that a class must implement. A class can implement multiple interfaces.
using System;
// ---------------------
// Method Overloading
// ---------------------
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public double Add(double a, double b) // Overloaded with different types
{
return a + b;
}
public int Add(int a, int b, int c) // Overloaded with more parameters
{
return a + b + c;
}
}
// ---------------------
// Abstract Class
// ---------------------
public abstract class Shape
{
public abstract double GetArea(); // Must be implemented by derived classes
public void Display()
{
Console.WriteLine("This is a shape.");
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public Circle(double radius)
{
Radius = radius;
}
public override double GetArea()
{
return Math.PI * Radius * Radius;
}
}
// ---------------------
// Interface
// ---------------------
public interface ILogger
{
void LogMessage(string message);
void LogError(string error);
}
public class ConsoleLogger : ILogger
{
public void LogMessage(string message)
{
Console.WriteLine($"[MESSAGE]: {message}");
}
public void LogError(string error)
{
Console.Error.WriteLine($"[ERROR]: {error}");
}
}
// ---------------------
// Usage Example
// ---------------------
class Program
{
static void Main()
{
// Method Overloading
Calculator calc = new Calculator();
Console.WriteLine(calc.Add(5, 3)); // Calls Add(int, int)
Console.WriteLine(calc.Add(5.5, 3.2)); // Calls Add(double, double)
Console.WriteLine(calc.Add(1, 2, 3)); // Calls Add(int, int, int)
// Abstract Class Polymorphism
Shape circle = new Circle(5); // Circle treated as Shape
Console.WriteLine($"Circle Area: {circle.GetArea():F2}");
circle.Display();
// Interface Polymorphism
ILogger logger = new ConsoleLogger(); // ConsoleLogger treated as ILogger
logger.LogMessage("Application started.");
logger.LogError("Something went wrong!");
}
}
5.5 Constructors and Destructors
- Constructors: Special methods used to initialize objects when they are created. They have the same name as the class and no return type.
- Default Constructor: Provided automatically if you don't define any.
- Parameterized Constructor: Takes arguments to initialize properties.
- Constructor Chaining: Calling one constructor from another using this() or base().
- Destructors: Special methods used to clean up resources before an object is garbage collected. They are rarely used in modern C# due to automatic garbage collection and IDisposable pattern.
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
// Default constructor
public Person()
{
FirstName = "Unknown";
LastName = "Unknown";
Age = 0;
Console.WriteLine("Default Person created.");
}
// Parameterized constructor
public Person(string firstName, string lastName) : this() // Calls default constructor first
{
FirstName = firstName;
LastName = lastName;
Console.WriteLine($"Person {FirstName} {LastName} created.");
}
// Another parameterized constructor
public Person(string firstName, string lastName, int age) : this(firstName, lastName) // Calls the (string, string) constructor
{
Age = age;
Console.WriteLine($"Person {FirstName} {LastName} (Age: {Age}) created.");
}
// Destructor (rarely used, for unmanaged resources)
~Person()
{
Console.WriteLine($"Person {FirstName} {LastName} is being finalized.");
}
}
// Usage
Person p1 = new Person();
Person p2 = new Person("John", "Doe");
Person p3 = new Person("Jane", "Smith", 25);
5.6 Static Members
static members belong to the class itself, not to any specific instance of the class. You access them directly using the class name.
- Static Fields: Shared across all instances of the class.
- Static Methods: Can only access static members of the class. Often used for utility functions.
- Static Classes: Contain only static members and cannot be instantiated.
public static class MathOperations // Static class
{
public static double PI = 3.14159; // Static field
public static int Add(int a, int b) // Static method
{
return a + b;
}
public static int Subtract(int a, int b)
{
return a - b;
}
}
public class Counter
{
public static int InstanceCount = 0; // Static field, tracks instances
public Counter()
{
InstanceCount++; // Incremented every time a new instance is created
}
}
// Usage
Console.WriteLine($"PI: {MathOperations.PI}");
Console.WriteLine($"Sum: {MathOperations.Add(10, 5)}");
Counter c1 = new Counter();
Counter c2 = new Counter();
Console.WriteLine($"Number of Counter instances: {Counter.InstanceCount}"); // Output: 2
6. Methods and Functions
Methods (often called functions in other languages) are blocks of code that perform a specific task.
6.1 Defining and Calling Methods
public class Greeter
{
// Method with no parameters and no return value (void)
public void SayHello()
{
Console.WriteLine("Hello!");
}
// Method with one parameter and no return value
public void SayHelloTo(string name)
{
Console.WriteLine($"Hello, {name}!");
}
// Method with parameters and a return value
public int AddNumbers(int num1, int num2)
{
return num1 + num2;
}
// Method with optional parameters (must be at the end)
public void Greet(string name, string greeting = "Hello")
{
Console.WriteLine($"{greeting}, {name}!");
}
}
// Usage
Greeter greeter = new Greeter();
greeter.SayHello();
greeter.SayHelloTo("Alice");
int sum = greeter.AddNumbers(10, 20);
Console.WriteLine($"Sum: {sum}");
greeter.Greet("Bob"); // Uses default greeting "Hello"
greeter.Greet("Charlie", "Hi"); // Uses provided greeting "Hi"
6.2 Method Parameters (ref, out, params)
- Value Parameters (default): A copy of the argument's value is passed. Changes to the parameter inside the method do not affect the original argument.
- ref Parameters: Passes the argument by reference. Changes inside the method affect the original argument. The argument must be initialized before passing.
- out Parameters: Passes the argument by reference. The method is responsible for initializing the out parameter before it returns. The argument does not need to be initialized before passing.
- params Keyword: Allows a method to accept a variable number of arguments of a specific type. Must be the last parameter.
public class ParameterExamples
{
public void Increment(int num) // Value parameter
{
num++;
Console.WriteLine($"Inside Increment (value): {num}");
}
public void IncrementRef(ref int num) // ref parameter
{
num++;
Console.WriteLine($"Inside IncrementRef (ref): {num}");
}
public void GetCoordinates(out int x, out int y) // out parameters
{
x = 10;
y = 20;
Console.WriteLine("Coordinates set.");
}
public int SumNumbers(params int[] numbers) // params parameter
{
int total = 0;
foreach (int num in numbers)
{
total += num;
}
return total;
}
}
// Usage
ParameterExamples pe = new ParameterExamples();
int val = 5;
pe.Increment(val); // val remains 5
Console.WriteLine($"Outside Increment (value): {val}"); // Output: 5
int refVal = 5;
pe.IncrementRef(ref refVal); // refVal becomes 6
Console.WriteLine($"Outside IncrementRef (ref): {refVal}"); // Output: 6
int coordX, coordY; // No need to initialize for out parameters
pe.GetCoordinates(out coordX, out coordY);
Console.WriteLine($"Coordinates: ({coordX}, {coordY})");
Console.WriteLine($"Sum of 1,2,3: {pe.SumNumbers(1, 2, 3)}");
Console.WriteLine($"Sum of 1,2,3,4,5: {pe.SumNumbers(1, 2, 3, 4, 5)}");
6.3 Return Values
Methods can return a value using the return keyword. The return type must match the method's declared return type. void means no value is returned.
6.4 Extension Methods
Extension methods allow you to add new methods to existing types without modifying the original type. They are static methods defined in a static class, where the first parameter is prefixed with this.
public static class StringExtensions // Must be a static class
{
public static string CapitalizeFirstLetter(this string input) // 'this' keyword
{
if (string.IsNullOrEmpty(input))
{
return input;
}
return char.ToUpper(input[0]) + input.Substring(1);
}
public static int WordCount(this string text)
{
if (string.IsNullOrWhiteSpace(text))
{
return 0;
}
return text.Split(new char[] { ' ', '.', '?', '!' }, StringSplitOptions.RemoveEmptyEntries).Length;
}
}
// Usage
string sentence = "hello world. how are you?";
Console.WriteLine(sentence.CapitalizeFirstLetter()); // Output: Hello world. how are you?
Console.WriteLine($"Word count: {sentence.WordCount()}"); // Output: 5
7. Arrays, Collections, and Generics
7.1 Arrays
Arrays are fixed-size collections of elements of the same data type.
// Declare and initialize an array
int[] numbers = new int[5]; // Array of 5 integers, initialized to 0s
numbers[0] = 10;
numbers[1] = 20;
// ...
// Declare and initialize with values
string[] names = { "Alice", "Bob", "Charlie" };
// Accessing elements
Console.WriteLine($"First name: {names[0]}"); // Alice
// Iterating through an array using for loop
for (int i = 0; i < names.Length; i++)
{
Console.WriteLine($"Name at index {i}: {names[i]}");
}
// Iterating through an array using foreach loop
foreach (string name in names)
{
Console.WriteLine($"Name: {name}");
}
// Multi-dimensional arrays
int[,] matrix = new int[2, 3]
{
{ 1, 2, 3 },
{ 4, 5, 6 }
};
Console.WriteLine($"Element at (0,1): {matrix[0, 1]}"); // Output: 2
7.2 Common Collections
The System.Collections.Generic namespace provides flexible, type-safe collections.
- List<T>: A dynamic array that can grow or shrink. T is the type of elements it holds.
- Dictionary<TKey, TValue>: Stores key-value pairs. Keys must be unique.
- HashSet<T>: Stores a collection of unique elements. Optimized for fast lookups.
- Queue<T>: A first-in, first-out (FIFO) collection.
- Stack<T>: A last-in, first-out (LIFO) collection.
using System.Collections.Generic; // Important for generic collections
// List<T>
List<string> cities = new List<string>();
cities.Add("New York");
cities.Add("London");
cities.Add("Paris");
cities.Insert(1, "Tokyo"); // Insert at specific index
cities.Remove("London");
Console.WriteLine("Cities in list:");
foreach (string city in cities)
{
Console.WriteLine(city);
}
Console.WriteLine($"List contains Paris: {cities.Contains("Paris")}");
// Dictionary<TKey, TValue>
Dictionary<string, int> ages = new Dictionary<string, int>();
ages.Add("Alice", 30);
ages.Add("Bob", 25);
ages["Charlie"] = 35; // Add or update
Console.WriteLine($"Alice's age: {ages["Alice"]}");
if (ages.TryGetValue("David", out int davidAge))
{
Console.WriteLine($"David's age: {davidAge}");
}
else
{
Console.WriteLine("David not found.");
}
Console.WriteLine("Ages in dictionary:");
foreach (var entry in ages)
{
Console.WriteLine($"{entry.Key}: {entry.Value}");
}
// HashSet<T>
HashSet<int> uniqueNumbers = new HashSet<int>();
uniqueNumbers.Add(1);
uniqueNumbers.Add(2);
uniqueNumbers.Add(1); // Duplicate, won't be added
Console.WriteLine("Unique numbers:");
foreach (int num in uniqueNumbers)
{
Console.WriteLine(num); // Output: 1, 2
}
// Queue<T>
Queue<string> tasks = new Queue<string>();
tasks.Enqueue("Task A");
tasks.Enqueue("Task B");
Console.WriteLine($"Next task: {tasks.Dequeue()}"); // Output: Task A
// Stack<T>
Stack<string> history = new Stack<string>();
history.Push("Page 1");
history.Push("Page 2");
Console.WriteLine($"Last visited: {history.Pop()}"); // Output: Page 2
// Generics
// Generic class
public class Box<T> // T is a type parameter
{
public T Content { get; set; }
public Box(T content)
{
Content = content;
}
public void DisplayContent()
{
Console.WriteLine($"Box content: {Content}");
}
}
// Generic method
public class Utilities
{
public static void Swap<T>(ref T a, ref T b) // T is a type parameter for the method
{
T temp = a;
a = b;
b = temp;
}
}
// Usage
Box<int> intBox = new Box<int>(123);
intBox.DisplayContent();
Box<string> stringBox = new Box<string>("Hello Generics");
stringBox.DisplayContent();
int x = 10, y = 20;
Console.WriteLine($"Before swap: x={x}, y={y}");
Utilities.Swap(ref x, ref y); // Type inference for T (int)
Console.WriteLine($"After swap: x={x}, y={y}");
string s1 = "First", s2 = "Second";
Console.WriteLine($"Before swap: s1={s1}, s2={s2}");
Utilities.Swap(ref s1, ref s2); // Type inference for T (string)
Console.WriteLine($"After swap: s1={s1}, s2={s2}");
8. Exception Handling
Exceptions are errors that occur during the execution of a program. C# provides a structured way to handle these errors using try-catch-finally blocks.
- try block: Contains the code that might throw an exception.
- catch block: Catches and handles a specific type of exception. You can have multiple catch blocks for different exception types.
- finally block: Contains code that will always execute, regardless of whether an exception occurred or was handled (e.g., for resource cleanup).
- throw keyword: Used to explicitly throw an exception.
public class ExceptionExample
{
public void Divide(int numerator, int denominator)
{
try
{
if (denominator == 0)
{
// Explicitly throw an exception
throw new DivideByZeroException("Denominator cannot be zero.");
}
int result = numerator / denominator;
Console.WriteLine($"Result of division: {result}");
}
catch (DivideByZeroException ex) // Catch specific exception
{
Console.WriteLine($"Error: {ex.Message}");
Console.WriteLine($"Stack Trace: {ex.StackTrace}");
}
catch (FormatException ex) // Catch another specific exception
{
Console.WriteLine($"Input format error: {ex.Message}");
}
catch (Exception ex) // Catch any other exception (general catch should be last)
{
Console.WriteLine($"An unexpected error occurred: {ex.Message}");
}
finally
{
Console.WriteLine("Division attempt finished."); // This always executes
}
}
// Custom Exception
public class InvalidAgeException : Exception
{
public InvalidAgeException()
: base("Age cannot be negative.") { }
public InvalidAgeException(string message)
: base(message) { }
public InvalidAgeException(string message, Exception innerException)
: base(message, innerException) { }
}
public void SetAge(int age)
{
if (age < 0)
{
throw new InvalidAgeException("Provided age is negative.");
}
Console.WriteLine($"Age set to: {age}");
}
}
// Usage
ExceptionExample exHandler = new ExceptionExample();
exHandler.Divide(10, 2); // Normal case
exHandler.Divide(10, 0); // Will throw and catch DivideByZeroException
try
{
exHandler.SetAge(-5); // Will throw InvalidAgeException
}
catch (ExceptionExample.InvalidAgeException ex)
{
Console.WriteLine($"Custom Exception Caught: {ex.Message}");
}
9. Delegates, Events, and Lambdas
These concepts are crucial for building flexible and extensible applications, especially for event-driven programming.
9.1 Delegates
A delegate is a type that represents references to methods with a particular parameter list and return type. It's like a type-safe function pointer.
// 1. Declare a delegate type
public delegate void MyDelegate(string message);
public class DelegateExample
{
public void Method1(string msg)
{
Console.WriteLine($"Method1 called: {msg}");
}
public void Method2(string msg)
{
Console.WriteLine($"Method2 called: {msg.ToUpper()}");
}
public static void StaticMethod(string msg)
{
Console.WriteLine($"StaticMethod called: {msg}");
}
}
// Usage
DelegateExample delEx = new DelegateExample();
// Create delegate instances
MyDelegate handler1 = new MyDelegate(delEx.Method1);
MyDelegate handler2 = delEx.Method2; // Shorthand syntax
MyDelegate handler3 = DelegateExample.StaticMethod;
// Invoke delegates
handler1("Hello from handler1");
handler2("hello from handler2");
handler3("Hello from static method");
// Multicast delegate (combining multiple methods)
MyDelegate multiHandler = handler1 + handler2;
multiHandler("Hello from multi-handler"); // Calls both Method1 and Method2
multiHandler -= handler1; // Remove a method
multiHandler("Hello again from multi-handler"); // Only calls Method2 now
9.2 Events
Events are a way for a class to provide notifications to other classes when something interesting happens. They are built on top of delegates.
public class Button
{
// 1. Declare a delegate for the event (often EventHandler or custom)
public delegate void ClickEventHandler(object sender, EventArgs e);
// 2. Declare the event using the 'event' keyword
public event ClickEventHandler Click;
public void SimulateClick()
{
Console.WriteLine("Button clicked!");
// 3. Raise the event (check if there are any subscribers)
Click?.Invoke(this, EventArgs.Empty); // Shorthand for null check and invocation
}
}
public class Form
{
public Form()
{
Button myButton = new Button();
// 4. Subscribe to the event
myButton.Click += MyButton_Click; // += to add event handler
myButton.Click += AnotherClickHandler;
myButton.SimulateClick(); // Simulate the event
}
// Event handler method (matches the delegate signature)
private void MyButton_Click(object sender, EventArgs e)
{
Console.WriteLine("MyButton_Click event handler executed!");
}
private void AnotherClickHandler(object sender, EventArgs e)
{
Console.WriteLine("AnotherClickHandler executed!");
}
}
// To run this, you'd typically instantiate Form: new Form();
9.3 Lambda Expressions
Lambda expressions are a concise way to create anonymous methods (methods without a name). They are often used with LINQ and events.
// Lambda as a delegate
Func<int, int, int> add = (a, b) => a + b; // Func is a built-in delegate type
Console.WriteLine($"Lambda sum: {add(10, 5)}");
Action<string> printMessage = msg => Console.WriteLine($"Lambda message: {msg}"); // Action is for void methods
printMessage("Hello from lambda!");
// Lambda with event subscription (from Button example)
public class AnotherForm
{
public AnotherForm()
{
Button myButton = new Button();
myButton.Click += (sender, e) =>
{
// Lambda expression as event handler
Console.WriteLine("Lambda event handler executed!");
Console.WriteLine($"Sender type: {sender.GetType().Name}");
};
myButton.SimulateClick();
}
}
// To run this, you'd typically instantiate AnotherForm: new AnotherForm();
10. Asynchronous Programming
Asynchronous programming allows your application to perform long-running operations (like network requests, file I/O) without blocking the main thread, keeping the UI responsive. C# uses the async and await keywords for this.
- async keyword: Marks a method as asynchronous. An async method must return Task, Task<T>, or void (though void is generally discouraged for async methods except for event handlers).
- await keyword: Pauses the execution of an async method until the awaited asynchronous operation completes. Control is returned to the caller, and the method resumes when the awaited task is done.
using System;
using System.Net.Http; // For HttpClient
using System.Threading.Tasks; // For Task and Task<T>
public class AsyncExample
{
// Asynchronous method that returns a Task (no return value)
public async Task SimulateLongRunningOperation()
{
Console.WriteLine("Starting long-running operation...");
await Task.Delay(3000); // Simulate 3 seconds of work
Console.WriteLine("Long-running operation finished.");
}
// Asynchronous method that returns a Task<T> (returns a value)
public async Task<string> DownloadWebpageContent(string url)
{
Console.WriteLine($"Downloading content from {url}...");
using (HttpClient client = new HttpClient())
{
string content = await client.GetStringAsync(url); // Await an I/O operation
Console.WriteLine($"Finished downloading content from {url}. Content length: {content.Length}");
return content.Substring(0, Math.Min(content.Length, 100)) + "..."; // Return first 100 chars
}
}
public async Task RunAsyncOperations()
{
Console.WriteLine("Main thread: Before async calls.");
// Start an async operation without waiting immediately
Task operationTask = SimulateLongRunningOperation();
// Start another async operation and await its result
string googleContent = await DownloadWebpageContent("https://www.google.com");
Console.WriteLine($"Main thread: Google content snippet: {googleContent}");
// Now await the first operation if it hasn't finished yet
await operationTask;
Console.WriteLine("Main thread: After all async calls.");
}
}
// To run this example, you'd typically call it from an async Main method (C# 7.1+)
// Example:
// static async Task Main(string[] args)
// {
// AsyncExample asyncEx = new AsyncExample();
// await asyncEx.RunAsyncOperations();
// Console.WriteLine("Press any key to exit.");
// Console.ReadKey();
// }
11. LINQ (Language Integrated Query)
LINQ provides a powerful and consistent way to query data from various sources (collections, databases, XML, etc.) directly within C#.
- Query Syntax: Resembles SQL.
- Method Syntax (Extension Methods): Uses extension methods on IEnumerable<T>.
using System;
using System.Collections.Generic;
using System.Linq; // Essential for LINQ
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public string Category { get; set; }
}
public class LinqExample
{
public void RunLinqQueries()
{
List<Product> products = new List<Product>
{
new Product { Id = 1, Name = "Laptop", Price = 1200m, Category = "Electronics" },
new Product { Id = 2, Name = "Keyboard", Price = 75m, Category = "Electronics" },
new Product { Id = 3, Name = "Mouse", Price = 25m, Category = "Electronics" },
new Product { Id = 4, Name = "Desk Chair", Price = 300m, Category = "Furniture" },
new Product { Id = 5, Name = "Monitor", Price = 400m, Category = "Electronics" },
new Product { Id = 6, Name = "Table", Price = 150m, Category = "Furniture" }
};
// 1. Filtering (Where) - Query Syntax
var electronicsProducts = from p in products
where p.Category == "Electronics"
select p;
Console.WriteLine("Electronics Products (Query Syntax):");
foreach (var p in electronicsProducts)
{
Console.WriteLine($"- {p.Name} (${p.Price})");
}
// 2. Filtering (Where) - Method Syntax
var expensiveProducts = products.Where(p => p.Price > 100m);
Console.WriteLine("\nExpensive Products (Method Syntax):");
foreach (var p in expensiveProducts)
{
Console.WriteLine($"- {p.Name} (${p.Price})");
}
// 3. Ordering (OrderBy, OrderByDescending)
var productsSortedByPrice = products.OrderBy(p => p.Price);
Console.WriteLine("\nProducts Sorted by Price (Ascending):");
foreach (var p in productsSortedByPrice)
{
Console.WriteLine($"- {p.Name} (${p.Price})");
}
// 4. Projection (Select)
var productNames = products.Select(p => p.Name);
Console.WriteLine("\nProduct Names:");
foreach (var name in productNames)
{
Console.WriteLine($"- {name}");
}
// 5. Grouping (GroupBy)
var productsByCategory = products.GroupBy(p => p.Category);
Console.WriteLine("\nProducts Grouped by Category:");
foreach (var group in productsByCategory)
{
Console.WriteLine($"- {group.Key} ({group.Count()} items)");
foreach (var p in group)
{
Console.WriteLine($" -- {p.Name}");
}
}
// 6. Aggregation (Sum, Average, Count, Max, Min)
decimal totalElectronicsPrice = products
.Where(p => p.Category == "Electronics")
.Sum(p => p.Price);
Console.WriteLine($"\nTotal price of Electronics: ${totalElectronicsPrice}");
// 7. Chaining LINQ methods
var cheapElectronicsNames = products
.Where(p => p.Category == "Electronics" && p.Price < 100)
.OrderBy(p => p.Name)
.Select(p => p.Name);
Console.WriteLine("\nCheap Electronics Names (Chained):");
foreach (var name in cheapElectronicsNames)
{
Console.WriteLine($"- {name}");
}
}
}
// To run this: new LinqExample().RunLinqQueries();
12. File I/O and Streams
C# provides classes in the System.IO namespace for reading from and writing to files and other data streams.
using System;
using System.IO; // For file operations
public class FileIOExample
{
private readonly string filePath = "mydata.txt";
public void WriteToFile(string content)
{
try
{
// Write all text to a file (overwrites if exists)
File.WriteAllText(filePath, content);
Console.WriteLine($"Content written to {filePath}");
// Append text to a file
File.AppendAllText(filePath, "\nAppended new line.");
Console.WriteLine("Content appended.");
}
catch (Exception ex)
{
Console.WriteLine($"Error writing to file: {ex.Message}");
}
}
public void ReadFromFile()
{
try
{
if (File.Exists(filePath))
{
// Read all text from a file
string content = File.ReadAllText(filePath);
Console.WriteLine($"\nContent read from {filePath}:\n{content}");
// Read line by line
Console.WriteLine("\nReading line by line:");
using (StreamReader reader = new StreamReader(filePath))
{
string line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine($"- {line}");
}
}
}
else
{
Console.WriteLine($"\nFile {filePath} does not exist.");
}
}
catch (Exception ex)
{
Console.WriteLine($"Error reading from file: {ex.Message}");
}
}
public void DeleteFile()
{
if (File.Exists(filePath))
{
File.Delete(filePath);
Console.WriteLine($"\nFile {filePath} deleted.");
}
else
{
Console.WriteLine($"\nFile {filePath} not found for deletion.");
}
}
}
// Usage:
// FileIOExample fileEx = new FileIOExample();
// fileEx.WriteToFile("This is the first line.\nThis is the second line.");
// fileEx.ReadFromFile();
// fileEx.DeleteFile();
13. Debugging Techniques
Debugging is the process of finding and fixing errors (bugs) in your code. Visual Studio provides powerful debugging tools.
- Breakpoints: Set a breakpoint by clicking in the left margin of the code editor. When the program runs in debug mode (F5), execution will pause at the breakpoint.
- Stepping:
- Step Over (F10): Executes the current line of code and moves to the next line. If the current line contains a method call, it executes the method entirely without stepping into it.
- Step Into (F11): Executes the current line. If it's a method call, it steps into the method's code.
- Step Out (Shift+F11): Executes the remainder of the current method and returns to the calling method.
- Watch Windows:
- Locals Window: Shows variables in the current scope.
- Autos Window: Shows variables used in the current and previous statements.
- Watch Window: Allows you to manually add variables or expressions to monitor their values as you step through code.
- Call Stack Window: Shows the sequence of method calls that led to the current execution point.
- Immediate Window: Allows you to execute C# code or evaluate expressions at runtime while debugging.
- Output Window: Displays messages from the debugger, build process, and Console.WriteLine() output.
How to Debug?
- Set a breakpoint on a line of code you want to inspect.
- Start debugging by pressing F5 or selecting "Debug > Start Debugging".
- When execution hits the breakpoint, use F10 (Step Over) or F11 (Step Into) to move through your code line by line.
- Hover over variables to see their current values, or use the Watch/Locals/Autos windows.
- Observe the Call Stack to understand the execution flow.
- If an exception occurs, the debugger will stop at the line where it happened, allowing you to inspect the state.
14. Best Practices and Design Principles
Writing good code goes beyond just making it work.
- Clean Code Principles:
- Meaningful Names: Use descriptive names for variables, methods, and classes (e.g., calculateTotalPrice instead of calc).
- Small Functions: Methods should do one thing and do it well. Keep them short.
- Readability: Code should be easy to understand by others (and your future self).
- DRY (Don't Repeat Yourself): Avoid duplicating code. Use methods, classes, or patterns to reuse logic.
- Naming Conventions (C# Specific):
- PascalCase: For class names, method names, properties, and public fields (e.g., MyClass, CalculateSum, ProductName).
- camelCase: For local variables and method parameters (e.g., myVariable, inputString).
- _camelCase (or this.camelCase): For private fields (e.g., _myField or this.myField).
- SOLID Principles (Briefly): A set of five design principles that help in creating maintainable and scalable object-oriented software.
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
- Error Handling: Use try-catch blocks judiciously. Don't just catch Exception everywhere; be specific. Log errors.
- Resource Management: Use using statements for objects that implement IDisposable (like file streams, database connections) to ensure they are properly cleaned up.
// Example of using statement for resource management
public void ProcessFile(string filePath)
{
// The StreamReader will be automatically disposed of when exiting the 'using' block
using (StreamReader reader = new StreamReader(filePath))
{
string line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}
} // reader.Dispose() is called here automatically
}
15. Key .NET Libraries and Tools
The .NET ecosystem is vast. Here are some essential components:
- .NET Class Library (BCL - Base Class Library): The foundation of .NET, providing fundamental types and functionalities (e.g., System, System.Collections.Generic, System.IO, System.Linq, System.Text). You've already used many of these.
- NuGet Package Manager: The package manager for .NET. It allows you to easily add, update, and remove third-party libraries (packages) to your projects. Think of it like npm for JavaScript or pip for Python.
- ASP.NET Core: A cross-platform, high-performance framework for building modern, cloud-based, internet-connected applications (web apps, APIs).
- WPF (Windows Presentation Foundation) / WinForms (Windows Forms): Frameworks for building desktop applications on Windows. WPF is newer and more powerful for rich UIs.
- .NET MAUI (Multi-platform App UI): A cross-platform framework for building native Android, iOS, macOS, and Windows applications from a single codebase.
- Entity Framework Core (EF Core): An object-relational mapper (ORM) that enables .NET developers to work with a database using .NET objects, eliminating the need for most of the data-access code.
- Unit Testing Frameworks (NUnit, xUnit, MSTest): Essential for writing automated tests to ensure your code works as expected and to prevent regressions.
- LINQ to SQL / LINQ to Entities: Specific LINQ providers for querying databases.