何时使用记录 vs 类 vs 结构

fri*_*ley 65 .net-5 c#-9.0 c#-record-type

  • 我是否应该Record用于在控制器和服务层之间移动数据的所有 DTO 类?

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

什么是记录?https://anthonygiretti.com/2020/06/17/introducing-c-9-records/

  public class HomeController 
  { 
    public IHttpAction Search([FromBody] SearchParameters searchParams)
    {
       _service.Search(searchParams);
    }
  }
Run Code Online (Sandbox Code Playgroud)

应该发SearchParameters一个Record

KUT*_*ime 107

精简版

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

class否则使用。所以...

  1. 是的,record如果是单向流程,请将 s 用于您的 DTO。
  2. 是的,不可变的请求绑定是一个理想的用户案例 record
  3. 是的,SearchParametersrecord.

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

长版

A struct、aclass和 arecord是用户数据类型

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

当您需要某种层次结构来描述您的数据类型(如继承或struct指向另一个struct或基本上指向其他事物的事物)时,您需要一个引用类型。

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

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


请问您的数据类型尊重所有这些规则

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

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

  • 是的?与record.
  • 不?与class.

顺便说一句:不要忘记匿名对象。在 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;
}
Run Code Online (Sandbox Code Playgroud)

默认情况下,记录的副本是记录的浅拷贝。该副本由 C# 编译器发出的特殊克隆方法创建。值类型成员被装箱。你可以做一个记录的深拷贝。

请参阅此示例(使用 C# 9.0 中的顶级功能):

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

var foo = new SomeRecord(new List<string>());
var fooAsCopy = foo;
var fooWithDifferentList = foo with { List = new List<string>() { "a", "b" } };
var differentFooWithSameList = new SomeRecord(foo.List);
foo.List.Add("a");

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

WriteLine($"Equals (foo & fooAsCopy): {Equals(foo, fooAsCopy)}"); // True
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($"ReferenceEquals (foo & fooAsCopy): {ReferenceEquals(foo, fooAsCopy)}"); // True, because pure shallow copy
WriteLine($"ReferenceEquals (foo & fooWithDifferentList): {ReferenceEquals(foo, fooWithDifferentList)}"); // False, the list are different
WriteLine($"ReferenceEquals (foo & differentFooWithSameList): {ReferenceEquals(foo, differentFooWithSameList)}"); // False, the different instances of the same record type

var bar = new SomeRecordWithValueProperty();
var barAsCopy = bar;
var barAsDeepCopy = bar with { }; // A deep copy

WriteLine($"Equals (bar & barAsCopy): {Equals(bar, barAsCopy)}"); // True
WriteLine($"Equals (bar & barAsDeepCopy): {Equals(bar, barAsDeepCopy)}"); // True, value equality 
WriteLine($"ReferenceEquals (bar & barAsCopy): {ReferenceEquals(bar, barAsCopy)}"); // True, the shallow copy
WriteLine($"ReferenceEquals (bar & barAsDeepCopy): {ReferenceEquals(bar, barAsDeepCopy)}"); // False, the deep copy

bar.MutableProperty = 2;
barAsCopy.MutableProperty = 3;
barAsDeepCopy.MutableProperty = 3;
WriteLine($"bar.MutableProperty = {bar.MutableProperty} | barAsCopy.MutableProperty = {barAsCopy.MutableProperty} ");
WriteLine($"Equals (bar & barAsCopy): {Equals(bar, barAsCopy)}"); // True, mutable property is boxed and both instance has value 3 in MutableProperty.
WriteLine($"Equals (bar & barAsDeepCopy): {Equals(bar, barAsDeepCopy)}"); // True
WriteLine($"ReferenceEquals (bar & barAsCopy): {ReferenceEquals(bar, barAsCopy)}"); // True
WriteLine($"ReferenceEquals (bar & barAsDeepCopy): {ReferenceEquals(bar, barAsDeepCopy)}"); // False

public record SomeRecord(List<string> List);

public record SomeRecordWithValueProperty
{
    public int MutableProperty { get; set; } = 1; // this property gets boxed
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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; }
    }
}
Run Code Online (Sandbox Code Playgroud)

