1539

什么时候应该在 C# 中使用 struct 而不是 class?我的概念模型是当项目仅仅是值类型的集合时使用结构。一种在逻辑上将它们组合成一个有凝聚力的整体的方法。

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

  • 一个结构应该代表一个单一的值。
  • 结构的内存占用应小于 16 字节。
  • 创建后不应更改结构。

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

4

30 回答 30

648

OP 引用的来源具有一定的可信度……但是 Microsoft 呢?对 struct 使用的立场是什么?我向Microsoft寻求了一些额外的学习,这就是我发现的:

如果类型的实例很小且通常短暂存在或通常嵌入在其他对象中,请考虑定义结构而不是类。

除非类型具有以下所有特征,否则不要定义结构:

  1. 它在逻辑上表示单个值,类似于原始类型(整数、双精度等)。
  2. 它的实例大小小于 16 字节。
  3. 它是不可变的。
  4. 它不必经常装箱。

微软一贯违反这些规则

好吧,无论如何#2和#3。我们心爱的字典有 2 个内部结构:

[StructLayout(LayoutKind.Sequential)]  // default for structs
private struct Entry  //<Tkey, TValue>
{
    //  View code at *Reference Source
}

[Serializable, StructLayout(LayoutKind.Sequential)]
public struct Enumerator : 
    IEnumerator<KeyValuePair<TKey, TValue>>, IDisposable, 
    IDictionaryEnumerator, IEnumerator
{
    //  View code at *Reference Source
}

*参考来源

'JonnyCantCode.com' 来源获得了 4 分中的 3 分——这是可以原谅的,因为 #4 可能不会成为问题。如果您发现自己在装箱结构,请重新考虑您的架构。

让我们看看微软为什么会使用这些结构:

  1. 每个 structEntryEnumerator代表单个值。
  2. 速度
  3. Entry永远不会作为 Dictionary 类之外的参数传递。进一步的调查表明,为了满足 IEnumerable 的实现,Dictionary 使用Enumerator它在每次请求枚举器时复制的结构......这是有道理的。
  4. Dictionary 类的内部。Enumerator是公共的,因为 Dictionary 是可枚举的,并且必须对 IEnumerator 接口实现具有相同的可访问性 - 例如 IEnumerator getter。

更新- 此外,请意识到当结构实现接口时 - 就像 Enumerator 所做的那样 - 并被强制转换为该实现的类型,该结构成为引用类型并被移动到堆中。在 Dictionary 类的内部,Enumerator仍然是一个值类型。但是,只要方法调用GetEnumerator(),就会返回引用类型IEnumerator

我们在这里没有看到任何保持结构不可变或保持实例大小仅为 16 字节或更少的尝试或要求证明:

  1. 上面的结构中没有任何东西被声明readonly——不是不可变的
  2. 这些结构的大小可能远远超过 16 个字节
  3. Entry具有未确定的生命周期(从Add()、到Remove()Clear()或垃圾回收);

并且... 4. 两个结构都存储 TKey 和 TValue,我们都知道它们非常有能力成为引用类型(添加了奖励信息)

尽管有散列键,但字典速度很快,部分原因是实例化结构比引用类型更快。在这里,我有一个Dictionary<int, int>存储 300,000 个具有顺序递增键的随机整数。

容量:312874
MemSize:2660827 字节
完成调整大小:5ms
总填充时间:889ms

容量:必须调整内部数组大小之前可用的元素数。

MemSize:通过将字典序列化为 MemoryStream 并获取字节长度来确定(对于我们的目的来说足够准确)。

Completed Resize:将内部数组的大小从 150862 个元素调整为 312874 个元素所需的时间。当您发现每个元素都通过 顺序复制时Array.CopyTo(),这并不太破旧。

总填充时间OnResize:由于日志记录和我添加到源的事件,诚然存在偏差;但是,在操作期间调整 15 次大小时填充 300k 整数仍然令人印象深刻。只是出于好奇,如果我已经知道容量,那么填充的总时间是多少?13ms

那么,现在,如果Entry是一堂课呢?这些时间或指标真的会有那么大的不同吗?

容量:312874内存大小
:2660827 字节
完成调整大小: 26 毫秒
总填充时间:964 毫秒

显然,最大的区别在于调整大小。如果 Dictionary 是用容量初始化的,有什么区别吗?不够关心... 12ms

发生的情况是,因为Entry它是一个结构,它不需要像引用类型那样初始化。这既是价值类型的美,也是祸根。为了Entry用作引用类型,我必须插入以下代码:

/*
 *  Added to satisfy initialization of entry elements --
 *  this is where the extra time is spent resizing the Entry array
 * **/
for (int i = 0 ; i < prime ; i++)
{
    destinationArray[i] = new Entry( );
}
/*  *********************************************** */  

我必须将每个数组元素初始化Entry为引用类型的原因可以在MSDN: Structure Design中找到。简而言之:

不要为结构提供默认构造函数。

如果结构定义了默认构造函数,则在创建结构的数组时,公共语言运行库会自动对每个数组元素执行默认构造函数。

某些编译器(例如 C# 编译器)不允许结构具有默认构造函数。

这实际上很简单,我们将借鉴阿西莫夫的机器人三定律

  1. 该结构必须可以安全使用
  2. 结构必须有效地执行其功能,除非这会违反规则 #1
  3. 结构在使用过程中必须保持完整,除非它的销毁需要满足规则#1

...我们从中得到什么:简而言之,负责使用值类型。它们快速高效,但如果维护不当(即无意复制),可能会导致许多意外行为。

于 2011-08-07T13:44:41.570 回答
177

每当您:

  1. 不需要多态性,
  2. 想要值语义,并且
  3. 希望避免堆分配和相关的垃圾收集开销。

然而,需要注意的是,结构(任意大)比类引用(通常是一个机器字)更昂贵,因此在实践中类最终可能会更快。

于 2009-02-06T17:40:45.147 回答
162

我不同意原帖中给出的规则。这是我的规则:

  1. 当存储在数组中时,您可以使用结构来提高性能。(另见什么时候结构是答案?

  2. 您在将结构化数据传入/传出 C/C++ 的代码中需要它们

  3. 不要使用结构,除非你需要它们:

    • 它们在赋值和作为参数传递时的行为与“普通对象”(引用类型)不同,这可能导致意外行为;如果查看代码的人不知道他们正在处理一个结构,这尤其危险。
    • 它们不能被继承。
    • 将结构作为参数传递比类更昂贵。
于 2009-02-28T16:33:23.393 回答
93

当您需要值语义而不是引用语义时,请使用结构。

编辑

不知道为什么人们不赞成这一点,但这是一个有效的观点,并且是操作人员澄清他的问题之前提出的,这是结构的最基本原因。

如果您需要引用语义,则需要一个类而不是结构。

于 2009-02-06T17:40:26.567 回答
65

除了“它是一个值”的答案之外,使用结构的一个特定场景是当您知道您有一组导致垃圾收集问题的数据并且您有很多对象时。例如,大量的 Person 实例列表/数组。这里的自然比喻是一个类,但如果你有大量长寿命的 Person 实例,它们最终可能会阻塞 GEN-2 并导致 GC 停顿。如果情况允许,这里一种可能的方法是使用 Person结构的数组(而不是列表),即Person[]. 现在,不是在 GEN-2 中拥有数百万个对象,而是在 LOH 上有一个块(我假设这里没有字符串等 - 即没有任何引用的纯值)。这对 GC 影响很小。

处理这些数据很尴尬,因为数据对于结构来说可能过大,而且您不想一直复制胖值。但是,直接在数组中访问它不会复制结构 - 它是就地的(与复制的列表索引器形成对比)。这意味着大量的索引工作:

int index = ...
int id = peopleArray[index].Id;

请注意,保持值本身不可变将对此有所帮助。对于更复杂的逻辑,使用带有 by-ref 参数的方法:

void Foo(ref Person person) {...}
...
Foo(ref peopleArray[index]);

同样,这是就地的——我们没有复制该值。

在非常具体的场景中,这种策略可以非常成功;但是,这是一个相当高级的场景,只有在您知道自己在做什么以及为什么做的情况下才应该尝试。这里的默认值是一个类。

于 2011-10-22T12:14:22.590 回答
42

来自C# 语言规范

1.7 结构

与类一样,结构是可以包含数据成员和函数成员的数据结构,但与类不同的是,结构是值类型,不需要堆分配。结构类型的变量直接存储结构的数据,而类类型的变量存储对动态分配对象的引用。结构类型不支持用户指定的继承,所有结构类型都隐式继承自类型对象。

结构对于具有值语义的小型数据结构特别有用。复数、坐标系中的点或字典中的键值对都是结构的好例子。对小型数据结构使用结构而不是类可以对应用程序执行的内存分配数量产生很大影响。例如,以下程序创建并初始化一个包含 100 个点的数组。将 Point 实现为一个类,实例化了 101 个单独的对象——一个用于数组,一个用于 100 个元素。

class Point
{
   public int x, y;

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

class Test
{
   static void Main() {
      Point[] points = new Point[100];
      for (int i = 0; i < 100; i++) points[i] = new Point(i, i);
   }
}

另一种方法是使 Point 成为一个结构。

struct Point
{
   public int x, y;

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }
}

现在,只实例化了一个对象——用于数组的对象——并且 Point 实例以内联方式存储在数组中。

使用 new 运算符调用结构构造函数,但这并不意味着正在分配内存。结构构造函数不是动态分配对象并返回对它的引用,而是简单地返回结构值本身(通常在堆栈上的临时位置),然后根据需要复制该值。

对于类,两个变量可能引用同一个对象,因此对一个变量的操作可能会影响另一个变量引用的对象。对于结构,每个变量都有自己的数据副本,并且对一个变量的操作不可能影响另一个变量。例如,以下代码片段产生的输出取决于 Point 是类还是结构。

Point a = new Point(10, 10);
Point b = a;
a.x = 20;
Console.WriteLine(b.x);

如果 Point 是一个类,则输出为 20,因为 a 和 b 引用同一个对象。如果 Point 是一个结构体,则输出为 10,因为将 a 赋值给 b 会创建一个值的副本,并且该副本不受后续赋值给 ax 的影响

前面的示例突出了结构的两个限制。首先,复制整个结构通常比复制对象引用效率低,因此结构的赋值和值参数传递可能比引用类型更昂贵。其次,除了 ref 和 out 参数,不能创建对结构的引用,这排除了它们在许多情况下的使用。

于 2012-09-17T15:42:08.143 回答
35

结构适用于数据的原子表示,其中所述数据可以通过代码多次复制。克隆对象通常比复制结构更昂贵,因为它涉及分配内存、运行构造函数以及在完成后解除分配/垃圾收集。

于 2009-02-06T17:58:07.310 回答
30

这是一个基本规则。

  • 如果所有成员字段都是值类型,则创建一个struct

  • 如果任何一个成员字段是引用类型,则创建一个。这是因为无论如何引用类型字段都需要堆分配。

示例

public struct MyPoint 
{
    public int X; // Value Type
    public int Y; // Value Type
}

public class MyPointWithName 
{
    public int X; // Value Type
    public int Y; // Value Type
    public string Name; // Reference Type
}
于 2014-01-22T10:17:43.193 回答
19

第一:互操作场景或需要指定内存布局时

第二:当数据的大小与引用指针几乎相同时。

于 2009-02-06T18:12:58.153 回答
17

在要使用StructLayoutAttribute显式指定内存布局的情况下,您需要使用“结构” - 通常用于 PInvoke。

编辑:评论指出您可以将类或结构与 StructLayoutAttribute 一起使用,这当然是正确的。在实践中,您通常会使用结构 - 它分配在堆栈上而不是堆上,如果您只是将参数传递给非托管方法调用,这很有意义。

于 2009-02-06T18:09:02.577 回答
16

我使用结构来打包或解包任何类型的二进制通信格式。这包括读取或写入磁盘、DirectX 顶点列表、网络协议或处理加密/压缩数据。

在这种情况下,您列出的三个准则对我没有用。当我需要按特定顺序写出 400 字节的内容时,我将定义一个 400 字节的结构,并用它应该具有的任何不相关的值填充它,然后我会去以当时最有意义的方式进行设置。(好吧,四百字节会很奇怪——但是当我以编写 Excel 文件为生时,我正在处理多达大约四十字节的结构,因为这就是一些 BIFF 记录的大小。)

于 2009-02-06T18:25:49.813 回答
15

除了运行时直接使用的值类型和其他各种用于 PInvoke 目的的值类型外,您应该只在 2 个场景中使用值类型。

  1. 当您需要复制语义时。
  2. 当您需要自动初始化时,通常在这些类型的数组中。
于 2009-02-06T18:15:06.840 回答
14

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

所以结论是:小心 LINQ 和隐藏结构的装箱/拆箱,以及使用结构进行微优化严格使用数组。

PS关于通过调用堆栈传递结构/类的另一个基准是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;
        }
    }
于 2017-07-24T09:16:37.260 回答
13

.NET supports value types and reference types (in Java, you can define only reference types). Instances of reference types get allocated in the managed heap and are garbage collected when there are no outstanding references to them. Instances of value types, on the other hand, are allocated in the stack, and hence allocated memory is reclaimed as soon as their scope ends. And of course, value types get passed by value, and reference types by reference. All C# primitive data types, except for System.String, are value types.

When to use struct over class,

In C#, structs are value types, classes are reference types. You can create value types, in C#, using the enum keyword and the struct keyword. Using a value type instead of a reference type will result in fewer objects on the managed heap, which results in lesser load on the garbage collector (GC), less frequent GC cycles, and consequently better performance. However, value types have their downsides too. Passing around a big struct is definitely costlier than passing a reference, that's one obvious problem. The other problem is the overhead associated with boxing/unboxing. In case you're wondering what boxing/unboxing mean, follow these links for a good explanation on boxing and unboxing. Apart from performance, there are times when you simply need types to have value semantics, which would be very difficult (or ugly) to implement if reference types are all you have. You should use value types only, When you need copy semantics or need automatic initialization, normally in arrays of these types.

于 2012-09-17T11:21:43.923 回答
13

结构是一种值类型。如果将结构分配给新变量,则新变量将包含原始变量的副本。

