Record
コントローラーとサービス層の間でデータを移動するすべての DTO クラスに使用する必要がありますか?Record
理想的には、コントローラーに送信されるリクエストがasp.net APIに対して不変になるようにしたいので、すべてのリクエストバインディングに使用する必要がありますか?
レコードとは何ですか?Anthony Giretti による C# 9 の紹介: レコード
public class HomeController : Controller
{
public async Task<IActionResult> Search(SearchParameters searchParams)
{
await _service.SearchAsync(searchParams);
}
}
SearchParameters
作るべきかRecord
?
ベストアンサー1
短縮版
データ型を値型にできますか? を使用してくださいstruct
。 できませんか? 型は値のような、できれば不変の状態を表しますか? を使用してくださいrecord
。
それ以外の場合は使用してくださいclass
。つまり...
- はい、
record
一方向フローの場合は DTO に s を使用します。 - はい、不変リクエストバインディングは、
record
- はい、
SearchParameters
の理想的なユーザーケースですrecord
。
さらに実用的なrecord
使用例については、こちらをご覧ください。レポ。
ロングバージョン
A struct
、a class
、a はrecord
ユーザーデータ型です。
構造体は値型です。クラスは参照型です。レコードは、デフォルトでは不変の参照型です。
継承や別のものへの参照、または基本的に他のものへの参照など、データ型を記述するために何らかの階層が必要な場合は、参照型が必要です。struct
struct
レコードは、デフォルトで型を値指向にしたい場合に問題を解決します。レコードは参照型ですが、値指向のセマンティクスを備えています。
そうは言っても、自分自身に次の質問をしてみてください...
あなたのデータ型は、これらのルール:
- プリミティブ型 (int、double など) と同様に、論理的に単一の値を表します。
- インスタンス サイズは 16 バイト未満です。
- それは不変です。
- 頻繁に箱詰めする必要はありません。
- はい?である必要があります
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
}
}
record Foo(string Bar)
{
internal 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 == 3
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("");
record SomeRecord(List<string> List);
record RecordOnlyWithValueNonMutableProperty(int NonMutableProperty);
record RecordOnlyWithValueMutableProperty
{
internal int MutableProperty { get; set; } = 1; // this property gets boxed
}
record MixedRecord(List<string> List, int NonMutableProperty)
{
internal List<string> MutableList { get; set; } = new();
internal 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;
}
}
record Foo(string Bar)
{
internal int MutableProperty { get; set; } = 10;
}
class FooClass
{
internal FooClass(string bar)
{
Bar = bar;
}
internal int MutableProperty { get; set; }
internal 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
方法 | 平均 | エラー | 標準偏差 |
---|---|---|---|
テスト記録 | 120.19マイクロ秒 | 2.299マイクロ秒 | 2.150マイクロ秒 |
テストクラス | 98.91マイクロ秒 | 0.856μs | 0.800μs |