[C#] Enhancing Robustness and Equality Checks with record Types

The record type, introduced in C# 9.0, is a reference type specialized for representing collections of data.

When using traditional class types, ensuring data equality (treating objects as “equal” if all values are the same) and immutability required writing a lot of boilerplate code. Using record types allows you to define objects with “value semantics” using concise syntax.

Here, using a product management system as an example, I will explain the basic definition, inheritance, and equality behavior of record types, which differ from class types.

目次

Features and Benefits of record Types

The main benefits of adopting record types are:

  • Value-based Equality: Equality is determined by whether all property values match, not by the instance reference (memory address).
  • Immutability: When using a primary constructor, properties become init-only by default (can only be initialized once).
  • Concise Syntax: Constructors, Deconstruct, ToString, Equals, and GetHashCode are automatically generated.

Practical Code Example: Defining Product Data and Comparing Equality

The following code defines a base “Product” record and a “Book” record that inherits from it, and then checks the comparison results for each.

using System;

namespace ProductManagement
{
    // Base Product record
    // Declare properties using a primary constructor.
    // This automatically makes Id and Name init-only properties.
    public record Product(int Id, string Name);

    // Book record inheriting from Product
    public record Book(int Id, string Name, string Author) : Product(Id, Name);

    class Program
    {
        static void Main()
        {
            // 1. Create separate instances with the same values
            var product1 = new Product(1001, "High-Performance Mouse");
            var product2 = new Product(1001, "High-Performance Mouse");

            // 2. Create an instance of the inherited record
            var book1 = new Book(2001, "Intro to C#", "Taro Yamada");
            
            // Copy reference (points to the same instance)
            var productReference = product1;

            // --- Output Verification Results ---

            // Check automatically generated ToString()
            // Unlike classes, property values are output.
            Console.WriteLine("--- Result of ToString() ---");
            Console.WriteLine($"product1: {product1}");
            Console.WriteLine($"book1   : {book1}");
            Console.WriteLine();

            Console.WriteLine("--- Equality Checks ---");

            // A. Value-based equality check (== operator)
            // Returns True if all property values are the same, even for different instances.
            bool isValueEqual = (product1 == product2);
            Console.WriteLine($"product1 == product2 : {isValueEqual}");

            // B. Reference-based equality check (Object.ReferenceEquals)
            // Returns False because they are different instances in memory.
            bool isReferenceEqual = Object.ReferenceEquals(product1, product2);
            Console.WriteLine($"ReferenceEquals(p1, p2) : {isReferenceEqual}");

            // C. Check when references are the same
            Console.WriteLine($"product1 == productReference : {product1 == productReference}");
            Console.WriteLine($"ReferenceEquals(p1, pRef) : {Object.ReferenceEquals(product1, productReference)}");

            // D. Check when types are different
            // Even if some properties match, it returns False if the types differ.
            // (Note: Compiler warnings may occur, but comparison is possible)
            Console.WriteLine($"product1 != book1 : {product1 != book1}"); // Becomes True (Not equal)

            Console.WriteLine();
            Console.WriteLine("--- HashCode Verification ---");
            // If values are the same, the GetHashCode result will be the same.
            Console.WriteLine($"product1 Hash: {product1.GetHashCode()}");
            Console.WriteLine($"product2 Hash: {product2.GetHashCode()}");
        }
    }
}

Execution Result

--- Result of ToString() ---
product1: Product { Id = 1001, Name = High-Performance Mouse }
book1   : Book { Id = 2001, Name = Intro to C#, Author = Taro Yamada }

--- Equality Checks ---
product1 == product2 : True
ReferenceEquals(p1, p2) : False
product1 == productReference : True
ReferenceEquals(p1, pRef) : True
product1 != book1 : True

--- HashCode Verification ---
product1 Hash: 1860543227
product2 Hash: 1860543227

Technical Points

1. Behaving as a Value While Being a Reference Type

Internally, record is compiled as a class (unless explicitly declared as record struct). However, since the compiler automatically adds the IEquatable<T> implementation and == operator overloading, developers can naturally treat them as “Value Objects.”

2. Instance Identification

In normal classes, different instances are treated as “different” even if properties like ID are the same. However, as shown in the execution results, product1 == product2 evaluates to True for records. This makes comparing DTOs (Data Transfer Objects) and API responses very easy.

3. Automatic HashCode Generation

The GetHashCode method is also overridden to calculate values based on properties. Therefore, when using record types as keys in HashSet<T> or Dictionary<TKey, TValue>, they are correctly recognized as the same key if the values are identical.

Summary

Using record types not only simplifies class definitions for data containers but also enables robust design based on value equality. It is a feature that should be actively adopted for domain model Value Objects and data structures requiring immutability.

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次