The C# Player's Guide by RB Whitaker
Hello World your first program
Quote
"You'll spend more time reading code than writing it. Do yourself a favor and go out of your way to make code easy to understand, regardless of what the compiler will tolerate."
The C# Type System
Integer types
- 1 byte = 8 bits
- so 1 byte can represent 2^8 = 256 different values
- u= unsigned, s= signed.
- For most type u prefixes indicate that the type can only represent non-negative values.
- The byte type is bit exception as it is unsigned by default. the byte type have signed counterpart sbyte.
| Type | Size (bytes) | Min | Max |
|---|---|---|---|
| byte | 1 | 0 | 255 |
| sbyte | 1 | -128 | 127 |
| short | 2 | -32,768 | 32,767 |
| ushort | 2 | 0 | 65,535 |
| int | 4 | -2,147,483,648 | 2,147,483,647 |
| uint | 4 | 0 | 4,294,967,295 |
| long | 8 | -9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 |
| ulong | 8 | 0 | 18,446,744,073,709,551,615 |
Tip
On modern computer using byte instead of int for small numbers may actually be slower because of the way the CPU handles memory. If you use byte to store value then the JIT compiler often loads bytes into registers as 32-bit values anyway. so when you use byte, short, or long instead of int, the CPU has to do extra work to convert the value to a size it can handle efficiently. Using byte only makes sense when you have a large array like storing pixel colors for 1920x1080 image. So best practice is to use int unless you have a specific reason to use a different type.
The Digit Separator
- For larger numbers, you can use the digit separator to make them more readable.
- Since we can't use commas in numeric literals, we can use the underscore character (_) instead.
Text Types
- C# has two primary text types: char and string.
- The char type represents a single Unicode character and is enclosed in single quotes (' ').
- The string type represents a sequence of characters and is enclosed in double quotes (" ").
- You can use Unicode escape sequences to represent characters that may not be easily typed on a keyboard.
char letterA = 'A';
char aLetter = '\u0061';
char baseball = 'âšľ';
string greeting = "Hello, World!";
Floating-Point Types
| Type | Bytes | Min | Max | Decimals |
|---|---|---|---|---|
| float | 4 | ±1.0 x 10^-45 | ±3.4 x 10^38 | 7 |
| double | 8 | ±5.0 x 10^-324 | ±1.7 x 10^308 | 15 |
| decimal | 16 | ±1.0 x 10^-28 | ±7.9 x 10^28 | 28 |
double number1 = 3.5623;
// Scientific notation by employing 'e' to represent powers of 10
double avogadrosNumber = 6.022e23;
float number2 = 3.5623f;
decimal number3 = 3.5623m;
Tip
Similar to integer types, use double as default floating-point type unless you have a specific reason to use float or decimal. Moder JIT compiler optimizes double operations well on modern CPUs. Use float when working with large arrays of floating-point numbers to save memory, such as in graphics programming. Use decimal for financial calculations where precision is crucial to avoid rounding errors.
Boolean Type
- The bool type represents a value that can be either true or false.
Info
The bool type gets its name from the mathematician George Boole, who developed Boolean algebra, a branch of mathematics that deals with true and false values.
Math
Prefix and Postfix Operators
int x;
x = 5;
int y= ++x;
Console.WriteLine($"x={x}, y={y}");
// Output: x=6, y=6
x= 5;
int z= x++;
Console.WriteLine($"x={x}, z={z}");
// Output: x=6, z=5
Type Casting
- Implicit Casting (automatically) - converting a smaller type to a larger type size
- Explicit Casting (manually) - converting a larger type to a smaller size type
Arrays
Indexing from the end
- You can use the ^ operator to index from the end of an array.
int[] numbers = { 10, 20, 30, 40, 50 };
int lastNumber = numbers[^1]; // 50
int secondLastNumber = numbers[^2]; // 40
Ranges
- You can use the .. operator to create ranges in arrays.
int[] numbers = { 10, 20, 30, 40, 50 };
int[] subArray = numbers[1..4]; // { 20, 30, 40 }
int[] fromStart = numbers[..3]; // { 10, 20, 30
int[] toEnd = numbers[2..]; // { 30, 40, 50 }
int[] allNumbers = numbers[..]; // { 10, 20, 30, 40, 50 }
int[] lastTwo = numbers[^2..]; // { 40, 50 }
int[] middleNumbers = numbers[1..^1]; // { 20, 30, 40 }
Null References
Null Conditinal Operator
- The null-conditional operator (?.) allows you to safely access members of an object that might be null.
string? nullableString = null;
int? length = nullableString?.Length; // length will be null
Console.WriteLine(length); // Output:
private string? GetTopPlayerName()
{
return _scoreManager?.GetScores()?[0]?.Name;
// Returns null if _scoreManager or GetScores() is null
}
The Null-Coalescing Operator
- The null-coalescing operator (??) allows you to provide a default value when a nullable type is null.
string? userInput = null;
string displayText = userInput ?? "Default Value";
Console.WriteLine(displayText); // Output: Default Value
The Null Forgiving Operator
- The null-forgiving operator (!) tells the compiler that you are sure a nullable reference type is not null at that point in the code.
- Use this operator with caution, as it can lead to runtime exceptions if the value is actually null.
string? nullableString = "Hello, World!";
int length = nullableString!.Length; // Tells the compiler that nullableString is not null
Console.WriteLine(length); // Output: 13
Records
What is a Record?
- A record is a reference type that provides built-in functionality for encapsulating data.
- Records are immutable by default, meaning their properties cannot be changed after creation.
- Records provide value-based equality, meaning two record instances with the same data are considered equal.
Creating a Record
- You can define a record using the
recordkeyword. - Here is an example of creating a simple record to represent a person:
- Single line of this code gives you
- Constructor
- Name and Age properties
- Equals() value-based equality method
- GetHashCode() method
- Deconstruct() method
public record person(string Name, int Age);
var person1 = new person("Alice", 30);
console.WriteLine(person1.Name); // Output: Alice
console.WriteLine(person1.Age); // Output: 30
var person2 = new person("Alice", 30);
Console.WriteLine(person1 == person2); // Output: True
With Expressions
- You can create a new record instance based on an existing one using the with expression.
var person1 = new person("Alice", 30);
var person2 = person1 with { Age = 60 };
Console.WriteLine(person2.Age); // Output: 60
Use Cases for Records
- Records are ideal for representing data models(Results, Configurations) (Class that primarily hold data).
- It's good alternative to tuples when you want to give meaningful names to the data fields.
- So you can use with dictionarirs, lists, or hashsets to store collections of records.
- Keep in mind that when you compare two records, the comparison is based on their values, not their references. So only use record for immutable data.
Difference between Record, Class, and Struct
- Class: Classes are reference types with reference semantics. You operate on them at the reference level because you care about the particular instance.
- Struct: Structs are value types with value semantics. You operate on them at the value level because you care about the data they hold.
- Record: Records are reference types with value semantics. You operate on them at the value level because you care about the data they hold, not the particular instance.
Delegates
What is a Delegate?
- Delegate is a variable that can hold a reference to a method.
- They allow you to pass methods as parameters.
How to use Delegates?
var calculator = new Calculator();
var squareValue= calculator.PerformOperation(5, x => x * x);
Console.WriteLine(squareValue);
var cubeValue= calculator.PerformOperation(5, x => x * x * x);
Console.WriteLine(cubeValue);
public class Calculator
{
public delegate int NumberOperation(int x);
public int PerformOperation(int x, NumberOperation operation)
{
return operation(x);
}
}
Action Delegate
- Action delegate is a predefined delegate type that represents a method that takes parameters but does not return a value.
- You can use Action
for 1 parameter, Action for 2 parameters, and so on up to 16 parameters.
var calculator = new Calculator();
calculator.PrintMessage("Hello from Action delegate!", msg => Console.WriteLine(msg));
public class Calculator
{
public void PrintMessage(string message, Action<string> messageOperation)
{
messageOperation(message);
}
}
Function Delegate
- Func delegate is a predefined delegate type that represents a method that takes parameters and returns a value.
- You can use Func
for 1 parameter, Func for 2 parameters, and so on up to 16 parameters plus return type.
Predicate Delegate
- Predicate delegate is a predefined delegate type that represents a method that takes a single parameter and returns a boolean value.
var calculator = new Calculator();
var isEven = calculator.PerformCheck(4, x => x % 2 == 0);
Console.WriteLine($"Is 4 even? {isEven}");
public class Calculator
{
public bool PerformCheck(int x, Predicate<int> check)
{
return check(x);
}
}
Events
What is an Event?
- In C#, events are a mechanism that allows an object to notify others that something has changed or happened so they can respond.
- An event is a way for a class to notify other classes or objects when something of interest occurs.
- Events are based on delegates, which are used to define the signature of the event handler methods.
How to use Events?
var ship = new Ship();
//subscribe to the ShipExploded event with different handlers
ship.ShipExploded += () => Console.WriteLine("The ship has exploded!");
ship.ShipExploded += () => Console.WriteLine("Game Over!");
ship.ShipExploded += () => Console.WriteLine("Restarting level...");
Console.WriteLine($"Ship Health: {ship.Health}");
ship.TakeDamage(50);
Console.WriteLine("Ship is taking 50 damage.");
Console.WriteLine($"ship Health: {ship.Health}");
ship.TakeDamage(25);
Console.WriteLine("Ship is taking 25 damage.");
Console.WriteLine($"ship Health: {ship.Health}");
ship.TakeDamage(50);
Console.WriteLine("Ship is taking 50 damage.");
Console.WriteLine($"ship Health: {ship.Health}");
public class Ship
{
public event Action? ShipExploded;
public int Health { get; private set; } = 100;
public void TakeDamage(int damage)
{
Health -= damage;
if (Health <= 0)
{
ShipExploded?.Invoke();
}
}
}
Events with Parameters
using System.Drawing;
var ship = new Ship();
//subscribe to the ShipExploded event with different handlers
ship.ShipExploded += (point) => Console.WriteLine($"The ship has exploded! at {point}");
ship.ShipExploded += (point) => Console.WriteLine("Game Over!");
ship.ShipExploded += (point) => Console.WriteLine("Restarting level...");
Console.WriteLine($"Ship Health: {ship.Health}");
ship.TakeDamage(50);
Console.WriteLine("Ship is taking 50 damage.");
Console.WriteLine($"ship Health: {ship.Health}");
ship.TakeDamage(25);
Console.WriteLine("Ship is taking 25 damage.");
Console.WriteLine($"ship Health: {ship.Health}");
ship.TakeDamage(50);
Console.WriteLine("Ship is taking 50 damage.");
Console.WriteLine($"ship Health: {ship.Health}");
public class Ship
{
public event Action<Point>? ShipExploded;
public int Health { get; private set; } = 100;
public Point Location { get; private set; } = new Point(0, 0);
public void TakeDamage(int damage)
{
Health -= damage;
if (Health <= 0)
{
ShipExploded?.Invoke(Location);
}
}
}
EventHandler
- if you don't need any arguments for your event, you can use the built-in EventHandler with EventArgs.
using System.Drawing;
var ship = new Ship();
//subscribe to the ShipExploded event with different handlers
ship.ShipExploded += (sender, e) => Console.WriteLine($"The ship has exploded! at {e.Location}");
ship.ShipExploded += (sender, e) => Console.WriteLine("Game Over!");
ship.ShipExploded += (sender, e) => Console.WriteLine("Restarting level...");
ship.TakeDamage(50);
ship.TakeDamage(25);
ship.TakeDamage(50);
public class Ship
{
public event EventHandler<ExplosionEventArgs>? ShipExploded;
public int Health { get; private set; } = 100;
public Point Location { get; set; } = new Point(0, 0);
public void TakeDamage(int damage)
{
Health -= damage;
if (Health <= 0)
{
ShipExploded?.Invoke(this, new ExplosionEventArgs(Location));
}
}
}
public class ExplosionEventArgs : EventArgs
{
public Point Location { get; }
public ExplosionEventArgs(Point location)
{
Location = location;
}
}
Event Leaks
- When you subscribe to an event, the publisher holds a reference to the subscriber.
- If the subscriber is not unsubscribed from the event, it can lead to memory leaks because the garbage collector cannot reclaim the memory used by the subscriber as long as the publisher holds a reference to it.
- To prevent event leaks, always unsubscribe from events when the subscriber is no longer needed.
- This is especially important in long-running applications or when dealing with events on objects with a longer lifetime than the subscribers. But you can ignore this for small apps or short-lived programs
Threads
What is a Thread?
- A thread is an independent execution path within a program.
- Every program has at least one thread, known as the main thread.
- Programs can create additional threads to perform tasks concurrently. when program does this it is called multithreaded application.
Creating a Thread
- Before creating thread Make sure that your code can run independently without relying on shared state or resources.
- If it's intertwined with other parts of the program, it does not make a good candidate for running in a separate thread.
- If it's too small in size, it won't worth the overhead of creating and managing a thread.
- Threds are expensive to create and manage, so avoid creating too many threads in your application.
- Threads are best suited for long-running tasks that can benefit from parallel execution.
System.Threading.Threadnamespace provides classes and methods for working with threads in C#.- In below code both threds write stuff in console window, but you cannot predict which thread will write first. Text
Main Thread Donemay appear before,after or in middle of the numbers from 1 to 100.
//Create new thread to count to 100
Thread thread = new Thread(CountTo100);
//Start the thread
thread.Start();
Console.WriteLine("Main Thread Done");
void CountTo100()
{
for (int i = 1; i <= 100; i++)
{
Console.WriteLine(i);
}
}
Waiting for a Thread to Complete
- You can use
Thread.Join()method to make main thread wait for the new thread to complete before proceeding. - You will not see
Main Thread Doneuntil the counting thread has finished counting to 100.
//Create new thread to count to 100
Thread thread = new Thread(CountTo100);
//Start the thread
thread.Start();
//Wait for the thread to finish
thread.Join();
Console.WriteLine("Main Thread Done");
Sharing Data Between Threads
var data= new MultiplicationData { A = 5, B = 10 };
//Create new thread to count to 100
Thread thread = new Thread(() => MultiplyNumbers(data));
//Start the thread
thread.Start();
//Wait for the thread to finish
thread.Join();
Console.WriteLine("Main Thread Done");
void MultiplyNumbers(MultiplicationData data)
{
data.Result = data.A * data.B;
Console.WriteLine($"Multiplication Result: {data.Result}");
}
class MultiplicationData
{
public double A { get; set; }
public double B { get; set; }
public double Result { get; set; }
}
Sleeping a Thread
- You can use
Thread.Sleep()method to pause a thread for a specified amount of time. Sleep()method is static and it makes the current thread sleep.- When you do this, thread will give up the rest of its currently scheduled time and won't get another change until after the time specified.
Console.WriteLine("Thread starting...");
Thread.Sleep(2000); // Sleep for 2 seconds
Console.WriteLine("Thread awake after 2 seconds.");
Thread Safety
- When multiple threads access shared data or resources, it can lead to race conditions and data corruption.
- To ensure thread safety, you can use synchronization mechanisms like locks, mutexes, or semaphores.
- The
lockstatement is a simple way to ensure that only one thread can access a block of code at a time.
SharedData sharedData = new SharedData();
Thread thread = new Thread(sharedData.Increment);
thread.Start();
sharedData.Increment();
thread.Join();
Console.WriteLine(sharedData.Number);
class SharedData
{
private readonly object _numberLock = new object();
private int _number;
public int Number
{
get
{
lock (_numberLock)
{
return _number;
}
}
}
public void Increment()
{
lock (_numberLock)
{
_number++;
}
}
}
Asynchronous Programming
What is Asynchronous Programming?
- Asynchronous programming allows your program to perform tasks without blocking the main thread.
Task
- In C#, Task represent a job that can run in background.
- C# uses two classes to represent tasks:
Task(void return) andTask<T>(returning a value of type T). - You can use it from background task like downloading a file, performing a long calculation without freezing the user interface.
- Task = "Work happening in the background that i can wait"
Task vs Thread
| Thread | Task |
|---|---|
| Low-level | High-level |
| Expensive | Lightweight |
| Manual management | Managed by runtime |
| One thread = one job | Tasks share thread pool |
Creating and Running a Task
- The
awaitkeyword is a convenient way to indicate that a method should asynchronously wait for a task to complete before proceeding. - You can only use
awaitkeyword with method marked as anasync. - When you use
await, the method is paused until the awaited task is finished, allowing other tasks to run in the meantime.
//Task with void return type
await DoWorkAsync();
Console.WriteLine("Completed");
async Task DoWorkAsync()
{
await Task.Delay(5000);
Console.WriteLine("Work done");
}
//Task with int return type
int result = await GetNumberAsync();
Console.WriteLine($"Completed with result: {result}");
async Task<int> GetNumberAsync()
{
await Task.Delay(500);
return 42;
}
Running Multiple Tasks in Parallel
Task<int> task1 = GetNumberAsync(1);
Task<int> task2 = GetNumberAsync(2);
int[] results = await Task.WhenAll(task1, task2);
Console.WriteLine($"Results: {results[0]}, {results[1]}");
async Task<int> GetNumberAsync(int id)
{
await Task.Delay(1000 * id);
return id * 10;
}
Task.Run
- You can use
Task.Runto run CPU-bound work on a background thread from the thread pool. - This will keep UI responsive by offloading heavy computations to a separate thread.
int result = await Task.Run(() => LongRunningCalculation());
Console.WriteLine($"Calculation result: {result}");
int LongRunningCalculation()
{
// Simulate a long calculation
Thread.Sleep(5000);
return 42;
}
Difference between async/await and Task.Run
| async/await | Task.Run |
|---|---|
| Used for I/O-bound operations | Used for CPU-bound operations |
| Non-blocking | Offloads work to thread pool |
| Keeps UI responsive | Keeps UI responsive |
| Easier to read and maintain | Useful for heavy computations |
Exception Handling in Async Methods
- You can use try-catch blocks to handle exceptions in async methods.
- When an exception occurs in an awaited task, it is propagated to the calling method.
try
{
int result = await GetNumberAsync();
Console.WriteLine($"Result: {result}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
async Task<int> GetNumberAsync()
{
await Task.Delay(1000);
throw new InvalidOperationException("Something went wrong!");
return 42;
}
Cancellation of Tasks
- You can use
CancellationTokento cancel a running task. - You create a
CancellationTokenSourceand pass its token to the task. - You can then call
Cancel()on the source to request cancellation.
var cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
var task = LongRunningOperationAsync(token);
// Cancel the task after 2 seconds
cts.CancelAfter(2000);
try
{
await task;
Console.WriteLine("Operation completed successfully.");
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was canceled.");
}
async Task LongRunningOperationAsync(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested();
Console.WriteLine($"Working... {i + 1}/10");
await Task.Delay(1000); // Simulate work
}
}
Other language features
Yield Keyword
- The
yieldkeyword is used in an iterator method to provide a value to the enumerator object or to signal the end of iteration. - When the
yield returnstatement is reached, the current location in the code is remembered, and execution is paused until the next value is requested. - This allows you to create custom collections that can be iterated over using
foreachloop without having to create an entire collection in memory.
IEnumerable<int> SingleDigits()
{
for (int number = 1; number <= 10; number++)
yield return number;
}
foreach (var number in SingleDigits())
{
Console.WriteLine(number);
}
yield breakstatement is used to end the iteration early.- Iterators do not have to ever complete; they can run indefinitely until
yield breakis called or the consumer stops requesting values.
List<int> numbers = AlternatingPattern().ToList();
// This will run indefinitely and likely crash your program
Attributes
- Attributes are a way to add metadata to your code.
- You can apply attributes to classes, methods, properties, and other code elements.
- Attributes are enclosed in square brackets (
[ ]) and placed above the code element they apply to. - All Attributes are classes derived from the
System.Attributebase class. you can also create your own custom attributes by defining a class that inherits fromSystem.Attribute.
Info
Attributes are usally created by people who want to work with yoru compiled code, including the people making the compiler, the .net runtime, and other development tools. They decide what metadata they need, design the attribute classes that will give them that metadata, and then provide you with documentation that tells you how to use them.
[Obsolete("This method is obsolete. Use NewMethod instead.")]
public void OldMethod()
{
// Method implementation
}
public void NewMethod()
{
// New method implementation
}
Tip
Attributes are like placing little notes on the different parts of your code. They Survive the compilation process, so tools working with your code can see these atributes and use them.
- The compiler notices the
[Obsolete]attribute and will give you a warning if you try to useOldMethod(), nudging you to switch toNewMethod()instead. - Attributes have parameters that allow you to provide additional information.
- In this case, the message "This method is obsolete. Use NewMethod instead." is passed to the
Obsoleteattribute to inform developers about the deprecation.