Skip to main content

Command Palette

Search for a command to run...

Mastering C# Part 2.3 - Fundamentals

Enumeration, Structs, Nullable, Conditional statements, Jump Statements

Updated
16 min read
Mastering C# Part 2.3 - Fundamentals

Enumeration

In C#, an enumeration (enum) is a special value type that allows you to define a set of named constants. Enums are used to represent values that are logically related, making the code easier to read, maintain, and understand.

Here’s a step-by-step guide to understanding and working with enumerations in C#:


1. Defining an Enum

You define an enum using the enum keyword, followed by the name of the enum and a set of named constants enclosed in curly braces {}.

Syntax:

enum EnumName
{
    Constant1,  // Automatically assigned value 0
    Constant2,  // Automatically assigned value 1
    Constant3   // Automatically assigned value 2
}

Example:

enum Day
{
    Sunday,    // 0
    Monday,    // 1
    Tuesday,   // 2
    Wednesday, // 3
    Thursday,  // 4
    Friday,    // 5
    Saturday   // 6
}

By default, the first value in an enum starts at 0, and each subsequent value increments by 1. You can change the default values if needed (explained below).


2. Using Enums

Once you define an enum, you can use it in your code like any other type, such as int, string, etc.

Example:

Day today = Day.Monday;
Console.WriteLine(today);  // Output: Monday

You can also use enums in conditional statements:

if (today == Day.Monday)
{
    Console.WriteLine("It's the start of the week!");
}

3. Assigning Custom Values to Enum Members

By default, enum members are assigned consecutive integer values starting from 0. However, you can assign specific values to the members.

Syntax:

enum EnumName
{
    Member1 = value1,
    Member2 = value2,
    Member3 = value3
}

Example:

enum Day
{
    Sunday = 1,    // 1
    Monday = 2,    // 2
    Tuesday = 3,   // 3
    Wednesday = 4, // 4
    Thursday = 5,  // 5
    Friday = 6,    // 6
    Saturday = 7   // 7
}

Now, each day of the week has a custom assigned value.


4. Enums with Different Base Types

By default, the underlying type of an enum is int. However, you can specify a different base type such as byte, short, long, etc.

Syntax:

enum EnumName : BaseType
{
    Member1 = value1,
    Member2 = value2
}

Example:

enum Day : byte
{
    Sunday = 1,
    Monday = 2,
    Tuesday = 3,
    Wednesday = 4,
    Thursday = 5,
    Friday = 6,
    Saturday = 7
}

Here, we are using byte as the base type for the Day enum, which uses less memory than the default int.


5. Enum Methods

C# provides some useful methods to work with enums:

  • Enum.GetValues(): Gets an array of all values in the enum.

  • Enum.GetName(): Gets the name of an enum member by its value.

  • Enum.IsDefined(): Checks whether a value is defined in the enum.

Example:

foreach (Day day in Enum.GetValues(typeof(Day)))
{
    Console.WriteLine(day); // Prints all the days
}

int dayValue = 3;
Console.WriteLine(Enum.GetName(typeof(Day), dayValue)); // Output: Wednesday
Console.WriteLine(Enum.IsDefined(typeof(Day), 5)); // Output: True

6. Converting Enums to and from Integers

You can easily convert between enum members and their corresponding integer values.

Example:

// Enum to Integer
Day day = Day.Monday;
int dayValue = (int)day;
Console.WriteLine(dayValue);  // Output: 2

// Integer to Enum
int value = 3;
Day dayFromValue = (Day)value;
Console.WriteLine(dayFromValue);  // Output: Wednesday

7. Flags Enum (Bit Flags)

Enums can also be used to represent bitwise flags, which allows you to combine multiple values using bitwise operations. This is useful when you want to represent multiple options in a single variable.

To define a flag enum, you use the [Flags] attribute and assign values that are powers of 2.

Example:

[Flags]
enum Permissions
{
    None = 0,
    Read = 1,
    Write = 2,
    Execute = 4,
    Admin = Read | Write | Execute // Combine Read, Write, and Execute
}

