什么时候应该在C#中使用结构而不是类?我的概念模型是,当项只是值类型的集合时,使用结构。一种将它们逻辑地结合在一起的方法。

我在这里遇到了这些规则:

结构应表示单个价值结构应具有内存占用空间小于16字节。结构不应在之后更改创造

这些规则有效吗?结构在语义上意味着什么?


当前回答

✔️ 考虑结构使用

创建一个对象或不需要创建该对象(您可以直接赋值,它创建对象)需要提高速度或性能无需施工人员和拆卸人员(静态承包商可用)不需要类继承,但可以接受接口工作负载小的对象工作,如果工作负载高,内存问题将增加不能为变量设置默认值。结构还可以使用方法、事件、静态构造函数、变量等GC中的工作量更少不需要引用类型,只需要值类型(每次创建新对象时)无不可变对象(字符串是不可变对象,因为任何操作都不会在不更改原始字符串的情况下每次返回新字符串)

其他回答

结构在大多数方面类似于类/对象。结构可以包含函数、成员,并且可以继承。但C#中的结构仅用于数据保存。结构比类占用更少的RAM,并且垃圾收集器更容易收集。但当您在结构中使用函数时,编译器实际上采用的结构与类/对象非常相似,所以如果您想要使用函数,请使用类/对象。

神话1:结构是轻量级类

这个神话有多种形式。有些人认为价值类型不能或不应该有方法或其他重要的行为,它们应该简单地使用数据传输类型,只有公共字段或简单的财产。DateTime类型是这是一个很好的反例:就存在而言,它是一种价值类型是有意义的一个基本单位,如数字或字符能够基于其值执行计算。从另一个角度看问题方向,数据传输类型通常应该是引用类型应该基于期望的值或引用类型语义,而不是类型。其他人认为价值类型比参考类型“更轻”性能。事实是,在某些情况下,价值类型更有表现力-它们不需要垃圾收集,除非它们是盒装的,没有类型例如,识别开销,并且不需要取消引用。但在其他方面方法,引用类型是性能更高的参数传递,将值分配给变量、返回值和类似操作只需要4或8个字节即可复制(取决于您运行的是32位还是64位CLR),而不是复制所有数据。想象一下,如果ArrayList是一个“纯”值类型,并且将ArrayList表达式传递给涉及复制其所有数据的方法!在几乎无论如何,性能并不是由这种决定决定的。瓶颈几乎永远不会出现在你认为会出现的地方,在你根据性能做出设计决策之前,你应该衡量不同的选择。值得注意的是,这两种信念的结合也不起作用。它不管一个类型有多少个方法(无论是类还是结构)-每个实例占用的内存不受影响。(内存方面有成本为代码本身占用,但这只发生一次,而不是每个实例。)

神话#2:引用类型存在于堆中;堆栈上存在值类型

这通常是由于重复的人的懒惰造成的部分是正确的,引用类型的实例总是在堆上创建的。这是导致问题的第二部分。正如我已经注意到的,变量的值存在于声明的任何地方,因此,如果您有一个类的实例变量类型为int,那么任何给定对象的变量值将始终位于该对象的其余数据的位置在堆上。仅局部变量(方法中声明的变量)和方法参数存在于堆栈中。在C#2和更高版本中,即使是一些局部变量在堆栈上生存,正如我们在第5章中研究匿名方法时所看到的那样。这些概念现在是否相关?如果您正在编写托管代码,那么应该让运行时考虑内存的最佳使用方式,这是有争议的。事实上,语言规范并不能保证哪里未来的运行时可能能够在堆栈上创建一些对象,如果或者C#编译器可以生成几乎不使用堆栈。下一个神话通常只是一个术语问题。

神话#3:默认情况下,对象在C中通过引用传递

这可能是流传最广的神话。再一次,制造这个的人声明经常(虽然不总是)知道C#的实际行为,但他们不知道“通过引用传递”的真正含义。不幸的是,对于那些知道这意味着什么。引用传递的形式定义相对复杂,涉及l值和类似的计算机科学术语,但重要的是,如果你通过了变量,您调用的方法可以通过更改其参数值来更改调用者变量的值。现在,请记住引用的值类型变量是引用,而不是对象本身。您可以更改参数引用的对象,而不通过引用传递参数本身。例如,以下方法更改StringBuilder的内容对象,但调用方的表达式仍将引用与之前:

