PYnative

Python Programming

  • Learn Python ▼
    • Python Tutorials
    • Python Basics
    • Python Interview Q&As
  • Exercises ▼
    • Python Exercises
    • C++ Exercises
    • C Programming Exercises
    • Java Exercises
    • C# Exercises
  • Quizzes
  • Online Compilers▼
    • Online Python Code Editor
    • Online C Compiler
    • Online C++ Compiler
    • Online Java Compiler
    • Online C# Compiler
Home » C# Exercises » C# Structs, Records, and Enums Exercises: 25+ Coding Problems with Solutions

C# Structs, Records, and Enums Exercises: 25+ Coding Problems with Solutions

Updated on: July 15, 2026 | Leave a Comment

C# gives you several ways to model data beyond a regular class, and knowing when to reach for a struct, a record, or an enum is a skill that separates intermediate developers from beginners.

This collection of 25 C# exercises focuses on these three types, covering value-type semantics with structs, immutable data modeling and value-based equality with records, and clean constant sets with enums.

Each exercise includes a Practice Problem, Purpose, Hint, and a full Solution with Explanation, so you learn not just the syntax but when each type is the right choice.

What You’ll Practice

  • Structs: Value-type behavior, readonly struct, and struct methods.
  • Records: Positional syntax, value equality, and non-destructive with updates.
  • Enums: Declaring enums, underlying values, and using them in switch statements.
  • Design Choices: When to use a struct/record versus a class.

Also, See: C# Exercises with  22 topic-wise sets and 620+ practice questions.

+ Table Of Contents (25 Exercises)

Table of contents

  • Exercise 1: Weekday or Weekend Checker
  • Exercise 2: Next Traffic Light
  • Exercise 3: Byte-Based Error Codes
  • Exercise 4: Combining Permission Flags
  • Exercise 5: Safely Parsing Enum Input
  • Exercise 6: Listing All Enum Values
  • Exercise 7: Checking Order Status Transitions
  • Exercise 8: Converting Length Units
  • Exercise 9: Calculating Distance Between Points
  • Exercise 10: Creating an Immutable Color Struct
  • Exercise 11: Formatting a Money Value
  • Exercise 12: Rectangle Area Using Properties
  • Exercise 13: Adding and Subtracting Vectors
  • Exercise 14: Converting Celsius to Fahrenheit
  • Exercise 15: Comparing Struct and Class Performance
  • Exercise 16: Why Ref Structs Can’t Be Boxed
  • Exercise 17: Creating a Product Record
  • Exercise 18: Comparing Records with ==
  • Exercise 19: Copying a Record with ‘with’
  • Exercise 20: Record Struct vs Record Class
  • Exercise 21: Inheriting from a Record
  • Exercise 22: Deconstructing a Record
  • Exercise 23: Making a Record Mutable
  • Exercise 24: Validating Data in a Record
  • Exercise 25: Nested Records and Equality

Exercise 1: Weekday or Weekend Checker

Practice Problem: Create an enum DaysOfWeek. Write a program that takes a day as input and prints whether it is a “Weekday” or “Weekend”.

Purpose: This exercise helps you practice defining a basic enum and comparing an enum variable against specific members to branch your program’s logic.

Given Input: day = DaysOfWeek.Wednesday

Expected Output:

Classifying Wednesday:
Result: Wednesday is a Weekday.
Classification completed successfully.
▼ Hint
  • Define an enum with all seven days as members.
  • Compare the given day against Saturday and Sunday to check if it’s a weekend.
  • Otherwise, treat it as a weekday.
▼ Solution & Explanation
using System;

namespace EnumUtilities
{
    enum DaysOfWeek
    {
        Sunday,
        Monday,
        Tuesday,
        Wednesday,
        Thursday,
        Friday,
        Saturday
    }

