【发布时间】:2010-09-10 06:38:48
【问题描述】:
MSDN 说,当您需要轻量级对象时,您应该使用结构。结构比类更可取时,还有其他情况吗?
有些人可能忘记了:
- 结构可以有方法。
- 结构不能被继承。
我了解结构体和类之间的技术差异,只是我对何时使用结构体没有很好的感觉。
【问题讨论】:
-
提醒一下 - 大多数人在这种情况下往往会忘记的是,在 C# 中,结构也可以有方法。
MSDN 说,当您需要轻量级对象时,您应该使用结构。结构比类更可取时,还有其他情况吗?
有些人可能忘记了:
我了解结构体和类之间的技术差异,只是我对何时使用结构体没有很好的感觉。
【问题讨论】:
MSDN 有答案: Choosing Between Classes and Structures.
基本上,该页面为您提供了一个包含 4 项的清单,并说除非您的类型满足所有标准,否则使用一个类。
不要定义结构,除非 类型具有以下所有内容 特点:
- 它在逻辑上表示单个值,类似于原始类型 (整数、双精度等)。
- 它的实例大小小于 16 字节。
- 它是不可变的。
- 不必经常装箱。
【讨论】:
ref 参数传递。将具有 4,000 个字段的结构作为 ref 参数传递给更改一个的方法比将具有 4 个字段的结构按值传递给返回修改版本的方法要便宜。
我很惊讶我没有读过之前的任何答案,我认为这是最关键的方面:
当我想要一个没有标识的类型时,我会使用结构。例如一个 3D 点:
public struct ThreeDimensionalPoint
{
public readonly int X, Y, Z;
public ThreeDimensionalPoint(int x, int y, int z)
{
this.X = x;
this.Y = y;
this.Z = z;
}
public override string ToString()
{
return "(X=" + this.X + ", Y=" + this.Y + ", Z=" + this.Z + ")";
}
public override int GetHashCode()
{
return (this.X + 2) ^ (this.Y + 2) ^ (this.Z + 2);
}
public override bool Equals(object obj)
{
if (!(obj is ThreeDimensionalPoint))
return false;
ThreeDimensionalPoint other = (ThreeDimensionalPoint)obj;
return this == other;
}
public static bool operator ==(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
{
return p1.X == p2.X && p1.Y == p2.Y && p1.Z == p2.Z;
}
public static bool operator !=(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
{
return !(p1 == p2);
}
}
如果你有这个结构的两个实例,你不在乎它们是内存中的单个数据还是两个。你只关心他们持有的价值。
【讨论】:
Bill Wagner 在他的“有效 c#”(http://www.amazon.com/Effective-Specific-Ways-Improve-Your/dp/0321245660)一书中有一个关于此的章节。他总结了以下原则:
- 是类型数据存储的主要职责吗?
- 它的公共接口是否完全由访问或修改其数据成员的属性定义?
- 您确定您的类型永远不会有子类吗?
- 您确定您的类型永远不会被多态处理吗?
如果您对所有 4 个问题都回答“是”:使用结构。否则,使用 类。
【讨论】:
当您想要值类型语义而不是引用类型时,请使用结构。结构是按值复制的,所以要小心!
另见之前的问题,例如
【讨论】:
在以下情况下使用类:
在以下情况下使用结构:
【讨论】:
我会在以下情况下使用结构:
一个对象应该是只读的(每次你传递/分配一个结构时,它都会被复制)。只读对象在多线程处理方面非常有用,因为它们在大多数情况下不需要锁定。
一个物体很小而且寿命很短。在这种情况下,很有可能将对象分配到堆栈上,这比将其放在托管堆上要高效得多。更重要的是,对象分配的内存一旦超出其范围就会被释放。换句话说,垃圾收集器的工作量更少,内存使用效率更高。
【讨论】:
这是一个老话题,但想提供一个简单的基准测试。
我创建了两个 .cs 文件:
public class TestClass
{
public long ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
和
public struct TestStruct
{
public long ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
运行基准测试:
结果:
BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
Intel Core i5-8250U CPU 1.60GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.101
[Host] : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT [AttachedDebugger]
DefaultJob : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT
| Method | Mean | Error | StdDev | Ratio | RatioSD | Rank | Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------- |---------------:|--------------:|--------------:|----------:|--------:|-----:|---------:|------:|------:|----------:|
| UseStruct | 0.0000 ns | 0.0000 ns | 0.0000 ns | 0.000 | 0.00 | 1 | - | - | - | - |
| UseClass | 8.1425 ns | 0.1873 ns | 0.1839 ns | 1.000 | 0.00 | 2 | 0.0127 | - | - | 40 B |
| Use100Struct | 36.9359 ns | 0.4026 ns | 0.3569 ns | 4.548 | 0.12 | 3 | - | - | - | - |
| Use100Class | 759.3495 ns | 14.8029 ns | 17.0471 ns | 93.144 | 3.24 | 4 | 1.2751 | - | - | 4000 B |
| Use10000Struct | 3,002.1976 ns | 25.4853 ns | 22.5920 ns | 369.664 | 8.91 | 5 | - | - | - | - |
| Use10000Class | 76,529.2751 ns | 1,570.9425 ns | 2,667.5795 ns | 9,440.182 | 346.76 | 6 | 127.4414 | - | - | 400000 B |
【讨论】:
当我想将一些值组合在一起以从方法调用中传回内容时,我总是使用结构体,但在阅读了这些值之后,我不需要将它用于任何事情。就像保持清洁的一种方式。我倾向于将结构中的东西视为“一次性”,而将类中的东西视为更有用和“功能性”
【讨论】:
如果一个实体是不可变的,那么是使用结构还是类的问题通常是性能问题而不是语义问题。在 32/64 位系统上,类引用需要 4/8 个字节来存储,而不管类中的信息量如何;复制一个类引用需要复制 4/8 个字节。另一方面,每个 distinct 类实例除了它持有的信息和引用它的内存成本外,还会有 8/16 字节的开销。假设需要一个包含 500 个实体的数组,每个实体包含四个 32 位整数。如果实体是结构类型,则该数组将需要 8,000 个字节,无论所有 500 个实体是否全都相同、全不同或介于两者之间。如果实体是类类型,则包含 500 个引用的数组将占用 4,000 个字节。如果这些引用都指向不同的对象,则每个对象将需要额外的 24 个字节(所有 500 个对象需要 12,000 个字节),总共 16,000 个字节——是结构类型存储成本的两倍。另一方面,代码创建了一个对象实例,然后复制了对所有 500 个数组槽的引用,该实例的总成本为 24 字节,数组为 4,000 字节——总共 4,024 字节。一大笔节省。很少有情况会像最后一种情况一样有效,但在某些情况下,可以将一些引用复制到足够多的数组槽以使这种共享变得有价值。
如果实体应该是可变的,那么使用类还是结构的问题在某些方面更容易。假设“Thing”是一个结构或类,它有一个名为 x 的整数字段,并且执行以下代码:
事物 t1,t2; ... t2 = t1; t2.x = 5;是否希望后一个语句影响 t1.x?
如果 Thing 是类类型,则 t1 和 t2 将是等价的,这意味着 t1.x 和 t2.x 也将是等价的。因此,第二条语句将影响 t1.x。如果 Thing 是结构体类型,则 t1 和 t2 将是不同的实例,这意味着 t1.x 和 t2.x 将引用不同的整数。因此,第二条语句不会影响 t1.x。
可变结构和可变类具有根本不同的行为,尽管 .net 在处理结构突变时有一些怪癖。如果想要值类型的行为(意味着“t2=t1”会将数据从 t1 复制到 t2,同时将 t1 和 t2 作为不同的实例),并且如果可以忍受 .net 处理值类型的怪癖,请使用一个结构。如果一个人想要值类型语义,但 .net 的怪癖会导致应用程序中的值类型语义被破坏,请使用一个类并喃喃自语。
【讨论】:
除了上面的优秀答案:
结构是值类型。
它们永远不能设置为Nothing。
设置结构 = Nothing ,会将其所有值类型设置为其默认值。
【讨论】:
当您并不真正需要行为,但您需要比简单数组或字典更多的结构时。
跟进 这就是我对一般结构的看法。我知道他们可以有方法,但我喜欢保持这种整体精神上的区别。
【讨论】:
正如@Simon 所说,结构提供“值类型”语义,因此如果您需要与内置数据类型类似的行为,请使用结构。由于结构是通过副本传递的,因此您需要确保它们的大小很小,大约 16 个字节。
【讨论】:
嗯……
我不会使用垃圾收集作为支持/反对使用结构与类的论据。托管堆的工作方式很像堆栈——创建一个对象只是将它放在堆的顶部,这几乎与在堆栈上分配一样快。此外,如果一个对象是短暂的并且无法在 GC 循环中存活,则释放是免费的,因为 GC 仅适用于仍可访问的内存。 (搜索MSDN,.NET内存管理有一系列文章,懒得去挖了)。
在我使用结构体的大多数时间里,我都会为此自责,因为后来我发现拥有引用语义会让事情变得更简单。
无论如何,上面发布的 MSDN 文章中的这四点似乎是一个很好的指导方针。
【讨论】:
class MutableHolder<T> { public T Value; MutableHolder(T value) {Value = value;} },然后MutableHolder<T>将是一个具有可变类语义的对象(如果T是一个struct 或不可变的类类型)。
结构在堆栈而不是堆上,因此它们是线程安全的,应该在实现传输对象模式时使用,你永远不想在堆上使用对象它们是易失性的,在这种情况下你希望使用调用堆栈,这是使用结构的基本案例,我对这里的所有答案感到惊讶,
【讨论】:
✔️ 如果类型的实例很小且通常寿命短或通常嵌入其他对象中,请考虑定义结构而不是类。
【讨论】:
我认为最好的答案就是当你需要的是属性集合时使用 struct,当它是属性和行为的集合时使用 class。
【讨论】: