Поиск  
Always will be ready notify the world about expectations as easy as possible: job change page
Feb 2

C# Records vs Classes

C# Records vs Classes
Источник:
Просмотров:
2492

With the release of C# 10 in November 2021 developers were introduced to a new concept called records, in this post I’ll outline some of the key differences between records and classes.

For this comparison I’ll only consider the default/common approach for creating class’ and records’, since a record is really just syntactic sugar on top of a class / struct the behavior is not isolated to records but by default they behave differently.

The record I’ll use for comparison is:

public record PointRecord(int X, int Y);

and the class is:

public class PointClass
{   
    public int X { get; set; }

    public int Y { get; set; }    
}

1. Mutability

Records are immutable because the properties are only settable when the record is created. A class can behave in a similar way if you use { get; init; } instead of { get; set; }

Properties of a class instance can be updated after its created
Properties of a class instance can be updated after its created.

Properties can’t be updated and will result in a compile time error
Properties can’t be updated and will result in a compile time error.

Records also support Nondestructive mutation which means that a record can be cloned and properties can be changed using the object initialize syntax for example:

var point1 = new PointRecord(1, 2);
var point2 = point1 with { X = 2 };

This would be equivalent creating a new record with X = 2 and Y = 2 as the value for Y is copied from point1.

2. Equality

Class equality is by reference and record equality is by value. This means that if two point classes are created with the same properties they will not be equal because they have different references.

Equality

If we test the same behavior with records we’ll find that they’ll be equal.

Records equality

You can easily override the equality behavior of a class to be value based by overriding the equals method.

3. Deconstruction

Deconstruction is a process of splitting a variable value into parts and storing them into new variables. This could be useful when a variable stores multiple values such as a tuple.

For example consider enumerating a list of point classes.

var points = new List<PointClass>();

foreach (var point in points)
{
    var z = point.X * point.Y;
}

The only way to access the properties X and Y is via the point instance. For records this can be simplified using deconstruction.

var points = new List<PointRecord>();

foreach (var (x, y) in points)
{
    var z = x * y;
}

A class can easily support deconstruction by adding a method named Deconstruct:

public class PointClass
{   
    public int X { get; set; }

    public int Y { get; set; }
    
    public void Deconstruct(out int x, out int y)
    {
        x = this.X;
        y = this.Y;
    }
}

4. ToString()

ToString() on a class will return the class name unless overridden.

ToString()

ToString() is overridden in records to print the class name as well as its properties. This is helpful for debugging as the values are easily visualized.

ToString() debugging

5. GetHashCode()

Hash codes for reference types are computed by calling the Object.GetHashCode method of the base class, which computes a hash code based on an object’s reference if not overridden. For records the has code is based on the value of its properties. This means that two class instances with the same properties will have different hash codes and two records with the same values will have the same hash code.

Class

Class

Record

Record

Lowered C# of a record

Records are really just syntactic sugar on a class which is generated at compile time. Below is the lowered C# of the point record which shows the implementation details and explains why record classes behave differently from classes. The key overridden methods are:

  • public override string ToString()
  • public override int GetHashCode()
  • public override bool Equals(object obj)
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;

