C# Part 3 – Flow Control and Iteration

In the 3rd part of our C# guide we will talk about flow control and iteration. Flow control mechanisms allow programmers to dictate how their code behaves under various conditions. It encompasses decision-making structures like if statements, which enable code to execute different instructions based on certain conditions.

This is essential for creating responsive and adaptable software that can react to different inputs and scenarios. Additionally, flow control includes switch statements, which provide a way to execute specific blocks of code based on the value of an expression, streamlining complex decision trees.

Moreover, Part 3 of C# covers iteration allowing developers to repeat a block of code multiple times, making it possible to perform tasks such as processing large data sets or implementing repetitive tasks efficiently. C# offers various mechanisms for iteration, including for loops, while loops, and foreach loops, each suited for different scenarios.

Flow Control and Iteration

C# Operators

C# operators are fundamental elements of the C# programming language used to perform various operations on data, variables, and values within your code. They can be classified into several categories, each serving a specific purpose:

Arithmetic Operators

These operators perform basic mathematical calculations.

  • Addition (+)
  • Subtraction (-)
  • Multiplication (*)
  • Division (/)
  • Modulus (%): Returns the remainder of a division operation.

Comparison Operators

Comparison operators are used to compare two values and return a Boolean result (true or false).

  • Equal (==)
  • Not Equal (!=)
  • Greater Than (>)
  • Less Than (<)
  • Greater Than or Equal To (>=)
  • Less Than or Equal To (<=)

Logical Operators

Logical operators are used to combine or manipulate Boolean expressions.

  • Logical AND (&&)
  • Logical OR (||)
  • Logical NOT (!)

Assignment Operators

These operators are used to assign values to variables and often combine assignment with another operation.

  • Assignment (=)
  • Addition Assignment (+=)
  • Subtraction Assignment (-=)
  • Multiplication Assignment (*=)
  • Division Assignment (/=)
  • Modulus Assignment (%=)

Bitwise Operators

Bitwise operators manipulate individual bits of integer types.

  • Bitwise AND (&)
  • Bitwise OR (|)
  • Bitwise XOR (^)
  • Bitwise NOT (~)
  • Left Shift (<<)
  • Right Shift (>>)

Conditional Operator (Ternary Operator)

This operator allows you to assign a value to a variable based on a condition.

  • Conditional (ternary) Operator (?:)

Null Coalescing Operator

It helps handle nullable types by providing a default value if a nullable value is null.

  • Null Coalescing Operator (??)

Type Operators

These operators are used to check and manipulate object types.

  • Type Of (typeof)
  • As Operator (as)
  • Is Operator (is)

Miscellaneous Operators

C# also includes other operators for specific purposes.

  • Member access operator (.)
  • Index operator ([])
  • Pointer operators (* and ->) used in unsafe code.

Understanding and using these operators effectively is essential for writing C# code that performs calculations, makes decisions, and manages data efficiently. They are fundamental tools in programming, allowing you to manipulate data and control the flow of your program.

Control Flow in C#

Control flow in C# refers to the order in which statements and instructions are executed within a program. It determines how a program’s logic progresses and how decisions are made. C# offers various control flow structures to manage the flow of execution, allowing you to create dynamic and responsive applications.

Sequential Execution

In a typical program, statements are executed sequentially, one after the other, from top to bottom. This is the default control flow when no branching or looping is involved.

Conditional Statements

Conditional statements allow you to make decisions and execute different code blocks based on conditions.

If Statement

if: Executes a block of code if a specified condition is true.

else: Specifies an alternative code block to execute if the condition is false.

else if: Allows for multiple conditions to be checked sequentially.

using System;
class Program
{
  static void Main()
  {
      // Prompt the user for their exam score
      Console.Write("Enter your exam score: ");
      int examScore = int.Parse(Console.ReadLine());
  
      // Determine the grade based on the exam score
      if (examScore >= 90)
      {
          Console.WriteLine("Grade: A");
      }
      else if (examScore >= 80)
      {
          Console.WriteLine("Grade: B");
      }
      else if (examScore >= 70)
      {
          Console.WriteLine("Grade: C");
      }
      else if (examScore >= 60)
      {
          Console.WriteLine("Grade: D");
      }
      else
      {
          Console.WriteLine("Grade: F");
      }
  
      Console.ReadLine(); // Pause to see the result
  }
  }

Switch Statement

switch: Provides a way to select one of several code blocks to execute based on the value of an expression.

