Dot-Net-5

何時使用記錄 vs 類 vs 結構

  • November 13, 2020
  • 我應該使用Record所有在控制器和服務層之間移動數據的 DTO 類嗎?
  • 我是否應該使用我Record的所有請求綁定,因為理想情況下我希望發送到控制器的請求對於我的 asp.net API 是不可變的

什麼是記錄?Anthony Giretti 介紹 C# 9:記錄

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

應該SearchParameters發一個Record

簡潔版本

你的數據類型可以是類型嗎?一起去struct。不?您的類型是否描述了類似值的、最好是不可變的狀態?一起去record

class否則使用。所以…

  1. 是的,record如果它是單向流,則將 s 用於您的 DTO。
  2. 是的,不可變的請求綁定是一個理想的使用者案例record
  3. SearchParameters的,是record.

有關更多實際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;
}

記錄的分配是記錄的淺拷貝。記錄的表達式複制with既不是淺拷貝也不是深拷貝。該副本由 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

引用自:https://stackoverflow.com/questions/64816714