我应该使用结构体还是类?

9
我陷入了一个经典的设计困境。我正在编写一个C#数据结构,用于包含值和测量单位元组(例如7.0毫米),我想知道是否应该使用引用类型或值类型。
使用struct的好处是在表达式中减少堆操作,提高性能,并减轻垃圾收集器的压力。对于这种简单类型,这通常是我的选择,但在这种具体情况下存在缺点。
该元组是相当通用的分析结果框架的一部分,在WPF应用程序中,根据结果值的类型以不同的方式呈现结果。 WPF非常擅长处理这种弱类型,具有所有的数据模板,值转换和模板选择器。这意味着如果我的元组表示为struct,则该值将经历大量装箱/拆箱。实际上,元组在表达式中的使用将比在装箱场景中的使用要少得多。为了避免所有装箱,我考虑将我的类型声明为类。另一个关于struct的担忧是,在WPF中进行双向绑定可能会有陷阱,因为很容易在代码中复制元组的副本而不是引用副本。
我还有一些方便的运算符重载。我可以使用重载的比较运算符轻松比较毫米和厘米。但是,如果我的元组是一个类,我不喜欢重载==和!=的想法,因为惯例是对于引用类型,==和!=是ReferenceEquals(与System.String不同,这是另一个经典的讨论)。如果重载==和!=,有人会写if(myValue == null),并在某一天myValue变为空时得到一个令人不快的运行时异常。
还有另一个方面,即在C#中没有明确的方法(与例如C ++不同)来区分引用和值类型在代码使用中,但语义非常不同。我担心我的元组的用户(如果声明struct)会假设该类型是类,因为大多数自定义数据结构都是,并且假定引用语义。这是另一个论据,为什么应该优先使用类,因为这是用户所期望的,而且没有“.” /“ - >”来区分它们。通常情况下,除非我的分析器告诉我要使用struct,否则我几乎总是使用类,因为类语义是最有可能被其他程序员期望的,而C#只有模糊的提示,无论它是一件事还是另一件事。
所以我的问题是:
在决定是否使用值或引用时,我应该考虑哪些其他因素?
在任何情况下,是否可以证明在类中重载== /!=?
程序员会假设一些东西。大多数人可能会认为称为“Point”的东西是值类型。如果阅读带有“UnitValue”的代码,您会假设什么?
在我的使用描述中,您会选择什么?
6个回答

9
结构体的好处在于,它应该会减少堆操作,使表达式的性能更好,并减轻垃圾收集器的压力。不过,如果没有上下文支持,这是一种极其简单和危险的概括。结构体并不自动适用于堆栈。结构体只有在以下情况下才能被放置在堆栈上:
* 它的生命周期和暴露不会超出声明它的函数。 * 它不会在该函数内部被装箱。 * 可能还有许多其他标准,但无法立即想到。
这意味着,将其作为lambda表达式或委托的一部分时,它仍将存储在堆上。重点不是要担心这个问题,因为你的瓶颈99.9%的情况下可能在别处。
至于运算符重载,从技术或哲学上也没有任何阻止你在自己的类型上重载运算符。虽然从技术上讲,在引用类型之间进行相等比较默认情况下是语义等效于object.ReferenceEquals,但这不是万能法则。关于运算符重载,有两点需要记住:
1. (从实际角度来看,可能是最重要的)运算符不是多态的。也就是说,你只会使用在类型上定义的运算符,而不是它们实际存在的方式。
例如,如果我声明了一个类型Foo,它定义了一个重载的等于运算符,该运算符始终返回true,那么我做到这一点:
Foo foo1 = new Foo();
Foo foo2 = new Foo();
object obj1 = foo1;

bool compare1 = foo1 == foo2; // true
bool compare2 = foo1 == obj1; // false

尽管obj1实际上是Foo的一个实例,但在我引用存储在obj1引用中的实例的类型层次结构时,重载运算符不存在,因此它会回退到引用比较。
2.) 比较操作应该是确定性的。不可能使用重载运算符比较相同的两个实例并产生不同的结果。实际上,这种要求通常导致类型是不可变的(因为能够区分类中一个或多个值,但从等于运算符获取true是相当反直觉的),但基本上这意味着您不应该能够更改实例中的状态值,这将更改比较操作的结果。如果在您的情况下,突变某些实例状态信息而不影响比较结果是有意义的,那么没有理由不这样做。这只是一个罕见的情况。

1
然而,如果我的元组是一个类,我不喜欢过载==和!=的想法,因为传统规定对于引用类型来说,==和!=是ReferenceEquals。
不,传统规定略有不同:
(与System.String不同,这是另一个经典的讨论)。
嗯,这是相同的讨论。
关键不在于一个类型是否是引用类型。 - 而在于类型的行为是否像值。这对于String是正确的,并且对于您想要重载operator ==和!=的任何类都应该是正确的。
当设计一个逻辑上是值的类型时,只有一件事情需要注意:使其不可变(请参见Stack Overflow中的其他讨论),并正确实现比较语义:
如果重载了==和!=,那么当myValue某天变为空时,有人将编写if(myValue == null),并获得令人不快的运行时异常。

不应该有任何异常(毕竟,(string)null == null也不会产生异常!),这将是重载运算符实现中的一个错误。