using System;
class Program
{
  static void Main()
  {
      Console.Write("Enter a number (1-7): ");
      int dayNumber = int.Parse(Console.ReadLine());

      string dayName;

      switch (dayNumber)
      {
        case 1:
            dayName = "Sunday";
            break;
        case 2:
            dayName = "Monday";
            break;
        case 3:
            dayName = "Tuesday";
            break;
        case 4:
            dayName = "Wednesday";
            break;
        case 5:
            dayName = "Thursday";
            break;
        case 6:
            dayName = "Friday";
            break;
        case 7:
            dayName = "Saturday";
            break;
        default:
            dayName = "Invalid day number";
            break;
    }

    Console.WriteLine($"The day is {dayName}.");
    Console.ReadLine();
  }
}

Looping Statements

Looping statements allow you to repeat a block of code multiple times until a specific condition is met.

While Statement

while: Repeats a block of code while a specified condition is true.

using System;

class Program
{
    static void Main()
    {
        int count = 1;

        while (count <= 5)
        {
            Console.WriteLine(count);
            count++;
        }

        Console.ReadLine();
    }
}

Do-While Statement

do-while: Repeats a block of code at least once and continues as long as a specified condition is true.

using System;

class Program
{
    static void Main()
    {
        string correctPassword = "1234";
        string userInput;
        
        do
        {
            Console.Write("Enter the password: ");
            userInput = Console.ReadLine();

        } while (userInput != correctPassword);

        Console.WriteLine("Access granted!");
        Console.ReadLine();
    }
}

For Statement

for: Executes a block of code for a specified number of iterations.

using System;

class Program
{
    static void Main()
    {
        for (int i = 1; i <= 5; i++)
        {
            Console.WriteLine(i);
        }

        Console.ReadLine();
    }
}

Foreach Statement

foreach: Iterates over elements in a collection, such as an array or a list.

using System;

class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3, 4, 5 };

        foreach (int number in numbers)
        {
            Console.WriteLine(number);
        }

        Console.ReadLine();
    }
}

Branching Statements

Branching statements allow you to control the flow of execution by transferring control to a different part of the code.

break: Used in loops and switch statements to exit the loop or switch block.

continue: Skips the current iteration and proceeds to the next iteration in a loop.

return: Exits a method and optionally returns a value to the caller.

goto (avoided in most cases): Transfers control to a labeled statement within the code. It’s generally discouraged due to potential code complexity and readability issues.

class Program
{
    static void Main(string[] args)
    {
        bool stop = false;
        while (stop == false)
        {
            Console.WriteLine("Menu:");
            Console.WriteLine("1. Option 1");
            Console.WriteLine("2. Option 2");
            Console.WriteLine("3. Exit");

            Console.Write("Enter your choice: ");
            int choice = int.Parse(Console.ReadLine());

            switch (choice)
            {
                case 1:
                    Console.WriteLine("You selected Option 1.");
                    // Using 'continue' to return to the menu
                    Console.WriteLine("Press Enter to continue...");
                    Console.ReadLine();
                    continue;

                case 2:
                    Console.WriteLine("You selected Option 2.");
                    // Using 'goto' to jump to a specific label
                    goto case 1;

                case 3:
                    Console.WriteLine("Exiting the program.");
                    // Using 'break' to exit the loop
                    stop = true;
                    break;

                default:
                    Console.WriteLine("Invalid choice. Try again.");
                    // Using 'return' to restart the program
                    Console.WriteLine("Press Enter to restart...");
                    Console.ReadLine();
                    return;
            }
        }

        Console.WriteLine("Program ended.");
        Console.ReadKey();
    }
}

Exception Handling

Exception handling is crucial for managing unexpected errors and exceptions in your code. C# provides try-catch-finally blocks for handling exceptions gracefully and ensuring proper resource cleanup.

using System;

class Program
{
    static void Main(string[] args)
    {
        try
        {
            Console.Write("Enter a number: ");
            int number = int.Parse(Console.ReadLine());

            int result = 10 / number; // Potential division by zero exception

            Console.WriteLine($"Result: {result}");
        }
        catch (FormatException)
        {
            Console.WriteLine("Invalid input. Please enter a valid number.");
        }
        catch (DivideByZeroException)
        {
            Console.WriteLine("Division by zero is not allowed.");
        }
        finally
        {
            Console.WriteLine("Program execution completed.");
        }

        Console.ReadLine();
    }
}

Collections in C#

C# Collections refer to data structures provided by the C# programming language and its associated libraries for storing, organizing, and manipulating groups of related data or objects. These collections offer various ways to manage and access data efficiently. Below are some of the commonly used C# collections:

Arrays

Arrays are fixed-size, ordered collections of elements, all of the same type. They have a fixed length that is determined when they are created. They are efficient for indexed access and are suitable for situations where the size of the collection is known in advance.

