【问题标题】:When to use record vs class vs struct何时使用记录 vs 类 vs 结构
【发布时间】:2020-11-13 06:58:04
【问题描述】:
  • 我是否应该将Record 用于在控制器和服务层之间移动数据的所有 DTO 类?

  • 我是否应该将Record 用于我的所有请求绑定,因为理想情况下我希望发送到控制器的请求对于我的 asp.net API 而言是不可变的

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

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

是否应该将SearchParameters 设为Record

【问题讨论】:

标签: .net-5 c#-9.0 c#-record-type


【解决方案1】:

短版

你的数据类型可以是 value 类型吗?使用struct。不?您的类型是否描述了类似值的、最好是不可变的状态?使用record

否则使用class。所以...

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

有关record使用的更多实际示例,您可以查看此repo

加长版

structclassrecord 是用户数据类型

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

当您需要某种层次结构来描述您的数据类型,例如继承或指向另一个structstruct 或基本上指向其他事物的事物时,您需要一个参考 输入。

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

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


您的数据类型是否尊重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
}

这里的性能损失很明显。要在您拥有的记录实例中复制更大的数据,您将获得更大的性能损失。一般来说,你应该创建小而细的类,这条规则也适用于记录。

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

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

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

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

【讨论】:

  • 还有元组
  • @springy76 你是对的。如果您将其设置为可变,则该记录可以是可变的。我已经编辑了我的答案以注意这一点。谢谢你提出来。
  • @RadimCernej 如果您有成员指向其他事物的数据结构,则需要引用类型。记录通过默认提供一个面向值的语义来解决这个问题,其中一条记录可以指向另一条记录。结构不能比。此外,结构不支持继承。
  • 当您声称 barAsCopybar浅克隆,并且 MutableProperty i> 被装箱。记录是引用类型,所以当您将bar 分配给barAsCopy 时,除了barAsCopybar 现在共享一个引用,这就是ReferenceEquals 返回true 的原因。没有任何复制发生。当您使用 with 操作符时,会发生克隆,但这将是浅克隆,而不是深度克隆,这意味着将复制对任何引用类型的引用,而不是引用的对象
  • @wired_in 是正确的; bar with { } 不是深拷贝/克隆 - 它是浅的。 “深拷贝”是一种克隆整个层次结构而不仅仅是顶层的拷贝; with 只复制顶层。此外,barAsCopy = bar 根本不是副本,而是共享引用。相反,如果这些值是结构(即值类型),则数据将真正被复制,而不仅仅是引用,并且对其中一个的突变不会影响另一个。
【解决方案2】:

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

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

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

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

  • 价值平等

  • 无损突变的简洁语法

  • 用于显示的内置格式

  • 支持继承层次结构

记录类型有一些缺点:

  • C# 记录不实现 IComparable 接口

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

  • 无法控制相等性检查

C#记录用例:

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

  • 记录对 DTO 有好处

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

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

^^

【讨论】:

  • 记录也非常适合 Value 对象,因为其语法简洁,具有不变性和其他相关功能
  • 是的,在简单的情况下,当您不需要丰富的、完全封装的域模型时,Record 就可以了,否则使用 Value 对象。
【解决方案3】:

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

随着我们发现 C# 和 .Net 在新领域中的应用,一些问题变得更加突出。作为比平均计算开销更关键的环境示例,我可以列出

  • 计算费用的云/数据中心场景和 响应能力是一项竞争优势。
  • 对延迟有软实时要求的游戏/VR/AR

所以,如果我错了,请纠正我,但我会关注the usual rules


class / record / ValueObject:

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

(readonly/record)struct:

  • 值类型;可以使用 in 关键字作为只读引用传递。
  • 堆栈分配;适用于云/数据中心/游戏/VR/AR。
  • 不允许非公共的无参数构造函数。
  • 不允许继承、多态,但interface 实现。
  • 可能需要经常装箱。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-11-17
  • 1970-01-01
  • 2010-11-16
  • 2013-08-12
  • 1970-01-01
  • 2017-11-20
  • 1970-01-01
相关资源
最近更新 更多