When to use record vs class vs struct

后端 未结 1 1295
-上瘾入骨i
-上瘾入骨i 2021-02-19 17:53
  • Should I be using Record for all of my DTO classes which move data between controller and service layer ?

  • Should I be using Record

1条回答
  •  星月不相逢
    2021-02-19 18:11

    Short version

    Can you data type be a value type? Go with struct. No? Does your type describe a value-like, preferably immutable state? Go with record.

    Use class otherwise. So...

    1. Yes, use records for your DTOs if it is one way flow.
    2. Yes, immutable request bindings are an ideal user case for a record
    3. Yes, SearchParameters are an ideal user case for a record.

    Long version

    A struct, a class and a record are user data types.

    Structures are value types. Classes are reference types. Records are by default immutable reference types.

    When you need some sort of hierarchy to describe your data types like inheritance or a struct pointing to another struct or basically things pointing to other things, you need a reference type.

    Records solve the problem when you want your type to be a value oriented by default. Records are reference types but with the value oriented semantic.

    With that being said, ask yourself these questions...


    Does your data type respect all of these rules:

    1. It logically represents a single value, similar to primitive types (int, double, etc.).
    2. It has an instance size under 16 bytes.
    3. It is immutable.
    4. It will not have to be boxed frequently.
    • Yes? It should be a struct.
    • No? It should be some reference type.

    Does your data type encapsulate some sort of a complex value? Is the value immutable? Do you use it in unidirectional (one way) flow?

    • Yes? Go with record.
    • No? Go with class.

    BTW: Don't forget about anonymous objects. There will be an anonymous records in C# 10.0.

    Notes

    A record instance can be mutable if you make it mutable.

    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;
    }
    

    A copy of a record is a deep copy of the record. That is what the records' immutability is all about. The copy is created by a special clone method emitted by C# compiler.

    See this example (an extension to my previous code):

    var foo = new Foo("a");
    var bar = foo with { Bar = "b" };
    bar.MutableProperty = 11;
    Console.WriteLine(foo.MutableProperty); // Result: 10
    

    The performance penalty is obvious here. A larger data to copy in a record instance you have, a larger performance penalty you get. Generally, you should create small, slim classes and this rule applies to records too.

    If your application is using database or file system, I wouldn't worry about this penalty much. The database/file system operations are generally slower.

    I made some synthetic test (full code below) where classes are wining but in real life application, the impact should be unnoticeable.

    In addition, the performance is not always number one priority. These days, the maintainability and readability of your code is preferable than highly optimized spaghetti code. It is the code author choice which way (s)he would prefer.

    using BenchmarkDotNet.Attributes;
    using BenchmarkDotNet.Running;
    
    namespace SmazatRecord
    {
        class Program
        {
            static void Main()
            {
                var summary = BenchmarkRunner.Run();
            }
        }
    
        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; }
        }
    }
    

    Result:

    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

    0 讨论(0)
提交回复
热议问题