Permissions userPermissions = Permissions.Read | Permissions.Write;
Console.WriteLine(userPermissions);  // Output: Read, Write

You can check for specific permissions using bitwise operations:

if ((userPermissions & Permissions.Read) == Permissions.Read)
{
    Console.WriteLine("User has read permission.");
}

8. Enum Best Practices

  • Use meaningful names: Enum names should clearly represent the concept they are modeling.

  • Avoid magic numbers: Don’t rely on hardcoded integer values; use enums instead.

  • Use Flags for bit operations: Use the [Flags] attribute when you are representing a set of options that can be combined.

Structure

In C#, a structure (or struct) is a value type that allows you to define a custom data type consisting of related variables. Structures are similar to classes but with some key differences, such as their default behavior as value types instead of reference types.


1. Defining a Structure

You define a structure using the struct keyword, followed by the name of the structure and its members (fields, properties, methods, etc.).

Syntax:

struct StructName
{
    // Fields
    public int Field1;
    public string Field2;

    // Constructor
    public StructName(int field1, string field2)
    {
        Field1 = field1;
        Field2 = field2;
    }

    // Method
    public void DisplayInfo()
    {
        Console.WriteLine($"Field1: {Field1}, Field2: {Field2}");
    }
}

Example:

struct Person
{
    public string Name;
    public int Age;

    // Constructor to initialize the fields
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }

    // Method to display person details
    public void DisplayInfo()
    {
        Console.WriteLine($"Name: {Name}, Age: {Age}");
    }
}

2. Using Structures

Once you've defined a structure, you can create instances of it and access its members.

Example:

class Program
{
    static void Main()
    {
        // Creating an instance of the struct
        Person person1 = new Person("John Doe", 30);
        person1.DisplayInfo(); // Output: Name: John Doe, Age: 30

        // Accessing individual fields
        Console.WriteLine(person1.Name); // Output: John Doe
    }
}

3. Default Values in Structures

Unlike classes, a structure cannot have a default constructor, but it automatically provides a parameterless constructor that initializes all fields to their default values (like 0 for numeric types, null for reference types, and false for booleans).

Example:

struct Point
{
    public int X;
    public int Y;
}

Point point = new Point(); // X and Y are initialized to 0 by default
Console.WriteLine($"X: {point.X}, Y: {point.Y}"); // Output: X: 0, Y: 0

4. Passing Structures by Value

Structures are value types, which means they are passed by value when used as method parameters. When you pass a struct to a method, the method gets a copy of the struct, not the original.

Example:

struct Point
{
    public int X;
    public int Y;
}

class Program
{
    static void ModifyPoint(Point point)
    {
        point.X = 10;
        point.Y = 20;
    }

    static void Main()
    {
        Point p1 = new Point { X = 5, Y = 5 };
        ModifyPoint(p1);
        Console.WriteLine($"X: {p1.X}, Y: {p1.Y}"); // Output: X: 5, Y: 5 (unchanged)
    }
}

Since Point is a value type, p1 was passed by value to ModifyPoint, meaning the method worked on a copy of the struct, not the original.


5. Passing Structures by Reference

To modify the original struct inside a method, you need to pass it by reference using the ref or out keyword.

Example using ref:

struct Point
{
    public int X;
    public int Y;
}

class Program
{
    static void ModifyPoint(ref Point point)
    {
        point.X = 10;
        point.Y = 20;
    }

    static void Main()
    {
        Point p1 = new Point { X = 5, Y = 5 };
        ModifyPoint(ref p1);
        Console.WriteLine($"X: {p1.X}, Y: {p1.Y}"); // Output: X: 10, Y: 20 (modified)
    }
}

Now, the original p1 struct is modified because it was passed by reference.


6. Difference Between Structs and Classes

While both structs and classes are used to define custom data types, there are key differences between them:

FeatureStructClass
TypeValue typeReference type
Default constructorCannot define a default constructorCan define a default constructor
Memory allocationStored on the stackStored on the heap
NullabilityCannot be nullCan be null
InheritanceCannot inherit from other structs or classesCan inherit from other classes