void AppendHello(StringBuilder builder)
{
    builder.Append("hello");
}

调用此方法时,参数值(对StringBuilder的引用)为通过值传递。如果要在方法,其中语句builder=null-改变不会是与神话相反。有趣的是,不仅神话中的“引用”部分不准确,而且“对象被传递”部分也不准确。对象本身也不会被传递通过引用或通过值。当涉及引用类型时通过引用传递,或者参数(引用)的值通过值传递。除此之外,这回答了当null为如果正在传递对象,则用作by-value参数,这将导致问题,因为不会有对象要通过!相反,空引用由传递值的方式与任何其他引用相同。如果这个简短的解释让您感到困惑,那么您可能想看看我的文章“在C#中传递参数”(http://mng.bz/otVt),其中包含更多内容细节这些神话并不是唯一的神话。拳击和拆箱是他们的我会努力澄清这一误解。

参考:Jon Skeet的C#深度第三版

.NET支持值类型和引用类型(在Java中,只能定义引用类型)。引用类型的实例在托管堆中分配,并且在没有未完成的引用时被垃圾收集。另一方面,值类型的实例是在堆栈中分配的,因此一旦其作用域结束,分配的内存就会被回收。当然,值类型通过值传递,引用类型通过引用传递。除了System.String之外,所有C#原语数据类型都是值类型。

何时在类上使用结构,

在C#中,结构是值类型,类是引用类型。您可以使用enum关键字和struct关键字在C#中创建值类型。使用值类型而不是引用类型将导致托管堆上的对象减少,从而减少垃圾收集器(GC)的负载,减少GC周期,从而提高性能。然而,价值类型也有其缺点。传递一个大结构肯定比传递一个引用成本更高,这是一个明显的问题。另一个问题是与装箱/拆箱相关的开销。如果您想知道装箱/拆箱是什么意思,请按照以下链接了解装箱和拆箱的详细说明。除了性能之外,有时您只需要类型具有值语义,如果引用类型是您的全部,那么这将很难实现(或者很难看)。当您需要复制语义或需要自动初始化时(通常在这些类型的数组中),您应该只使用值类型。

我用BenchmarkDotNet做了一个小的基准测试,以更好地理解数字中的“结构”好处。我正在测试遍历结构(或类)数组(或列表)的循环。创建这些数组或列表超出了基准测试的范围——很明显,“类”更重会占用更多内存,并且会涉及GC。

因此,结论是:小心LINQ和隐藏结构装箱/拆箱,并使用结构进行微优化严格遵守数组。

P.S.关于通过调用堆栈传递结构/类的另一个基准是https://stackoverflow.com/a/47864451/506147

BenchmarkDotNet=v0.10.8, OS=Windows 10 Redstone 2 (10.0.15063)
Processor=Intel Core i5-2500K CPU 3.30GHz (Sandy Bridge), ProcessorCount=4
Frequency=3233542 Hz, Resolution=309.2584 ns, Timer=TSC
  [Host] : Clr 4.0.30319.42000, 64bit RyuJIT-v4.7.2101.1
  Clr    : Clr 4.0.30319.42000, 64bit RyuJIT-v4.7.2101.1
  Core   : .NET Core 4.6.25211.01, 64bit RyuJIT


          Method |  Job | Runtime |      Mean |     Error |    StdDev |       Min |       Max |    Median | Rank |  Gen 0 | Allocated |