using System;
namespace Examples
{
    class Program
    {
        static void Main(string[] args)
        {
            // Define an array of names
            string[] names = { "Alice", "Bob", "Charlie", "David", "Eve" };

            // Display the names using a for loop
            Console.WriteLine("List of Names:");
            for (int i = 0; i < names.Length; i++)
            {
                Console.WriteLine(names[i]);
            }

            Console.ReadLine();
        }
    }
}

Lists

Lists, provided by the List<T> class in the System.Collections.Generic namespace, are dynamic collections of elements. They can grow or shrink in size as needed. Lists allow for efficient insertion, deletion, and random access of elements.

using System;
using System.Collections.Generic;

namespace Examples
{
    class Program
    {
        static void Main(string[] args)
        {
            // Define a List of names
            List<string> names = new List<string> { "Alice", "Bob", "Charlie", "David", "Eve" };

            // Display the names using a foreach loop
            Console.WriteLine("List of Names:");
            foreach (string name in names)
            {
                Console.WriteLine(name);
            }

            Console.ReadLine();
        }
    }
}

Dictionaries

Dictionaries, represented by the Dictionary<TKey, TValue> class in System.Collections.Generic, store key-value pairs. They offer fast retrieval of values based on their associated keys. Dictionaries are useful for mapping unique keys to values.

using System;
using System.Collections.Generic;

namespace Examples
{
    class Program
    {
        static void Main(string[] args)
        {
            // Define a Dictionary of names and ages
            Dictionary<string, int> nameAgeDictionary = new Dictionary<string, int>
        {
            { "Alice", 25 },
            { "Bob", 30 },
            { "Charlie", 28 },
            { "David", 35 },
            { "Eve", 22 }
        };

            // Display the names and ages using a foreach loop
            Console.WriteLine("List of Names and Ages:");
            foreach (var pair in nameAgeDictionary)
            {
                Console.WriteLine($"{pair.Key}: {pair.Value} years old");
            }

            Console.ReadLine();
        }
    }
}

Sets

Sets, available as the HashSet<T> and SortedSet<T> classes, store unique elements. They are used for tasks like removing duplicates from a collection.

HashSet is an unordered set.

using System;
using System.Collections.Generic;

namespace Examples
{
    class Program
    {
        static void Main(string[] args)
        {
            // Define a HashSet of names
            HashSet<string> uniqueNames = new HashSet<string>
            {
                "Alice",
                "Bob",
                "Charlie",
                "David",
                "Eve"
            };

            // Display the unique names using a foreach loop
            Console.WriteLine("Unique Names:");
            foreach (string name in uniqueNames)
            {
                Console.WriteLine(name);
            }

            Console.ReadLine();
        }
    }
}

SortedSet maintains elements in sorted order.

using System;
using System.Collections.Generic;

namespace Examples
{
    class Program
    {
        static void Main(string[] args)
        {
            // Define a SortedSet of names
            SortedSet<string> sortedNames = new SortedSet<string>
            {
                "Eve",
                "Charlie",
                "Bob",
                "David",
                "Alice"
            };

            // Display the sorted names using a foreach loop
            Console.WriteLine("Sorted Names:");
            foreach (string name in sortedNames)
            {
                Console.WriteLine(name);
            }

            Console.ReadLine();
        }
    }
}

Queues

Queues, provided by the Queue<T> class, implement a First-In-First-Out (FIFO) data structure. They are suitable for scenarios where elements need to be processed in a specific order.

using System;
using System.Collections.Generic;

namespace Examples
{
    class Program
    {
        static void Main(string[] args)
        {
            // Define a Queue of names
            Queue<string> nameQueue = new Queue<string>();

            // Enqueue names into the queue
            nameQueue.Enqueue("Alice");
            nameQueue.Enqueue("Bob");
            nameQueue.Enqueue("Charlie");
            nameQueue.Enqueue("David");
            nameQueue.Enqueue("Eve");

            // Display the names in the queue
            Console.WriteLine("Names in the Queue:");

            while (nameQueue.Count > 0)
            {
                string name = nameQueue.Dequeue();
                Console.WriteLine(name);
            }

            Console.ReadLine();
        }
    }
}

Stacks

Stacks, represented by the Stack<T> class, implement a Last-In-First-Out (LIFO) data structure. They are used when you need to maintain a history or undo/redo functionality.

using System;
using System.Collections.Generic;

