何时使用 record vs class vs struct

When to use record vs class vs struct

什么是记录? Anthony Giretti  Introducing C# 9: Records

  public class HomeController 
  { 
    public IHttpAction Search([FromBody] SearchParameters searchParams)
    {
       _service.Search(searchParams);
    }
  }

应该 SearchParameters 成为 Record 吗?

简短版

你的数据类型可以是类型吗?选择 struct。不?您的类型是否描述了类似值的状态,最好是不可变的状态?选择 record.

否则使用class。所以...

  1. 是的,如果是单向流,请为您的 DTO 使用 records。
  2. 是的,不可变请求绑定是 record
  3. 的理想用户案例
  4. 是的,SearchParametersrecord 的理想用户案例。

更多record使用的实际例子,可以查看这个repo.

长版

一个struct、一个class和一个record是用户数据类型.

结构是值类型。 类 是 引用类型 。默认情况下,记录是 不可变的 引用类型。

当您需要某种层次结构来描述您的 数据类型时 例如继承或指向另一个 structstruct 或基本上指向其他的东西事情,你需要一个 reference 类型。

当您希望类型默认 时,记录解决了这个问题。记录是引用类型,但具有面向值的语义。

话虽如此,问问自己这些问题...


你的数据类型是否符合these rules所有:

  1. 它在逻辑上表示单个值,类似于原始类型(int、double 等)。
  2. 实例大小小于 16 字节。
  3. 它是不可变的。
  4. 不用经常装箱
  • 是吗? 应该是一个struct.
  • 没有?应该是一些参考类型.

您的数据类型是否封装了某种复杂的值?值是不可变的吗?您是否在单向(单向)流中使用它?

  • 是吗?选择 record.
  • 没有?选择 class.

顺便说一句:不要忘记 anonymous objects。 C# 10.0 会有匿名记录。

备注

如果您使其可变,则记录实例可以是可变的。

class Program
{
    static void Main()
    {
        var test = new Foo("a");
        Console.WriteLine(test.MutableProperty);
        test.MutableProperty = 15;
        Console.WriteLine(test.MutableProperty);
        //test.Bar = "new string"; // will not compile
    }
}

public record Foo(string Bar)
{
    public double MutableProperty { get; set; } = 10.0;
}

记录的赋值是记录的浅拷贝。 with 表达式的记录副本既不是浅副本也不是深层副本。该副本由 C# 编译器发出的特殊 clone 方法创建。值类型成员被复制和装箱。引用类型成员指向同一个引用。当且仅当记录仅具有值类型属性时,您才能对记录进行深拷贝。记录的任何引用类型成员 属性 都被复制为浅表副本。