结果:

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


Run Code Online (Sandbox Code Playgroud)
方法 意思 错误 标准差
测试记录 120.19 ?s 2.299 ?s 2.150 ?s
测试类 98.91 ?s 0.856 ?s 0.800 ?s

  • 虽然其中大部分是正确的,但记录不一定是不可变的,但对此无法保证。但他们所做的是复制(克隆)并检查平等性*所有内容*,包括私有只读字段和具有设置器的可变属性。 (6认同)
  • @RadimCernej 如果您的数据结构中的成员指向其他事物,则需要引用类型。记录通过默认提供面向值的语义来解决此问题,其中一条记录可以指向另一条记录。结构不能做到这一点。此外,结构不支持继承。 (5认同)
  • 当您声称“barAsCopy”是“bar”的“浅克隆”并且“MutableProperty”被装箱时,我不明白您在说什么。记录是引用类型,因此当您将“bar”分配给“barAsCopy”时,除了“barAsCopy”之外不会发生任何事情,并且“bar”现在共享单个引用,这就是“ReferenceEquals”返回 true 的原因。没有任何复制发生。当您使用 with 运算符时,即发生克隆,但这将是浅克隆,而不是深层克隆,这意味着将复制对任何引用类型的引用,而不是引用的对象 (5认同)
  • @wired_in 是正确的;`bar with { }` 不是深层复制/克隆 - 它是浅层的。“深层复制”是克隆整个层次结构而不仅仅是顶层的复制;`with` 只复制顶层。此外,“barAsCopy = bar”根本不是副本,而是共享引用。相反,如果这些值是结构(即值类型),则数据将真正被复制而不是仅仅引用,并且其中一个值的突变不会影响另一个值。 (4认同)
  • 还有元组 (3认同)
  • @springy76 你是对的。如果您使其可变,则记录可以是可变的。我编辑了我的答案以记录这一点。谢谢你提出来。 (3认同)

fib*_*iel 46

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

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

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

所以,如果我错了,请纠正我,但我会遵循通常的规则


class// :recordValueObject

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

( readonly/ record) struct:

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

  • 有关“struct”和“record struct”之间的一些性能比较,请参阅:https://nietras.com/2021/06/14/csharp-10-record-struct/ (9认同)

Ali*_*yat 21

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

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

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

\n
    \n
  • 用于创建具有不可变属性的引用类型的简明语法\n

    \n
  • \n
  • 价值平等

    \n
  • \n
  • 非破坏性突变的简洁语法

    \n
  • \n
  • 内置显示格式

    \n
  • \n
  • 支持继承层次结构

    \n
  • \n
\n

记录类型有一些缺点:

\n
    \n
  • C# 记录不实现 IComparable 接口

    \n
  • \n
  • 就封装性而言,比xe2x80x99records好得多,因为你不能将无参构造函数隐藏在结构体中,但封装性仍然很差,我们可以实例化一个处于无效状态的对象。structsRecord

    \n
  • \n
  • 无法控制相等性检查

    \n
  • \n
\n

C#记录用例:

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

    \n
  • \n
  • 记录对 DTO 有利

    \n
  • \n
  • 在将数据加载到数据库或\n从数据库检索数据或进行某些预处理时,您可能还需要临时数据类。\n这与上述 DTO 类似,但这些数据不是充当应用程序和外部系统之间的数据\n契约,而是\n类充当您自己系统的不同层之间的 DTO。C#\nrecords 也非常适合这样做。

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

    \n
  • \n
\n

^^

\n

  • 由于不变性和其他相关功能的简洁语法,记录对于值对象也非常有用 (3认同)

Ome*_*r K 7

记录为基本用途是存储数据的类型提供简洁的语法。对于面向对象的类,基本用途是定义职责。

来自微软:

记录添加了另一种定义类型的方式。您可以使用class定义来创建面向对象的层次结构,重点关注对象的职责和行为。您可以struct为存储数据且足够小以便高效复制的数据结构创建类型。record当您需要基于值的相等和比较、不想复制值并且想要使用引用变量时,可以创建 类型。record struct当您需要足够小的类型记录功能以进行有效复制时,您可以创建 类型。

https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/records