---------------- |----- |-------- |----------:|----------:|----------:|----------:|----------:|----------:|-----:|-------:|----------:|
   TestListClass |  Clr |     Clr |  5.599 us | 0.0408 us | 0.0382 us |  5.561 us |  5.689 us |  5.583 us |    3 |      - |       0 B |
  TestArrayClass |  Clr |     Clr |  2.024 us | 0.0102 us | 0.0096 us |  2.011 us |  2.043 us |  2.022 us |    2 |      - |       0 B |
  TestListStruct |  Clr |     Clr |  8.427 us | 0.1983 us | 0.2204 us |  8.101 us |  9.007 us |  8.374 us |    5 |      - |       0 B |
 TestArrayStruct |  Clr |     Clr |  1.539 us | 0.0295 us | 0.0276 us |  1.502 us |  1.577 us |  1.537 us |    1 |      - |       0 B |
   TestLinqClass |  Clr |     Clr | 13.117 us | 0.1007 us | 0.0892 us | 13.007 us | 13.301 us | 13.089 us |    7 | 0.0153 |      80 B |
  TestLinqStruct |  Clr |     Clr | 28.676 us | 0.1837 us | 0.1534 us | 28.441 us | 28.957 us | 28.660 us |    9 |      - |      96 B |
   TestListClass | Core |    Core |  5.747 us | 0.1147 us | 0.1275 us |  5.567 us |  5.945 us |  5.756 us |    4 |      - |       0 B |
  TestArrayClass | Core |    Core |  2.023 us | 0.0299 us | 0.0279 us |  1.990 us |  2.069 us |  2.013 us |    2 |      - |       0 B |
  TestListStruct | Core |    Core |  8.753 us | 0.1659 us | 0.1910 us |  8.498 us |  9.110 us |  8.670 us |    6 |      - |       0 B |
 TestArrayStruct | Core |    Core |  1.552 us | 0.0307 us | 0.0377 us |  1.496 us |  1.618 us |  1.552 us |    1 |      - |       0 B |
   TestLinqClass | Core |    Core | 14.286 us | 0.2430 us | 0.2273 us | 13.956 us | 14.678 us | 14.313 us |    8 | 0.0153 |      72 B |
  TestLinqStruct | Core |    Core | 30.121 us | 0.5941 us | 0.5835 us | 28.928 us | 30.909 us | 30.153 us |   10 |      - |      88 B |

代码:

[RankColumn, MinColumn, MaxColumn, StdDevColumn, MedianColumn]
    [ClrJob, CoreJob]
    [HtmlExporter, MarkdownExporter]
    [MemoryDiagnoser]
    public class BenchmarkRef
    {
        public class C1
        {
            public string Text1;
            public string Text2;
            public string Text3;
        }

        public struct S1
        {
            public string Text1;
            public string Text2;
            public string Text3;
        }

        List<C1> testListClass = new List<C1>();
        List<S1> testListStruct = new List<S1>();
        C1[] testArrayClass;
        S1[] testArrayStruct;
        public BenchmarkRef()
        {
            for(int i=0;i<1000;i++)
            {
                testListClass.Add(new C1  { Text1= i.ToString(), Text2=null, Text3= i.ToString() });
                testListStruct.Add(new S1 { Text1 = i.ToString(), Text2 = null, Text3 = i.ToString() });
            }
            testArrayClass = testListClass.ToArray();
            testArrayStruct = testListStruct.ToArray();
        }

        [Benchmark]
        public int TestListClass()
        {
            var x = 0;
            foreach(var i in testListClass)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestArrayClass()
        {
            var x = 0;
            foreach (var i in testArrayClass)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestListStruct()
        {
            var x = 0;
            foreach (var i in testListStruct)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestArrayStruct()
        {
            var x = 0;
            foreach (var i in testArrayStruct)
            {
                x += i.Text1.Length + i.Text3.Length;
            }
            return x;
        }

        [Benchmark]
        public int TestLinqClass()
        {
            var x = testListClass.Select(i=> i.Text1.Length + i.Text3.Length).Sum();
            return x;
        }

        [Benchmark]
        public int TestLinqStruct()
        {
            var x = testListStruct.Select(i => i.Text1.Length + i.Text3.Length).Sum();
            return x;
        }
    }

除了常见的性能差异之外,让我再补充一个方面,那就是默认值的使用意图。

如果其字段的默认值不表示建模概念的合理默认值,请不要使用结构。

Eg.

即使所有字段都设置为默认值,“颜色”或“点”也有意义。RGB 0,0,0是一种非常好的颜色,(0,0)作为2D中的点也是如此。但是Address或PersonName没有合理的默认值。我的意思是,你能理解FirstName=null和LastName=null的PersonName吗?

如果你用一个类实现了一个概念,那么你可以强制执行某些不变量,例如一个人必须有名字和姓氏。但对于结构,总是可以创建一个实例,将其所有字段设置为默认值。

因此,当对没有合理默认值的概念进行建模时,更喜欢类。类的用户会明白null意味着没有指定PersonName,但如果您给他们一个PersonName结构实例,并将其所有财产设置为null,他们会感到困惑。

(通常的免责声明:性能考虑可能会凌驾于此建议之上。如果您有性能问题,请在决定解决方案之前进行衡量。试试BenchmarkDotNet,这很好!)