public struct IntStruct {
    public int Value {get; set;}
}

执行以下结果会导致5 个结构实例存储在内存中:

var struct1 = new IntStruct() { Value = 0 }; // original
var struct2 = struct1;  // A copy is made
var struct3 = struct2;  // A copy is made
var struct4 = struct3;  // A copy is made
var struct5 = struct4;  // A copy is made

// NOTE: A "copy" will occur when you pass a struct into a method parameter.
// To avoid the "copy", use the ref keyword.

// Although structs are designed to use less system resources
// than classes.  If used incorrectly, they could use significantly more.

类是引用类型当您将一个类分配给一个新变量时,该变量包含对原始类对象的引用。

public class IntClass {
    public int Value {get; set;}
}

执行以下操作会导致内存中只有一个类对象实例。

var class1 = new IntClass() { Value = 0 };
var class2 = class1;  // A reference is made to class1
var class3 = class2;  // A reference is made to class1
var class4 = class3;  // A reference is made to class1
var class5 = class4;  // A reference is made to class1  

Struct s 可能会增加代码错误的可能性。如果将值对象视为可变引用对象,则当所做的更改意外丢失时,开发人员可能会感到惊讶。

var struct1 = new IntStruct() { Value = 0 };
var struct2 = struct1;
struct2.Value = 1;
// At this point, a developer may be surprised when 
// struct1.Value is 0 and not 1
于 2017-07-20T18:36:18.090 回答
11

C# 或其他 .net 语言中的结构类型通常用于保存应该表现得像固定大小的值组的东西。结构类型的一个有用方面是结构类型实例的字段可以通过修改它所在的存储位置来修改,而没有其他方式。可以以这样一种方式对结构进行编码,即改变任何字段的唯一方法是构造一个全新的实例,然后使用结构赋值来改变目标的所有字段,方法是用新实例中的值覆盖它们,但是除非结构没有提供创建其字段具有非默认值的实例的方法,否则如果结构本身存储在可变位置,则其所有字段都将是可变的。

请注意,如果结构包含私有类类型字段,并且将其自己的成员重定向到包装的类对象的成员,则可以设计一个结构类型,使其本质上表现得像一个类类型。例如, aPersonCollection可能提供属性SortedByNameand SortedById,它们都持有对 a 的“不可变”引用PersonCollection(在它们的构造函数中设置)并GetEnumerator通过调用creator.GetNameSortedEnumeratoror来实现creator.GetIdSortedEnumerator。此类结构的行为很像对 a 的引用PersonCollection,只是它们的GetEnumerator方法将绑定到PersonCollection. 也可以有一个结构包裹数组的一部分(例如,可以定义一个ArrayRange<T>结构,该结构将包含一个T[]被调用Arr的、一个 intOffset和一个 intLength,具有索引属性,对于idx0 到 范围内的索引Length-1,将访问Arr[idx+Offset])。不幸的是,如果foo是这种结构的只读实例,当前的编译器版本将不允许这样的操作,foo[3]+=4;因为它们无法确定此类操作是否会尝试写入foo.