7. When to Use a Struct

  • Small, simple data types: Structures are ideal for representing small data structures that hold a few related values.

  • Performance-sensitive scenarios: Structures are allocated on the stack and are often more efficient than classes, especially when dealing with many small, short-lived objects.

Example:

struct Rectangle
{
    public int Width;
    public int Height;

    public int Area()
    {
        return Width * Height;
    }
}

Rectangle rect = new Rectangle { Width = 5, Height = 10 };
Console.WriteLine($"Area: {rect.Area()}");  // Output: Area: 50

8. Common Structure Pitfalls

  • Immutability: Since structs are value types, modifying a struct can sometimes lead to unexpected behavior, especially if it’s passed by value. Always remember that structs are copied when passed to methods or assigned to variables.

  • Large structs: Large structs can have a negative performance impact, especially if they are passed by value. In this case, using a class might be more efficient.


Conclusion

In summary, structures in C# are a great way to define simple data types that hold related values. They offer performance advantages for small objects due to their stack allocation but come with limitations like immutability by default and the lack of inheritance. You should use structs when you need a lightweight, value-based type that represents small, logically grouped data.

If you have any further questions about structures or would like examples on a particular topic, feel free to ask!

Nullable Types

In C#, nullable types allow value types (such as int, float, bool, etc.) to represent null in addition to their normal range of values. This is particularly useful when working with databases or situations where you need to differentiate between an "unset" or "missing" value and the default value of a type.

1. What is a Nullable Type?

Normally, value types in C# (like int, float, double, bool, etc.) cannot be null. However, sometimes you need to represent a scenario where the value is not provided, or it is "undefined." For this, C# provides nullable types.

A nullable type is a special version of a value type that can also represent null. You can define a nullable type using the ? modifier after the type.

Syntax:

Type? variableName;

2. Declaring Nullable Types

You can declare a nullable type by appending a ? to a value type.

Example:

int? myNullableInt = null; // Nullable int, can hold null or an integer value
bool? myNullableBool = null; // Nullable boolean, can hold null or true/false
DateTime? myNullableDate = null; // Nullable DateTime, can hold null or a DateTime value

Here, myNullableInt, myNullableBool, and myNullableDate can store null values in addition to their normal data types.


3. Working with Nullable Types

You can assign null to a nullable type just like any other reference type, but when working with nullable types, you need to check whether they have a value or are null before accessing the value.

a. Checking for null

You can check if a nullable type has a value by using the HasValue property or by comparing it directly to null.

Example:

int? myNullableInt = 10;
if (myNullableInt.HasValue)
{
    Console.WriteLine($"Value: {myNullableInt.Value}");
}
else
{
    Console.WriteLine("No value assigned");
}

// Alternatively
if (myNullableInt != null)
{
    Console.WriteLine($"Value: {myNullableInt.Value}");
}

b. Accessing the Value

To get the value of a nullable type, you can use the Value property. However, this will throw an exception if the nullable type is null. It’s safer to check HasValue first, or use the null-coalescing operator (??).

Example:

int? myNullableInt = 10;
int value = myNullableInt ?? -1;  // If myNullableInt is null, -1 will be used
Console.WriteLine(value);  // Output: 10

If myNullableInt is null, the null-coalescing operator (??) provides a default value (-1 in this case).


4. Null-Coalescing Operator (??)

The null-coalescing operator (??) is used to provide a default value when a nullable type is null.

Example:

int? myNullableInt = null;
int value = myNullableInt ?? 100;  // If myNullableInt is null, use 100
Console.WriteLine(value);  // Output: 100

In this case, myNullableInt is null, so the operator returns the default value 100.


5. Null Conditional Operator (?.)

The null-conditional operator (?.) allows you to safely access members of an object or value types without having to explicitly check for null. It returns null if the value is null rather than throwing an exception.

Example:

int? myNullableInt = null;
int? result = myNullableInt?.ToString().Length; // Will be null if myNullableInt is null
Console.WriteLine(result);  // Output: null

Here, myNullableInt is null, so the ?. operator avoids a NullReferenceException.


