1. Basic Data Types in C#
Numeric Types
Numeric types like int
are used for whole numbers. Here’s an example:
int i = 10;
int j = 20;
Console.WriteLine(i + j); // Output: 30
Console.WriteLine(typeof(int)); // Output: System.Int32
Boolean Types
Boolean types represent true or false values:
bool IsPresent = true;
Console.WriteLine(typeof(bool)); // Output: System.Boolean
Console.WriteLine(IsPresent); // Output: True
Using var
var
is a shorthand for declaring variables with inferred types:
var i = 10;
Console.WriteLine(i); // Output: 10
However, you cannot assign a value of a different type:
i = "Hello"; // Error: Cannot implicitly convert type 'string' to 'int'
Dynamic Typing
The dynamic
type allows changing the type at runtime:
dynamic i = 10;
Console.WriteLine(i); // Output: 10
i = "Hello";
Console.WriteLine(i); // Output: Hello
String Types
Strings represent text. Example:
string Name = "John";
Console.WriteLine(typeof(string)); // Output: System.String
Console.WriteLine(Name); // Output: John
String Interpolation
You can use string interpolation to include variables in strings:
var Name = "John";
var Age = 10;
Console.WriteLine($"The person {Name} is {Age} years old");
Verbatim Strings
Verbatim strings ignore escape sequences:
var s = @"Hello\World";
Console.WriteLine(s); // Output: Hello\World
2. Working with Collections in C#
Arrays
Arrays are fixed-size collections:
var nums = new[] { 1, 2, 3 };
Console.WriteLine(nums[0]); // Output: 1
nums[1] = 5;
foreach (var num in nums)
{
Console.WriteLine(num); // Output: 1, 5, 3
}
Lists
Lists are dynamic-sized collections:
IList<int> nums = new List<int>();
nums.Add(1);
nums.Add(2);
nums.Add(3);
Console.WriteLine(nums[0]); // Output: 1
nums[0] = 15;
nums.RemoveAt(1);
foreach (var num in nums)
{
Console.WriteLine(num); // Output: 15, 3
}
Dictionaries
Dictionaries store key-value pairs:
var countrywiseParticipants = new Dictionary<string, int>
{
{ "India", 20 },
{ "US", 25 },
{ "UK", 30 }
};
Console.WriteLine($"Participants from India: {countrywiseParticipants["India"]}");
countrywiseParticipants.Remove("US");
foreach(var countrywiseParticipant in countrywiseParticipants)
{
Console.WriteLine($"{countrywiseParticipant.Key} - {countrywiseParticipant.Value}");
}
3. Object-Oriented Programming in C#
Basic Class
Defining a simple class with getters and setters:
public class Contact
{
private string name;
public string GetName()
{
return name;
}
public void SetName(string name)
{
this.name = name;
}
}
public class Program
{
public static void Main()
{
var c = new Contact();
c.SetName("John");
Console.WriteLine(c.GetName()); // Output: John
}
}
Constructor and Properties
Using constructors and properties for initialization:
public class Contact
{
public string Name { get; set; }
public Contact(string name)
{
this.Name = name;
}
}
public class Program
{
public static void Main()
{
var c = new Contact("John");
Console.WriteLine(c.Name); // Output: John
c.Name = "George";
Console.WriteLine(c.Name); // Output: George
}
}
Overriding Methods
Overriding the ToString
method:
public class Contact
{
public string Name { get; set; }
public Contact(string name)
{
this.Name = name;
}
public override string ToString()
{
return $"Contact({Name})";
}
}
public class Program
{
public static void Main()
{
var c = new Contact("John");
Console.WriteLine(c); // Output: Contact(John)
c.Name = "George";
Console.WriteLine(c); // Output: Contact(George)
}
}
Inheritance
Using inheritance to extend functionality:
public class Person
{
public string Name { get; set; }
public Person(string name)
{
Console.WriteLine("The Person constructor is called");
Name = name;
}
public override string ToString()
{
return $"Person({Name})";
}
}
public class Employee : Person
{
public Employee(string name) : base(name)
{
Console.WriteLine("The rest of the Employee constructor runs now");
}
}
public class Program
{
public static void Main()
{
new Employee("John");
}
}
4. Interfaces in C#
Basic Interface Implementation
Defining and implementing interfaces:
interface IHello
{
void SayHello();
}
public class Hello : IHello
{
public void SayHello()
{
Console.WriteLine("Hello");
}
}
public class Program
{
public static void Main()
{
IHello myHello = new Hello();
myHello.SayHello(); // Output: Hello
}
}
Multiple Interfaces
Implementing multiple interfaces with different methods:
interface IHello
{
void SayHello();
}
interface IHello2
{
void SayHello(string name);
}
public class Hello : IHello, IHello2
{
public void SayHello()
{
Console.WriteLine("Hello");
}
public void SayHello(string name)
{
Console.WriteLine($"Hello {name}");
}
}
public class Program
{
public static void Main()
{
var h = new Hello();
h.SayHello(); // Output: Hello
h.SayHello("George"); // Output: Hello George
}
}
Explicit Interface Implementation
Handling methods with the same signature in different interfaces:
interface IHello
{
void SayHello();
}
interface IHello2
{
void SayHello();
}
public class Hello : IHello, IHello2
{
void IHello.SayHello()
{
Console.WriteLine("IHello's Hello");
}
void IHello2.SayHello()
{
Console.WriteLine("IHello2's Hello");
}
}
public class Program
{
public static void Main()
{
var h = new Hello();
((IHello)h).SayHello(); // Output: IHello's Hello
((IHello2)h).SayHello(); // Output: IHello2's Hello
}
}
5. Understanding Value Equality vs Reference Equality in C#
When working with objects in C#, it’s crucial to understand the difference between value equality and reference equality. This concept determines how two objects are compared, either by their memory location or by their data.
Reference Equality (==
Operator)
The ==
operator in C# checks if two objects refer to the same location in memory. If they do, they are considered equal. Otherwise, they are not, even if the data they hold is identical.
Example 1: Different Objects in Memory
class Contact
{
public string? Name { get; set; }
}
public class Program
{
public static void Main()
{
Contact c1 = new Contact { Name = "John" };
Contact c2 = new Contact { Name = "John" };
Console.WriteLine(c1 == c2); // Output: False
}
}
In this example, c1
and c2
are two different objects in memory, so c1 == c2
returns false
.
Example 2: Same Object Reference
class Contact
{
public string? Name { get; set; }
}
public class Program
{
public static void Main()
{
Contact c1 = new Contact { Name = "John" };
Contact c2 = c1;
Console.WriteLine(c1 == c2); // Output: True
}
}
Here, c2
is a reference to c1
, meaning they are the same object in memory. Therefore, c1 == c2
returns true
.
Value Equality (.Equals
Method)
The .Equals
method checks if two objects have the same data. By default, it behaves like the ==
operator unless overridden.
Example 1: Default .Equals
Implementation
class Contact
{
public string? Name { get; set; }
}
public class Program
{
public static void Main()
{
Contact c1 = new Contact { Name = "John" };
Contact c2 = new Contact { Name = "John" };
Console.WriteLine(c1.Equals(c2)); // Output: False
}
}
Without overriding, .Equals
checks for reference equality, so c1.Equals(c2)
returns false
.
Example 2: Custom .Equals
Implementation
class Contact
{
public string? Name { get; set; }
public override bool Equals(object? obj)
{
if (obj is Contact)
{
return Name == ((Contact)obj).Name;
}
return this == obj;
}
}
public class Program
{
public static void Main()
{
Contact c1 = new Contact { Name = "John" };
Contact c2 = new Contact { Name = "John" };
Console.WriteLine(c1.Equals(c2)); // Output: True
}
}
By overriding .Equals
, we can compare the Name
properties, so c1.Equals(c2)
returns true
.
Understanding HashCodes
A hashcode is a numerical value used to identify objects during operations like inserting them into a hash table. Even if two objects are equal in terms of their content, their hashcodes can differ unless overridden.
Example: Comparing Hashcodes
class Contact
{
public string? Name { get; set; }
}
public class Program
{
public static void Main()
{
Contact c1 = new Contact { Name = "John" };
Contact c2 = new Contact { Name = "John" };
Contact c3 = c1;
Console.WriteLine($"Hashcode of c1: {c1.GetHashCode()}");
Console.WriteLine($"Hashcode of c2: {c2.GetHashCode()}");
Console.WriteLine($"Hashcode of c3: {c3.GetHashCode()}");
c2.Name = "John Doe";
Console.WriteLine($"Hashcode of c2: {c2.GetHashCode()}");
}
}
In this example:
c1
andc2
initially have different hashcodes.c3
shares the same hashcode asc1
because it refers to the same object.- Changing the name of
c2
does not change its hashcode.
Passing Objects to Methods
When you pass an object to a method, the method receives a reference to the object. Changes made to the object within the method are reflected outside the method because the reference points to the same memory location.
Example: Modifying an Object in a Method
class Contact
{
public string? Name { get; set; }
}
public class Program
{
private static void modifyName(Contact c, string name)
{
c.Name = name;
}
public static void Main()
{
Contact c1 = new Contact { Name = "John" };
modifyName(c1, "John Doe");
Console.WriteLine(c1.Name); // Output: John Doe
}
}
Swapping Objects
Swapping two object references within a method does not swap the original objects’ references outside the method.
Example: Swapping References
class Contact
{
public string? Name { get; set; }
}
public class Program
{
private static void swapContacts(Contact x1, Contact x2)
{
Contact tempContact = x1;
x1 = x2;
x2 = tempContact;
}
public static void Main()
{
Contact c1 = new Contact { Name = "John" };
Contact c2 = new Contact { Name = "George" };
swapContacts(c1, c2);
Console.WriteLine(c1.Name); // Output: John
}
}
In this case, c1
and c2
are not swapped outside the method since the method only swaps the references locally.
HashCode Consistency
The hashcode of an object remains consistent between the caller and the callee when passed to a method.
Example: Hashcode in Caller and Callee
class Contact
{
public string? Name { get; set; }
}
public class Program
{
static void PrintHashCode(Contact c)
{
Console.WriteLine(c.GetHashCode());
}
public static void Main()
{
Contact c1 = new Contact { Name = "John" };
Console.WriteLine(c1.GetHashCode());
PrintHashCode(c1);
}
}
The hashcode printed in both the Main
method and the PrintHashCode
method will be the same, as they refer to the same object.
6. Understanding Structs in C#
What is a Struct?
In C#, a struct
is a value type that is stored on the stack, unlike reference types which are stored on the heap. Structs are particularly useful when you need a lightweight object that does not require the overhead of a class.
Defining a Struct
You can define a struct using the struct
keyword. Below is an example of a simple Contact
struct:
struct Contact
{
public string? Name { get; set; }
// We can have methods in the struct just like classes
public void PrintName()
{
Console.WriteLine($"Contact.PrintName: {Name}");
}
}
In the example above, the Contact
struct has a property Name
and a method PrintName
.
Working with Structs
When you assign one struct to another, it is copied by value. This means that modifying the copy does not affect the original struct.
public class Program
{
static void ChangeName(Contact c, string name)
{
Console.WriteLine(c.GetHashCode());
c.Name = name;
}
public static void Main()
{
Contact c1 = new Contact { Name = "John" };
Contact c2 = new Contact { Name = "John" };
// When we assign a struct object to another variable, it's a copy by value
Contact c3 = c1;
Console.WriteLine(c1.Equals(c2));
Console.WriteLine($"Hashcode of c1: {c1.GetHashCode()}");
Console.WriteLine($"Hashcode of c2: {c2.GetHashCode()}");
Console.WriteLine($"Hashcode of c3: {c3.GetHashCode()}");
// Invoking the methods of the struct
c1.PrintName();
// Modifying the fields, modifies its hashcode
c1.Name = "John Doe";
Console.WriteLine($"Hashcode of c1: {c1.GetHashCode()}");
// Modifying c1 does not modify c3, because it's a different copy
Console.WriteLine(c3.Name);
Console.WriteLine($"Hashcode of c3: {c3.GetHashCode()}");
// Passing a struct to a method passes it by value, so changes are not reflected outside
ChangeName(c1, "John Doe");
Console.WriteLine(c1.Name);
}
}
Limitations of Structs
- Equality Comparison: You cannot use the
==
operator with structs unless you override it.
7. Readonly Structs
Readonly Fields in Structs
You can make fields in a struct readonly, meaning they can only be assigned during the struct's initialization and cannot be modified later.
struct Contact
{
public readonly string? Name { get; }
public Contact(string? name)
{
Name = name;
}
// This will throw an error because Name is readonly
void SetName(string? name)
{
this.Name = name;
}
public void PrintName()
{
Console.WriteLine($"Contact.PrintName: {Name}");
}
}
Mixing Readonly and Writable Fields
It's possible to have a mix of readonly and writable fields within a struct.
struct Contact
{
public readonly string? Name { get; }
public string? Email { get; set; }
public Contact(string? name)
{
Name = name;
}
}
Readonly Structs
When an entire struct is marked as readonly, all its fields must be readonly as well.
readonly struct Contact
{
public readonly string? Name { get; }
public readonly string? Email { get; }
public Contact(string? name)
{
Name = name;
}
}
Examples in the .NET Framework
Many primitive data types, such as int
, are readonly structs.
public readonly struct Int32
{
// Structure of Int32
}
8. Enums in C#
What is an Enum?
An enum
(short for enumeration) is a special value type that allows you to define a group of named constants.
enum Color
{
Red,
Green,
Blue
}
public class Program
{
public static void Main()
{
Color color = Color.Blue;
Console.WriteLine(color);
Console.WriteLine((int)color);
}
}
In the example above, Color
is an enum with three possible values: Red
, Green
, and Blue
. You can cast an enum to its underlying integer value.
9. Records in C#
What is a Record?
A record
is a reference type that provides built-in functionality for value equality, meaning two records with the same values are considered equal.
record Contact
{
public string? Name { get; set; }
// We can have methods in records as well
public void PrintName()
{
Console.WriteLine($"Contact.PrintName: {Name}");
}
}
Working with Records
Records allow you to use the ==
operator for equality checks, unlike structs.
public class Program
{
public static void Main()
{
Contact c1 = new Contact { Name = "John" };
c1.PrintName();
Contact c2 = new Contact { Name = "John" };
Console.WriteLine($"Hashcode of c1: {c1.GetHashCode()}");
Console.WriteLine($"Hashcode of c2: {c2.GetHashCode()}");
Console.WriteLine(c1 == c2); // == works with records
}
}
10. Handling Exceptions in C#
Exception handling is crucial for building robust applications. This section covers how to throw and catch exceptions in C#.
Basic Exception Handling
-
Throwing an exception:
The following code demonstrates a method that throws an exception when an error occurs:
public class Program { public static void Foo() { throw new Exception("Exception occurred!"); } public static void Main() { Foo(); } }
If the exception is not handled, it will result in a runtime error.
-
Catching an exception:
To handle the exception, wrap the method call in a
try-catch
block:public class Program { public static void Main() { try { Foo(); } catch (Exception) { Console.WriteLine("Exception occurred when calling Foo"); } } }
-
Accessing exception details:
You can access the exception details using the
Exception
object:public class Program { public static void Main() { try { Foo(); } catch (Exception e) { Console.WriteLine($"Exception occurred when calling Foo. Callee's message: {e.Message}"); } } }
Custom Exceptions
Custom exceptions allow you to create specific error types for your application.
-
Creating a custom exception:
public class MyException : Exception { public MyException(string message) : base(message) { } }
-
Using the custom exception:
public class Program { public static void Main() { try { Foo(); } catch (MyException e) { Console.WriteLine($"Exception occurred when calling Foo. Callee's message: {e.Message}"); } } public static void Foo() { throw new MyException("Exception occurred!"); } }
Multiple Catch Blocks
You can use multiple catch
blocks to handle different types of exceptions:
public class NegativeNumberException : Exception
{
public NegativeNumberException(string message) : base(message)
{
}
}
public class Program
{
public static void Main()
{
try
{
SendOnlyPositiveNumber(-10);
}
catch (NegativeNumberException e)
{
Console.WriteLine(e.Message);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Console.WriteLine("End of program");
}
public static int SendOnlyPositiveNumber(int x)
{
if (x < 0)
{
throw new NegativeNumberException($"Number {x} < 0");
}
return x;
}
}
The finally
Block
The finally
block is executed regardless of whether an exception is thrown:
public class Program
{
public static void Main()
{
try
{
AnotherMethod(10);
AnotherMethod(-10);
}
catch
{
Console.WriteLine("The catch in the Main method");
}
}
public static void AnotherMethod(int x)
{
try
{
var y = SendOnlyPositiveNumber(x);
Console.WriteLine(y);
}
catch (NegativeNumberException e)
{
Console.WriteLine(e.Message);
throw;
}
finally
{
Console.WriteLine("finally in AnotherMethod");
}
}
public static int SendOnlyPositiveNumber(int x)
{
if (x < 0)
{
throw new NegativeNumberException($"Number {x} < 0");
}
return x;
}
}
Exception Filters
Exception filters allow you to catch exceptions based on certain conditions:
public class Program
{
public static void Main()
{
try
{
Foo(0);
}
catch (Exception e) when (e.Message.Contains(">"))
{
Console.WriteLine($"First block: {e.Message}");
}
catch (Exception e)
{
Console.WriteLine($"Another block: {e.Message}");
}
}
public static void Foo(int num)
{
if (num < 10)
throw new Exception("num < 10");
else
throw new Exception("num >= 10");
}
}
11. Working with Nullable and Non-Nullable Types
C# provides mechanisms to handle null
values effectively.
Nullable Properties
A property can be made nullable by appending a question mark (?
) to its type:
public class Contact
{
public string? Name { get; set; }
}
Handling Null Values
To safely access methods of possibly null
valued fields:
-
Using conditional operators:
var contactNameInLowerCase = contact.Name == null ? null : contact.Name.ToLower();
-
Using null-conditional operator (
?.
):Console.WriteLine($"The contact's name in lowercase is: {contact.Name?.ToLower()}");
-
Using null-coalescing operator (
??
):Console.WriteLine($"The contact's name in lowercase is: {contact.Name?.ToLower() ?? "unknown"}");
This is a shorthand for:
var contactNameInLowerCase = contact.Name == null ? "unknown" : contact.Name.ToLower();
12. Generics
Introduction to Generics
Generics in C# are a powerful feature that allows you to create classes, methods, and data structures that can work with any data type. The main advantage of using generics is that they provide compile-time type safety. This means that the type of data being passed is checked during compilation, reducing the risk of runtime errors.
var l = new List<int>();
l.Add("John"); // This will cause a compile-time error
In the example above, trying to add a string to a list of integers will generate a compile-time error.
Generic Methods and Classes
A generic method is a method that is declared with a type parameter. This allows the method to be more flexible, accepting arguments of various types.
public static T Add<T>(T x, T y)
{
if (typeof(T) == typeof(int))
{
return (T)(object)(Convert.ToInt32(x) + Convert.ToInt32(y));
}
else if (typeof(T) == typeof(string))
{
return (T)(object)(Convert.ToString(x) + Convert.ToString(y));
}
throw new Exception("Pass either int or string as input");
}
In this example, the Add
method can work with both integers and strings, providing flexibility while maintaining type safety.
Implementing a Generic Map Function
Generics can also be used in data structures and algorithms. Here's an example of a generic map function:
public class Program
{
delegate T2 Operation<T1, T2>(T1 x);
private static IList<T2> MyMap<T1, T2>(IList<T1> inputList, Operation<T1, T2> op)
{
var outputList = new List<T2>();
foreach (var item in inputList)
{
outputList.Add(op(item));
}
return outputList;
}
}
This MyMap
function applies an operation to each item in a list and returns a new list with the results.
13. Generic Type Constraints
C# allows you to impose constraints on the types that can be used with a generic class or method. Some common constraints include:
where T : class
- T must be a reference type.where T : struct
- T must be a value type.where T : new()
- T must have a parameterless constructor.where T : someBaseType
- T must derive fromsomeBaseType
.
Example: Generic Instance Creation
public class Program
{
public static T CreateInstance<T>() where T : new()
{
return new T();
}
public static void Main()
{
Contact contact = CreateInstance<Contact>();
contact.Name = "John";
Console.WriteLine(contact.Name);
}
}
This method creates an instance of a type that has a parameterless constructor.
14. Reflection
Exploring Methods with Reflection
Reflection in C# allows you to inspect and interact with objects and types at runtime. For example, you can list all methods in a class using reflection:
using System.Reflection;
class MyClass
{
public void Method1() { }
void Method2() { }
private void Method3() { }
}
public class Program
{
public static void Main()
{
Type t = typeof(MyClass);
foreach(var method in t.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
Console.WriteLine(method.Name);
}
}
}
Invoking Constructors with Reflection
You can use reflection to invoke constructors dynamically:
using System.Reflection;
public class Contact
{
public string? Name { get; set; }
public Contact(string name) { Name = name; }
}
public class Program
{
public static T CreateInstance<T>(string param) where T : class
{
Type? t = typeof(T);
ConstructorInfo c = t?.GetConstructor(new[] { typeof(string) });
return (T)c.Invoke(new object[] { param });
}
public static void Main()
{
Contact contact = CreateInstance<Contact>("John");
Console.WriteLine(contact.Name);
}
}
15. Attributes
Introduction to Attributes
Attributes in C# provide a way to add metadata to your code. This metadata can be accessed at runtime using reflection.
public class MyClass
{
[Obsolete("Use NewMethod instead", false)]
public static void SomeDeprecatedMethod() { }
public static void NewMethod() { }
}
public class Program
{
public static void Main()
{
MyClass.SomeDeprecatedMethod();
}
}
In this example, the [Obsolete]
attribute marks a method as deprecated.
Custom Attributes
You can define your custom attributes to add specific metadata to your classes or methods:
public class DeveloperInfoAttribute : Attribute
{
public string? Developer { get; set; }
}
[DeveloperInfo(Developer = "John")]
public class Program
{
public static void Main() { }
}
16. Indexers
Indexers allow you to use objects like arrays. They are defined using the this
keyword:
public class ShoppingCart
{
private int[] prices = new int[10];
public int this[int index]
{
get { return prices[index]; }
set { prices[index] = value; }
}
}
17. Extension Methods
Introduction to Extension Methods
Extension methods allow you to add new methods to existing types without modifying them. These methods are defined in a static class:
public static class IntExtension
{
public static bool IsEven(this int number)
{
return number % 2 == 0;
}
}
public class Program
{
public static void Main()
{
int i = 10;
Console.WriteLine(i.IsEven());
}
}