[NullableContext(1)]
[Nullable(0)]
public class PointRecord :
/*[Nullable(0)]*/
IEquatable<PointRecord>
{
  [CompilerGenerated]
  [DebuggerBrowsable(DebuggerBrowsableState.Never)]
  private int <X>k__BackingField;
  [CompilerGenerated]
  [DebuggerBrowsable(DebuggerBrowsableState.Never)]
  private int <Y>k__BackingField;

  [CompilerGenerated]
  protected virtual Type EqualityContract
  {
    [CompilerGenerated] get
    {
      return typeof (PointRecord);
    }
  }

  public int X
  {
    [CompilerGenerated] get
    {
      return this.<X>k__BackingField;
    }
    [CompilerGenerated] set
    {
      this.<X>k__BackingField = value;
    }
  }

  public int Y
  {
    [CompilerGenerated] get
    {
      return this.<Y>k__BackingField;
    }
    [CompilerGenerated] set
    {
      this.<Y>k__BackingField = value;
    }
  }

  [CompilerGenerated]
  public override string ToString()
  {
    StringBuilder builder = new StringBuilder();
    builder.Append("PointRecord");
    builder.Append(" { ");
    if (this.PrintMembers(builder))
      builder.Append(' ');
    builder.Append('}');
    return builder.ToString();
  }

  [CompilerGenerated]
  protected virtual bool PrintMembers(StringBuilder builder)
  {
    RuntimeHelpers.EnsureSufficientExecutionStack();
    builder.Append("X = ");
    builder.Append(this.X.ToString());
    builder.Append(", Y = ");
    builder.Append(this.Y.ToString());
    return true;
  }

  [NullableContext(2)]
  [CompilerGenerated]
  [SpecialName]
  public static bool op_Inequality(PointRecord left, PointRecord right)
  {
    return !PointRecord.op_Equality(left, right);
  }

  [NullableContext(2)]
  [CompilerGenerated]
  [SpecialName]
  public static bool op_Equality(PointRecord left, PointRecord right)
  {
    if ((object) left == (object) right)
      return true;
    return (object) left != null && left.Equals(right);
  }

  [CompilerGenerated]
  public override int GetHashCode()
  {
    return (EqualityComparer<Type>.Default.GetHashCode(this.EqualityContract) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(this.<X>k__BackingField)) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(this.<Y>k__BackingField);
  }

  [NullableContext(2)]
  [CompilerGenerated]
  public override bool Equals(object obj)
  {
    return this.Equals(obj as PointRecord);
  }

  [NullableContext(2)]
  [CompilerGenerated]
  public virtual bool Equals(PointRecord other)
  {
    if ((object) this == (object) other)
      return true;
    return (object) other != null && Type.op_Equality(this.EqualityContract, other.EqualityContract) && EqualityComparer<int>.Default.Equals(this.<X>k__BackingField, other.<X>k__BackingField) && EqualityComparer<int>.Default.Equals(this.<Y>k__BackingField, other.<Y>k__BackingField);
  }

  [CompilerGenerated]
  public virtual PointRecord <Clone>$()
  {
    return new PointRecord(this);
  }

  [CompilerGenerated]
  protected PointRecord(PointRecord original)
  {
    base..ctor();
    this.<X>k__BackingField = original.<X>k__BackingField;
    this.<Y>k__BackingField = original.<Y>k__BackingField;
  }

  public PointRecord()
  {
    base..ctor();
  }
}
Похожее
Mar 25
Author: Henrique Siebert Domareski
Pagination allows you to retrieve a large number of records split into pages, instead of returning all the results at once. This is especially useful in scenarios where you need to retrieve a large number of records. In this article,...
Jul 25, 2023
Unleashing the Power of Meta-Programming: A Comprehensive Guide to C# Reflection Reflection, put simply, is a mechanism provided by the .NET framework that allows a running program to examine and manipulate itself. It’s like a coding mirror that gives your...
Aug 11, 2021
Author: Mel Grubb
Code Generation Code generation is a great way to apply patterns consistently across a solution or to create multiple similar classes based on some outside input file, or even other classes in the same solution. The tooling has changed over...
May 30
Author: Erik Pourali
Imagine crafting a library app where users effortlessly find books by title, author, or genre. Traditional search methods drown you in code. But fear not! Dynamic Querying in C# saves the day. In our tale, crafting separate search methods for...
Написать сообщение
Тип
Почта
Имя
*Сообщение
RSS
Если вам понравился этот сайт и вы хотите меня поддержать, вы можете
GraphQL решает кучу проблем — рассказываем, за что мы его любим
Типичные взаимные блокировки в MS SQL и способы борьбы с ними
9 главных трендов в разработке фронтенда в 2024 году
Из интровертов в менторы: как мидлы становятся сеньорами
NULL в SQL: что это такое и почему его знание необходимо каждому разработчику
Модуль, пакет, библиотека, фреймворк: разбираемся в разнице
Универсальный ускоритель инженера: как расти быстрее с помощью проектов
Soft skills: 18 самых важных навыков, которыми должен владеть каждый работник
5 приемов увеличения продуктивности разработчика
Почему вы никогда не должны соглашаться на собеседования с программированием
LinkedIn: Sergey Drozdov
Boosty
Donate to support the project
GitHub account
GitHub profile