Records in C# are a game-changer when it comes to defining data-centric types. They offer built-in features like value-based equality, immutability, and a concise syntax. While the primary constructor is great for defining essential properties, you might need to extend records with optional properties, methods, or derived values for more complex scenarios.
In this article, we’ll explore how to enhance records by adding additional members and discuss scenarios where they shine. We’ll include clear examples and practical assignments to deepen your understanding.
Why Extend a Record?
The primary constructor in a record defines the minimum information needed to create an instance. However, there are cases where you might need:
- Derived Properties: Compute values based on existing properties.
- Optional Fields: Handle data that isn’t always required during initialization.
- Custom Methods: Add functionality specific to the record’s purpose.
C# allows records to be flexible by defining a body where you can add fields, properties, and methods.
Adding Derived Properties
Derived properties are useful when you need to compute values based on other properties. Let’s define a Book
record with Title
and Author
as required fields. We’ll also add a computed property, DisplayTitle
, that combines them for display purposes.
public record Book(string Title, string Author)
{
// Derived property
public string DisplayTitle => $"{Title} by {Author}";
}
Usage Example:
var book = new Book("The Great Gatsby", "F. Scott Fitzgerald");
Console.WriteLine(book.DisplayTitle); // Output: The Great Gatsby by F. Scott Fitzgerald
Adding Optional Properties with init
Optional properties aren’t always a part of the primary constructor. Using the init
keyword, you can define properties that can only be set during initialization and remain immutable afterward.
Here’s an example with a Car
record:
public record Car(string Make, string Model)
{
// Optional immutable property
public int? Year { get; init; }
}
Usage Example:
var car = new Car("Tesla", "Model S") { Year = 2021 };
Console.WriteLine($"{car.Make} {car.Model}, Year: {car.Year}");
// Output: Tesla Model S, Year: 2021
// Attempting to modify 'Year' after initialization will cause a compiler error:
// car.Year = 2022; // Error
The init
keyword ensures that optional properties are immutable after being initialized.
Using Nested Records for Complex Data
Nested records are a powerful way to handle complex data. Let’s define a Customer
record with a nested Address
record:
public record Address(string Street, string City);
public record Customer(string Name)
{
// Optional property with a nested record
public Address? Address { get; init; }
}
Usage Example:
var customer = new Customer("Alice")
{
Address = new Address("123 Elm Street", "Springfield")
};
Console.WriteLine($"{customer.Name} lives at {customer.Address?.Street}, {customer.Address?.City}");
// Output: Alice lives at 123 Elm Street, Springfield
The nested Address
record integrates seamlessly, allowing for clean and concise data structures.
Adding Custom Methods
Custom methods let you add specific functionality to records. For instance, let’s add a method to a Transaction
record to calculate the total cost:
public record Transaction(string Item, int Quantity, decimal UnitPrice)
{
// Method to calculate the total cost
public decimal CalculateTotal() => Quantity * UnitPrice;
}
Usage Example:
var transaction = new Transaction("Laptop", 2, 1500.00m);
Console.WriteLine(transaction.CalculateTotal()); // Output: 3000.00
This method encapsulates behavior directly within the record, keeping the logic close to the data.
Immutability with with
Expressions
While records are immutable, you can create new instances with modified properties using the with
expression. This is particularly useful when you want to update data while preserving the original object.
Let’s enhance the Customer
record with this feature:
var originalCustomer = new Customer("Alice")
{
Address = new Address("123 Elm Street", "Springfield")
};
var updatedCustomer = originalCustomer with { Address = new Address("456 Oak Avenue", "Shelbyville") };
Console.WriteLine(originalCustomer.Address?.Street); // Output: 123 Elm Street
Console.WriteLine(updatedCustomer.Address?.Street); // Output: 456 Oak Avenue
The with
expression ensures immutability while allowing flexibility for updates.
Assignments to Test Your Understanding
Easy Level
- Create a
Movie
record with:-
Title
andDirector
as primary constructor properties. - A computed property
Description
that combines both.
-
Example Output:
var movie = new Movie("Inception", "Christopher Nolan");
Console.WriteLine(movie.Description); // Output: Inception by Christopher Nolan
- Add an optional property
Rating
usinginit
to theMovie
record.
Medium Level
- Define a
Product
record with:-
Name
andPrice
in the primary constructor. - A method
ApplyDiscount(decimal percentage)
that returns a newProduct
with the discounted price.
-
Example Output:
var product = new Product("Laptop", 2000.00m);
var discountedProduct = product.ApplyDiscount(10);
Console.WriteLine(discountedProduct.Price); // Output: 1800.00
- Add a nested record
Category
to represent the product category, and integrate it into theProduct
record.
Difficult Level
- Create an
Order
record:- Include
OrderId
,ItemName
,Quantity
, andUnitPrice
in the primary constructor. - Add a computed property
TotalPrice
that calculates the total. - Add a method
UpdateQuantity(int newQuantity)
that returns a newOrder
with the updated quantity.
- Include
Example Output:
var order = new Order("ORD001", "Tablet", 2, 500.00m);
Console.WriteLine(order.TotalPrice); // Output: 1000.00
var updatedOrder = order.UpdateQuantity(5);
Console.WriteLine(updatedOrder.TotalPrice); // Output: 2500.00
When to Use Records vs. Classes
-
Use records for:
- Immutable data-centric types.
- Scenarios requiring value-based equality.
- Clean and compact definitions.
-
Use classes for:
- Types with significant behavior or mutable state.
- Complex hierarchies and relationships.
Conclusion
Extending records with additional members, computed properties, and methods allows you to create versatile and maintainable data models. By leveraging features like the init
keyword and with
expressions, you can balance immutability and flexibility.
Top comments (0)