也可以设计一个结构,使其表现得像一个值类型,它包含一个可变大小的集合(只要结构存在,它就会被复制),但完成这项工作的唯一方法是确保没有对象struct 持有一个引用将永远暴露给任何可能改变它的东西。例如,可以有一个类似数组的结构,该结构包含一个私有数组,其索引“put”方法创建一个新数组,其内容与原始数组相同,只是有一个更改的元素。不幸的是,要使这样的结构有效地执行可能有些困难。虽然有时结构语义很方便(例如,能够将类似数组的集合传递给例程,但调用者和被调用者都知道外部代码不会修改集合,

于 2012-07-24T22:07:44.887 回答
11

误区一:结构是轻量级的类

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

误区二:引用类型在堆上;价值类型存在于堆栈中

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

误区 #3:对象在 C# 中默认通过引用传递

这大概是流传最广的神话了。同样,经常(尽管并非总是)提出这种说法的人知道 C# 的实际行为方式,但他们不知道“通过引用传递”的真正含义。不幸的是,这让知道这意味着什么的人感到困惑。引用传递的正式定义比较复杂,涉及左值和类似的计算机科学术语,但重要的是,如果你通过引用传递一个变量,你调用的方法可以改变调用者变量的值通过更改其参数值。现在,请记住引用类型变量的值是引用,而不是对象本身。您可以更改参数引用的对象的内容,而无需通过引用传递参数本身。例如,

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

调用此方法时,参数值(对 StringBuilder 的引用)按值传递。如果您要在方法中更改 builder 变量的值——例如,使用语句 builder = null;——调用者不会看到该更改,这与神话相反。有趣的是,不仅神话中的“通过引用”位不准确,“对象被传递”位也是如此。对象本身永远不会通过引用或值传递。当涉及引用类型时,要么变量通过引用传递,要么参数(引用)的值通过值传递。除此之外,这回答了当 null 用作按值参数时会发生什么的问题——如果对象被传递,那会导致问题,因为没有对象可以传递!反而,空引用以与任何其他引用相同的方式按值传递。如果这个快速的解释让你感到困惑,你可能想看看我的文章“C#中的参数传递”(http://mng.bz/otVt),其中更详细。这些神话并不是唯一的。装箱和拆箱是因为它们存在相当大的误解,接下来我将尝试澄清。

参考: C# in Depth 3rd Edition by Jon Skeet

于 2019-02-27T16:39:27.580 回答
10

不——我不完全同意这些规则。它们是考虑性能和标准化的好指南,但不是考虑到可能性。

正如您在回复中看到的那样,有很多创造性的方式可以使用它们。因此,为了性能和效率,这些指导方针必须如此。

在这种情况下,我使用类来以更大的形式表示现实世界的对象,我使用结构来表示具有更精确用途的较小对象。就像你说的那样,“一个更有凝聚力的整体”。关键字具有凝聚力。这些类将是更多面向对象的元素,而结构可以具有其中一些特征,尽管规模较小。国际海事组织。

我在 Treeview 和 Listview 标签中经常使用它们,可以非常快速地访问常见的静态属性。我一直在努力以另一种方式获取这些信息。例如,在我的数据库应用程序中,我使用 Treeview,其中包含表、SP、函数或任何其他对象。我创建并填充我的结构,将其放入标签中,将其拉出,获取选择的数据等等。我不会在课堂上这样做!

我确实尝试让它们保持小,在单一实例情况下使用它们,并防止它们改变。谨慎注意内存、分配和性能。测试是如此必要。

于 2009-02-06T19:17:59.973 回答
8

我的规则是

1、始终使用类;

2,如果有任何性能问题,我尝试根据@IAbstract 提到的规则将一些类更改为struct,然后进行测试,看看这些更改是否可以提高性能。

于 2013-08-30T08:22:20.080 回答
8

类是引用类型。当创建类的对象时,分配给该对象的变量只保存对该内存的引用。当对象引用分配给新变量时,新变量引用原始对象。通过一个变量所做的更改会反映在另一个变量中,因为它们都引用相同的数据。结构是一种值类型。创建结构时,分配给结构的变量保存结构的实际数据。当结构被分配给一个新变量时,它被复制。因此,新变量和原始变量包含相同数据的两个单独副本。对一个副本所做的更改不会影响另一个副本。通常,类用于建模更复杂的行为,或在创建类对象后要修改的数据。

类和结构(C# 编程指南)

于 2014-05-23T09:52:52.953 回答
6

我只是在处理 Windows Communication Foundation [WCF] Named Pipe,我确实注意到使用 Structs 来确保数据交换是value type而不是reference type确实有意义。

于 2016-03-27T22:16:35.113 回答
4

简而言之,在以下情况下使用 struct:

  1. 您的对象属性/字段不需要更改。我的意思是你只想给它们一个初始值,然后读取它们。

  2. 对象中的属性和字段是值类型,它们不是那么大。

如果是这种情况,您可以利用结构来获得更好的性能和优化的内存分配,因为它们只使用堆栈而不是堆栈和堆(在类中)

于 2016-05-26T14:31:11.493 回答
4

C# 结构是类的轻量级替代方案。它可以做的几乎与类相同,但使用结构而不是类更“昂贵”。这样做的原因有点技术性,但总而言之,一个类的新实例放在堆上,新实例化的结构放在堆栈上。此外,您不是像处理类那样处理对结构的引用,而是直接使用结构实例。这也意味着当您将结构传递给函数时,它是按值传递的,而不是作为引用。在关于函数参数的章节中有更多关于这一点的内容。

因此,当您希望表示更简单的数据结构时,您应该使用结构,尤其是当您知道您将实例化大量它们时。.NET 框架中有很多示例,其中 Microsoft 使用结构而不是类,例如 Point、Rectangle 和 Color 结构。

于 2017-10-24T13:15:22.553 回答
3

我认为一个好的第一个近似值是“从不”。

我认为一个好的第二个近似值是“从不”。

如果您迫切需要性能,请考虑它们,但要始终衡量。

于 2009-02-06T17:44:34.093 回答
3

结构可用于提高垃圾收集性能。虽然您通常不必担心 GC 性能,但在某些情况下它可能会成为杀手。就像低延迟应用程序中的大型缓存一样。有关示例,请参见此帖子:

http://00sharp.wordpress.com/2013/07/03/a-case-for-the-struct/

于 2013-07-03T14:20:39.040 回答
3

以下是 Microsoft 网站上定义的规则:

✔️ 如果类型的实例很小且通常短暂存在或通常嵌入在其他对象中,请考虑定义结构而不是类。

❌ 避免定义结构,除非该类型具有以下所有特征:

它在逻辑上表示单个值,类似于原始类型(int、double 等)。

它的实例大小小于 16 个字节。

它是不可变的。

它不必经常装箱。

供进一步阅读

于 2020-10-15T14:24:47.670 回答
3

除了通常提到的性能差异之外,让我添加另一个方面,那就是揭示默认值使用的意图。

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

例如。

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

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

因此,在对没有合理默认值的概念进行建模时,更喜欢使用类。您的类的用户将理解 null 意味着未指定 PersonName,但如果您将其所有属性都设置为 null 的 PersonName 结构实例交给他们,他们会感到困惑。

(通常的免责声明:性能考虑可能会覆盖此建议。如果您有性能问题,请始终在决定解决方案之前进行测量。试试BenchmarkDotNet,这太棒了!)

于 2021-05-02T17:18:46.217 回答
2

我很少使用结构来做事情。但这只是我。这取决于我是否需要对象可以为空。

如其他答案所述,我将类用于真实世界的对象。我也有结构用于存储少量数据的心态。

于 2009-02-06T19:21:34.663 回答
0

✔️ 考虑结构用法

  1. 创建对象或不需要创建对象(可以直接赋值,创建对象)
  2. 需要速度或性能改进
  3. 不需要构造函数和析构函数(提供静态承包商)
  4. 不需要类继承,但接口是可以接受的
  5. 小工作量对象工作,如果它变高,就会引发内存问题
  6. 您不能为变量设置默认值。
  7. struct 还有可用的方法、事件、静态构造函数、变量等
  8. 减少 GC 的工作量
  9. 不需要引用类型,只需要值类型(每次创建新对象时)
  10. 没有不可变对象(字符串是不可变对象,因为任何操作都不会每次返回任何新字符串而不更改原始字符串)
于 2021-04-12T20:50:19.660 回答
-12

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

于 2014-11-20T20:02:07.380 回答