6. Nullable Value Type Default Behavior

By default, nullable types are initialized to null when declared.

Example:

int? myNullableInt;  // Default is null
Console.WriteLine(myNullableInt == null);  // Output: True

This is in contrast to regular value types (like int), which are initialized to their default value (0 for int, false for bool, etc.) when declared.


7. Converting Between Nullable and Non-Nullable Types

You can assign a non-nullable value to a nullable type, but the reverse operation requires checking for null.

Example of nullable to non-nullable assignment:

int? myNullableInt = 20;
int myInt = myNullableInt ?? 0;  // If null, 0 will be used
Console.WriteLine(myInt);  // Output: 20

Example of non-nullable to nullable assignment:

int normalInt = 25;
int? myNullableInt = normalInt;  // This works because normalInt is not null
Console.WriteLine(myNullableInt);  // Output: 25

8. Nullable Types with Collections

Nullable types are useful when working with collections (like List<T>) where elements can be missing or optional.

Example with List<int?>:

List<int?> numbers = new List<int?> { 1, 2, null, 4 };

foreach (var number in numbers)
{
    if (number.HasValue)
    {
        Console.WriteLine(number.Value);  // Output: 1, 2, 4
    }
    else
    {
        Console.WriteLine("Null value");  // Output: Null value
    }
}

9. Nullable Types with Database Interactions

Nullable types are especially useful when working with databases (e.g., when dealing with null values in SQL columns). For example, a NULL value in a database column can be represented by a nullable value type in C#.

Example with Entity Framework:

public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int? DepartmentId { get; set; }  // Nullable DepartmentId
}

In this case, DepartmentId can be null, indicating that the employee does not belong to any department.


Conclusion

Nullable types in C# are a powerful feature, allowing value types to represent null in addition to their usual values. This is particularly useful in scenarios where data can be optional or missing, such as working with databases or dealing with uninitialized variables. You can use nullable types with ease by leveraging features like the HasValue property, Value property, and null-coalescing operator (??) to handle null values safely.

Control Flow and Exception Handling

Welcome to this tutorial on control transfer and exception handling in C#. In this lesson, we'll explore two important concepts in C# programming: the goto statement and the throw statement. We'll also show how to combine them with loops like for and foreach to enhance your understanding.

1. The goto Statement

The goto statement is used to transfer control to a labeled statement within the same method. This can be useful for skipping parts of a program or jumping to specific blocks of code.

Example: Using goto in a switch Statement