    class WeekdayClassifier
    {
        static void Main(string[] args)
        {
            DaysOfWeek day = DaysOfWeek.Wednesday;

            Console.WriteLine($"Classifying {day}:");

            if (day == DaysOfWeek.Saturday || day == DaysOfWeek.Sunday)
            {
                Console.WriteLine($"Result: {day} is a Weekend.");
            }
            else
            {
                Console.WriteLine($"Result: {day} is a Weekday.");
            }

            Console.WriteLine("Classification completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • enum DaysOfWeek { ... }: Defines a named set of constant values representing the days of the week.
  • DaysOfWeek day = DaysOfWeek.Wednesday: Declares a variable of the enum type and assigns it one of the enum’s members.
  • day == DaysOfWeek.Saturday || day == DaysOfWeek.Sunday: Checks if the day matches either of the two weekend values.
  • $"Result: {day} is a Weekday.": String interpolation automatically calls ToString() on the enum, printing its member name.

Exercise 2: Next Traffic Light

Practice Problem: Define a TrafficLight enum (Red, Yellow, Green). Write a method that takes the current light and returns the next light in the sequence.

Purpose: This exercise helps you practice writing a method that accepts and returns an enum type, using a switch statement to map each state to the next one in a cycle.

Given Input: currentLight = TrafficLight.Red

Expected Output:

Current Light: Red
Next Light: Green
Traffic light simulation completed successfully.
▼ Hint
  • Define an enum with the three traffic light states.
  • Write a method that takes the current light and returns the next one using a switch statement.
  • Map Red to Green, Green to Yellow, and Yellow back to Red.
▼ Solution & Explanation
using System;

namespace EnumUtilities
{
    enum TrafficLight
    {
        Red,
        Yellow,
        Green
    }

    class TrafficLightSimulator
    {
        static TrafficLight GetNextLight(TrafficLight current)
        {
            switch (current)
            {
                case TrafficLight.Red:
                    return TrafficLight.Green;
                case TrafficLight.Green:
                    return TrafficLight.Yellow;
                case TrafficLight.Yellow:
                    return TrafficLight.Red;
                default:
                    return TrafficLight.Red;
            }
        }

        static void Main(string[] args)
        {
            TrafficLight currentLight = TrafficLight.Red;

            Console.WriteLine($"Current Light: {currentLight}");

            TrafficLight nextLight = GetNextLight(currentLight);

            Console.WriteLine($"Next Light: {nextLight}");
            Console.WriteLine("Traffic light simulation completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • enum TrafficLight { Red, Yellow, Green }: Defines the three possible states of a traffic light.
  • static TrafficLight GetNextLight(TrafficLight current): Defines a method that accepts an enum value and returns another value of the same enum type.
  • switch (current): Branches based on which enum member was passed in.
  • case TrafficLight.Red: return TrafficLight.Green;: Maps each light to the next one in the standard Red to Green to Yellow to Red cycle.

Exercise 3: Byte-Based Error Codes

Practice Problem: Create an enum called ErrorCode where the underlying type is a byte instead of the default int, and assign it small values that fit within a byte’s range.

Purpose: This exercise helps you practice specifying a custom underlying type for an enum, and understanding why the assigned values must fit within that type’s range.

Given Input: code = ErrorCode.ServerError

Expected Output:

Inspecting a byte-based enum:
Name: ServerError
Value: 20
Underlying Type Size: 1 byte(s)
Error code inspection completed successfully.
▼ Hint
  • Add : byte after the enum name to change its underlying type.
  • Assign values that fit within the byte range of 0 to 255, such as 10, 20, and 30, instead of a larger number like 404.
  • Cast an enum value to byte to see its underlying numeric value.
▼ Solution & Explanation
using System;

namespace EnumUtilities
{
    enum ErrorCode : byte
    {
        NotFound = 10,
        ServerError = 20,
        Unauthorized = 30
    }

    class ErrorCodeDemo
    {
        static void Main(string[] args)
        {
            ErrorCode code = ErrorCode.ServerError;

            Console.WriteLine("Inspecting a byte-based enum:");
            Console.WriteLine($"Name: {code}");
            Console.WriteLine($"Value: {(byte)code}");
            Console.WriteLine($"Underlying Type Size: {sizeof(ErrorCode)} byte(s)");

            Console.WriteLine("Error code inspection completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • enum ErrorCode : byte { ... }: Declares the enum’s underlying type as byte instead of the default int, so each value must fit within 0-255.
  • NotFound = 10, ServerError = 20, Unauthorized = 30: Assigns specific byte-sized values to each error, since a value like 404 would overflow a byte.
  • (byte)code: Casts the enum value back to its underlying byte value for display.
  • sizeof(ErrorCode): Confirms the enum only occupies 1 byte of memory, unlike the default 4-byte int-backed enum.

Exercise 4: Combining Permission Flags

Practice Problem: Create a UserPermissions enum using the [Flags] attribute. Write a program that combines permissions and checks if a user has Write access.

Purpose: This exercise helps you practice using the [Flags] attribute along with bitwise OR and AND operators to combine and check multiple enum values stored in a single variable.

Given Input: userPermissions = UserPermissions.Read | UserPermissions.Write

Expected Output:

Combined Permissions: Read, Write
Result: User has Write access.
Permission check completed successfully.
▼ Hint
  • Add the [Flags] attribute above the enum and assign each member a power of 2 (1, 2, 4, and so on).
  • Combine permissions using the bitwise OR operator (|).
  • Check for a specific permission using the bitwise AND operator (&) and comparing the result to that permission.
▼ Solution & Explanation
using System;

namespace EnumUtilities
{
    [Flags]
    enum UserPermissions
    {
        None = 0,
        Read = 1,
        Write = 2,
        Execute = 4
    }

    class UserPermissionsDemo
    {
        static void Main(string[] args)
        {
            UserPermissions userPermissions = UserPermissions.Read | UserPermissions.Write;

            Console.WriteLine($"Combined Permissions: {userPermissions}");

            bool hasWriteAccess = (userPermissions & UserPermissions.Write) == UserPermissions.Write;

            if (hasWriteAccess)
            {
                Console.WriteLine("Result: User has Write access.");
            }
            else
            {
                Console.WriteLine("Result: User does not have Write access.");
            }

            Console.WriteLine("Permission check completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • [Flags]: Marks the enum as a bit-field, allowing its members to be combined using bitwise operators and displayed as a comma-separated list.
  • Read = 1, Write = 2, Execute = 4: Assigns each permission a distinct power of 2, so they can be combined without overlapping bits.
  • UserPermissions.Read | UserPermissions.Write: Combines two permissions using the bitwise OR operator.
  • (userPermissions & UserPermissions.Write) == UserPermissions.Write: Uses bitwise AND to check whether the Write bit is set within the combined value.

Exercise 5: Safely Parsing Enum Input

Practice Problem: Write a program that takes a string input (e.g., “Premium”) and safely parses it into a SubscriptionTier enum using Enum.TryParse.

Purpose: This exercise helps you practice converting a string into an enum value safely, handling invalid input without relying on exception handling.

Given Input: tierInput = "Premium"

Expected Output:

Parsing "Premium" into a SubscriptionTier:
Result: Successfully parsed as Premium.
Enum parsing completed successfully.
▼ Hint
  • Use Enum.TryParse<T>() instead of Enum.Parse() to avoid exceptions on invalid input.
  • Pass the string and an out variable to receive the parsed enum value.
  • Check the boolean return value to determine whether the parse succeeded.
▼ Solution & Explanation
using System;

namespace EnumUtilities
{
    enum SubscriptionTier
    {
        Free,
        Basic,
        Premium,
        Enterprise
    }

    class EnumParsingDemo
    {
        static void Main(string[] args)
        {
            string tierInput = "Premium";

            Console.WriteLine($"Parsing \"{tierInput}\" into a SubscriptionTier:");

            bool success = Enum.TryParse<subscriptiontier>(tierInput, out SubscriptionTier parsedTier);

            if (success)
            {
                Console.WriteLine($"Result: Successfully parsed as {parsedTier}.");
            }
            else
            {
                Console.WriteLine($"Result: \"{tierInput}\" is not a valid SubscriptionTier.");
            }

            Console.WriteLine("Enum parsing completed successfully.");
        }
    }
}</subscriptiontier>Code language: C# (cs)

Explanation:

  • enum SubscriptionTier { Free, Basic, Premium, Enterprise }: Defines the set of valid subscription levels.
  • Enum.TryParse<SubscriptionTier>(tierInput, out SubscriptionTier parsedTier): Attempts to convert the string into a matching enum value, returning true or false instead of throwing an exception if it fails.
  • out SubscriptionTier parsedTier: Declares an output variable that receives the parsed enum value if the parse succeeds.
  • if (success): Branches based on whether the parse attempt worked, allowing the invalid case to be handled safely.

Exercise 6: Listing All Enum Values

Practice Problem: Write a method that dynamically prints all the names and underlying values of any given enum using Enum.GetValues.

Purpose: This exercise helps you practice writing a generic method constrained to enum types, and using reflection-based methods to inspect an enum’s members at runtime.

Given Input: enum Season { Spring, Summer, Autumn, Winter }

Expected Output:

Listing all values of the Season enum:
Spring = 0
Summer = 1
Autumn = 2
Winter = 3
Enum listing completed successfully.
▼ Hint
  • Write a generic method constrained with where T : Enum so it can accept any enum type.
  • Use Enum.GetValues(typeof(T)) to retrieve all the members of that enum.
  • Loop through the results, printing each member’s name and its underlying numeric value.
▼ Solution & Explanation
using System;

namespace EnumUtilities
{
    enum Season
    {
        Spring,
        Summer,
        Autumn,
        Winter
    }

    class EnumValueLister
    {
        static void PrintEnumValues<t>() where T : Enum
        {
            foreach (T value in Enum.GetValues(typeof(T)))
            {
                Console.WriteLine($"{value} = {Convert.ToInt32(value)}");
            }
        }

        static void Main(string[] args)
        {
            Console.WriteLine("Listing all values of the Season enum:");

            PrintEnumValues<season>();

            Console.WriteLine("Enum listing completed successfully.");
        }
    }
}</season></t>Code language: C# (cs)

Explanation:

  • static void PrintEnumValues<T>() where T : Enum: Defines a generic method that works with any enum type, constrained so it only accepts enum types.
  • Enum.GetValues(typeof(T)): Retrieves an array containing every member defined in the enum.
  • foreach (T value in Enum.GetValues(typeof(T))): Iterates through each enum member returned.
  • Convert.ToInt32(value): Converts the enum member to its underlying numeric value for display.

Exercise 7: Checking Order Status Transitions

Practice Problem: Create an OrderStatus enum (Pending, Shipped, Delivered, Cancelled). Write a method that validates if an order can transition from Delivered to Cancelled (it shouldn’t!).

Purpose: This exercise helps you practice using enum comparisons to enforce business rules about which state changes are allowed in a workflow.

Given Input: currentStatus = OrderStatus.Delivered, newStatus = OrderStatus.Cancelled

Expected Output:

Checking transition from Delivered to Cancelled:
Result: This transition is not allowed.
Order status validation completed successfully.
▼ Hint
  • Define an enum with the possible order statuses.
  • Write a method that takes the current and proposed next status.
  • Return false specifically when the current status is Delivered and the next status is Cancelled; return true otherwise.
▼ Solution & Explanation
using System;

namespace EnumUtilities
{
    enum OrderStatus
    {
        Pending,
        Shipped,
        Delivered,
        Cancelled
    }

    class OrderStatusValidator
    {
        static bool IsValidTransition(OrderStatus current, OrderStatus next)
        {
            if (current == OrderStatus.Delivered && next == OrderStatus.Cancelled)
            {
                return false;
            }

            return true;
        }

        static void Main(string[] args)
        {
            OrderStatus currentStatus = OrderStatus.Delivered;
            OrderStatus newStatus = OrderStatus.Cancelled;

            Console.WriteLine($"Checking transition from {currentStatus} to {newStatus}:");

            bool isValid = IsValidTransition(currentStatus, newStatus);

            if (isValid)
            {
                Console.WriteLine("Result: This transition is allowed.");
            }
            else
            {
                Console.WriteLine("Result: This transition is not allowed.");
            }

            Console.WriteLine("Order status validation completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • enum OrderStatus { Pending, Shipped, Delivered, Cancelled }: Defines the possible stages an order can be in.
  • static bool IsValidTransition(OrderStatus current, OrderStatus next): Defines a method that checks whether moving from one status to another is allowed.
  • if (current == OrderStatus.Delivered && next == OrderStatus.Cancelled): Specifically blocks the one transition that shouldn’t be allowed, an already-delivered order being cancelled.
  • Final return true: Allows any other transition that doesn’t match the blocked rule.

Exercise 8: Converting Length Units

Practice Problem: Create an enum LengthUnit (Meter, Centimeter, Inch, Foot). Write a conversion function that converts a value from one unit to another based on the enum provided.

Purpose: This exercise helps you practice using an enum to select between multiple conversion factors, and structuring a conversion through a shared base unit instead of every possible pair.

Given Input: value = 5, fromUnit = LengthUnit.Meter, toUnit = LengthUnit.Foot

Expected Output:

Converting 5 Meter to Foot:
Result: 16.40 Foot
Unit conversion completed successfully.
▼ Hint
  • Define an enum with the supported length units.
  • Write a helper method that converts any unit into a common base unit, such as meters.
  • Convert the value to the base unit first, then convert from the base unit into the target unit.
▼ Solution & Explanation
using System;

namespace EnumUtilities
{
    enum LengthUnit
    {
        Meter,
        Centimeter,
        Inch,
        Foot
    }

    class LengthUnitConverter
    {
        static double ToMeters(double value, LengthUnit unit)
        {
            switch (unit)
            {
                case LengthUnit.Meter:
                    return value;
                case LengthUnit.Centimeter:
                    return value * 0.01;
                case LengthUnit.Inch:
                    return value * 0.0254;
                case LengthUnit.Foot:
                    return value * 0.3048;
                default:
                    return value;
            }
        }

        static double ConvertLength(double value, LengthUnit fromUnit, LengthUnit toUnit)
        {
            double meters = ToMeters(value, fromUnit);

            switch (toUnit)
            {
                case LengthUnit.Meter:
                    return meters;
                case LengthUnit.Centimeter:
                    return meters / 0.01;
                case LengthUnit.Inch:
                    return meters / 0.0254;
                case LengthUnit.Foot:
                    return meters / 0.3048;
                default:
                    return meters;
            }
        }

        static void Main(string[] args)
        {
            double value = 5;
            LengthUnit fromUnit = LengthUnit.Meter;
            LengthUnit toUnit = LengthUnit.Foot;

            Console.WriteLine($"Converting {value} {fromUnit} to {toUnit}:");

            double result = ConvertLength(value, fromUnit, toUnit);

            Console.WriteLine($"Result: {result:F2} {toUnit}");
            Console.WriteLine("Unit conversion completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • enum LengthUnit { Meter, Centimeter, Inch, Foot }: Defines the set of supported length units.
  • static double ToMeters(double value, LengthUnit unit): Converts any supported unit into meters, used as a common intermediate unit for all conversions.
  • switch (unit): Selects the correct conversion factor based on which enum member was passed in.
  • ConvertLength(...): Converts the input value to meters first, then converts from meters into the target unit, avoiding the need for a direct conversion factor between every possible pair of units.
  • {result:F2}: Formats the result to two decimal places for readability.

Exercise 9: Calculating Distance Between Points

Practice Problem: Create a Point2D struct with X and Y coordinates. Include a method to calculate the distance between two points.

Purpose: This exercise helps you practice defining a struct with fields, a constructor, and a method, and applying the Pythagorean theorem to solve a geometry problem.

Given Input: pointA = new Point2D(0, 0), pointB = new Point2D(3, 4)

Expected Output:

Calculating distance between (0, 0) and (3, 4):
Distance = 5
Distance calculation completed successfully.
▼ Hint
  • Define a struct with two public fields for X and Y.
  • Add a constructor that sets both fields from parameters.
  • Write a method that takes another point and calculates the distance using the Pythagorean theorem: sqrt((x2-x1)^2 + (y2-y1)^2).
▼ Solution & Explanation
using System;

namespace StructUtilities
{
    struct Point2D
    {
        public double X;
        public double Y;

        public Point2D(double x, double y)
        {
            X = x;
            Y = y;
        }

        public double DistanceTo(Point2D other)
        {
            double deltaX = X - other.X;
            double deltaY = Y - other.Y;
            return Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
        }
    }

    class DistanceCalculator
    {
        static void Main(string[] args)
        {
            Point2D pointA = new Point2D(0, 0);
            Point2D pointB = new Point2D(3, 4);

            Console.WriteLine($"Calculating distance between ({pointA.X}, {pointA.Y}) and ({pointB.X}, {pointB.Y}):");

            double distance = pointA.DistanceTo(pointB);

            Console.WriteLine($"Distance = {distance}");
            Console.WriteLine("Distance calculation completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • struct Point2D: Defines a value type with X and Y coordinate fields, suited for small, simple data like a point in space.
  • public Point2D(double x, double y): A constructor that initializes both fields when a new point is created.
  • DistanceTo(Point2D other): A method that calculates the Euclidean distance between the current point and another point using the distance formula.
  • Math.Sqrt(deltaX * deltaX + deltaY * deltaY): Applies the Pythagorean theorem to compute the straight-line distance between the two points.

Exercise 10: Creating an Immutable Color Struct

Practice Problem: Create a readonly struct called ColorRGB with fields for Red, Green, and Blue. Ensure they can only be set via the constructor.

Purpose: This exercise helps you practice using the readonly modifier and get-only properties to create a struct that is fully immutable once it’s constructed.

Given Input: color = new ColorRGB(255, 100, 50)

Expected Output:

Creating an immutable RGB color:
Red = 255, Green = 100, Blue = 50
Color creation completed successfully.
▼ Hint
  • Declare the struct with the readonly modifier.
  • Use get-only properties (with no set accessor) for Red, Green, and Blue.
  • Assign all three properties inside the constructor, since that is the only place they can be set.
▼ Solution & Explanation
using System;

namespace StructUtilities
{
    readonly struct ColorRGB
    {
        public byte Red { get; }
        public byte Green { get; }
        public byte Blue { get; }

        public ColorRGB(byte red, byte green, byte blue)
        {
            Red = red;
            Green = green;
            Blue = blue;
        }
    }

    class ColorDemo
    {
        static void Main(string[] args)
        {
            ColorRGB color = new ColorRGB(255, 100, 50);

            Console.WriteLine("Creating an immutable RGB color:");
            Console.WriteLine($"Red = {color.Red}, Green = {color.Green}, Blue = {color.Blue}");

            Console.WriteLine("Color creation completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • readonly struct ColorRGB: Marks the entire struct as read-only, meaning none of its fields or properties can be modified after the object is constructed.
  • public byte Red { get; }: A get-only auto-property, which can only be assigned inside the constructor and never changed afterward.
  • public ColorRGB(byte red, byte green, byte blue): The only place where the three color components can be set.
  • After construction: color.Red, color.Green, and color.Blue are fixed and cannot be reassigned anywhere else in the program.

Exercise 11: Formatting a Money Value

Practice Problem: Create a Money struct that holds an Amount (decimal) and a Currency (string). Override the ToString() method to display it nicely.

Purpose: This exercise helps you practice overriding ToString() on a struct so that printing it directly produces a clean, formatted representation instead of the default type name.

Given Input: wallet = new Money(10.50m, "USD")

Expected Output:

Formatting a wallet amount:
$10.50 USD
Money formatting completed successfully.
▼ Hint
  • Declare an Amount field of type decimal and a Currency field of type string.
  • Override the ToString() method using the override keyword.
  • Format the amount to two decimal places using {Amount:F2} inside the returned string.
▼ Solution & Explanation
using System;

namespace StructUtilities
{
    struct Money
    {
        public decimal Amount;
        public string Currency;

        public Money(decimal amount, string currency)
        {
            Amount = amount;
            Currency = currency;
        }

        public override string ToString()
        {
            return $"${Amount:F2} {Currency}";
        }
    }

    class WalletDemo
    {
        static void Main(string[] args)
        {
            Money wallet = new Money(10.50m, "USD");

            Console.WriteLine("Formatting a wallet amount:");
            Console.WriteLine(wallet);

            Console.WriteLine("Money formatting completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • struct Money: Bundles an Amount and a Currency together into a single value type.
  • public override string ToString(): Overrides the default ToString() method inherited from object, which would otherwise just print the struct’s type name.
  • $"${Amount:F2} {Currency}": Formats the amount to two decimal places and appends the currency code, producing a readable representation.
  • Console.WriteLine(wallet): Automatically calls ToString() on the object being printed, so the overridden formatting is used.

Exercise 12: Rectangle Area Using Properties

Practice Problem: Create a Rectangle struct. Give it Width and Height properties, and a read-only property for Area.

Purpose: This exercise helps you practice the difference between settable auto-properties and a read-only computed property that derives its value from the others.

Given Input: rectangle = new Rectangle { Width = 5, Height = 3 }

Expected Output:

Calculating area for a 5 x 3 rectangle:
Area = 15
Rectangle area calculation completed successfully.
▼ Hint
  • Use auto-implemented properties with get and set for Width and Height.
  • Define Area as a read-only property (using the => expression-bodied syntax) that computes Width times Height.
  • Since Area is calculated, it stays accurate automatically whenever Width or Height changes.
▼ Solution & Explanation
using System;

namespace StructUtilities
{
    struct Rectangle
    {
        public double Width { get; set; }
        public double Height { get; set; }

        public double Area => Width * Height;
    }

    class RectangleDemo
    {
        static void Main(string[] args)
        {
            Rectangle rectangle = new Rectangle { Width = 5, Height = 3 };

            Console.WriteLine($"Calculating area for a {rectangle.Width} x {rectangle.Height} rectangle:");
            Console.WriteLine($"Area = {rectangle.Area}");

            Console.WriteLine("Rectangle area calculation completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • public double Width { get; set; }: An auto-implemented property with both a getter and a setter, allowing the width to be read and changed.
  • public double Area => Width * Height;: A read-only computed property using expression-bodied syntax, calculated on demand rather than stored as a field.
  • new Rectangle { Width = 5, Height = 3 }: Uses object initializer syntax to set the settable properties right after construction.
  • Because Area has no setter: It always reflects the current Width and Height and can never be set directly, keeping it automatically in sync.

Exercise 13: Adding and Subtracting Vectors

Practice Problem: Create a Vector2D struct. Overload the + and - operators so you can add or subtract two vectors directly.

Purpose: This exercise helps you practice operator overloading, which lets you define what standard arithmetic operators mean for a custom type.

Given Input: vectorA = new Vector2D(2, 3), vectorB = new Vector2D(4, 1)

Expected Output:

Vector A = (2, 3), Vector B = (4, 1)
Sum = (6, 4)
Difference = (-2, 2)
Vector operations completed successfully.
▼ Hint
  • Define operator overload methods using the operator keyword, marked static and public.
  • For +, create a new vector whose X and Y are the sums of the two input vectors’ components.
  • For -, do the same using subtraction instead of addition.
▼ Solution & Explanation
using System;

namespace StructUtilities
{
    struct Vector2D
    {
        public double X;
        public double Y;

        public Vector2D(double x, double y)
        {
            X = x;
            Y = y;
        }

        public static Vector2D operator +(Vector2D a, Vector2D b)
        {
            return new Vector2D(a.X + b.X, a.Y + b.Y);
        }

        public static Vector2D operator -(Vector2D a, Vector2D b)
        {
            return new Vector2D(a.X - b.X, a.Y - b.Y);
        }

        public override string ToString()
        {
            return $"({X}, {Y})";
        }
    }

    class VectorDemo
    {
        static void Main(string[] args)
        {
            Vector2D vectorA = new Vector2D(2, 3);
            Vector2D vectorB = new Vector2D(4, 1);

            Console.WriteLine($"Vector A = {vectorA}, Vector B = {vectorB}");

            Vector2D sum = vectorA + vectorB;
            Vector2D difference = vectorA - vectorB;

            Console.WriteLine($"Sum = {sum}");
            Console.WriteLine($"Difference = {difference}");
            Console.WriteLine("Vector operations completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • public static Vector2D operator +(Vector2D a, Vector2D b): Overloads the + operator so two Vector2D values can be added directly using standard arithmetic syntax.
  • new Vector2D(a.X + b.X, a.Y + b.Y): Defines what addition means for this type, adding the corresponding X and Y components.
  • public static Vector2D operator -(Vector2D a, Vector2D b): Overloads the - operator in the same way, subtracting corresponding components.
  • vectorA + vectorB: Once overloaded, the operator can be used naturally, just like adding two numbers.

Exercise 14: Converting Celsius to Fahrenheit

Practice Problem: Create a Fahrenheit struct and a Celsius struct. Implement an explicit or implicit conversion operator to convert between the two.

Purpose: This exercise helps you practice defining a custom conversion operator between two related types, and understanding when to make a conversion explicit rather than implicit.

Given Input: celsius = new Celsius(25)

Expected Output:

Converting 25°C to Fahrenheit:
Result: 77°F
Temperature conversion completed successfully.
▼ Hint
  • Define the conversion operator using public static explicit operator TargetType(SourceType value).
  • Use the Celsius-to-Fahrenheit formula (Celsius * 9 / 5 + 32) inside the operator body.
  • Trigger the conversion in your code with an explicit cast, such as (Fahrenheit)celsius.
▼ Solution & Explanation
using System;

namespace StructUtilities
{
    struct Celsius
    {
        public double Degrees;

        public Celsius(double degrees)
        {
            Degrees = degrees;
        }
    }

    struct Fahrenheit
    {
        public double Degrees;

        public Fahrenheit(double degrees)
        {
            Degrees = degrees;
        }

        public static explicit operator Fahrenheit(Celsius c)
        {
            return new Fahrenheit(c.Degrees * 9 / 5 + 32);
        }
    }

    class TemperatureConverter
    {
        static void Main(string[] args)
        {
            Celsius celsius = new Celsius(25);

            Console.WriteLine($"Converting {celsius.Degrees}°C to Fahrenheit:");

            Fahrenheit fahrenheit = (Fahrenheit)celsius;

            Console.WriteLine($"Result: {fahrenheit.Degrees}°F");
            Console.WriteLine("Temperature conversion completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • public static explicit operator Fahrenheit(Celsius c): Defines a custom explicit conversion that allows a Celsius value to be cast into a Fahrenheit value.
  • explicit (rather than implicit): Requires the cast to be written out with (Fahrenheit)celsius, making it clear in the code that a conversion, and a potential change in meaning, is happening.
  • c.Degrees * 9 / 5 + 32: Applies the standard Celsius-to-Fahrenheit formula inside the conversion operator.
  • (Fahrenheit)celsius: Triggers the custom conversion operator, producing a new Fahrenheit value from the Celsius input.

Exercise 15: Comparing Struct and Class Performance

Practice Problem: Write a program that creates an array of 1,000,000 class objects versus 1,000,000 structs. Observe the instantiation time differences.

Purpose: This exercise helps you practice using Stopwatch to measure execution time, and observe how value types (structs) and reference types (classes) behave differently when allocated in bulk.

Given Input: count = 1000000

Expected Output (exact timing values will vary depending on your machine, but structs are typically faster to allocate since they avoid a separate heap allocation for every single object):

Creating 1000000 structs and 1000000 class objects:
Struct array creation time: 6 ms
Class array creation time: 22 ms
Benchmark completed successfully.
▼ Hint
  • Define a struct and a class with identical X and Y fields for a fair comparison.
  • Use Stopwatch.StartNew() before filling each array, and check ElapsedMilliseconds after.
  • Fill each array inside a loop of 1,000,000 iterations, then compare the two recorded times.
▼ Solution & Explanation
using System;
using System.Diagnostics;

namespace StructUtilities
{
    struct PointStruct
    {
        public double X;
        public double Y;
    }

    class PointClass
    {
        public double X;
        public double Y;
    }

    class PerformanceBenchmark
    {
        static void Main(string[] args)
        {
            int count = 1000000;

            Console.WriteLine($"Creating {count} structs and {count} class objects:");

            Stopwatch structStopwatch = Stopwatch.StartNew();
            PointStruct[] structArray = new PointStruct[count];
            for (int i = 0; i < count; i++)
            {
                structArray[i] = new PointStruct { X = i, Y = i };
            }
            structStopwatch.Stop();

            Stopwatch classStopwatch = Stopwatch.StartNew();
            PointClass[] classArray = new PointClass[count];
            for (int i = 0; i < count; i++)
            {
                classArray[i] = new PointClass { X = i, Y = i };
            }
            classStopwatch.Stop();

            Console.WriteLine($"Struct array creation time: {structStopwatch.ElapsedMilliseconds} ms");
            Console.WriteLine($"Class array creation time: {classStopwatch.ElapsedMilliseconds} ms");
            Console.WriteLine("Benchmark completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • struct PointStruct vs class PointClass: Two identical-looking types, one a value type and one a reference type, used to isolate the performance difference.
  • Stopwatch.StartNew(): Starts a high-precision timer used to measure how long each array-filling loop takes.
  • new PointStruct[count]: Allocates a single contiguous block of memory holding all the struct values directly, with no separate allocation per element.
  • new PointClass[count]: Allocates an array of references, and each new PointClass { ... } inside the loop performs a separate heap allocation, which is typically slower.

Exercise 16: Why Ref Structs Can’t Be Boxed

Practice Problem: Create a ref struct called LargeBuffer. Try to box it or use it inside a regular class to see why the compiler prevents it.

Purpose: This exercise helps you practice understanding ref struct restrictions, a feature used by types like Span<T> to guarantee stack-only, high-performance memory usage.

Given Input: buffer = new LargeBuffer(1024)

Expected Output:

Creating a ref struct on the stack:
Buffer Size = 1024 bytes
First Byte = 255
Ref struct demonstration completed successfully.
▼ Hint
  • Declare the struct with the ref struct modifiers, giving it a field such as a Span<byte>.
  • Use it normally within a single method, on the stack, and it will work fine.
  • If you try to assign it to an object variable or store it as a field in a class, the compiler will produce an error, since ref structs cannot be boxed or moved to the heap.
▼ Solution & Explanation
using System;

namespace StructUtilities
{
    ref struct LargeBuffer
    {
        public Span<byte> Data;

        public LargeBuffer(int size)
        {
            Data = new byte[size];
        }
    }

    class RefStructDemo
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Creating a ref struct on the stack:");

            LargeBuffer buffer = new LargeBuffer(1024);
            buffer.Data[0] = 255;

            Console.WriteLine($"Buffer Size = {buffer.Data.Length} bytes");
            Console.WriteLine($"First Byte = {buffer.Data[0]}");

            // The following lines would NOT compile, because ref structs
            // cannot be boxed or stored as fields in a regular class:
            //
            // object boxed = buffer;                 // Error: cannot convert ref struct to object
            // class Holder { LargeBuffer Field; }     // Error: ref struct cannot be a field of a class

            Console.WriteLine("Ref struct demonstration completed successfully.");
        }
    }
}</byte>Code language: C# (cs)

Explanation:

  • ref struct LargeBuffer: Declares a struct that must live entirely on the stack, so it can never be boxed, stored on the heap, or captured by a lambda or async method.
  • public Span<byte> Data: Uses Span<T>, itself a ref struct, which is the typical reason ref struct types exist, for safely working with contiguous memory without extra heap allocations.
  • The commented-out lines: Illustrate two operations the compiler would reject: converting the ref struct to object (which would require boxing), and storing it as a field inside a normal class (which could let it outlive the stack frame it belongs to).
  • Why this matters: These restrictions are what let ref struct guarantee high performance and safety for scenarios like Span<T>, at the cost of some flexibility.

Exercise 17: Creating a Product Record

Practice Problem: Create a positional record Product that holds an Id, Name, and Price. Instantiate it using the concise positional syntax.

Purpose: This exercise helps you practice declaring a record using the compact positional syntax, which automatically generates a constructor, properties, and a readable ToString().

Given Input: product = new Product(1, "Laptop", 999.99m)

Expected Output:

Creating a product using positional record syntax:
Product { Id = 1, Name = Laptop, Price = 999.99 }
Product creation completed successfully.
▼ Hint
  • Declare the record using the positional syntax: record Product(int Id, string Name, decimal Price);
  • Create an instance by passing values in order, just like calling a constructor.
  • Print the record directly; it automatically formats its properties for you.
▼ Solution & Explanation
using System;

namespace RecordUtilities
{
    record Product(int Id, string Name, decimal Price);

    class ProductDemo
    {
        static void Main(string[] args)
        {
            Product product = new Product(1, "Laptop", 999.99m);

            Console.WriteLine("Creating a product using positional record syntax:");
            Console.WriteLine(product);

            Console.WriteLine("Product creation completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • record Product(int Id, string Name, decimal Price);: Declares a positional record in a single line, which automatically generates a constructor, get-only properties, and value-based equality.
  • new Product(1, "Laptop", 999.99m): Creates an instance using positional syntax, passing arguments in the same order as the record’s declaration.
  • Console.WriteLine(product): Records automatically override ToString() to print all property names and values, unlike a plain class which would print just its type name.

Exercise 18: Comparing Records with ==

Practice Problem: Create two instances of a Book record with identical properties. Prove that book1 == book2 evaluates to true, unlike standard classes.

Purpose: This exercise helps you practice understanding value equality in records, where the == operator compares property values instead of object references.

Given Input: book1 = new Book("1984", "George Orwell"), book2 = new Book("1984", "George Orwell")

Expected Output:

Comparing two record instances with identical values:
book1 == book2: True
Equality check completed successfully.
▼ Hint
  • Declare a positional record with Title and Author.
  • Create two separate instances with the exact same property values.
  • Compare them using ==; records compare by value automatically, so the result will be true even though they are two different object instances.
▼ Solution & Explanation
using System;

namespace RecordUtilities
{
    record Book(string Title, string Author);

    class EqualityDemo
    {
        static void Main(string[] args)
        {
            Book book1 = new Book("1984", "George Orwell");
            Book book2 = new Book("1984", "George Orwell");

            Console.WriteLine("Comparing two record instances with identical values:");

            bool areEqual = book1 == book2;

            Console.WriteLine($"book1 == book2: {areEqual}");
            Console.WriteLine("Equality check completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • record Book(string Title, string Author);: Declares a record, which automatically generates value-based equality members.
  • book1 == book2: Compares the two instances using the == operator, which records overload to check whether all properties are equal, rather than checking reference identity.
  • With a standard class: The same comparison would return false, since two separately created objects have different references even if their properties match.
  • Automatic behavior: Records generate this equality logic for you, without requiring you to override Equals() or == yourself.

Exercise 19: Copying a Record with ‘with’

Practice Problem: Create a UserAccount record with Username, Email, and IsActive. Use the with expression to create a copy of the user but with IsActive set to false.

Purpose: This exercise helps you practice non-destructive mutation, where you produce a modified copy of a record instead of changing the original.

Given Input: originalUser = new UserAccount("jdoe", "jdoe@example.com", true)

Expected Output:

Creating a modified copy using the 'with' expression:
Original: UserAccount { Username = jdoe, Email = jdoe@example.com, IsActive = True }
Copy: UserAccount { Username = jdoe, Email = jdoe@example.com, IsActive = False }
Non-destructive mutation completed successfully.
▼ Hint
  • Declare a positional record with Username, Email, and IsActive.
  • Create one instance, then use the with keyword to produce a modified copy: originalUser with { IsActive = false }.
  • Confirm that the original instance is unchanged and only the new copy reflects the updated value.
▼ Solution & Explanation
using System;

namespace RecordUtilities
{
    record UserAccount(string Username, string Email, bool IsActive);

    class WithExpressionDemo
    {
        static void Main(string[] args)
        {
            UserAccount originalUser = new UserAccount("jdoe", "jdoe@example.com", true);

            Console.WriteLine("Creating a modified copy using the 'with' expression:");
            Console.WriteLine($"Original: {originalUser}");

            UserAccount deactivatedUser = originalUser with { IsActive = false };

            Console.WriteLine($"Copy: {deactivatedUser}");
            Console.WriteLine("Non-destructive mutation completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • record UserAccount(string Username, string Email, bool IsActive);: Declares an immutable record with three properties.
  • originalUser with { IsActive = false }: Creates a brand new UserAccount instance, copying all properties from originalUser except IsActive, which is overridden to false.
  • originalUser remains unchanged: Since records are immutable by default, the with expression never modifies the original instance, it always produces a new one.
  • Why this matters: This pattern lets you “update” a record’s data without breaking immutability, since you get a new object rather than mutating the existing one.

Exercise 20: Record Struct vs Record Class

Practice Problem: Define a PointRecordStruct as a record struct and a PointRecordClass as a standard record. Modify one and observe how value vs reference semantics apply.

Purpose: This exercise helps you practice the core difference between record structs and record classes: assigning a struct copies its data, while assigning a class copies only a reference to the same data.

Given Input: structOriginal = new PointRecordStruct { X = 1, Y = 1 }, classOriginal = new PointRecordClass { X = 1, Y = 1 }

Expected Output:

Comparing record struct (value type) and record class (reference type):
Struct Original: PointRecordStruct { X = 1, Y = 1 }
Struct Copy: PointRecordStruct { X = 100, Y = 1 }
Class Original: PointRecordClass { X = 100, Y = 1 }
Class Reference: PointRecordClass { X = 100, Y = 1 }
Comparison completed successfully.
▼ Hint
  • Declare one type as a record struct and another as a standard record, both with the same properties.
  • Assign each original instance to a second variable, then modify a property through the second variable.
  • For the struct, the original stays unchanged; for the class, both variables reflect the change since they reference the same object.
▼ Solution & Explanation
using System;

namespace RecordUtilities
{
    record struct PointRecordStruct
    {
        public int X { get; set; }
        public int Y { get; set; }
    }

    record class PointRecordClass
    {
        public int X { get; set; }
        public int Y { get; set; }
    }

    class RecordStructVsClassDemo
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Comparing record struct (value type) and record class (reference type):");

            PointRecordStruct structOriginal = new PointRecordStruct { X = 1, Y = 1 };
            PointRecordStruct structCopy = structOriginal;
            structCopy.X = 100;

            Console.WriteLine($"Struct Original: {structOriginal}");
            Console.WriteLine($"Struct Copy: {structCopy}");

            PointRecordClass classOriginal = new PointRecordClass { X = 1, Y = 1 };
            PointRecordClass classReference = classOriginal;
            classReference.X = 100;

            Console.WriteLine($"Class Original: {classOriginal}");
            Console.WriteLine($"Class Reference: {classReference}");

            Console.WriteLine("Comparison completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • record struct PointRecordStruct { ... }: Declares a record with value-type semantics; each variable holds its own independent copy of the data.
  • record class PointRecordClass { ... }: Declares a record with reference-type semantics; variables hold references to the same underlying object.
  • PointRecordStruct structCopy = structOriginal;: Copies the entire struct’s data into a new independent value, so structCopy and structOriginal are unrelated afterward.
  • PointRecordClass classReference = classOriginal;: Copies only the reference, so classReference and classOriginal point to the exact same object in memory.
  • Result of modifying each copy: Changing structCopy.X leaves structOriginal untouched, while changing classReference.X also changes classOriginal, since both variables share the same object.

Exercise 21: Inheriting from a Record

Practice Problem: Create a base record Vehicle (Make, Model) and a derived record ElectricCar that adds BatteryCapacity. Ensure value equality still works accurately.

Purpose: This exercise helps you practice record inheritance, where a derived record extends a base record’s properties while preserving value-based equality across the entire hierarchy.

Given Input: car1 = new ElectricCar("Tesla", "Model 3", 75), car2 = new ElectricCar("Tesla", "Model 3", 75)

Expected Output:

Comparing two derived record instances:
Car 1: ElectricCar { Make = Tesla, Model = Model 3, BatteryCapacity = 75 }
Car 2: ElectricCar { Make = Tesla, Model = Model 3, BatteryCapacity = 75 }
car1 == car2: True
Record inheritance check completed successfully.
▼ Hint
  • Declare a base record with the shared properties.
  • Declare a derived record that inherits from the base, passing the shared properties to the base constructor and adding its own new property.
  • Create two derived instances with identical values and compare them with ==; the equality check considers every property, including inherited ones.
▼ Solution & Explanation
using System;

namespace RecordUtilities
{
    record Vehicle(string Make, string Model);

    record ElectricCar(string Make, string Model, int BatteryCapacity) : Vehicle(Make, Model);

    class RecordInheritanceDemo
    {
        static void Main(string[] args)
        {
            ElectricCar car1 = new ElectricCar("Tesla", "Model 3", 75);
            ElectricCar car2 = new ElectricCar("Tesla", "Model 3", 75);

            Console.WriteLine("Comparing two derived record instances:");
            Console.WriteLine($"Car 1: {car1}");
            Console.WriteLine($"Car 2: {car2}");

            bool areEqual = car1 == car2;

            Console.WriteLine($"car1 == car2: {areEqual}");
            Console.WriteLine("Record inheritance check completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • record Vehicle(string Make, string Model);: Declares a base record with two shared properties.
  • record ElectricCar(...) : Vehicle(Make, Model);: Declares a derived record that inherits from Vehicle, passing Make and Model up to the base constructor while adding its own BatteryCapacity property.
  • car1 == car2: Compares every property across the entire inheritance chain, including both the inherited Make and Model and the derived BatteryCapacity.
  • Result: Because both instances have identical values for all three properties, including the inherited ones, the equality check returns true, showing that records preserve value equality even through inheritance.

Exercise 22: Deconstructing a Record

Practice Problem: Create a Student record with Name, Grade, and Major. Write code that deconstructs an instance of the student into individual variables in a single line.

Purpose: This exercise helps you practice using the automatic Deconstruct method that positional records generate, unpacking a record’s properties into separate variables.

Given Input: student = new Student("Alice", "A", "Computer Science")

Expected Output:

Deconstructing Student { Name = Alice, Grade = A, Major = Computer Science }:
Name = Alice
Grade = A
Major = Computer Science
Deconstruction completed successfully.
▼ Hint
  • Declare a positional record with three properties.
  • Deconstruct an instance using tuple-like syntax: var (name, grade, major) = student;
  • The values are unpacked in the same order the properties were declared.
▼ Solution & Explanation
using System;

namespace RecordUtilities
{
    record Student(string Name, string Grade, string Major);

    class DeconstructionDemo
    {
        static void Main(string[] args)
        {
            Student student = new Student("Alice", "A", "Computer Science");

            Console.WriteLine($"Deconstructing {student}:");

            var (name, grade, major) = student;

            Console.WriteLine($"Name = {name}");
            Console.WriteLine($"Grade = {grade}");
            Console.WriteLine($"Major = {major}");
            Console.WriteLine("Deconstruction completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • record Student(string Name, string Grade, string Major);: Declares a positional record, which automatically generates a Deconstruct method matching its constructor parameters.
  • var (name, grade, major) = student;: Calls the generated Deconstruct method, unpacking all three properties into separate local variables in a single line.
  • name, grade, and major: Behave as ordinary local variables afterward, holding copies of the record’s property values.
  • Automatic behavior: This deconstruction syntax works for any positional record, without any extra code required.

Exercise 23: Making a Record Mutable

Practice Problem: Create a record that uses standard properties with { get; set; } instead of init. Demonstrate how it breaks traditional record immutability.

Purpose: This exercise helps you practice recognizing that records are not immutable by default just because they’re records, immutability comes from using init-only properties, which you can opt out of.

Given Input: user = new MutableUser { Name = "Alex", Age = 30 }

Expected Output:

Before mutation: MutableUser { Name = Alex, Age = 30 }
After mutation: MutableUser { Name = Alex, Age = 31 }
Mutable record demonstration completed successfully.
▼ Hint
  • Declare the record’s properties using { get; set; } instead of the default init-only pattern.
  • Create an instance using object initializer syntax.
  • Reassign a property directly after construction to show that the record is no longer immutable.
▼ Solution & Explanation
using System;

namespace RecordUtilities
{
    record MutableUser
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }

    class MutableRecordDemo
    {
        static void Main(string[] args)
        {
            MutableUser user = new MutableUser { Name = "Alex", Age = 30 };

            Console.WriteLine($"Before mutation: {user}");

            user.Age = 31;

            Console.WriteLine($"After mutation: {user}");
            Console.WriteLine("Mutable record demonstration completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • public string Name { get; set; }: Uses a standard settable property instead of the init keyword that positional records normally generate.
  • new MutableUser { Name = "Alex", Age = 30 }: Uses object initializer syntax to set the properties at creation time.
  • user.Age = 31;: Directly reassigns a property after the object has already been constructed, something that is not allowed with init-only properties.
  • Why this matters: Records only guarantee value-based equality by default, not immutability itself; using { get; set; } instead of { get; init; } removes the immutability that positional records normally provide out of the box.

Exercise 24: Validating Data in a Record

Practice Problem: Create a SmartHomeDevice record. Customize the positional constructor to throw an ArgumentException if the DeviceName is null or empty.

Purpose: This exercise helps you practice adding custom validation logic to a record’s constructor, ensuring invalid data can never be used to construct an instance.

Given Input: A valid device new SmartHomeDevice("Living Room Light", "Light"), and an invalid attempt new SmartHomeDevice("", "Thermostat")

Expected Output:

Creating a valid smart home device:
SmartHomeDevice { DeviceName = Living Room Light, DeviceType = Light }
Attempting to create a device with an empty name:
Caught Exception: Device name cannot be null or empty. (Parameter 'deviceName')
Validation demonstration completed successfully.
▼ Hint
  • Give the record explicit get-only properties and a custom constructor instead of using the positional syntax directly.
  • Inside the constructor, check if the name is null or empty using string.IsNullOrEmpty() before assigning it.
  • Throw an ArgumentException with a descriptive message if the check fails, and wrap the invalid creation attempt in a try/catch block to observe it.
▼ Solution & Explanation
using System;

namespace RecordUtilities
{
    record SmartHomeDevice
    {
        public string DeviceName { get; }
        public string DeviceType { get; }

        public SmartHomeDevice(string deviceName, string deviceType)
        {
            if (string.IsNullOrEmpty(deviceName))
            {
                throw new ArgumentException("Device name cannot be null or empty.", nameof(deviceName));
            }

            DeviceName = deviceName;
            DeviceType = deviceType;
        }
    }

    class ValidationDemo
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Creating a valid smart home device:");
            SmartHomeDevice validDevice = new SmartHomeDevice("Living Room Light", "Light");
            Console.WriteLine(validDevice);

            Console.WriteLine("Attempting to create a device with an empty name:");
            try
            {
                SmartHomeDevice invalidDevice = new SmartHomeDevice("", "Thermostat");
            }
            catch (ArgumentException ex)
            {
                Console.WriteLine($"Caught Exception: {ex.Message}");
            }

            Console.WriteLine("Validation demonstration completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • public SmartHomeDevice(string deviceName, string deviceType): A custom constructor that adds validation logic before assigning the properties, something a plain positional record wouldn’t do by default.
  • string.IsNullOrEmpty(deviceName): Checks whether the provided name is null or an empty string.
  • throw new ArgumentException(...): Immediately stops object construction and signals to the caller that invalid data was provided.
  • try / catch (ArgumentException ex): Demonstrates safely handling the exception when an invalid device name is passed, preventing the program from crashing.

Exercise 25: Nested Records and Equality

Practice Problem: Create a WeatherReport record that contains nested data, such as a Temperature struct inside it. Test if the value equality checks all the way down into the nested struct.

Purpose: This exercise helps you practice recognizing that record equality is recursive, it compares nested value types by their own value equality too, not just by reference.

Given Input: report1 = new WeatherReport("Seattle", new Temperature { Celsius = 15.5 }), report2 = new WeatherReport("Seattle", new Temperature { Celsius = 15.5 })

Expected Output:

Comparing two weather reports with nested temperature data:
Report 1: WeatherReport { City = Seattle, CurrentTemperature = 15.5°C }
Report 2: WeatherReport { City = Seattle, CurrentTemperature = 15.5°C }
report1 == report2: True
Nested equality check completed successfully.
▼ Hint
  • Define a small struct to represent the nested data, such as Temperature with a Celsius field.
  • Declare the record with the struct as one of its properties.
  • Compare two record instances that have identical nested struct values using ==; since structs also use value-based equality by default, the outer comparison will correctly return true.
▼ Solution & Explanation
using System;

namespace RecordUtilities
{
    struct Temperature
    {
        public double Celsius;

        public override string ToString()
        {
            return $"{Celsius}°C";
        }
    }

    record WeatherReport(string City, Temperature CurrentTemperature);

    class NestedEqualityDemo
    {
        static void Main(string[] args)
        {
            WeatherReport report1 = new WeatherReport("Seattle", new Temperature { Celsius = 15.5 });
            WeatherReport report2 = new WeatherReport("Seattle", new Temperature { Celsius = 15.5 });

            Console.WriteLine("Comparing two weather reports with nested temperature data:");
            Console.WriteLine($"Report 1: {report1}");
            Console.WriteLine($"Report 2: {report2}");

            bool areEqual = report1 == report2;

            Console.WriteLine($"report1 == report2: {areEqual}");
            Console.WriteLine("Nested equality check completed successfully.");
        }
    }
}Code language: C# (cs)

Explanation:

  • struct Temperature { ... }: A simple value type holding a single Celsius field, with its ToString() overridden for readable output.
  • record WeatherReport(string City, Temperature CurrentTemperature);: A positional record whose second property is itself a struct, rather than a primitive type.
  • report1 == report2: Compares every property, including CurrentTemperature. Since Temperature is a struct, its default equality (inherited from ValueType) compares the values of its own fields.
  • Result: Because the nested Celsius field is identical in both structs, and the City strings match too, the outer record equality check returns true, confirming that record value equality reaches all the way down into nested value types.

Filed Under: C# Exercises

Did you find this page helpful? Let others know about it. Sharing helps me continue to create free Python resources.

TweetF  sharein  shareP  Pin

About Vishal

I’m Vishal Hule, the Founder of PYnative.com. As a Python developer, I enjoy assisting students, developers, and learners. Follow me on Twitter.

Related Tutorial Topics:

C# Exercises

All Coding Exercises:

C Exercises
C++ Exercises
Python Exercises
Java Exercises
C# Exercises

Python Exercises and Quizzes

Free coding exercises and quizzes cover Python basics, data structure, data analytics, and more.

  • 15+ Topic-specific Exercises and Quizzes
  • Each Exercise contains 25+ questions
  • Each Quiz contains 25 MCQ
Exercises
Quizzes

Leave a Reply Cancel reply

your email address will NOT be published. all comments are moderated according to our comment policy.

Use <pre> tag for posting code. E.g. <pre> Your entire code </pre>

In: C# Exercises
TweetF  sharein  shareP  Pin

  C# Exercises

  • C# Exercise for Beginners
  • C# Loops Exercise
  • C# Array Exercise
  • C# String Exercise
  • C# OOP Exercise
  • C# Structs, Records and Enums Exercise
  • C# Collections Exercise
  • C# List Exercise
  • C# Dictionary Exercise
  • C# LINQ Exercise
  • C# Exception Handling Exercise
  • C# File Handling Exercise
  • C# Date and Time Exercise
  • C# Generics Exercise
  • C# Lambda Expressions Exercise
  • C# Delegates and Events Exercise
  • C# Extension Methods Exercise
  • C# Regex Exercise
  • C# Pattern Matching Exercise
  • C# Iterators and Yield Exercise
  • C# Indexers and Operator Overloading Exercise
  • C# Random Data Generation Exercise

All Coding Exercises

Python Exercises C Exercises C++ Exercises Java Exercises C# Exercises

About PYnative

PYnative.com is for Python lovers. Here, You can get Tutorials, Exercises, and Quizzes to practice and improve your Python skills.

Follow Us

To get New Python Tutorials, Exercises, and Quizzes

  • Twitter
  • Facebook
  • Sitemap

Explore Python

  • Learn Python
  • Python Basics
  • Python Databases
  • Python Exercises
  • Python Quizzes
  • Online Python Code Editor
  • Python Tricks

Coding Exercises

  • C Exercises
  • C++ Exercises
  • Python Exercises
  • Java Exercises
  • C# Exercises

Legal Stuff

  • About Us
  • Contact Us

We use cookies to improve your experience. While using PYnative, you agree to have read and accepted our:

  • Terms Of Use
  • Privacy Policy
  • Cookie Policy

Copyright © 2018–2026 pynative.com