查看此示例(使用 C# 9.0 中的顶级功能):

using System.Collections.Generic;
using static System.Console;

var foo = new SomeRecord(new List<string>());
var fooAsShallowCopy = foo;
var fooAsWithCopy = foo with { }; // A syntactic sugar for new SomeRecord(foo.List);
var fooWithDifferentList = foo with { List = new List<string>() { "a", "b" } };
var differentFooWithSameList = new SomeRecord(foo.List); // This is the same like foo with { };
foo.List.Add("a");

WriteLine($"Count in foo: {foo.List.Count}"); // 1
WriteLine($"Count in fooAsShallowCopy: {fooAsShallowCopy.List.Count}"); // 1
WriteLine($"Count in fooWithDifferentList: {fooWithDifferentList.List.Count}"); // 2
WriteLine($"Count in differentFooWithSameList: {differentFooWithSameList.List.Count}"); // 1
WriteLine($"Count in fooAsWithCopy: {fooAsWithCopy.List.Count}"); // 1
WriteLine("");

WriteLine($"Equals (foo & fooAsShallowCopy): {Equals(foo, fooAsShallowCopy)}"); // True. The lists inside are the same.
WriteLine($"Equals (foo & fooWithDifferentList): {Equals(foo, fooWithDifferentList)}"); // False. The lists are different
WriteLine($"Equals (foo & differentFooWithSameList): {Equals(foo, differentFooWithSameList)}"); // True. The list are the same.
WriteLine($"Equals (foo & fooAsWithCopy): {Equals(foo, fooAsWithCopy)}"); // True. The list are the same, see below.
WriteLine($"ReferenceEquals (foo.List & fooAsShallowCopy.List): {ReferenceEquals(foo.List, fooAsShallowCopy.List)}"); // True. The records property points to the same reference.
WriteLine($"ReferenceEquals (foo.List & fooWithDifferentList.List): {ReferenceEquals(foo.List, fooWithDifferentList.List)}"); // False. The list are different instances.
WriteLine($"ReferenceEquals (foo.List & differentFooWithSameList.List): {ReferenceEquals(foo.List, differentFooWithSameList.List)}"); // True. The records property points to the same reference.
WriteLine($"ReferenceEquals (foo.List & fooAsWithCopy.List): {ReferenceEquals(foo.List, fooAsWithCopy.List)}"); // True. The records property points to the same reference.
WriteLine("");

WriteLine($"ReferenceEquals (foo & fooAsShallowCopy): {ReferenceEquals(foo, fooAsShallowCopy)}"); // True. !!! fooAsCopy is pure shallow copy of foo. !!!
WriteLine($"ReferenceEquals (foo & fooWithDifferentList): {ReferenceEquals(foo, fooWithDifferentList)}"); // False. These records are two different reference variables.
WriteLine($"ReferenceEquals (foo & differentFooWithSameList): {ReferenceEquals(foo, differentFooWithSameList)}"); // False. These records are two different reference variables and reference type property hold by these records does not matter in ReferenceEqual.
WriteLine($"ReferenceEquals (foo & fooAsWithCopy): {ReferenceEquals(foo, fooAsWithCopy)}"); // False. The same story as differentFooWithSameList.
WriteLine("");

var bar = new RecordOnlyWithValueNonMutableProperty(0);
var barAsShallowCopy = bar;
var differentBarDifferentProperty = bar with { NonMutableProperty = 1 };
var barAsWithCopy = bar with { };

WriteLine($"Equals (bar & barAsShallowCopy): {Equals(bar, barAsShallowCopy)}"); // True.
WriteLine($"Equals (bar & differentBarDifferentProperty): {Equals(bar, differentBarDifferentProperty)}"); // False. Remember, the value equality is used.
WriteLine($"Equals (bar & barAsWithCopy): {Equals(bar, barAsWithCopy)}"); // True. Remember, the value equality is used.
WriteLine($"ReferenceEquals (bar & barAsShallowCopy): {ReferenceEquals(bar, barAsShallowCopy)}"); // True. The shallow copy.
WriteLine($"ReferenceEquals (bar & differentBarDifferentProperty): {ReferenceEquals(bar, differentBarDifferentProperty)}"); // False. Operator with creates a new reference variable.
WriteLine($"ReferenceEquals (bar & barAsWithCopy): {ReferenceEquals(bar, barAsWithCopy)}"); // False. Operator with creates a new reference variable.
WriteLine("");

var fooBar = new RecordOnlyWithValueMutableProperty();
var fooBarAsShallowCopy = fooBar; // A shallow copy, the reference to bar is assigned to barAsCopy
var fooBarAsWithCopy = fooBar with { }; // A deep copy by coincidence because fooBar has only one value property which is copied into barAsDeepCopy.

WriteLine($"Equals (fooBar & fooBarAsShallowCopy): {Equals(fooBar, fooBarAsShallowCopy)}"); // True.
WriteLine($"Equals (fooBar & fooBarAsWithCopy): {Equals(fooBar, fooBarAsWithCopy)}"); // True. Remember, the value equality is used.
WriteLine($"ReferenceEquals (fooBar & fooBarAsShallowCopy): {ReferenceEquals(fooBar, fooBarAsShallowCopy)}"); // True. The shallow copy.
WriteLine($"ReferenceEquals (fooBar & fooBarAsWithCopy): {ReferenceEquals(fooBar, fooBarAsWithCopy)}"); // False. Operator with creates a new reference variable.
WriteLine("");

fooBar.MutableProperty = 2;
fooBarAsShallowCopy.MutableProperty = 3;
fooBarAsWithCopy.MutableProperty = 3;
WriteLine($"fooBar.MutableProperty = {fooBar.MutableProperty} | fooBarAsShallowCopy.MutableProperty = {fooBarAsShallowCopy.MutableProperty} | fooBarAsWithCopy.MutableProperty = {fooBarAsWithCopy.MutableProperty}"); // fooBar.MutableProperty = 3 | fooBarAsShallowCopy.MutableProperty = 3 | fooBarAsWithCopy.MutableProperty = 3
WriteLine($"Equals (fooBar & fooBarAsShallowCopy): {Equals(fooBar, fooBarAsShallowCopy)}"); // True.
WriteLine($"Equals (fooBar & fooBarAsWithCopy): {Equals(fooBar, fooBarAsWithCopy)}"); // True. Remember, the value equality is used. 3 != 4
WriteLine($"ReferenceEquals (fooBar & fooBarAsShallowCopy): {ReferenceEquals(fooBar, fooBarAsShallowCopy)}"); // True. The shallow copy.
WriteLine($"ReferenceEquals (fooBar & fooBarAsWithCopy): {ReferenceEquals(fooBar, fooBarAsWithCopy)}"); // False. Operator with creates a new reference variable.
WriteLine("");

fooBarAsWithCopy.MutableProperty = 4;
WriteLine($"fooBar.MutableProperty = {fooBar.MutableProperty} | fooBarAsShallowCopy.MutableProperty = {fooBarAsShallowCopy.MutableProperty} | fooBarAsWithCopy.MutableProperty = {fooBarAsWithCopy.MutableProperty}"); // fooBar.MutableProperty = 3 | fooBarAsShallowCopy.MutableProperty = 3 | fooBarAsWithCopy.MutableProperty = 4
WriteLine($"Equals (fooBar & fooBarAsWithCopy): {Equals(fooBar, fooBarAsWithCopy)}"); // False. Remember, the value equality is used. 3 != 4
WriteLine("");

var venom = new MixedRecord(new List<string>(), 0); // Reference/Value property, mutable non-mutable.
var eddieBrock = venom;
var carnage = venom with { };
venom.List.Add("I'm a predator.");
carnage.List.Add("All I ever wanted in this world is a carnage.");
WriteLine($"Count in venom: {venom.List.Count}"); // 2
WriteLine($"Count in eddieBrock: {eddieBrock.List.Count}"); // 2
WriteLine($"Count in carnage: {carnage.List.Count}"); // 2
WriteLine($"Equals (venom & eddieBrock): {Equals(venom, eddieBrock)}"); // True.
WriteLine($"Equals (venom & carnage): {Equals(venom, carnage)}"); // True. Value properties has the same values, the List property points to the same reference.
WriteLine($"ReferenceEquals (venom & eddieBrock): {ReferenceEquals(venom, eddieBrock)}"); // True. The shallow copy.
WriteLine($"ReferenceEquals (venom & carnage): {ReferenceEquals(venom, carnage)}"); // False. Operator with creates a new reference variable.
WriteLine("");

eddieBrock.MutableList = new List<string>();
eddieBrock.MutableProperty = 3;
WriteLine($"Equals (venom & eddieBrock): {Equals(venom, eddieBrock)}"); // True. Reference or value type does not matter. Still a shallow copy of venom, still true.
WriteLine($"Equals (venom & carnage): {Equals(venom, carnage)}"); // False. the venom.List property does not points to the same reference like in carnage.List anymore.
WriteLine($"ReferenceEquals (venom & eddieBrock): {ReferenceEquals(venom, eddieBrock)}"); // True. The shallow copy.
WriteLine($"ReferenceEquals (venom & carnage): {ReferenceEquals(venom, carnage)}"); // False. Operator with creates a new reference variable.
WriteLine($"ReferenceEquals (venom.List & carnage.List): {ReferenceEquals(venom.List, carnage.List)}"); // True. Non mutable reference type.
WriteLine($"ReferenceEquals (venom.MutableList & carnage.MutableList): {ReferenceEquals(venom.MutableList, carnage.MutableList)}"); // False. This is why Equals(venom, carnage) returns false.
WriteLine("");


public record SomeRecord(List<string> List);

public record RecordOnlyWithValueNonMutableProperty(int NonMutableProperty);

public record RecordOnlyWithValueMutableProperty
{
    public int MutableProperty { get; set; } = 1; // this property gets boxed
}

public record MixedRecord(List<string> List, int NonMutableProperty)
{
    public List<string> MutableList { get; set; } = new();
    public int MutableProperty { get; set; } = 1; // this property gets boxed
}

这里的性能损失很明显。要在您拥有的记录实例中复制的数据越大,您获得的性能损失就越大。通常,您应该创建小而纤细的 类,此规则也适用于记录。

如果您的应用程序正在使用数据库或文件系统,我不会太担心这种损失。 database/file 系统操作通常较慢。

我做了一些综合测试(下面的完整代码),其中 类 是赢家,但在实际应用中,影响应该不明显。

此外,性能并不总是第一要务。如今,代码的可维护性和可读性优于高度优化的意大利面条代码。他更喜欢哪种方式是代码作者的选择。

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

namespace SmazatRecord
{
    class Program
    {
        static void Main()
        {
            var summary = BenchmarkRunner.Run<Test>();
        }
    }

    public class Test
    {

        [Benchmark]
        public int TestRecord()
        {
            var foo = new Foo("a");
            for (int i = 0; i < 10000; i++)
            {
                var bar = foo with { Bar = "b" };
                bar.MutableProperty = i;
                foo.MutableProperty += bar.MutableProperty;
            }
            return foo.MutableProperty;
        }

        [Benchmark]
        public int TestClass()
        {
            var foo = new FooClass("a");
            for (int i = 0; i < 10000; i++)
            {
                var bar = new FooClass("b")
                {
                    MutableProperty = i
                };
                foo.MutableProperty += bar.MutableProperty;
            }
            return foo.MutableProperty;
        }
    }

    public record Foo(string Bar)
    {
        public int MutableProperty { get; set; } = 10;
    }

    public class FooClass
    {
        public FooClass(string bar)
        {
            Bar = bar;
        }
        public int MutableProperty { get; set; }
        public string Bar { get; }
    }
}

结果:

BenchmarkDotNet=v0.12.1, OS=Windows 10.0.18363.1379 (1909/November2018Update/19H2)
AMD FX(tm)-8350, 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=5.0.103
  [Host]     : .NET Core 5.0.3 (CoreCLR 5.0.321.7212, CoreFX 5.0.321.7212), X64 RyuJIT
  DefaultJob : .NET Core 5.0.3 (CoreCLR 5.0.321.7212, CoreFX 5.0.321.7212), X64 RyuJIT


Method Mean Error StdDev
TestRecord 120.19 μs 2.299 μs 2.150 μs
TestClass 98.91 μs 0.856 μs 0.800 μs

您可以使用结构类型来设计提供值相等很少或没有行为的以数据为中心的类型。但是对于比较大的数据模型,结构类型有一些缺点:

  • 他们不支持继承
  • 它们在确定 价值平等 时效率较低。对于值类型,ValueType.Equals 方法使用反射来查找所有字段。对于记录,编译器生成 Equals 方法。在实践中,记录中值相等的实现明显更快。
  • 它们在某些情况下使用更多内存,因为每个实例都有一个 所有数据的完整副本。记录类型是引用类型, 因此记录实例仅包含对数据的引用。

虽然记录可以是可变的,但它们主要用于支持不可变数据模型。记录类型提供以下功能:

  • 创建不可变引用类型的简明语法 属性

  • 值相等

  • 非破坏性变异的简洁语法

  • 显示的内置格式

  • 支持继承层次结构

记录类型有一些缺点:

  • C# 记录没有实现 IComparable 接口

  • 在封装方面,recordsstructs好很多,因为你不能把无参构造函数隐藏在一个struct中,但是Record还是有封装不良,我们可以实例化一个具有无效状态的对象。

  • 无法控制相等性检查

C#记录用例:

  • Records 将取代 C# 中的 Fluent Interface 模式。测试数据生成器模式就是一个很好的例子。您现在可以使用新的 with 功能,而不是编写自己的样板代码,从而节省大量时间和精力。

  • 记录对 DTO 有利

  • 在将数据加载到或时,您可能还需要临时数据 类 从数据库中检索它或在进行一些预处理时。 这类似于上面的 DTO,但不是作为数据 您的应用程序和外部系统之间的合同,这些数据 类 充当您自己系统不同层之间的 DTO。 C# 记录也很重要。

  • 最后,并非所有应用程序都需要丰富的、完全封装的域模型。在大多数不需要太多封装的简单情况下,C# 记录就可以了。否则使用 DDD 值对象

^ ^

我非常喜欢上面的答案,它们非常精确和完整,但我缺少一个重要类型readonly struct (C#7.2) and, coming soon, record struct (C#10)

As we find C# and .Net used in new domains, some problems become more prominent. As examples of environments that are more critical than average about computation overheads, I can list

  • cloud/datacenter scenarios where computation is billed for and responsiveness is a competitive advantage.
  • Games/VR/AR with soft-realtime requirements on latencies

所以,如果我错了,请纠正我,但我会遵循 the usual rules:


class / record / ValueObject:

  • 引用类型;不需要 refin 关键字。
  • 已分配堆; GC 的更多工作。
  • 允许非public无参数构造函数。
  • 允许继承、多态和 interface 实现。
  • 不必装箱。
  • 使用 record 作为 DTO 和 immutable/value 对象。
  • 当您既需要不变性又需要 IComparable 或精确控制相等性检查时,请使用 ValueObject

(readonly / record) struct:

  • 值类型;可以使用 in 关键字作为只读引用传递。
  • 堆栈已分配;适合 cloud/datacenter/Games/VR/AR.
  • 不允许非public无参数构造函数。
  • 不允许继承、多态,但interface实现。
  • 可能要经常装箱。