In this example, we use goto to transfer control from one case to another in a switch statement.

    •     using System;
      
          namespace Data_Types
          {
              class DataTypes
              {
                  static void Main(string[] args)
                  {
                      int num = 5;
      
                      switch (num)
                      {
                          case 1:
                          case 2:
                              Console.WriteLine("The number is 1");
                              break;
      
                          case 3:
                          case 4:
                              Console.WriteLine("The number is 3");
                              break;
      
                          case 5:
                              Console.WriteLine("I found it but let's go to case 3 and execute");
                              goto case 3; // Control is transferred to case 3
                          default:
                              Console.WriteLine("Default case");
                              break;
                      }
                  }
              }
          }
      

      Output:

          I found it but let's go to case 3 and execute
          The number is 3
      

      Explanation:

      In the switch statement, when num is 5, we use goto case 3; to transfer control to the code block where num is 3. This allows us to skip the lines in between and reuse the code for a similar case.

      2. The throw Statement

      The throw statement is used to manually raise exceptions in C#. You create an exception object (typically derived from the Exception class) and throw it using the throw keyword.

      Example: Using throw to Handle Exceptions

      In this example, we'll create a NullReferenceException manually using the throw statement. We'll handle the exception using try-catch.

          using System;
      
          namespace Data_Types
          {
              class ThrowStatements
              {
                  // Taking null in the string
                  static string sub = null;
      
                  static void displaySubject(string sub1)
                  {
                      // Check if the string is null and throw an exception
                      if (sub1 == null)
                      {
                          throw new NullReferenceException("Subject cannot be null");
                      }
                  }
      
                  static void Main(string[] args)
                  {
                      try
                      {
                          displaySubject(sub); // Passing null to trigger exception
                      }
                      catch (Exception ex)
                      {
                          // Catching the exception and displaying the message
                          Console.WriteLine(ex.Message); // "Subject cannot be null"
                      }
                  }
              }
          }
      

      Output:

          Subject cannot be null
      

      Explanation:

      In the method displaySubject, we check if the sub1 parameter is null. If it is, we throw a NullReferenceException. The try-catch block in the Main method catches the exception and prints the exception message.

      3. Combining Loops and Exception Handling

      Now, let's combine loops and exception handling to create a more comprehensive example. We'll use foreach to loop through an array of numbers and simulate some errors using the throw statement.

          using System;
      
          namespace Data_Types
          {
              class Program
              {
                  static void Main(string[] args)
                  {
                      int[] numbers = { 1, 2, 3, -1, 4, -5, 6 };
      
                      foreach (int num in numbers)
                      {
                          try
                          {
                              Console.WriteLine($"Processing number: {num}");
      
                              // Simulate an error if the number is negative
                              if (num < 0)
                              {
                                  throw new ArgumentException("Negative number detected");
                              }
      
                              // Simulate successful processing
                              Console.WriteLine($"Successfully processed: {num}");
                          }
                          catch (ArgumentException ex)
                          {
                              // Catch and handle the exception
                              Console.WriteLine($"Error: {ex.Message}");
                          }
                      }
                  }
              }
          }
      

      Output:

          Processing number: 1
          Successfully processed: 1
          Processing number: 2
          Successfully processed: 2
          Processing number: 3
          Successfully processed: 3
          Processing number: -1
          Error: Negative number detected
          Processing number: 4
          Successfully processed: 4
          Processing number: -5
          Error: Negative number detected
          Processing number: 6
          Successfully processed: 6
      

      Explanation:

      In this example:

      • We use a foreach loop to process each number in the numbers array.

      • If the number is negative, we throw an ArgumentException.

      • The try-catch block ensures that the exception is handled gracefully, allowing the program to continue processing other numbers.

4. A Practical Example: Using goto and throw with Loops

Let's create a more complex scenario where we use both goto and throw together. This example simulates a simple menu system where we can process different commands. If an invalid command is entered, the program throws an exception, and we use goto to jump back to the main menu.

        using System;

        namespace Data_Types
        {
            class Program
            {
                static void Main(string[] args)
                {
                    string command;
                    bool continueRunning = true;

                    while (continueRunning)
                    {
                        Console.WriteLine("Enter a command (start, stop, quit):");
                        command = Console.ReadLine();

                        try
                        {
                            switch (command.ToLower())
                            {
                                case "start":
                                    Console.WriteLine("Starting the process...");
                                    break;
                                case "stop":
                                    Console.WriteLine("Stopping the process...");
                                    break;
                                case "quit":
                                    Console.WriteLine("Exiting the program.");
                                    continueRunning = false;
                                    break;
                                default:
                                    throw new InvalidOperationException("Invalid command entered.");
                            }
                        }
                        catch (InvalidOperationException ex)
                        {
                            Console.WriteLine(ex.Message);
                            goto MainMenu; // Jump back to the main menu
                        }

                    MainMenu: ;
                    }
                }
            }
        }

Output:

        Enter a command (start, stop, quit):
        foo
        Invalid command entered.
        Enter a command (start, stop, quit):
        quit
        Exiting the program.

Explanation:

  • The program continuously prompts the user for a command.

  • If the user enters an invalid command, an exception is thrown and caught.

  • Using the goto MainMenu; statement, the program jumps back to the prompt for the command.

Conclusion

In this tutorial, we learned how to use the goto and throw statements in C#:

  1. goto: Used to transfer control to a labeled statement within the same method.

  2. throw: Used to throw exceptions manually, enabling error handling with try-catch blocks.

  3. Combining Loops and Exception Handling: We saw how to combine loops like foreach with exception handling for more robust programs.

Mastering C# Part 2.3 - Fundamentals