"Learn how to customize compiler-generated methods in C# records, including ToString
, PrintMembers
, Equals
, and GetHashCode
. This detailed guide features full code examples to help you enhance string representations, implement advanced equality logic, and tailor records to your specific needs."
C# records are a powerful feature for creating immutable data types with value-based equality. By default, the compiler generates several methods for records, including ToString
, Equals
, GetHashCode
, and PrintMembers
. While the default implementations are usually sufficient, certain scenarios may require customization to meet specific needs.
This article explores how to customize these compiler-generated methods with clear explanations and full code examples.
Overview of Compiler-Generated Methods in Records
When you define a record in C#, the compiler automatically provides:
-
ToString
: Creates a string representation of the record, including its type name and properties. -
Equals
: Compares records based on their property values. -
GetHashCode
: Generates a hash code derived from the record's property values. -
PrintMembers
: Used internally byToString
to format the string representation of the record's properties.
These methods enable records to behave predictably and efficiently. However, there are times when the default behavior needs to be adjusted.
Customizing ToString
The ToString
method provides a string representation of a record, including the type name and property values. You might want to customize this behavior for more readable or domain-specific output.
Example: Customizing ToString
Here’s how you can override ToString
for a Product
record and its derived record DiscountedProduct
:
using System;
public record Product(int Id, string Name, decimal Price)
{
public override string ToString() => $"Product: {Name} (ID: {Id}), Price: ${Price:F2}";
}
public record DiscountedProduct(int Id, string Name, decimal Price, decimal Discount) : Product(Id, Name, Price)
{
public override string ToString() =>
$"{base.ToString()}, Discount: ${Discount:F2}, Final Price: ${(Price - Discount):F2}";
}
class Program
{
static void Main()
{
var product = new Product(1, "Book", 25.99m);
var discountedProduct = new DiscountedProduct(2, "Laptop", 999.99m, 100m);
Console.WriteLine(product);
Console.WriteLine(discountedProduct);
}
}
Output:
Product: Book (ID: 1), Price: $25.99
Product: Laptop (ID: 2), Price: $999.99, Discount: $100.00, Final Price: $899.99
By overriding ToString
, you can tailor the string output to your specific requirements.
Customizing PrintMembers
The ToString
method relies on the PrintMembers
method to format the properties of a record. Customizing PrintMembers
provides more control over how properties are represented in the output.
Example: Customizing PrintMembers
Here’s how you can override PrintMembers
to control the output:
using System;
using System.Text;
public record Product(int Id, string Name, decimal Price)
{
protected virtual bool PrintMembers(StringBuilder builder)
{
builder.Append($"ID = {Id}, Name = {Name}, Price = ${Price:F2}");
return true;
}
public override string ToString()
{
var builder = new StringBuilder();
builder.Append(GetType().Name).Append(" { ");
if (PrintMembers(builder)) builder.Append(" }");
return builder.ToString();
}
}
public record DiscountedProduct(int Id, string Name, decimal Price, decimal Discount) : Product(Id, Name, Price)
{
protected override bool PrintMembers(StringBuilder builder)
{
base.PrintMembers(builder);
builder.Append($", Discount = ${Discount:F2}, Final Price = ${(Price - Discount):F2}");
return true;
}
}
class Program
{
static void Main()
{
var product = new Product(1, "Book", 25.99m);
var discountedProduct = new DiscountedProduct(2, "Laptop", 999.99m, 100m);
Console.WriteLine(product);
Console.WriteLine(discountedProduct);
}
}
Output:
Product { ID = 1, Name = Book, Price = $25.99 }
DiscountedProduct { ID = 2, Name = Laptop, Price = $999.99, Discount = $100.00, Final Price = $899.99 }
This approach provides granular control over how properties are formatted when ToString
is called.
Customizing Equals
and GetHashCode
Customizing Equals
and GetHashCode
can be useful for advanced scenarios where the default value-based equality isn’t sufficient.
Example: Customizing Equals
and GetHashCode
Here’s how you can customize these methods for the Product
record:
using System;
public record Product(int Id, string Name, decimal Price)
{
public override bool Equals(object? obj) =>
obj is Product product &&
Id == product.Id &&
Name == product.Name &&
Price == product.Price;
public override int GetHashCode() => HashCode.Combine(Id, Name, Price);
}
class Program
{
static void Main()
{
var product1 = new Product(1, "Book", 25.99m);
var product2 = new Product(1, "Book", 25.99m);
var product3 = new Product(2, "Laptop", 999.99m);
Console.WriteLine(product1.Equals(product2)); // Output: True
Console.WriteLine(product1.Equals(product3)); // Output: False
}
}
Output:
True
False
This example ensures that equality is based only on the Id
, Name
, and Price
properties.
When to Customize Methods in Records
Scenarios for Customization
- Logging: Provide detailed and clear information for debugging or monitoring.
- Improved Readability: Tailor the string representation to be user-friendly or domain-specific.
- Advanced Formatting: Include derived or computed values in the output.
Best Practices
- Use
ToString
for general-purpose string representation. - Leverage
PrintMembers
for fine-grained control over string formatting. - Override
Equals
andGetHashCode
only when necessary.
Conclusion
C# records simplify immutable data types by generating methods like ToString
, Equals
, GetHashCode
, and PrintMembers
. While the default implementations work for most use cases, customizing these methods can provide better control, especially for logging, debugging, and specific domain requirements.
The examples provided in this article demonstrate how to override and customize these methods effectively. Experiment with the code to explore further possibilities and tailor records to your needs.
Top comments (0)