namespace Examples
{
    class Program
    {
        static void Main(string[] args)
        {
            // Define a Stack of names
            Stack<string> nameStack = new Stack<string>();

            // Push names onto the stack
            nameStack.Push("Alice");
            nameStack.Push("Bob");
            nameStack.Push("Charlie");
            nameStack.Push("David");
            nameStack.Push("Eve");

            // Display the names from the stack
            Console.WriteLine("Names in the Stack:");

            while (nameStack.Count > 0)
            {
                string name = nameStack.Pop();
                Console.WriteLine(name);
            }

            Console.ReadLine();
        }
    }
}

LinkedLists

Linked lists, available as LinkedList<T>, are dynamic data structures where each element points to the next. They are useful when frequent insertion and removal operations are required within a collection.

using System;
using System.Collections.Generic;

namespace Examples
{
    class Program
    {
        static void Main(string[] args)
        {
            // Define a LinkedList of names
            LinkedList<string> nameList = new LinkedList<string>();

            // Add names to the linked list
            nameList.AddLast("Alice");
            nameList.AddLast("Bob");
            nameList.AddLast("Charlie");
            nameList.AddLast("David");
            nameList.AddLast("Eve");

            // Display the names from the linked list
            Console.WriteLine("Names in the Linked List:");

            foreach (string name in nameList)
            {
                Console.WriteLine(name);
            }

            Console.ReadLine();
        }
    }
}

ArrayLists

ArrayLists, found in the System.Collections namespace, are resizable collections that can store elements of different types. They offer flexibility but can lead to performance issues due to boxing/unboxing when storing value types.

using System;
using System.Collections;

namespace Examples
{
    class Program
    {
        static void Main(string[] args)
        {
            // Define an ArrayList of names
            ArrayList nameList = new ArrayList();

            // Add names to the ArrayList
            nameList.Add("Alice");
            nameList.Add("Bob");
            nameList.Add("Charlie");
            nameList.Add("David");
            nameList.Add("Eve");

            // Display the names from the ArrayList
            Console.WriteLine("Names in the ArrayList:");

            foreach (string name in nameList)
            {
                Console.WriteLine(name);
            }

            Console.ReadLine();
        }
    }
}

Concurrent Collections

C# provides thread-safe collections in the System.Collections.Concurrent namespace for concurrent programming. Lets look at two examples.

ConcurrentDictionary

A ConcurrentDictionary in C# is a thread-safe collection provided by the System.Collections.Concurrent namespace that combines the features of a dictionary and thread-safe synchronization. It allows multiple threads to read and modify key-value pairs concurrently without the need for external locking mechanisms.

This data structure is particularly valuable in multi-threaded applications where efficient access to key-value pairs with concurrent access and updates is required. The ConcurrentDictionary provides methods like TryAdd, TryGetValue, TryUpdate, and TryRemove that enable safe and efficient concurrent operations on the dictionary.

It’s a powerful tool for scenarios involving data sharing among multiple threads, ensuring high performance and thread safety in the process.

using System;
using System.Collections.Concurrent;

namespace Examples
{
    class Program
    {
        static void Main(string[] args)
        {
            // Define a ConcurrentDictionary of names and ages
            ConcurrentDictionary<string, int> nameAgeDictionary = new ConcurrentDictionary<string, int>();

            // Add names and ages to the ConcurrentDictionary concurrently
            nameAgeDictionary.TryAdd("Alice", 25);
            nameAgeDictionary.TryAdd("Bob", 30);
            nameAgeDictionary.TryAdd("Charlie", 28);
            nameAgeDictionary.TryAdd("David", 35);
            nameAgeDictionary.TryAdd("Eve", 22);

            // Display the names and ages from the ConcurrentDictionary
            Console.WriteLine("Names and Ages in the ConcurrentDictionary:");

            foreach (var pair in nameAgeDictionary)
            {
                Console.WriteLine($"{pair.Key}: {pair.Value} years old");
            }

            Console.ReadLine();
        }
    }
}
ConcurrentBag

A ConcurrentBag in C# is a thread-safe collection provided by the System.Collections.Concurrent namespace that allows for concurrent access and modification of its elements by multiple threads. It is particularly useful in multi-threaded scenarios where multiple threads need to add, remove, or access items from a shared collection without explicit locking or synchronization.

Unlike other concurrent collections, a ConcurrentBag does not guarantee a specific order for its elements, making it well-suited for scenarios where the order of elements is not critical, such as task parallelism.

It provides methods like Add, TryTake, and GetEnumerator that can be safely used by multiple threads simultaneously, making it a convenient choice for managing thread-safe collections of items in C# applications.

using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;