0

数据结构用于包含值和测量单位元组(例如7.0毫米)。

听起来它具有值语义。框架提供了一种创建具有值语义的类型的机制,即struct。使用它。

你在问题的下一段中所说的几乎所有内容,无论是支持还是反对值类型,都是基于它将与运行时实现细节交互的方式进行优化的问题。由于在这方面存在利弊,因此没有明显的效率赢家。由于您无法在实际尝试之前找到明显的效率赢家,因此在这方面进行任何优化尝试都会过早。虽然我已经厌倦了那句关于过早优化的引言,但它确实适用于这里。

不过,有一件事与优化无关:

如果我的元组是一个类,我不喜欢重载==和!=,因为约定是对于引用类型,==和!=是ReferenceEquals。

完全不正确。默认情况下,== 和 != 处理引用相等性,但这仅仅是因为在没有更多了解类语义的情况下,这是唯一有意义的默认值。当符合类的语义时,应该重载 == 和 !=,当只关心引用相等性时,应该使用 ReferenceEquals。

如果重载了 == 和 !=,有人会写 if (myValue == null),当 myValue 有一天变成 null 时,会得到一个令人讨厌的运行时异常。

只有当 == 重载存在新手错误时才会出现这种情况。正常的做法是:

public static bool operator == (MyType x, MyType y)
{
  if(ReferenceEquals(x, null))
    return ReferenceEquls(y, null);
  if(ReferenceEquals(y, null))
    return false;
  return x.Equals(y);
}

当然,Equals重载也应该检查参数是否为空,如果是,则返回false,以供直接调用的人使用。即使一个或两个值为null时调用它,与默认的==行为相比,甚至没有显著的性能影响,那么有什么问题呢?

另一个方面是,在C#中没有明确的方法(不像在C++中)来区分引用类型和值类型的代码用法,但语义却非常不同。

并不完全如此。就平等性而言,默认语义相当不同,但由于您将某些内容描述为具有值语义,这倾向于将其作为值类型而不是类类型。除此之外,可用的语义大致相同。机制可能会因装箱、引用共享等而有所不同,但这又回到了优化的问题上。

在任何情况下都可以证明在类中重载== /!=吗?

我更愿意问,当这对于类是明智的事情时,不能重载==和!=吗?

就我作为程序员而言,对于“UnitValue”我可能会假设它是一个结构体,因为它听起来应该是这样的。但实际上,我甚至不会假设它是什么,因为在我需要处理它并且它很重要时,我才会关心它。考虑到它听起来应该是不可变的,所以这是一个减少了选择的集合(在实践中,可变引用类型和可变结构体之间的语义差异更大,但这个问题是一个不需要思考的不可变)。

0
也许你可以从 Eric Lippert 最近的博客文章(链接)中获得一些启示。在使用结构体时需要牢记的最重要的事情是使它们不可变。Jon Skeet 的这篇有趣的博客文章展示了可变结构体会导致非常难以调试的问题。

我刚在想这个……又一次。我们将使这成为有史以来最受欢迎的帖子 :) - Bryan
我实际上认为这有可能会成为被视为规则的准则之一,因为在某些情况下可变结构体是有意义的。同样,不可变引用类型也相对被低估了。尽管如此,对于大多数结构体而言这是正确的,在这里问题中不可变性比值类型与引用类型之间的选择更为明显。 - Jon Hanna

0

我不确定在 UI 代码中装箱/拆箱值的性能惩罚是否应该是您关注的主要问题。与布局过程相比,这种性能损失将是微不足道的。

实际上,您可以用另一种方式来阐述您的问题:您希望您的类型是可变的还是不可变的?根据您的规格,我认为不可变性是合乎逻辑的。它是一个值,您自己说了,通过将其命名为 UnitValue。作为开发人员,如果一个 UnitValue 不是一个值,我会感到相当惊讶 ;) => 使用不可变结构体

此外,null 对于测量没有任何意义。相等性和比较也应该按照测量规则进行实现。

不,我不认为在您的情况下使用引用类型而不是值类型有任何相关原因。


类型不仅在 UI 中使用,但需要在“对象”中存储以供 UI 显示。我担心将来会有人在性能重的表达式中使用这种类型。 - Holstebroe
@Holstebroe,但如果有人在处理性能密集型任务时,为什么要将其作为对象访问并处理所有的装箱和拆箱操作呢?如果某些操作被证明是性能密集型的,那么他们会考虑将其更改为直接处理结构体类型,这样问题就解决了,并且可能(取决于性能密集型操作是什么)比引用类型获得更好的性能。 - Jon Hanna

0
在我看来,你的设计需要值类型语义来处理元组。从程序员的角度来看,<7.0, mm> 应该始终等于 <7.0, mm>。 <7.0,mm> 正好是其部分之和,没有自己的标识。否则可能会让人非常困惑,这也有点意味着不可变性。
现在,如果你要实现这个功能,使用结构体还是类取决于性能以及是否必须支持每个元组的空值。如果你使用结构体,只需要在少数情况下支持 null 值,那么可以通过 Nullable 来解决。
此外,你不能提供一个用于显示目的的引用类型包装器吗?我对 WPF 不太熟悉,但我想这样做可以消除所有装箱操作。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接