我是否应该对在控制器和服务层之间移动数据的所有DTO类使用Record ? 我应该使用记录为我所有的请求绑定,因为理想情况下,我希望发送到控制器的请求是不可变的asp.net API

什么是纪录?Anthony Giretti介绍c# 9:记录

public class HomeController : Controller
{ 
    public async Task<IActionResult> Search(SearchParameters searchParams)
    {
        await _service.SearchAsync(searchParams);
    }
}

搜索参数应该成为一个记录吗?


当前回答

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

来自微软:

Records add another way to define types. You use class definitions to create object-oriented hierarchies that focus on the responsibilities and behavior of objects. You create struct types for data structures that store data and are small enough to copy efficiently. You create record types when you want value-based equality and comparison, don't want to copy values, and want to use reference variables. You create record struct types when you want the features of records for a type that is small enough to copy efficiently.

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

其他回答

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

来自微软:

Records add another way to define types. You use class definitions to create object-oriented hierarchies that focus on the responsibilities and behavior of objects. You create struct types for data structures that store data and are small enough to copy efficiently. You create record types when you want value-based equality and comparison, don't want to copy values, and want to use reference variables. You create record struct types when you want the features of records for a type that is small enough to copy efficiently.

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

我真的很喜欢上面的答案,它们非常精确和完整,但是我缺少一个重要的类型:只读结构体(c# 7.2)和即将到来的记录结构体(c# 10)。

当我们发现c#和。net被用于新的领域时,一些问题变得更加突出。作为计算开销比平均水平更关键的环境的例子,我可以列出 云/数据中心场景,其中计算为和计费 响应能力是一种竞争优势。 对延迟有软实时要求的游戏/VR/AR

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


class / record / ValueObject:

引用类型;不需要Ref和in关键字。 堆分配;GC的更多工作。 允许非公共无参数构造函数。 允许继承、多态和接口实现。 不需要被装箱。 使用记录作为dto和不可变/值对象。 当你既需要不可变性,又需要对相等性检查进行IComparable或精确控制时,请使用ValueObject。


(只读/记录)struct:

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

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

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

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

用于创建具有不可变引用类型的简明语法 属性 价值平等 非破坏性突变的简明语法 内置显示格式 支持继承层次结构

记录类型有一些缺点:

c#记录没有实现IComparable接口 在封装方面,记录要比结构好得多,因为你不能在结构中隐藏无参数的构造函数,但记录的封装仍然很差,我们可以实例化一个无效状态的对象。 不要控制相等性检查

记录用例:

Records will replace the Fluent Interface pattern in C#. The Test Data Builder pattern is a great example here. Instead of writing your own boilerplate code, you can now use the new with feature and save yourself tons of time and effort. Record is good for DTOs You may also need interim data classes while loading data to or retrieving it from the database or while doing some preprocessing. This is similar to the above DTOs, but instead of serving as data contracts between your application and external systems, these data classes act as DTOs between different layers of your own system. C# records are great for that too. Finally, not all applications require a rich, fully encapsulated domain model. In most simpler cases that don’t need much encapsulation, C# records would do just fine. otherwise use DDD value object

^ ^

短的版本

数据类型可以是值类型吗?使用struct。没有?您的类型是否描述了类似于值的、最好是不可变的状态?要有记录。

否则使用class。所以…

是的,如果是单向流,请为dto使用记录。 是的,不可变请求绑定是记录的理想用户用例 是的,SearchParameters是一条记录的理想用户用例。

有关记录使用的进一步实际示例,您可以检查此repo。

长版本

结构、类和记录是用户数据类型。

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

当你需要某种层次结构来描述你的数据类型时,比如继承或指向另一个结构的结构,或者基本上是指向其他东西的东西,你需要引用类型。

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

话虽如此,还是问自己这些问题吧……


你的数据类型是否尊重所有这些规则:

它在逻辑上表示一个值,类似于基本类型(int、double等)。 它的实例大小小于16字节。 它是不可变的。 它不需要经常装箱。

是吗?它应该是一个结构体。 没有?它应该是某种引用类型。


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

是吗?要有记录。 没有?跟着班级走。

顺便说一句:不要忘记匿名对象。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;
}

记录的赋值是记录的浅拷贝。通过记录的表达式进行复制既不是浅复制也不是深复制。复制是由c#编译器发出的特殊克隆方法创建的。值类型成员被复制并装箱。引用类型成员指向同一个引用。当且仅当记录仅具有值类型属性时,可以对记录进行深度复制。记录的任何引用类型成员属性都被复制为浅复制。

请看这个例子(使用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