namespace Examples
{
    class Program
    {
        static void Main(string[] args)
        {
            // Define a ConcurrentBag of names
            ConcurrentBag<string> nameBag = new ConcurrentBag<string>();

            // Add names to the ConcurrentBag concurrently using tasks
            Parallel.Invoke(
                () => nameBag.Add("Alice"),
                () => nameBag.Add("Bob"),
                () => nameBag.Add("Charlie"),
                () => nameBag.Add("David"),
                () => nameBag.Add("Eve")
            );

            // Display the names from the ConcurrentBag
            Console.WriteLine("Names in the ConcurrentBag:");

            foreach (string name in nameBag)
            {
                Console.WriteLine(name);
            }

            Console.ReadLine();
        }
    }
}
Concurrent Collections in Unity

Unity supports C# as its primary scripting language, and C# includes the concurrent collections in the System.Collections.Concurrent namespace. This means you can use them in your Unity standalone projects. As far as we know, WebGL is not multithreaded.

Concurrent collections are particularly useful in Unity when you need to manage shared data among multiple threads or when you want to ensure thread safety while accessing and modifying data structures concurrently. This is common in scenarios like multi-threaded asset loading, managing object pools, or handling data shared between game logic and rendering threads.

When using concurrent collections in Unity, make sure to handle thread safety and synchronization correctly, as Unity may involve multiple threads (e.g., main thread, worker threads, and rendering threads) depending on your project’s complexity. Proper synchronization and thread safety are essential to prevent race conditions and data corruption.

Custom Collections

Developers can create custom collections by implementing interfaces like IEnumerable<T>, ICollection<T>, and IList<T>, or by deriving from existing collection classes.

using System;
using System.Collections;
using System.Collections.Generic;

namespace Examples
{
    class Program
    {
        static void Main(string[] args)
        {
            // Create an instance of the custom collection
            SimpleCollection<int> numbers = new SimpleCollection<int>();

            // Add items to the collection
            numbers.Add(1);
            numbers.Add(2);
            numbers.Add(3);

            // Iterate through the collection
            foreach (int number in numbers)
            {
                Console.WriteLine(number);
            }

            Console.ReadLine();
        }
    }

    class SimpleCollection<T> : IEnumerable<T>
    {
        private List<T> items = new List<T>();

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

        public bool Remove(T item)
        {
            return items.Remove(item);
        }

        public void Clear()
        {
            items.Clear();
        }

        public int Count
        {
            get { return items.Count; }
        }

        public IEnumerator<T> GetEnumerator()
        {
            return items.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
}

Iteration over collections in C#

Iteration over collections in C# allows you to access and manipulate the elements within various data structures like arrays, lists, dictionaries, and more. Collection iteration involves traversing through the elements of a collection, one by one, to perform operations such as retrieval, modification, or analysis.

The most used iteration construct for collections is the foreach loop. It simplifies the process of iterating through the elements by automatically handling the internal iteration logic. When you use foreach, you don’t need to worry about indexes or explicit iteration variables. Instead, you directly access each element in the collection, making the code more readable and less error-prone.

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
foreach (int number in numbers)
{
    // Access and work with each 'number' here
    Console.WriteLine(number);
}

Within the loop, you can perform operations on each element, like displaying its value, performing calculations, or filtering elements based on specific criteria.

Deleting items in a collection through a foreach loop can be challenging and often leads to unexpected behavior. When iterating over a collection using foreach, you are essentially traversing it in a forward-only manner.

Attempting to remove items within the loop may disrupt the internal iterator, leading to errors like collection modification exceptions or skipped elements. To safely delete items while iterating, it’s recommended to first identify the elements you want to remove, collect their references in a separate list, and then iterate through that list after the initial loop to remove the elements from the original collection.

using System;
using System.Collections.Generic;

namespace Examples
{
    class Program
    {
        static void Main(string[] args)
        {
            List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

            // Create a list to store items to be removed
            List<int> itemsToRemove = new List<int>();

            // Display the updated list
            Console.WriteLine("Before");
            foreach (int number in numbers)
            {
                Console.WriteLine(number);
            }

            // Iterate through the list and identify items to remove
            foreach (int number in numbers)
            {
                if (number % 2 == 0) // Remove even numbers
                {
                    itemsToRemove.Add(number);
                }
            }

            // Remove the identified items from the original list
            foreach (int item in itemsToRemove)
            {
                numbers.Remove(item);
            }

            // Display the updated list
            Console.WriteLine("");
            Console.WriteLine("After");
            foreach (int number in numbers)
            {
                Console.WriteLine(number);
            }

            Console.ReadLine();
        }
    }
}

C# Statements in Microsoft Learn.